@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
|
@@ -1,25 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/orchestration/runtime.mjs — Construct-owned local orchestration runtime
|
|
2
|
+
* lib/orchestration/runtime.mjs — Construct-owned local orchestration runtime.
|
|
3
3
|
*
|
|
4
4
|
* The product goal: hosts are front doors, Construct is the system. A
|
|
5
5
|
* host that lacks native multi-agent execution should still reach equivalent
|
|
6
6
|
* Construct outcomes through a Construct-owned runtime rather than depending on
|
|
7
|
-
* whichever editor exposes the strongest teammate mechanics. This module
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* whichever editor exposes the strongest teammate mechanics. This module intakes
|
|
8
|
+
* a request, plans/decomposes it into a sequenced specialist chain (reusing the
|
|
9
|
+
* orchestration-policy planner), resolves the execution-capability contract
|
|
10
|
+
* (reusing resolveExecution), persists a durable run + task lifecycle through a
|
|
11
|
+
* pluggable run store (filesystem default, sqlite Mode-B, postgres Mode-C), and
|
|
12
|
+
* emits lifecycle traces.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* `inline`
|
|
16
|
-
* observability, and PREPARES each specialist task
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
14
|
+
* Worker backends (ADR-0020, ADR-0021):
|
|
15
|
+
* - `inline` (default): OWNS planning, sequencing, handoff state, persistence,
|
|
16
|
+
* and observability, and PREPARES each specialist task for a downstream
|
|
17
|
+
* executor. It does NOT itself perform specialist LLM reasoning. Tasks reach
|
|
18
|
+
* status `prepared`; the prepare-only disclaimer applies.
|
|
19
|
+
* - `provider`: EXECUTES each specialist task by calling the configured
|
|
20
|
+
* provider/model with the specialist persona prompt. Tasks reach status
|
|
21
|
+
* `done` carrying real model output; the prepare-only disclaimer does NOT
|
|
22
|
+
* apply to provider-executed tasks. A failing task is recorded (status
|
|
23
|
+
* `failed`) and the run completes `completed-with-failures` rather than
|
|
24
|
+
* crashing.
|
|
25
|
+
*
|
|
26
|
+
* The run reports `workerBackend` and per-task `executor` so a host can never
|
|
27
|
+
* mistake a prepared task for executed specialist output. When the execution
|
|
28
|
+
* contract resolves to prompt-only or host-direct, Construct owns no specialist
|
|
29
|
+
* task sequence and the run records that explicitly.
|
|
23
30
|
*/
|
|
24
31
|
|
|
25
32
|
import { randomBytes } from 'node:crypto';
|
|
@@ -27,13 +34,19 @@ import { randomBytes } from 'node:crypto';
|
|
|
27
34
|
import { routeRequest } from '../orchestration-policy.mjs';
|
|
28
35
|
import { resolveExecution } from '../embedded-contract/execution.mjs';
|
|
29
36
|
import { detectHostCapabilities } from '../host-capabilities.mjs';
|
|
37
|
+
import { loadProjectConfig } from '../config/project-config.mjs';
|
|
38
|
+
import { CHAIN_OF_THOUGHT_MODES } from '../config/schema.mjs';
|
|
30
39
|
import { newTraceId, newSpanId, emitTraceEvent } from '../worker/trace.mjs';
|
|
31
|
-
import {
|
|
40
|
+
import { resolveRunStore } from './store.mjs';
|
|
41
|
+
import { runTaskViaProvider, INLINE, PROVIDER, WORKER_BACKEND_SET } from './worker.mjs';
|
|
42
|
+
import { emitRunEvent, isCancelRequested, clearCancel } from './events.mjs';
|
|
32
43
|
|
|
33
|
-
export const WORKER_BACKENDS =
|
|
34
|
-
export const DEFAULT_WORKER_BACKEND =
|
|
44
|
+
export const WORKER_BACKENDS = WORKER_BACKEND_SET;
|
|
45
|
+
export const DEFAULT_WORKER_BACKEND = INLINE;
|
|
46
|
+
export const CHAIN_OF_THOUGHT = CHAIN_OF_THOUGHT_MODES;
|
|
47
|
+
export const DEFAULT_CHAIN_OF_THOUGHT = 'hidden';
|
|
35
48
|
|
|
36
|
-
const RUNTIME_SEMANTICS = '
|
|
49
|
+
const RUNTIME_SEMANTICS = 'The inline worker backend owns planning, sequencing, handoff state, persistence, and observability, and prepares each specialist task for a downstream executor. It does not perform specialist LLM reasoning itself; tasks reaching status "prepared" are ready for a worker backend or host to execute. The provider worker backend executes specialist reasoning via the configured model and records real output as task.output.';
|
|
37
50
|
|
|
38
51
|
function newRunId() {
|
|
39
52
|
return `run-${randomBytes(6).toString('hex')}`;
|
|
@@ -87,25 +100,58 @@ function buildTasks(route) {
|
|
|
87
100
|
handoffContract: handoffByProducer.get(role.replace(/^cx-/, '')) || null,
|
|
88
101
|
status: 'queued',
|
|
89
102
|
executor: null,
|
|
103
|
+
output: null,
|
|
104
|
+
reasoning: null,
|
|
105
|
+
error: null,
|
|
90
106
|
startedAt: null,
|
|
91
107
|
finishedAt: null,
|
|
92
108
|
}));
|
|
93
109
|
}
|
|
94
110
|
|
|
111
|
+
// The worker backend is a deployment concern: an explicit option wins, then the
|
|
112
|
+
// run's recorded backend, then config (orchestration.workerBackend), then the
|
|
113
|
+
// inline default. An unknown value falls back to inline rather than guessing.
|
|
114
|
+
|
|
115
|
+
function resolveWorkerBackend({ explicit, run, config }) {
|
|
116
|
+
const candidate = explicit || run?.workerBackend || config?.orchestration?.workerBackend || DEFAULT_WORKER_BACKEND;
|
|
117
|
+
return WORKER_BACKENDS.includes(candidate) ? candidate : DEFAULT_WORKER_BACKEND;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Chain-of-thought disclosure is a deployment concern (ADR-0030): an explicit
|
|
121
|
+
// option wins, then the run's recorded mode, then config
|
|
122
|
+
// (orchestration.chainOfThought), then the hidden default. An unknown value
|
|
123
|
+
// falls back to hidden — specialist reasoning is never surfaced by guess.
|
|
124
|
+
|
|
125
|
+
function resolveChainOfThought({ explicit, run, config }) {
|
|
126
|
+
const candidate = explicit || run?.chainOfThought || config?.orchestration?.chainOfThought || DEFAULT_CHAIN_OF_THOUGHT;
|
|
127
|
+
return CHAIN_OF_THOUGHT.includes(candidate) ? candidate : DEFAULT_CHAIN_OF_THOUGHT;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function loadConfig(cwd, env) {
|
|
131
|
+
try {
|
|
132
|
+
return loadProjectConfig(cwd, env).config || {};
|
|
133
|
+
} catch {
|
|
134
|
+
return {};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
95
138
|
/**
|
|
96
139
|
* Plan a run: resolve the execution contract, decompose into a specialist chain,
|
|
97
140
|
* and persist a durable run record (status `planned`). Pure of model calls.
|
|
98
141
|
*
|
|
99
142
|
* @param {object} request
|
|
100
143
|
* @param {object} [opts] { env, cwd }
|
|
101
|
-
* @returns {object} the persisted run
|
|
144
|
+
* @returns {Promise<object>} the persisted run
|
|
102
145
|
*/
|
|
103
|
-
export function planRun(request = {}, { env = process.env, cwd = process.cwd() } = {}) {
|
|
146
|
+
export async function planRun(request = {}, { env = process.env, cwd = process.cwd() } = {}) {
|
|
104
147
|
const {
|
|
105
148
|
request: text = '', workflowType = null, requestedStrategy = 'auto', useConstruct = true,
|
|
106
149
|
host = null, hostModel = null, hostProvider = null, fileCount = 0, moduleCount = 0,
|
|
107
150
|
} = request;
|
|
108
151
|
|
|
152
|
+
const config = loadConfig(cwd, env);
|
|
153
|
+
const { store, warnings: storeWarnings } = resolveRunStore({ config, env, cwd });
|
|
154
|
+
|
|
109
155
|
const execData = resolveExecution(
|
|
110
156
|
{ workflowType, requestedStrategy, useConstruct, host, hostModel, hostProvider },
|
|
111
157
|
{ env, cwd },
|
|
@@ -115,6 +161,8 @@ export function planRun(request = {}, { env = process.env, cwd = process.cwd() }
|
|
|
115
161
|
const traceId = newTraceId();
|
|
116
162
|
const runId = newRunId();
|
|
117
163
|
const now = new Date().toISOString();
|
|
164
|
+
const workerBackend = resolveWorkerBackend({ config });
|
|
165
|
+
const chainOfThought = resolveChainOfThought({ config });
|
|
118
166
|
|
|
119
167
|
// Construct owns a specialist task sequence only when the contract resolves to
|
|
120
168
|
// orchestrated execution; prompt-only and host-direct own none, and the run
|
|
@@ -129,7 +177,8 @@ export function planRun(request = {}, { env = process.env, cwd = process.cwd() }
|
|
|
129
177
|
updatedAt: now,
|
|
130
178
|
request: { summary: truncate(text, 200), workflowType, requestedStrategy },
|
|
131
179
|
hostRole: resolveHostRole(host),
|
|
132
|
-
workerBackend
|
|
180
|
+
workerBackend,
|
|
181
|
+
chainOfThought,
|
|
133
182
|
execution: pickExecution(execData),
|
|
134
183
|
plan: {
|
|
135
184
|
intent: route.intent,
|
|
@@ -140,69 +189,153 @@ export function planRun(request = {}, { env = process.env, cwd = process.cwd() }
|
|
|
140
189
|
},
|
|
141
190
|
tasks,
|
|
142
191
|
status: 'planned',
|
|
143
|
-
warnings: execData.warnings || [],
|
|
192
|
+
warnings: [...(execData.warnings || []), ...storeWarnings],
|
|
144
193
|
semantics: RUNTIME_SEMANTICS,
|
|
145
194
|
executionSemantics: execData.semantics,
|
|
146
195
|
};
|
|
147
196
|
|
|
148
|
-
saveRun(
|
|
197
|
+
await store.saveRun(run);
|
|
149
198
|
emitTraceEvent({
|
|
150
199
|
rootDir: cwd, env, traceId, spanId: newSpanId(), eventType: 'task_graph.created',
|
|
151
200
|
metadata: { runId, executionMode: run.execution.executionMode, specialists: run.plan.specialists, workerBackend: run.workerBackend },
|
|
152
201
|
});
|
|
202
|
+
emitRunEvent(runId, { type: 'planned', status: 'planned', executionMode: run.execution.executionMode, taskCount: tasks.length });
|
|
153
203
|
return run;
|
|
154
204
|
}
|
|
155
205
|
|
|
206
|
+
// The inline backend prepares one task: queued → running → prepared, no model
|
|
207
|
+
// reasoning. The boundary ADR-0020 makes explicit.
|
|
208
|
+
|
|
209
|
+
function prepareTaskInline(task) {
|
|
210
|
+
task.executor = 'inline:prepared';
|
|
211
|
+
task.status = 'prepared';
|
|
212
|
+
task.finishedAt = new Date().toISOString();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// The provider backend executes one task against the configured model. A failed
|
|
216
|
+
// task is recorded (status `failed`, task.error) and does not abort the run, so
|
|
217
|
+
// one specialist failure cannot lose the work of the others.
|
|
218
|
+
|
|
219
|
+
async function executeTaskViaProvider(task, run, env, fetchImpl, chainOfThought) {
|
|
220
|
+
try {
|
|
221
|
+
const result = await runTaskViaProvider({
|
|
222
|
+
task, run,
|
|
223
|
+
model: run.execution?.selectedModel,
|
|
224
|
+
provider: run.execution?.selectedProvider,
|
|
225
|
+
env, fetchImpl, chainOfThought,
|
|
226
|
+
});
|
|
227
|
+
task.output = result.output;
|
|
228
|
+
task.executor = `provider:${result.provider}:${result.model}`;
|
|
229
|
+
task.status = 'done';
|
|
230
|
+
task.finishedAt = new Date().toISOString();
|
|
231
|
+
|
|
232
|
+
// `surface` attaches reasoning to the task so every display surface renders
|
|
233
|
+
// it; `telemetry_only` keeps it off the task (recorded only in the trace).
|
|
234
|
+
if (chainOfThought === 'surface' && result.reasoning) task.reasoning = result.reasoning;
|
|
235
|
+
return { ok: true, reasoning: result.reasoning || '' };
|
|
236
|
+
} catch (err) {
|
|
237
|
+
task.executor = `provider:error`;
|
|
238
|
+
task.error = { code: err.code || 'PROVIDER_EXECUTION_FAILED', message: err.message };
|
|
239
|
+
task.status = 'failed';
|
|
240
|
+
task.finishedAt = new Date().toISOString();
|
|
241
|
+
return { ok: false, reasoning: '' };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
156
245
|
/**
|
|
157
|
-
* Execute a planned run through the
|
|
158
|
-
* task
|
|
159
|
-
* is resumable and observable. The inline backend prepares tasks; it does not
|
|
160
|
-
* perform specialist LLM reasoning.
|
|
246
|
+
* Execute a planned run through the chosen worker backend, persisting after each
|
|
247
|
+
* task transition so the run is resumable and observable.
|
|
161
248
|
*
|
|
162
249
|
* @param {string} cwd
|
|
163
250
|
* @param {string} runId
|
|
164
|
-
* @param {object} [opts] { env }
|
|
165
|
-
* @returns {object} the completed run
|
|
251
|
+
* @param {object} [opts] { env, workerBackend, fetchImpl }
|
|
252
|
+
* @returns {Promise<object>} the completed run
|
|
166
253
|
*/
|
|
167
|
-
export function executeRun(cwd, runId, { env = process.env } = {}) {
|
|
168
|
-
const
|
|
254
|
+
export async function executeRun(cwd, runId, { env = process.env, workerBackend = null, fetchImpl } = {}) {
|
|
255
|
+
const config = loadConfig(cwd, env);
|
|
256
|
+
const { store } = resolveRunStore({ config, env, cwd });
|
|
257
|
+
const run = await store.loadRun(runId);
|
|
169
258
|
if (!run) {
|
|
170
259
|
const err = new Error(`Orchestration run not found: ${runId}`);
|
|
171
260
|
err.code = 'RUN_NOT_FOUND';
|
|
172
261
|
throw err;
|
|
173
262
|
}
|
|
174
263
|
|
|
264
|
+
const backend = resolveWorkerBackend({ explicit: workerBackend, run, config });
|
|
265
|
+
const chainOfThought = resolveChainOfThought({ run, config });
|
|
266
|
+
run.workerBackend = backend;
|
|
267
|
+
run.chainOfThought = chainOfThought;
|
|
175
268
|
run.status = 'running';
|
|
176
269
|
run.updatedAt = new Date().toISOString();
|
|
177
|
-
saveRun(
|
|
270
|
+
await store.saveRun(run);
|
|
271
|
+
emitRunEvent(runId, { type: 'running', status: 'running', workerBackend: backend });
|
|
178
272
|
|
|
273
|
+
let anyFailed = false;
|
|
274
|
+
let cancelled = false;
|
|
179
275
|
for (const task of run.tasks) {
|
|
276
|
+
if (isCancelRequested(runId)) { cancelled = true; break; }
|
|
180
277
|
task.status = 'running';
|
|
181
278
|
task.startedAt = new Date().toISOString();
|
|
182
|
-
emitTraceEvent({ rootDir: cwd, env, traceId: run.traceId, spanId: newSpanId(), eventType: 'worker.started', role: task.role, taskId: task.id, metadata: { runId } });
|
|
279
|
+
emitTraceEvent({ rootDir: cwd, env, traceId: run.traceId, spanId: newSpanId(), eventType: 'worker.started', role: task.role, taskId: task.id, metadata: { runId, workerBackend: backend } });
|
|
280
|
+
emitRunEvent(runId, { type: 'task', taskId: task.id, role: task.role, status: 'running' });
|
|
183
281
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
282
|
+
let taskReasoning = '';
|
|
283
|
+
if (backend === PROVIDER) {
|
|
284
|
+
const res = await executeTaskViaProvider(task, run, env, fetchImpl, chainOfThought);
|
|
285
|
+
if (!res.ok) anyFailed = true;
|
|
286
|
+
taskReasoning = res.reasoning;
|
|
287
|
+
} else {
|
|
288
|
+
prepareTaskInline(task);
|
|
289
|
+
}
|
|
188
290
|
|
|
291
|
+
// telemetry_only records reasoning to the trace without ever surfacing it to
|
|
292
|
+
// a display; surface keeps it off the trace (it rides on task.reasoning).
|
|
293
|
+
const completedMeta = { runId, status: task.status };
|
|
294
|
+
if (chainOfThought === 'telemetry_only' && taskReasoning) {
|
|
295
|
+
completedMeta.reasoning = taskReasoning;
|
|
296
|
+
completedMeta.reasoningChars = taskReasoning.length;
|
|
297
|
+
}
|
|
298
|
+
emitTraceEvent({ rootDir: cwd, env, traceId: run.traceId, spanId: newSpanId(), eventType: 'worker.completed', role: task.role, taskId: task.id, metadata: completedMeta });
|
|
299
|
+
emitRunEvent(runId, { type: 'task', taskId: task.id, role: task.role, status: task.status, executor: task.executor, ...(task.reasoning ? { reasoning: task.reasoning } : {}), ...(task.error ? { error: task.error } : {}) });
|
|
189
300
|
run.updatedAt = new Date().toISOString();
|
|
190
|
-
saveRun(
|
|
301
|
+
await store.saveRun(run);
|
|
191
302
|
}
|
|
192
303
|
|
|
193
|
-
run.status = 'completed';
|
|
304
|
+
run.status = cancelled ? 'cancelled' : (anyFailed ? 'completed-with-failures' : 'completed');
|
|
194
305
|
run.updatedAt = new Date().toISOString();
|
|
195
|
-
saveRun(
|
|
306
|
+
await store.saveRun(run);
|
|
307
|
+
clearCancel(runId);
|
|
196
308
|
emitTraceEvent({ rootDir: cwd, env, traceId: run.traceId, spanId: newSpanId(), eventType: 'lifecycle.completed', metadata: { runId, status: run.status, tasks: run.tasks.length } });
|
|
309
|
+
emitRunEvent(runId, { type: 'completed', status: run.status });
|
|
197
310
|
return run;
|
|
198
311
|
}
|
|
199
312
|
|
|
200
313
|
/**
|
|
201
314
|
* Plan and execute in one call (the common `orchestrate run` path).
|
|
202
315
|
*/
|
|
203
|
-
export function runOrchestration(request = {}, { env = process.env, cwd = process.cwd() } = {}) {
|
|
204
|
-
const planned = planRun(request, { env, cwd });
|
|
205
|
-
return executeRun(cwd, planned.runId, { env });
|
|
316
|
+
export async function runOrchestration(request = {}, { env = process.env, cwd = process.cwd(), workerBackend = null, fetchImpl } = {}) {
|
|
317
|
+
const planned = await planRun(request, { env, cwd });
|
|
318
|
+
return executeRun(cwd, planned.runId, { env, workerBackend, fetchImpl });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Start a run for a thin client: plan + persist synchronously, then execute in
|
|
323
|
+
* the BACKGROUND (not awaited) so the caller gets a runId immediately and tracks
|
|
324
|
+
* progress via the event stream / run store. Execution failures are caught and
|
|
325
|
+
* surfaced as a terminal `error` run event rather than an unhandled rejection.
|
|
326
|
+
*
|
|
327
|
+
* @param {object} request
|
|
328
|
+
* @param {object} [opts] { env, cwd, workerBackend, fetchImpl }
|
|
329
|
+
* @returns {Promise<object>} the planned run (status `planned`)
|
|
330
|
+
*/
|
|
331
|
+
export async function startRun(request = {}, { env = process.env, cwd = process.cwd(), workerBackend = null, fetchImpl } = {}) {
|
|
332
|
+
const planned = await planRun(request, { env, cwd });
|
|
333
|
+
Promise.resolve()
|
|
334
|
+
.then(() => executeRun(cwd, planned.runId, { env, workerBackend, fetchImpl }))
|
|
335
|
+
.catch((err) => {
|
|
336
|
+
emitRunEvent(planned.runId, { type: 'error', status: 'error', error: { code: err.code || 'RUN_FAILED', message: err.message } });
|
|
337
|
+
});
|
|
338
|
+
return planned;
|
|
206
339
|
}
|
|
207
340
|
|
|
208
341
|
/**
|
|
@@ -219,22 +352,27 @@ export function hostAdapterMetadata(run) {
|
|
|
219
352
|
executionMode: e.executionMode,
|
|
220
353
|
constructCapabilitiesActive: e.constructCapabilitiesActive,
|
|
221
354
|
workerBackend: run.workerBackend,
|
|
355
|
+
chainOfThought: run.chainOfThought ?? null,
|
|
222
356
|
hostRole: run.hostRole,
|
|
223
357
|
degraded: e.degraded,
|
|
224
358
|
degradationReason: e.degradationReason,
|
|
225
359
|
selectedProvider: e.selectedProvider,
|
|
226
360
|
selectedModel: e.selectedModel,
|
|
227
|
-
tasks: (run.tasks || []).map((t) => ({ id: t.id, role: t.role, status: t.status, executor: t.executor })),
|
|
361
|
+
tasks: (run.tasks || []).map((t) => ({ id: t.id, role: t.role, status: t.status, executor: t.executor, reasoning: t.reasoning ?? null })),
|
|
228
362
|
warnings: run.warnings || [],
|
|
229
363
|
semantics: run.semantics,
|
|
230
364
|
executionSemantics: run.executionSemantics,
|
|
231
365
|
};
|
|
232
366
|
}
|
|
233
367
|
|
|
234
|
-
export function getRun(cwd, runId) {
|
|
235
|
-
|
|
368
|
+
export async function getRun(cwd, runId, { env = process.env } = {}) {
|
|
369
|
+
const config = loadConfig(cwd, env);
|
|
370
|
+
const { store } = resolveRunStore({ config, env, cwd });
|
|
371
|
+
return store.loadRun(runId);
|
|
236
372
|
}
|
|
237
373
|
|
|
238
|
-
export function getRuns(cwd, opts) {
|
|
239
|
-
|
|
374
|
+
export async function getRuns(cwd, { env = process.env, ...opts } = {}) {
|
|
375
|
+
const config = loadConfig(cwd, env);
|
|
376
|
+
const { store } = resolveRunStore({ config, env, cwd });
|
|
377
|
+
return store.listRuns(opts);
|
|
240
378
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/orchestration/store.mjs — run-store resolver for the orchestration runtime.
|
|
3
|
+
*
|
|
4
|
+
* One interface — saveRun(run), loadRun(runId), listRuns(opts) — over three
|
|
5
|
+
* backends: filesystem (Mode-A default, zero dependency), sqlite (Mode-B,
|
|
6
|
+
* node:sqlite), and postgres (Mode-C, shared team/enterprise store). The runtime
|
|
7
|
+
* resolves a store through here instead of importing run-store functions
|
|
8
|
+
* directly, so the backend is a deployment concern rather than a code edit.
|
|
9
|
+
*
|
|
10
|
+
* Selection precedence: explicit config (`orchestration.store`) or the
|
|
11
|
+
* CONSTRUCT_ORCHESTRATION_STORE env override win; otherwise the deployment mode
|
|
12
|
+
* decides (solo→filesystem, team/enterprise→postgres). When the chosen backend
|
|
13
|
+
* is unavailable (sqlite on Node <22.5, or postgres with no DATABASE_URL / null
|
|
14
|
+
* sql client) the resolver falls back to filesystem and records a warning rather
|
|
15
|
+
* than failing — durability of the default tier is never sacrificed to an
|
|
16
|
+
* optional one. The returned store is always async-uniform so callers await it
|
|
17
|
+
* regardless of backend; the filesystem and sqlite backends resolve synchronously
|
|
18
|
+
* under the hood, so the default Mode-A run record is byte-for-byte unchanged.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { getDeploymentMode } from '../deployment-mode.mjs';
|
|
22
|
+
import { createSqlClient } from '../storage/backend.mjs';
|
|
23
|
+
import { saveRun as fsSaveRun, loadRun as fsLoadRun, listRuns as fsListRuns } from './run-store.mjs';
|
|
24
|
+
import { sqliteAvailable, createSqliteRunStore } from './run-store-sqlite.mjs';
|
|
25
|
+
import { PostgresRunStore } from './run-store-postgres.mjs';
|
|
26
|
+
|
|
27
|
+
export const ORCHESTRATION_STORES = ['filesystem', 'sqlite', 'postgres'];
|
|
28
|
+
export const STORE_ENV_KEY = 'CONSTRUCT_ORCHESTRATION_STORE';
|
|
29
|
+
|
|
30
|
+
function filesystemStore(cwd) {
|
|
31
|
+
return {
|
|
32
|
+
kind: 'filesystem',
|
|
33
|
+
saveRun: (run) => fsSaveRun(cwd, run),
|
|
34
|
+
loadRun: (runId) => fsLoadRun(cwd, runId),
|
|
35
|
+
listRuns: (opts) => fsListRuns(cwd, opts),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function sqliteStore(cwd) {
|
|
40
|
+
const store = createSqliteRunStore({ cwd });
|
|
41
|
+
return {
|
|
42
|
+
kind: 'sqlite',
|
|
43
|
+
saveRun: (run) => store.saveRun(run),
|
|
44
|
+
loadRun: (runId) => store.loadRun(runId),
|
|
45
|
+
listRuns: (opts) => store.listRuns(opts),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function postgresStore({ sql, project }) {
|
|
50
|
+
const store = new PostgresRunStore({ sql, project });
|
|
51
|
+
let ensured = null;
|
|
52
|
+
const ready = () => {
|
|
53
|
+
if (!ensured) ensured = store.ensureSchema();
|
|
54
|
+
return ensured;
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
kind: 'postgres',
|
|
58
|
+
saveRun: async (run) => { await ready(); return store.saveRun(run); },
|
|
59
|
+
loadRun: async (runId) => { await ready(); return store.loadRun(runId); },
|
|
60
|
+
listRuns: async (opts) => { await ready(); return store.listRuns(opts); },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function projectKey(config, cwd) {
|
|
65
|
+
return config?.deployment?.projectName || cwd || 'default';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function chooseBackend({ config, env, cwd }) {
|
|
69
|
+
const explicit = String(env?.[STORE_ENV_KEY] || config?.orchestration?.store || '').trim().toLowerCase();
|
|
70
|
+
if (ORCHESTRATION_STORES.includes(explicit)) return explicit;
|
|
71
|
+
const mode = getDeploymentMode(env, { cwd });
|
|
72
|
+
return mode === 'solo' ? 'filesystem' : 'postgres';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve a run store for the runtime.
|
|
77
|
+
*
|
|
78
|
+
* @param {object} opts
|
|
79
|
+
* @param {object} [opts.config] loaded project config (reads orchestration.store)
|
|
80
|
+
* @param {Record<string,string>} [opts.env]
|
|
81
|
+
* @param {string} [opts.cwd]
|
|
82
|
+
* @returns {{ store: {kind:string, saveRun:Function, loadRun:Function, listRuns:Function}, backend: string, warnings: string[] }}
|
|
83
|
+
*/
|
|
84
|
+
export function resolveRunStore({ config = {}, env = process.env, cwd = process.cwd() } = {}) {
|
|
85
|
+
const warnings = [];
|
|
86
|
+
const requested = chooseBackend({ config, env, cwd });
|
|
87
|
+
|
|
88
|
+
if (requested === 'sqlite') {
|
|
89
|
+
if (sqliteAvailable()) return { store: sqliteStore(cwd), backend: 'sqlite', warnings };
|
|
90
|
+
warnings.push('orchestration.store "sqlite" requires Node >=22.5 (node:sqlite unavailable); falling back to filesystem.');
|
|
91
|
+
return { store: filesystemStore(cwd), backend: 'filesystem', warnings };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (requested === 'postgres') {
|
|
95
|
+
const sql = createSqlClient(env);
|
|
96
|
+
if (sql) return { store: postgresStore({ sql, project: projectKey(config, cwd) }), backend: 'postgres', warnings };
|
|
97
|
+
warnings.push('orchestration.store "postgres" requires a reachable DATABASE_URL; falling back to filesystem.');
|
|
98
|
+
return { store: filesystemStore(cwd), backend: 'filesystem', warnings };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { store: filesystemStore(cwd), backend: 'filesystem', warnings };
|
|
102
|
+
}
|