@orchestraworks/cli 0.5.2 → 0.5.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 (128) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/api/compute-provider-check.d.ts +29 -0
  3. package/dist/cli/api/compute-provider-check.js +207 -0
  4. package/dist/cli/api/tail.d.ts +66 -0
  5. package/dist/cli/api/tail.js +564 -0
  6. package/dist/cli/main.d.ts +5 -1
  7. package/dist/cli/main.js +22 -0
  8. package/dist/constructs/agent.d.ts +10 -0
  9. package/dist/constructs/agent.js +39 -4
  10. package/dist/constructs/environment.d.ts +29 -1
  11. package/dist/constructs/environment.js +28 -1
  12. package/dist/constructs/skill.d.ts +6 -1
  13. package/dist/constructs/skill.js +21 -2
  14. package/dist/engine/reconcile.js +15 -1
  15. package/dist/generated/cli-commands.json +1646 -198
  16. package/dist/generated/cli-dispatch.js +9 -0
  17. package/dist/generated/managed-skills.d.ts +2 -0
  18. package/dist/generated/managed-skills.js +18 -0
  19. package/dist/index.d.ts +1 -1
  20. package/docs/REFERENCE.md +21 -1
  21. package/node_modules/@orchestraworks/client/dist/apis/OrganizationInvitationsApi.d.ts +29 -0
  22. package/node_modules/@orchestraworks/client/dist/apis/OrganizationInvitationsApi.js +96 -0
  23. package/node_modules/@orchestraworks/client/dist/esm/apis/OrganizationInvitationsApi.d.ts +29 -0
  24. package/node_modules/@orchestraworks/client/dist/esm/apis/OrganizationInvitationsApi.js +96 -0
  25. package/node_modules/@orchestraworks/client/dist/esm/models/ComputeProvider.d.ts +3 -3
  26. package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.d.ts +13 -0
  27. package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.js +5 -0
  28. package/node_modules/@orchestraworks/client/dist/esm/models/CreateOrganizationInvitationRequest.d.ts +2 -2
  29. package/node_modules/@orchestraworks/client/dist/esm/models/Environment.d.ts +13 -0
  30. package/node_modules/@orchestraworks/client/dist/esm/models/Environment.js +5 -0
  31. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentHook.d.ts +38 -0
  32. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentHook.js +45 -0
  33. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentHooks.d.ts +39 -0
  34. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentHooks.js +44 -0
  35. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentHooksRequest.d.ts +39 -0
  36. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentHooksRequest.js +44 -0
  37. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpec.d.ts +2 -1
  38. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpecRequest.d.ts +2 -1
  39. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentSnapshot.d.ts +19 -0
  40. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentSnapshot.js +7 -0
  41. package/node_modules/@orchestraworks/client/dist/esm/models/Machine.d.ts +7 -1
  42. package/node_modules/@orchestraworks/client/dist/esm/models/Machine.js +2 -0
  43. package/node_modules/@orchestraworks/client/dist/esm/models/OrganizationAuthPolicy.d.ts +1 -1
  44. package/node_modules/@orchestraworks/client/dist/esm/models/OrganizationInvitation.d.ts +14 -1
  45. package/node_modules/@orchestraworks/client/dist/esm/models/OrganizationInvitation.js +8 -1
  46. package/node_modules/@orchestraworks/client/dist/esm/models/Principal.d.ts +6 -0
  47. package/node_modules/@orchestraworks/client/dist/esm/models/Principal.js +2 -0
  48. package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.d.ts +13 -0
  49. package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.js +5 -0
  50. package/node_modules/@orchestraworks/client/dist/esm/models/UpdateOrganizationInvitationRequest.d.ts +2 -1
  51. package/node_modules/@orchestraworks/client/dist/esm/models/UpdateOrganizationInvitationRequest.js +2 -1
  52. package/node_modules/@orchestraworks/client/dist/esm/models/User.d.ts +6 -0
  53. package/node_modules/@orchestraworks/client/dist/esm/models/User.js +4 -0
  54. package/node_modules/@orchestraworks/client/dist/esm/models/index.d.ts +3 -0
  55. package/node_modules/@orchestraworks/client/dist/esm/models/index.js +3 -0
  56. package/node_modules/@orchestraworks/client/dist/models/ComputeProvider.d.ts +3 -3
  57. package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.d.ts +13 -0
  58. package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.js +5 -0
  59. package/node_modules/@orchestraworks/client/dist/models/CreateOrganizationInvitationRequest.d.ts +2 -2
  60. package/node_modules/@orchestraworks/client/dist/models/Environment.d.ts +13 -0
  61. package/node_modules/@orchestraworks/client/dist/models/Environment.js +5 -0
  62. package/node_modules/@orchestraworks/client/dist/models/EnvironmentHook.d.ts +38 -0
  63. package/node_modules/@orchestraworks/client/dist/models/EnvironmentHook.js +52 -0
  64. package/node_modules/@orchestraworks/client/dist/models/EnvironmentHooks.d.ts +39 -0
  65. package/node_modules/@orchestraworks/client/dist/models/EnvironmentHooks.js +51 -0
  66. package/node_modules/@orchestraworks/client/dist/models/EnvironmentHooksRequest.d.ts +39 -0
  67. package/node_modules/@orchestraworks/client/dist/models/EnvironmentHooksRequest.js +51 -0
  68. package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpec.d.ts +2 -1
  69. package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpecRequest.d.ts +2 -1
  70. package/node_modules/@orchestraworks/client/dist/models/EnvironmentSnapshot.d.ts +19 -0
  71. package/node_modules/@orchestraworks/client/dist/models/EnvironmentSnapshot.js +7 -0
  72. package/node_modules/@orchestraworks/client/dist/models/Machine.d.ts +7 -1
  73. package/node_modules/@orchestraworks/client/dist/models/Machine.js +2 -0
  74. package/node_modules/@orchestraworks/client/dist/models/OrganizationAuthPolicy.d.ts +1 -1
  75. package/node_modules/@orchestraworks/client/dist/models/OrganizationInvitation.d.ts +14 -1
  76. package/node_modules/@orchestraworks/client/dist/models/OrganizationInvitation.js +8 -1
  77. package/node_modules/@orchestraworks/client/dist/models/Principal.d.ts +6 -0
  78. package/node_modules/@orchestraworks/client/dist/models/Principal.js +2 -0
  79. package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.d.ts +13 -0
  80. package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.js +5 -0
  81. package/node_modules/@orchestraworks/client/dist/models/UpdateOrganizationInvitationRequest.d.ts +2 -1
  82. package/node_modules/@orchestraworks/client/dist/models/UpdateOrganizationInvitationRequest.js +2 -1
  83. package/node_modules/@orchestraworks/client/dist/models/User.d.ts +6 -0
  84. package/node_modules/@orchestraworks/client/dist/models/User.js +4 -0
  85. package/node_modules/@orchestraworks/client/dist/models/index.d.ts +3 -0
  86. package/node_modules/@orchestraworks/client/dist/models/index.js +3 -0
  87. package/node_modules/@orchestraworks/client/package.json +1 -1
  88. package/package.json +4 -4
  89. package/skills/orchestra/SKILL.md +167 -42
  90. package/skills/orchestra-fdk/SKILL.md +112 -545
  91. package/skills/orchestra-fdk/references/authoring.md +29 -2
  92. package/skills/orchestra-fdk/references/environment.md +295 -41
  93. package/skills/orchestra-fdk/references/integrations/analytics.md +18 -0
  94. package/skills/orchestra-fdk/references/integrations/ci-cd.md +20 -0
  95. package/skills/orchestra-fdk/references/integrations/cloud.md +19 -0
  96. package/skills/orchestra-fdk/references/integrations/code-repo.md +76 -0
  97. package/skills/orchestra-fdk/references/integrations/databases.md +22 -0
  98. package/skills/orchestra-fdk/references/integrations/dependencies.md +29 -0
  99. package/skills/orchestra-fdk/references/integrations/design.md +18 -0
  100. package/skills/orchestra-fdk/references/integrations/feature-flags.md +19 -0
  101. package/skills/orchestra-fdk/references/integrations/incident.md +29 -0
  102. package/skills/orchestra-fdk/references/integrations/issue-tracker.md +65 -0
  103. package/skills/orchestra-fdk/references/integrations/knowledge-base.md +43 -0
  104. package/skills/orchestra-fdk/references/integrations/messaging.md +38 -0
  105. package/skills/orchestra-fdk/references/integrations/observability.md +36 -0
  106. package/skills/orchestra-fdk/references/integrations/secrets-managers.md +25 -0
  107. package/skills/orchestra-fdk/references/integrations/support-desk.md +31 -0
  108. package/skills/orchestra-fdk/references/phases/0-start.md +62 -0
  109. package/skills/orchestra-fdk/references/phases/1-discover.md +111 -0
  110. package/skills/orchestra-fdk/references/phases/2-plan.md +157 -0
  111. package/skills/orchestra-fdk/references/phases/3-connect.md +99 -0
  112. package/skills/orchestra-fdk/references/phases/4-build.md +159 -0
  113. package/skills/orchestra-fdk/references/phases/5-prove.md +107 -0
  114. package/skills/orchestra-fdk/references/phases/6-handover.md +42 -0
  115. package/skills/orchestra-fdk/references/troubleshooting.md +128 -0
  116. package/templates/bug-fix/agents/bugfix.md +1 -1
  117. package/templates/bug-fix/main.ts +1 -1
  118. package/templates/engineering-team/README.md +45 -181
  119. package/templates/engineering-team/agents/engineer-agent.md +10 -7
  120. package/templates/engineering-team/agents/manager-agent.md +23 -120
  121. package/templates/engineering-team/inputs.json +1 -3
  122. package/templates/engineering-team/lib/environment.ts +10 -90
  123. package/templates/engineering-team/lib/inputs.ts +5 -97
  124. package/templates/engineering-team/main.ts +22 -69
  125. package/templates/todo-factory/agents/bugfix.md +1 -1
  126. package/templates/todo-factory/main.ts +1 -1
  127. package/templates/engineering-team/agents/engineer-bare.md +0 -22
  128. package/templates/engineering-team/onboarding/create-score-metrics.sh +0 -33
@@ -0,0 +1,564 @@
1
+ // `orchestra sessions tail <id>`: print a conversation's items as they are saved, and what each gains later.
2
+ //
3
+ // It reads the public items list through its `after` cursor. While a turn is open, each read is the list's
4
+ // waiting read, named by the open turn's prompt and set to answer once the event after the prompt's
5
+ // recorded position is saved, so the platform paces the loop and the command never polls on a timer of its
6
+ // own. While no turn is open there is nothing for a read to wait on, and only then does it pause, with a
7
+ // backoff, before it reads again.
8
+ //
9
+ // More than the last row can change: a reply keeps growing when a message is appended behind it, and the
10
+ // fold patches any call of the open turn by its id. So each read classifies every row from its own fields,
11
+ // the cursor never passes a row that can still change, and the table prints what a row gains as it grows.
12
+ import { httpStatusOf } from "../../engine/client.js";
13
+ import { findCommand } from "../../generated/cli-commands.js";
14
+ import { authFlagsFrom, platformTargetFrom, resolveAuthWithWarnings } from "../core/auth.js";
15
+ import { renderError, runCommand, settleExitCode } from "../core/errors.js";
16
+ import { defaultIo, parseOutputMode, printRedactionNotes, printWarnings, writeLines, } from "../core/output.js";
17
+ import { createPlatformApis, REQUEST_TIMEOUT_MS } from "../core/platform-client.js";
18
+ import { redact } from "../core/redaction.js";
19
+ import { SETTLED_SESSION_STATUSES } from "./wait.js";
20
+ /** Seconds. The first pause after a failed read, doubled per failure up to {@link RETRY_MAX_SECONDS}. */
21
+ export const RETRY_FIRST_SECONDS = 0.5;
22
+ export const RETRY_MAX_SECONDS = 8;
23
+ /** Seconds. The pause while no turn is open, doubled per empty pass up to {@link IDLE_MAX_SECONDS}. */
24
+ export const IDLE_FIRST_SECONDS = 1;
25
+ /** The platform's own waiting-read budget, so an idle follow reacts no slower than a running one. */
26
+ export const IDLE_MAX_SECONDS = 5;
27
+ /** The list's own ceiling; a longer window is walked through the cursor. */
28
+ const PAGE_LIMIT = 500;
29
+ /** A prompt in these states has a turn waiting or running (`OPEN_PROMPT_STATES` on the platform). */
30
+ const OPEN_TURN_STATES = ["QUEUED", "IN_FLIGHT"];
31
+ const PROMPT_TYPES = ["user_message", "turn_start"];
32
+ /** The fold appends chunks to its open run only, which is always its turn's newest output row. */
33
+ const GROWING_TEXT_TYPES = ["agent_message", "agent_thought"];
34
+ const FINISHED_CALL_STATUSES = ["completed", "failed"];
35
+ export class TailUsageError extends Error {
36
+ constructor(message) {
37
+ super(message);
38
+ this.name = "TailUsageError";
39
+ }
40
+ }
41
+ /** Thrown out of any wait or read once the command is stopped, and answered with a clean exit. */
42
+ class Stopped extends Error {
43
+ }
44
+ export async function tailCommand(id, options = {}, deps = {}) {
45
+ const io = deps.io ?? defaultIo;
46
+ const ctx = {};
47
+ const { signal, release } = stopSignal(deps.signal);
48
+ try {
49
+ const code = await runCommand(async () => {
50
+ try {
51
+ await follow(id, options, deps, ctx, signal);
52
+ }
53
+ catch (error) {
54
+ if (!signal.aborted) {
55
+ throw error;
56
+ }
57
+ }
58
+ }, ctx, io);
59
+ settleExitCode(code, deps.onExitCode);
60
+ }
61
+ finally {
62
+ release();
63
+ }
64
+ }
65
+ /** The caller's signal when a test supplies one; otherwise one Ctrl-C aborts, and the listener is removed after. */
66
+ function stopSignal(given) {
67
+ if (given !== undefined) {
68
+ return { signal: given, release: () => undefined };
69
+ }
70
+ const controller = new AbortController();
71
+ const onInterrupt = () => controller.abort();
72
+ process.once("SIGINT", onInterrupt);
73
+ return { signal: controller.signal, release: () => process.removeListener("SIGINT", onInterrupt) };
74
+ }
75
+ async function follow(id, options, deps, ctx, signal) {
76
+ const env = deps.env ?? process.env;
77
+ const io = deps.io ?? defaultIo;
78
+ const sleep = deps.sleep ?? abortableDelay;
79
+ const mode = parseOutputMode(options.output, (message) => new TailUsageError(message));
80
+ ctx.mode = mode;
81
+ ctx.notFound = { resource: "session", key: id };
82
+ const printer = new ItemPrinter(mode, sessionItemSchema(), options.reveal === true, io);
83
+ const { auth, warnings } = await resolveAuthWithWarnings(env, authFlagsFrom(options), deps.loadProfile !== undefined ? { loadProfile: deps.loadProfile } : {});
84
+ printWarnings(warnings, io);
85
+ ctx.provider = auth.provider;
86
+ ctx.baseUrl = auth.baseUrl;
87
+ const platformTarget = platformTargetFrom(auth);
88
+ const apis = createPlatformApis(platformTarget, deps.fetchApi, { warn: (lines) => printWarnings(lines, io) });
89
+ const organizationId = platformTarget.organizationId;
90
+ const scope = organizationId !== undefined ? { xOrganizationId: organizationId } : {};
91
+ const pause = async (seconds) => {
92
+ await sleep(seconds, signal);
93
+ if (signal.aborted) {
94
+ throw new Stopped();
95
+ }
96
+ };
97
+ const read = (call) => readWithRetry(call, signal, pause, ctx, io);
98
+ const readPage = (after, shape) => read((init) => apis.sessions.listSessionItemsRaw({ id, ...scope, ...(after !== undefined ? { after } : {}), ...shape }, init));
99
+ /**
100
+ * `null` when the item the read started after is gone: a queued or steered message taken off the
101
+ * conversation answers the cursor with a 404.
102
+ */
103
+ const readPageAfter = (after, shape) => after === undefined ? readPage(after, shape) : orGone(readPage(after, shape));
104
+ /** Turns some read has seen end; a read made after that holds their rows as they will stay. */
105
+ const endedTurns = new Set();
106
+ let cursor = options.after;
107
+ /** Every earlier cursor, newest last, to step back to when the current one is gone. */
108
+ const earlierCursors = [];
109
+ let idleSeconds = IDLE_FIRST_SECONDS;
110
+ let lastBounds;
111
+ for (;;) {
112
+ if (signal.aborted) {
113
+ throw new Stopped();
114
+ }
115
+ const readSession = () => read((init) => apis.sessions.getSessionRaw({ id, ...scope }, init));
116
+ let session = await readSession();
117
+ let openTurnId = stringOf(session["openTurnId"]);
118
+ let bounds;
119
+ let window = [];
120
+ let timedOut = false;
121
+ /** The `after` of a read that answered 404, when one did. */
122
+ let missing;
123
+ if (openTurnId !== undefined) {
124
+ const turnId = openTurnId;
125
+ const prompt = await orGone(read((init) => apis.sessions.getSessionItemRaw({ id, itemId: turnId, ...scope }, init)));
126
+ const page = prompt === null ? null : await orGone(readPage(cursor, (bounds = waitBoundsFor(turnId, prompt))));
127
+ if (page === null) {
128
+ // The waited prompt is gone, as an interrupted queued turn's is, or the cursor is: the session is
129
+ // read again, and this pass reads without bounds, whose own 404 says whether the cursor went.
130
+ session = await readSession();
131
+ openTurnId = stringOf(session["openTurnId"]);
132
+ bounds = undefined;
133
+ }
134
+ else {
135
+ window = itemsOf(page);
136
+ timedOut = page["waitTimedOut"] === true;
137
+ }
138
+ }
139
+ if (bounds === undefined) {
140
+ let scan = cursor;
141
+ for (;;) {
142
+ const page = await readPageAfter(scan, { limit: PAGE_LIMIT });
143
+ if (page === null) {
144
+ missing = { after: scan };
145
+ break;
146
+ }
147
+ const rows = itemsOf(page);
148
+ window.push(...rows);
149
+ const total = typeof page["total"] === "number" ? page["total"] : rows.length;
150
+ if (total <= rows.length || rows.length === 0) {
151
+ break;
152
+ }
153
+ scan = stringOf(rows.at(-1)?.["id"]);
154
+ }
155
+ }
156
+ if (missing !== undefined) {
157
+ // A 404 on the session read here ends the follow with the usual not-found error.
158
+ await read((init) => apis.sessions.getSessionRaw({ id, ...scope }, init));
159
+ // A page boundary that went missing is read again from the cursor. A cursor that went missing steps
160
+ // back to the one before it, or to the start; the printer skips by id what it has already printed.
161
+ if (missing.after === cursor) {
162
+ cursor = earlierCursors.pop();
163
+ }
164
+ lastBounds = undefined;
165
+ continue;
166
+ }
167
+ const { states, unseenTurns } = classifyItems(window, endedTurns, openTurnId);
168
+ const printed = printer.show(window, states);
169
+ for (const [index, item] of window.entries()) {
170
+ if (states[index] !== "final") {
171
+ break;
172
+ }
173
+ const itemId = stringOf(item["id"]);
174
+ if (itemId !== undefined && itemId !== cursor) {
175
+ earlierCursors.push(cursor);
176
+ cursor = itemId;
177
+ }
178
+ printer.forget(item);
179
+ }
180
+ // A turn this read cannot see the prompt of is learned from its prompt now, for the next read to use.
181
+ for (const turnId of unseenTurns) {
182
+ if (turnId !== openTurnId && (await turnHasEnded(turnId))) {
183
+ endedTurns.add(turnId);
184
+ }
185
+ }
186
+ const turnChanging = states.includes("turn");
187
+ if (printed > 0 || bounds !== undefined) {
188
+ idleSeconds = IDLE_FIRST_SECONDS;
189
+ }
190
+ if (bounds !== undefined) {
191
+ // A read that answered at once, with the same bounds as the pass before and nothing new, is a turn
192
+ // whose state has not caught up with the session read: without a pause that would be a busy loop.
193
+ const stale = !timedOut && printed === 0 && sameBounds(bounds, lastBounds);
194
+ lastBounds = bounds;
195
+ if (stale) {
196
+ await pause(RETRY_FIRST_SECONDS);
197
+ }
198
+ continue;
199
+ }
200
+ lastBounds = undefined;
201
+ // Rows of a turn the session read did not name: the session is read again before anything settles.
202
+ if (turnChanging && printed > 0) {
203
+ continue;
204
+ }
205
+ const status = stringOf(session["status"]);
206
+ const settled = status !== undefined && SETTLED_SESSION_STATUSES.includes(status);
207
+ if (options.untilSettled === true && settled && !turnChanging) {
208
+ printer.flush(window, states);
209
+ printStatus(id, status, mode, io);
210
+ return;
211
+ }
212
+ await pause(idleSeconds);
213
+ idleSeconds = Math.min(idleSeconds * 2, IDLE_MAX_SECONDS);
214
+ }
215
+ async function turnHasEnded(turnId) {
216
+ try {
217
+ const prompt = await read((init) => apis.sessions.getSessionItemRaw({ id, itemId: turnId, ...scope }, init));
218
+ return !OPEN_TURN_STATES.includes(stringOf(prompt["turnState"]) ?? "");
219
+ }
220
+ catch (error) {
221
+ // A prompt that is gone has no turn left to write to.
222
+ if (httpStatusOf(error) === 404) {
223
+ return true;
224
+ }
225
+ throw error;
226
+ }
227
+ }
228
+ }
229
+ /**
230
+ * The state of each row, from the row's own fields and the read it came in. One read is one snapshot, so a
231
+ * prompt in it that shows its turn ended means that turn's rows in the same read are as they will stay.
232
+ * A row of a turn whose prompt is not in the read, that no earlier read saw end, and that the session read
233
+ * did not name, counts as changing; its turn is returned in `unseenTurns` for the caller to look up.
234
+ */
235
+ export function classifyItems(window, endedTurns, openTurnId) {
236
+ const openTurns = new Set();
237
+ for (const item of window) {
238
+ if (!isPrompt(item)) {
239
+ continue;
240
+ }
241
+ const itemId = stringOf(item["id"]);
242
+ if (itemId === undefined) {
243
+ continue;
244
+ }
245
+ if (OPEN_TURN_STATES.includes(stringOf(item["turnState"]) ?? "")) {
246
+ openTurns.add(itemId);
247
+ }
248
+ else {
249
+ endedTurns.add(itemId);
250
+ }
251
+ }
252
+ const unseenTurns = new Set();
253
+ const turnOpen = (turnId) => {
254
+ if (openTurns.has(turnId)) {
255
+ return true;
256
+ }
257
+ if (endedTurns.has(turnId)) {
258
+ return false;
259
+ }
260
+ if (turnId !== openTurnId) {
261
+ unseenTurns.add(turnId);
262
+ }
263
+ return true;
264
+ };
265
+ const newestOutput = new Map();
266
+ window.forEach((item, index) => {
267
+ const turnId = stringOf(item["turnId"]);
268
+ if (turnId !== undefined && !PROMPT_TYPES.includes(stringOf(item["type"]) ?? "")) {
269
+ newestOutput.set(turnId, index);
270
+ }
271
+ });
272
+ const states = window.map((item, index) => {
273
+ const type = stringOf(item["type"]) ?? "";
274
+ const turnId = stringOf(item["turnId"]);
275
+ if (type === "user_message" && turnId === undefined) {
276
+ // Queued: it may still open a turn of its own, or be taken off the conversation.
277
+ return "turn";
278
+ }
279
+ if (isPrompt(item)) {
280
+ return openTurns.has(stringOf(item["id"]) ?? "") ? "turn" : "final";
281
+ }
282
+ if (type === "user_message") {
283
+ // Steered into a turn: when that turn settles, the message is marked delivered, queued again as a
284
+ // prompt of its own, or deleted by an interrupt, so it changes until its turn's settle has run.
285
+ return stringOf(item["turnState"]) === "STEERED" ? "turn" : "final";
286
+ }
287
+ const open = turnId !== undefined && turnOpen(turnId);
288
+ if (open && (!GROWING_TEXT_TYPES.includes(type) || newestOutput.get(turnId) === index)) {
289
+ return "turn";
290
+ }
291
+ if (type === "background_task" && stringOf(item["taskStatus"]) === "running") {
292
+ return "background";
293
+ }
294
+ if (type === "tool_call" && item["taskId"] !== undefined && !FINISHED_CALL_STATUSES.includes(stringOf(item["status"]) ?? "")) {
295
+ return "background";
296
+ }
297
+ return "final";
298
+ });
299
+ return { states, unseenTurns };
300
+ }
301
+ /** A row that opened its turn: a message whose turn is its own id, or a turn the harness started. */
302
+ function isPrompt(item) {
303
+ const type = stringOf(item["type"]);
304
+ if (type === "turn_start") {
305
+ return true;
306
+ }
307
+ return type === "user_message" && item["turnId"] !== undefined && item["turnId"] === item["id"];
308
+ }
309
+ /**
310
+ * Remembers what each row has printed, so nothing prints twice and nothing is lost. In the table, a reply
311
+ * that can still grow prints its complete lines and keeps the rest until the next line ends or the reply
312
+ * can no longer grow, and any other row prints its line again when the line changes. In json, a row prints
313
+ * once it is final.
314
+ */
315
+ class ItemPrinter {
316
+ mode;
317
+ schema;
318
+ reveal;
319
+ io;
320
+ /** Per item id: the text a reply has printed, or the line another row has printed. */
321
+ shown = new Map();
322
+ done = new Set();
323
+ lastPrinted;
324
+ constructor(mode, schema, reveal, io) {
325
+ this.mode = mode;
326
+ this.schema = schema;
327
+ this.reveal = reveal;
328
+ this.io = io;
329
+ }
330
+ /** Returns how many rows printed something. */
331
+ show(window, states) {
332
+ let printed = 0;
333
+ window.forEach((item, index) => {
334
+ if (this.showOne(item, states[index] === "final")) {
335
+ printed += 1;
336
+ }
337
+ });
338
+ return printed;
339
+ }
340
+ /** At exit, background rows print as they stand, since nothing will print them later. */
341
+ flush(window, states) {
342
+ window.forEach((item, index) => {
343
+ if (states[index] === "background") {
344
+ this.showOne(item, true);
345
+ }
346
+ });
347
+ }
348
+ /**
349
+ * Once the cursor has passed a row, only a read started again from an earlier cursor returns it, and then
350
+ * its id in `done` keeps it from printing twice.
351
+ */
352
+ forget(item) {
353
+ this.shown.delete(stringOf(item["id"]) ?? "");
354
+ }
355
+ showOne(item, final) {
356
+ const itemId = stringOf(item["id"]) ?? "";
357
+ if (this.done.has(itemId)) {
358
+ return false;
359
+ }
360
+ const redaction = redact(item, this.schema, { reveal: this.reveal });
361
+ const rendered = redaction.rendered;
362
+ let text;
363
+ if (this.mode === "json") {
364
+ text = final ? JSON.stringify(rendered) : undefined;
365
+ }
366
+ else if (GROWING_TEXT_TYPES.includes(stringOf(rendered["type"]) ?? "")) {
367
+ text = this.growth(itemId, rendered, final);
368
+ }
369
+ else {
370
+ const line = itemLine(rendered);
371
+ text = this.shown.get(itemId) === line ? undefined : line;
372
+ this.shown.set(itemId, line);
373
+ }
374
+ if (final) {
375
+ this.done.add(itemId);
376
+ }
377
+ if (text === undefined) {
378
+ return false;
379
+ }
380
+ writeLines(this.io, "out", text);
381
+ printRedactionNotes(redaction, this.io);
382
+ this.lastPrinted = itemId;
383
+ return true;
384
+ }
385
+ /** The part of a reply not printed yet: whole lines while it can grow, everything once it cannot. */
386
+ growth(itemId, item, final) {
387
+ const label = itemLabel(stringOf(item["type"]) ?? "");
388
+ const full = contentText(item["content"]);
389
+ const before = this.shown.get(itemId);
390
+ if (before !== undefined && !full.startsWith(before)) {
391
+ // Rewritten rather than extended: print it whole again, so nothing is lost.
392
+ this.shown.set(itemId, full);
393
+ return `${label} (rewritten): ${full}`;
394
+ }
395
+ const from = before?.length ?? 0;
396
+ const upTo = final ? full.length : full.lastIndexOf("\n") + 1;
397
+ if (upTo <= from && !(final && before === undefined)) {
398
+ return undefined;
399
+ }
400
+ const part = full.slice(from, Math.max(from, upTo)).replace(/\n$/, "");
401
+ this.shown.set(itemId, full.slice(0, Math.max(from, upTo)));
402
+ if (before === undefined) {
403
+ return `${label}: ${part}`;
404
+ }
405
+ return this.lastPrinted === itemId ? part : `${label} (continued): ${part}`;
406
+ }
407
+ }
408
+ /**
409
+ * The read answers once the prompt's recorded position reaches `waitForSeq - 1`, so one past the recorded
410
+ * event waits for the next one. A prompt that has not started a run yet carries no attempt: its first run is
411
+ * attempt 1, and the read waits for that run's first event.
412
+ */
413
+ export function waitBoundsFor(promptId, prompt) {
414
+ const attempt = typeof prompt["attempt"] === "number" && prompt["attempt"] >= 1 ? prompt["attempt"] : 1;
415
+ const recorded = typeof prompt["recordedSeq"] === "number" ? prompt["recordedSeq"] : 0;
416
+ return { waitForItem: promptId, waitForAttempt: attempt, waitForSeq: recorded + 2 };
417
+ }
418
+ function sameBounds(a, b) {
419
+ return (b !== undefined &&
420
+ a.waitForItem === b.waitForItem &&
421
+ a.waitForAttempt === b.waitForAttempt &&
422
+ a.waitForSeq === b.waitForSeq);
423
+ }
424
+ /** `null` for a read that answered 404, so the caller can tell what went missing. */
425
+ async function orGone(pending) {
426
+ try {
427
+ return await pending;
428
+ }
429
+ catch (error) {
430
+ if (httpStatusOf(error) === 404) {
431
+ return null;
432
+ }
433
+ throw error;
434
+ }
435
+ }
436
+ /**
437
+ * A 5xx and a request that never got an answer are retried with a doubling pause; every other failure, a
438
+ * 404 included, ends the command through the shared error report. Each request keeps the CLI's own timeout,
439
+ * joined with the stop signal so Ctrl-C ends a waiting read at once.
440
+ */
441
+ async function readWithRetry(call, signal, pause, ctx, io) {
442
+ let backoff = RETRY_FIRST_SECONDS;
443
+ for (;;) {
444
+ try {
445
+ const response = await call({ signal: AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]) });
446
+ const text = await response.raw.text().catch((cause) => {
447
+ throw new BodyReadError(cause);
448
+ });
449
+ const body = text.trim() === "" ? undefined : JSON.parse(text);
450
+ return typeof body === "object" && body !== null && !Array.isArray(body) ? body : {};
451
+ }
452
+ catch (error) {
453
+ if (signal.aborted) {
454
+ throw new Stopped();
455
+ }
456
+ if (!retryable(error)) {
457
+ throw error;
458
+ }
459
+ const reason = (await renderError(error, ctx)).split("\n")[0].replace(/^error:\s*/, "");
460
+ printWarnings([`${reason} Retrying in ${backoff}s.`], io);
461
+ await pause(backoff);
462
+ backoff = Math.min(backoff * 2, RETRY_MAX_SECONDS);
463
+ }
464
+ }
465
+ }
466
+ /** The headers arrived and the connection dropped while the body was read. */
467
+ class BodyReadError extends Error {
468
+ constructor(cause) {
469
+ super("the connection closed while the platform's answer was being read.", { cause });
470
+ this.name = "BodyReadError";
471
+ }
472
+ }
473
+ /**
474
+ * The generated runtime raises `ResponseError` for a status and `FetchError` for a request with no answer;
475
+ * a body cut off after the headers fails outside it, as {@link BodyReadError}.
476
+ */
477
+ function retryable(error) {
478
+ const failure = error;
479
+ const status = failure?.response?.status;
480
+ if (typeof status === "number") {
481
+ return status >= 500;
482
+ }
483
+ return failure?.name === "FetchError" || error instanceof BodyReadError;
484
+ }
485
+ function sessionItemSchema() {
486
+ const schema = findCommand("sessions", "list-items")?.responseSchema;
487
+ const data = schema?.kind === "object" ? schema.fields["data"]?.schema : undefined;
488
+ return data?.kind === "array" ? data.items : undefined;
489
+ }
490
+ function itemsOf(page) {
491
+ const data = page["data"];
492
+ return Array.isArray(data)
493
+ ? data.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item))
494
+ : [];
495
+ }
496
+ function printStatus(id, status, mode, io) {
497
+ io.out(mode === "json" ? JSON.stringify({ id, status }) : `session ${id}: ${status}`);
498
+ }
499
+ /** Messages and thoughts in full; every other item on one line, from its own fields. */
500
+ export function itemLine(item) {
501
+ const type = stringOf(item["type"]) ?? "item";
502
+ switch (type) {
503
+ case "user_message":
504
+ case "agent_message":
505
+ case "agent_thought":
506
+ return `${itemLabel(type)}: ${contentText(item["content"])}`;
507
+ case "tool_call":
508
+ return oneLine(`tool: ${stringOf(item["title"]) ?? stringOf(item["toolCallId"]) ?? ""}${facts(item["kind"], item["status"])}`);
509
+ case "background_task":
510
+ return oneLine(`task: ${stringOf(item["description"]) ?? stringOf(item["taskId"]) ?? ""}${facts(item["taskKind"], item["taskStatus"])}`);
511
+ case "turn_start":
512
+ return "turn started";
513
+ default:
514
+ return `${type}: ${stringOf(item["id"]) ?? ""}`;
515
+ }
516
+ }
517
+ function itemLabel(type) {
518
+ return type === "user_message" ? "user" : type === "agent_thought" ? "thought" : "agent";
519
+ }
520
+ function facts(...values) {
521
+ const present = values.map(stringOf).filter((value) => value !== undefined);
522
+ return present.length === 0 ? "" : ` (${present.join(", ")})`;
523
+ }
524
+ /** Text blocks as they are; any other block as a bracketed marker naming what it is. */
525
+ function contentText(content) {
526
+ if (!Array.isArray(content)) {
527
+ return "";
528
+ }
529
+ return content
530
+ .map((block) => {
531
+ const record = (typeof block === "object" && block !== null ? block : {});
532
+ const kind = stringOf(record["type"]) ?? "block";
533
+ if (kind === "text") {
534
+ return stringOf(record["text"]) ?? "";
535
+ }
536
+ const resource = record["resource"];
537
+ const uri = stringOf(record["uri"]) ?? stringOf(resource?.["uri"]);
538
+ return uri === undefined ? `[${kind}]` : `[${kind} ${uri}]`;
539
+ })
540
+ .join("");
541
+ }
542
+ function oneLine(value) {
543
+ return value.replace(/[\r\n\t]+/g, " ");
544
+ }
545
+ function stringOf(value) {
546
+ return typeof value === "string" ? value : undefined;
547
+ }
548
+ function abortableDelay(seconds, signal) {
549
+ return new Promise((resolve) => {
550
+ if (signal.aborted) {
551
+ resolve();
552
+ return;
553
+ }
554
+ const onAbort = () => {
555
+ clearTimeout(timer);
556
+ resolve();
557
+ };
558
+ const timer = setTimeout(() => {
559
+ signal.removeEventListener("abort", onAbort);
560
+ resolve();
561
+ }, seconds * 1000);
562
+ signal.addEventListener("abort", onAbort, { once: true });
563
+ });
564
+ }
@@ -12,7 +12,9 @@ import { type WorkerInstallOptions } from "./commands/worker/install.js";
12
12
  import { type WorkerRunOptions } from "./commands/worker/run.js";
13
13
  import { type InterviewFlags } from "./firstRun.js";
14
14
  import { type RawApiOptions } from "./api/raw.js";
15
+ import { type TailCommandOptions } from "./api/tail.js";
15
16
  import { type WaitCommandOptions } from "./api/wait.js";
17
+ import { type ComputeProviderCheckOptions } from "./api/compute-provider-check.js";
16
18
  import { type InitCommandOptions } from "./factory/init.js";
17
19
  import { type GrantCommandOptions } from "./api/grant.js";
18
20
  import { type SkillPointer } from "./core/skill-pointer.js";
@@ -32,7 +34,7 @@ import { type SynthCommandOptions } from "./factory/synth.js";
32
34
  *
33
35
  * {@link topLevelHelpText} is what `orchestra --help` actually prints — this body plus a runtime epilogue.
34
36
  */
35
- export declare const TOP_LEVEL_HELP = "orchestra \u2014 the Orchestra command line.\n\norchestra fdk is the software factory as code. A software factory \u2014\nagents, environments, workflows, and the credentials that power them \u2014 is\ndefined in code: versioned, reviewed, type-checked. fdk apply reconciles\nthe platform to it, like cdk or terraform.\n\nUSAGE\n orchestra <command> [flags]\n\nThe orchestra CLI does two jobs:\n\n 1 Factory as code \u2014 where change happens.\n Declare the factory in code; fdk synth renders the plan, fdk diff\n previews it, fdk apply makes the platform match. Code is the source of\n truth, so every change is version-controlled, reviewed, and type-guarded.\n\n 2 Platform commands \u2014 probe the live factory.\n Read state, reason about what exists, trial an idea before codifying\n it, or fall back for a one-off: orchestra <noun> <verb>.\n\n Rule of thumb: observe with commands. Change in code.\n\nSIGN IN\n auth login Authenticate and save a profile (both jobs use it)\n\nFACTORY AS CODE\n fdk init Write a starter factory into this folder\n fdk synth Render the factory to a plan\n fdk diff Preview what apply would change\n fdk apply Make the platform match the code\n\nPLATFORM COMMANDS orchestra <noun> <verb> \u2014 nouns match API paths exactly\n Build & run agents environments sessions machines compute-enrollments\n pipelines work-items workflows decisions skills\n files artifacts volumes projects room-tokens\n compute-providers\n Events & wiring triggers events deliveries connections\n connection-intents install-links connectors providers\n Models models model-aliases model-configs model-providers\n model-provider-keys gateway-keys\n Credentials secrets api-keys\n Organization organizations organization-memberships organization-invitations\n users groups user-grants service-accounts roles policies me\n Observability metrics dashboards health\n\n Standard verbs: list \u00B7 get \u00B7 create \u00B7 update \u00B7 delete.\n Command endpoints keep their own verb: sessions interrupt, work-items move,\n workflows revert.\n Typed flows: user-grants grant (mint a user-grant and walk its consent flow) \u00B7\n sessions wait (poll a session until it settles) \u00B7\n artifacts preview (serve an HTML page locally).\n Write verbs exist for trial and fallback; durable change belongs in code.\n Writing to a factory-managed resource warns: the next apply reverts it.\n orchestra <noun> --help lists a noun's verbs and flags.\n\nBACKUP\n api <METHOD> </path> Raw authenticated call (like gh api). For when a typed\n command is missing or wrong \u2014 needing it is a bug; file it.\n\nYOUR CODING AGENT\n skills install Link the two bundled skills, orchestra and orchestra-fdk,\n into Claude Code or Codex. Both flags are required:\n --harness claude|codex|both|other --scope user|project.\n\nTHIS MACHINE\n install Install the CLI globally, ask what this machine is, then\n install the worker and the harness families you choose.\n worker run Serve this machine: claim its identity, then register.\n worker install Install the worker here, taking every answer from flags.\n worker enroll Redeem an enrollment token, then serve this machine.\n\nRUN YOUR AGENT HERE The local environment on this machine, and the machines it runs.\n local up Start it, and print the two lines a shell needs to reach it.\n local status Read its four checks: boot, content, turns, environment.\n local down Stop it, and the machines it launched.\n outpost token Mint an offer for a machine you already own.\n outpost up Enrol this machine, or this container, and hold its worker.\n outpost down Stop that worker and revoke its enrollment.\n Every other command reaches it the ordinary way, by base URL.\n\nCONVENTIONS\n --output json On every platform command; the default is a human table.\n --body @file.json Full request body where flags are too coarse.\n --file <path> On files create and artifacts create / create-version:\n store the bytes at <path> and let the create name them.\n Credential values in responses print masked; --reveal or --output-file is the\n deliberate path.\n fdk apply defaults to any-change: it confirms the whole pre-flight preview\n before any write, and stays silent when nothing changes. A single platform\n command defaults to sensitive-only \u2014 it asks for credential values and IAM only.\n Override with --require-approval never | sensitive-only | any-change or with\n ORCHESTRA_REQUIRE_APPROVAL; the legacy --approve-secrets is exactly never.\n Prompts and bindings are written as prompt`\u2026` / binding`\u2026` tagged templates\n over the typed context handle \u2014 never hand-spelled {{\u2026}} strings.\n\nEXAMPLES\n orchestra fdk apply # reconcile the factory in ./\n orchestra secrets create --name deepseek --value-from-command-line\n orchestra sessions list --status RUNNING --output json\n orchestra work-items move bug-12 --stage review\n orchestra gateway-keys create --slug ci-smoke --output-file key.json\n\nLEARN MORE\n orchestra <command> --help \u00B7 orchestra skills install --harness <name>\n --scope user (links the reference and setup skills into your coding agent \u2014\n the same documents AI agents use)\n";
37
+ export declare const TOP_LEVEL_HELP = "orchestra \u2014 the Orchestra command line.\n\norchestra fdk is the software factory as code. A software factory \u2014\nagents, environments, workflows, and the credentials that power them \u2014 is\ndefined in code: versioned, reviewed, type-checked. fdk apply reconciles\nthe platform to it, like cdk or terraform.\n\nUSAGE\n orchestra <command> [flags]\n\nThe orchestra CLI does two jobs:\n\n 1 Factory as code \u2014 where change happens.\n Declare the factory in code; fdk synth renders the plan, fdk diff\n previews it, fdk apply makes the platform match. Code is the source of\n truth, so every change is version-controlled, reviewed, and type-guarded.\n\n 2 Platform commands \u2014 probe the live factory.\n Read state, reason about what exists, trial an idea before codifying\n it, or fall back for a one-off: orchestra <noun> <verb>.\n\n Rule of thumb: observe with commands. Change in code.\n\nSIGN IN\n auth login Authenticate and save a profile (both jobs use it)\n\nFACTORY AS CODE\n fdk init Write a starter factory into this folder\n fdk synth Render the factory to a plan\n fdk diff Preview what apply would change\n fdk apply Make the platform match the code\n\nPLATFORM COMMANDS orchestra <noun> <verb> \u2014 nouns match API paths exactly\n Build & run agents environments sessions machines compute-enrollments\n pipelines work-items workflows decisions skills\n files artifacts volumes projects room-tokens\n compute-providers\n Events & wiring triggers events deliveries connections\n connection-intents install-links connectors providers\n Models models model-aliases model-configs model-providers\n model-provider-keys gateway-keys\n Credentials secrets api-keys\n Organization organizations organization-memberships organization-invitations\n users groups user-grants service-accounts roles policies me\n Observability metrics dashboards health\n\n Standard verbs: list \u00B7 get \u00B7 create \u00B7 update \u00B7 delete.\n Command endpoints keep their own verb: sessions interrupt, work-items move,\n workflows revert.\n Typed flows: user-grants grant (mint a user-grant and walk its consent flow) \u00B7\n sessions wait (poll a session until it settles) \u00B7\n sessions tail (print a session's items as they are saved) \u00B7\n compute-providers check (show why a Launcher fails) \u00B7\n artifacts preview (serve an HTML page locally).\n Write verbs exist for trial and fallback; durable change belongs in code.\n Writing to a factory-managed resource warns: the next apply reverts it.\n orchestra <noun> --help lists a noun's verbs and flags.\n\nBACKUP\n api <METHOD> </path> Raw authenticated call (like gh api). For when a typed\n command is missing or wrong \u2014 needing it is a bug; file it.\n\nYOUR CODING AGENT\n skills install Link the two bundled skills, orchestra and orchestra-fdk,\n into Claude Code or Codex. Both flags are required:\n --harness claude|codex|both|other --scope user|project.\n\nTHIS MACHINE\n install Install the CLI globally, ask what this machine is, then\n install the worker and the harness families you choose.\n worker run Serve this machine: claim its identity, then register.\n worker install Install the worker here, taking every answer from flags.\n worker enroll Redeem an enrollment token, then serve this machine.\n\nRUN YOUR AGENT HERE The local environment on this machine, and the machines it runs.\n local up Start it, and print the two lines a shell needs to reach it.\n local status Read its four checks: boot, content, turns, environment.\n local down Stop it, and the machines it launched.\n outpost token Mint an offer for a machine you already own.\n outpost up Enrol this machine, or this container, and hold its worker.\n outpost down Stop that worker and revoke its enrollment.\n Every other command reaches it the ordinary way, by base URL.\n\nCONVENTIONS\n --output json On every platform command; the default is a human table.\n --body @file.json Full request body where flags are too coarse.\n --file <path> On files create and artifacts create / create-version:\n store the bytes at <path> and let the create name them.\n Credential values in responses print masked; --reveal or --output-file is the\n deliberate path.\n fdk apply defaults to any-change: it confirms the whole pre-flight preview\n before any write, and stays silent when nothing changes. A single platform\n command defaults to sensitive-only \u2014 it asks for credential values and IAM only.\n Override with --require-approval never | sensitive-only | any-change or with\n ORCHESTRA_REQUIRE_APPROVAL; the legacy --approve-secrets is exactly never.\n Prompts and bindings are written as prompt`\u2026` / binding`\u2026` tagged templates\n over the typed context handle \u2014 never hand-spelled {{\u2026}} strings.\n\nEXAMPLES\n orchestra fdk apply # reconcile the factory in ./\n orchestra secrets create --name deepseek --value-from-command-line\n orchestra sessions list --status RUNNING --output json\n orchestra work-items move bug-12 --stage review\n orchestra gateway-keys create --slug ci-smoke --output-file key.json\n\nLEARN MORE\n orchestra <command> --help \u00B7 orchestra skills install --harness <name>\n --scope user (links the reference and setup skills into your coding agent \u2014\n the same documents AI agents use)\n";
36
38
  /**
37
39
  * A function rather than a constant because the epilogue names an ABSOLUTE PATH that differs per install and
38
40
  * can only be resolved by the running process. `buildProgram` calls it once and pins the result, so the text
@@ -76,6 +78,8 @@ export interface CommandHandlers {
76
78
  apply: (options: ApplyOptions) => void | Promise<void>;
77
79
  grant: (options: GrantCommandOptions) => void | Promise<void>;
78
80
  wait: (id: string, options: WaitCommandOptions) => void | Promise<void>;
81
+ tail: (id: string, options: TailCommandOptions) => void | Promise<void>;
82
+ computeProvidersCheck: (id: string, options: ComputeProviderCheckOptions) => void | Promise<void>;
79
83
  api: (method: string, path: string, options: RawApiOptions) => void | Promise<void>;
80
84
  skillInstall: (options: SkillInstallOptions) => void | Promise<void>;
81
85
  artifactsPreview: (file: string, options: PreviewCommandOptions) => void | Promise<void>;
package/dist/cli/main.js CHANGED
@@ -30,7 +30,9 @@ import { REQUIRE_APPROVAL_ENV, requireApprovalHelp } from "./core/approval-polic
30
30
  import { AUTH_ENV, authFlagsFrom, DEFAULT_PLATFORM_BASE_URL } from "./core/auth.js";
31
31
  import { apiCommand, RAW_METHODS } from "./api/raw.js";
32
32
  import { registerPlatformCommands } from "./api/register.js";
33
+ import { tailCommand } from "./api/tail.js";
33
34
  import { waitCommand } from "./api/wait.js";
35
+ import { computeProviderCheckCommand } from "./api/compute-provider-check.js";
34
36
  import { APPLY_APPROVAL_DEFAULT, applyCommand } from "./factory/apply.js";
35
37
  import { diffCommand } from "./factory/diff.js";
36
38
  import { DEFAULT_TEMPLATE, initCommand, MODEL_KEY_CHOICES, OFFERED_TEMPLATES } from "./factory/init.js";
@@ -107,6 +109,8 @@ PLATFORM COMMANDS orchestra <noun> <verb> — nouns match API paths exactly
107
109
  workflows revert.
108
110
  Typed flows: user-grants grant (mint a user-grant and walk its consent flow) ·
109
111
  sessions wait (poll a session until it settles) ·
112
+ sessions tail (print a session's items as they are saved) ·
113
+ compute-providers check (show why a Launcher fails) ·
110
114
  artifacts preview (serve an HTML page locally).
111
115
  Write verbs exist for trial and fallback; durable change belongs in code.
112
116
  Writing to a factory-managed resource warns: the next apply reverts it.
@@ -225,6 +229,8 @@ export const defaultHandlers = {
225
229
  apply: (options) => applyCommand(options),
226
230
  grant: (options) => grantCommand(options),
227
231
  wait: (id, options) => waitCommand(id, options),
232
+ tail: (id, options) => tailCommand(id, options),
233
+ computeProvidersCheck: (id, options) => computeProviderCheckCommand(id, options),
228
234
  api: (method, path, options) => apiCommand(method, path, options),
229
235
  skillInstall: (options) => skillInstallCommand(options),
230
236
  artifactsPreview: async (file, options) => {
@@ -489,6 +495,22 @@ function attachTypedFlows(program, handlers, pointer) {
489
495
  .option("--output-file <path>", "write the full raw settled resource as JSON to <path> (mode 0600)")
490
496
  .description("poll a session until it settles")
491
497
  .action((id, options) => handlers.wait(id, options));
498
+ // Reads the items list through its cursor and its waiting read, so a running turn paces the loop.
499
+ addAuthOptions(generatedNoun(program, "sessions", "tail").command("tail"))
500
+ .argument("<id>", "session id (session_…)")
501
+ .option("--after <item id>", "start after this item (default: print the items that already exist first)")
502
+ .option("--until-settled", "exit when the turn ends, printing the session's status")
503
+ .option("--output <mode>", "output mode: table (default) or json, one item per line")
504
+ .option("--reveal", "print credential material in full instead of masking it")
505
+ .description("print each item of a session as it is saved: messages and thoughts in full, tool calls as one line")
506
+ .addHelpText("after", "\nWith --until-settled it exits 0 once the turn has ended, whatever the status, as `sessions wait` does.\n" +
507
+ "Without it, it follows the session until it is stopped with Ctrl-C.\n")
508
+ .action((id, options) => handlers.tail(id, options));
509
+ addAuthOptions(generatedNoun(program, "compute-providers", "check").command("check"))
510
+ .argument("<id>", "compute provider id or slug")
511
+ .option("--output <mode>", "output mode: table (default) or json")
512
+ .description("read a compute provider and show why its Launcher does not work; exit 1 when it does not")
513
+ .action((id, options) => handlers.computeProvidersCheck(id, options));
492
514
  // A grant is never a declared resource: this mints the intent under the developer's own token and waits
493
515
  // while they finish the dance in their own authenticated browser session.
494
516
  addAuthOptions(generatedNoun(program, "user-grants", "grant").command("grant"))