@openrig/cli 0.4.3 → 0.4.4

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 (287) hide show
  1. package/daemon/assets/guidance/openrig-start.md +6 -0
  2. package/daemon/assets/plugins/openrig-core/skills/mission-slice-sop/SKILL.md +49 -21
  3. package/daemon/assets/plugins/openrig-core/skills/openrig-user/SKILL.md +12 -12
  4. package/daemon/dist/build-info.d.ts +12 -0
  5. package/daemon/dist/build-info.d.ts.map +1 -0
  6. package/daemon/dist/build-info.js +11 -0
  7. package/daemon/dist/build-info.js.map +1 -0
  8. package/daemon/dist/db/migrations/048_queue_item_evidence_ref.d.ts +18 -0
  9. package/daemon/dist/db/migrations/048_queue_item_evidence_ref.d.ts.map +1 -0
  10. package/daemon/dist/db/migrations/048_queue_item_evidence_ref.js +22 -0
  11. package/daemon/dist/db/migrations/048_queue_item_evidence_ref.js.map +1 -0
  12. package/daemon/dist/domain/feed/attention-aggregator.d.ts +31 -0
  13. package/daemon/dist/domain/feed/attention-aggregator.d.ts.map +1 -0
  14. package/daemon/dist/domain/feed/attention-aggregator.js +119 -0
  15. package/daemon/dist/domain/feed/attention-aggregator.js.map +1 -0
  16. package/daemon/dist/domain/files/file-write-service.d.ts +11 -2
  17. package/daemon/dist/domain/files/file-write-service.d.ts.map +1 -1
  18. package/daemon/dist/domain/files/file-write-service.js +77 -0
  19. package/daemon/dist/domain/files/file-write-service.js.map +1 -1
  20. package/daemon/dist/domain/hosts/fanout-contract.d.ts +33 -0
  21. package/daemon/dist/domain/hosts/fanout-contract.d.ts.map +1 -0
  22. package/daemon/dist/domain/hosts/fanout-contract.js +23 -0
  23. package/daemon/dist/domain/hosts/fanout-contract.js.map +1 -0
  24. package/daemon/dist/domain/hosts/hosts-registry-reader.d.ts +49 -0
  25. package/daemon/dist/domain/hosts/hosts-registry-reader.d.ts.map +1 -0
  26. package/daemon/dist/domain/hosts/hosts-registry-reader.js +165 -0
  27. package/daemon/dist/domain/hosts/hosts-registry-reader.js.map +1 -0
  28. package/daemon/dist/domain/hosts/remote-daemon-http.d.ts +35 -0
  29. package/daemon/dist/domain/hosts/remote-daemon-http.d.ts.map +1 -0
  30. package/daemon/dist/domain/hosts/remote-daemon-http.js +105 -0
  31. package/daemon/dist/domain/hosts/remote-daemon-http.js.map +1 -0
  32. package/daemon/dist/domain/human-route-enforcer.d.ts +71 -0
  33. package/daemon/dist/domain/human-route-enforcer.d.ts.map +1 -0
  34. package/daemon/dist/domain/human-route-enforcer.js +80 -0
  35. package/daemon/dist/domain/human-route-enforcer.js.map +1 -0
  36. package/daemon/dist/domain/mission-control/audit-browse.d.ts +8 -0
  37. package/daemon/dist/domain/mission-control/audit-browse.d.ts.map +1 -1
  38. package/daemon/dist/domain/mission-control/audit-browse.js +19 -0
  39. package/daemon/dist/domain/mission-control/audit-browse.js.map +1 -1
  40. package/daemon/dist/domain/mission-control/mission-control-action-log.d.ts +1 -1
  41. package/daemon/dist/domain/mission-control/mission-control-action-log.d.ts.map +1 -1
  42. package/daemon/dist/domain/mission-control/mission-control-action-log.js +4 -0
  43. package/daemon/dist/domain/mission-control/mission-control-action-log.js.map +1 -1
  44. package/daemon/dist/domain/mission-control/mission-control-fleet-cli-capability.js +1 -1
  45. package/daemon/dist/domain/mission-control/mission-control-fleet-cli-capability.js.map +1 -1
  46. package/daemon/dist/domain/mission-control/mission-control-read-layer.js +1 -1
  47. package/daemon/dist/domain/mission-control/mission-control-read-layer.js.map +1 -1
  48. package/daemon/dist/domain/mission-control/mission-control-write-contract.d.ts +28 -0
  49. package/daemon/dist/domain/mission-control/mission-control-write-contract.d.ts.map +1 -1
  50. package/daemon/dist/domain/mission-control/mission-control-write-contract.js +125 -0
  51. package/daemon/dist/domain/mission-control/mission-control-write-contract.js.map +1 -1
  52. package/daemon/dist/domain/ps-projection.d.ts +28 -1
  53. package/daemon/dist/domain/ps-projection.d.ts.map +1 -1
  54. package/daemon/dist/domain/ps-projection.js +39 -0
  55. package/daemon/dist/domain/ps-projection.js.map +1 -1
  56. package/daemon/dist/domain/queue-repository.d.ts +25 -1
  57. package/daemon/dist/domain/queue-repository.d.ts.map +1 -1
  58. package/daemon/dist/domain/queue-repository.js +111 -7
  59. package/daemon/dist/domain/queue-repository.js.map +1 -1
  60. package/daemon/dist/domain/review/brief-spine.d.ts +19 -0
  61. package/daemon/dist/domain/review/brief-spine.d.ts.map +1 -0
  62. package/daemon/dist/domain/review/brief-spine.js +86 -0
  63. package/daemon/dist/domain/review/brief-spine.js.map +1 -0
  64. package/daemon/dist/domain/review/compose.d.ts +228 -0
  65. package/daemon/dist/domain/review/compose.d.ts.map +1 -0
  66. package/daemon/dist/domain/review/compose.js +773 -0
  67. package/daemon/dist/domain/review/compose.js.map +1 -0
  68. package/daemon/dist/domain/review/freeze.d.ts +31 -0
  69. package/daemon/dist/domain/review/freeze.d.ts.map +1 -0
  70. package/daemon/dist/domain/review/freeze.js +257 -0
  71. package/daemon/dist/domain/review/freeze.js.map +1 -0
  72. package/daemon/dist/domain/review/gather.d.ts +100 -0
  73. package/daemon/dist/domain/review/gather.d.ts.map +1 -0
  74. package/daemon/dist/domain/review/gather.js +676 -0
  75. package/daemon/dist/domain/review/gather.js.map +1 -0
  76. package/daemon/dist/domain/review/types.d.ts +258 -0
  77. package/daemon/dist/domain/review/types.d.ts.map +1 -0
  78. package/daemon/dist/domain/review/types.js +35 -0
  79. package/daemon/dist/domain/review/types.js.map +1 -0
  80. package/daemon/dist/domain/scope/scope-approve.d.ts +64 -0
  81. package/daemon/dist/domain/scope/scope-approve.d.ts.map +1 -0
  82. package/daemon/dist/domain/scope/scope-approve.js +153 -0
  83. package/daemon/dist/domain/scope/scope-approve.js.map +1 -0
  84. package/daemon/dist/domain/scope/scope-audit.d.ts +9 -1
  85. package/daemon/dist/domain/scope/scope-audit.d.ts.map +1 -1
  86. package/daemon/dist/domain/scope/scope-audit.js +173 -1
  87. package/daemon/dist/domain/scope/scope-audit.js.map +1 -1
  88. package/daemon/dist/domain/topology/multi-rig-launcher.d.ts +49 -0
  89. package/daemon/dist/domain/topology/multi-rig-launcher.d.ts.map +1 -0
  90. package/daemon/dist/domain/topology/multi-rig-launcher.js +132 -0
  91. package/daemon/dist/domain/topology/multi-rig-launcher.js.map +1 -0
  92. package/daemon/dist/domain/topology/remote-up-leaf.d.ts +29 -0
  93. package/daemon/dist/domain/topology/remote-up-leaf.d.ts.map +1 -0
  94. package/daemon/dist/domain/topology/remote-up-leaf.js +60 -0
  95. package/daemon/dist/domain/topology/remote-up-leaf.js.map +1 -0
  96. package/daemon/dist/domain/topology/topology-manifest.d.ts +34 -0
  97. package/daemon/dist/domain/topology/topology-manifest.d.ts.map +1 -0
  98. package/daemon/dist/domain/topology/topology-manifest.js +195 -0
  99. package/daemon/dist/domain/topology/topology-manifest.js.map +1 -0
  100. package/daemon/dist/domain/types.d.ts +5 -0
  101. package/daemon/dist/domain/types.d.ts.map +1 -1
  102. package/daemon/dist/domain/types.js.map +1 -1
  103. package/daemon/dist/domain/up-command-router.d.ts +1 -1
  104. package/daemon/dist/domain/up-command-router.d.ts.map +1 -1
  105. package/daemon/dist/domain/up-command-router.js +34 -2
  106. package/daemon/dist/domain/up-command-router.js.map +1 -1
  107. package/daemon/dist/domain/user-settings/settings-store.d.ts +17 -0
  108. package/daemon/dist/domain/user-settings/settings-store.d.ts.map +1 -1
  109. package/daemon/dist/domain/user-settings/settings-store.js +109 -0
  110. package/daemon/dist/domain/user-settings/settings-store.js.map +1 -1
  111. package/daemon/dist/domain/workspace/default-workspace-scaffold.d.ts.map +1 -1
  112. package/daemon/dist/domain/workspace/default-workspace-scaffold.js +51 -6
  113. package/daemon/dist/domain/workspace/default-workspace-scaffold.js.map +1 -1
  114. package/daemon/dist/domain/workspace/workspace-doctor.d.ts +11 -0
  115. package/daemon/dist/domain/workspace/workspace-doctor.d.ts.map +1 -1
  116. package/daemon/dist/domain/workspace/workspace-doctor.js +82 -0
  117. package/daemon/dist/domain/workspace/workspace-doctor.js.map +1 -1
  118. package/daemon/dist/routes/config.d.ts.map +1 -1
  119. package/daemon/dist/routes/config.js +15 -6
  120. package/daemon/dist/routes/config.js.map +1 -1
  121. package/daemon/dist/routes/files.d.ts.map +1 -1
  122. package/daemon/dist/routes/files.js +45 -1
  123. package/daemon/dist/routes/files.js.map +1 -1
  124. package/daemon/dist/routes/mission-control.d.ts.map +1 -1
  125. package/daemon/dist/routes/mission-control.js +67 -2
  126. package/daemon/dist/routes/mission-control.js.map +1 -1
  127. package/daemon/dist/routes/queue.d.ts +8 -2
  128. package/daemon/dist/routes/queue.d.ts.map +1 -1
  129. package/daemon/dist/routes/queue.js +44 -4
  130. package/daemon/dist/routes/queue.js.map +1 -1
  131. package/daemon/dist/routes/review.d.ts +3 -0
  132. package/daemon/dist/routes/review.d.ts.map +1 -0
  133. package/daemon/dist/routes/review.js +169 -0
  134. package/daemon/dist/routes/review.js.map +1 -0
  135. package/daemon/dist/routes/scope-approve.d.ts +3 -0
  136. package/daemon/dist/routes/scope-approve.d.ts.map +1 -0
  137. package/daemon/dist/routes/scope-approve.js +55 -0
  138. package/daemon/dist/routes/scope-approve.js.map +1 -0
  139. package/daemon/dist/routes/scope-audit.d.ts.map +1 -1
  140. package/daemon/dist/routes/scope-audit.js +28 -1
  141. package/daemon/dist/routes/scope-audit.js.map +1 -1
  142. package/daemon/dist/routes/slices.d.ts.map +1 -1
  143. package/daemon/dist/routes/slices.js +49 -9
  144. package/daemon/dist/routes/slices.js.map +1 -1
  145. package/daemon/dist/routes/terminal-ws.d.ts.map +1 -1
  146. package/daemon/dist/routes/terminal-ws.js +58 -21
  147. package/daemon/dist/routes/terminal-ws.js.map +1 -1
  148. package/daemon/dist/routes/up.d.ts.map +1 -1
  149. package/daemon/dist/routes/up.js +84 -0
  150. package/daemon/dist/routes/up.js.map +1 -1
  151. package/daemon/dist/routes/workspace.js +2 -2
  152. package/daemon/dist/server.d.ts +1 -0
  153. package/daemon/dist/server.d.ts.map +1 -1
  154. package/daemon/dist/server.js +14 -1
  155. package/daemon/dist/server.js.map +1 -1
  156. package/daemon/dist/startup.d.ts.map +1 -1
  157. package/daemon/dist/startup.js +18 -3
  158. package/daemon/dist/startup.js.map +1 -1
  159. package/daemon/docs/reference/agent-startup-guide.md +1 -1
  160. package/daemon/docs/reference/product-factory-vps-runbook.md +132 -0
  161. package/daemon/docs/reference/sdlc-conventions.md +187 -0
  162. package/daemon/specs/agents/conveyor/builder/agent.yaml +1 -1
  163. package/daemon/specs/agents/conveyor/builder/guidance/role.md +1 -0
  164. package/daemon/specs/agents/conveyor/lead/agent.yaml +1 -1
  165. package/daemon/specs/agents/conveyor/lead/guidance/role.md +1 -0
  166. package/daemon/specs/agents/conveyor/planner/agent.yaml +1 -1
  167. package/daemon/specs/agents/conveyor/planner/guidance/role.md +1 -0
  168. package/daemon/specs/agents/conveyor/reviewer/agent.yaml +1 -1
  169. package/daemon/specs/agents/conveyor/reviewer/guidance/role.md +1 -0
  170. package/daemon/specs/agents/design/product-designer/agent.yaml +1 -1
  171. package/daemon/specs/agents/design/product-designer/guidance/role.md +1 -0
  172. package/daemon/specs/agents/development/implementer/agent.yaml +1 -1
  173. package/daemon/specs/agents/development/implementer/guidance/role.md +1 -0
  174. package/daemon/specs/agents/development/qa/agent.yaml +1 -1
  175. package/daemon/specs/agents/development/qa/guidance/role.md +1 -0
  176. package/daemon/specs/agents/orchestration/orchestrator/agent.yaml +1 -1
  177. package/daemon/specs/agents/orchestration/orchestrator/guidance/role.md +1 -0
  178. package/daemon/specs/agents/product-management/pm/agent.yaml +1 -0
  179. package/daemon/specs/agents/product-management/pm/guidance/role.md +13 -0
  180. package/daemon/specs/agents/review/independent-reviewer/agent.yaml +1 -1
  181. package/daemon/specs/agents/review/independent-reviewer/guidance/role.md +1 -0
  182. package/daemon/specs/agents/shared/agent.yaml +2 -0
  183. package/daemon/specs/agents/shared/skills/core/mission-slice-sop/SKILL.md +115 -0
  184. package/daemon/specs/agents/shared/skills/core/openrig-operator/SKILL.md +4 -4
  185. package/daemon/specs/agents/shared/skills/core/openrig-user/SKILL.md +69 -23
  186. package/daemon/specs/agents/shared/skills/pods/orchestration-team/SKILL.md +2 -2
  187. package/daemon/specs/rigs/focused/pm-team/CULTURE.md +4 -0
  188. package/daemon/specs/rigs/launch/kernel/agents/operator/agent/guidance/role.md +1 -1
  189. package/daemon/specs/rigs/launch/kernel/agents/operator/agent/startup/context.md +1 -1
  190. package/daemon/specs/rigs/preview/product-team/CULTURE.md +4 -0
  191. package/dist/build-info.d.ts +8 -0
  192. package/dist/build-info.d.ts.map +1 -0
  193. package/dist/build-info.js +11 -0
  194. package/dist/build-info.js.map +1 -0
  195. package/dist/commands/config-init-workspace.d.ts.map +1 -1
  196. package/dist/commands/config-init-workspace.js +20 -4
  197. package/dist/commands/config-init-workspace.js.map +1 -1
  198. package/dist/commands/doctor.js +2 -2
  199. package/dist/commands/doctor.js.map +1 -1
  200. package/dist/commands/expand.js +1 -1
  201. package/dist/commands/expand.js.map +1 -1
  202. package/dist/commands/file.d.ts +7 -0
  203. package/dist/commands/file.d.ts.map +1 -0
  204. package/dist/commands/file.js +77 -0
  205. package/dist/commands/file.js.map +1 -0
  206. package/dist/commands/host.d.ts +34 -0
  207. package/dist/commands/host.d.ts.map +1 -0
  208. package/dist/commands/host.js +448 -0
  209. package/dist/commands/host.js.map +1 -0
  210. package/dist/commands/proof.d.ts +54 -0
  211. package/dist/commands/proof.d.ts.map +1 -0
  212. package/dist/commands/proof.js +311 -0
  213. package/dist/commands/proof.js.map +1 -0
  214. package/dist/commands/ps.d.ts +41 -7
  215. package/dist/commands/ps.d.ts.map +1 -1
  216. package/dist/commands/ps.js +343 -131
  217. package/dist/commands/ps.js.map +1 -1
  218. package/dist/commands/queue.d.ts.map +1 -1
  219. package/dist/commands/queue.js +76 -0
  220. package/dist/commands/queue.js.map +1 -1
  221. package/dist/commands/scope.d.ts.map +1 -1
  222. package/dist/commands/scope.js +140 -3
  223. package/dist/commands/scope.js.map +1 -1
  224. package/dist/commands/send.js +13 -4
  225. package/dist/commands/send.js.map +1 -1
  226. package/dist/commands/up.d.ts +2 -0
  227. package/dist/commands/up.d.ts.map +1 -1
  228. package/dist/commands/up.js +69 -2
  229. package/dist/commands/up.js.map +1 -1
  230. package/dist/commands/workspace.js +6 -6
  231. package/dist/commands/workspace.js.map +1 -1
  232. package/dist/config-store.d.ts +4 -0
  233. package/dist/config-store.d.ts.map +1 -1
  234. package/dist/config-store.js +68 -0
  235. package/dist/config-store.js.map +1 -1
  236. package/dist/cross-host-executor.d.ts +1 -0
  237. package/dist/cross-host-executor.d.ts.map +1 -1
  238. package/dist/cross-host-executor.js +4 -1
  239. package/dist/cross-host-executor.js.map +1 -1
  240. package/dist/daemon-lifecycle.d.ts.map +1 -1
  241. package/dist/daemon-lifecycle.js +4 -1
  242. package/dist/daemon-lifecycle.js.map +1 -1
  243. package/dist/host-registry.d.ts +9 -0
  244. package/dist/host-registry.d.ts.map +1 -1
  245. package/dist/host-registry.js +32 -2
  246. package/dist/host-registry.js.map +1 -1
  247. package/dist/index.d.ts.map +1 -1
  248. package/dist/index.js +7 -0
  249. package/dist/index.js.map +1 -1
  250. package/dist/lib/file-transfer.d.ts +89 -0
  251. package/dist/lib/file-transfer.d.ts.map +1 -0
  252. package/dist/lib/file-transfer.js +300 -0
  253. package/dist/lib/file-transfer.js.map +1 -0
  254. package/dist/lib/hosts/fanout-contract.d.ts +33 -0
  255. package/dist/lib/hosts/fanout-contract.d.ts.map +1 -0
  256. package/dist/lib/hosts/fanout-contract.js +23 -0
  257. package/dist/lib/hosts/fanout-contract.js.map +1 -0
  258. package/dist/lib/scope/scope-audit.d.ts +9 -1
  259. package/dist/lib/scope/scope-audit.d.ts.map +1 -1
  260. package/dist/lib/scope/scope-audit.js +173 -1
  261. package/dist/lib/scope/scope-audit.js.map +1 -1
  262. package/dist/lib/scope/templates.d.ts +1 -0
  263. package/dist/lib/scope/templates.d.ts.map +1 -1
  264. package/dist/lib/scope/templates.js +9 -0
  265. package/dist/lib/scope/templates.js.map +1 -1
  266. package/dist/lib/scope-templates/backlog-deprecation.md +16 -0
  267. package/dist/lib/scope-templates/backlog-tech-debt.md +16 -0
  268. package/dist/lib/scope-templates/bug-fix.md +16 -0
  269. package/dist/lib/scope-templates/implementation-prd.md +33 -0
  270. package/dist/lib/scope-templates/mission-placeholder.md +1 -1
  271. package/dist/lib/scope-templates/mission-progress.md +1 -1
  272. package/dist/lib/scope-templates/mission-release.md +1 -1
  273. package/dist/lib/scope-templates/placeholder.md +11 -7
  274. package/dist/lib/scope-templates/proof.md +5 -1
  275. package/dist/lib/scope-templates/release-feature.md +14 -6
  276. package/dist/lib/scope-templates/research.md +16 -0
  277. package/dist/lib/scope-templates/slice-progress.md +1 -1
  278. package/dist/version.d.ts +5 -0
  279. package/dist/version.d.ts.map +1 -1
  280. package/dist/version.js +11 -1
  281. package/dist/version.js.map +1 -1
  282. package/package.json +1 -1
  283. package/ui/dist/assets/index-D69ZhNIr.js +598 -0
  284. package/ui/dist/assets/index-DJYun-8d.css +32 -0
  285. package/ui/dist/index.html +2 -2
  286. package/ui/dist/assets/index-DYZniQcc.js +0 -597
  287. package/ui/dist/assets/index-trcb4Yf_.css +0 -32
@@ -55,6 +55,7 @@ const NUMERIC_OPERATORS = [">=", "<=", ">", "<", "="];
55
55
  // rig-level only; `rigName` is the alias and exists at both levels (per the
56
56
  // closed `rig ps trust and scale-safety` slice at openrig 0a9fb43).
57
57
  const ALLOWED_RIG_FIELDS = new Set([
58
+ "attentionCount",
58
59
  "rigId",
59
60
  "name",
60
61
  "rigName",
@@ -196,6 +197,45 @@ function parseFields(input, allowed, level) {
196
197
  }
197
198
  return fields;
198
199
  }
200
+ function parsePsControls(opts) {
201
+ let effectiveFilter = opts.filter;
202
+ if (opts.active) {
203
+ if (effectiveFilter) {
204
+ return {
205
+ error: `--active and --filter cannot be combined. ` +
206
+ `--active is sugar for --filter agentActivity.state=running. ` +
207
+ `Pick one form, or compose by upgrading to --filter directly.`,
208
+ };
209
+ }
210
+ effectiveFilter = "agentActivity.state=running";
211
+ }
212
+ let parsedFilter = null;
213
+ if (effectiveFilter) {
214
+ const result = parseFilter(effectiveFilter);
215
+ if ("error" in result)
216
+ return result;
217
+ parsedFilter = result;
218
+ }
219
+ const limit = opts.limit !== undefined ? Number(opts.limit) : null;
220
+ if (limit !== null && (!Number.isInteger(limit) || limit < 0)) {
221
+ return { error: `--limit must be a non-negative integer; got '${opts.limit}'` };
222
+ }
223
+ let fields = null;
224
+ if (opts.fields !== undefined) {
225
+ const fieldsLevel = opts.nodes ? "nodes" : "rig";
226
+ const fieldsAllowed = fieldsLevel === "nodes" ? ALLOWED_NODE_FIELDS : ALLOWED_RIG_FIELDS;
227
+ const fieldsResult = parseFields(opts.fields, fieldsAllowed, fieldsLevel);
228
+ if ("error" in fieldsResult)
229
+ return fieldsResult;
230
+ fields = fieldsResult;
231
+ }
232
+ return {
233
+ parsedFilter,
234
+ limit,
235
+ fields,
236
+ useEnvelope: parsedFilter !== null || limit !== null || fields !== null || opts.summary === true,
237
+ };
238
+ }
199
239
  function applyRigFilter(entries, filter) {
200
240
  return entries.filter((e) => {
201
241
  if (filter.key === "status")
@@ -258,6 +298,65 @@ function applyNodeFilter(entries, filter) {
258
298
  // OPR.0.3.3.19 - build the /api/ps path, opting archived rigs back in when
259
299
  // --include-archived is set. Default (no flag) leaves the daemon's
260
300
  // archived-excluding default in force.
301
+ function fanOutNodesError() {
302
+ return [
303
+ "rig ps --all-hosts/--hosts --nodes: per-node fan-out requires the FULL explicit ladder.",
304
+ " rig ps --all-hosts --nodes -A (fleet nodes per host, projected rows)",
305
+ " rig ps --all-hosts --nodes -A --full (complete per-node records — the last rung)",
306
+ "One rig's seats on one host: rig ps --host <id> --nodes --rig <name>.",
307
+ ].join("\n");
308
+ }
309
+ /**
310
+ * OPR.0.4.4.21 FR-2/FR-3 — THE centralized disclosure-ladder validator.
311
+ * Runs BEFORE the local/HTTP-host/SSH/fan-out dispatch split so every path
312
+ * obeys one grammar (qa1 plan-review: remote mode bypassed the session-rig
313
+ * injection and fanned wide). Principle (arch-ratified): IMPLICIT SCOPE
314
+ * DEFAULTS DON'T CROSS HOST BOUNDARIES — the session-rig default has no
315
+ * stable referent on a remote host (a same-named rig there would silently
316
+ * misresolve), so remote per-node views are explicit-or-error.
317
+ * Returns the teaching error text, or null when the invocation is valid.
318
+ */
319
+ export function validatePsLadder(opts, callerRig) {
320
+ const isFanOut = !!(opts.allHosts || opts.hosts);
321
+ const isSingleRemote = !!opts.host;
322
+ // FR-3: -A/--all-rigs has exactly ONE meaning — the --nodes fleet widener.
323
+ if (opts.allRigs && !opts.nodes) {
324
+ return [
325
+ "rig ps -A: '-A/--all-rigs' now has exactly one meaning — the --nodes fleet widener.",
326
+ "The consolidated all-rigs view IS the default: just run 'rig ps'.",
327
+ "Fleet nodes: rig ps --nodes -A (add --full for complete per-node records).",
328
+ "Archived history: rig ps --include-archived.",
329
+ ].join("\n");
330
+ }
331
+ // FR-5: multi-host fan-out is rollup-only UNLESS the full explicit
332
+ // ladder is requested — `--nodes -A` per host (`--full` for complete
333
+ // records), exactly as the PRD writes it. Anything less explicit under
334
+ // --nodes errors; never silent rig-tier data under a --nodes flag.
335
+ if (opts.nodes && isFanOut && !opts.allRigs) {
336
+ return fanOutNodesError();
337
+ }
338
+ // FR-2: --nodes always names its scope — session default locally,
339
+ // explicit --rig / -A everywhere else. Never an implicit fan-out.
340
+ if (opts.nodes && !opts.rig && !opts.allRigs) {
341
+ if (isFanOut) {
342
+ return fanOutNodesError();
343
+ }
344
+ if (isSingleRemote) {
345
+ return [
346
+ `rig ps --host ${opts.host} --nodes: no target — the local session's rig is not a remote scope`,
347
+ "(implicit scope defaults don't cross host boundaries; a same-named remote rig would silently misresolve).",
348
+ `Name one: rig ps --host ${opts.host} --nodes --rig <name>, or that host's fleet explicitly: rig ps --host ${opts.host} --nodes -A.`,
349
+ ].join("\n");
350
+ }
351
+ if (!callerRig) {
352
+ return [
353
+ "rig ps --nodes: no target — outside a managed session there is no current rig to default to.",
354
+ "Name one: rig ps --nodes --rig <name>, or go fleet-wide explicitly: rig ps --nodes -A.",
355
+ ].join("\n");
356
+ }
357
+ }
358
+ return null;
359
+ }
261
360
  function psApiPath(opts) {
262
361
  return opts.includeArchived ? "/api/ps?includeArchived=true" : "/api/ps";
263
362
  }
@@ -269,6 +368,12 @@ function selectFields(entries, fields) {
269
368
  return out;
270
369
  });
271
370
  }
371
+ function selectFanOutFields(entries, fields) {
372
+ return entries.map((e) => {
373
+ const [selected] = selectFields([e], fields);
374
+ return { ...selected, hostId: e.hostId };
375
+ });
376
+ }
272
377
  function needsAttention(node) {
273
378
  return node.lifecycleState === "attention_required"
274
379
  || node.startupStatus === "attention_required"
@@ -373,22 +478,39 @@ function abbrevNodeLifecycle(state) {
373
478
  return "—";
374
479
  }
375
480
  /**
376
- * `rig ps` — list running rigs and optionally their nodes.
481
+ * `rig ps` — the consolidated fleet map + explicit disclosure ladder.
377
482
  *
378
- * L3-followup adds CLI-side shaping flags (`--limit`, `--fields`, `--summary`,
379
- * `--filter`, `--full`) for context-window-safe output on large hosts. Default
380
- * `rig ps` (human) truncates to ~50 rigs with an explicit footer naming totals
381
- * and the `--full` opt-out. Default `rig ps --json` keeps the bare-array shape
382
- * for back-compat; the truncation/envelope shape only applies when at least
383
- * one of `--limit`/`--summary`/`--fields`/`--filter` is specified.
483
+ * OPR.0.4.4.21: the default is ALL active rigs, one compact O(rigs) row each
484
+ * (the current-rig-only default is retired). The token-safety invariant: no
485
+ * invocation returns per-node detail across all rigs unless explicitly
486
+ * flagged (--nodes -A), and even then rows are projected unless --full.
487
+ * Default `rig ps --json` keeps the bare-array shape for back-compat; the
488
+ * truncation/envelope shape only applies when at least one of
489
+ * `--limit`/`--summary`/`--fields`/`--filter` is specified.
384
490
  */
385
491
  export function psCommand(depsOverride) {
386
492
  const cmd = new Command("ps")
387
493
  .description("List rigs and their status")
388
494
  .addHelpText("after", `
389
- Default: current-rig scope (derived from OPENRIG_SESSION_NAME's @<rig> suffix).
390
- Shows ALL states (stopped, recoverable, attention, idle non-running IS the
391
- actionable signal for topology/readiness). Use -A/--all-rigs for fleet breadth.
495
+ Default (OPR.0.4.4.21): ALL active rigs, ONE compact row each — O(rigs), never
496
+ a fleet node fan-out — plus the host rollup line ("N rigs · M seats · K need
497
+ attention"), the archived/stopped count line, and the drill-ladder footer.
498
+ STATED contract: default --json is a bare array of ALL non-archived rigs
499
+ INCLUDING stopped ones (existing keys preserved; additive attentionCount);
500
+ only the HUMAN table folds stopped rigs into the count line.
501
+
502
+ The disclosure ladder (each heavier view is an explicit step):
503
+ rig ps the consolidated map (this default)
504
+ rig ps --rig <name> one rig's detail
505
+ rig ps --nodes per-node, CURRENT rig (session default, local only)
506
+ rig ps --nodes --rig <name> per-node, named rig
507
+ rig ps --nodes -A fleet nodes, projected rows
508
+ rig ps --nodes -A --full complete per-node records (the only full fan-out)
509
+
510
+ -A/--all-rigs has exactly ONE meaning: the --nodes fleet widener. Bare -A
511
+ errors (all-rigs IS the default; archived history stays behind
512
+ --include-archived). The session-rig default never crosses host boundaries:
513
+ remote --nodes requires an explicit --rig or -A.
392
514
 
393
515
  Compact defaults: 'rig ps --nodes' shows a compact summary per node
394
516
  (rig, session, lifecycle, activity state, reason when attention, queue counts,
@@ -402,11 +524,10 @@ null even with --full; fetch the full recovery guidance + currentUsage from the
402
524
  single-node detail (/api/rigs/:rigId/nodes/:logicalId) or 'rig whoami'.
403
525
 
404
526
  Examples:
405
- rig ps Current rig summary (compact)
406
- rig ps -A All rigs (fleet breadth)
407
- rig ps --full Current rig, full detail
408
- rig ps --json Current rig, compact JSON
409
- rig ps -A --json All rigs, compact JSON
527
+ rig ps All active rigs, one row each + rollup
528
+ rig ps --rig <name> One rig's detail
529
+ rig ps --full All rigs, no table truncation
530
+ rig ps --json All non-archived rigs, bare JSON array
410
531
  rig ps --json --limit 20 Bounded JSON envelope
411
532
  rig ps --json --summary Aggregate-only JSON
412
533
  rig ps --json --fields rigName,status,lifecycleState
@@ -426,12 +547,16 @@ Examples:
426
547
  rig ps --nodes --running Same as --active
427
548
  rig ps --nodes --filter agentActivity.state=running
428
549
  Same as --active (the explicit form)
429
- rig ps --host vm-claude-test --nodes --json Remote host (no current-rig default)
550
+ rig ps --host vm-1 --nodes --rig <name> --json Remote host per-node (explicit target required)
551
+ rig ps --all-hosts Per-host O(rigs) rollups (AggregatedPayload JSON)
430
552
  rig ps --include-archived Include archived rigs (marked with *); hidden by default
431
553
 
432
- --rig <name> overrides the current-rig default. -A/--all-rigs disables it.
433
- --session <name> filters within the effective rig scope; use -A if the target
434
- session is in a different rig.
554
+ --rig <name> scopes to one rig. -A/--all-rigs is ONLY the --nodes fleet widener.
555
+ --session <name> filters within the effective rig scope; use --nodes -A if the
556
+ target session is in a different rig.
557
+ Multi-host fan-out (--all-hosts/--hosts) is rollup-only by default; the full
558
+ explicit ladder (--all-hosts --nodes -A, --full for complete records) fans out
559
+ per-node with hostId-stamped projected rows.
435
560
 
436
561
  --active/--running narrow to agentActivity.state=running. Cannot combine with --filter.
437
562
 
@@ -439,7 +564,8 @@ session is in a different rig.
439
564
  contextUsage.percent, contextUsage.state. Other keys are rejected.
440
565
 
441
566
  --fields accepts (rig-level): rigId, name, rigName, nodeCount, runningCount,
442
- activeCount, hasWorkCount, status, lifecycleState, uptime, latestSnapshot.
567
+ activeCount, hasWorkCount, attentionCount, status, lifecycleState, uptime,
568
+ latestSnapshot.
443
569
  --fields accepts (node-level, with --nodes): rigId, rigName, logicalId, podId,
444
570
  podNamespace, canonicalSessionName, nodeKind, runtime, sessionStatus,
445
571
  startupStatus, restoreOutcome, lifecycleState, tmuxAttachCommand,
@@ -478,15 +604,49 @@ Exit codes:
478
604
  if (opts.running)
479
605
  opts.active = true;
480
606
  const isRemote = !!(opts.host || opts.allHosts || opts.hosts);
481
- if (!opts.allRigs && !opts.rig && !isRemote) {
482
- const sessionName = readOpenRigEnv("OPENRIG_SESSION_NAME", "RIGGED_SESSION_NAME");
483
- const callerRig = sessionName ? extractRigName(sessionName) : undefined;
484
- if (callerRig)
485
- opts.rig = callerRig;
607
+ // OPR.0.4.4.21 FR-1: the rig tier is consolidated ALL-ACTIVE-RIGS by
608
+ // default (the current-rig-only default is RETIRED — it hid running
609
+ // rigs from the operator's field of view). The session-rig default
610
+ // now applies ONLY to the node tier (FR-2's scoped --nodes), and
611
+ // ONLY locally (the ladder validator enforces explicit-or-error on
612
+ // every remote path).
613
+ const sessionName = readOpenRigEnv("OPENRIG_SESSION_NAME", "RIGGED_SESSION_NAME");
614
+ const callerRig = sessionName ? extractRigName(sessionName) : undefined;
615
+ const ladderError = validatePsLadder(opts, callerRig);
616
+ if (ladderError) {
617
+ console.error(ladderError);
618
+ process.exitCode = 1;
619
+ return;
620
+ }
621
+ if (opts.nodes && !opts.allRigs && !opts.rig && !isRemote && callerRig) {
622
+ opts.rig = callerRig;
486
623
  }
487
624
  const deps = getDepsF();
625
+ // OPR.0.4.4.21 rev1-r2 fixback: the shared shaping controls
626
+ // (--active/--filter/--limit/--fields/--summary) parse + validate
627
+ // BEFORE any dispatch — local, single-host, or fan-out — so every
628
+ // path honors the same composition contract and rejections stay
629
+ // pre-HTTP on remote paths too.
630
+ // Parse shared composition controls once up front so local, single-host,
631
+ // and fan-out paths reject malformed --filter/--limit/--fields before
632
+ // any HTTP call and apply the same shaping semantics.
633
+ const controls = parsePsControls(opts);
634
+ if ("error" in controls) {
635
+ console.error(controls.error);
636
+ process.exitCode = 1;
637
+ return;
638
+ }
639
+ const { parsedFilter, limit, fields, useEnvelope } = controls;
488
640
  if (opts.allHosts || opts.hosts) {
489
- await runFanOutPs(opts, deps);
641
+ // AggregatedPayload is the closed fan-out contract (items + hosts).
642
+ // Do not add an ad-hoc summary member here; teach the per-host form.
643
+ if (opts.summary) {
644
+ console.error("rig ps --all-hosts/--hosts --summary: summary does not compose with the merged fan-out payload.\n" +
645
+ "Summarize one host: rig ps --host <id> --summary; or drop --summary for the merged AggregatedPayload.");
646
+ process.exitCode = 1;
647
+ return;
648
+ }
649
+ await runFanOutPs(opts, deps, { parsedFilter, limit, fields });
490
650
  return;
491
651
  }
492
652
  if (opts.host) {
@@ -499,73 +659,31 @@ Exit codes:
499
659
  process.exitCode = 1;
500
660
  return;
501
661
  }
502
- // Parse filter once up front so unknown keys/malformed filters surface
503
- // as exit-1 errors before any HTTP call.
504
- //
505
- // PL-019 item 1: --active is sugar for --filter agentActivity.state=running.
506
- // Combining --active with --filter is rejected (composition is
507
- // ambiguous — pick one explicit form). Parity is verified end-to-end
508
- // by the focused test: `--active` and the explicit filter must yield
509
- // identical output on the same fixture.
510
- let effectiveFilter = opts.filter;
511
- if (opts.active) {
512
- if (effectiveFilter) {
513
- console.error(`--active and --filter cannot be combined. ` +
514
- `--active is sugar for --filter agentActivity.state=running. ` +
515
- `Pick one form, or compose by upgrading to --filter directly.`);
516
- process.exitCode = 1;
517
- return;
518
- }
519
- effectiveFilter = "agentActivity.state=running";
520
- }
521
- let parsedFilter = null;
522
- if (effectiveFilter) {
523
- const result = parseFilter(effectiveFilter);
524
- if ("error" in result) {
525
- console.error(result.error);
526
- process.exitCode = 1;
527
- return;
528
- }
529
- parsedFilter = result;
530
- }
531
- const limit = opts.limit !== undefined ? Number(opts.limit) : null;
532
- if (limit !== null && (!Number.isInteger(limit) || limit < 0)) {
533
- console.error(`--limit must be a non-negative integer; got '${opts.limit}'`);
534
- process.exitCode = 1;
535
- return;
536
- }
537
- // C9a: validate --fields against the per-level allow-list before any HTTP
538
- // call, mirroring the --filter validation above. Level is determined by
539
- // --nodes; both branches downstream use the same `fields` variable, so
540
- // one up-front validation covers both paths.
541
- let fields = null;
542
- if (opts.fields !== undefined) {
543
- const fieldsLevel = opts.nodes ? "nodes" : "rig";
544
- const fieldsAllowed = fieldsLevel === "nodes" ? ALLOWED_NODE_FIELDS : ALLOWED_RIG_FIELDS;
545
- const fieldsResult = parseFields(opts.fields, fieldsAllowed, fieldsLevel);
546
- if ("error" in fieldsResult) {
547
- console.error(fieldsResult.error);
548
- process.exitCode = 1;
549
- return;
550
- }
551
- fields = fieldsResult;
552
- }
553
- const useEnvelope = parsedFilter !== null || limit !== null || fields !== null || opts.summary === true;
554
662
  const client = deps.clientFactory(getDaemonUrl(status));
555
663
  if (opts.nodes) {
556
664
  await handleNodes(client, opts, parsedFilter, limit, fields, useEnvelope);
557
665
  return;
558
666
  }
559
- const res = await client.get(psApiPath(opts));
667
+ // OPR.0.4.4.21 FR-1: ONE O(rigs) fetch including archived so the
668
+ // count line can be computed; visibility is split client-side below
669
+ // (JSON keeps today's default of excluding archived — parity).
670
+ const res = await client.get("/api/ps?includeArchived=true");
560
671
  if (res.status >= 400) {
561
672
  console.error(`Failed to fetch rig list from daemon (HTTP ${res.status}). Check daemon status with: rig status`);
562
673
  process.exitCode = 2;
563
674
  return;
564
675
  }
565
676
  const all = res.data;
677
+ // OPR.0.4.4.21 — archived visibility parity: the daemon call above
678
+ // always includes archived (for the count line); without
679
+ // --include-archived they are dropped from BOTH renders here, exactly
680
+ // as the daemon default used to do. archivedCount feeds the FR-1
681
+ // count line only.
682
+ const archivedCount = all.filter((e) => e.isArchived === true).length;
683
+ const visible = opts.includeArchived ? all : all.filter((e) => e.isArchived !== true);
566
684
  const rigScoped = opts.rig
567
- ? all.filter((e) => (e.rigName ?? e.name) === opts.rig)
568
- : all;
685
+ ? visible.filter((e) => (e.rigName ?? e.name) === opts.rig)
686
+ : visible;
569
687
  // Apply CLI-side filter (Amendment A: prefer CLI shaping).
570
688
  const filtered = parsedFilter ? applyRigFilter(rigScoped, parsedFilter) : rigScoped;
571
689
  // Summary mode short-circuits per-entry output.
@@ -608,15 +726,32 @@ Exit codes:
608
726
  if (parsedFilter) {
609
727
  console.log(`No rigs match --filter ${parsedFilter.key}=${parsedFilter.value}`);
610
728
  }
729
+ else if (archivedCount > 0 && !opts.includeArchived) {
730
+ // Proven-empty with the history pointer, never a bare "No rigs"
731
+ // while archived history exists.
732
+ console.log(`No active rigs · ${archivedCount} archived (rig ps --include-archived)`);
733
+ }
611
734
  else {
612
735
  console.log("No rigs");
613
736
  }
614
737
  return;
615
738
  }
739
+ // OPR.0.4.4.21 FR-1 (human table ONLY — JSON keeps ALL non-archived
740
+ // entries including stopped; this scope split is a STATED contract
741
+ // sentence in the help text): on the BARE default (no filter, no
742
+ // --rig, no --include-archived) stopped rigs fold into the count
743
+ // line — history is what the default drops, not field of view.
744
+ const bareDefault = !parsedFilter && !opts.rig && !opts.includeArchived;
745
+ const tableRows = bareDefault ? limited.filter((e) => e.status !== "stopped") : limited;
746
+ const stoppedCount = bareDefault ? limited.length - tableRows.length : 0;
747
+ // FR-1 display element 1: the host rollup line.
748
+ const rollupSeats = tableRows.reduce((n, e) => n + e.nodeCount, 0);
749
+ const rollupAttention = tableRows.reduce((n, e) => n + (e.attentionCount ?? 0), 0);
750
+ console.log(`${tableRows.length} rig${tableRows.length === 1 ? "" : "s"} · ${rollupSeats} seat${rollupSeats === 1 ? "" : "s"} · ${rollupAttention} need${rollupAttention === 1 ? "s" : ""} attention`);
616
751
  // Human output: apply default truncation budget unless --full.
617
- const humanList = (opts.full || limit !== null) ? limited : limited.slice(0, HUMAN_RIG_BUDGET);
618
- const humanTruncated = !opts.full && limit === null && filtered.length > HUMAN_RIG_BUDGET;
619
- const header = padRigRow("RIG", "NODES", "RUNNING", "ACTIVE", "WORK", "STATUS", "LIFECYCLE", "UPTIME", "SNAPSHOT");
752
+ const humanList = (opts.full || limit !== null) ? tableRows : tableRows.slice(0, HUMAN_RIG_BUDGET);
753
+ const humanTruncated = !opts.full && limit === null && tableRows.length > HUMAN_RIG_BUDGET;
754
+ const header = padRigRow("RIG", "NODES", "RUNNING", "ACTIVE", "WORK", "ATTN", "STATUS", "LIFECYCLE", "UPTIME", "SNAPSHOT");
620
755
  console.log(header);
621
756
  let anyArchivedShown = false;
622
757
  for (const e of humanList) {
@@ -627,11 +762,27 @@ Exit codes:
627
762
  anyArchivedShown = true;
628
763
  console.log(padRigRow(e.isArchived ? `${e.rigName ?? e.name} *` : (e.rigName ?? e.name), String(e.nodeCount), String(e.runningCount),
629
764
  // Slice 15 — "—" when daemon predates the field; honest absence.
630
- e.activeCount !== undefined ? String(e.activeCount) : "—", e.hasWorkCount !== undefined ? String(e.hasWorkCount) : "—", e.status, abbrevRigLifecycle(e.lifecycleState), e.uptime ?? "—", e.latestSnapshot ?? "—"));
765
+ e.activeCount !== undefined ? String(e.activeCount) : "—", e.hasWorkCount !== undefined ? String(e.hasWorkCount) : "—",
766
+ // OPR.0.4.4.21 — the founder's field-of-view anchor: where is
767
+ // something that might concern me. "—" = daemon predates the field.
768
+ e.attentionCount !== undefined ? (e.attentionCount > 0 ? `▲${e.attentionCount}` : "0") : "—", e.status, abbrevRigLifecycle(e.lifecycleState), e.uptime ?? "—", e.latestSnapshot ?? "—"));
631
769
  }
632
770
  if (anyArchivedShown) {
633
771
  console.log("* = archived (hidden from the default view; shown via --include-archived). Reverse with: rig unarchive <rig>");
634
772
  }
773
+ // FR-1 display element 2: history as ONE count line, never rows.
774
+ if (stoppedCount > 0 || (!opts.includeArchived && archivedCount > 0)) {
775
+ const parts = [];
776
+ if (stoppedCount > 0)
777
+ parts.push(`${stoppedCount} stopped (rig ps --filter status=stopped)`);
778
+ if (!opts.includeArchived && archivedCount > 0)
779
+ parts.push(`${archivedCount} archived (rig ps --include-archived)`);
780
+ console.log(`not shown: ${parts.join(" · ")}`);
781
+ }
782
+ // FR-1 display element 3: the affordance footer — the drill ladder.
783
+ if (bareDefault) {
784
+ console.log("drill: rig ps --rig <name> (one rig) · rig ps --nodes --rig <name> (its seats) · --full (everything)");
785
+ }
635
786
  if (humanTruncated) {
636
787
  const remaining = filtered.length - HUMAN_RIG_BUDGET;
637
788
  console.log(`... and ${remaining} more rig${remaining === 1 ? "" : "s"} (truncated at ${HUMAN_RIG_BUDGET}).`);
@@ -666,7 +817,12 @@ async function handleNodes(client, opts, parsedFilter, limit, fields, useEnvelop
666
817
  console.error(`Warning: failed to fetch nodes for rig "${rig.rigName ?? rig.name}" (HTTP ${nodesRes.status}). List rigs with: rig ps`);
667
818
  continue;
668
819
  }
669
- allNodes.push(...nodesRes.data);
820
+ const parentRigName = rig.rigName ?? rig.name;
821
+ allNodes.push(...nodesRes.data.map((n) => ({
822
+ ...n,
823
+ rigId: n.rigId ?? rig.rigId,
824
+ rigName: n.rigName ?? parentRigName,
825
+ })));
670
826
  }
671
827
  let narrowed = allNodes;
672
828
  if (opts.rig)
@@ -769,7 +925,7 @@ function truncate(s, max) {
769
925
  function fitCell(value, width) {
770
926
  return truncate(value, width).padEnd(width);
771
927
  }
772
- function padRigRow(rig, nodes, running, active, work, status, lifecycle, uptime, snapshot) {
928
+ function padRigRow(rig, nodes, running, active, work, attn, status, lifecycle, uptime, snapshot) {
773
929
  return [
774
930
  fitCell(rig, 24),
775
931
  fitCell(nodes, 7),
@@ -780,6 +936,8 @@ function padRigRow(rig, nodes, running, active, work, status, lifecycle, uptime,
780
936
  // operators see which dimension differs at a glance.
781
937
  fitCell(active, 8),
782
938
  fitCell(work, 6),
939
+ // OPR.0.4.4.21 — ATTN: seats needing attention (the field-of-view anchor).
940
+ fitCell(attn, 6),
783
941
  fitCell(status, 10),
784
942
  fitCell(lifecycle, 11),
785
943
  fitCell(uptime, 11),
@@ -878,7 +1036,10 @@ async function runCrossHostPs(hostId, opts, deps) {
878
1036
  // OPR.0.4.0.34: forward the breadth flag so `--host h -A` keeps all-rigs
879
1037
  // breadth across the hop (the current-rig default is local-only and never
880
1038
  // applied to a remote call, but an explicit -A must still reach the remote).
881
- if (opts.allRigs)
1039
+ // OPR.0.4.4.21 FR-3: -A is legal only alongside --nodes (the validator
1040
+ // already rejected the bare form before any dispatch; this guard keeps
1041
+ // the reconstructed remote argv obeying the same grammar).
1042
+ if (opts.allRigs && opts.nodes)
882
1043
  argv.push("--all-rigs");
883
1044
  if (opts.limit !== undefined)
884
1045
  argv.push("--limit", opts.limit);
@@ -932,46 +1093,15 @@ async function runHttpPs(host, opts, deps) {
932
1093
  process.exitCode = 1;
933
1094
  return;
934
1095
  }
935
- let effectiveFilter = opts.filter;
936
- if (opts.active) {
937
- if (effectiveFilter) {
938
- console.error("--active and --filter cannot be combined.");
939
- process.exitCode = 1;
940
- return;
941
- }
942
- effectiveFilter = "agentActivity.state=running";
943
- }
944
- let parsedFilter = null;
945
- if (effectiveFilter) {
946
- const result = parseFilter(effectiveFilter);
947
- if ("error" in result) {
948
- console.error(result.error);
949
- process.exitCode = 1;
950
- return;
951
- }
952
- parsedFilter = result;
953
- }
954
- const limit = opts.limit !== undefined ? Number(opts.limit) : null;
955
- if (limit !== null && (!Number.isInteger(limit) || limit < 0)) {
956
- console.error(`--limit must be a non-negative integer; got '${opts.limit}'`);
1096
+ const controls = parsePsControls(opts);
1097
+ if ("error" in controls) {
1098
+ console.error(controls.error);
957
1099
  process.exitCode = 1;
958
1100
  return;
959
1101
  }
960
- let fields = null;
961
- if (opts.fields !== undefined) {
962
- const fieldsLevel = opts.nodes ? "nodes" : "rig";
963
- const fieldsAllowed = fieldsLevel === "nodes" ? ALLOWED_NODE_FIELDS : ALLOWED_RIG_FIELDS;
964
- const fieldsResult = parseFields(opts.fields, fieldsAllowed, fieldsLevel);
965
- if ("error" in fieldsResult) {
966
- console.error(fieldsResult.error);
967
- process.exitCode = 1;
968
- return;
969
- }
970
- fields = fieldsResult;
971
- }
1102
+ const { parsedFilter, limit, fields, useEnvelope } = controls;
972
1103
  const client = deps.clientFactory(host.url);
973
1104
  const headers = buildRemoteHeaders(bearerResult.token);
974
- const useEnvelope = parsedFilter !== null || limit !== null || fields !== null || opts.summary === true;
975
1105
  try {
976
1106
  if (opts.nodes) {
977
1107
  await handleNodes(client, opts, parsedFilter, limit, fields, useEnvelope, headers);
@@ -1020,7 +1150,22 @@ async function runHttpPs(host, opts, deps) {
1020
1150
  process.exitCode = 1;
1021
1151
  }
1022
1152
  }
1023
- async function runFanOutPs(opts, deps) {
1153
+ /** OPR.0.4.4.21 FR-5 adapter to the intra-P4 shared contract
1154
+ * (fanout-contract.ts, slice 15 first-lander at 0ecd329b; the closed
1155
+ * status enum is THE contract, failedStep rides as additive detail). */
1156
+ function toPerHostStatus(r) {
1157
+ const status = r.ok ? "ok"
1158
+ : r.failedStep === "unsupported-transport" ? "unsupported-transport"
1159
+ : r.failedStep === "permission-gate" ? "auth-failed"
1160
+ : "unreachable";
1161
+ const out = { hostId: r.host, status };
1162
+ if (r.error)
1163
+ out.error = r.error;
1164
+ if (!r.ok && r.failedStep !== "unsupported-transport")
1165
+ out.failedStep = r.failedStep;
1166
+ return out;
1167
+ }
1168
+ async function runFanOutPs(opts, deps, shaping) {
1024
1169
  const loader = deps.hostRegistryLoader ?? loadHostRegistry;
1025
1170
  const registry = loader();
1026
1171
  if (!registry.ok) {
@@ -1040,14 +1185,20 @@ async function runFanOutPs(opts, deps) {
1040
1185
  }
1041
1186
  }
1042
1187
  else {
1043
- targetIds = allHosts.filter((h) => h.transport === "http").map((h) => h.id);
1188
+ // OPR.0.4.4.21 fixback (qa1 F1): target EVERY declared host non-HTTP
1189
+ // hosts must appear in hosts[] as unsupported-transport (R15-2), never
1190
+ // be silently absent. The per-host leg classifies transport.
1191
+ targetIds = allHosts.map((h) => h.id);
1044
1192
  }
1045
1193
  const results = await Promise.all(targetIds.map(async (id) => {
1046
1194
  const host = allHosts.find((h) => h.id === id);
1047
1195
  if (!host)
1048
1196
  return { host: id, ok: false, failedStep: "remote-daemon-unreachable", error: `unknown host ${id}` };
1049
1197
  if (host.transport !== "http") {
1050
- return { host: id, ok: false, failedStep: "remote-command-failed", error: `host ${id} uses transport ${host.transport}; HTTP fan-out requires transport: http` };
1198
+ // R15-2 (shared contract): an SSH-declared host is a STRUCTURED
1199
+ // unsupported-transport status — never prose-only, never silently
1200
+ // thinner output.
1201
+ return { host: id, ok: false, failedStep: "unsupported-transport", error: `host ${id} uses transport ${host.transport}; HTTP fan-out requires transport: http` };
1051
1202
  }
1052
1203
  const httpHost = host;
1053
1204
  const bearerResult = resolveRemoteBearer(httpHost);
@@ -1055,16 +1206,35 @@ async function runFanOutPs(opts, deps) {
1055
1206
  return { host: id, ok: false, failedStep: bearerResult.failedStep, error: bearerResult.error };
1056
1207
  }
1057
1208
  const client = deps.clientFactory(httpHost.url);
1209
+ const headers = buildRemoteHeaders(bearerResult.token);
1058
1210
  try {
1059
1211
  const psQuery = opts.includeArchived ? "?includeArchived=true" : "";
1060
- const res = await client.get(`/api/ps${psQuery}`, {
1061
- headers: buildRemoteHeaders(bearerResult.token),
1062
- });
1212
+ const res = await client.get(`/api/ps${psQuery}`, { headers });
1063
1213
  const failedStep = classifyHttpFailedStep(res.status);
1064
1214
  if (failedStep !== "none") {
1065
1215
  return { host: id, ok: false, failedStep, error: `HTTP ${res.status}` };
1066
1216
  }
1067
- return { host: id, ok: true, failedStep: "none", data: res.data };
1217
+ if (!opts.nodes) {
1218
+ return { host: id, ok: true, failedStep: "none", data: res.data };
1219
+ }
1220
+ // OPR.0.4.4.21 FR-5 — the full explicit ladder (--nodes -A, --full
1221
+ // for complete records): per-host node fan-out, projected unless
1222
+ // --full (the invariant's last rung; validated as -A-only upstream).
1223
+ const rigs = Array.isArray(res.data) ? res.data : [];
1224
+ const nodes = [];
1225
+ for (const rig of rigs) {
1226
+ const nodesRes = await client.get(`/api/rigs/${encodeURIComponent(rig.rigId)}/nodes`, { headers });
1227
+ if (nodesRes.status >= 400) {
1228
+ return { host: id, ok: false, failedStep: classifyHttpFailedStep(nodesRes.status), error: `HTTP ${nodesRes.status} fetching nodes for rig ${rig.rigName ?? rig.name}` };
1229
+ }
1230
+ const parentRigName = rig.rigName ?? rig.name;
1231
+ nodes.push(...nodesRes.data.map((n) => ({
1232
+ ...n,
1233
+ rigId: n.rigId ?? rig.rigId,
1234
+ rigName: n.rigName ?? parentRigName,
1235
+ })));
1236
+ }
1237
+ return { host: id, ok: true, failedStep: "none", data: nodes };
1068
1238
  }
1069
1239
  catch (err) {
1070
1240
  return { host: id, ok: false, failedStep: classifyHttpError(err), error: err.message };
@@ -1072,7 +1242,49 @@ async function runFanOutPs(opts, deps) {
1072
1242
  }));
1073
1243
  const hasFailure = results.some((r) => !r.ok);
1074
1244
  if (opts.json) {
1075
- console.log(JSON.stringify({ hosts: results }));
1245
+ // OPR.0.4.4.21 FR-5 — the intra-P4 shared payload (ONE contract with
1246
+ // slice 15): items = each host's rows stamped with their origin hostId
1247
+ // (flat-mergeable; origin never positional), hosts = the per-host
1248
+ // structured status array. EVERY targeted host appears in hosts[] —
1249
+ // ok or not (no silent thinning).
1250
+ const rawItems = results.flatMap((r) => r.ok && Array.isArray(r.data)
1251
+ ? r.data.map((row) => ({ ...row, hostId: r.host }))
1252
+ : []);
1253
+ const hostStatuses = results.map(toPerHostStatus);
1254
+ let narrowed = rawItems;
1255
+ if (opts.rig)
1256
+ narrowed = narrowed.filter((e) => (e.rigName ?? e.name) === opts.rig);
1257
+ if (opts.nodes && opts.session)
1258
+ narrowed = narrowed.filter((n) => n.canonicalSessionName === opts.session);
1259
+ const filtered = shaping.parsedFilter
1260
+ ? opts.nodes
1261
+ ? applyNodeFilter(narrowed, shaping.parsedFilter)
1262
+ : applyRigFilter(narrowed, shaping.parsedFilter)
1263
+ : narrowed;
1264
+ let items;
1265
+ if (opts.summary) {
1266
+ items = [
1267
+ opts.nodes
1268
+ ? summarizeNodes(filtered)
1269
+ : summarizeRigs(filtered),
1270
+ ];
1271
+ }
1272
+ else {
1273
+ const limited = shaping.limit !== null ? filtered.slice(0, shaping.limit) : filtered;
1274
+ items = shaping.fields
1275
+ ? selectFanOutFields(limited, shaping.fields)
1276
+ : opts.nodes && !opts.full
1277
+ ? compactNodeProjection(limited).map((row, i) => ({
1278
+ ...row,
1279
+ hostId: limited[i]?.hostId,
1280
+ }))
1281
+ : limited;
1282
+ }
1283
+ const payload = {
1284
+ items,
1285
+ hosts: hostStatuses,
1286
+ };
1287
+ console.log(JSON.stringify(payload));
1076
1288
  }
1077
1289
  else {
1078
1290
  for (const r of results) {