@pleri/olam-cli 0.1.148 → 0.1.150

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 (86) hide show
  1. package/dist/agent-stream/agent-sdk-to-chunks.js +276 -0
  2. package/dist/agent-stream/agent-stream-launch.js +348 -0
  3. package/dist/agent-stream/chunks-subscriber-transport.js +262 -0
  4. package/dist/agent-stream/codex-runner.js +188 -0
  5. package/dist/agent-stream/driver-runner.js +347 -0
  6. package/dist/agent-stream/operator-subscription.js +179 -0
  7. package/dist/commands/create.d.ts.map +1 -1
  8. package/dist/commands/create.js +39 -0
  9. package/dist/commands/create.js.map +1 -1
  10. package/dist/commands/doctor.d.ts +23 -0
  11. package/dist/commands/doctor.d.ts.map +1 -1
  12. package/dist/commands/doctor.js +77 -3
  13. package/dist/commands/doctor.js.map +1 -1
  14. package/dist/commands/init.d.ts +46 -0
  15. package/dist/commands/init.d.ts.map +1 -1
  16. package/dist/commands/init.js +90 -0
  17. package/dist/commands/init.js.map +1 -1
  18. package/dist/commands/kg-build.d.ts +23 -0
  19. package/dist/commands/kg-build.d.ts.map +1 -1
  20. package/dist/commands/kg-build.js +104 -2
  21. package/dist/commands/kg-build.js.map +1 -1
  22. package/dist/commands/restart.d.ts +18 -0
  23. package/dist/commands/restart.d.ts.map +1 -0
  24. package/dist/commands/restart.js +113 -0
  25. package/dist/commands/restart.js.map +1 -0
  26. package/dist/commands/setup-linux-gate.d.ts +26 -0
  27. package/dist/commands/setup-linux-gate.d.ts.map +1 -0
  28. package/dist/commands/setup-linux-gate.js +42 -0
  29. package/dist/commands/setup-linux-gate.js.map +1 -0
  30. package/dist/commands/setup-metrics.d.ts +26 -0
  31. package/dist/commands/setup-metrics.d.ts.map +1 -0
  32. package/dist/commands/setup-metrics.js +57 -0
  33. package/dist/commands/setup-metrics.js.map +1 -0
  34. package/dist/commands/setup-phase-5a-skill-source.d.ts +68 -0
  35. package/dist/commands/setup-phase-5a-skill-source.d.ts.map +1 -0
  36. package/dist/commands/setup-phase-5a-skill-source.js +196 -0
  37. package/dist/commands/setup-phase-5a-skill-source.js.map +1 -0
  38. package/dist/commands/setup-phase-5b-project-sweep.d.ts +38 -0
  39. package/dist/commands/setup-phase-5b-project-sweep.d.ts.map +1 -0
  40. package/dist/commands/setup-phase-5b-project-sweep.js +176 -0
  41. package/dist/commands/setup-phase-5b-project-sweep.js.map +1 -0
  42. package/dist/commands/setup.d.ts +19 -0
  43. package/dist/commands/setup.d.ts.map +1 -1
  44. package/dist/commands/setup.js +22 -0
  45. package/dist/commands/setup.js.map +1 -1
  46. package/dist/commands/skills-10x.d.ts +23 -0
  47. package/dist/commands/skills-10x.d.ts.map +1 -0
  48. package/dist/commands/skills-10x.js +308 -0
  49. package/dist/commands/skills-10x.js.map +1 -0
  50. package/dist/image-digests.json +7 -7
  51. package/dist/index.js +17873 -15826
  52. package/dist/index.js.map +1 -1
  53. package/dist/lib/build-if-stale.d.ts +33 -0
  54. package/dist/lib/build-if-stale.d.ts.map +1 -0
  55. package/dist/lib/build-if-stale.js +156 -0
  56. package/dist/lib/build-if-stale.js.map +1 -0
  57. package/dist/lib/bundle-freshness.d.ts +57 -0
  58. package/dist/lib/bundle-freshness.d.ts.map +1 -0
  59. package/dist/lib/bundle-freshness.js +223 -0
  60. package/dist/lib/bundle-freshness.js.map +1 -0
  61. package/dist/lib/bundle-source.d.ts +52 -0
  62. package/dist/lib/bundle-source.d.ts.map +1 -0
  63. package/dist/lib/bundle-source.js +83 -0
  64. package/dist/lib/bundle-source.js.map +1 -0
  65. package/dist/lib/manifest-refresh.d.ts +34 -0
  66. package/dist/lib/manifest-refresh.d.ts.map +1 -1
  67. package/dist/lib/manifest-refresh.js +66 -0
  68. package/dist/lib/manifest-refresh.js.map +1 -1
  69. package/dist/lib/upgrade-kubernetes.d.ts +17 -1
  70. package/dist/lib/upgrade-kubernetes.d.ts.map +1 -1
  71. package/dist/lib/upgrade-kubernetes.js +125 -1
  72. package/dist/lib/upgrade-kubernetes.js.map +1 -1
  73. package/dist/mcp-server.js +84 -58
  74. package/host-cp/compose.yaml +6 -0
  75. package/host-cp/k8s/manifests/30-configmap.yaml +6 -0
  76. package/host-cp/k8s/manifests/50-deployment.yaml +46 -9
  77. package/host-cp/k8s/manifests/auth-service/50-deployment.yaml +7 -4
  78. package/host-cp/k8s/manifests/kg-service/50-deployment.yaml +1 -1
  79. package/host-cp/k8s/manifests/mcp-auth-service/50-deployment.yaml +7 -4
  80. package/host-cp/k8s/manifests/memory-service/50-deployment.yaml +6 -1
  81. package/host-cp/src/agent-runtime-trigger.mjs +7 -5
  82. package/host-cp/src/plan-chat-secret.mjs +13 -2
  83. package/host-cp/src/plan-chat-service.mjs +94 -12
  84. package/host-cp/src/server.mjs +19 -7
  85. package/host-cp/src/upgrade-spawner.mjs +10 -5
  86. package/package.json +4 -2
@@ -0,0 +1,347 @@
1
+ /**
2
+ * driver-runner.ts — Phase B B5 (minimum-demo cut) driver hot-loop.
3
+ *
4
+ * Composes the existing primitives into a runnable agent process:
5
+ * transport (B1) → operator-pickup filter (C6) → SDK query → adapter (C1) → POST chunks
6
+ *
7
+ * Entry point: spawned as a child of B6 supervisor; reads env for
8
+ * HOST_CP_URL / HOST_CP_BEARER / WORLD_ID / SESSION_ID /
9
+ * DRIVER_SYSTEM_PROMPT_APPEND (legacy: DRIVER_SYSTEM_PROMPT — falls back to
10
+ * the new name post-`olam-sdk-latency-tuning` Phase A) /
11
+ * AGENT_DRIVER_TOOLS_ENABLED (set to "1" to re-enable Bash/Edit/Write/Read/
12
+ * Glob/Grep/Task for the conversational driver; default off per Phase A).
13
+ *
14
+ * Demo-cut simplifications (per minimum-demo decision; full contract
15
+ * deferred to follow-up Phase B B5-full):
16
+ * - lastConsumedSeq is IN-MEMORY only (no chunks_subscriber_state
17
+ * server-side table). Restart loses cursor; for demo this means a
18
+ * re-spawn replays operator chunks from `seq=0` (the cold-start
19
+ * sentinel from `interject` Fallback). Idempotency at the adapter
20
+ * level (PK message_id+seq) keeps the substrate clean.
21
+ * - No host-cp REST endpoint for cursor write/read.
22
+ * - No PID-recycle-safe cursor file naming (cursor is in process memory only).
23
+ * - Pure shared-secret auth (the bearer env value); JWT scope-claim
24
+ * migration is B9 / post-demo.
25
+ *
26
+ * Iteration model:
27
+ * - Wait for the next operator chunk via transport (or boot-time
28
+ * `dispatch` chunk on first poll).
29
+ * - Build the iteration's prompt prefix from accumulated pickups.
30
+ * - Invoke SDK query() with the prefix + a small system prompt.
31
+ * - Stream SDK output via streamSdkToChunks back to host-cp.
32
+ * - Loop until SIGTERM.
33
+ *
34
+ * Source: docs/design/olam-plan-chat-agent-runtime.md `interject` +
35
+ * `subscriber` sections, minimum-demo cut.
36
+ */
37
+ import { randomUUID } from 'node:crypto';
38
+ import { makeHostCpChunkPoster, streamMultiTurnSdkToChunks, } from './agent-sdk-to-chunks.js';
39
+ import { subscribeToChunks } from './chunks-subscriber-transport.js';
40
+ import { filterOperatorPickups, pickupConsumedKey, pickupsToPromptPrefix, } from './operator-subscription.js';
41
+ /**
42
+ * Default `append` text for the Claude Code preset system prompt.
43
+ * Exported so the regression test can assert non-empty + coordinator-shaped
44
+ * content (Phase A.1 adv-7 follow-up: the snapshot test alone doesn't catch
45
+ * a "default emptied during refactor" regression).
46
+ */
47
+ export const DEFAULT_SYSTEM_PROMPT_APPEND = `You are the driver agent inside an olam plan-chat session. The operator may interject mid-iteration; their interjects appear as a prefix to your next prompt. Acknowledge interjects explicitly in your response (e.g. "noted — switching to retry middleware") and continue the work. Keep responses focused and under 200 tokens unless deep analysis is required. You are a conversational coordinator — do not invoke filesystem/code tools unless explicitly asked.`;
48
+ /**
49
+ * Builds the SDK options object used for every `query()` call. Exported
50
+ * for snapshot testing (Phase A A2) — the snapshot is the regression
51
+ * gate that prevents the pre-Phase-A malformation from recurring.
52
+ *
53
+ * The `abortController` field is NOT included here; the caller adds it
54
+ * at the call site so this helper stays deterministic + snapshot-safe.
55
+ *
56
+ * Phase A levers applied here:
57
+ * - `systemPrompt: { type: 'preset', preset: 'claude_code', append: <hint> }` —
58
+ * fixes the pre-Phase-A `{type:'preset', preset:<custom-string>}`
59
+ * malformation; the SDK runtime extracts only `append` on the preset
60
+ * branch (Pass 2 source-read at sdk.mjs:847641; Pass 3 runtime probe).
61
+ * - `allowedTools: []` — empty allowlist filters out ALL tools from the
62
+ * Claude Code preset. Allowlist (not denylist — corrects adv-1 from
63
+ * Phase A CP3): the SDK's tool surface grows over time; allowlist
64
+ * means new tools stay disabled by default until explicitly opted in,
65
+ * which preserves the "coordinator-not-coder" seam.
66
+ * - `effort: 'low'` + `thinking: { type: 'disabled' }` — driver doesn't
67
+ * do deep reasoning; sub-agents inherit their own config.
68
+ * - `model: 'claude-sonnet-4-6'` — Pass 4 OQ7 resolution. Driver
69
+ * coordinator runs Sonnet, not Opus. (`claude-sonnet-4-6` is the
70
+ * SDK's documented model-ID example shape per sdk.d.ts:1472,1626.)
71
+ *
72
+ * `permissionMode` is intentionally omitted: with `allowedTools: []` the
73
+ * permission decision is moot (zero tools to permission). The pre-fix
74
+ * `permissionMode: 'dontAsk'` was kept as belt-and-suspenders but its
75
+ * SDK semantic — "deny if not pre-approved" — is exactly what an empty
76
+ * allowlist already enforces (corrects adv-2).
77
+ *
78
+ * Escape hatch: `AGENT_DRIVER_TOOLS_ENABLED=1` removes the allowedTools
79
+ * field entirely so the SDK falls back to the preset's full tool set.
80
+ * Documented per plan T2 mitigation. Note the env-var is read from the
81
+ * driver child process; the supervisor propagates its env, so setting
82
+ * this on the supervisor enables it FLEET-WIDE (adv-6 — by design).
83
+ */
84
+ export function buildSdkOptions(input) {
85
+ const driverToolsEnabled = process.env.AGENT_DRIVER_TOOLS_ENABLED === '1';
86
+ return {
87
+ systemPrompt: {
88
+ type: 'preset',
89
+ preset: 'claude_code',
90
+ ...(input.systemPromptAppend ? { append: input.systemPromptAppend } : {}),
91
+ },
92
+ // Empty allowlist = strict coordinator (no tools); undefined = full preset tools.
93
+ ...(driverToolsEnabled ? {} : { allowedTools: [] }),
94
+ effort: 'low',
95
+ thinking: { type: 'disabled' },
96
+ model: 'claude-sonnet-4-6',
97
+ };
98
+ }
99
+ /**
100
+ * Factory: takes the raw SDK `query` function, returns the `SdkQueryFn`
101
+ * that `runDriver` consumes. Extracted from `main()` so the call-site
102
+ * composition (buildSdkOptions output + abortController forwarding) is
103
+ * directly testable with a mocked `query` (Phase A.1 adv-5 follow-up).
104
+ *
105
+ * Behavior contract:
106
+ * - `options` passed to `query` is `buildSdkOptions(...)` output spread
107
+ * FIRST, then `abortController` added LAST (cannot be clobbered).
108
+ * - `abortController` is a fresh AbortController per call; the input
109
+ * `abortSignal` is forwarded via `addEventListener('abort', ...)`.
110
+ * - The `prompt` is passed through unchanged.
111
+ */
112
+ export function createSdkQuery(query) {
113
+ return ({ prompt, abortSignal, systemPromptAppend }) => {
114
+ // eslint-disable-next-line no-console
115
+ console.error(`[driver] sdkQuery fired (prompt: ${typeof prompt === 'string' ? `${prompt.length} chars` : 'AsyncIterable<SdkUserMessage>'})`);
116
+ // Forward the runDriver-owned abortSignal to a fresh AbortController
117
+ // (the SDK's Options.abortController expects an AbortController, not
118
+ // a bare AbortSignal). One per query() call — cheap; no global state.
119
+ const sdkAbortController = new AbortController();
120
+ if (abortSignal) {
121
+ if (abortSignal.aborted)
122
+ sdkAbortController.abort();
123
+ else
124
+ abortSignal.addEventListener('abort', () => sdkAbortController.abort(), {
125
+ once: true,
126
+ });
127
+ }
128
+ const options = {
129
+ ...buildSdkOptions({ systemPromptAppend }),
130
+ abortController: sdkAbortController,
131
+ };
132
+ return query({ prompt, options });
133
+ };
134
+ }
135
+ /**
136
+ * Run the driver hot-loop. Returns a handle to stop it cleanly.
137
+ *
138
+ * The driver doesn't terminate on its own — it runs until aborted
139
+ * (SIGTERM via the supervisor → the abortSignal here).
140
+ */
141
+ export function runDriver(opts) {
142
+ const { hostCpUrl, bearer, worldId, sessionId, systemPromptAppend = DEFAULT_SYSTEM_PROMPT_APPEND, sdkQuery, abortSignal: externalSignal, subscribeImpl = subscribeToChunks, postChunkImpl = (url, b) => makeHostCpChunkPoster({ sidecarUrl: url, bearer: b }), } = opts;
143
+ const controller = new AbortController();
144
+ if (externalSignal) {
145
+ if (externalSignal.aborted)
146
+ controller.abort();
147
+ else
148
+ externalSignal.addEventListener('abort', () => controller.abort(), { once: true });
149
+ }
150
+ const signal = controller.signal;
151
+ const postChunk = postChunkImpl(hostCpUrl, bearer);
152
+ // In-memory dedup set (Phase D D6). Replaces the prior `lastConsumedSeq`
153
+ // monotonic-cursor model which dropped every operator chunk after the
154
+ // first one in a session (every SPA dispatch has seq=0 with its own
155
+ // message_id; `seq <= lastConsumedSeq` rejected 0 <= 0). Set keyed on
156
+ // `${messageId}:${seq}` matches the substrate's PK shape exactly.
157
+ // Per crash-resume: process restart loses the set; operator chunks are
158
+ // re-presented; substrate idempotency (PK message_id+seq) keeps the
159
+ // adapter writes clean.
160
+ const consumedKeys = new Set();
161
+ // Pending pickups accumulate from the transport; drained by the
162
+ // userMessageStream generator each time it wakes.
163
+ const pendingPickups = [];
164
+ let iterationWakeup = null;
165
+ // P2 (plan risk candidate): bound the queue to prevent memory leak in
166
+ // long-lived iteration (operator pickups accumulate over many-hour
167
+ // sessions). Above this limit, drop the oldest pickups + log + emit
168
+ // system error chunk so the operator sees something went wrong.
169
+ const PENDING_PICKUPS_MAX = 100;
170
+ const transport = subscribeImpl({
171
+ baseUrl: hostCpUrl,
172
+ bearer,
173
+ worldId,
174
+ sessionId,
175
+ abortSignal: signal,
176
+ onChunk: (chunk) => {
177
+ const pickups = filterOperatorPickups([chunk], consumedKeys);
178
+ if (pickups.length === 0)
179
+ return;
180
+ pendingPickups.push(...pickups);
181
+ if (pendingPickups.length > PENDING_PICKUPS_MAX) {
182
+ const overflow = pendingPickups.length - PENDING_PICKUPS_MAX;
183
+ pendingPickups.splice(0, overflow);
184
+ // eslint-disable-next-line no-console
185
+ console.error(`[driver] pendingPickups overflow: dropped ${overflow} oldest pickups (queue cap=${PENDING_PICKUPS_MAX})`);
186
+ }
187
+ for (const p of pickups) {
188
+ consumedKeys.add(pickupConsumedKey(p));
189
+ }
190
+ if (iterationWakeup) {
191
+ iterationWakeup();
192
+ iterationWakeup = null;
193
+ }
194
+ },
195
+ });
196
+ // Convert a batch of pickups into ONE SdkUserMessage (preserves the
197
+ // pre-refactor batched-prompt behavior: multiple operator chunks
198
+ // arriving in the same wakeup window combine into one user turn).
199
+ function batchToUserMessage(batch) {
200
+ const content = pickupsToPromptPrefix(batch);
201
+ if (!content)
202
+ return null;
203
+ return {
204
+ type: 'user',
205
+ message: { role: 'user', content },
206
+ parent_tool_use_id: null,
207
+ };
208
+ }
209
+ // Iteration loop runs in parallel with the transport. NEW architecture
210
+ // (Phase B B2): opens ONE long-lived sdkQuery with an AsyncIterable
211
+ // prompt that pulls from pendingPickups. Each `'result'` boundary in
212
+ // the SDK's response stream allocates a fresh messageId via the
213
+ // turn-splitter helper.
214
+ const iterationLoop = async () => {
215
+ if (signal.aborted)
216
+ return;
217
+ async function* userMessageStream() {
218
+ while (!signal.aborted) {
219
+ if (pendingPickups.length === 0) {
220
+ await new Promise((resolve) => {
221
+ iterationWakeup = resolve;
222
+ const abortHandler = () => {
223
+ iterationWakeup = null;
224
+ resolve();
225
+ };
226
+ signal.addEventListener('abort', abortHandler, { once: true });
227
+ });
228
+ if (signal.aborted)
229
+ return;
230
+ if (pendingPickups.length === 0)
231
+ continue;
232
+ }
233
+ const batch = pendingPickups.splice(0);
234
+ const userMessage = batchToUserMessage(batch);
235
+ if (userMessage !== null)
236
+ yield userMessage;
237
+ }
238
+ }
239
+ try {
240
+ const messages = sdkQuery({
241
+ prompt: userMessageStream(),
242
+ abortSignal: signal,
243
+ systemPromptAppend,
244
+ });
245
+ await streamMultiTurnSdkToChunks({
246
+ messages,
247
+ worldId,
248
+ sessionId,
249
+ allocateMessageId: () => `driver-${randomUUID()}`,
250
+ postChunk,
251
+ });
252
+ }
253
+ catch (err) {
254
+ if (signal.aborted)
255
+ return;
256
+ // eslint-disable-next-line no-console
257
+ console.error('[driver] iteration failed:', err);
258
+ try {
259
+ await postChunk({
260
+ world_id: worldId,
261
+ session_id: sessionId,
262
+ message_id: `driver-error-${randomUUID()}`,
263
+ seq: 0,
264
+ role: 'system',
265
+ kind: 'result',
266
+ chunk: JSON.stringify({ error: 'iteration failed', detail: String(err) }),
267
+ created_at: new Date().toISOString(),
268
+ });
269
+ }
270
+ catch {
271
+ // swallow — postChunk failure on top of iteration failure is unrecoverable
272
+ }
273
+ }
274
+ };
275
+ const done = Promise.all([transport.done, iterationLoop()]).then(() => undefined);
276
+ return {
277
+ stop: async () => {
278
+ controller.abort();
279
+ try {
280
+ await done;
281
+ }
282
+ catch {
283
+ // swallow — caller can inspect done directly if needed
284
+ }
285
+ },
286
+ done,
287
+ };
288
+ }
289
+ /**
290
+ * CLI entry point when this module is run directly (via supervisor fork).
291
+ * Reads env, validates, calls runDriver, installs SIGTERM handler.
292
+ */
293
+ export async function main() {
294
+ const hostCpUrl = process.env.HOST_CP_URL;
295
+ const bearer = process.env.HOST_CP_BEARER;
296
+ const worldId = process.env.WORLD_ID;
297
+ const sessionId = process.env.SESSION_ID;
298
+ if (!hostCpUrl || !bearer || !worldId || !sessionId) {
299
+ // eslint-disable-next-line no-console
300
+ console.error('[driver] missing required env: HOST_CP_URL, HOST_CP_BEARER, WORLD_ID, SESSION_ID');
301
+ process.exit(1);
302
+ }
303
+ // Lazy-load SDK so tests don't need the package installed.
304
+ const { query } = await import('@anthropic-ai/claude-agent-sdk');
305
+ const controller = new AbortController();
306
+ process.on('SIGTERM', () => controller.abort());
307
+ process.on('SIGINT', () => controller.abort());
308
+ // eslint-disable-next-line no-console
309
+ console.error(`[driver] starting · world=${worldId} session=${sessionId} ` +
310
+ `hostCp=${hostCpUrl}`);
311
+ const handle = runDriver({
312
+ hostCpUrl,
313
+ bearer,
314
+ worldId,
315
+ sessionId,
316
+ // Backwards-compat: read both env var names. `DRIVER_SYSTEM_PROMPT_APPEND`
317
+ // is the new (post-Phase-A) name; `DRIVER_SYSTEM_PROMPT` is honored for
318
+ // operators with legacy supervisor configs.
319
+ systemPromptAppend: process.env.DRIVER_SYSTEM_PROMPT_APPEND ?? process.env.DRIVER_SYSTEM_PROMPT,
320
+ sdkQuery: createSdkQuery(query),
321
+ abortSignal: controller.signal,
322
+ });
323
+ try {
324
+ await handle.done;
325
+ // eslint-disable-next-line no-console
326
+ console.error('[driver] done.resolved — clean exit');
327
+ process.exit(0);
328
+ }
329
+ catch (err) {
330
+ // eslint-disable-next-line no-console
331
+ console.error('[driver] done.rejected — fatal:', err);
332
+ process.exit(1);
333
+ }
334
+ }
335
+ // Run main() only when invoked directly (not when imported as a module).
336
+ // The check uses `process.argv[1]` to compare against this module's path.
337
+ // Note: this is a Node-specific pattern; tests bypass it by importing runDriver.
338
+ if (typeof process !== 'undefined' &&
339
+ process.argv[1] &&
340
+ (process.argv[1].endsWith('driver-runner.js') || process.argv[1].endsWith('driver-runner.ts'))) {
341
+ main().catch((err) => {
342
+ // eslint-disable-next-line no-console
343
+ console.error('[driver] fatal:', err);
344
+ process.exit(1);
345
+ });
346
+ }
347
+ //# sourceMappingURL=driver-runner.js.map
@@ -0,0 +1,179 @@
1
+ /**
2
+ * operator-subscription.ts — Phase C C6 agent-as-subscriber primitive.
3
+ *
4
+ * Driver-agent's hot loop subscribes to operator chunks; each new operator
5
+ * chunk feeds the agent's next reasoning iteration as steer input. The
6
+ * substrate IS the orchestration channel — no RPC, no event bus, no
7
+ * "interject" API on the agent runtime: the agent simply reads chunks
8
+ * where `actor_type='operator' AND seq > last_consumed_seq`.
9
+ *
10
+ * This module ships the PURE-FUNCTION pattern for both the driver
11
+ * subscriber and other agent kinds (codex, lookouts). The devbox
12
+ * bootstrap launcher that spawns these processes is C+1 follow-up
13
+ * scope — the pure pattern is what's load-bearing for the seam.
14
+ *
15
+ * Architecture (per pass-7 plan body):
16
+ * - Driver in devbox container opens a shape subscription via
17
+ * `host.docker.internal:3112/v1/shape` (the host-cp sidecar from
18
+ * Phase B B1).
19
+ * - On each new operator chunk, the driver's NEXT reasoning iteration
20
+ * receives it as a prompt prefix.
21
+ * - Codex runs as a SEPARATE process with the same subscription
22
+ * pattern, but with a CONTENT FILTER (PR-draft regex).
23
+ * - Lookouts (Scout / PM / Brainstorm from PR #443) follow the
24
+ * same pattern with persona-specific trigger heuristics; emit
25
+ * SIGNALS (to plan_sidebar_signals) instead of chunks.
26
+ *
27
+ * Per OQ10 lean (a): driver + codex + lookouts all talk to host-cp's
28
+ * /v1/shape via `host.docker.internal:3112`. No in-container PGlite
29
+ * mirror; no LISTEN/NOTIFY direct.
30
+ *
31
+ * Source: docs/plans/olam-plan-chat-chunks-substrate/phase-c-tasks.md (C6)
32
+ */
33
+ /**
34
+ * Pure: derive the consumed-key for an operator pickup. The key is
35
+ * `${messageId}:${seq}` — the (PK) shape Postgres uses for chunks.
36
+ * Callers store these in a Set to dedup the operator stream.
37
+ */
38
+ export function pickupConsumedKey(p) {
39
+ return `${p.messageId}:${p.seq}`;
40
+ }
41
+ /**
42
+ * Pure: filter a chunk stream to operator chunks NOT YET CONSUMED.
43
+ * Used as the load-bearing primitive by the driver's hot loop —
44
+ * each iteration call materialises the next batch of operator pickups
45
+ * to feed into the next reasoning turn.
46
+ *
47
+ * `consumedKeys` is tracked by the caller (driver hot loop's local
48
+ * state). On first call (cold start), pass an empty Set to receive
49
+ * all historical operator chunks for the session.
50
+ *
51
+ * **History note** (Phase D D6 dedup fix): pre-fix this took a
52
+ * `lastConsumedSeq: number` and filtered with `c.seq <= lastConsumedSeq`.
53
+ * That broke in production because each SPA-dispatched operator chunk is
54
+ * a NEW message_id with `seq=0` (the chunks substrate scopes seq per
55
+ * message_id, not per session). Under that model, the second operator
56
+ * dispatch with `seq=0` was rejected because `0 <= 0`, silently dropping
57
+ * every operator chunk after the first one. Switched to message_id+seq
58
+ * Set dedup which matches the substrate's PK shape exactly.
59
+ */
60
+ export function filterOperatorPickups(chunks, consumedKeys) {
61
+ const out = [];
62
+ for (const c of chunks) {
63
+ if (c.actor_type !== 'operator')
64
+ continue;
65
+ const key = `${c.message_id}:${c.seq}`;
66
+ if (consumedKeys.has(key))
67
+ continue;
68
+ out.push({
69
+ messageId: c.message_id,
70
+ seq: c.seq,
71
+ content: c.chunk,
72
+ createdAt: c.created_at,
73
+ });
74
+ }
75
+ return out;
76
+ }
77
+ /**
78
+ * Pure: compose a prompt-prefix string from operator pickups. Each
79
+ * pickup becomes a `[operator]: ...` line; multiple pickups stack
80
+ * in seq order. The driver's next-iteration prompt prepends this
81
+ * prefix so the agent sees the operator's interjects without
82
+ * needing a custom message-injection hook in the SDK.
83
+ *
84
+ * Returns null when there are no pickups (skip the prefix; vanilla
85
+ * iteration).
86
+ */
87
+ export function pickupsToPromptPrefix(pickups) {
88
+ if (pickups.length === 0)
89
+ return null;
90
+ const sorted = [...pickups].sort((a, b) => a.seq - b.seq);
91
+ const lines = sorted.map((p) => `[operator]: ${p.content}`);
92
+ return lines.join('\n');
93
+ }
94
+ /**
95
+ * Pure: derive the next-iteration `consumedKeys` Set after processing
96
+ * a batch of pickups. The driver hot loop calls this after each
97
+ * iteration to update its local state. Returns a NEW Set (caller
98
+ * keeps the prior set immutable; cheap on the small operator volumes
99
+ * a single session produces).
100
+ */
101
+ export function advanceConsumedKeys(pickups, current) {
102
+ const next = new Set(current);
103
+ for (const p of pickups) {
104
+ next.add(pickupConsumedKey(p));
105
+ }
106
+ return next;
107
+ }
108
+ /**
109
+ * Pure: detect PR-draft patterns in driver chunks for codex pickup
110
+ * (Phase C C6 codex-runner trigger).
111
+ *
112
+ * Codex runs as a parallel process subscribing to driver chunks; on
113
+ * detecting "PR draft" / "draft PR" / "opening PR" / "PR title"
114
+ * patterns, codex generates an approval/rejection chunk. This pure
115
+ * helper isolates the trigger heuristic for unit-test coverage
116
+ * separate from the codex process's stateful execution loop.
117
+ */
118
+ export function detectCodexTrigger(chunk) {
119
+ if (chunk.actor_type !== 'agent')
120
+ return { matched: false };
121
+ const text = chunk.chunk;
122
+ const patterns = [
123
+ /\bdraft\s+pr\b/i,
124
+ /\bpr\s+draft\b/i,
125
+ /\bpr\s+title\b/i,
126
+ /\bopening\s+pr\b/i,
127
+ /\bcreate\s+(a\s+)?pull\s+request\b/i,
128
+ ];
129
+ for (const re of patterns) {
130
+ const m = re.exec(text);
131
+ if (m !== null) {
132
+ return { matched: true, matchedPattern: m[0] };
133
+ }
134
+ }
135
+ return { matched: false };
136
+ }
137
+ /**
138
+ * Pure: generate a codex approval/rejection chunk content for a
139
+ * matched PR-draft trigger. The codex runner uses this to compose
140
+ * the chunk it writes back via host-cp POST /v1/chunks (with
141
+ * actor_type='codex').
142
+ *
143
+ * The actual reasoning runs through Claude Agent SDK with a codex
144
+ * system prompt; this helper covers the deterministic boilerplate
145
+ * (decision + reasoning summary). Real codex output flows through
146
+ * streamSdkToChunks (Phase C C1's adapter).
147
+ */
148
+ export function codexApprovalDraft(opts) {
149
+ const verdict = opts.approved ? 'APPROVED' : 'BLOCKED';
150
+ return {
151
+ content: [
152
+ `[codex] ${verdict}`,
153
+ `Trigger: chunk ${opts.triggerChunk.message_id}#${opts.triggerChunk.seq}`,
154
+ `Reason: ${opts.reason}`,
155
+ ].join('\n'),
156
+ actorType: 'codex',
157
+ };
158
+ }
159
+ /**
160
+ * Pure: minimal v1 trigger heuristic for Scout (lookout persona). Fires
161
+ * on agent chunks containing "TODO" or "FIXME" or "XXX" markers as a
162
+ * conservative starting point. Real Scout uses LLM-based judgment;
163
+ * this v1 heuristic is the deterministic fallback for tests + the
164
+ * pattern other personas extend.
165
+ */
166
+ export const scoutTrigger = (chunk) => {
167
+ if (chunk.actor_type !== 'agent')
168
+ return { triggered: false };
169
+ const markers = /\b(TODO|FIXME|XXX|HACK)\b/.exec(chunk.chunk);
170
+ if (markers === null)
171
+ return { triggered: false };
172
+ return {
173
+ triggered: true,
174
+ urgency: 'p2',
175
+ reason: `marker ${markers[0]} surfaced`,
176
+ content: `Scout noted: ${markers[0]} in chunk ${chunk.message_id}#${chunk.seq}.`,
177
+ };
178
+ };
179
+ //# sourceMappingURL=operator-subscription.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/commands/create.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgDzC,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAsjBrD"}
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/commands/create.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmDzC,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA6lBrD"}
@@ -17,6 +17,9 @@ import { printError, printInfo, printHeader, printWarning } from '../output.js';
17
17
  import { probeHostCp, gatherProbeFailureDiagnostics, callHostCpProxy, openHostCpUrl } from './host-cp.js';
18
18
  import { readMemorySecretOrNull } from '../lib/memory-secret.js';
19
19
  import { registerAgentMemoryMcp } from '../lib/world-mcp-register.js';
20
+ import { buildIfStale } from '../lib/build-if-stale.js';
21
+ import { resolveBundleSource, BundleSourceNotFoundError } from '../lib/bundle-source.js';
22
+ import { isDevMode } from '../install-root.js';
20
23
  // Phase B1 — mirror of AGENTMEMORY_HOST_INTERNAL_URL in
21
24
  // packages/adapters/src/docker/container.ts. Hardcoded here for the
22
25
  // post-spawn MCP registration call so the host knows what URL was
@@ -283,6 +286,41 @@ export function registerCreate(program) {
283
286
  }
284
287
  }
285
288
  }
289
+ // C2 (olam-world-bundle-freshness): resolve the agent-stream bundle
290
+ // source. Handles both source-mode (OLAM_DEV=1 + packages/ sibling →
291
+ // auto-build if stale) and install-mode (CLI's own dist/agent-stream/).
292
+ let agentStreamDistPath;
293
+ try {
294
+ const bundleSource = resolveBundleSource();
295
+ if (bundleSource.mode === 'source') {
296
+ // B3 path: auto-build when dist is stale.
297
+ const bundleRepoRoot = resolveRepoRoot(process.cwd());
298
+ const buildResult = buildIfStale(bundleRepoRoot);
299
+ if (!buildResult.ok) {
300
+ printError('Agent-stream bundle build failed. Not creating world with a stale bundle.\n' +
301
+ 'Fix the build error and re-run `olam create`.');
302
+ process.exitCode = 1;
303
+ return;
304
+ }
305
+ if (buildResult.built) {
306
+ printInfo('Bundle', buildResult.message);
307
+ }
308
+ }
309
+ // Both modes: pass the resolved path so the docker provider bind-mounts
310
+ // the correct dist (source-mode: repo dist; install-mode: CLI dist).
311
+ agentStreamDistPath = bundleSource.path;
312
+ }
313
+ catch (err) {
314
+ if (err instanceof BundleSourceNotFoundError) {
315
+ // Graceful degradation: warn but do not block world create.
316
+ // The world will use the image-baked bundle as fallback.
317
+ printWarning(`agent-stream bundle path unresolved; world will use image-baked bundle.\n` +
318
+ ` ${err.message}`);
319
+ }
320
+ else {
321
+ throw err;
322
+ }
323
+ }
286
324
  const spinner = ora('Creating world...').start();
287
325
  try {
288
326
  const world = await ctx.worldManager.createWorld({
@@ -293,6 +331,7 @@ export function registerCreate(program) {
293
331
  branchName: opts.branch,
294
332
  planFile: opts.plan,
295
333
  noAuth: !opts.auth,
334
+ ...(agentStreamDistPath ? { agentStreamDistPath } : {}),
296
335
  // ADV-B3-LOW2: strict-boolean coercion. `?? false` would pass through
297
336
  // truthy non-booleans (e.g. if the flag's shape ever grows to
298
337
  // `--carry-uncommitted [mode]` where commander returns a string).