@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
@@ -241,6 +241,45 @@ export declare function findTaskByExternalKey({ externalKey, spaceId, goalId, pa
241
241
  task: TaskRecord | null;
242
242
  duplicates: TaskRecord[];
243
243
  }>;
244
+ export type ExternalKeyLookup = {
245
+ externalKey: string;
246
+ spaceId?: unknown;
247
+ goalId?: unknown;
248
+ };
249
+ export type ExternalKeyHit = {
250
+ task: TaskRecord | null;
251
+ duplicates: TaskRecord[];
252
+ /** Set when the lookup itself failed: "could not look" is not "nothing there". */
253
+ error?: Error;
254
+ };
255
+ /** The identity of one lookup: a key means a task only within its scope. */
256
+ export declare function externalKeyLookupId(lookup: ExternalKeyLookup): string;
257
+ /**
258
+ * Which of these tasks already exist? Answers for many keys at once.
259
+ *
260
+ * `create-tree`, `--resume` and `--verify` all start by asking that of every node
261
+ * of a tree, and asked it once per node, one after another: 94 round trips
262
+ * before the first write (OSK-S-440 #3). Two things bring that down without
263
+ * changing a single answer:
264
+ *
265
+ * - Keys that share a prefix (`aap/c1`, `aap/c2`, ... — a tree is written
266
+ * under one) are answered by ONE paged read of the prefix, per scope. The
267
+ * read can return rows that are not this tree's; they are ignored, since a
268
+ * key is matched whole. If the prefix is broad enough that the read hits its
269
+ * page cap, whatever it did not find is looked up directly, because absence
270
+ * from a truncated read proves nothing.
271
+ * - Whatever is left is looked up by exact key, a few at a time rather than
272
+ * one at a time.
273
+ *
274
+ * A server that predates `externalKeyPrefix` ignores it and returns the scope's
275
+ * tasks: still correct (matched whole), merely a wider read, and bounded by the
276
+ * same cap.
277
+ */
278
+ export declare function findTasksByExternalKeys(lookups: readonly ExternalKeyLookup[], opts?: {
279
+ concurrency?: number;
280
+ minKeysForPrefix?: number;
281
+ maxPages?: number;
282
+ }): Promise<Map<string, ExternalKeyHit>>;
244
283
  /**
245
284
  * What the CLI reports under `defaultAgent` in `--json` output.
246
285
  *
package/dist/lib/tasks.js CHANGED
@@ -21,6 +21,8 @@ exports.applyProspectiveTaskExecutionMode = applyProspectiveTaskExecutionMode;
21
21
  exports.validateTaskCreateReferences = validateTaskCreateReferences;
22
22
  exports.resolveTaskCreateScope = resolveTaskCreateScope;
23
23
  exports.findTaskByExternalKey = findTaskByExternalKey;
24
+ exports.externalKeyLookupId = externalKeyLookupId;
25
+ exports.findTasksByExternalKeys = findTasksByExternalKeys;
24
26
  exports.droppedDefaultAssignees = droppedDefaultAssignees;
25
27
  exports.applyTaskAssigneeDefaults = applyTaskAssigneeDefaults;
26
28
  exports.hasExplicitTaskAssignee = hasExplicitTaskAssignee;
@@ -622,6 +624,136 @@ async function findTaskByExternalKey({ externalKey, spaceId, goalId, parentId, }
622
624
  }
623
625
  return { task: items[0] ?? null, duplicates: [] };
624
626
  }
627
+ /** The identity of one lookup: a key means a task only within its scope. */
628
+ function externalKeyLookupId(lookup) {
629
+ const scope = (value) => (typeof value === 'string' ? value : '');
630
+ return `${scope(lookup.spaceId)}|${scope(lookup.goalId)}|${lookup.externalKey}`;
631
+ }
632
+ function longestCommonPrefix(values) {
633
+ if (values.length === 0)
634
+ return '';
635
+ let prefix = values[0];
636
+ for (const value of values) {
637
+ let i = 0;
638
+ while (i < prefix.length && i < value.length && prefix[i] === value[i])
639
+ i += 1;
640
+ prefix = prefix.slice(0, i);
641
+ if (!prefix)
642
+ break;
643
+ }
644
+ return prefix;
645
+ }
646
+ async function mapWithConcurrency(items, limit, fn) {
647
+ const results = new Array(items.length);
648
+ let next = 0;
649
+ const worker = async () => {
650
+ while (next < items.length) {
651
+ const index = next;
652
+ next += 1;
653
+ results[index] = await fn(items[index]);
654
+ }
655
+ };
656
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
657
+ return results;
658
+ }
659
+ /**
660
+ * Which of these tasks already exist? Answers for many keys at once.
661
+ *
662
+ * `create-tree`, `--resume` and `--verify` all start by asking that of every node
663
+ * of a tree, and asked it once per node, one after another: 94 round trips
664
+ * before the first write (OSK-S-440 #3). Two things bring that down without
665
+ * changing a single answer:
666
+ *
667
+ * - Keys that share a prefix (`aap/c1`, `aap/c2`, ... — a tree is written
668
+ * under one) are answered by ONE paged read of the prefix, per scope. The
669
+ * read can return rows that are not this tree's; they are ignored, since a
670
+ * key is matched whole. If the prefix is broad enough that the read hits its
671
+ * page cap, whatever it did not find is looked up directly, because absence
672
+ * from a truncated read proves nothing.
673
+ * - Whatever is left is looked up by exact key, a few at a time rather than
674
+ * one at a time.
675
+ *
676
+ * A server that predates `externalKeyPrefix` ignores it and returns the scope's
677
+ * tasks: still correct (matched whole), merely a wider read, and bounded by the
678
+ * same cap.
679
+ */
680
+ async function findTasksByExternalKeys(lookups, opts = {}) {
681
+ const concurrency = opts.concurrency ?? 6;
682
+ const minKeysForPrefix = opts.minKeysForPrefix ?? 8;
683
+ const maxPages = opts.maxPages ?? 10;
684
+ const hits = new Map();
685
+ const unique = new Map();
686
+ for (const lookup of lookups)
687
+ unique.set(externalKeyLookupId(lookup), lookup);
688
+ const byScope = new Map();
689
+ for (const lookup of unique.values()) {
690
+ const scopeId = externalKeyLookupId({ ...lookup, externalKey: '' });
691
+ byScope.set(scopeId, [...(byScope.get(scopeId) ?? []), lookup]);
692
+ }
693
+ const settle = (lookup, rows) => {
694
+ const matching = rows.filter((row) => row.externalKey === lookup.externalKey);
695
+ hits.set(externalKeyLookupId(lookup), {
696
+ task: matching.length === 1 ? matching[0] : null,
697
+ duplicates: matching.length > 1 ? matching : [],
698
+ });
699
+ };
700
+ for (const group of byScope.values()) {
701
+ let unresolved = group;
702
+ const prefix = longestCommonPrefix(group.map((lookup) => lookup.externalKey));
703
+ if (group.length >= minKeysForPrefix && prefix.length >= 3) {
704
+ try {
705
+ const rows = [];
706
+ let cursor;
707
+ let complete = false;
708
+ for (let page = 0; page < maxPages; page += 1) {
709
+ const response = (await data_provider_1.dataService.listTasks({
710
+ externalKeyPrefix: prefix,
711
+ ...(typeof group[0].spaceId === 'string' && group[0].spaceId
712
+ ? { spaceId: group[0].spaceId }
713
+ : {}),
714
+ ...(typeof group[0].goalId === 'string' && group[0].goalId
715
+ ? { goalId: group[0].goalId }
716
+ : {}),
717
+ limit: 200,
718
+ ...(cursor ? { cursor } : {}),
719
+ }));
720
+ rows.push(...listItems(response));
721
+ if (!response?.has_more || !response.next_cursor) {
722
+ complete = true;
723
+ break;
724
+ }
725
+ cursor = response.next_cursor;
726
+ }
727
+ unresolved = [];
728
+ for (const lookup of group) {
729
+ const found = rows.some((row) => row.externalKey === lookup.externalKey);
730
+ // Absence is only an answer from a read that reached the end.
731
+ if (found || complete)
732
+ settle(lookup, rows);
733
+ else
734
+ unresolved.push(lookup);
735
+ }
736
+ }
737
+ catch {
738
+ unresolved = group; // the prefix read failing is not an answer; look directly
739
+ }
740
+ }
741
+ await mapWithConcurrency(unresolved, concurrency, async (lookup) => {
742
+ try {
743
+ const { task, duplicates } = await findTaskByExternalKey(lookup);
744
+ hits.set(externalKeyLookupId(lookup), { task, duplicates });
745
+ }
746
+ catch (err) {
747
+ hits.set(externalKeyLookupId(lookup), {
748
+ task: null,
749
+ duplicates: [],
750
+ error: err instanceof Error ? err : new Error(String(err)),
751
+ });
752
+ }
753
+ });
754
+ }
755
+ return hits;
756
+ }
625
757
  /**
626
758
  * Which DEFAULTED assignees the create route will strip because they are not
627
759
  * members of the task's space (`ASSIGNEE_NOT_IN_SPACE` degradation). Only a
@@ -1,4 +1,4 @@
1
- import { type EmbeddedActionRef, type TriggerPreflightReport, type TriggerSensorConfig } from '@skrr-ai/data-provider';
1
+ import { type EmbeddedActionRef, type TriggerIntentUnsupportedItem, type TriggerPreflightReport, type TriggerSensorConfig } from '@skrr-ai/data-provider';
2
2
  export declare const TRIGGER_KINDS: readonly ["schedule", "once", "event", "stochastic", "self_scheduled", "on_demand", "form", "chat_message"];
3
3
  export declare const TRIGGER_SCOPE_KINDS: readonly ["agent", "group_chat_subscriber", "space_oracle", "space_subscriber", "key_result_watcher", "task_watcher", "goal_watcher", "entity_reminder"];
4
4
  /**
@@ -160,6 +160,13 @@ export interface TriggerParseIntentResponse {
160
160
  message?: string;
161
161
  needsClarification?: boolean;
162
162
  proposedTrigger?: Record<string, unknown> | null;
163
+ /** Conditions the person asked for that the proposal does NOT apply (OSK-15402). */
164
+ unsupported?: TriggerIntentUnsupportedItem[];
165
+ /** The transcript including this turn; send it back with a reply to answer a question. */
166
+ history?: Array<{
167
+ role: 'user' | 'assistant';
168
+ content: string;
169
+ }>;
163
170
  usage?: Record<string, unknown>;
164
171
  }
165
172
  export interface TriggerEventCatalogEntry {
@@ -225,6 +232,10 @@ export interface SpaceAssistantResponse {
225
232
  export declare const SCOPE_IMPLIED_REF_KEY: Partial<Record<TriggerScopeKind, string>>;
226
233
  export declare function parseKeyValueFlags(values: string[] | undefined, flagName: string, scopeKind?: string): Record<string, string>;
227
234
  export declare function parseJsonObject(raw: string | undefined, flagName: string): Record<string, unknown> | undefined;
235
+ /**
236
+ * A JSON-array flag value: inline JSON, `@path` (a file), or a bare path to an existing file. A
237
+ * path used to reach `JSON.parse` and fail with "Unexpected token '/'" (OSK-15402).
238
+ */
228
239
  export declare function parseJsonArray(raw: string | undefined, flagName: string): unknown[] | undefined;
229
240
  export declare function buildBuiltinActionPayload(input: {
230
241
  builtinAction?: string;
@@ -288,6 +299,29 @@ export declare const FLAG_OWNER_KINDS: Readonly<Record<string, readonly string[]
288
299
  export declare function assertKindFlagsApply(kind: unknown, flags: Record<string, unknown>, options?: {
289
300
  kindIsFixed?: boolean;
290
301
  }): void;
302
+ /**
303
+ * The request body path each `triggers create` flag writes, for reading a server refusal back
304
+ * in the person's own words (`lib/request-field-flags.ts`). The server validates the body and
305
+ * names `execution.workflowSignal`; the person typed `--workflow-event-key` (OSK-15313). A path
306
+ * missing here is simply left as the server wrote it.
307
+ */
308
+ export declare const TRIGGER_CREATE_FIELD_FLAGS: Readonly<Record<string, string>>;
309
+ /**
310
+ * Flags an execution mode cannot work without, refused before the request is sent. The inverse
311
+ * case (`--workflow-outcome` without `--workflow-event-key`) was already refused by oclif's
312
+ * `dependsOn`; the missing key for `--mode workflow_signal` went to the server and came back
313
+ * naming an internal path (OSK-15313).
314
+ */
315
+ export declare const TRIGGER_MODE_REQUIRED_FLAGS: Readonly<Record<string, ReadonlyArray<{
316
+ flag: string;
317
+ field: string;
318
+ why: string;
319
+ }>>>;
320
+ /**
321
+ * Refuse a mode whose required flag is missing, unless the body the person supplied
322
+ * (`--from-json`) already carries the field the flag would write.
323
+ */
324
+ export declare function assertModeRequiredFlags(mode: unknown, flags: Record<string, unknown>, execution: Record<string, unknown> | undefined): void;
291
325
  /**
292
326
  * The sentence for a fire's `scheduledFire` report: what a MANUAL fire skipped that a scheduled
293
327
  * one would not. A manual fire is exempt from the enabled gate, the working/quiet-hours overlay and
@@ -302,6 +336,13 @@ export declare function describeScheduledFire(result: Record<string, unknown>):
302
336
  * about `in_progress` once). Only these are success for `triggers fire --strict`.
303
337
  */
304
338
  export declare function fireWasAccepted(result: Record<string, unknown>): boolean;
339
+ /**
340
+ * Why `triggers fire --strict` failed. A strict failure used to print exactly what the exit-0 run
341
+ * printed and then exit 1, so nothing said what the flag had judged (OSK-16474). It names the
342
+ * outcome it refused, the reason, the server's sentence when there is one, and what --strict
343
+ * accepts.
344
+ */
345
+ export declare function describeStrictFireRefusal(result: Record<string, unknown>): string;
305
346
  /**
306
347
  * The lines a fire or replay owes beyond `outcome=… reason=…` (OSK-15494): the sentence the
307
348
  * server wrote ("Replayed fire … resolved a step that was not waiting when that fire ran:
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.triggerApi = exports.AgentTriggerScopeError = exports.FLAG_OWNER_KINDS = exports.SCOPE_IMPLIED_REF_KEY = exports.TRIGGER_EVENT_CATALOG = exports.TRIGGER_EVENT_ENTITY_TYPES = exports.TRIGGER_CHAT_AUTH_MODES = exports.TRIGGER_CHAT_RESPONSE_MODES = exports.TRIGGER_CHAT_MODES = exports.TRIGGER_FORM_RESPONSE_MODES = exports.TRIGGER_CONCURRENCY_POLICIES = exports.TRIGGER_EVENT_FILTER_MODES = exports.TRIGGER_EXECUTOR_MODES = exports.TRIGGER_SCOPE_KINDS = exports.TRIGGER_KINDS = void 0;
3
+ exports.triggerApi = exports.AgentTriggerScopeError = exports.TRIGGER_MODE_REQUIRED_FLAGS = exports.TRIGGER_CREATE_FIELD_FLAGS = exports.FLAG_OWNER_KINDS = exports.SCOPE_IMPLIED_REF_KEY = exports.TRIGGER_EVENT_CATALOG = exports.TRIGGER_EVENT_ENTITY_TYPES = exports.TRIGGER_CHAT_AUTH_MODES = exports.TRIGGER_CHAT_RESPONSE_MODES = exports.TRIGGER_CHAT_MODES = exports.TRIGGER_FORM_RESPONSE_MODES = exports.TRIGGER_CONCURRENCY_POLICIES = exports.TRIGGER_EVENT_FILTER_MODES = exports.TRIGGER_EXECUTOR_MODES = exports.TRIGGER_SCOPE_KINDS = exports.TRIGGER_KINDS = void 0;
4
4
  exports.validateEventReference = validateEventReference;
5
5
  exports.validateEventFilter = validateEventFilter;
6
6
  exports.serverEventCatalogIncludes = serverEventCatalogIncludes;
@@ -24,8 +24,10 @@ exports.setDefined = setDefined;
24
24
  exports.mergeObjectField = mergeObjectField;
25
25
  exports.mergeTriggerKindConfig = mergeTriggerKindConfig;
26
26
  exports.assertKindFlagsApply = assertKindFlagsApply;
27
+ exports.assertModeRequiredFlags = assertModeRequiredFlags;
27
28
  exports.describeScheduledFire = describeScheduledFire;
28
29
  exports.fireWasAccepted = fireWasAccepted;
30
+ exports.describeStrictFireRefusal = describeStrictFireRefusal;
29
31
  exports.describeFireDetail = describeFireDetail;
30
32
  exports.formatFireInZone = formatFireInZone;
31
33
  exports.nextFireLines = nextFireLines;
@@ -49,6 +51,7 @@ exports.renderTriggerSummary = renderTriggerSummary;
49
51
  exports.renderLegacyRuns = renderLegacyRuns;
50
52
  exports.renderFireAudit = renderFireAudit;
51
53
  const data_provider_1 = require("@skrr-ai/data-provider");
54
+ const node_fs_1 = require("node:fs");
52
55
  const format_1 = require("./format");
53
56
  exports.TRIGGER_KINDS = [
54
57
  'schedule',
@@ -389,12 +392,28 @@ function parseJsonObject(raw, flagName) {
389
392
  }
390
393
  return parsed;
391
394
  }
395
+ /**
396
+ * A JSON-array flag value: inline JSON, `@path` (a file), or a bare path to an existing file. A
397
+ * path used to reach `JSON.parse` and fail with "Unexpected token '/'" (OSK-15402).
398
+ */
392
399
  function parseJsonArray(raw, flagName) {
393
400
  if (raw === undefined)
394
401
  return undefined;
402
+ let text = raw;
403
+ const trimmed = raw.trim();
404
+ if (trimmed.startsWith('@') || (!/^[[{]/.test(trimmed) && (0, node_fs_1.existsSync)(trimmed))) {
405
+ const path = trimmed.startsWith('@') ? trimmed.slice(1) : trimmed;
406
+ try {
407
+ text = (0, node_fs_1.readFileSync)(path, 'utf8');
408
+ }
409
+ catch (err) {
410
+ const msg = err instanceof Error ? err.message : String(err);
411
+ throw new Error(`Cannot read ${flagName} file ${path}: ${msg}`);
412
+ }
413
+ }
395
414
  let parsed;
396
415
  try {
397
- parsed = JSON.parse(raw);
416
+ parsed = JSON.parse(text);
398
417
  }
399
418
  catch (err) {
400
419
  const msg = err instanceof Error ? err.message : String(err);
@@ -442,6 +461,13 @@ function buildScheduleConfigFromFlags(flags) {
442
461
  endAt: flags['end-at'],
443
462
  staleCatchupMs: flags['stale-catchup-ms'],
444
463
  });
464
+ // The range is the server's (`TRIGGER_STALE_CATCHUP_MS_RANGE`), checked here too so the refusal
465
+ // names the flag the person typed and happens before anything is sent (OSK-16474).
466
+ if (flags['stale-catchup-ms'] !== undefined) {
467
+ const refusal = (0, data_provider_1.describeStaleCatchupMsRefusal)(flags['stale-catchup-ms'], '--stale-catchup-ms');
468
+ if (refusal)
469
+ throw new Error(refusal);
470
+ }
445
471
  return schedule;
446
472
  }
447
473
  function assertScheduleTimingShape(schedule, options = {}) {
@@ -746,6 +772,59 @@ function assertKindFlagsApply(kind, flags, options = {}) {
746
772
  }
747
773
  throw new Error(`${named.join(', ')} ${verb} not apply to ${subject}.${fix}`);
748
774
  }
775
+ /**
776
+ * The request body path each `triggers create` flag writes, for reading a server refusal back
777
+ * in the person's own words (`lib/request-field-flags.ts`). The server validates the body and
778
+ * names `execution.workflowSignal`; the person typed `--workflow-event-key` (OSK-15313). A path
779
+ * missing here is simply left as the server wrote it.
780
+ */
781
+ exports.TRIGGER_CREATE_FIELD_FLAGS = Object.freeze({
782
+ 'execution.mode': '--mode',
783
+ 'execution.promptTemplate': '--prompt',
784
+ 'execution.conversationId': '--conversation',
785
+ 'execution.builtinAction': '--builtin-action',
786
+ 'execution.effectPolicy': '--effect-contract',
787
+ 'execution.effectPolicy.contract': '--effect-contract',
788
+ 'execution.effectPolicy.writeMode': '--effect-write-mode',
789
+ 'execution.effectPolicy.options': '--effect-options',
790
+ // The whole block is written by --workflow-event-key: without it the block is not sent.
791
+ 'execution.workflowSignal': '--workflow-event-key',
792
+ 'execution.workflowSignal.eventKey': '--workflow-event-key',
793
+ 'execution.workflowSignal.outcome': '--workflow-outcome',
794
+ 'execution.dispatch': '--dispatch',
795
+ 'execution.references': '--reference',
796
+ 'execution.workingHours': '--working-hours',
797
+ 'execution.quietHours': '--quiet-hours',
798
+ });
799
+ /**
800
+ * Flags an execution mode cannot work without, refused before the request is sent. The inverse
801
+ * case (`--workflow-outcome` without `--workflow-event-key`) was already refused by oclif's
802
+ * `dependsOn`; the missing key for `--mode workflow_signal` went to the server and came back
803
+ * naming an internal path (OSK-15313).
804
+ */
805
+ exports.TRIGGER_MODE_REQUIRED_FLAGS = Object.freeze({
806
+ workflow_signal: [
807
+ {
808
+ flag: 'workflow-event-key',
809
+ field: 'workflowSignal',
810
+ why: 'the exact event key the parked workflow step waits on',
811
+ },
812
+ ],
813
+ });
814
+ /**
815
+ * Refuse a mode whose required flag is missing, unless the body the person supplied
816
+ * (`--from-json`) already carries the field the flag would write.
817
+ */
818
+ function assertModeRequiredFlags(mode, flags, execution) {
819
+ if (typeof mode !== 'string')
820
+ return;
821
+ const missing = (exports.TRIGGER_MODE_REQUIRED_FLAGS[mode] ?? []).filter((requirement) => !flagWasPassed(flags[requirement.flag]) && execution?.[requirement.field] === undefined);
822
+ if (missing.length === 0)
823
+ return;
824
+ throw new Error(missing
825
+ .map((requirement) => `--mode ${mode} requires --${requirement.flag}: ${requirement.why}.`)
826
+ .join(' '));
827
+ }
749
828
  /**
750
829
  * The sentence for a fire's `scheduledFire` report: what a MANUAL fire skipped that a scheduled
751
830
  * one would not. A manual fire is exempt from the enabled gate, the working/quiet-hours overlay and
@@ -786,6 +865,23 @@ function fireWasAccepted(result) {
786
865
  typeof result.originalOutcome === 'string' &&
787
866
  accepted.includes(result.originalOutcome));
788
867
  }
868
+ /**
869
+ * Why `triggers fire --strict` failed. A strict failure used to print exactly what the exit-0 run
870
+ * printed and then exit 1, so nothing said what the flag had judged (OSK-16474). It names the
871
+ * outcome it refused, the reason, the server's sentence when there is one, and what --strict
872
+ * accepts.
873
+ */
874
+ function describeStrictFireRefusal(result) {
875
+ const text = (value) => (typeof value === 'string' && value.trim() ? value.trim() : '');
876
+ const outcome = text(result.outcome) || 'unknown';
877
+ const original = outcome === 'duplicate' ? text(result.originalOutcome) : '';
878
+ const reason = text(result.reason);
879
+ const sentence = text(result.detail) || text(result.errorMessage);
880
+ return (`--strict: the fire was not accepted (outcome ${outcome}` +
881
+ `${original ? `, a repeat of ${original}` : ''}${reason ? `, reason ${reason}` : ''})` +
882
+ `${sentence ? `: ${sentence}` : '.'} ` +
883
+ `--strict accepts only ${data_provider_1.ACCEPTED_AGENT_TRIGGER_FIRE_OUTCOMES.join(', ')}, or a repeat of one.`);
884
+ }
789
885
  /**
790
886
  * The lines a fire or replay owes beyond `outcome=… reason=…` (OSK-15494): the sentence the
791
887
  * server wrote ("Replayed fire … resolved a step that was not waiting when that fire ran:
@@ -0,0 +1,14 @@
1
+ import type { WorkflowIssue } from '@skrr-ai/data-provider';
2
+ /**
3
+ * Lines that say what in a workflow draft is wrong, and where.
4
+ *
5
+ * A draft may be STORED invalid — the canvas saves while you are still building
6
+ * — so `create` and `update` succeed on a graph `publish` will refuse. They used
7
+ * to print a bare success, and the author learned at publish (OSK-15328). The
8
+ * server now returns the shared linter's `issues` on every write and on
9
+ * validate; this renders them the same way on all three, each naming its node
10
+ * and field so it can be acted on without reading the graph again.
11
+ */
12
+ export declare function formatWorkflowIssues(issues: WorkflowIssue[] | undefined): string[];
13
+ /** The heading plus issue lines for a draft write, or nothing when the draft is clean. */
14
+ export declare function describeDraftIssues(issues: WorkflowIssue[] | undefined): string[];
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatWorkflowIssues = formatWorkflowIssues;
4
+ exports.describeDraftIssues = describeDraftIssues;
5
+ /**
6
+ * Lines that say what in a workflow draft is wrong, and where.
7
+ *
8
+ * A draft may be STORED invalid — the canvas saves while you are still building
9
+ * — so `create` and `update` succeed on a graph `publish` will refuse. They used
10
+ * to print a bare success, and the author learned at publish (OSK-15328). The
11
+ * server now returns the shared linter's `issues` on every write and on
12
+ * validate; this renders them the same way on all three, each naming its node
13
+ * and field so it can be acted on without reading the graph again.
14
+ */
15
+ function formatWorkflowIssues(issues) {
16
+ return (issues ?? []).map((issue) => {
17
+ const where = [issue.nodeKey, issue.path].filter(Boolean).join(' ');
18
+ const label = issue.severity === 'error' ? 'error' : 'warning';
19
+ return ` ${label}${where ? ` [${where}]` : ''}: ${issue.message}`;
20
+ });
21
+ }
22
+ /** The heading plus issue lines for a draft write, or nothing when the draft is clean. */
23
+ function describeDraftIssues(issues) {
24
+ const list = issues ?? [];
25
+ if (!list.length)
26
+ return [];
27
+ const errors = list.filter((issue) => issue.severity === 'error').length;
28
+ const heading = errors
29
+ ? `Saved, but this draft cannot be published yet (${errors} error${errors === 1 ? '' : 's'}):`
30
+ : `Saved. ${list.length} warning${list.length === 1 ? '' : 's'} (publishing is allowed):`;
31
+ return [heading, ...formatWorkflowIssues(list)];
32
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * What the plain-text workflow run view says about each node beyond its status.
3
+ *
4
+ * The table showed key/type/status/task, so three facts the server already knew were visible
5
+ * only under `--json` (OSK-15325): a loop that `succeeded` while one of its items failed, what a
6
+ * `waiting` node was waiting FOR (an event key, an approval, a child run), and how to resolve a
7
+ * wait a signal can resolve. Rows were also in node-creation order rather than graph order.
8
+ * Everything here reads server-derived fields (`depth`, `loopSummary`, `signalable`, `waitKind`)
9
+ * and recomputes none of them.
10
+ */
11
+ export interface RunViewNode {
12
+ nodeKey?: unknown;
13
+ nodeType?: unknown;
14
+ status?: unknown;
15
+ depth?: unknown;
16
+ lane?: unknown;
17
+ attentionReason?: unknown;
18
+ error?: {
19
+ message?: unknown;
20
+ } | null;
21
+ waitKind?: unknown;
22
+ signalable?: unknown;
23
+ waitingFor?: unknown;
24
+ stoppedBecause?: {
25
+ code?: unknown;
26
+ message?: unknown;
27
+ } | null;
28
+ loopSummary?: {
29
+ total?: unknown;
30
+ completed?: unknown;
31
+ succeeded?: unknown;
32
+ failed?: unknown;
33
+ partial?: unknown;
34
+ settled?: unknown;
35
+ } | null;
36
+ }
37
+ /** One printable line: control characters (an ESC sequence included) become spaces. */
38
+ export declare function printable(value: string): string;
39
+ /**
40
+ * Nodes in graph order: the server's topological `depth` first, then the order the server
41
+ * returned them in (a stable sort), so siblings at one depth keep their relative order.
42
+ */
43
+ export declare function orderNodesByGraph<T extends RunViewNode>(nodes: readonly T[]): T[];
44
+ /** "2 of 3 items succeeded, 1 failed" for a loop, or '' when the server sent no summary. */
45
+ export declare function describeLoopProgress(node: RunViewNode): string;
46
+ /** The DETAIL cell: what needs eyes, what a wait waits for, and how far a loop got. */
47
+ export declare function describeNodeDetail(node: RunViewNode, attention?: string): string;
48
+ /**
49
+ * How to resolve each wait a signal can resolve. Only the server's `signalable` decides: a loop
50
+ * or child-run wait is settled by its work, and offering a command for it would invite a person
51
+ * to skip that work.
52
+ */
53
+ export declare function describeSignalHints(spaceId: string, runId: string, nodes: readonly RunViewNode[], bin?: string): string[];
54
+ /**
55
+ * Lines a run's headline status cannot carry: a node that succeeded although some of its items
56
+ * failed. A loop under `onItemFailure: continue` with a permissive join succeeds with a failed
57
+ * item, so "run succeeded" alone hid the failure.
58
+ */
59
+ export declare function describePartialOutcomes(nodes: readonly RunViewNode[]): string[];
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.printable = printable;
4
+ exports.orderNodesByGraph = orderNodesByGraph;
5
+ exports.describeLoopProgress = describeLoopProgress;
6
+ exports.describeNodeDetail = describeNodeDetail;
7
+ exports.describeSignalHints = describeSignalHints;
8
+ exports.describePartialOutcomes = describePartialOutcomes;
9
+ const data_provider_1 = require("@skrr-ai/data-provider");
10
+ const format_1 = require("./format");
11
+ /** One printable line: control characters (an ESC sequence included) become spaces. */
12
+ function printable(value) {
13
+ // eslint-disable-next-line no-control-regex
14
+ return (0, format_1.collapseWhitespace)(value.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' '));
15
+ }
16
+ /**
17
+ * Nodes in graph order: the server's topological `depth` first, then the order the server
18
+ * returned them in (a stable sort), so siblings at one depth keep their relative order.
19
+ */
20
+ function orderNodesByGraph(nodes) {
21
+ const depthOf = (node) => typeof node.depth === 'number' && Number.isFinite(node.depth)
22
+ ? node.depth
23
+ : Number.MAX_SAFE_INTEGER;
24
+ return nodes
25
+ .map((node, index) => ({ node, index }))
26
+ .sort((a, b) => depthOf(a.node) - depthOf(b.node) || a.index - b.index)
27
+ .map((entry) => entry.node);
28
+ }
29
+ /** "2 of 3 items succeeded, 1 failed" for a loop, or '' when the server sent no summary. */
30
+ function describeLoopProgress(node) {
31
+ const summary = node.loopSummary;
32
+ if (node.nodeType !== 'loop' || !summary)
33
+ return '';
34
+ const total = Number(summary.total);
35
+ const completed = Number(summary.completed);
36
+ const succeeded = Number(summary.succeeded);
37
+ const failed = Number(summary.failed);
38
+ if (![total, completed, succeeded, failed].every(Number.isFinite))
39
+ return '';
40
+ const parts = [`${succeeded} of ${total} item${total === 1 ? '' : 's'} succeeded`];
41
+ if (failed > 0)
42
+ parts.push(`${failed} failed`);
43
+ if (summary.settled === true && completed < total)
44
+ parts.push(`${total - completed} not run`);
45
+ return parts.join(', ');
46
+ }
47
+ /** The DETAIL cell: what needs eyes, what a wait waits for, and how far a loop got. */
48
+ function describeNodeDetail(node, attention = '') {
49
+ const parts = [];
50
+ const wait = (0, data_provider_1.describeWorkflowNodeWait)(node);
51
+ // "approval; waiting for an approval" says one thing twice: the wait phrase carries it.
52
+ if (attention && attention !== wait?.kind)
53
+ parts.push(attention);
54
+ if (wait)
55
+ parts.push(wait.text);
56
+ // A cancelled node says why it stopped (the run's deadline, a person's stop), not the
57
+ // problem it had been parked on before (OSK-16473).
58
+ const stopped = node.status === 'cancelled' ? node.stoppedBecause?.message : undefined;
59
+ if (typeof stopped === 'string' && stopped)
60
+ parts.push(`stopped: ${stopped}`);
61
+ const loop = describeLoopProgress(node);
62
+ if (loop)
63
+ parts.push(loop);
64
+ return printable(parts.join('; '));
65
+ }
66
+ /**
67
+ * How to resolve each wait a signal can resolve. Only the server's `signalable` decides: a loop
68
+ * or child-run wait is settled by its work, and offering a command for it would invite a person
69
+ * to skip that work.
70
+ */
71
+ function describeSignalHints(spaceId, runId, nodes, bin = 'skrr') {
72
+ const lines = [];
73
+ for (const node of nodes) {
74
+ if (node.status !== 'waiting' || node.signalable !== true)
75
+ continue;
76
+ const key = printable(String(node.nodeKey ?? ''));
77
+ if (!key)
78
+ continue;
79
+ const wait = (0, data_provider_1.describeWorkflowNodeWait)(node);
80
+ const what = wait?.kind === 'approval' ? 'approve or reject it' : 'resolve it by hand';
81
+ lines.push(`To ${what}: ${bin} spaces workflows signal ${spaceId} ${runId} ${key}` +
82
+ (wait?.kind === 'approval'
83
+ ? ' [--outcome failure]'
84
+ : ' [--outcome failure] [--payload <json>]'));
85
+ }
86
+ return lines;
87
+ }
88
+ /**
89
+ * Lines a run's headline status cannot carry: a node that succeeded although some of its items
90
+ * failed. A loop under `onItemFailure: continue` with a permissive join succeeds with a failed
91
+ * item, so "run succeeded" alone hid the failure.
92
+ */
93
+ function describePartialOutcomes(nodes) {
94
+ const lines = [];
95
+ for (const node of nodes) {
96
+ const summary = node.loopSummary;
97
+ // A failed loop already says so in its STATUS column; only a success can hide one.
98
+ if (node.nodeType !== 'loop' || node.status !== 'succeeded')
99
+ continue;
100
+ if (!summary || summary.partial !== true)
101
+ continue;
102
+ const failed = Number(summary.failed);
103
+ const total = Number(summary.total);
104
+ if (!Number.isFinite(failed) || failed <= 0)
105
+ continue;
106
+ lines.push(`Note: ${printable(String(node.nodeKey ?? 'loop'))} succeeded, but ` +
107
+ `${failed} of ${Number.isFinite(total) ? total : '?'} item${total === 1 ? '' : 's'} failed.`);
108
+ }
109
+ return lines;
110
+ }