@skrr-ai/cli 0.1.108 → 0.1.110

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 (202) hide show
  1. package/README.md +10 -0
  2. package/dist/base-command.d.ts +74 -0
  3. package/dist/base-command.js +182 -24
  4. package/dist/commands/agents/triggers/create.js +1 -1
  5. package/dist/commands/agents/triggers/update.js +1 -1
  6. package/dist/commands/agents/update.d.ts +25 -0
  7. package/dist/commands/agents/update.js +98 -3
  8. package/dist/commands/commitments/accounting.js +7 -2
  9. package/dist/commands/commitments/cost.d.ts +11 -0
  10. package/dist/commands/commitments/cost.js +26 -3
  11. package/dist/commands/commitments/create.js +2 -1
  12. package/dist/commands/commitments/operating-review.d.ts +15 -0
  13. package/dist/commands/commitments/operating-review.js +103 -50
  14. package/dist/commands/commitments/update.js +12 -5
  15. package/dist/commands/daemon/install.js +17 -1
  16. package/dist/commands/daemon/login.js +14 -0
  17. package/dist/commands/doctor.js +9 -0
  18. package/dist/commands/goals/list.js +6 -1
  19. package/dist/commands/grants/rotate.js +4 -0
  20. package/dist/commands/harnesses/prune.d.ts +26 -0
  21. package/dist/commands/harnesses/prune.js +172 -0
  22. package/dist/commands/login.js +53 -14
  23. package/dist/commands/machines/list.js +28 -6
  24. package/dist/commands/projects/show.js +1 -1
  25. package/dist/commands/revoke-daemon.js +2 -1
  26. package/dist/commands/spaces/workflows/archive.d.ts +1 -0
  27. package/dist/commands/spaces/workflows/archive.js +1 -0
  28. package/dist/commands/spaces/workflows/artifact.d.ts +1 -0
  29. package/dist/commands/spaces/workflows/artifact.js +1 -0
  30. package/dist/commands/spaces/workflows/cancel-node.d.ts +1 -0
  31. package/dist/commands/spaces/workflows/cancel-node.js +1 -0
  32. package/dist/commands/spaces/workflows/cancel.d.ts +1 -0
  33. package/dist/commands/spaces/workflows/cancel.js +6 -2
  34. package/dist/commands/spaces/workflows/create.d.ts +1 -0
  35. package/dist/commands/spaces/workflows/create.js +7 -1
  36. package/dist/commands/spaces/workflows/events.d.ts +1 -0
  37. package/dist/commands/spaces/workflows/events.js +1 -0
  38. package/dist/commands/spaces/workflows/fixtures/create.d.ts +1 -0
  39. package/dist/commands/spaces/workflows/fixtures/create.js +1 -0
  40. package/dist/commands/spaces/workflows/fixtures/list.d.ts +1 -0
  41. package/dist/commands/spaces/workflows/fixtures/list.js +1 -0
  42. package/dist/commands/spaces/workflows/lifecycle-diagnostics.d.ts +1 -0
  43. package/dist/commands/spaces/workflows/lifecycle-diagnostics.js +1 -0
  44. package/dist/commands/spaces/workflows/list.d.ts +1 -0
  45. package/dist/commands/spaces/workflows/list.js +1 -0
  46. package/dist/commands/spaces/workflows/node-types.d.ts +2 -0
  47. package/dist/commands/spaces/workflows/node-types.js +24 -6
  48. package/dist/commands/spaces/workflows/pause.d.ts +1 -0
  49. package/dist/commands/spaces/workflows/pause.js +1 -0
  50. package/dist/commands/spaces/workflows/plan/show.d.ts +1 -0
  51. package/dist/commands/spaces/workflows/plan/show.js +1 -0
  52. package/dist/commands/spaces/workflows/plan/update.d.ts +1 -0
  53. package/dist/commands/spaces/workflows/plan/update.js +1 -0
  54. package/dist/commands/spaces/workflows/proposals/apply.d.ts +1 -0
  55. package/dist/commands/spaces/workflows/proposals/apply.js +1 -0
  56. package/dist/commands/spaces/workflows/proposals/confirm.d.ts +1 -0
  57. package/dist/commands/spaces/workflows/proposals/confirm.js +1 -0
  58. package/dist/commands/spaces/workflows/proposals/create.d.ts +1 -0
  59. package/dist/commands/spaces/workflows/proposals/create.js +1 -0
  60. package/dist/commands/spaces/workflows/proposals/dismiss.d.ts +1 -0
  61. package/dist/commands/spaces/workflows/proposals/dismiss.js +1 -0
  62. package/dist/commands/spaces/workflows/proposals/list.d.ts +1 -0
  63. package/dist/commands/spaces/workflows/proposals/list.js +1 -0
  64. package/dist/commands/spaces/workflows/publish.d.ts +1 -0
  65. package/dist/commands/spaces/workflows/publish.js +9 -0
  66. package/dist/commands/spaces/workflows/reconcile.d.ts +1 -0
  67. package/dist/commands/spaces/workflows/reconcile.js +1 -0
  68. package/dist/commands/spaces/workflows/resume.d.ts +1 -0
  69. package/dist/commands/spaces/workflows/resume.js +1 -0
  70. package/dist/commands/spaces/workflows/retry.d.ts +1 -0
  71. package/dist/commands/spaces/workflows/retry.js +1 -0
  72. package/dist/commands/spaces/workflows/run.d.ts +1 -0
  73. package/dist/commands/spaces/workflows/run.js +14 -7
  74. package/dist/commands/spaces/workflows/runs.d.ts +1 -0
  75. package/dist/commands/spaces/workflows/runs.js +1 -0
  76. package/dist/commands/spaces/workflows/show.d.ts +25 -0
  77. package/dist/commands/spaces/workflows/show.js +42 -4
  78. package/dist/commands/spaces/workflows/signal.d.ts +12 -0
  79. package/dist/commands/spaces/workflows/signal.js +40 -2
  80. package/dist/commands/spaces/workflows/start.d.ts +1 -0
  81. package/dist/commands/spaces/workflows/start.js +4 -1
  82. package/dist/commands/spaces/workflows/task-execution/create.d.ts +1 -0
  83. package/dist/commands/spaces/workflows/task-execution/create.js +1 -0
  84. package/dist/commands/spaces/workflows/test.d.ts +1 -0
  85. package/dist/commands/spaces/workflows/test.js +1 -0
  86. package/dist/commands/spaces/workflows/triggers/create.d.ts +1 -0
  87. package/dist/commands/spaces/workflows/triggers/create.js +1 -0
  88. package/dist/commands/spaces/workflows/triggers/fire.d.ts +1 -0
  89. package/dist/commands/spaces/workflows/triggers/fire.js +1 -0
  90. package/dist/commands/spaces/workflows/triggers/list.d.ts +1 -0
  91. package/dist/commands/spaces/workflows/triggers/list.js +1 -0
  92. package/dist/commands/spaces/workflows/triggers/update.d.ts +1 -0
  93. package/dist/commands/spaces/workflows/triggers/update.js +1 -0
  94. package/dist/commands/spaces/workflows/update.d.ts +1 -0
  95. package/dist/commands/spaces/workflows/update.js +4 -0
  96. package/dist/commands/spaces/workflows/validate.d.ts +1 -0
  97. package/dist/commands/spaces/workflows/validate.js +27 -2
  98. package/dist/commands/tasks/activity.js +17 -3
  99. package/dist/commands/tasks/bulk-update.d.ts +1 -0
  100. package/dist/commands/tasks/bulk-update.js +7 -0
  101. package/dist/commands/tasks/complete.js +2 -1
  102. package/dist/commands/tasks/create-tree.d.ts +69 -0
  103. package/dist/commands/tasks/create-tree.js +413 -47
  104. package/dist/commands/tasks/list.d.ts +2 -0
  105. package/dist/commands/tasks/list.js +25 -3
  106. package/dist/commands/tasks/rearm-recurrence.d.ts +22 -0
  107. package/dist/commands/tasks/rearm-recurrence.js +87 -0
  108. package/dist/commands/tasks/show.d.ts +9 -0
  109. package/dist/commands/tasks/show.js +79 -33
  110. package/dist/commands/tasks/timeline.js +4 -0
  111. package/dist/commands/tasks/unschedule.js +5 -1
  112. package/dist/commands/tasks/update.d.ts +1 -0
  113. package/dist/commands/tasks/update.js +17 -2
  114. package/dist/commands/triggers/audit.js +12 -3
  115. package/dist/commands/triggers/create.js +9 -3
  116. package/dist/commands/triggers/fire-detail.d.ts +7 -0
  117. package/dist/commands/triggers/fire-detail.js +30 -12
  118. package/dist/commands/triggers/fire.js +13 -3
  119. package/dist/commands/triggers/parse-intent.d.ts +7 -0
  120. package/dist/commands/triggers/parse-intent.js +31 -9
  121. package/dist/commands/triggers/update.js +1 -1
  122. package/dist/commands/workspaces/grants.js +20 -3
  123. package/dist/commands/workspaces/members.d.ts +20 -0
  124. package/dist/commands/workspaces/members.js +64 -0
  125. package/dist/commands/workspaces/stewardship.js +14 -2
  126. package/dist/help.js +12 -0
  127. package/dist/hooks/command-not-found.js +27 -1
  128. package/dist/lib/command-miss.d.ts +52 -2
  129. package/dist/lib/command-miss.js +187 -18
  130. package/dist/lib/commitment-loop-status.d.ts +8 -0
  131. package/dist/lib/commitment-loop-status.js +45 -0
  132. package/dist/lib/commitment-product.js +5 -2
  133. package/dist/lib/commitments.d.ts +18 -3
  134. package/dist/lib/commitments.js +85 -45
  135. package/dist/lib/daemon-command-hint.d.ts +15 -0
  136. package/dist/lib/daemon-command-hint.js +21 -0
  137. package/dist/lib/daemon-machine-identity-copy.d.ts +14 -0
  138. package/dist/lib/daemon-machine-identity-copy.js +27 -0
  139. package/dist/lib/daemon-machine-identity.d.ts +27 -0
  140. package/dist/lib/daemon-machine-identity.js +61 -0
  141. package/dist/lib/daemon-setup.js +10 -1
  142. package/dist/lib/daemonBrokerRefusal.js +3 -2
  143. package/dist/lib/daemonHandoff.d.ts +19 -1
  144. package/dist/lib/daemonHandoff.js +61 -1
  145. package/dist/lib/deployed-build.d.ts +43 -0
  146. package/dist/lib/deployed-build.js +66 -0
  147. package/dist/lib/grants.d.ts +27 -1
  148. package/dist/lib/grants.js +39 -21
  149. package/dist/lib/harnesses.d.ts +71 -0
  150. package/dist/lib/harnesses.js +70 -0
  151. package/dist/lib/json-mode.d.ts +11 -0
  152. package/dist/lib/json-mode.js +19 -0
  153. package/dist/lib/json-output.d.ts +25 -0
  154. package/dist/lib/json-output.js +30 -0
  155. package/dist/lib/list-output.d.ts +10 -1
  156. package/dist/lib/list-output.js +22 -1
  157. package/dist/lib/local-daemon-readiness.d.ts +90 -0
  158. package/dist/lib/local-daemon-readiness.js +143 -0
  159. package/dist/lib/machines.d.ts +117 -0
  160. package/dist/lib/machines.js +139 -0
  161. package/dist/lib/managed-instruction-local.d.ts +2 -0
  162. package/dist/lib/managed-instruction-local.js +2 -0
  163. package/dist/lib/profile-machine-identity-guard.d.ts +18 -0
  164. package/dist/lib/profile-machine-identity-guard.js +39 -0
  165. package/dist/lib/request-field-flags.d.ts +20 -0
  166. package/dist/lib/request-field-flags.js +34 -0
  167. package/dist/lib/stewardship-output.d.ts +1 -1
  168. package/dist/lib/stewardship-output.js +26 -30
  169. package/dist/lib/task-batch.d.ts +61 -4
  170. package/dist/lib/task-batch.js +316 -80
  171. package/dist/lib/task-economics.d.ts +15 -0
  172. package/dist/lib/task-economics.js +39 -2
  173. package/dist/lib/task-recurrence-outcome.d.ts +6 -6
  174. package/dist/lib/task-recurrence-outcome.js +27 -3
  175. package/dist/lib/tasks.d.ts +39 -0
  176. package/dist/lib/tasks.js +132 -0
  177. package/dist/lib/triggers.d.ts +42 -1
  178. package/dist/lib/triggers.js +98 -2
  179. package/dist/lib/workflow-issues.d.ts +14 -0
  180. package/dist/lib/workflow-issues.js +32 -0
  181. package/dist/lib/workflow-run-view.d.ts +59 -0
  182. package/dist/lib/workflow-run-view.js +110 -0
  183. package/dist/lib/workspaces.d.ts +48 -4
  184. package/dist/lib/workspaces.js +55 -0
  185. package/dist/lib/write-target.d.ts +29 -0
  186. package/dist/lib/write-target.js +72 -0
  187. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  188. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +5 -3
  189. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +60 -0
  190. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +130 -6
  191. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/workspaceIsolationProbeWire.d.ts +51 -0
  192. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/workspaceIsolationProbeWire.js +50 -0
  193. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  194. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
  195. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +60 -0
  196. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +128 -6
  197. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/workspaceIsolationProbeWire.d.ts +51 -0
  198. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/workspaceIsolationProbeWire.js +46 -0
  199. package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
  200. package/dist/node_modules/@skrr-ai/data-provider/index.js +7523 -7140
  201. package/oclif.manifest.json +473 -59
  202. package/package.json +28 -1
package/README.md CHANGED
@@ -1780,8 +1780,18 @@ sky tasks retry-schedule <task-id> --at 2026-06-02T15:00:00Z
1780
1780
  # Pause / resume a recurring schedule without losing its definition
1781
1781
  sky tasks pause-schedule <task-id>
1782
1782
  sky tasks resume-schedule <task-id>
1783
+
1784
+ # A finished occurrence whose next occurrence was never armed (`tasks show` says
1785
+ # "The next occurrence was not scheduled: ..."): continue the series from it.
1786
+ # `tasks schedule` refuses a finished Task; this is the way back. --cron fixes an
1787
+ # invalid pattern first.
1788
+ skrr tasks rearm-recurrence <finished-task-id>
1789
+ skrr tasks rearm-recurrence <finished-task-id> --cron "0 9 * * 1-5"
1783
1790
  ```
1784
1791
 
1792
+ A pattern is validated when it is written: `--cron nonsense` is refused with the
1793
+ expected format and an example, never stored and armed at some other time.
1794
+
1785
1795
  Pass `--from-json schedule.json` to `schedule` for fields not exposed as
1786
1796
  typed flags (matches the `TaskScheduleParams` shape).
1787
1797
 
@@ -1,5 +1,7 @@
1
1
  import { Command, Flags, type Interfaces } from '@oclif/core';
2
2
  export { resolveProfileFromArgv } from './lib/keychain';
3
+ import { dataService } from '@skrr-ai/data-provider';
4
+ import { type WriteTarget } from './lib/write-target';
3
5
  import { type AssigneeResolution, type MemberKind, type SpaceMember } from './lib/assignee-resolver';
4
6
  import { type CliConfig } from './lib/config';
5
7
  import { type OutboxMethod } from './lib/outbox';
@@ -59,6 +61,24 @@ export declare abstract class BaseCommand extends Command {
59
61
  */
60
62
  log(message?: string, ...args: unknown[]): void;
61
63
  protected wantsRawJson(): boolean;
64
+ /**
65
+ * Print a command's `--json` RESULT: pretty JSON, with `ok` on an object so a
66
+ * script can test one field for success and failure alike (OSK-16425).
67
+ *
68
+ * A failure already prints `{ok:false, error}`; a success printed whatever the
69
+ * server sent, so `jq '{id, title}'` turned the failure into `{id:null,
70
+ * title:null}` — an empty success. `ok` is added HERE, by the command that
71
+ * knows its own outcome, and not in `log()`: `log` cannot see the exit status
72
+ * to come, and several commands print a document and then exit non-zero.
73
+ * Pass `{ ok: false }` for one of those; the command still owns its exit code.
74
+ *
75
+ * An array is printed as it is (it cannot carry a field), and an object that
76
+ * already has `ok` keeps its own. The exit status stays the test for a command
77
+ * that has not been moved here yet.
78
+ */
79
+ protected printJsonResult(value: unknown, options?: {
80
+ ok?: boolean;
81
+ }): void;
62
82
  protected cliConfig: CliConfig;
63
83
  /**
64
84
  * Resolved credential for this process. Populated in `init()` and
@@ -220,6 +240,17 @@ export declare abstract class BaseCommand extends Command {
220
240
  * friendly error on any API failure, so callers can `await` it directly.
221
241
  */
222
242
  protected resolveCurrentUserId(): Promise<string>;
243
+ /**
244
+ * The signed-in user, read ONCE per command run.
245
+ *
246
+ * `me`, the default-assignee policy and the write-target notice all need it,
247
+ * and a command that touches many nodes (`create-tree` with ninety-four)
248
+ * asked for it once per node. Who you are does not change during one command,
249
+ * so one read answers every caller — and a failed read is not remembered, so a
250
+ * transient error does not poison the rest of the run.
251
+ */
252
+ private currentUserRead?;
253
+ protected fetchCurrentUser(): Promise<Awaited<ReturnType<typeof dataService.getUser>>>;
223
254
  /**
224
255
  * Decide who owns a task this invocation is creating, when the caller did
225
256
  * not say.
@@ -249,6 +280,27 @@ export declare abstract class BaseCommand extends Command {
249
280
  * worse, resolve a typo to a stranger the caller never intended to name.
250
281
  */
251
282
  protected loadSpaceMembers(spaceId: string): Promise<SpaceMember[]>;
283
+ /** Lazily created: a command built without its constructor (specs do) must still work. */
284
+ private spaceMemberReads?;
285
+ /**
286
+ * Where a write from this invocation is about to land: the signed-in account,
287
+ * the server and the workspace. Best effort on the account (a failed read
288
+ * leaves it null and the line says so) — it is a statement, not a gate; the
289
+ * gate is `assertWriteTarget`.
290
+ */
291
+ protected describeWriteTarget(): Promise<WriteTarget>;
292
+ /**
293
+ * Say where a bulk write is about to land, and refuse if the caller said it
294
+ * should land somewhere else.
295
+ *
296
+ * The line goes to stderr and only when stderr is a person's terminal: an
297
+ * agent capturing `2>&1` gets no extra bytes (the update nudge taught that
298
+ * lesson), and gets the same facts in-band instead — the commands that call
299
+ * this put `target` in their `--json`. `--expect-target` is the control for a
300
+ * caller that cannot watch: it is checked HERE, before the first write, and a
301
+ * mismatch is a non-retryable usage error.
302
+ */
303
+ protected assertWriteTarget(expected?: readonly string[]): Promise<WriteTarget>;
252
304
  /**
253
305
  * Turn free-text assignee references ("frank", "frank@example.com", "me",
254
306
  * or a raw id) into ids, failing the command if ANY of them can't be pinned
@@ -316,6 +368,13 @@ export declare abstract class BaseCommand extends Command {
316
368
  * subject is folded into the message, so the human line and the `--json`
317
369
  * envelope carry the same identifying text.
318
370
  */
371
+ /**
372
+ * Request body paths this invocation built from its flags, each mapped to the flag that wrote
373
+ * it. Set by a command before it sends a body it assembled from flags, and left unset when the
374
+ * person supplied the body themselves; `handleApiError` uses it to name the flag instead of a
375
+ * path the person never typed. See `lib/request-field-flags.ts`.
376
+ */
377
+ protected requestFieldFlags?: Readonly<Record<string, string>>;
319
378
  protected handleApiError(err: unknown, { subject }?: {
320
379
  subject?: string;
321
380
  }): never;
@@ -526,6 +585,21 @@ export declare function isTypedServerRefusal(body: Record<string, unknown> | nul
526
585
  */
527
586
  export declare function isApplicationCodedUnavailable(status: number | undefined, body: Record<string, unknown> | null): boolean;
528
587
  export declare function isRetryableFailure(status: number | undefined, body: Record<string, unknown> | null): boolean;
588
+ /**
589
+ * The `details` of a CLI error envelope, in ONE shape. The server's refusal body becomes the
590
+ * envelope's details; when that body carries its own `details` object (a typed refusal's
591
+ * findings, e.g. `signalableNodeKeys`), those fields are lifted to the same level instead of
592
+ * arriving as `error.details.details`, a depth no other error used (OSK-16494). A body without
593
+ * a nested object is passed through unchanged.
594
+ */
595
+ export declare function envelopeDetails(body: Record<string, unknown> | null | undefined): unknown;
596
+ /**
597
+ * The hint for a `Nonexistent flags:` refusal, or null. Two cases:
598
+ * - a flag retired from THIS command, which this command does not define again: where it went;
599
+ * - a flag token holding a space (`"--outcome failed"` passed as one argument): what happened,
600
+ * which oclif reports only as an unknown flag.
601
+ */
602
+ export declare function retiredFlagHint(message: string, commandId?: string, definedFlags?: readonly string[]): string | null;
529
603
  /**
530
604
  * P6: a parse error used to say what was wrong and drop the remedy that text
531
605
  * mode (and every domain error) carries. Adds `fix` (one imperative line an
@@ -17,12 +17,16 @@ exports.describeDaemonScopedCredential = describeDaemonScopedCredential;
17
17
  exports.isTypedServerRefusal = isTypedServerRefusal;
18
18
  exports.isApplicationCodedUnavailable = isApplicationCodedUnavailable;
19
19
  exports.isRetryableFailure = isRetryableFailure;
20
+ exports.envelopeDetails = envelopeDetails;
21
+ exports.retiredFlagHint = retiredFlagHint;
20
22
  exports.parseErrorRemedy = parseErrorRemedy;
21
23
  const node_fs_1 = require("node:fs");
22
24
  const core_1 = require("@oclif/core");
23
25
  Object.defineProperty(exports, "Flags", { enumerable: true, get: function () { return core_1.Flags; } });
24
26
  const option_hint_1 = require("./lib/option-hint");
27
+ const request_field_flags_1 = require("./lib/request-field-flags");
25
28
  const json_output_1 = require("./lib/json-output");
29
+ const json_mode_1 = require("./lib/json-mode");
26
30
  const failed_lookup_1 = require("./lib/failed-lookup");
27
31
  const internal_id_1 = require("./lib/internal-id");
28
32
  const http_error_1 = require("./lib/http-error");
@@ -36,6 +40,7 @@ const data_provider_1 = require("@skrr-ai/data-provider");
36
40
  const auth_core_1 = require("@skrr-ai/auth-core");
37
41
  const auth_failure_1 = require("./lib/auth-failure");
38
42
  const list_output_1 = require("./lib/list-output");
43
+ const write_target_1 = require("./lib/write-target");
39
44
  const assignee_resolver_1 = require("./lib/assignee-resolver");
40
45
  const task_ref_resolver_1 = require("./lib/task-ref-resolver");
41
46
  const space_resolver_1 = require("./lib/space-resolver");
@@ -176,6 +181,24 @@ class BaseCommand extends core_1.Command {
176
181
  wantsRawJson() {
177
182
  return this.argv.includes('--raw') || process.env.OVERSKY_CLI_RAW === '1';
178
183
  }
184
+ /**
185
+ * Print a command's `--json` RESULT: pretty JSON, with `ok` on an object so a
186
+ * script can test one field for success and failure alike (OSK-16425).
187
+ *
188
+ * A failure already prints `{ok:false, error}`; a success printed whatever the
189
+ * server sent, so `jq '{id, title}'` turned the failure into `{id:null,
190
+ * title:null}` — an empty success. `ok` is added HERE, by the command that
191
+ * knows its own outcome, and not in `log()`: `log` cannot see the exit status
192
+ * to come, and several commands print a document and then exit non-zero.
193
+ * Pass `{ ok: false }` for one of those; the command still owns its exit code.
194
+ *
195
+ * An array is printed as it is (it cannot carry a field), and an object that
196
+ * already has `ok` keeps its own. The exit status stays the test for a command
197
+ * that has not been moved here yet.
198
+ */
199
+ printJsonResult(value, options = {}) {
200
+ this.log(JSON.stringify((0, json_output_1.withOk)(value, options.ok ?? true), null, 2));
201
+ }
179
202
  cliConfig;
180
203
  /**
181
204
  * Resolved credential for this process. Populated in `init()` and
@@ -928,7 +951,7 @@ class BaseCommand extends core_1.Command {
928
951
  async resolveCurrentUserId() {
929
952
  let user;
930
953
  try {
931
- user = await data_provider_1.dataService.getUser();
954
+ user = await this.fetchCurrentUser();
932
955
  }
933
956
  catch (err) {
934
957
  this.handleApiError(err);
@@ -941,6 +964,25 @@ class BaseCommand extends core_1.Command {
941
964
  }
942
965
  return user.id;
943
966
  }
967
+ /**
968
+ * The signed-in user, read ONCE per command run.
969
+ *
970
+ * `me`, the default-assignee policy and the write-target notice all need it,
971
+ * and a command that touches many nodes (`create-tree` with ninety-four)
972
+ * asked for it once per node. Who you are does not change during one command,
973
+ * so one read answers every caller — and a failed read is not remembered, so a
974
+ * transient error does not poison the rest of the run.
975
+ */
976
+ currentUserRead;
977
+ fetchCurrentUser() {
978
+ if (!this.currentUserRead) {
979
+ this.currentUserRead = data_provider_1.dataService.getUser().catch((err) => {
980
+ this.currentUserRead = undefined;
981
+ throw err;
982
+ });
983
+ }
984
+ return this.currentUserRead;
985
+ }
944
986
  /**
945
987
  * Decide who owns a task this invocation is creating, when the caller did
946
988
  * not say.
@@ -979,6 +1021,12 @@ class BaseCommand extends core_1.Command {
979
1021
  * worse, resolve a typo to a stranger the caller never intended to name.
980
1022
  */
981
1023
  async loadSpaceMembers(spaceId) {
1024
+ // A space's roster does not change during one command; a tree of ninety-four
1025
+ // nodes that name people read it ninety-four times.
1026
+ const reads = (this.spaceMemberReads ??= new Map());
1027
+ const cached = reads.get(spaceId);
1028
+ if (cached)
1029
+ return cached;
982
1030
  let response;
983
1031
  try {
984
1032
  response = await data_provider_1.dataService.getResourcePermissions(data_provider_1.ResourceType.SPACE, spaceId);
@@ -987,7 +1035,67 @@ class BaseCommand extends core_1.Command {
987
1035
  this.handleApiError(err);
988
1036
  }
989
1037
  const principals = (response.principals ?? []);
990
- return (0, assignee_resolver_1.membersFromPrincipals)(principals);
1038
+ const members = (0, assignee_resolver_1.membersFromPrincipals)(principals);
1039
+ reads.set(spaceId, Promise.resolve(members));
1040
+ return members;
1041
+ }
1042
+ /** Lazily created: a command built without its constructor (specs do) must still work. */
1043
+ spaceMemberReads;
1044
+ /**
1045
+ * Where a write from this invocation is about to land: the signed-in account,
1046
+ * the server and the workspace. Best effort on the account (a failed read
1047
+ * leaves it null and the line says so) — it is a statement, not a gate; the
1048
+ * gate is `assertWriteTarget`.
1049
+ */
1050
+ async describeWriteTarget() {
1051
+ let user;
1052
+ try {
1053
+ user = await this.fetchCurrentUser();
1054
+ }
1055
+ catch {
1056
+ user = undefined;
1057
+ }
1058
+ const workspace = process.env.OVERSKY_SESSION_WORKSPACE_ID?.trim() ||
1059
+ process.env.OVERSKY_WORKSPACE_ID?.trim() ||
1060
+ null;
1061
+ return {
1062
+ server: this.cliConfig?.baseURL ?? '',
1063
+ account: user?.email ?? null,
1064
+ accountId: user?.id ?? null,
1065
+ workspace,
1066
+ };
1067
+ }
1068
+ /**
1069
+ * Say where a bulk write is about to land, and refuse if the caller said it
1070
+ * should land somewhere else.
1071
+ *
1072
+ * The line goes to stderr and only when stderr is a person's terminal: an
1073
+ * agent capturing `2>&1` gets no extra bytes (the update nudge taught that
1074
+ * lesson), and gets the same facts in-band instead — the commands that call
1075
+ * this put `target` in their `--json`. `--expect-target` is the control for a
1076
+ * caller that cannot watch: it is checked HERE, before the first write, and a
1077
+ * mismatch is a non-retryable usage error.
1078
+ */
1079
+ async assertWriteTarget(expected) {
1080
+ const target = await this.describeWriteTarget();
1081
+ const unmet = (0, write_target_1.firstUnmetExpectation)(target, expected);
1082
+ if (unmet !== undefined) {
1083
+ this.failWithCliError({
1084
+ message: `Refusing to write: this command would write to ${(0, write_target_1.formatWriteTarget)(target)}, ` +
1085
+ `which does not match --expect-target ${unmet}. Nothing was created.`,
1086
+ code: 'WRITE_TARGET_MISMATCH',
1087
+ exit: 2,
1088
+ retryable: false,
1089
+ details: {
1090
+ suggestion: `Sign in to the intended account or server (\`${this.config.bin} login\`, \`${this.config.bin} whoami\`), or correct --expect-target.`,
1091
+ context: { target, expected: [...(expected ?? [])] },
1092
+ },
1093
+ });
1094
+ }
1095
+ if (process.stderr.isTTY) {
1096
+ process.stderr.write(`Writing to ${(0, write_target_1.formatWriteTarget)(target)}\n`);
1097
+ }
1098
+ return target;
991
1099
  }
992
1100
  /**
993
1101
  * Turn free-text assignee references ("frank", "frank@example.com", "me",
@@ -1085,7 +1193,7 @@ class BaseCommand extends core_1.Command {
1085
1193
  // so a caller following older instructions — including the managed block the
1086
1194
  // platform itself installed — got a bare `Nonexistent flags:` with no hint
1087
1195
  // that the capability moved rather than vanished. Name where each went.
1088
- const retiredHint = retiredFlagHint(err.message || '');
1196
+ const retiredHint = retiredFlagHint(err.message || '', this.id, Object.keys(this.constructor.flags ?? {}));
1089
1197
  if (retiredHint) {
1090
1198
  err.message = `${err.message}\n\n${retiredHint}`;
1091
1199
  }
@@ -1170,6 +1278,13 @@ class BaseCommand extends core_1.Command {
1170
1278
  * subject is folded into the message, so the human line and the `--json`
1171
1279
  * envelope carry the same identifying text.
1172
1280
  */
1281
+ /**
1282
+ * Request body paths this invocation built from its flags, each mapped to the flag that wrote
1283
+ * it. Set by a command before it sends a body it assembled from flags, and left unset when the
1284
+ * person supplied the body themselves; `handleApiError` uses it to name the flag instead of a
1285
+ * path the person never typed. See `lib/request-field-flags.ts`.
1286
+ */
1287
+ requestFieldFlags;
1173
1288
  handleApiError(err, { subject } = {}) {
1174
1289
  // An error this command already raised for the user is not an API failure
1175
1290
  // to translate. `this.error()` throws a CLIError carrying `oclif.exit`, and
@@ -1194,7 +1309,8 @@ class BaseCommand extends core_1.Command {
1194
1309
  this.lastRetryAfterMs = e.retryAfterMs;
1195
1310
  const body = parseErrorBody(e.body);
1196
1311
  const bodyCode = stringProp(body, 'code') || stringProp(body, 'errorType');
1197
- const bodyMessage = stringProp(body, 'message') || stringProp(body, 'error');
1312
+ // A body path the person never typed is read back as the flag that wrote it (OSK-15313).
1313
+ const bodyMessage = (0, request_field_flags_1.translateRequestFieldPaths)(stringProp(body, 'message') || stringProp(body, 'error'), this.requestFieldFlags);
1198
1314
  // `requireAuth()` catches this before requests for normal commands, but
1199
1315
  // keep the API-error check too: a handful of commands make a direct
1200
1316
  // request and a supplied `--token` can bypass their normal preflight.
@@ -1222,7 +1338,7 @@ class BaseCommand extends core_1.Command {
1222
1338
  status: e.status,
1223
1339
  exit: 2,
1224
1340
  retryable: false,
1225
- details: body ?? undefined,
1341
+ details: envelopeDetails(body),
1226
1342
  });
1227
1343
  }
1228
1344
  else if (bodyCode === 'AGENT_RUNTIME_UNAVAILABLE') {
@@ -1236,7 +1352,7 @@ class BaseCommand extends core_1.Command {
1236
1352
  status: e.status,
1237
1353
  exit: 1,
1238
1354
  retryable: false,
1239
- details: body ?? undefined,
1355
+ details: envelopeDetails(body),
1240
1356
  });
1241
1357
  }
1242
1358
  else if (bodyCode === 'GITHUB_APP_NOT_CONFIGURED') {
@@ -1246,7 +1362,7 @@ class BaseCommand extends core_1.Command {
1246
1362
  status: e.status,
1247
1363
  exit: 2,
1248
1364
  retryable: false,
1249
- details: body ?? undefined,
1365
+ details: envelopeDetails(body),
1250
1366
  });
1251
1367
  }
1252
1368
  else if (bodyCode === 'GITHUB_UNAVAILABLE') {
@@ -1262,7 +1378,7 @@ class BaseCommand extends core_1.Command {
1262
1378
  status: e.status,
1263
1379
  exit: 5,
1264
1380
  retryable: true,
1265
- details: body ?? undefined,
1381
+ details: envelopeDetails(body),
1266
1382
  });
1267
1383
  }
1268
1384
  else if (bodyCode === 'LABEL_SUGGESTION_UNAVAILABLE') {
@@ -1276,7 +1392,7 @@ class BaseCommand extends core_1.Command {
1276
1392
  status: e.status,
1277
1393
  exit: 1,
1278
1394
  retryable: false,
1279
- details: body ?? undefined,
1395
+ details: envelopeDetails(body),
1280
1396
  });
1281
1397
  }
1282
1398
  else if (bodyCode === 'ASSIGNEE_NOT_IN_SPACE') {
@@ -1289,7 +1405,7 @@ class BaseCommand extends core_1.Command {
1289
1405
  status: e.status,
1290
1406
  exit: 2,
1291
1407
  retryable: false,
1292
- details: body ?? undefined,
1408
+ details: envelopeDetails(body),
1293
1409
  });
1294
1410
  }
1295
1411
  else if (e.status === 401) {
@@ -1438,6 +1554,10 @@ class BaseCommand extends core_1.Command {
1438
1554
  status: e.status,
1439
1555
  exit: 4,
1440
1556
  retryable: false,
1557
+ // A typed 404 carries what the server found (a workflow signal names the nodes it CAN
1558
+ // resolve); dropping the body here made the 404 the one refusal with no machine-readable
1559
+ // details while every 409 kept them (OSK-16473).
1560
+ ...(body ? { details: envelopeDetails(body) } : {}),
1441
1561
  });
1442
1562
  }
1443
1563
  else if (e.code === 'ECONNREFUSED' ||
@@ -1470,7 +1590,7 @@ class BaseCommand extends core_1.Command {
1470
1590
  status: e.status,
1471
1591
  exit: 1,
1472
1592
  retryable: isRetryableFailure(e.status, body),
1473
- details: body ?? undefined,
1593
+ details: envelopeDetails(body),
1474
1594
  requestId: stringProp(body, 'requestId') || e.requestId,
1475
1595
  });
1476
1596
  }
@@ -1509,7 +1629,7 @@ class BaseCommand extends core_1.Command {
1509
1629
  status: e.status,
1510
1630
  exit: retryable ? 5 : 1,
1511
1631
  retryable,
1512
- details: body ?? undefined,
1632
+ details: envelopeDetails(body),
1513
1633
  ...(requestId ? { requestId } : {}),
1514
1634
  });
1515
1635
  }
@@ -1547,7 +1667,7 @@ class BaseCommand extends core_1.Command {
1547
1667
  status: e.status,
1548
1668
  exit: 5,
1549
1669
  retryable: true,
1550
- details: body ?? undefined,
1670
+ details: envelopeDetails(body),
1551
1671
  });
1552
1672
  }
1553
1673
  else if (typeof e.status === 'number' && e.status >= 500) {
@@ -1575,7 +1695,7 @@ class BaseCommand extends core_1.Command {
1575
1695
  status: e.status,
1576
1696
  exit: 1,
1577
1697
  retryable: isRetryableFailure(e.status, body),
1578
- details: body ?? undefined,
1698
+ details: envelopeDetails(body),
1579
1699
  });
1580
1700
  }
1581
1701
  else {
@@ -1585,7 +1705,7 @@ class BaseCommand extends core_1.Command {
1585
1705
  status: e.status,
1586
1706
  exit: 1,
1587
1707
  retryable: isRetryableFailure(e.status, body),
1588
- details: body ?? undefined,
1708
+ details: envelopeDetails(body),
1589
1709
  });
1590
1710
  }
1591
1711
  }
@@ -1817,10 +1937,7 @@ class BaseCommand extends core_1.Command {
1817
1937
  process.stderr.write(`${line}\n`);
1818
1938
  }
1819
1939
  wantsJsonOutput() {
1820
- return (this.argv.includes('--json') ||
1821
- this.argv.includes('--ndjson') ||
1822
- process.env.SKY_JSON === '1' ||
1823
- process.env.OVERSKY_CLI_JSON === '1');
1940
+ return (0, json_mode_1.wantsJsonFromArgv)(this.argv);
1824
1941
  }
1825
1942
  }
1826
1943
  exports.BaseCommand = BaseCommand;
@@ -2343,13 +2460,54 @@ const RETIRED_FLAG_REPLACEMENTS = {
2343
2460
  '`tasks deliverable add`.',
2344
2461
  '--supersede': '--supersede is implicit: submitting again creates a new Result version.',
2345
2462
  };
2346
- function retiredFlagHint(message) {
2463
+ /**
2464
+ * The `details` of a CLI error envelope, in ONE shape. The server's refusal body becomes the
2465
+ * envelope's details; when that body carries its own `details` object (a typed refusal's
2466
+ * findings, e.g. `signalableNodeKeys`), those fields are lifted to the same level instead of
2467
+ * arriving as `error.details.details`, a depth no other error used (OSK-16494). A body without
2468
+ * a nested object is passed through unchanged.
2469
+ */
2470
+ function envelopeDetails(body) {
2471
+ if (!body)
2472
+ return undefined;
2473
+ const nested = body.details;
2474
+ if (!nested || typeof nested !== 'object' || Array.isArray(nested))
2475
+ return body;
2476
+ const { details: _nested, ...rest } = body;
2477
+ return { ...rest, ...nested };
2478
+ }
2479
+ /**
2480
+ * The commands those flags were retired FROM. The table was applied to every command, so a
2481
+ * mis-quoted `"--outcome failed"` on `triggers audit` (where --outcome is a live flag) was told
2482
+ * "--outcome is now --summary" (OSK-16494). A rename is a fact about one command.
2483
+ */
2484
+ const RETIRED_FLAG_COMMANDS = new Set(['tasks:complete', 'tasks:result:submit']);
2485
+ /**
2486
+ * The hint for a `Nonexistent flags:` refusal, or null. Two cases:
2487
+ * - a flag retired from THIS command, which this command does not define again: where it went;
2488
+ * - a flag token holding a space (`"--outcome failed"` passed as one argument): what happened,
2489
+ * which oclif reports only as an unknown flag.
2490
+ */
2491
+ function retiredFlagHint(message, commandId, definedFlags = []) {
2347
2492
  if (!/nonexistent flag/i.test(message))
2348
2493
  return null;
2349
- const hits = Object.keys(RETIRED_FLAG_REPLACEMENTS).filter((flag) => new RegExp(`${flag}(?![\\w-])`).test(message));
2350
- if (hits.length === 0)
2351
- return null;
2352
- return hits.map((flag) => ` ${RETIRED_FLAG_REPLACEMENTS[flag]}`).join('\n');
2494
+ const lines = [];
2495
+ const quoted = /(?:^|[\s,:])(--[a-z][\w-]*)[ =]([^\s,][^,\n]*)/i.exec(message.slice(message.search(/nonexistent flag/i)));
2496
+ const quotedFlag = quoted?.[1];
2497
+ if (quotedFlag && definedFlags.includes(quotedFlag.replace(/^--/, ''))) {
2498
+ lines.push(` "${quoted[0].trim()}" reached the command as ONE argument, so it was read as a flag named ` +
2499
+ `"${quoted[0].trim()}". Pass the flag and its value separately: ${quotedFlag} ${quoted[2].trim()}`);
2500
+ }
2501
+ if (commandId && RETIRED_FLAG_COMMANDS.has(commandId)) {
2502
+ for (const flag of Object.keys(RETIRED_FLAG_REPLACEMENTS)) {
2503
+ if (definedFlags.includes(flag.replace(/^--/, '')))
2504
+ continue;
2505
+ if (new RegExp(`${flag}(?![\\w-])`).test(message)) {
2506
+ lines.push(` ${RETIRED_FLAG_REPLACEMENTS[flag]}`);
2507
+ }
2508
+ }
2509
+ }
2510
+ return lines.length ? lines.join('\n') : null;
2353
2511
  }
2354
2512
  /**
2355
2513
  * P6: a parse error used to say what was wrong and drop the remedy that text
@@ -76,7 +76,7 @@ class AgentsTriggersCreate extends base_command_1.BaseCommand {
76
76
  description: 'ISO-8601 datetime after which a schedule stops firing (--kind schedule)',
77
77
  }),
78
78
  'stale-catchup-ms': core_1.Flags.integer({
79
- description: 'How late a missed occurrence may still run, in ms (a positive number). After downtime, an occurrence more than this overdue is skipped rather than run late, and the schedule moves to its next future slot. Unset: 3600000 (1 hour).',
79
+ description: 'How late a missed occurrence may still run, in ms, from 1000 (1 second) to 31622400000 (366 days). After downtime, an occurrence more than this overdue is skipped rather than run late, and the schedule moves to its next future slot. Unset: 3600000 (1 hour).',
80
80
  }),
81
81
  // once
82
82
  at: core_1.Flags.string({
@@ -63,7 +63,7 @@ class AgentsTriggersUpdate extends base_command_1.BaseCommand {
63
63
  description: 'ISO-8601 datetime after which a schedule stops firing',
64
64
  }),
65
65
  'stale-catchup-ms': core_1.Flags.integer({
66
- description: 'How late a missed occurrence may still run, in ms (a positive number). After downtime, an occurrence more than this overdue is skipped rather than run late, and the schedule moves to its next future slot. Unset: 3600000 (1 hour).',
66
+ description: 'How late a missed occurrence may still run, in ms, from 1000 (1 second) to 31622400000 (366 days). After downtime, an occurrence more than this overdue is skipped rather than run late, and the schedule moves to its next future slot. Unset: 3600000 (1 hour).',
67
67
  }),
68
68
  at: core_1.Flags.string({ description: 'ISO-8601 datetime for once triggers' }),
69
69
  entity: core_1.Flags.string({
@@ -1,3 +1,4 @@
1
+ import { type AgentWriteReceipt } from '@skrr-ai/data-provider';
1
2
  import { BaseCommand } from '../../base-command';
2
3
  export default class AgentsUpdate extends BaseCommand {
3
4
  static description: string;
@@ -72,9 +73,33 @@ export default class AgentsUpdate extends BaseCommand {
72
73
  * through `--from-json` was reported as "not an agent field" after the server
73
74
  * had cleared it on purpose (OSK-12167).
74
75
  *
76
+ * All of that is the fallback for a server that predates `writeReceipt`. When
77
+ * the response carries a receipt, the server's own account of each key is used
78
+ * instead and nothing is inferred from the response's shape (OSK-15070).
79
+ *
75
80
  * Exported for tests.
76
81
  */
77
82
  export declare function agentUpdateWarnings(payload: Record<string, unknown>, updated: unknown): string[];
83
+ /**
84
+ * The Agent row inside an update reply: the reply itself for an ordinary
85
+ * PATCH, `agent` inside the Always-On control envelope, or null when the
86
+ * control reply carries none (a PATCH of `isAlwaysOn` alone). Exported for tests.
87
+ */
88
+ export declare function agentRowFromUpdate(updated: unknown): Record<string, unknown> | null;
89
+ /**
90
+ * One line for an `isAlwaysOn` write, from the receipt: applied, or already in
91
+ * that state so nothing changed (OSK-16477). Exported for tests.
92
+ */
93
+ export declare function alwaysOnReceiptLine(payload: Record<string, unknown>, updated: unknown): string | null;
94
+ /**
95
+ * Warnings from the server's `writeReceipt` (OSK-15070). The receipt names
96
+ * every key the write saw as applied (and through which control: the legacy
97
+ * `isAlwaysOn` goes to the Always-On control and is applied, not "not kept") or
98
+ * ignored with a reason. A sent key it does not mention never reached the write.
99
+ *
100
+ * Exported for tests.
101
+ */
102
+ export declare function writeReceiptWarnings(payload: Record<string, unknown>, receipt: AgentWriteReceipt): string[];
78
103
  /**
79
104
  * The per-Agent mutation fence's "another request holds this right now"
80
105
  * refusals. `updateAgent` takes a short lease on the Agent before it writes, so