@geraldmaron/construct 1.0.20 → 1.0.23
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 -5
- package/bin/construct +194 -16
- 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 +44 -16
- package/lib/comment-lint.mjs +115 -0
- package/lib/completions.mjs +7 -1
- package/lib/config/schema.mjs +22 -0
- package/lib/decisions/enforced-baseline.json +3 -0
- package/lib/docs-verify.mjs +15 -18
- package/lib/doctor/cli.mjs +8 -1
- package/lib/document-export.mjs +124 -0
- package/lib/document-ingest.mjs +13 -4
- package/lib/embed/daemon.mjs +1 -1
- package/lib/embed/inbox.mjs +25 -7
- package/lib/embedded-contract/triage.mjs +24 -2
- package/lib/embedded-contract/workflow-invoke.mjs +21 -0
- 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/orchestration-dispatch-guard.mjs +153 -0
- package/lib/hooks/pre-push-gate.mjs +15 -6
- 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/ingest/strategy.mjs +35 -3
- 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/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/model-registry.mjs +40 -33
- package/lib/observation-store.mjs +6 -2
- package/lib/opencode-config.mjs +1 -1
- package/lib/orchestration/events.mjs +66 -0
- package/lib/orchestration/run-store-postgres.mjs +85 -0
- package/lib/orchestration/run-store-sqlite.mjs +122 -0
- package/lib/orchestration/runtime.mjs +188 -50
- package/lib/orchestration/store.mjs +102 -0
- package/lib/orchestration/worker.mjs +215 -0
- package/lib/orchestration-policy.mjs +27 -3
- package/lib/parity.mjs +80 -26
- 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 +171 -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/reconcile/postgres-namespace.mjs +102 -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 +39 -14
- package/lib/setup-prompts.mjs +2 -1
- package/lib/setup.mjs +165 -141
- package/lib/storage/backend.mjs +14 -0
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/template-registry.mjs +73 -0
- package/lib/term-format.mjs +75 -0
- package/lib/uninstall/uninstall.mjs +180 -7
- package/package.json +2 -2
- package/personas/construct.md +7 -8
- package/platforms/claude/settings.template.json +30 -4
- 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 +381 -95
- 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 +2 -11
- 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 +29 -29
- 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 +14 -14
- 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/lib/server/static/index.html +0 -1
package/lib/model-registry.mjs
CHANGED
|
@@ -20,17 +20,14 @@ import path from 'node:path';
|
|
|
20
20
|
* 1. Environment variables (CX_MODEL_*): Emergency overrides
|
|
21
21
|
* 2. User config (~/.construct/config.env): Personal defaults
|
|
22
22
|
* 3. Registry (specialists/registry.json): Project recommendations
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
*
|
|
24
|
+
* Construct ships with NO implicit model defaults (ADR-0027,
|
|
25
|
+
* tests/model-router-no-defaults.test.mjs). When none of the three sources
|
|
26
|
+
* resolves a tier, that tier returns null with source 'not configured'.
|
|
27
|
+
* Callers must surface a clear remediation hint rather than silently
|
|
28
|
+
* substituting a vendor-specific default.
|
|
26
29
|
*/
|
|
27
30
|
|
|
28
|
-
const BUILTIN_DEFAULTS = {
|
|
29
|
-
reasoning: 'anthropic/claude-3-5-sonnet',
|
|
30
|
-
standard: 'anthropic/claude-3-haiku',
|
|
31
|
-
fast: 'anthropic/claude-3-haiku',
|
|
32
|
-
};
|
|
33
|
-
|
|
34
31
|
const ENV_PREFIX = 'CX_MODEL';
|
|
35
32
|
|
|
36
33
|
// ---------------------------------------------------------------------------
|
|
@@ -86,28 +83,30 @@ export function resolveModelTiers(options = {}) {
|
|
|
86
83
|
}
|
|
87
84
|
}
|
|
88
85
|
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
// No source resolved this tier — leave null + 'not configured' so callers
|
|
87
|
+
// can surface a clear remediation hint. Aligns with the wired path
|
|
88
|
+
// (lib/model-router.mjs:resolveTierAssignments) and the no-defaults
|
|
89
|
+
// contract locked in by tests/model-router-no-defaults.test.mjs.
|
|
90
|
+
|
|
91
|
+
result[tier] = null;
|
|
92
|
+
sources[tier] = 'not configured';
|
|
92
93
|
}
|
|
93
|
-
|
|
94
|
-
// Validation
|
|
94
|
+
|
|
95
95
|
const configured = Object.values(result).filter(Boolean).length;
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
if (strict && configured < 3) {
|
|
98
98
|
const missing = Object.entries(result)
|
|
99
99
|
.filter(([, v]) => !v)
|
|
100
100
|
.map(([k]) => k);
|
|
101
101
|
errors.push(`Missing configuration for tiers: ${missing.join(', ')}`);
|
|
102
102
|
}
|
|
103
|
-
|
|
103
|
+
|
|
104
104
|
return {
|
|
105
105
|
models: result,
|
|
106
106
|
sources,
|
|
107
107
|
configured,
|
|
108
108
|
complete: configured === 3,
|
|
109
109
|
errors: errors.length > 0 ? errors : null,
|
|
110
|
-
defaultsUsed: Object.values(sources).filter(s => s === 'default').length,
|
|
111
110
|
};
|
|
112
111
|
}
|
|
113
112
|
|
|
@@ -117,7 +116,7 @@ export function resolveModelTiers(options = {}) {
|
|
|
117
116
|
|
|
118
117
|
export function getModelForTier(tier, options = {}) {
|
|
119
118
|
const resolved = resolveModelTiers(options);
|
|
120
|
-
return resolved.models[tier]
|
|
119
|
+
return resolved.models[tier];
|
|
121
120
|
}
|
|
122
121
|
|
|
123
122
|
export function getModelSource(tier, options = {}) {
|
|
@@ -131,12 +130,15 @@ export function getModelSource(tier, options = {}) {
|
|
|
131
130
|
|
|
132
131
|
export function validateModelTiers(options = {}) {
|
|
133
132
|
const resolved = resolveModelTiers({ ...options, strict: true });
|
|
134
|
-
|
|
133
|
+
const unconfigured = Object.entries(resolved.sources)
|
|
134
|
+
.filter(([, source]) => source === 'not configured')
|
|
135
|
+
.map(([tier]) => tier);
|
|
136
|
+
|
|
135
137
|
return {
|
|
136
138
|
valid: resolved.complete && !resolved.errors,
|
|
137
139
|
errors: resolved.errors,
|
|
138
|
-
warnings:
|
|
139
|
-
? [`
|
|
140
|
+
warnings: unconfigured.length > 0
|
|
141
|
+
? [`Unconfigured tier${unconfigured.length === 1 ? '' : 's'}: ${unconfigured.join(', ')}. Run 'construct models --apply' or set CX_MODEL_<TIER>.`]
|
|
140
142
|
: [],
|
|
141
143
|
resolution: resolved,
|
|
142
144
|
};
|
|
@@ -144,25 +146,30 @@ export function validateModelTiers(options = {}) {
|
|
|
144
146
|
|
|
145
147
|
export function formatModelStatus(options = {}) {
|
|
146
148
|
const resolved = resolveModelTiers(options);
|
|
147
|
-
|
|
149
|
+
|
|
148
150
|
let output = 'Model Configuration:\n\n';
|
|
149
|
-
|
|
151
|
+
|
|
150
152
|
for (const tier of ['reasoning', 'standard', 'fast']) {
|
|
151
153
|
const model = resolved.models[tier];
|
|
152
154
|
const source = resolved.sources[tier];
|
|
153
|
-
const icon = source === '
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
const icon = source === 'not configured' ? '⚠' : '✓';
|
|
156
|
+
const display = model ?? '(not configured)';
|
|
157
|
+
|
|
158
|
+
output += `${icon} ${tier.padEnd(10)} ${display}\n`;
|
|
156
159
|
output += ` Source: ${source}\n\n`;
|
|
157
160
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
output += '
|
|
161
|
+
|
|
162
|
+
const unconfigured = Object.entries(resolved.sources)
|
|
163
|
+
.filter(([, source]) => source === 'not configured')
|
|
164
|
+
.map(([tier]) => tier);
|
|
165
|
+
if (unconfigured.length > 0) {
|
|
166
|
+
output += `\nUnconfigured: ${unconfigured.join(', ')}. To configure:\n`;
|
|
167
|
+
output += ' construct models --apply (interactive, picks from available providers)\n';
|
|
168
|
+
output += ' export CX_MODEL_REASONING=... (env override)\n';
|
|
169
|
+
output += ' export CX_MODEL_STANDARD=...\n';
|
|
170
|
+
output += ' export CX_MODEL_FAST=...\n';
|
|
164
171
|
}
|
|
165
|
-
|
|
172
|
+
|
|
166
173
|
return output;
|
|
167
174
|
}
|
|
168
175
|
|
|
@@ -238,8 +238,12 @@ export async function addObservation(rootDir, {
|
|
|
238
238
|
return record;
|
|
239
239
|
}
|
|
240
240
|
await client.close();
|
|
241
|
-
} catch {
|
|
242
|
-
//
|
|
241
|
+
} catch (err) {
|
|
242
|
+
// A reachable backend that still fails to store (dimension drift, auth, transient
|
|
243
|
+
// network) must not look identical to "no backend configured" — surface it, then
|
|
244
|
+
// preserve the observation in the local index so the write never silently vanishes.
|
|
245
|
+
|
|
246
|
+
process.stderr.write(`[observation-store] SQL store failed; kept observation in local index: ${err?.message || err}\n`);
|
|
243
247
|
}
|
|
244
248
|
|
|
245
249
|
// Fallback: local JSON vector index with hashing-bow-v1.
|
package/lib/opencode-config.mjs
CHANGED
|
@@ -48,7 +48,7 @@ export function sanitizeOpenCodeConfig(config) {
|
|
|
48
48
|
|
|
49
49
|
export function writeOpenCodeConfig(config, file = findOpenCodeConfigPath()) {
|
|
50
50
|
// Treat the canonical home config as the default target; any explicit path
|
|
51
|
-
// outside the canonical home dir (e.g. project-scoped .opencode/
|
|
51
|
+
// outside the canonical home dir (e.g. project-scoped .opencode/opencode.json)
|
|
52
52
|
// is honored as-is so per-project sync writes to the right tree.
|
|
53
53
|
|
|
54
54
|
const canonical = getCanonicalOpenCodeConfigPath();
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/orchestration/events.mjs — in-process run lifecycle event bus.
|
|
3
|
+
*
|
|
4
|
+
* The orchestration runtime persists runs to a store and appends durable JSONL
|
|
5
|
+
* traces, but has no live pub/sub a server can stream from. This is that
|
|
6
|
+
* primitive: a process-local EventEmitter keyed by runId so an HTTP SSE handler
|
|
7
|
+
* can subscribe to a single run's transitions (planned → running → per-task →
|
|
8
|
+
* completed) and forward them to a thin client. Events are lightweight status
|
|
9
|
+
* deltas — never task output or any credential — so the stream stays cheap and
|
|
10
|
+
* secret-free; clients fetch full run records (envelope-guarded) for outputs.
|
|
11
|
+
*
|
|
12
|
+
* Process-local by design: it streams runs executing in THIS process. Durable,
|
|
13
|
+
* cross-process history lives in the run store and the trace log.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { EventEmitter } from 'node:events';
|
|
17
|
+
|
|
18
|
+
const bus = new EventEmitter();
|
|
19
|
+
bus.setMaxListeners(0);
|
|
20
|
+
|
|
21
|
+
function channel(runId) {
|
|
22
|
+
return `run:${runId}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Emit a run lifecycle event. Stamps runId + an ISO timestamp; callers pass a
|
|
27
|
+
* small status delta (type, status, taskId, role, executor, error).
|
|
28
|
+
*
|
|
29
|
+
* @param {string} runId
|
|
30
|
+
* @param {object} event
|
|
31
|
+
*/
|
|
32
|
+
export function emitRunEvent(runId, event = {}) {
|
|
33
|
+
if (!runId) return;
|
|
34
|
+
bus.emit(channel(runId), { runId, at: new Date().toISOString(), ...event });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Subscribe to one run's events.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} runId
|
|
41
|
+
* @param {(event:object)=>void} handler
|
|
42
|
+
* @returns {() => void} unsubscribe
|
|
43
|
+
*/
|
|
44
|
+
export function onRunEvent(runId, handler) {
|
|
45
|
+
const ch = channel(runId);
|
|
46
|
+
bus.on(ch, handler);
|
|
47
|
+
return () => bus.off(ch, handler);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Cooperative, between-task cancellation. executeRun checks this before each
|
|
51
|
+
// task and stops cleanly if set — an honest soft cancel (it cannot abort a
|
|
52
|
+
// model call already in flight), not a forced kill.
|
|
53
|
+
|
|
54
|
+
const cancelled = new Set();
|
|
55
|
+
|
|
56
|
+
export function requestCancel(runId) {
|
|
57
|
+
if (runId) cancelled.add(runId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isCancelRequested(runId) {
|
|
61
|
+
return cancelled.has(runId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function clearCancel(runId) {
|
|
65
|
+
cancelled.delete(runId);
|
|
66
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/orchestration/run-store-postgres.mjs — Postgres-backed run store (Mode-C).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors lib/intake/postgres-queue.mjs: a class taking the tagged-template `sql`
|
|
5
|
+
* client from createSqlClient(env) (porsager/postgres style) and a project key.
|
|
6
|
+
* Orchestration runs persist to `construct_orchestration_runs`; triage-like
|
|
7
|
+
* columns (status, execution_mode) are flattened for filtering while the full run
|
|
8
|
+
* round-trips in `payload jsonb`. Multi-project isolation comes from the
|
|
9
|
+
* (run_id, project) composite primary key.
|
|
10
|
+
*
|
|
11
|
+
* Scope: the STORE is the deliverable. Container-worker spawning (Mode-C's
|
|
12
|
+
* optional worker topology) is out of scope (ADR-0021); a Postgres run store lets
|
|
13
|
+
* a team/enterprise deployment share durable runs across processes today.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export class PostgresRunStore {
|
|
17
|
+
constructor({ sql, project } = {}) {
|
|
18
|
+
if (!sql) throw new Error('PostgresRunStore: sql client is required');
|
|
19
|
+
if (!project) throw new Error('PostgresRunStore: project is required');
|
|
20
|
+
this.sql = sql;
|
|
21
|
+
this.project = project;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async ensureSchema() {
|
|
25
|
+
await this.sql`
|
|
26
|
+
CREATE TABLE IF NOT EXISTS construct_orchestration_runs (
|
|
27
|
+
run_id TEXT,
|
|
28
|
+
project TEXT,
|
|
29
|
+
created_at TEXT,
|
|
30
|
+
status TEXT,
|
|
31
|
+
execution_mode TEXT,
|
|
32
|
+
payload JSONB,
|
|
33
|
+
PRIMARY KEY (run_id, project)
|
|
34
|
+
)
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async saveRun(run) {
|
|
39
|
+
if (!run?.runId) throw new Error('saveRun: run.runId is required');
|
|
40
|
+
await this.sql`
|
|
41
|
+
INSERT INTO construct_orchestration_runs (
|
|
42
|
+
run_id, project, created_at, status, execution_mode, payload
|
|
43
|
+
) VALUES (
|
|
44
|
+
${run.runId}, ${this.project}, ${run.createdAt || null},
|
|
45
|
+
${run.status || null}, ${run.execution?.executionMode || null},
|
|
46
|
+
${this.sql.json(run)}
|
|
47
|
+
)
|
|
48
|
+
ON CONFLICT (run_id, project) DO UPDATE SET
|
|
49
|
+
created_at = EXCLUDED.created_at,
|
|
50
|
+
status = EXCLUDED.status,
|
|
51
|
+
execution_mode = EXCLUDED.execution_mode,
|
|
52
|
+
payload = EXCLUDED.payload
|
|
53
|
+
`;
|
|
54
|
+
return run;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async loadRun(runId) {
|
|
58
|
+
if (!runId) return null;
|
|
59
|
+
const rows = await this.sql`
|
|
60
|
+
SELECT payload FROM construct_orchestration_runs
|
|
61
|
+
WHERE run_id = ${runId} AND project = ${this.project}
|
|
62
|
+
LIMIT 1
|
|
63
|
+
`;
|
|
64
|
+
return rows[0]?.payload || null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async listRuns({ limit = 20 } = {}) {
|
|
68
|
+
const rows = await this.sql`
|
|
69
|
+
SELECT payload FROM construct_orchestration_runs
|
|
70
|
+
WHERE project = ${this.project}
|
|
71
|
+
ORDER BY created_at DESC
|
|
72
|
+
LIMIT ${limit}
|
|
73
|
+
`;
|
|
74
|
+
return rows.map((row) => {
|
|
75
|
+
const run = row.payload || {};
|
|
76
|
+
return {
|
|
77
|
+
runId: run.runId,
|
|
78
|
+
status: run.status,
|
|
79
|
+
executionMode: run.execution?.executionMode || null,
|
|
80
|
+
createdAt: run.createdAt,
|
|
81
|
+
request: run.request?.summary || null,
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/orchestration/run-store-sqlite.mjs — SQLite-backed run store (Mode-B).
|
|
3
|
+
*
|
|
4
|
+
* Same saveRun/loadRun/listRuns surface as the filesystem store (run-store.mjs),
|
|
5
|
+
* backed by a single `node:sqlite` database under
|
|
6
|
+
* `.cx/runtime/orchestration/runs.db`. A daemon-grade local store: durable,
|
|
7
|
+
* single-file, queryable, with no Postgres/Docker dependency.
|
|
8
|
+
*
|
|
9
|
+
* Node compatibility boundary: `node:sqlite` exists only on Node >=22.5, but the
|
|
10
|
+
* CI matrix also runs Node 20. So the import is LAZY (inside a function, never at
|
|
11
|
+
* module top level) and `sqliteAvailable()` reports false when it throws. The
|
|
12
|
+
* store factory throws a structured SQLITE_UNAVAILABLE error (with remediation)
|
|
13
|
+
* rather than crashing, so the resolver can fall back to filesystem. Nothing on
|
|
14
|
+
* the default test path statically imports node:sqlite.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { mkdirSync } from 'node:fs';
|
|
18
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const RUNTIME_REL = join('.cx', 'runtime', 'orchestration');
|
|
23
|
+
|
|
24
|
+
let cachedDatabaseSync;
|
|
25
|
+
|
|
26
|
+
// The node:sqlite probe is cached and synchronous. createRequire above is a
|
|
27
|
+
// plain static import of node:module (always present); only node:sqlite itself
|
|
28
|
+
// is loaded lazily here, so Node 20 never touches the unavailable builtin.
|
|
29
|
+
|
|
30
|
+
function loadDatabaseSync() {
|
|
31
|
+
if (cachedDatabaseSync !== undefined) return cachedDatabaseSync;
|
|
32
|
+
try {
|
|
33
|
+
cachedDatabaseSync = require('node:sqlite').DatabaseSync;
|
|
34
|
+
} catch {
|
|
35
|
+
cachedDatabaseSync = null;
|
|
36
|
+
}
|
|
37
|
+
return cachedDatabaseSync;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function sqliteAvailable() {
|
|
41
|
+
return loadDatabaseSync() != null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function projectRoot(cwd) {
|
|
45
|
+
return isAbsolute(cwd) ? cwd : resolve(cwd);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function dbPath(cwd) {
|
|
49
|
+
const dir = join(projectRoot(cwd), RUNTIME_REL);
|
|
50
|
+
mkdirSync(dir, { recursive: true });
|
|
51
|
+
return join(dir, 'runs.db');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function openDb(cwd) {
|
|
55
|
+
const DatabaseSync = loadDatabaseSync();
|
|
56
|
+
if (!DatabaseSync) {
|
|
57
|
+
const err = new Error('SQLite run store requires Node >=22.5 (node:sqlite is unavailable on this runtime).');
|
|
58
|
+
err.code = 'SQLITE_UNAVAILABLE';
|
|
59
|
+
err.remediation = 'Upgrade to Node >=22.5, or set orchestration.store to "filesystem" / "postgres".';
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
const db = new DatabaseSync(dbPath(cwd));
|
|
63
|
+
db.exec(`CREATE TABLE IF NOT EXISTS runs (
|
|
64
|
+
run_id TEXT PRIMARY KEY,
|
|
65
|
+
created_at TEXT,
|
|
66
|
+
status TEXT,
|
|
67
|
+
execution_mode TEXT,
|
|
68
|
+
json TEXT
|
|
69
|
+
)`);
|
|
70
|
+
return db;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build a SQLite-backed run store bound to a project directory.
|
|
75
|
+
*
|
|
76
|
+
* @param {object} opts
|
|
77
|
+
* @param {string} opts.cwd
|
|
78
|
+
* @returns {{ saveRun(run):object, loadRun(runId):object|null, listRuns(opts):Array }}
|
|
79
|
+
*/
|
|
80
|
+
export function createSqliteRunStore({ cwd = process.cwd() } = {}) {
|
|
81
|
+
const db = openDb(cwd);
|
|
82
|
+
return {
|
|
83
|
+
saveRun(run) {
|
|
84
|
+
const stmt = db.prepare(`INSERT INTO runs (run_id, created_at, status, execution_mode, json)
|
|
85
|
+
VALUES (?, ?, ?, ?, ?)
|
|
86
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
87
|
+
created_at = excluded.created_at,
|
|
88
|
+
status = excluded.status,
|
|
89
|
+
execution_mode = excluded.execution_mode,
|
|
90
|
+
json = excluded.json`);
|
|
91
|
+
stmt.run(run.runId, run.createdAt || null, run.status || null, run.execution?.executionMode || null, JSON.stringify(run));
|
|
92
|
+
return run;
|
|
93
|
+
},
|
|
94
|
+
loadRun(runId) {
|
|
95
|
+
if (!runId) return null;
|
|
96
|
+
const row = db.prepare('SELECT json FROM runs WHERE run_id = ?').get(runId);
|
|
97
|
+
if (!row || !row.json) return null;
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(row.json);
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
listRuns({ limit = 20 } = {}) {
|
|
105
|
+
const rows = db.prepare('SELECT json FROM runs ORDER BY created_at DESC LIMIT ?').all(limit);
|
|
106
|
+
const out = [];
|
|
107
|
+
for (const row of rows) {
|
|
108
|
+
try {
|
|
109
|
+
const run = JSON.parse(row.json);
|
|
110
|
+
out.push({
|
|
111
|
+
runId: run.runId,
|
|
112
|
+
status: run.status,
|
|
113
|
+
executionMode: run.execution?.executionMode || null,
|
|
114
|
+
createdAt: run.createdAt,
|
|
115
|
+
request: run.request?.summary || null,
|
|
116
|
+
});
|
|
117
|
+
} catch { /* a corrupt row is skipped, not fatal to listing */ }
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|