@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.
Files changed (124) hide show
  1. package/README.md +23 -5
  2. package/bin/construct +194 -16
  3. package/bin/construct-postinstall.mjs +25 -15
  4. package/lib/acp/server.mjs +113 -0
  5. package/lib/agent-instructions/inject.mjs +94 -0
  6. package/lib/auto-docs.mjs +10 -2
  7. package/lib/cli-commands.mjs +44 -16
  8. package/lib/comment-lint.mjs +115 -0
  9. package/lib/completions.mjs +7 -1
  10. package/lib/config/schema.mjs +22 -0
  11. package/lib/decisions/enforced-baseline.json +3 -0
  12. package/lib/docs-verify.mjs +15 -18
  13. package/lib/doctor/cli.mjs +8 -1
  14. package/lib/document-export.mjs +124 -0
  15. package/lib/document-ingest.mjs +13 -4
  16. package/lib/embed/daemon.mjs +1 -1
  17. package/lib/embed/inbox.mjs +25 -7
  18. package/lib/embedded-contract/triage.mjs +24 -2
  19. package/lib/embedded-contract/workflow-invoke.mjs +21 -0
  20. package/lib/features.mjs +11 -11
  21. package/lib/git-hooks-path.mjs +61 -0
  22. package/lib/home-namespace.mjs +60 -0
  23. package/lib/hooks/ci-status-check.mjs +62 -40
  24. package/lib/hooks/orchestration-dispatch-guard.mjs +153 -0
  25. package/lib/hooks/pre-push-gate.mjs +15 -6
  26. package/lib/hooks/stop-notify.mjs +32 -17
  27. package/lib/hooks/stop-typecheck.mjs +7 -2
  28. package/lib/host-capabilities.mjs +24 -8
  29. package/lib/host-disposition.mjs +76 -0
  30. package/lib/ingest/provider-extract.mjs +1 -1
  31. package/lib/ingest/strategy.mjs +35 -3
  32. package/lib/init-docs.mjs +1 -1
  33. package/lib/init-unified.mjs +320 -219
  34. package/lib/init-update.mjs +4 -84
  35. package/lib/init.mjs +9 -25
  36. package/lib/install/stage-project.mjs +8 -2
  37. package/lib/intent-classifier.mjs +1 -1
  38. package/lib/knowledge/search.mjs +52 -3
  39. package/lib/mcp/server.mjs +57 -14
  40. package/lib/mcp/tools/memory.mjs +2 -2
  41. package/lib/mcp/tools/orchestration-run.mjs +125 -0
  42. package/lib/model-registry.mjs +40 -33
  43. package/lib/observation-store.mjs +6 -2
  44. package/lib/opencode-config.mjs +1 -1
  45. package/lib/orchestration/events.mjs +66 -0
  46. package/lib/orchestration/run-store-postgres.mjs +85 -0
  47. package/lib/orchestration/run-store-sqlite.mjs +122 -0
  48. package/lib/orchestration/runtime.mjs +188 -50
  49. package/lib/orchestration/store.mjs +102 -0
  50. package/lib/orchestration/worker.mjs +215 -0
  51. package/lib/orchestration-policy.mjs +27 -3
  52. package/lib/parity.mjs +80 -26
  53. package/lib/policy/unified-gates.mjs +96 -0
  54. package/lib/project-init-shared.mjs +0 -173
  55. package/lib/reconcile/adapter-prune.mjs +105 -0
  56. package/lib/reconcile/agent-instructions-rewrap.mjs +98 -0
  57. package/lib/reconcile/gitignore-coverage.mjs +88 -0
  58. package/lib/reconcile/index.mjs +171 -0
  59. package/lib/reconcile/legacy-doctrine-strip.mjs +139 -0
  60. package/lib/reconcile/legacy-guide-decommit.mjs +67 -0
  61. package/lib/reconcile/legacy-skills-cleanup.mjs +200 -0
  62. package/lib/reconcile/mcp-entry-reconcile.mjs +142 -0
  63. package/lib/reconcile/postgres-namespace.mjs +102 -0
  64. package/lib/runtime/uv-bootstrap.mjs +27 -3
  65. package/lib/schema-infer.mjs +16 -2
  66. package/lib/server/csrf.mjs +14 -2
  67. package/lib/server/index.mjs +95 -0
  68. package/lib/service-manager.mjs +39 -14
  69. package/lib/setup-prompts.mjs +2 -1
  70. package/lib/setup.mjs +165 -141
  71. package/lib/storage/backend.mjs +14 -0
  72. package/lib/storage/unified-storage.mjs +550 -0
  73. package/lib/template-registry.mjs +73 -0
  74. package/lib/term-format.mjs +75 -0
  75. package/lib/uninstall/uninstall.mjs +180 -7
  76. package/package.json +2 -2
  77. package/personas/construct.md +7 -8
  78. package/platforms/claude/settings.template.json +30 -4
  79. package/platforms/opencode/config.template.json +2 -2
  80. package/rules/common/neurodivergent-output.md +66 -0
  81. package/rules/common/tool-invisibility.md +37 -0
  82. package/scripts/sync-specialists.mjs +381 -95
  83. package/skills/operating/orchestration-reference.md +2 -16
  84. package/specialists/policy-inventory.json +14 -0
  85. package/specialists/prompts/cx-accessibility.md +2 -6
  86. package/specialists/prompts/cx-ai-engineer.md +0 -4
  87. package/specialists/prompts/cx-architect.md +3 -5
  88. package/specialists/prompts/cx-business-strategist.md +0 -5
  89. package/specialists/prompts/cx-data-analyst.md +0 -4
  90. package/specialists/prompts/cx-data-engineer.md +0 -4
  91. package/specialists/prompts/cx-debugger.md +2 -6
  92. package/specialists/prompts/cx-designer.md +0 -8
  93. package/specialists/prompts/cx-devil-advocate.md +2 -2
  94. package/specialists/prompts/cx-docs-keeper.md +0 -13
  95. package/specialists/prompts/cx-engineer.md +0 -13
  96. package/specialists/prompts/cx-evaluator.md +2 -2
  97. package/specialists/prompts/cx-explorer.md +4 -5
  98. package/specialists/prompts/cx-legal-compliance.md +4 -5
  99. package/specialists/prompts/cx-operations.md +0 -5
  100. package/specialists/prompts/cx-orchestrator.md +0 -4
  101. package/specialists/prompts/cx-platform-engineer.md +0 -8
  102. package/specialists/prompts/cx-product-manager.md +0 -8
  103. package/specialists/prompts/cx-qa.md +2 -11
  104. package/specialists/prompts/cx-rd-lead.md +0 -5
  105. package/specialists/prompts/cx-release-manager.md +0 -8
  106. package/specialists/prompts/cx-researcher.md +5 -29
  107. package/specialists/prompts/cx-reviewer.md +2 -6
  108. package/specialists/prompts/cx-security.md +2 -11
  109. package/specialists/prompts/cx-sre.md +0 -15
  110. package/specialists/prompts/cx-test-automation.md +0 -4
  111. package/specialists/prompts/cx-trace-reviewer.md +2 -2
  112. package/specialists/prompts/cx-ux-researcher.md +0 -4
  113. package/specialists/registry.json +29 -29
  114. package/templates/distribution/run.mjs +36 -7
  115. package/templates/docs/accessibility-audit.md +56 -0
  116. package/templates/docs/architecture-review.md +59 -0
  117. package/templates/docs/code-review-report.md +46 -0
  118. package/templates/docs/construct_guide.md +14 -14
  119. package/templates/docs/debug-investigation.md +53 -0
  120. package/templates/docs/qa-report.md +48 -0
  121. package/templates/docs/security-audit-report.md +48 -0
  122. package/templates/docs/task-packet.md +49 -0
  123. package/templates/docs/verdict.md +40 -0
  124. package/lib/server/static/index.html +0 -1
@@ -1,25 +1,32 @@
1
1
  /**
2
- * lib/orchestration/runtime.mjs — Construct-owned local orchestration runtime (Mode-A).
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 is the
8
- * Mode-A (zero-dependency, single-process, filesystem-backed) slice of that
9
- * runtime: it intakes a request, plans/decomposes it into a sequenced specialist
10
- * chain (reusing the orchestration-policy planner), resolves the execution-
11
- * capability contract (reusing resolveExecution), persists a durable run + task
12
- * lifecycle (run-store), and emits lifecycle traces.
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
- * Honesty boundary (ADR-0020, extending ADR-0019): the Mode-A worker backend is
15
- * `inline` it OWNS planning, sequencing, handoff state, persistence, and
16
- * observability, and PREPARES each specialist task (role, reason, handoff
17
- * contract) for a downstream executor. It does NOT itself perform specialist LLM
18
- * reasoning; a provider-backed worker backend is a separate, later backend. The
19
- * run reports `workerBackend` and per-task `executor` so a host can never mistake
20
- * a prepared task for executed specialist output. When the execution contract
21
- * resolves to prompt-only or host-direct, Construct owns no specialist task
22
- * sequence and the run records that explicitly rather than implying orchestration.
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 { saveRun, loadRun, listRuns } from './run-store.mjs';
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 = ['inline'];
34
- export const DEFAULT_WORKER_BACKEND = 'inline';
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 = 'Mode-A inline 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.';
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: DEFAULT_WORKER_BACKEND,
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(cwd, run);
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 inline (Mode-A) worker backend: advance each
158
- * task queued running prepared, persisting after each transition so the run
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 run = loadRun(cwd, runId);
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(cwd, run);
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
- task.executor = 'inline:prepared';
185
- task.status = 'prepared';
186
- task.finishedAt = new Date().toISOString();
187
- emitTraceEvent({ rootDir: cwd, env, traceId: run.traceId, spanId: newSpanId(), eventType: 'worker.completed', role: task.role, taskId: task.id, metadata: { runId, status: 'prepared' } });
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(cwd, run);
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(cwd, run);
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
- return loadRun(cwd, runId);
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
- return listRuns(cwd, opts);
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
+ }