@geraldmaron/construct 1.2.0 → 1.2.2
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 +8 -7
- package/bin/construct +206 -62
- package/commands/plan/decide.md +1 -1
- package/commands/remember/runbook.md +1 -1
- package/examples/README.md +1 -1
- package/examples/distribution/README.md +1 -1
- package/examples/distribution/sources/adr.md +2 -2
- package/examples/distribution/sources/deck-one-pager.md +1 -1
- package/examples/distribution/sources/prd-platform.md +1 -1
- package/examples/distribution/sources/research-brief.md +2 -2
- package/examples/distribution/sources/rfc-platform.md +1 -1
- package/examples/distribution/sources/runbook.md +2 -2
- package/examples/distribution/sources/strategy.md +1 -1
- package/lib/agent-instructions/inject.mjs +1 -1
- package/lib/artifact-manifest.mjs +150 -4
- package/lib/artifact-type-from-path.mjs +9 -5
- package/lib/artifact-workflow.mjs +252 -0
- package/lib/audit-specialists.mjs +1 -1
- package/lib/audit-trail.mjs +3 -3
- package/lib/auto-docs.mjs +13 -11
- package/lib/bootstrap/built-ins.mjs +3 -3
- package/lib/boundary.mjs +3 -2
- package/lib/brand-prose.mjs +1 -1
- package/lib/cache-governor.js +4 -4
- package/lib/certification/document-io-fixtures.mjs +1 -1
- package/lib/chat/config.mjs +2 -2
- package/lib/chat/desktop-binary.mjs +5 -3
- package/lib/chat/present.mjs +1 -0
- package/lib/cli-commands.mjs +23 -6
- package/lib/cli-service-inventory.mjs +42 -0
- package/lib/comment-lint.mjs +9 -5
- package/lib/config/alias.mjs +3 -2
- package/lib/config/intake-policy.mjs +139 -0
- package/lib/config/project-config.mjs +106 -7
- package/lib/config/schema.mjs +45 -6
- package/lib/config/source-targets.mjs +311 -0
- package/lib/config/xdg.mjs +63 -0
- package/lib/contracts/violation-log.mjs +2 -2
- package/lib/cost-ledger.mjs +4 -3
- package/lib/cost.mjs +3 -2
- package/lib/decisions/registry.mjs +3 -3
- package/lib/distill.mjs +2 -2
- package/lib/docs-verify.mjs +2 -0
- package/lib/doctor/audit.mjs +4 -3
- package/lib/doctor/cli.mjs +0 -1
- package/lib/doctor/index.mjs +4 -4
- package/lib/doctor/report.mjs +3 -3
- package/lib/doctor/source-checkout.mjs +16 -0
- package/lib/doctor/watchers/bd-watch.mjs +5 -5
- package/lib/doctor/watchers/cost.mjs +4 -4
- package/lib/doctor/watchers/disk.mjs +4 -3
- package/lib/doctor/watchers/service-health.mjs +5 -5
- package/lib/document-export.mjs +39 -17
- package/lib/document-extract.mjs +3 -3
- package/lib/document-ingest.mjs +13 -3
- package/lib/efficiency.mjs +2 -1
- package/lib/embed/approval-queue.mjs +3 -2
- package/lib/embed/auto-sources.mjs +44 -0
- package/lib/embed/cli.mjs +9 -6
- package/lib/embed/config.mjs +6 -5
- package/lib/embed/daemon.mjs +6 -5
- package/lib/embed/demand-fetch.mjs +50 -19
- package/lib/embed/inbox-live-watcher.mjs +1 -1
- package/lib/embed/inbox.mjs +52 -41
- package/lib/embed/intake-metrics.mjs +3 -2
- package/lib/embed/reconcile.mjs +1 -1
- package/lib/embed/semantic.mjs +3 -2
- package/lib/embed/supervision.mjs +4 -2
- package/lib/embed/worker.mjs +2 -1
- package/lib/embedded-contract/index.mjs +11 -0
- package/lib/engine/registry.mjs +3 -3
- package/lib/env-config.mjs +34 -12
- package/lib/export-branding.mjs +32 -0
- package/lib/features.mjs +4 -3
- package/lib/health-check.mjs +7 -6
- package/lib/hook-health.mjs +5 -4
- package/lib/hooks/_lib/log.mjs +4 -3
- package/lib/hooks/_lib/output-mode.mjs +2 -1
- package/lib/hooks/agent-tracker.mjs +3 -4
- package/lib/hooks/audit-reads.mjs +2 -2
- package/lib/hooks/audit-trail.mjs +2 -2
- package/lib/hooks/bash-output-logger.mjs +3 -3
- package/lib/hooks/brand-prose-lint.mjs +1 -1
- package/lib/hooks/ci-status-check.mjs +3 -2
- package/lib/hooks/context-watch.mjs +2 -2
- package/lib/hooks/context-window-recovery.mjs +5 -5
- package/lib/hooks/dep-audit.mjs +2 -2
- package/lib/hooks/doc-coupling-check.mjs +3 -2
- package/lib/hooks/edit-accumulator.mjs +3 -2
- package/lib/hooks/edit-guard.mjs +3 -3
- package/lib/hooks/guard-bash.mjs +2 -2
- package/lib/hooks/model-fallback.mjs +2 -1
- package/lib/hooks/policy-engine.mjs +4 -3
- package/lib/hooks/pre-compact.mjs +7 -7
- package/lib/hooks/readme-age-check.mjs +2 -2
- package/lib/hooks/session-optimize.mjs +4 -3
- package/lib/hooks/session-start.mjs +20 -16
- package/lib/hooks/stop-notify.mjs +9 -8
- package/lib/hooks/stop-typecheck.mjs +4 -3
- package/lib/hooks/test-watch.mjs +2 -2
- package/lib/host-disposition.mjs +1 -1
- package/lib/ingest/provider-extract.mjs +2 -1
- package/lib/init/detect-existing-structure.mjs +2 -2
- package/lib/init/doc-lanes.mjs +3 -10
- package/lib/init-docs.mjs +12 -54
- package/lib/init-unified.mjs +47 -92
- package/lib/init.mjs +7 -5
- package/lib/install/desktop-binary-download.mjs +5 -2
- package/lib/intake/constants.mjs +29 -0
- package/lib/intake/daemon.mjs +31 -8
- package/lib/intake/intake-config.mjs +32 -112
- package/lib/integrations/intake-integrations.mjs +8 -7
- package/lib/knowledge/rag.mjs +2 -2
- package/lib/knowledge/search.mjs +15 -15
- package/lib/maintenance/cleanup.mjs +14 -10
- package/lib/mcp/server.mjs +20 -2
- package/lib/mcp/tools/embedded-contract.mjs +14 -0
- package/lib/mcp/tools/project.mjs +2 -1
- package/lib/model-cheapest-provider.mjs +4 -3
- package/lib/model-pricing.mjs +3 -2
- package/lib/model-router.mjs +4 -3
- package/lib/models/catalog.mjs +2 -1
- package/lib/models/execution-capability-profile.mjs +2 -1
- package/lib/models/provider-poll.mjs +2 -1
- package/lib/ollama-manager.mjs +0 -1
- package/lib/op-log.mjs +2 -1
- package/lib/opencode-runtime-plugin.mjs +3 -2
- package/lib/oracle/artifact-gate.mjs +3 -0
- package/lib/oracle/cli.mjs +2 -1
- package/lib/oracle/execute.mjs +3 -2
- package/lib/oracle/index.mjs +2 -1
- package/lib/oracle/read-model.mjs +3 -2
- package/lib/performance/generate.mjs +4 -3
- package/lib/platforms/capabilities.mjs +1 -1
- package/lib/playwright-demo.mjs +2 -0
- package/lib/plugin-registry.mjs +2 -1
- package/lib/profiles/lifecycle.mjs +3 -3
- package/lib/project-profile.mjs +2 -1
- package/lib/project-root.mjs +9 -7
- package/lib/provider-capabilities.js +4 -3
- package/lib/providers/auth-manager.mjs +2 -1
- package/lib/providers/copilot-auth.mjs +3 -2
- package/lib/providers/creds.mjs +3 -2
- package/lib/providers/registry.mjs +3 -3
- package/lib/providers/secret-resolver.mjs +3 -2
- package/lib/read-tracker-store.mjs +2 -1
- package/lib/reconcile/mcp-entry-reconcile.mjs +2 -2
- package/lib/reflect.mjs +2 -1
- package/lib/registry/generate-docs.mjs +2 -2
- package/lib/roles/approval-surface.mjs +5 -4
- package/lib/roles/event-bus.mjs +0 -1
- package/lib/roles/gateway.mjs +6 -2
- package/lib/rules-delivery.mjs +1 -1
- package/lib/runtime-pressure.mjs +5 -3
- package/lib/sandbox.mjs +3 -2
- package/lib/scheduler/solo.mjs +6 -4
- package/lib/server/auth.mjs +4 -3
- package/lib/server/index.mjs +53 -26
- package/lib/server/insights.mjs +5 -4
- package/lib/server/rate-limit.mjs +5 -3
- package/lib/server/webhook.mjs +5 -3
- package/lib/service-manager.mjs +5 -3
- package/lib/setup.mjs +9 -8
- package/lib/status.mjs +6 -5
- package/lib/storage/embeddings-local.mjs +3 -2
- package/lib/storage/sync.mjs +2 -2
- package/lib/telemetry/client.mjs +0 -1
- package/lib/telemetry/intent-verifications.mjs +6 -6
- package/lib/telemetry/model-pricing-catalog.mjs +4 -3
- package/lib/telemetry/rule-calls.mjs +3 -3
- package/lib/telemetry/skill-calls.mjs +3 -3
- package/lib/template-registry.mjs +1 -0
- package/lib/templates/visual-requirements.mjs +1 -1
- package/lib/test-corpus-inventory.mjs +1 -1
- package/lib/uninstall/uninstall.mjs +12 -10
- package/package.json +3 -3
- package/platforms/claude/settings.template.json +43 -43
- package/rules/common/no-fabrication.md +1 -1
- package/rules/common/release-gates.md +1 -1
- package/rules/common/research.md +1 -1
- package/rules/common/review-before-change.md +1 -1
- package/scripts/sync-specialists.mjs +11 -8
- package/skills/docs/adr-workflow.md +3 -3
- package/skills/docs/init-docs.md +9 -9
- package/skills/docs/prd-workflow.md +5 -5
- package/skills/docs/product-intelligence-review.md +1 -1
- package/skills/docs/product-intelligence-workflow.md +2 -2
- package/skills/docs/product-signal-workflow.md +1 -1
- package/skills/docs/runbook-workflow.md +4 -4
- package/skills/exploration/unknown-codebase-onboarding.md +1 -1
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/routing.md +3 -3
- package/specialists/artifact-manifest.json +11 -2
- package/specialists/artifact-manifest.schema.json +31 -1
- package/specialists/prompts/cx-architect.md +1 -1
- package/specialists/prompts/cx-docs-keeper.md +1 -1
- package/specialists/prompts/cx-researcher.md +1 -1
- package/specialists/prompts/cx-sre.md +1 -1
- package/specialists/role-manifests.json +6 -6
- package/templates/distribution/construct-reference.docx +0 -0
- package/templates/docs/README.md +125 -0
- package/templates/docs/construct_guide.md +3 -4
- package/templates/docs/persona-artifact.md +1 -1
- package/templates/docs/prds/README.md +25 -0
- package/templates/docs/prds/templates/_template.md +206 -0
- package/templates/docs/prds/templates/meta-prd.template.md +177 -0
- package/templates/docs/prds/templates/prd-business.template.md +61 -0
- package/templates/docs/prds/templates/prd-platform.template.md +81 -0
- package/templates/docs/prds/templates/prfaq.template.md +38 -0
- package/templates/docs/rfcs/README.md +22 -0
- package/templates/docs/rfcs/templates/_template.md +58 -0
- package/templates/docs/rfcs/templates/rfc-platform.template.md +72 -0
- package/templates/homebrew/construct.rb +1 -1
- package/templates/workflows/new-feature.yml +8 -8
package/lib/embed/cli.mjs
CHANGED
|
@@ -21,6 +21,8 @@ import fs from 'node:fs';
|
|
|
21
21
|
import os from 'node:os';
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import { spawn } from 'node:child_process';
|
|
24
|
+
import { configDir } from '../config/xdg.mjs';
|
|
25
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
24
26
|
|
|
25
27
|
const STATE_FILE = 'embed-daemon.json';
|
|
26
28
|
const LOG_FILE = 'embed-daemon.log';
|
|
@@ -30,7 +32,7 @@ const LOG_FILE = 'embed-daemon.log';
|
|
|
30
32
|
// ---------------------------------------------------------------------------
|
|
31
33
|
|
|
32
34
|
function runtimeDir(homeDir = os.homedir()) {
|
|
33
|
-
return path.join(homeDir, '
|
|
35
|
+
return path.join(doctorRoot(homeDir), 'runtime');
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
function statePath(homeDir = os.homedir()) {
|
|
@@ -273,8 +275,8 @@ async function cmdEmbedStart(args, { homeDir = os.homedir(), rootDir, _workerPat
|
|
|
273
275
|
const { config } = parseArgs(args);
|
|
274
276
|
const configPath = config
|
|
275
277
|
? path.resolve(config)
|
|
276
|
-
: fs.existsSync(path.join(
|
|
277
|
-
? path.join(
|
|
278
|
+
: fs.existsSync(path.join(configDir(), 'embed.yaml'))
|
|
279
|
+
? path.join(configDir(), 'embed.yaml')
|
|
278
280
|
: null;
|
|
279
281
|
|
|
280
282
|
const workerPath = _workerPath ?? path.join(rootDir, 'lib', 'embed', 'worker.mjs');
|
|
@@ -374,7 +376,7 @@ async function cmdEmbedStatus(_args, { homeDir = os.homedir() } = {}) {
|
|
|
374
376
|
process.stdout.write(` pid: ${state.pid}\n`);
|
|
375
377
|
process.stdout.write(` config: ${state.configPath}\n`);
|
|
376
378
|
process.stdout.write(` rootDir: ${rootDir}\n`);
|
|
377
|
-
process.stdout.write(` inbox: ${path.join(rootDir, '
|
|
379
|
+
process.stdout.write(` inbox: ${path.join(rootDir, 'inbox')}\n`);
|
|
378
380
|
process.stdout.write(` started at: ${state.startedAt}\n`);
|
|
379
381
|
process.stdout.write(` log: ${logPath(homeDir)}\n`);
|
|
380
382
|
}
|
|
@@ -387,7 +389,7 @@ async function cmdEmbedSnapshot(args, { homeDir = os.homedir() } = {}) {
|
|
|
387
389
|
const { config } = parseArgs(args);
|
|
388
390
|
const configPath = config
|
|
389
391
|
? path.resolve(config)
|
|
390
|
-
: path.join(
|
|
392
|
+
: path.join(configDir(), 'embed.yaml');
|
|
391
393
|
|
|
392
394
|
const { loadEmbedConfig, EMPTY_CONFIG } = await import('./config.mjs');
|
|
393
395
|
const { SnapshotEngine, renderMarkdown } = await import('./snapshot.mjs');
|
|
@@ -401,7 +403,8 @@ async function cmdEmbedSnapshot(args, { homeDir = os.homedir() } = {}) {
|
|
|
401
403
|
if (fs.existsSync(configPath)) {
|
|
402
404
|
cfg = loadEmbedConfig(configPath);
|
|
403
405
|
} else {
|
|
404
|
-
|
|
406
|
+
const { resolveAutoEmbedSources } = await import('./auto-sources.mjs');
|
|
407
|
+
cfg = { ...EMPTY_CONFIG, sources: resolveAutoEmbedSources({ cwd: process.cwd(), env, registry }) };
|
|
405
408
|
}
|
|
406
409
|
|
|
407
410
|
if (!cfg.sources.length) {
|
package/lib/embed/config.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/embed/config.mjs — embed mode configuration schema and loader.
|
|
3
3
|
*
|
|
4
|
-
* Reads embed.yaml from
|
|
4
|
+
* Reads embed.yaml from the XDG config dir (or a supplied path) and validates
|
|
5
5
|
* the file against the expected schema. Returns a parsed, normalized config object.
|
|
6
6
|
*
|
|
7
7
|
* Schema:
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
import fs from 'node:fs';
|
|
56
56
|
import os from 'node:os';
|
|
57
57
|
import path from 'node:path';
|
|
58
|
+
import { configDir, stateDir } from '../config/xdg.mjs';
|
|
58
59
|
|
|
59
60
|
const DEFAULTS = {
|
|
60
61
|
snapshot: { intervalMs: 300_000, maxItems: 100 },
|
|
@@ -74,13 +75,13 @@ const DEFAULTS = {
|
|
|
74
75
|
* ref: github.com/org/project-y
|
|
75
76
|
* path: /Users/user/Github/project-y # both — prefer local, fall back to remote
|
|
76
77
|
* - type: workspace # always present as fallback
|
|
77
|
-
* path:
|
|
78
|
+
* path: <XDG_STATE_HOME>/construct/workspace
|
|
78
79
|
*
|
|
79
80
|
* roles:
|
|
80
81
|
* primary: product-manager # shapes embed analysis lens
|
|
81
82
|
* secondary: architect # persistent background concern
|
|
82
83
|
*/
|
|
83
|
-
export const DEFAULT_WORKSPACE_PATH = path.join(
|
|
84
|
+
export const DEFAULT_WORKSPACE_PATH = path.join(stateDir(), 'workspace');
|
|
84
85
|
|
|
85
86
|
export const WORKSPACE_DOCS_LANES = ['adrs', 'prds', 'memos', 'notes', 'intake'];
|
|
86
87
|
|
|
@@ -99,7 +100,7 @@ export const DEFAULT_OPERATING_PROFILE = {
|
|
|
99
100
|
},
|
|
100
101
|
focalResources: [
|
|
101
102
|
{ type: 'plan', path: 'plan.md', purpose: 'active implementation plan' },
|
|
102
|
-
{ type: 'architecture', path: 'docs/concepts/architecture.md', purpose: 'system architecture and operating model' },
|
|
103
|
+
{ type: 'architecture', path: 'docs/guides/concepts/architecture.md', purpose: 'system architecture and operating model' },
|
|
103
104
|
{ type: 'knowledge', path: '.cx/knowledge', purpose: 'typed project knowledge and ingested evidence' },
|
|
104
105
|
{ type: 'runtime', path: '.cx/roadmap.md', purpose: 'generated roadmap output' },
|
|
105
106
|
],
|
|
@@ -351,5 +352,5 @@ export function parseEmbedYaml(text) {
|
|
|
351
352
|
}
|
|
352
353
|
|
|
353
354
|
export function defaultEmbedConfigPath() {
|
|
354
|
-
return path.join(
|
|
355
|
+
return path.join(configDir(), 'embed.yaml');
|
|
355
356
|
}
|
package/lib/embed/daemon.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - approval-expiry — expires stale approval queue items
|
|
14
14
|
* - eval-dataset-sync — syncs scored traces to telemetry dataset items
|
|
15
15
|
* - prompt-regression-check — detects low-quality score clusters per promptHash
|
|
16
|
-
* - inbox-watcher — ingests new files from
|
|
16
|
+
* - inbox-watcher — ingests new files from inbox/ + CX_INBOX_DIRS into
|
|
17
17
|
* observations (agnostic: specs, ADRs, meeting notes, etc.)
|
|
18
18
|
* - roadmap — cross-references open items + observations → .cx/roadmap.md
|
|
19
19
|
*
|
|
@@ -48,6 +48,7 @@ import { emitEmbedNotification, notifySlack } from './notifications.mjs';
|
|
|
48
48
|
import { intentToCategory } from './providers/slack.mjs';
|
|
49
49
|
import { runTelemetrySetup } from '../telemetry/setup.mjs';
|
|
50
50
|
import { emitTraceEvent, newTraceId, newSpanId } from '../worker/trace.mjs';
|
|
51
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
54
|
* Log a non-fatal error to stderr. Use instead of empty catch {} blocks
|
|
@@ -121,8 +122,8 @@ function resolveDaemonStatePath(env = process.env) {
|
|
|
121
122
|
|
|
122
123
|
// ─── Self-repair helpers ────────────────────────────────────────────────────
|
|
123
124
|
|
|
124
|
-
const LOCK_PATH = join(
|
|
125
|
-
const DAEMON_STATE_PATH = join(
|
|
125
|
+
const LOCK_PATH = join(doctorRoot(), 'sync.lock');
|
|
126
|
+
const DAEMON_STATE_PATH = join(doctorRoot(), 'runtime', 'embed-daemon.json');
|
|
126
127
|
|
|
127
128
|
/**
|
|
128
129
|
* Run in-process health checks and fix what can be fixed.
|
|
@@ -562,7 +563,7 @@ export class EmbedDaemon {
|
|
|
562
563
|
'daemon-log-rotation',
|
|
563
564
|
60_000,
|
|
564
565
|
async () => {
|
|
565
|
-
const logPath = join(
|
|
566
|
+
const logPath = join(doctorRoot(), 'runtime', 'embed-daemon.log');
|
|
566
567
|
const capMb = Number(process.env.CONSTRUCT_EMBED_LOG_MAX_MB) || 50;
|
|
567
568
|
const maxBytes = Math.floor(capMb * 1024 * 1024);
|
|
568
569
|
const maxSegments = Number(process.env.CONSTRUCT_EMBED_LOG_MAX_SEGMENTS) || 5;
|
|
@@ -729,7 +730,7 @@ this.#scheduler.register(
|
|
|
729
730
|
// Every 2 min — scan configured inbox dirs for new files, ingest them, and
|
|
730
731
|
// record observations. Agnostic to content: specs, ADRs, meeting notes,
|
|
731
732
|
// internal docs, PDFs, Office files — anything on the local filesystem.
|
|
732
|
-
// Dirs: <rootDir
|
|
733
|
+
// Dirs: <rootDir>/inbox/ always; CX_INBOX_DIRS env for extra paths.
|
|
733
734
|
this.#scheduler.register(
|
|
734
735
|
'inbox-watcher',
|
|
735
736
|
2 * 60_000,
|
|
@@ -19,6 +19,11 @@ import { homedir } from 'node:os';
|
|
|
19
19
|
import { join } from 'node:path';
|
|
20
20
|
import { loadConstructEnv } from '../env-config.mjs';
|
|
21
21
|
import { addObservation } from '../observation-store.mjs';
|
|
22
|
+
import { loadProjectConfig } from '../config/project-config.mjs';
|
|
23
|
+
import {
|
|
24
|
+
resolveEffectiveSourceTargetsFromConfig,
|
|
25
|
+
resolveKnownSourcesFromTargets,
|
|
26
|
+
} from '../config/source-targets.mjs';
|
|
22
27
|
|
|
23
28
|
// ─── Self-query detection ────────────────────────────────────────────────────
|
|
24
29
|
|
|
@@ -62,7 +67,13 @@ function isSelfQuery(query) {
|
|
|
62
67
|
* @param {object} env
|
|
63
68
|
* @returns {{ id: string, provider: string, ref: string, display: string }[]}
|
|
64
69
|
*/
|
|
65
|
-
export function resolveKnownSources(env = process.env) {
|
|
70
|
+
export function resolveKnownSources(env = process.env, cwd = process.cwd()) {
|
|
71
|
+
const { config } = loadProjectConfig(cwd, env);
|
|
72
|
+
const targets = resolveEffectiveSourceTargetsFromConfig(config, env);
|
|
73
|
+
if (targets.length > 0) {
|
|
74
|
+
return resolveKnownSourcesFromTargets(targets);
|
|
75
|
+
}
|
|
76
|
+
|
|
66
77
|
const sources = [];
|
|
67
78
|
|
|
68
79
|
// GitHub repos
|
|
@@ -112,8 +123,8 @@ export function resolveKnownSources(env = process.env) {
|
|
|
112
123
|
* @param {object} env
|
|
113
124
|
* @returns {{ provider: string, ref: string, display: string } | null}
|
|
114
125
|
*/
|
|
115
|
-
export function matchSourceFromQuery(query, env = process.env) {
|
|
116
|
-
const sources = resolveKnownSources(env);
|
|
126
|
+
export function matchSourceFromQuery(query, env = process.env, cwd = process.cwd()) {
|
|
127
|
+
const sources = resolveKnownSources(env, cwd);
|
|
117
128
|
const q = query.toLowerCase();
|
|
118
129
|
|
|
119
130
|
// Exact or substring match — longest id wins
|
|
@@ -138,13 +149,12 @@ export function matchSourceFromQuery(query, env = process.env) {
|
|
|
138
149
|
* @param {object} [opts.env] - Env override
|
|
139
150
|
* @returns {Promise<DemandFetchResult>}
|
|
140
151
|
*/
|
|
141
|
-
export async function demandFetch({ query, rootDir, env } = {}) {
|
|
152
|
+
export async function demandFetch({ query, rootDir, env, cwd } = {}) {
|
|
142
153
|
const resolvedEnv = env ?? loadConstructEnv();
|
|
143
154
|
const root = rootDir ?? (resolvedEnv.CX_DATA_DIR?.trim() || homedir());
|
|
155
|
+
const projectRoot = cwd ?? root;
|
|
144
156
|
|
|
145
|
-
|
|
146
|
-
// always fetch from that provider — even if isSelfQuery() also fires.
|
|
147
|
-
const match = matchSourceFromQuery(query, resolvedEnv);
|
|
157
|
+
const match = matchSourceFromQuery(query, resolvedEnv, projectRoot);
|
|
148
158
|
|
|
149
159
|
// Self-referential queries about Construct itself → route to knowledge_search,
|
|
150
160
|
// but only when no external provider source was matched.
|
|
@@ -168,8 +178,7 @@ export async function demandFetch({ query, rootDir, env } = {}) {
|
|
|
168
178
|
}
|
|
169
179
|
|
|
170
180
|
if (!match) {
|
|
171
|
-
|
|
172
|
-
return demandFetchAll({ query, rootDir: root, env: resolvedEnv });
|
|
181
|
+
return demandFetchAll({ query, rootDir: root, env: resolvedEnv, cwd: projectRoot });
|
|
173
182
|
}
|
|
174
183
|
|
|
175
184
|
// Dynamically import provider registry to avoid loading all providers at module init
|
|
@@ -188,7 +197,7 @@ export async function demandFetch({ query, rootDir, env } = {}) {
|
|
|
188
197
|
}
|
|
189
198
|
|
|
190
199
|
// Build read calls for the matched source and execute them
|
|
191
|
-
const readCalls = buildReadCalls(match, resolvedEnv);
|
|
200
|
+
const readCalls = buildReadCalls(match, resolvedEnv, projectRoot);
|
|
192
201
|
if (!readCalls.length) {
|
|
193
202
|
return {
|
|
194
203
|
ok: false,
|
|
@@ -281,23 +290,32 @@ export async function demandFetch({ query, rootDir, env } = {}) {
|
|
|
281
290
|
* @param {object} [opts.env] - Resolved env
|
|
282
291
|
* @returns {Promise<DemandFetchResult>}
|
|
283
292
|
*/
|
|
284
|
-
async function demandFetchAll({ query, rootDir, env } = {}) {
|
|
293
|
+
async function demandFetchAll({ query, rootDir, env, cwd } = {}) {
|
|
285
294
|
const { ProviderRegistry } = await import('./providers/registry.mjs');
|
|
286
295
|
const registry = await ProviderRegistry.fromEnv(env);
|
|
287
|
-
const
|
|
296
|
+
const projectRoot = cwd ?? rootDir ?? process.cwd();
|
|
297
|
+
const { config } = loadProjectConfig(projectRoot, env);
|
|
298
|
+
const targets = resolveEffectiveSourceTargetsFromConfig(config, env);
|
|
299
|
+
const scoped = targets.length > 0;
|
|
300
|
+
|
|
301
|
+
const providerNames = scoped
|
|
302
|
+
? [...new Set(targets.map((t) => t.provider))].filter((name) => registry.get(name))
|
|
303
|
+
: registry.names();
|
|
288
304
|
|
|
289
305
|
if (!providerNames.length) {
|
|
290
306
|
return {
|
|
291
307
|
ok: false,
|
|
292
308
|
reason: 'no_providers',
|
|
293
|
-
message:
|
|
309
|
+
message: scoped
|
|
310
|
+
? 'No provider credentials configured for the declared source targets'
|
|
311
|
+
: 'No provider credentials configured in config.env',
|
|
294
312
|
items: [],
|
|
295
313
|
};
|
|
296
314
|
}
|
|
297
315
|
|
|
298
316
|
const allItems = [];
|
|
299
317
|
const errors = [];
|
|
300
|
-
const sources = resolveKnownSources(env);
|
|
318
|
+
const sources = resolveKnownSources(env, projectRoot);
|
|
301
319
|
|
|
302
320
|
// Group known sources by provider so we can batch-fetch per-provider
|
|
303
321
|
const byProvider = new Map();
|
|
@@ -306,11 +324,16 @@ async function demandFetchAll({ query, rootDir, env } = {}) {
|
|
|
306
324
|
byProvider.get(src.provider).push(src);
|
|
307
325
|
}
|
|
308
326
|
|
|
309
|
-
// Also include providers that have no explicit sources (Jira, Linear default fetch)
|
|
310
327
|
for (const name of providerNames) {
|
|
311
328
|
if (!byProvider.has(name)) byProvider.set(name, []);
|
|
312
329
|
}
|
|
313
330
|
|
|
331
|
+
if (scoped) {
|
|
332
|
+
for (const key of [...byProvider.keys()]) {
|
|
333
|
+
if (!providerNames.includes(key)) byProvider.delete(key);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
314
337
|
for (const [providerName, provSources] of byProvider) {
|
|
315
338
|
const provider = registry.get(providerName);
|
|
316
339
|
if (!provider) continue;
|
|
@@ -318,10 +341,12 @@ async function demandFetchAll({ query, rootDir, env } = {}) {
|
|
|
318
341
|
// Build read calls — one set per known source, or a default if none listed
|
|
319
342
|
const matchList = provSources.length > 0
|
|
320
343
|
? provSources
|
|
321
|
-
: [{ provider: providerName, ref: null, display: providerName }];
|
|
344
|
+
: (scoped ? [] : [{ provider: providerName, ref: null, display: providerName }]);
|
|
345
|
+
|
|
346
|
+
if (!matchList.length) continue;
|
|
322
347
|
|
|
323
348
|
for (const src of matchList) {
|
|
324
|
-
const readCalls = buildReadCalls(src, env);
|
|
349
|
+
const readCalls = buildReadCalls(src, env, projectRoot);
|
|
325
350
|
if (!readCalls.length) continue;
|
|
326
351
|
for (const { ref, opts, display } of readCalls) {
|
|
327
352
|
try {
|
|
@@ -384,7 +409,7 @@ async function demandFetchAll({ query, rootDir, env } = {}) {
|
|
|
384
409
|
* @param {object} env
|
|
385
410
|
* @returns {{ ref: string, opts: object, display: string }[]}
|
|
386
411
|
*/
|
|
387
|
-
function buildReadCalls(match, env) {
|
|
412
|
+
function buildReadCalls(match, env, cwd = process.cwd()) {
|
|
388
413
|
if (match.provider === 'github') {
|
|
389
414
|
const opts = { repos: [match.ref], limit: 25 };
|
|
390
415
|
return [
|
|
@@ -397,7 +422,13 @@ function buildReadCalls(match, env) {
|
|
|
397
422
|
];
|
|
398
423
|
}
|
|
399
424
|
if (match.provider === 'jira') {
|
|
400
|
-
const
|
|
425
|
+
const { config } = loadProjectConfig(cwd, env);
|
|
426
|
+
const targets = resolveEffectiveSourceTargetsFromConfig(config, env)
|
|
427
|
+
.filter((t) => t.provider === 'jira')
|
|
428
|
+
.map((t) => t.selector.project);
|
|
429
|
+
const projects = targets.length
|
|
430
|
+
? targets
|
|
431
|
+
: (env.JIRA_PROJECTS ?? '').split(',').map((p) => p.trim()).filter(Boolean);
|
|
401
432
|
const recencyDays = parseInt(env.JIRA_FETCH_RECENCY_DAYS, 10) || 30;
|
|
402
433
|
|
|
403
434
|
let jql;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Registers a node:fs.watch listener on each inbox directory and triggers
|
|
5
5
|
* `InboxWatcher.poll()` shortly after filesystem activity settles, so files
|
|
6
|
-
* dropped into
|
|
6
|
+
* dropped into `inbox/` flow through ingest + the review queue in
|
|
7
7
|
* seconds rather than waiting for the scheduler's next interval.
|
|
8
8
|
*
|
|
9
9
|
* Two limits keep this stable across platforms:
|
package/lib/embed/inbox.mjs
CHANGED
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
* handles specs, ADRs, meeting notes, internal docs, research, PDFs,
|
|
7
7
|
* Office files, plain text, code, or any format supported by document-extract.
|
|
8
8
|
*
|
|
9
|
-
* Watch dirs
|
|
10
|
-
* 1.
|
|
11
|
-
* 2.
|
|
12
|
-
*
|
|
9
|
+
* Watch dirs:
|
|
10
|
+
* 1. <rootDir>/inbox/ — the canonical project-root drop zone (always watched)
|
|
11
|
+
* 2. CX_INBOX_DIRS env var + intakePolicy.additionalDirs — opt-in extra paths
|
|
12
|
+
*
|
|
13
|
+
* Atomic handoff (ADR-0045 §C): writers stage a file under `inbox/.staging/`
|
|
14
|
+
* and atomically rename it into `inbox/`. The watcher ignores `.staging/` and
|
|
15
|
+
* dotfiles, and skips any top-level file whose size is still changing between
|
|
16
|
+
* two stats, so a partially-written drop is never consumed mid-write.
|
|
13
17
|
*
|
|
14
18
|
* State tracking:
|
|
15
19
|
* <rootDir>/.cx/runtime/inbox-state.json — maps filePath → processedAt
|
|
@@ -23,7 +27,6 @@
|
|
|
23
27
|
|
|
24
28
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, appendFileSync } from 'node:fs';
|
|
25
29
|
import { join, resolve, isAbsolute } from 'node:path';
|
|
26
|
-
import { homedir } from 'node:os';
|
|
27
30
|
import { isExtractableDocumentPath, AUDIO_VIDEO_EXTS } from '../document-extract.mjs';
|
|
28
31
|
import { ingestDocuments } from '../document-ingest.mjs';
|
|
29
32
|
import { addObservation } from '../observation-store.mjs';
|
|
@@ -36,29 +39,29 @@ import { loadIntakeConfig, INTAKE_DEFAULT_MAX_DEPTH } from '../intake/intake-con
|
|
|
36
39
|
import { loadManifest, recordFile, hasFile, sha256Of, MANIFEST_REL_PATH } from '../intake/manifest.mjs';
|
|
37
40
|
import { gatherAttribution } from '../intake/attribution.mjs';
|
|
38
41
|
import { acquirePollLock, releasePollLock } from '../intake/poll-lock.mjs';
|
|
42
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
39
43
|
|
|
40
44
|
const STATE_FILE = '.cx/runtime/inbox-state.json';
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const ARCHETYPE_INBOX_SUBDIR = 'inbox';
|
|
45
|
+
const ROOT_INBOX_SUBDIR = 'inbox';
|
|
46
|
+
const STAGING_SUBDIR = '.staging';
|
|
44
47
|
const MAX_FILE_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB hard cap
|
|
45
48
|
const STALE_STATE_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
46
49
|
|
|
47
50
|
// ─── Extraction-warning helpers ───────────────────────────────────────────────
|
|
48
51
|
|
|
49
52
|
function extractionWarningsLog() {
|
|
50
|
-
return join(
|
|
53
|
+
return join(doctorRoot(), 'intake', 'extraction-warnings.jsonl');
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
function needsAsrDir() {
|
|
54
|
-
return join(
|
|
57
|
+
return join(doctorRoot(), 'intake', 'needs-asr');
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
function writeExtractionWarning({ sourcePath, droppedInfo }) {
|
|
58
61
|
if (!droppedInfo?.length) return;
|
|
59
62
|
const logPath = extractionWarningsLog();
|
|
60
63
|
try {
|
|
61
|
-
mkdirSync(join(
|
|
64
|
+
mkdirSync(join(doctorRoot(), 'intake'), { recursive: true });
|
|
62
65
|
const line = JSON.stringify({ ts: new Date().toISOString(), sourcePath, droppedInfo }) + '\n';
|
|
63
66
|
appendFileSync(logPath, line);
|
|
64
67
|
} catch { /* non-fatal */ }
|
|
@@ -139,35 +142,20 @@ function writeState(rootDir, state) {
|
|
|
139
142
|
|
|
140
143
|
/**
|
|
141
144
|
* Resolve inbox watch directories from the persisted intake-config.
|
|
142
|
-
*
|
|
143
|
-
* config
|
|
144
|
-
*
|
|
145
|
-
*
|
|
145
|
+
* The canonical <rootDir>/inbox/ is always watched (ADR-0045 §C). `parentDirs`
|
|
146
|
+
* from intake-config plus CX_INBOX_DIRS env var add opt-in extras.
|
|
147
|
+
*
|
|
148
|
+
* The canonical inbox gets a `.staging/` subdir on resolution so writers have
|
|
149
|
+
* a stable place to assemble a file before the atomic rename into `inbox/`.
|
|
146
150
|
*/
|
|
147
151
|
export function resolveInboxDirs(rootDir, env = process.env) {
|
|
148
152
|
const config = loadIntakeConfig(rootDir, env);
|
|
149
153
|
const dirs = [];
|
|
150
154
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (config.includeDocsIntake) {
|
|
158
|
-
const docsIntake = join(rootDir, DOCS_INTAKE_SUBDIR);
|
|
159
|
-
if (existsSync(docsIntake)) dirs.push(docsIntake);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Archetype inbox is a profile-driven drop zone for raw source files
|
|
163
|
-
// (capabilities.intake.inbox). Off by default; init flips it on when the
|
|
164
|
-
// active profile declares the capability so non-archetype projects keep
|
|
165
|
-
// their pre-Piece-C behavior.
|
|
166
|
-
|
|
167
|
-
if (config.includeArchetypeInbox) {
|
|
168
|
-
const archetypeInbox = join(rootDir, ARCHETYPE_INBOX_SUBDIR);
|
|
169
|
-
if (existsSync(archetypeInbox)) dirs.push(archetypeInbox);
|
|
170
|
-
}
|
|
155
|
+
const rootInbox = join(rootDir, ROOT_INBOX_SUBDIR);
|
|
156
|
+
mkdirSync(rootInbox, { recursive: true });
|
|
157
|
+
mkdirSync(join(rootInbox, STAGING_SUBDIR), { recursive: true });
|
|
158
|
+
dirs.push(rootInbox);
|
|
171
159
|
|
|
172
160
|
for (const candidate of config.parentDirs) {
|
|
173
161
|
if (!candidate) continue;
|
|
@@ -181,6 +169,20 @@ export function resolveInboxDirs(rootDir, env = process.env) {
|
|
|
181
169
|
|
|
182
170
|
// ─── File scanning ────────────────────────────────────────────────────────────
|
|
183
171
|
|
|
172
|
+
// Dotfiles and dot-directories — including the `inbox/.staging/` assembly
|
|
173
|
+
// area writers rename out of — are skipped wholesale, so a staged file is
|
|
174
|
+
// never visible to the scanner until it lands at a top-level name.
|
|
175
|
+
|
|
176
|
+
function isStableFile(full) {
|
|
177
|
+
try {
|
|
178
|
+
const a = statSync(full);
|
|
179
|
+
const b = statSync(full);
|
|
180
|
+
return a.size === b.size && a.mtimeMs === b.mtimeMs ? a : null;
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
184
186
|
/**
|
|
185
187
|
* Walk `dir` up to `maxDepth` subdirectory levels.
|
|
186
188
|
* maxDepth=0 → only files in `dir` itself (no subdirs).
|
|
@@ -202,11 +204,15 @@ function scanDir(dir, maxDepth = INTAKE_DEFAULT_MAX_DEPTH) {
|
|
|
202
204
|
}
|
|
203
205
|
if (!entry.isFile()) continue;
|
|
204
206
|
if (!isExtractableDocumentPath(full)) continue;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
|
|
208
|
+
// Stability backstop for writers that drop in place instead of staging:
|
|
209
|
+
// a file whose size/mtime is still moving between two stats is mid-write,
|
|
210
|
+
// so leave it for the next poll once it settles.
|
|
211
|
+
|
|
212
|
+
const st = isStableFile(full);
|
|
213
|
+
if (!st) continue;
|
|
214
|
+
if (st.size === 0 || st.size > MAX_FILE_SIZE_BYTES) continue;
|
|
215
|
+
results.push({ path: full, size: st.size, mtimeMs: st.mtimeMs });
|
|
210
216
|
}
|
|
211
217
|
}
|
|
212
218
|
} catch (err) { process.stderr.write('[inbox.mjs] scan-dir: ' + (err?.message ?? String(err)) + '\n'); }
|
|
@@ -273,7 +279,12 @@ function maybePromoteToDocs(rootDir, { sourcePath, extractedText }) {
|
|
|
273
279
|
if (!existsSync(laneDir)) return null;
|
|
274
280
|
|
|
275
281
|
const sourceName = sourcePath.split('/').pop() ?? 'intake-doc';
|
|
276
|
-
|
|
282
|
+
|
|
283
|
+
// A source already in markdown keeps its name; others gain a .md rendering
|
|
284
|
+
// suffix. Without the guard an intake item named foo.md promotes to foo.md.md.
|
|
285
|
+
|
|
286
|
+
const docFileName = /\.md$/i.test(sourceName) ? sourceName : `${sourceName}.md`;
|
|
287
|
+
const targetPath = nextAvailablePath(join(laneDir, docFileName));
|
|
277
288
|
const title = sourceName.replace(/\.[^.]+$/, '').replace(/[-_]+/g, ' ').trim() || 'Promoted intake document';
|
|
278
289
|
const markdown = [
|
|
279
290
|
'# ' + title,
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
18
18
|
import { join } from 'node:path';
|
|
19
|
-
|
|
19
|
+
|
|
20
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Scan an intake directory and return file metadata.
|
|
@@ -125,7 +126,7 @@ export function computeIntakeMetrics({ rootDir } = {}) {
|
|
|
125
126
|
// — Metrics from recommendation store
|
|
126
127
|
let recStats = { active: 0, dismissed: 0, total: 0 };
|
|
127
128
|
try {
|
|
128
|
-
const recIndexPath = join(
|
|
129
|
+
const recIndexPath = join(doctorRoot(), 'intake', 'recommendations-index.json');
|
|
129
130
|
if (existsSync(recIndexPath)) {
|
|
130
131
|
const index = JSON.parse(readFileSync(recIndexPath, 'utf8'));
|
|
131
132
|
const entries = Object.values(index);
|
package/lib/embed/reconcile.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* with no changes re-embeds nothing.
|
|
13
13
|
*
|
|
14
14
|
* Reconciliation-on-sync is preferred over a fixed-interval polling cron (see
|
|
15
|
-
* docs/research/vector-search-best-practices.md): the pass runs where Construct
|
|
15
|
+
* docs/notes/research/vector-search-best-practices.md): the pass runs where Construct
|
|
16
16
|
* already touches storage (the model-change re-embed, the storage CLI), keys
|
|
17
17
|
* idempotency on a content hash, and re-embeds on model change.
|
|
18
18
|
*/
|
package/lib/embed/semantic.mjs
CHANGED
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
22
22
|
import { join } from 'node:path';
|
|
23
23
|
import { createHash, randomUUID } from 'node:crypto';
|
|
24
|
-
import { homedir } from 'node:os';
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
26
|
+
|
|
27
|
+
const CACHE_DIR = join(doctorRoot(), 'cache', 'embeddings');
|
|
27
28
|
const INDEX_FILE = join(CACHE_DIR, 'index.json');
|
|
28
29
|
const MODEL_NAME = 'Xenova/all-MiniLM-L6-v2';
|
|
29
30
|
const DIMENSIONS = 384;
|
|
@@ -21,13 +21,15 @@ import os from 'node:os';
|
|
|
21
21
|
import path from 'node:path';
|
|
22
22
|
import { spawnSync } from 'node:child_process';
|
|
23
23
|
|
|
24
|
+
import { stateDir, doctorRoot } from '../config/xdg.mjs';
|
|
25
|
+
|
|
24
26
|
const HOME = os.homedir();
|
|
25
27
|
const PLATFORM = process.platform;
|
|
26
28
|
const NODE_BIN = process.execPath;
|
|
27
29
|
|
|
28
30
|
// Path to the construct bin — we use absolute path so launchd/systemd can find it.
|
|
29
31
|
function constructBin() {
|
|
30
|
-
const local = path.join(HOME, '
|
|
32
|
+
const local = path.join(stateDir(HOME), 'bin', 'construct');
|
|
31
33
|
if (fs.existsSync(local)) return local;
|
|
32
34
|
const result = spawnSync('which', ['construct'], { encoding: 'utf8' });
|
|
33
35
|
return result.status === 0 ? result.stdout.trim() : 'construct';
|
|
@@ -195,7 +197,7 @@ function windowsStatus() {
|
|
|
195
197
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
196
198
|
|
|
197
199
|
function logPath() {
|
|
198
|
-
return path.join(
|
|
200
|
+
return path.join(doctorRoot(), 'runtime', 'embed-daemon.log');
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
/**
|
package/lib/embed/worker.mjs
CHANGED
|
@@ -28,8 +28,9 @@ if (configPath) {
|
|
|
28
28
|
} else {
|
|
29
29
|
const { ProviderRegistry } = await import('./providers/registry.mjs');
|
|
30
30
|
const { EMPTY_CONFIG } = await import('./config.mjs');
|
|
31
|
+
const { resolveAutoEmbedSources } = await import('./auto-sources.mjs');
|
|
31
32
|
const registry = await ProviderRegistry.fromEnv(env);
|
|
32
|
-
const sources =
|
|
33
|
+
const sources = resolveAutoEmbedSources({ cwd: process.cwd(), env, registry });
|
|
33
34
|
daemonOpts.registry = registry;
|
|
34
35
|
daemonOpts.config = {
|
|
35
36
|
...EMPTY_CONFIG,
|
|
@@ -13,6 +13,7 @@ import { recommendPlan as recommendPlanCore } from './triage.mjs';
|
|
|
13
13
|
import { invokeWorkflow as invokeWorkflowCore } from './workflow-invoke.mjs';
|
|
14
14
|
import { buildCapabilityContract } from './capability.mjs';
|
|
15
15
|
import { resolveExecution as resolveExecutionCore } from './execution.mjs';
|
|
16
|
+
import { planArtifactWorkflow as planArtifactWorkflowCore, runArtifactWorkflow as runArtifactWorkflowCore } from '../artifact-workflow.mjs';
|
|
16
17
|
|
|
17
18
|
export { CONTRACT_VERSION, MIN_CLIENT_CONTRACT_VERSION, isClientCompatible } from './contract-version.mjs';
|
|
18
19
|
export { APPROVAL_MODES } from './audit.mjs';
|
|
@@ -85,3 +86,13 @@ export function describeCapabilities({ env, cwd, rootDir } = {}) {
|
|
|
85
86
|
export function resolveExecution(request = {}, { env, cwd, registryPath } = {}) {
|
|
86
87
|
return wrapContractResult(resolveExecutionCore(request, { env, cwd, registryPath }), { surface: 'sdk', env, cwd });
|
|
87
88
|
}
|
|
89
|
+
|
|
90
|
+
/** Plan a manifest-backed artifact workflow without executing specialists. */
|
|
91
|
+
export function planArtifactWorkflow(request = {}, { env, cwd, rootDir } = {}) {
|
|
92
|
+
return wrapContractResult(planArtifactWorkflowCore(request, { cwd, rootDir }), { surface: 'sdk', env, cwd });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Run only locally observable artifact steps and return truthful provenance. */
|
|
96
|
+
export function runArtifactWorkflow(request = {}, { env, cwd, rootDir } = {}) {
|
|
97
|
+
return wrapContractResult(runArtifactWorkflowCore(request, { cwd, rootDir }), { surface: 'sdk', env, cwd });
|
|
98
|
+
}
|
package/lib/engine/registry.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Resolution order (last-write wins):
|
|
5
5
|
* 1. Built-in defaults (lib/engine/defaults.mjs)
|
|
6
|
-
* 2.
|
|
6
|
+
* 2. plugins.json in the XDG config dir (operator-level overrides)
|
|
7
7
|
* 3. <project>/.cx/plugins.json (repo-level overrides)
|
|
8
8
|
* 4. CONSTRUCT_PLUGIN_<LAYER> environment variables (one-off overrides)
|
|
9
9
|
*
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { readFileSync, existsSync } from 'node:fs';
|
|
23
|
-
import { homedir } from 'node:os';
|
|
24
23
|
import { join, isAbsolute, resolve } from 'node:path';
|
|
25
24
|
import { pathToFileURL } from 'node:url';
|
|
26
25
|
import { LAYERS, assertContract } from './contracts.mjs';
|
|
27
26
|
import { loadDefaults } from './defaults.mjs';
|
|
27
|
+
import { configDir } from '../config/xdg.mjs';
|
|
28
28
|
|
|
29
29
|
const ENV_PREFIX = 'CONSTRUCT_PLUGIN_';
|
|
30
30
|
|
|
@@ -43,7 +43,7 @@ function readJsonIfExists(path) {
|
|
|
43
43
|
function gatherOverrides(rootDir) {
|
|
44
44
|
const overrides = [];
|
|
45
45
|
|
|
46
|
-
const userConfig = readJsonIfExists(join(
|
|
46
|
+
const userConfig = readJsonIfExists(join(configDir(), 'plugins.json'));
|
|
47
47
|
if (userConfig?.plugins) overrides.push(...userConfig.plugins);
|
|
48
48
|
|
|
49
49
|
const projectConfig = readJsonIfExists(join(rootDir, '.cx', 'plugins.json'));
|