@pleri/olam-cli 0.1.180 → 0.1.185

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 (209) hide show
  1. package/dist/agent-stream/agent-sdk-to-chunks.js +44 -30
  2. package/dist/ask/checkout.d.ts +19 -0
  3. package/dist/ask/checkout.d.ts.map +1 -0
  4. package/dist/ask/checkout.js +40 -0
  5. package/dist/ask/checkout.js.map +1 -0
  6. package/dist/ask/knowledge-pack-builder.d.ts +72 -0
  7. package/dist/ask/knowledge-pack-builder.d.ts.map +1 -0
  8. package/dist/ask/knowledge-pack-builder.js +91 -0
  9. package/dist/ask/knowledge-pack-builder.js.map +1 -0
  10. package/dist/ask/knowledge-pack.generated.d.ts +8 -0
  11. package/dist/ask/knowledge-pack.generated.d.ts.map +1 -0
  12. package/dist/ask/knowledge-pack.generated.js +1947 -0
  13. package/dist/ask/knowledge-pack.generated.js.map +1 -0
  14. package/dist/ask/one-shot.d.ts +21 -0
  15. package/dist/ask/one-shot.d.ts.map +1 -0
  16. package/dist/ask/one-shot.js +50 -0
  17. package/dist/ask/one-shot.js.map +1 -0
  18. package/dist/ask/repl.d.ts +30 -0
  19. package/dist/ask/repl.d.ts.map +1 -0
  20. package/dist/ask/repl.js +109 -0
  21. package/dist/ask/repl.js.map +1 -0
  22. package/dist/ask/sdk-client.d.ts +87 -0
  23. package/dist/ask/sdk-client.d.ts.map +1 -0
  24. package/dist/ask/sdk-client.js +118 -0
  25. package/dist/ask/sdk-client.js.map +1 -0
  26. package/dist/ask/system-prompt.d.ts +30 -0
  27. package/dist/ask/system-prompt.d.ts.map +1 -0
  28. package/dist/ask/system-prompt.js +31 -0
  29. package/dist/ask/system-prompt.js.map +1 -0
  30. package/dist/commands/ask.d.ts +27 -0
  31. package/dist/commands/ask.d.ts.map +1 -0
  32. package/dist/commands/ask.js +63 -0
  33. package/dist/commands/ask.js.map +1 -0
  34. package/dist/commands/auth-list-json.d.ts +53 -0
  35. package/dist/commands/auth-list-json.d.ts.map +1 -0
  36. package/dist/commands/auth-list-json.js +47 -0
  37. package/dist/commands/auth-list-json.js.map +1 -0
  38. package/dist/commands/auth.d.ts.map +1 -1
  39. package/dist/commands/auth.js +80 -19
  40. package/dist/commands/auth.js.map +1 -1
  41. package/dist/commands/config.d.ts.map +1 -1
  42. package/dist/commands/config.js +93 -0
  43. package/dist/commands/config.js.map +1 -1
  44. package/dist/commands/destroy.d.ts +41 -0
  45. package/dist/commands/destroy.d.ts.map +1 -1
  46. package/dist/commands/destroy.js +81 -33
  47. package/dist/commands/destroy.js.map +1 -1
  48. package/dist/commands/dispatch-resolve.d.ts +54 -0
  49. package/dist/commands/dispatch-resolve.d.ts.map +1 -0
  50. package/dist/commands/dispatch-resolve.js +105 -0
  51. package/dist/commands/dispatch-resolve.js.map +1 -0
  52. package/dist/commands/dispatch.d.ts.map +1 -1
  53. package/dist/commands/dispatch.js +40 -9
  54. package/dist/commands/dispatch.js.map +1 -1
  55. package/dist/commands/doctor.js +11 -11
  56. package/dist/commands/doctor.js.map +1 -1
  57. package/dist/commands/flywheel/k5-validate.d.ts +31 -0
  58. package/dist/commands/flywheel/k5-validate.d.ts.map +1 -1
  59. package/dist/commands/flywheel/k5-validate.js +80 -19
  60. package/dist/commands/flywheel/k5-validate.js.map +1 -1
  61. package/dist/commands/keys-list-json.d.ts +55 -0
  62. package/dist/commands/keys-list-json.d.ts.map +1 -0
  63. package/dist/commands/keys-list-json.js +54 -0
  64. package/dist/commands/keys-list-json.js.map +1 -0
  65. package/dist/commands/keys.d.ts.map +1 -1
  66. package/dist/commands/keys.js +6 -0
  67. package/dist/commands/keys.js.map +1 -1
  68. package/dist/commands/kg-classify.d.ts.map +1 -1
  69. package/dist/commands/kg-classify.js +20 -0
  70. package/dist/commands/kg-classify.js.map +1 -1
  71. package/dist/commands/kg-doctor.d.ts +67 -6
  72. package/dist/commands/kg-doctor.d.ts.map +1 -1
  73. package/dist/commands/kg-doctor.js +126 -46
  74. package/dist/commands/kg-doctor.js.map +1 -1
  75. package/dist/commands/lanes-list-json.d.ts +69 -0
  76. package/dist/commands/lanes-list-json.d.ts.map +1 -0
  77. package/dist/commands/lanes-list-json.js +42 -0
  78. package/dist/commands/lanes-list-json.js.map +1 -0
  79. package/dist/commands/lanes.d.ts.map +1 -1
  80. package/dist/commands/lanes.js +18 -7
  81. package/dist/commands/lanes.js.map +1 -1
  82. package/dist/commands/list.d.ts +27 -0
  83. package/dist/commands/list.d.ts.map +1 -1
  84. package/dist/commands/list.js +67 -19
  85. package/dist/commands/list.js.map +1 -1
  86. package/dist/commands/memory/status.d.ts +18 -0
  87. package/dist/commands/memory/status.d.ts.map +1 -1
  88. package/dist/commands/memory/status.js +38 -2
  89. package/dist/commands/memory/status.js.map +1 -1
  90. package/dist/commands/memory-service-container.d.ts +44 -0
  91. package/dist/commands/memory-service-container.d.ts.map +1 -1
  92. package/dist/commands/memory-service-container.js +49 -0
  93. package/dist/commands/memory-service-container.js.map +1 -1
  94. package/dist/commands/plans-list-json.d.ts +77 -0
  95. package/dist/commands/plans-list-json.d.ts.map +1 -0
  96. package/dist/commands/plans-list-json.js +61 -0
  97. package/dist/commands/plans-list-json.js.map +1 -0
  98. package/dist/commands/plans.d.ts.map +1 -1
  99. package/dist/commands/plans.js +10 -0
  100. package/dist/commands/plans.js.map +1 -1
  101. package/dist/commands/ps.d.ts +32 -0
  102. package/dist/commands/ps.d.ts.map +1 -1
  103. package/dist/commands/ps.js +34 -0
  104. package/dist/commands/ps.js.map +1 -1
  105. package/dist/commands/repos-list-json.d.ts +58 -0
  106. package/dist/commands/repos-list-json.d.ts.map +1 -0
  107. package/dist/commands/repos-list-json.js +45 -0
  108. package/dist/commands/repos-list-json.js.map +1 -0
  109. package/dist/commands/repos.d.ts +1 -1
  110. package/dist/commands/repos.d.ts.map +1 -1
  111. package/dist/commands/repos.js +12 -2
  112. package/dist/commands/repos.js.map +1 -1
  113. package/dist/commands/runbooks.d.ts +32 -0
  114. package/dist/commands/runbooks.d.ts.map +1 -1
  115. package/dist/commands/runbooks.js +79 -22
  116. package/dist/commands/runbooks.js.map +1 -1
  117. package/dist/commands/services.d.ts +47 -1
  118. package/dist/commands/services.d.ts.map +1 -1
  119. package/dist/commands/services.js +59 -33
  120. package/dist/commands/services.js.map +1 -1
  121. package/dist/commands/skills-source.d.ts.map +1 -1
  122. package/dist/commands/skills-source.js +77 -2
  123. package/dist/commands/skills-source.js.map +1 -1
  124. package/dist/commands/skills.d.ts +27 -0
  125. package/dist/commands/skills.d.ts.map +1 -1
  126. package/dist/commands/skills.js +17 -2
  127. package/dist/commands/skills.js.map +1 -1
  128. package/dist/commands/upgrade-history.d.ts +0 -2
  129. package/dist/commands/upgrade-history.d.ts.map +1 -1
  130. package/dist/commands/upgrade-history.js +0 -6
  131. package/dist/commands/upgrade-history.js.map +1 -1
  132. package/dist/commands/upgrade-lock.d.ts +0 -9
  133. package/dist/commands/upgrade-lock.d.ts.map +1 -1
  134. package/dist/commands/upgrade-lock.js +1 -1
  135. package/dist/commands/upgrade-lock.js.map +1 -1
  136. package/dist/commands/workspace-list-json.d.ts +73 -0
  137. package/dist/commands/workspace-list-json.d.ts.map +1 -0
  138. package/dist/commands/workspace-list-json.js +59 -0
  139. package/dist/commands/workspace-list-json.js.map +1 -0
  140. package/dist/commands/workspace.d.ts.map +1 -1
  141. package/dist/commands/workspace.js +7 -1
  142. package/dist/commands/workspace.js.map +1 -1
  143. package/dist/commands/world-snapshot.d.ts +13 -0
  144. package/dist/commands/world-snapshot.d.ts.map +1 -1
  145. package/dist/commands/world-snapshot.js +81 -1
  146. package/dist/commands/world-snapshot.js.map +1 -1
  147. package/dist/commands/yolo.d.ts +0 -4
  148. package/dist/commands/yolo.d.ts.map +1 -1
  149. package/dist/commands/yolo.js +2 -2
  150. package/dist/commands/yolo.js.map +1 -1
  151. package/dist/image-digests.json +8 -8
  152. package/dist/index.js +6097 -2563
  153. package/dist/index.js.map +1 -1
  154. package/dist/lib/anthropic-base-url-file.d.ts +37 -0
  155. package/dist/lib/anthropic-base-url-file.d.ts.map +1 -0
  156. package/dist/lib/anthropic-base-url-file.js +46 -0
  157. package/dist/lib/anthropic-base-url-file.js.map +1 -0
  158. package/dist/lib/auth-remote.d.ts +9 -0
  159. package/dist/lib/auth-remote.d.ts.map +1 -1
  160. package/dist/lib/auth-remote.js +19 -4
  161. package/dist/lib/auth-remote.js.map +1 -1
  162. package/dist/lib/cf-access-token.d.ts +32 -0
  163. package/dist/lib/cf-access-token.d.ts.map +1 -0
  164. package/dist/lib/cf-access-token.js +52 -0
  165. package/dist/lib/cf-access-token.js.map +1 -0
  166. package/dist/lib/config.d.ts +17 -3
  167. package/dist/lib/config.d.ts.map +1 -1
  168. package/dist/lib/config.js +28 -4
  169. package/dist/lib/config.js.map +1 -1
  170. package/dist/lib/k8s-bootstrap.d.ts.map +1 -1
  171. package/dist/lib/k8s-bootstrap.js +13 -1
  172. package/dist/lib/k8s-bootstrap.js.map +1 -1
  173. package/dist/lib/k8s-secret-render.d.ts +2 -0
  174. package/dist/lib/k8s-secret-render.d.ts.map +1 -1
  175. package/dist/lib/k8s-secret-render.js +27 -0
  176. package/dist/lib/k8s-secret-render.js.map +1 -1
  177. package/dist/lib/kubectl-context.d.ts +49 -0
  178. package/dist/lib/kubectl-context.d.ts.map +1 -1
  179. package/dist/lib/kubectl-context.js +64 -2
  180. package/dist/lib/kubectl-context.js.map +1 -1
  181. package/dist/lib/peripheral-registry.d.ts +1 -1
  182. package/dist/lib/peripheral-registry.d.ts.map +1 -1
  183. package/dist/lib/peripheral-registry.js +13 -0
  184. package/dist/lib/peripheral-registry.js.map +1 -1
  185. package/dist/lib/upgrade-kubernetes.d.ts +13 -0
  186. package/dist/lib/upgrade-kubernetes.d.ts.map +1 -1
  187. package/dist/lib/upgrade-kubernetes.js +42 -9
  188. package/dist/lib/upgrade-kubernetes.js.map +1 -1
  189. package/dist/mcp-server.js +2624 -1041
  190. package/hermes-bundle/version.json +1 -1
  191. package/host-cp/k8s/manifests/30-configmap.yaml +11 -6
  192. package/host-cp/k8s/manifests/45-pvc.yaml +6 -2
  193. package/host-cp/k8s/manifests/50-deployment.yaml +15 -1
  194. package/host-cp/k8s/manifests/auth-service/50-deployment.yaml +1 -1
  195. package/host-cp/k8s/manifests/kg-service/50-deployment.yaml +1 -1
  196. package/host-cp/k8s/manifests/mcp-auth-service/50-deployment.yaml +1 -1
  197. package/host-cp/k8s/manifests/memory-service/50-deployment.yaml +1 -1
  198. package/host-cp/k8s/templates/chunks-postgres-secret-template.yaml +24 -0
  199. package/host-cp/k8s/templates/plan-chat-service-secret-template.yaml +35 -0
  200. package/host-cp/observability/trace-summary.mjs +267 -0
  201. package/host-cp/src/bootstrap-selective.mjs +30 -28
  202. package/host-cp/src/host-stream.mjs +52 -0
  203. package/host-cp/src/plan-chat-service.mjs +99 -74
  204. package/host-cp/src/redirect.mjs +7 -0
  205. package/host-cp/src/router.mjs +168 -0
  206. package/host-cp/src/serve-only-config.mjs +85 -0
  207. package/host-cp/src/server.mjs +482 -217
  208. package/host-cp/src/world-services.mjs +136 -0
  209. package/package.json +4 -2
@@ -1,4 +1,4 @@
1
1
  {
2
- "bundledAt": "2026-05-24T18:50:58.922Z",
2
+ "bundledAt": "2026-05-27T14:19:47.128Z",
3
3
  "kgFirstSha": "29a9ccce1b115d049e375c4a90eb5cf7c123e610e2d0590270a4db2cdbc64a28"
4
4
  }
@@ -24,12 +24,17 @@ data:
24
24
  OLAM_WORLDS_DB: "/data/worlds.db"
25
25
  OLAM_PLAN_DB_PATH: "/data/plan.db"
26
26
  OLAM_PLAN_DIR: "/data/plan"
27
- # Same /data routing for the plan-chat bearer gateway. K8s pod runs as
28
- # UID 1000 with readOnlyRootFilesystem: true, so the bearer file CAN
29
- # NEVER be created under /home/node/.olam even with mkdir-recursive
30
- # the env override is mandatory here, not optional. Bind-mounted /data
31
- # is the writable PVC.
32
- OLAM_PLAN_CHAT_SECRET_PATH: "/data/plan-chat-secret"
27
+ # Phase B Model B: bearer file is now sourced from the shared
28
+ # olam-plan-chat-secret Kubernetes Secret (mounted at /etc/olam-plan-chat/).
29
+ # Two readers, one source-of-truth replaces the per-pod /data/plan-chat-secret
30
+ # file that couldn't be shared across pods on RWO PVCs. The plan-chat-service
31
+ # pod also mounts the SAME Secret at the SAME path so bearer comparisons
32
+ # work both ways.
33
+ OLAM_PLAN_CHAT_SECRET_PATH: "/etc/olam-plan-chat/secret"
34
+ # In-cluster plan-chat-service URL. Rewritten by upgrade-kubernetes.ts step 2.5
35
+ # (buildK8sDnsUrl) — the default below is a sane fallback for raw
36
+ # `kubectl apply -f` operators who skip the CLI wrapper.
37
+ PLAN_CHAT_SERVICE_URL: "http://olam-plan-chat-service.olam.svc.cluster.local:3200"
33
38
  # NDJSON span sink + recovery ledger — route to the writable PVC mount at
34
39
  # /data rather than the default ~/.olam/logs (which resolves to
35
40
  # /home/node/.olam/logs and is not writable with readOnlyRootFilesystem: true).
@@ -1,4 +1,4 @@
1
- # PersistentVolumeClaim for olam-host-cp /data volume.
1
+ # PersistentVolumeClaim for olam-host-cp /data volume — k3d substrate default.
2
2
  #
3
3
  # Why PVC instead of hostPath:
4
4
  # hostPath volumes on k3d nodes resolve to paths INSIDE the k3d node
@@ -9,7 +9,11 @@
9
9
  # root-owned hostPath mounts even when fsGroup: 1000 is set.
10
10
  #
11
11
  # local-path StorageClass ships with k3d by default (rancher/local-path-provisioner).
12
- # On non-k3d clusters, substitute with the appropriate StorageClass name.
12
+ # On non-k3d clusters, substitute with the appropriate StorageClass name (D24,
13
+ # operator-editable). For managed clusters (GKE, EKS, AKS) use the GKE-variant
14
+ # manifest instead: packages/host-cp/k8s/manifests/gke/45-pvc.yaml (storageClassName:
15
+ # standard-rwo). See docs/architecture/peripheral-services-on-k3s.md Decision #3
16
+ # for the full per-cluster storageclass table.
13
17
  apiVersion: v1
14
18
  kind: PersistentVolumeClaim
15
19
  metadata:
@@ -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:50598fca503775b8984c8717505f4f6266d61d86f5e55343733215a4cb7794e7
121
+ image: ghcr.io/pleri/olam-host-cp@sha256:843bea5159a7b5ec792a7d0a9766c49eb0443180023526c0c21d56e32982fd4e
122
122
  imagePullPolicy: IfNotPresent
123
123
  securityContext:
124
124
  runAsNonRoot: true
@@ -147,6 +147,13 @@ spec:
147
147
  readOnly: true
148
148
  - name: tmp
149
149
  mountPath: /tmp
150
+ # Phase B Model B: shared olam-plan-chat-secret mounted read-only
151
+ # so renderSpaShell can inject window.__OLAM_PLAN_CHAT_BEARER__.
152
+ # Plan-chat-service mounts the SAME Secret at the SAME path so
153
+ # bearer compares match across pods.
154
+ - name: plan-chat-secret
155
+ mountPath: /etc/olam-plan-chat
156
+ readOnly: true
150
157
  # docker-socket volumeMount REMOVED in olam-k3d-on-mac-substrate-
151
158
  # decision Phase B B2. Docker access now goes via TCP to the
152
159
  # docker-socket-proxy ExternalName Service in the olam namespace.
@@ -191,6 +198,13 @@ spec:
191
198
  type: DirectoryOrCreate
192
199
  - name: tmp
193
200
  emptyDir: {}
201
+ - name: plan-chat-secret
202
+ secret:
203
+ secretName: olam-plan-chat-secret
204
+ defaultMode: 0400
205
+ items:
206
+ - key: PLAN_CHAT_SECRET
207
+ path: secret
194
208
  # host-colima + docker-socket volumes REMOVED in olam-k3d-on-mac-
195
209
  # substrate-decision Phase B B2 (2026-05-21). R3-A's two-volume
196
210
  # hostPath approach is fully retracted: round-4 R4-W2-F demonstrated
@@ -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:5dec481a1bf4ab87a5dc354a3d6f71323f3f5403476cea50a934afde18a58bc5
73
+ image: ghcr.io/pleri/olam-auth@sha256:59de0618b656c45ed75465aefab637bd3b50ef803ae8d802c828c26d97183328
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:aa4138021389e303064babf7c81169e5b159aaf1570ba97429e29899f56f5000
64
+ image: ghcr.io/pleri/olam-kg-service@sha256:b53af63d452bb04420a1f89b5834888a324ee3995f45c908fa9321ec76b84047
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:e9d7c5c4222e2af20a2eb204dc3fa8090504bf99ddb5489115c21849706f1ad7
71
+ image: ghcr.io/pleri/olam-mcp-auth@sha256:635c1518e45cc0a1b5859cfb412d5a6eb9ab389630553eb9cdb6c903d97d8d15
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:052c0d82d7888a0bd8970847dd5d3e001036e16feefda615b0e88eecf82d111f
73
+ image: ghcr.io/pleri/olam-memory-service@sha256:efad791c760420e6bccc68120621aba97a2532da517eb3f7d0e0349f6a2f8a06
74
74
  imagePullPolicy: IfNotPresent
75
75
  securityContext:
76
76
  runAsNonRoot: true
@@ -0,0 +1,24 @@
1
+ # Secret TEMPLATE for olam-chunks-postgres.
2
+ #
3
+ # Generates a random 64-char hex POSTGRES_PASSWORD on first apply (via
4
+ # k8s-secret-render.ts generate-if-missing). The Secret is consumed by:
5
+ # - chunks-postgres StatefulSet (envFrom → POSTGRES_PASSWORD)
6
+ # - chunks-electric Deployment (env: valueFrom.secretKeyRef)
7
+ # - plan-chat-service Deployment (env: valueFrom.secretKeyRef)
8
+ #
9
+ # All three resolve the SAME random value because the secret-renderer
10
+ # persists generated values in ~/.olam/k8s-secrets-state.json so reapply
11
+ # is idempotent (no rotation unless --rotate-secrets).
12
+ apiVersion: v1
13
+ kind: Secret
14
+ metadata:
15
+ name: olam-chunks-postgres-secret
16
+ namespace: olam
17
+ labels:
18
+ app: olam-chunks-postgres
19
+ olam.io/component: substrate
20
+ type: Opaque
21
+ stringData:
22
+ # Postgres superuser password. Generated by the CLI's secret-renderer on
23
+ # first apply (no host-side file to read; this is in-cluster-only state).
24
+ POSTGRES_PASSWORD: "REPLACE_ME_GENERATE_RANDOM_HEX"
@@ -0,0 +1,35 @@
1
+ # Secret TEMPLATE for olam-plan-chat-secret.
2
+ #
3
+ # This file is a TEMPLATE — it MUST NOT be applied directly without substituting
4
+ # the placeholder values. The placeholders are intentionally invalid; a raw
5
+ # `kubectl apply` will result in auth failures rather than silently shipping
6
+ # fake credentials.
7
+ #
8
+ # Preferred substitution (keeps secrets out of git):
9
+ # kubectl create secret generic olam-plan-chat-secret -n olam \
10
+ # --from-literal=PLAN_CHAT_SECRET=$(cat ~/.olam/plan-chat-secret) \
11
+ # --dry-run=client -o yaml | kubectl apply -f -
12
+ #
13
+ # This template lives in packages/host-cp/k8s/templates/ (NOT manifests/)
14
+ # so that `kubectl apply -f manifests/plan-chat-service/` does NOT apply it —
15
+ # operators must explicitly handle Secret provisioning before applying manifests.
16
+ #
17
+ # Architecture: this Secret is mounted by BOTH the host-cp pod (so its
18
+ # renderSpaShell can inject window.__OLAM_PLAN_CHAT_BEARER__) AND the
19
+ # plan-chat-service pod (so its bearer-auth gate timing-safe-compares incoming
20
+ # Authorization: Bearer headers against the same value). One source-of-truth,
21
+ # two readers — replaces the previous "/data/plan-chat-secret in host-cp PVC"
22
+ # pattern that couldn't be shared across pods (RWO PVC).
23
+ apiVersion: v1
24
+ kind: Secret
25
+ metadata:
26
+ name: olam-plan-chat-secret
27
+ namespace: olam
28
+ labels:
29
+ olam.io/component: substrate
30
+ type: Opaque
31
+ stringData:
32
+ # Shared bearer secret for plan-chat-service's POST /v1/chunks and
33
+ # GET /v1/shape endpoints. host-cp injects this into window.__OLAM_PLAN_CHAT_BEARER__.
34
+ # Source: cat ~/.olam/plan-chat-secret
35
+ PLAN_CHAT_SECRET: "REPLACE_ME_FROM_HOME_DOTOLAM_PLAN_CHAT_SECRET"
@@ -0,0 +1,267 @@
1
+ // Trace summary — operator triage digest over the NDJSON span trace.
2
+ //
3
+ // The NDJSON span sink (see `ndjson-span-sink.mjs`) writes one JSON line
4
+ // per span to ~/.olam/logs/host.trace.ndjson. Operators triage it today
5
+ // with hand-typed `jq` one-liners (README § Observability): "longest 5
6
+ // spans", "all failed spans", "failure-kind tally". This module codifies
7
+ // those recipes into ONE digest so the common questions get one answer
8
+ // without remembering jq incantations.
9
+ //
10
+ // Design:
11
+ // - `summarizeSpans(spans, opts)` is PURE — no I/O. Given an array of
12
+ // parsed span records (the exact shape the sink writes) it returns a
13
+ // digest object. This is the unit-testable core.
14
+ // - `parseTrace(ndjsonText)` turns raw file bytes into { spans, skipped }.
15
+ // Malformed lines (truncated tail line, partial write mid-rotation)
16
+ // are COUNTED, never thrown — triage tooling must survive a corrupt
17
+ // line, not die on it.
18
+ // - `summarizeTraceFile(path, opts)` is the thin file-reading wrapper.
19
+ // - `formatDigest(digest)` renders a human-readable report for the CLI.
20
+ //
21
+ // Read-only + additive: this module never writes the trace, never changes
22
+ // the line schema. It only READS fields the sink already emits
23
+ // (durationMs, exit._tag, exit.reason, name, attributes.failureKind).
24
+
25
+ import { readFile } from 'node:fs/promises';
26
+
27
+ const DEFAULT_TOP_N = 5;
28
+
29
+ /**
30
+ * Parse NDJSON trace text into spans, tolerating malformed lines.
31
+ *
32
+ * @param {string} text raw file contents
33
+ * @returns {{ spans: object[], skipped: number }}
34
+ */
35
+ export function parseTrace(text) {
36
+ const spans = [];
37
+ let skipped = 0;
38
+ for (const line of String(text).split('\n')) {
39
+ const trimmed = line.trim();
40
+ if (trimmed === '') continue;
41
+ try {
42
+ spans.push(JSON.parse(trimmed));
43
+ } catch {
44
+ // Truncated tail line or a partial write straddling rotation — the
45
+ // append-only log can leave one half-line. Triage must not crash on
46
+ // it; count and move on.
47
+ skipped += 1;
48
+ }
49
+ }
50
+ return { spans, skipped };
51
+ }
52
+
53
+ function isFailure(span) {
54
+ return span?.exit?._tag === 'Failure';
55
+ }
56
+
57
+ /**
58
+ * Compute a triage digest over parsed spans. Pure.
59
+ *
60
+ * @param {object[]} spans
61
+ * @param {{ topN?: number }} [opts]
62
+ * @returns {{
63
+ * totalSpans: number,
64
+ * failures: number,
65
+ * successes: number,
66
+ * failureRate: number,
67
+ * slowest: object[],
68
+ * recentFailures: object[],
69
+ * failureReasons: { reason: string, count: number }[],
70
+ * failureKinds: { kind: string, count: number }[],
71
+ * byName: { name: string, count: number, failures: number, meanMs: number|null, maxMs: number|null }[],
72
+ * }}
73
+ */
74
+ export function summarizeSpans(spans, { topN = DEFAULT_TOP_N } = {}) {
75
+ const list = Array.isArray(spans) ? spans : [];
76
+ const totalSpans = list.length;
77
+ const failingSpans = list.filter(isFailure);
78
+ const failures = failingSpans.length;
79
+ const successes = totalSpans - failures;
80
+ const failureRate = totalSpans === 0 ? 0 : failures / totalSpans;
81
+
82
+ // Slowest spans by durationMs. Spans with a null duration (in-flight or
83
+ // missing endedAt) are excluded — they carry no comparable cost signal.
84
+ const timed = list.filter((s) => typeof s?.durationMs === 'number');
85
+ const slowest = [...timed]
86
+ .sort((a, b) => b.durationMs - a.durationMs)
87
+ .slice(0, topN)
88
+ .map(projectSpan);
89
+
90
+ // Recent failures — the trace is append-only, so the last failures in
91
+ // file order are the most recent. Take the tail.
92
+ const recentFailures = failingSpans.slice(-topN).reverse().map(projectSpan);
93
+
94
+ const failureReasons = tally(
95
+ failingSpans,
96
+ (s) => (s?.exit?.reason != null ? String(s.exit.reason) : '(no reason)'),
97
+ 'reason',
98
+ );
99
+
100
+ // failureKind is the world.lifecycle attribute the README already greps
101
+ // for; surface it as a first-class tally regardless of span name so
102
+ // recovery-relevant failures aggregate even when span names differ.
103
+ const failureKinds = tally(
104
+ list.filter((s) => s?.attributes?.failureKind != null),
105
+ (s) => String(s.attributes.failureKind),
106
+ 'kind',
107
+ );
108
+
109
+ const byName = aggregateByName(list);
110
+
111
+ return {
112
+ totalSpans,
113
+ failures,
114
+ successes,
115
+ failureRate,
116
+ slowest,
117
+ recentFailures,
118
+ failureReasons,
119
+ failureKinds,
120
+ byName,
121
+ };
122
+ }
123
+
124
+ function projectSpan(s) {
125
+ return {
126
+ name: s?.name ?? null,
127
+ traceId: s?.traceId ?? null,
128
+ spanId: s?.spanId ?? null,
129
+ durationMs: typeof s?.durationMs === 'number' ? s.durationMs : null,
130
+ startedAt: typeof s?.startedAt === 'number' ? s.startedAt : null,
131
+ reason: s?.exit?.reason != null ? String(s.exit.reason) : null,
132
+ };
133
+ }
134
+
135
+ // Group spans by a string key and count occurrences, labelling the key
136
+ // field per the caller (`reason` for failure reasons, `kind` for failure
137
+ // kinds). Sorted by count descending so the dominant cause leads.
138
+ function tally(spans, keyFn, label) {
139
+ const counts = new Map();
140
+ for (const s of spans) {
141
+ const key = keyFn(s);
142
+ counts.set(key, (counts.get(key) ?? 0) + 1);
143
+ }
144
+ const out = [];
145
+ for (const [k, count] of counts) out.push({ count, [label]: k });
146
+ return out.sort((a, b) => b.count - a.count);
147
+ }
148
+
149
+ /**
150
+ * Per-span-name aggregate: count, failure count, mean + max duration.
151
+ * Sorted by count descending so the busiest spans surface first.
152
+ */
153
+ function aggregateByName(spans) {
154
+ const groups = new Map();
155
+ for (const s of spans) {
156
+ const name = s?.name != null ? String(s.name) : '(unnamed)';
157
+ let g = groups.get(name);
158
+ if (!g) {
159
+ g = { name, count: 0, failures: 0, durSum: 0, durCount: 0, maxMs: null };
160
+ groups.set(name, g);
161
+ }
162
+ g.count += 1;
163
+ if (isFailure(s)) g.failures += 1;
164
+ if (typeof s?.durationMs === 'number') {
165
+ g.durSum += s.durationMs;
166
+ g.durCount += 1;
167
+ g.maxMs = g.maxMs === null ? s.durationMs : Math.max(g.maxMs, s.durationMs);
168
+ }
169
+ }
170
+ return [...groups.values()]
171
+ .map((g) => ({
172
+ name: g.name,
173
+ count: g.count,
174
+ failures: g.failures,
175
+ meanMs: g.durCount === 0 ? null : g.durSum / g.durCount,
176
+ maxMs: g.maxMs,
177
+ }))
178
+ .sort((a, b) => b.count - a.count);
179
+ }
180
+
181
+ /**
182
+ * Read + summarize a trace file. Missing file → empty digest (an operator
183
+ * who hasn't generated any spans yet sees a clean zero-state, not a crash).
184
+ *
185
+ * @param {string} path
186
+ * @param {{ topN?: number }} [opts]
187
+ */
188
+ export async function summarizeTraceFile(path, opts = {}) {
189
+ let text;
190
+ try {
191
+ text = await readFile(path, 'utf8');
192
+ } catch (err) {
193
+ if (err && err.code === 'ENOENT') {
194
+ return { ...summarizeSpans([], opts), skipped: 0, missing: true };
195
+ }
196
+ throw err;
197
+ }
198
+ const { spans, skipped } = parseTrace(text);
199
+ return { ...summarizeSpans(spans, opts), skipped, missing: false };
200
+ }
201
+
202
+ function fmtMs(ms) {
203
+ if (ms == null) return '—';
204
+ if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`;
205
+ return `${Math.round(ms)}ms`;
206
+ }
207
+
208
+ /**
209
+ * Render a digest as a human-readable, plain-text report for the CLI.
210
+ *
211
+ * @param {ReturnType<typeof summarizeSpans> & { skipped?: number, missing?: boolean, path?: string }} digest
212
+ * @returns {string}
213
+ */
214
+ export function formatDigest(digest) {
215
+ const lines = [];
216
+ const path = digest.path ? ` (${digest.path})` : '';
217
+ lines.push(`Trace summary${path}`);
218
+ if (digest.missing) {
219
+ lines.push(' no trace file yet — nothing recorded.');
220
+ return lines.join('\n');
221
+ }
222
+ const pct = (digest.failureRate * 100).toFixed(1);
223
+ lines.push(
224
+ ` ${digest.totalSpans} spans · ${digest.failures} failed (${pct}%) · ${digest.successes} ok` +
225
+ (digest.skipped ? ` · ${digest.skipped} malformed line(s) skipped` : ''),
226
+ );
227
+
228
+ if (digest.slowest.length) {
229
+ lines.push('');
230
+ lines.push(`Top ${digest.slowest.length} slowest:`);
231
+ for (const s of digest.slowest) {
232
+ lines.push(` ${fmtMs(s.durationMs).padStart(7)} ${s.name ?? '(unnamed)'}${s.traceId ? ` [${s.traceId}]` : ''}`);
233
+ }
234
+ }
235
+
236
+ if (digest.recentFailures.length) {
237
+ lines.push('');
238
+ lines.push(`Recent failures (${digest.recentFailures.length}):`);
239
+ for (const f of digest.recentFailures) {
240
+ lines.push(` ${f.name ?? '(unnamed)'}: ${f.reason ?? '(no reason)'}${f.traceId ? ` [${f.traceId}]` : ''}`);
241
+ }
242
+ }
243
+
244
+ if (digest.failureKinds.length) {
245
+ lines.push('');
246
+ lines.push('Failure kinds:');
247
+ for (const k of digest.failureKinds) lines.push(` ${String(k.count).padStart(4)} ${k.kind}`);
248
+ }
249
+
250
+ if (digest.failureReasons.length) {
251
+ lines.push('');
252
+ lines.push('Failure reasons:');
253
+ for (const r of digest.failureReasons) lines.push(` ${String(r.count).padStart(4)} ${r.reason}`);
254
+ }
255
+
256
+ if (digest.byName.length) {
257
+ lines.push('');
258
+ lines.push('By span name (count · failures · mean · max):');
259
+ for (const n of digest.byName) {
260
+ lines.push(
261
+ ` ${String(n.count).padStart(5)} · ${String(n.failures).padStart(4)}f · ${fmtMs(n.meanMs).padStart(7)} · ${fmtMs(n.maxMs).padStart(7)} ${n.name}`,
262
+ );
263
+ }
264
+ }
265
+
266
+ return lines.join('\n');
267
+ }
@@ -1,19 +1,32 @@
1
- // bootstrap-selective.mjs — Phase D1 helper.
1
+ // bootstrap-selective.mjs — Phase D1 helper, collapsed to a wildcard in
2
+ // Phase E5 (ATOMIC SERVING CUTOVER).
2
3
  //
3
4
  // Determines whether a SPA shell render path should SKIP the host-cp
4
5
  // BOOTSTRAP_SCRIPT injection (cookie-bootstrap + fetch/EventSource
5
- // rewrite shim) and instead let plan-chat-spa's own readBearer()
6
- // resolver handle auth.
6
+ // rewrite shim) and instead let the served SPA's own auth resolver +
7
+ // world-fetch shim handle auth.
7
8
  //
8
- // Reversal: set BOOTSTRAP_NOOP_PLANNING_PATHS to [] to restore universal
9
- // injection. Single-line revert.
9
+ // Phase E5: plan-chat-spa is now host-cp's SOLE served SPA. Its bundle
10
+ // re-homes the cookie-bootstrap + world-fetch-rewrite + 401-recover shim
11
+ // (packages/plan-chat-spa/src/lib/worldFetch.ts, installed at the top of
12
+ // src/main.tsx — Phase C). Therefore host-cp NEVER needs to inject
13
+ // BOOTSTRAP_SCRIPT anymore: every path is a "planning" (== SPA-owned)
14
+ // path. isPlanningPath() is collapsed to a wildcard accordingly.
10
15
  //
11
- // Per K1 SCP-3 + phase-d-tasks.md D1.
16
+ // Reversal: set isPlanningPath to consult BOOTSTRAP_NOOP_PLANNING_PATHS
17
+ // again (restore the prefix-match body below) to re-narrow the no-op to
18
+ // the explicit planning prefixes; or, for full pre-D behaviour, also set
19
+ // BOOTSTRAP_NOOP_PLANNING_PATHS to []. The const is retained as the
20
+ // documented revert seam.
21
+ //
22
+ // Per K1 SCP-3 + phase-d-tasks.md D1 + phase-e-tasks.md E2.
12
23
 
13
24
  /**
14
- * Path prefixes that are owned by plan-chat-spa. Bootstrap injection
15
- * is suppressed for these so the SPA's readBearer() resolver is the
16
- * sole auth path on planning surfaces.
25
+ * Path prefixes that WERE owned by plan-chat-spa under the Phase D
26
+ * selective no-op. Retained as the documented single-line revert seam:
27
+ * to re-narrow the bootstrap no-op back to only the planning surfaces,
28
+ * restore the prefix-match body in isPlanningPath() (see git history of
29
+ * this file at the Phase E5 commit) so it consults this array again.
17
30
  *
18
31
  * Format: include both the bare segment ("/plan") and the trailing-slash
19
32
  * variant ("/plan/"). The trailing-slash form is the prefix-match
@@ -27,30 +40,19 @@ export const BOOTSTRAP_NOOP_PLANNING_PATHS = Object.freeze([
27
40
  ]);
28
41
 
29
42
  /**
30
- * True when `pathname` is a planning surface owned by plan-chat-spa.
43
+ * Phase E5 wildcard: TRUE for every string path.
31
44
  *
32
- * Matches:
33
- * /plan
34
- * /plan/
35
- * /plan/<sid>
36
- * /plan/<sid>/<anything>
45
+ * host-cp now serves plan-chat-spa exclusively, whose bundle re-homes the
46
+ * cookie-bootstrap + world-fetch-rewrite shim (worldFetch.ts). No served
47
+ * path needs host-cp's BOOTSTRAP_SCRIPT injection anymore, so every path
48
+ * is treated as an SPA-owned ("planning") path and skips bootstrap.
37
49
  *
38
- * Does NOT match:
39
- * /plan-something (different top-level segment)
40
- * /workspaces/plan (planning isn't the top segment)
41
- * /plans (different segment)
42
- * undefined / null (defensive)
50
+ * Returns false only for non-string input (defensive — a non-string
51
+ * pathname is never a real served path).
43
52
  *
44
53
  * @param {unknown} pathname
45
54
  * @returns {boolean}
46
55
  */
47
56
  export function isPlanningPath(pathname) {
48
- if (typeof pathname !== 'string') return false;
49
- for (const prefix of BOOTSTRAP_NOOP_PLANNING_PATHS) {
50
- if (pathname === prefix) return true;
51
- const bare = prefix.replace(/\/$/, '');
52
- if (pathname === bare) return true;
53
- if (prefix.endsWith('/') && pathname.startsWith(prefix)) return true;
54
- }
55
- return false;
57
+ return typeof pathname === 'string';
56
58
  }
@@ -23,6 +23,13 @@
23
23
  // in-memory buffer; overflow drops oldest events with an
24
24
  // `:overflow` comment so consumers know they missed updates.
25
25
  // - E4: per-event-type broadcast counter + sink count metric line.
26
+ // - E5: the metrics tick ALSO broadcasts a `stream.health` typed event
27
+ // carrying the same counters it logs, so any SPA tab can observe
28
+ // live stream health (sink count, per-event broadcast rates,
29
+ // overflow drops) without polling. Snapshot-cached like every
30
+ // other state event — reconnecting clients replay the last
31
+ // health payload immediately (first-paint parity). Opt out via
32
+ // `deps.healthEvents = false`.
26
33
  //
27
34
  // Pure module: no docker, no DB, no global clock except `setInterval`
28
35
  // for the heartbeat/metrics timers (injectable in tests). Wiring those
@@ -43,7 +50,9 @@ import crypto from 'node:crypto';
43
50
  * @property {number} [debounceMs.default] default trailing-edge ms (Phase E1)
44
51
  * @property {number} [heartbeatMs] per-sink heartbeat interval (Phase E2)
45
52
  * @property {number} [metricsMs] per-broadcaster metrics tick (Phase E4)
53
+ * @property {boolean} [healthEvents] broadcast `stream.health` on each metrics tick (Phase E5; default true)
46
54
  * @property {number} [maxQueuedPerSink] bounded queue size (Phase E3)
55
+ * @property {() => number} [now] injectable clock for `stream.health.at` (tests)
47
56
  * @property {(cb: () => void, ms: number) => any} [setTimer] injectable setInterval (tests)
48
57
  * @property {(handle: any) => void} [clearTimer] injectable clearInterval (tests)
49
58
  */
@@ -66,6 +75,26 @@ import crypto from 'node:crypto';
66
75
  * @property {number} overflows total `:overflow` drops since last reset
67
76
  */
68
77
 
78
+ /**
79
+ * Payload wire-shape for the `stream.health` event (Phase E5). A
80
+ * point-in-time projection of the broadcaster's own observability
81
+ * counters, emitted on each metrics tick. `events` carries the
82
+ * per-event-type broadcast counts accrued during the just-elapsed
83
+ * interval (reset afterward), so consumers see a per-interval RATE
84
+ * rather than a monotonic total. `at` is the wall-clock emit time so a
85
+ * reconnecting client can tell how stale the replayed snapshot is.
86
+ *
87
+ * @typedef {object} StreamHealthPayload
88
+ * @property {Record<string, number>} events per-event broadcasts during the interval
89
+ * @property {number} sinks active-sink count at emit time
90
+ * @property {number} overflows `:overflow` drops during the interval
91
+ * @property {number} intervalMs the metrics-tick cadence that produced this payload
92
+ * @property {number} at Date.now() at emit time
93
+ */
94
+
95
+ /** Event type emitted by the metrics tick (Phase E5). */
96
+ export const STREAM_HEALTH_EVENT = 'stream.health';
97
+
69
98
  const DEFAULT_DEBOUNCE_MS = 100;
70
99
  const DEFAULT_HEARTBEAT_MS = 25_000;
71
100
  const DEFAULT_METRICS_MS = 60_000;
@@ -104,6 +133,8 @@ export function createHostStream(deps = {}) {
104
133
  const defaultDebounceMs = deps.debounceMs?.default ?? DEFAULT_DEBOUNCE_MS;
105
134
  const heartbeatMs = deps.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
106
135
  const metricsMs = deps.metricsMs ?? DEFAULT_METRICS_MS;
136
+ const healthEvents = deps.healthEvents ?? true;
137
+ const now = deps.now ?? (() => Date.now());
107
138
  const maxQueuedPerSink = deps.maxQueuedPerSink ?? DEFAULT_MAX_QUEUED;
108
139
  const setTimer = deps.setTimer ?? ((cb, ms) => setInterval(cb, ms));
109
140
  const clearTimer = deps.clearTimer ?? ((h) => clearInterval(h));
@@ -273,6 +304,27 @@ export function createHostStream(deps = {}) {
273
304
  const events = {};
274
305
  for (const [type, count] of eventCounters) events[type] = count;
275
306
  log(`events=${JSON.stringify(events)} sinks=${sinks.size}${overflowCounter > 0 ? ` overflows=${overflowCounter}` : ''}`);
307
+
308
+ // Phase E5: broadcast the same counters as a typed `stream.health`
309
+ // event so SPA tabs can observe live stream health without polling.
310
+ // Built from the interval's counters BEFORE the reset below, so the
311
+ // payload is a per-interval rate. The broadcast itself bumps the
312
+ // `stream.health` counter, but the immediately-following reset wipes
313
+ // it — the next interval never double-counts this tick's own emit.
314
+ // Bypasses debounce (immediate path) since each tick is already
315
+ // rate-limited to the metrics cadence.
316
+ if (healthEvents) {
317
+ /** @type {StreamHealthPayload} */
318
+ const payload = {
319
+ events,
320
+ sinks: sinks.size,
321
+ overflows: overflowCounter,
322
+ intervalMs: metricsMs,
323
+ at: now(),
324
+ };
325
+ doBroadcast(STREAM_HEALTH_EVENT, payload);
326
+ }
327
+
276
328
  eventCounters.clear();
277
329
  overflowCounter = 0;
278
330
  }