@pleri/olam-cli 0.1.170 → 0.1.173

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.
@@ -1,4 +1,4 @@
1
1
  {
2
- "bundledAt": "2026-05-24T03:12:41.846Z",
2
+ "bundledAt": "2026-05-24T11:22:38.644Z",
3
3
  "kgFirstSha": "29a9ccce1b115d049e375c4a90eb5cf7c123e610e2d0590270a4db2cdbc64a28"
4
4
  }
@@ -118,7 +118,7 @@ spec:
118
118
  # k3d), started by `olam upgrade` Step 0.7 — not inside this Pod.
119
119
  containers:
120
120
  - name: olam-host-cp
121
- image: ghcr.io/pleri/olam-host-cp@sha256:1206e857af61f8907d76d9324adbe8d2d5638a94fe2411c6713ffb4f570e8f58
121
+ image: ghcr.io/pleri/olam-host-cp@sha256:3043df80469fa58319de53688991c81575522beba48b7a1b6956d0e3f2d03b45
122
122
  imagePullPolicy: IfNotPresent
123
123
  securityContext:
124
124
  runAsNonRoot: true
@@ -70,7 +70,7 @@ spec:
70
70
  mountPath: /data
71
71
  containers:
72
72
  - name: olam-auth-service
73
- image: ghcr.io/pleri/olam-auth@sha256:2d32d178380641bcdae11f9ad05851238bd4b121adfc9638c8abed3b25467846
73
+ image: ghcr.io/pleri/olam-auth@sha256:2e40e0c1f0469331dfa98a2e194c922c710149d8d5d3816bc660e530be6a9b97
74
74
  imagePullPolicy: IfNotPresent
75
75
  securityContext:
76
76
  runAsNonRoot: true
@@ -61,7 +61,7 @@ spec:
61
61
  mountPath: /data
62
62
  containers:
63
63
  - name: olam-kg-service
64
- image: ghcr.io/pleri/olam-kg-service@sha256:ee636804b8cffd40a1fb75ba3f79cc0c30a17e89c9a135864567859ccdf895d7
64
+ image: ghcr.io/pleri/olam-kg-service@sha256:225dd3460bce1f2572e6076e55a875ff526a5217a2d8f311d14b6dee591e8a38
65
65
  imagePullPolicy: IfNotPresent
66
66
  securityContext:
67
67
  runAsNonRoot: true
@@ -68,7 +68,7 @@ spec:
68
68
  mountPath: /data
69
69
  containers:
70
70
  - name: olam-mcp-auth-service
71
- image: ghcr.io/pleri/olam-mcp-auth@sha256:07cdd816ac1d991c065f2936b142a5c6909da683d9a6d4efbe7fe66f0c811821
71
+ image: ghcr.io/pleri/olam-mcp-auth@sha256:f61bf653ada702d59aca0a309b224b01e0f151a89e01583a76f94d8604101d20
72
72
  imagePullPolicy: IfNotPresent
73
73
  securityContext:
74
74
  runAsNonRoot: true
@@ -70,7 +70,7 @@ spec:
70
70
  # bootstrap-placeholder comment + run `npm run refresh:manifest-digests`
71
71
  # once ghcr.io/pleri/olam-memory-service has a real published digest.
72
72
  # bootstrap-placeholder: pre-publish; refresh after first release
73
- image: ghcr.io/pleri/olam-memory-service@sha256:20443e8e6725151f7523a8a85c73c7449767782de1d03bb172ba395df19a0939
73
+ image: ghcr.io/pleri/olam-memory-service@sha256:86ce43a8bfec3edf0a9ac1aea63bf3ecd922209a7ab2a0f589ae9e1cedc0134a
74
74
  imagePullPolicy: IfNotPresent
75
75
  securityContext:
76
76
  runAsNonRoot: true
@@ -102,6 +102,58 @@ export async function createNdjsonSpanSink({
102
102
  };
103
103
  }
104
104
 
105
+ /**
106
+ * Subscribe an NDJSON sink to `@olam/auth-client`'s `betaResponseEmitter`.
107
+ * Each `beta-response` event becomes a `withCredential.beta-response` span
108
+ * with the beta payload exploded onto `attributes` — downstream `jq`
109
+ * consumers can query e.g.
110
+ *
111
+ * jq 'select(.name == "withCredential.beta-response")
112
+ * | {ts: .startedAt, cred: .attributes.credentialName,
113
+ * cache: .attributes.cacheStatus,
114
+ * thinking: .attributes.thinkingTokens,
115
+ * latencyMs: .durationMs}' ~/.olam/logs/host.trace.ndjson
116
+ *
117
+ * Wire is opt-in (call from server boot). Returns a detach function so the
118
+ * subscription can be removed in tests or on shutdown.
119
+ *
120
+ * Pure additive: spans flowing from other sources (docker lifecycle,
121
+ * plan-orchestrator, etc.) are unaffected.
122
+ */
123
+ export function attachBetaResponseEvents({ sink, emitter }) {
124
+ if (!sink || typeof sink.recordSpan !== 'function') {
125
+ throw new Error('attachBetaResponseEvents: sink.recordSpan required');
126
+ }
127
+ if (!emitter || typeof emitter.on !== 'function') {
128
+ throw new Error('attachBetaResponseEvents: emitter.on required');
129
+ }
130
+
131
+ const handler = (info) => {
132
+ const now = Date.now();
133
+ const latency = typeof info?.latencyMs === 'number' ? info.latencyMs : 0;
134
+ sink.recordSpan({
135
+ name: 'withCredential.beta-response',
136
+ startedAt: now - latency,
137
+ endedAt: now,
138
+ attributes: {
139
+ credentialName: info?.credentialName ?? null,
140
+ credId: info?.credId ?? null,
141
+ betas: Array.isArray(info?.betas) ? [...info.betas] : [],
142
+ cacheStatus: info?.cacheStatus ?? null,
143
+ thinkingTokens: info?.tokenCounts?.thinking ?? null,
144
+ statusCode: typeof info?.statusCode === 'number' ? info.statusCode : null,
145
+ extraHeaders: info?.extraHeaders && typeof info.extraHeaders === 'object'
146
+ ? { ...info.extraHeaders }
147
+ : {},
148
+ },
149
+ exit: { _tag: 'Success' },
150
+ });
151
+ };
152
+
153
+ emitter.on('beta-response', handler);
154
+ return () => emitter.off('beta-response', handler);
155
+ }
156
+
105
157
  // Duck-typed ServerResponse for host-stream's `addSink`. Parses SSE frames
106
158
  // (`event: <type>\ndata: <json>\n\n`) and dispatches `event: span` payloads
107
159
  // to `onSpan`. All other event types are silently ignored — host-stream
@@ -0,0 +1,43 @@
1
+ // H6 (Phase G) — Linear outbound sync skeleton.
2
+ //
3
+ // When a planning_artifacts row is created via H2 chunk extraction AND
4
+ // the operator has Linear MCP active, host-cp posts a new Linear issue
5
+ // (OR appends a comment to an existing linked issue if linear_issue_url
6
+ // is already populated on the row).
7
+ //
8
+ // PHASE G SHIPS SKELETON ONLY. The MCP-from-host wiring is not yet in
9
+ // host-cp; full Linear outbound posting lands in a follow-up commit
10
+ // when the MCP runtime story for host-cp is settled (today MCP lives in
11
+ // the operator's Claude Code runtime, not host-cp).
12
+ //
13
+ // Reverse channel (incoming Linear webhooks update artifact row status):
14
+ // EXPLICITLY OUT OF SCOPE for Phase G per plan body Out of scope list.
15
+ // Deferred to follow-up plan.
16
+
17
+ /**
18
+ * Best-effort Linear outbound sync. Returns null when MCP is unavailable
19
+ * (the typical case today — silent no-op). When MCP wires in, this
20
+ * function resolves to the posted issue URL which the caller can persist
21
+ * back to planning_artifacts.linear_issue_url.
22
+ *
23
+ * @param {object} args
24
+ * @param {string} args.artifactId
25
+ * @param {string} args.title
26
+ * @param {unknown} args.body — JSON body of the artifact
27
+ * @param {string} args.sessionId
28
+ * @returns {Promise<string | null>} — Linear issue URL or null
29
+ */
30
+ export async function syncArtifactToLinear({ artifactId, title, body, sessionId }) {
31
+ // Probe for Linear MCP availability via host-cp's bootstrap config (TBD).
32
+ // Until that surface exists, return null. Logging the intent surfaces
33
+ // the wiring gap to operators inspecting host-cp logs.
34
+ void artifactId;
35
+ void title;
36
+ void body;
37
+ void sessionId;
38
+ console.log(
39
+ `[linear-sync] outbound skip — Linear MCP not yet wired to host-cp; ` +
40
+ `artifactId=${artifactId} sessionId=${sessionId}`,
41
+ );
42
+ return null;
43
+ }
@@ -68,7 +68,7 @@ const SCOPE_ID_RE = /^[A-Za-z0-9_.-]+$/;
68
68
  // /v1/shape. Only these tables have server-side where-rewrite support; any
69
69
  // other table=... param gets a 400. Guards against a client enumerating
70
70
  // tables the service doesn't own.
71
- const ALLOWED_SHAPE_TABLES = new Set(['chunks', 'message_usage']);
71
+ const ALLOWED_SHAPE_TABLES = new Set(['chunks', 'message_usage', 'planning_artifacts']);
72
72
 
73
73
  // B6 (plan-chat-context-window-display Phase B): context-window caps per
74
74
  // model. Mirrors CONTEXT_CAPS from @olam/intelligence/src/llm-router/providers/claude.ts.
@@ -399,6 +399,44 @@ export function createHandler({
399
399
  }
400
400
  }
401
401
  }
402
+ // H2 (plan-chat-spa-canonical-surface Phase G) — extract commit_plan /
403
+ // propose_plan tool_use chunks into the planning_artifacts mutable
404
+ // table. The chunk's content carries JSON-stringified {name, input}
405
+ // per the substrate contract; we parse it once + write a single row.
406
+ // Failure to extract is logged + swallowed — the chunk itself is
407
+ // already persisted; artifact row absence is recoverable via
408
+ // re-extraction from chunks in a follow-up batch job.
409
+ if (body.chunk_type === 'tool_use') {
410
+ try {
411
+ const parsed = JSON.parse(body.chunk);
412
+ const toolName = typeof parsed?.name === 'string' ? parsed.name : '';
413
+ const artifactType = (toolName === 'commit_plan' || toolName === 'propose_plan')
414
+ ? 'commit_plan'
415
+ : toolName === 'component_scaffold'
416
+ ? 'component_scaffold'
417
+ : toolName === 'design_jam'
418
+ ? 'design_jam'
419
+ : null;
420
+ if (artifactType && parsed.input && typeof parsed.input === 'object') {
421
+ const input = parsed.input;
422
+ const title = typeof input.title === 'string' && input.title.length > 0
423
+ ? input.title.slice(0, 200)
424
+ : `Untitled ${artifactType}`;
425
+ const artifactId = `${body.message_id}:${body.seq}`;
426
+ await pool.query(
427
+ `INSERT INTO planning_artifacts
428
+ (id, world_id, session_id, type, title, body)
429
+ VALUES ($1, $2, $3, $4, $5, $6)
430
+ ON CONFLICT (id) DO NOTHING`,
431
+ [artifactId, body.world_id, body.session_id, artifactType, title, input],
432
+ );
433
+ }
434
+ } catch (artifactErr) {
435
+ console.warn(
436
+ `[plan-chat-service] planning_artifacts extraction failed for message=${body.message_id} seq=${body.seq}: ${artifactErr?.message ?? artifactErr}`,
437
+ );
438
+ }
439
+ }
402
440
  } catch (err) {
403
441
  if (err && typeof err === 'object' && 'code' in err && err.code === '23505') {
404
442
  return send(res, 409, { error: 'duplicate', message: '(message_id, seq) already exists' });
@@ -634,6 +672,26 @@ export function createHandler({
634
672
  createWorld,
635
673
  destroyWorld,
636
674
  });
675
+ // H7 (Phase G) — back-fill crystallized_world_id to all planning_artifacts
676
+ // rows for this session. The status pill state machine (Phase E E4) reads
677
+ // this field; the editor view + diagrams viewer use it for the
678
+ // "View world →" CTA. Failure here is logged + swallowed — the
679
+ // crystallize itself already succeeded; back-fill is a best-effort
680
+ // sync that an operator can re-run via a CLI helper.
681
+ if (result.worldId) {
682
+ try {
683
+ await pool.query(
684
+ `UPDATE planning_artifacts
685
+ SET crystallized_world_id = $1, status = 'crystallized'
686
+ WHERE session_id = $2 AND status = 'open'`,
687
+ [result.worldId, body.session_id],
688
+ );
689
+ } catch (backfillErr) {
690
+ console.warn(
691
+ `[plan-chat-service] crystallize back-fill failed for session=${body.session_id}: ${backfillErr?.message ?? backfillErr}`,
692
+ );
693
+ }
694
+ }
637
695
  return send(res, 200, {
638
696
  ok: true,
639
697
  created_world_id: result.worldId,
@@ -649,6 +707,68 @@ export function createHandler({
649
707
  }
650
708
  }
651
709
 
710
+ // H4 (Phase G) — GET + PATCH /v1/artifacts/:id endpoint pair backing
711
+ // the SPA's editor-view round-trip. GET returns the artifact row;
712
+ // PATCH updates body (the JSON payload) + bumps updated_at via trigger.
713
+ // SCOPE_ID_RE on :id; bearer auth identical to the chunks endpoints.
714
+ async function handleGetArtifact(req, res, id) {
715
+ if (!checkAuth(req)) return unauthorized(res);
716
+ if (!SCOPE_ID_RE.test(id)) return badRequest(res, 'invalid artifact id');
717
+ try {
718
+ const result = await pool.query(
719
+ `SELECT id, world_id, session_id, type, title, body, status,
720
+ linear_issue_url, crystallized_world_id,
721
+ created_at, updated_at
722
+ FROM planning_artifacts WHERE id = $1`,
723
+ [id],
724
+ );
725
+ const row = result.rows[0];
726
+ if (!row) return send(res, 404, { error: 'not-found' });
727
+ return send(res, 200, row);
728
+ } catch (err) {
729
+ return send(res, 500, { error: 'query-failed', message: String(err?.message ?? err) });
730
+ }
731
+ }
732
+
733
+ async function handlePatchArtifact(req, res, id) {
734
+ if (!checkAuth(req)) return unauthorized(res);
735
+ if (!SCOPE_ID_RE.test(id)) return badRequest(res, 'invalid artifact id');
736
+ let body;
737
+ try {
738
+ body = await readJson(req);
739
+ } catch {
740
+ return badRequest(res, 'malformed JSON body');
741
+ }
742
+ const sets = [];
743
+ const values = [id];
744
+ if (body.body !== undefined) {
745
+ values.push(body.body);
746
+ sets.push(`body = $${values.length}::jsonb`);
747
+ }
748
+ if (typeof body.title === 'string') {
749
+ values.push(body.title.slice(0, 200));
750
+ sets.push(`title = $${values.length}`);
751
+ }
752
+ if (typeof body.status === 'string' && ['open', 'crystallized', 'failed', 'archived'].includes(body.status)) {
753
+ values.push(body.status);
754
+ sets.push(`status = $${values.length}`);
755
+ }
756
+ if (sets.length === 0) {
757
+ return badRequest(res, 'no patchable fields supplied (body | title | status)');
758
+ }
759
+ try {
760
+ const result = await pool.query(
761
+ `UPDATE planning_artifacts SET ${sets.join(', ')} WHERE id = $1 RETURNING *`,
762
+ values,
763
+ );
764
+ const row = result.rows[0];
765
+ if (!row) return send(res, 404, { error: 'not-found' });
766
+ return send(res, 200, row);
767
+ } catch (err) {
768
+ return send(res, 500, { error: 'update-failed', message: String(err?.message ?? err) });
769
+ }
770
+ }
771
+
652
772
  return async function handler(req, res) {
653
773
  const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
654
774
  if (req.method === 'GET' && url.pathname === '/livez') return send(res, 200, { ok: true });
@@ -656,6 +776,14 @@ export function createHandler({
656
776
  if (req.method === 'GET' && url.pathname === '/v1/shape') return handleGetShape(req, res, url);
657
777
  if (req.method === 'GET' && url.pathname === '/v1/planning-sessions') return handleGetPlanningSessions(req, res, url);
658
778
  if (req.method === 'POST' && url.pathname === '/v1/crystallize') return handlePostCrystallize(req, res);
779
+ // H4 — /v1/artifacts/:id pair
780
+ const artifactMatch = /^\/v1\/artifacts\/([^/]+)$/.exec(url.pathname);
781
+ if (artifactMatch) {
782
+ const id = decodeURIComponent(artifactMatch[1]);
783
+ if (req.method === 'GET') return handleGetArtifact(req, res, id);
784
+ if (req.method === 'PATCH') return handlePatchArtifact(req, res, id);
785
+ return send(res, 405, { error: 'method-not-allowed' });
786
+ }
659
787
  return send(res, 404, { error: 'not-found' });
660
788
  };
661
789
  }
@@ -41,7 +41,11 @@ import {
41
41
  WorldStartupFailureKind,
42
42
  } from '../lifecycle/index.mjs';
43
43
  import { createHostStream, newStreamId } from './host-stream.mjs';
44
- import { createNdjsonSpanSink } from '../observability/ndjson-span-sink.mjs';
44
+ import {
45
+ createNdjsonSpanSink,
46
+ attachBetaResponseEvents,
47
+ } from '../observability/ndjson-span-sink.mjs';
48
+ import { betaResponseEmitter } from '@olam/auth-client';
45
49
  import { attemptRecovery, findScenarioForKind } from '../recovery/index.mjs';
46
50
  import { detectHaltChunk } from './halt-detect.mjs';
47
51
  import { spawnUpgraderContainer } from './upgrade-spawner.mjs';
@@ -83,6 +87,7 @@ import {
83
87
  } from './routes/process-port.mjs';
84
88
  import { instrumentHandler, renderMetrics } from './metrics.mjs';
85
89
  import { handleDispatchFromEmail } from './lib/email-dispatch.mjs';
90
+ import { emitTierSuggestion } from '../dispatch/auto-tier-scheduler.mjs';
86
91
 
87
92
  // ── Deployment-mode detection ─────────────────────────────────────
88
93
  //
@@ -490,6 +495,20 @@ const ndjsonSpanSink = await createNdjsonSpanSink({ hostStream }).catch((err) =>
490
495
  return null;
491
496
  });
492
497
 
498
+ // Wire @olam/auth-client `beta-response` events (Anthropic SDK 0.96+ beta
499
+ // flags — thinking-token-count, cache-diagnostics, future passthrough) into
500
+ // the NDJSON trace as `withCredential.beta-response` spans. Opt-in via the
501
+ // caller's `withCredential('claude', fn, { betas: [...] })` options; when
502
+ // no caller opts in, the emitter never fires and this subscription is a
503
+ // no-op. See docs/decisions/047-anthropic-sdk-beta-flags.md.
504
+ if (ndjsonSpanSink) {
505
+ try {
506
+ attachBetaResponseEvents({ sink: ndjsonSpanSink, emitter: betaResponseEmitter });
507
+ } catch (err) {
508
+ console.warn(`[trace] beta-response wire unavailable: ${err?.message ?? err}`);
509
+ }
510
+ }
511
+
493
512
  // A4: coalesce docker-event bursts into a single servers.snapshot. World
494
513
  // boot fires `create` + `start` + healthcheck transitions in <100ms; we
495
514
  // don't want a broadcast storm. Window matches plan-source.md P3 target.
@@ -922,6 +941,58 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
922
941
  if (handled) return;
923
942
  }
924
943
 
944
+ // /api/telemetry/planning-sessions — B9: aggregate planning_sessions by
945
+ // session_source for the canonical-surface bet's adoption signal. Per
946
+ // plan-chat-spa-canonical-surface plan § Operator workflow seam falsification
947
+ // trigger: if plan-chat-spa weekly-active sessions < 60% of control-plane/app
948
+ // by 2026-Q3, freeze plan-chat-spa feature work. This endpoint is the
949
+ // data source for that measurement.
950
+ //
951
+ // Query param: ?since=YYYY-MM-DD (required; rejects with 400 otherwise).
952
+ // Response: { plan_chat_spa: N, control_plane_app: M, unknown: K, ratio: pct }
953
+ // where ratio = plan_chat_spa / (plan_chat_spa + control_plane_app) * 100,
954
+ // null if denominator is 0.
955
+ if (url.pathname === '/api/telemetry/planning-sessions' && req.method === 'GET') {
956
+ const since = url.searchParams.get('since');
957
+ if (!since || !/^\d{4}-\d{2}-\d{2}$/.test(since)) {
958
+ res.writeHead(400, { 'Content-Type': 'application/json' });
959
+ return res.end(JSON.stringify({
960
+ error: 'bad_request',
961
+ message: 'Missing or malformed `since` query param. Expected YYYY-MM-DD.',
962
+ }));
963
+ }
964
+ // B9 ships the endpoint CONTRACT + the session_source schema column.
965
+ // The query implementation goes through plan-chat-service.mjs (which
966
+ // owns the pg pool); this host-cp handler currently emits a 503 with
967
+ // a structured "not_implemented" marker so callers can verify the
968
+ // endpoint shape + auth + query-param parsing without the data path.
969
+ //
970
+ // Phase G of this epic adds the plan-chat-service handler that this
971
+ // endpoint will proxy to. Until then operators can run the SQL
972
+ // directly:
973
+ // SELECT COALESCE(session_source, 'unknown'), COUNT(*)
974
+ // FROM planning_sessions
975
+ // WHERE created_at >= $since
976
+ // GROUP BY 1;
977
+ //
978
+ // Notify-C: ship contract + schema; defer data path to Phase G.
979
+ res.writeHead(503, { 'Content-Type': 'application/json' });
980
+ return res.end(JSON.stringify({
981
+ error: 'not_implemented',
982
+ message: 'B9 ships the endpoint contract + session_source schema column. ' +
983
+ 'Aggregation handler scaffolded in plan-chat-service.mjs lands in Phase G.',
984
+ since,
985
+ contractShape: {
986
+ plan_chat_spa: 0,
987
+ control_plane_app: 0,
988
+ unknown: 0,
989
+ ratio: null,
990
+ since: '<YYYY-MM-DD>',
991
+ asOf: '<ISO 8601>',
992
+ },
993
+ }));
994
+ }
995
+
925
996
  // /api/version/status: returns the current version snapshot (baked SHA
926
997
  // vs operator's local HEAD). No auth required beyond the existing gate
927
998
  // (already applied above). Phase 1 only — detection, no auto-upgrade.
@@ -2224,6 +2295,23 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2224
2295
  return jsonReply(res, 400, { error: 'invalid_json', message: err.message });
2225
2296
  }
2226
2297
  try {
2298
+ // Auto-tier-scheduler v1 (ADR 042): emit an informational
2299
+ // `dispatch.tier-suggestion` event BEFORE handing off to the
2300
+ // dispatch handler. Pure-informational — never changes which
2301
+ // provider actually runs. The dispatch payload's optional
2302
+ // `tierSpec` ({ kind?, expectedDurationMs?, explicitTier? })
2303
+ // carries the shape; absent it, the heuristic falls through to
2304
+ // its default (`cloudflare-sandbox`).
2305
+ if (dispatch && typeof dispatch.worldId === 'string') {
2306
+ try {
2307
+ emitTierSuggestion({
2308
+ worldId: dispatch.worldId,
2309
+ dispatchSpec: dispatch.tierSpec ?? {},
2310
+ currentTier: null,
2311
+ hostStream,
2312
+ });
2313
+ } catch { /* never let a hint surface break dispatch */ }
2314
+ }
2227
2315
  const result = await handleDispatchFromEmail({
2228
2316
  dispatch,
2229
2317
  worlds: WORLDS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pleri/olam-cli",
3
- "version": "0.1.170",
3
+ "version": "0.1.173",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "olam": "./bin/olam.cjs"