@skrr-ai/cli 0.1.107 → 0.1.109

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 (175) hide show
  1. package/README.md +10 -0
  2. package/dist/base-command.d.ts +59 -0
  3. package/dist/base-command.js +118 -7
  4. package/dist/commands/agents/update.d.ts +14 -0
  5. package/dist/commands/agents/update.js +40 -1
  6. package/dist/commands/commitments/accounting.js +7 -2
  7. package/dist/commands/commitments/cost.d.ts +11 -0
  8. package/dist/commands/commitments/cost.js +26 -3
  9. package/dist/commands/commitments/create.js +2 -1
  10. package/dist/commands/commitments/operating-review.d.ts +15 -0
  11. package/dist/commands/commitments/operating-review.js +103 -50
  12. package/dist/commands/commitments/update.js +12 -5
  13. package/dist/commands/doctor.js +9 -0
  14. package/dist/commands/goals/list.js +6 -1
  15. package/dist/commands/grants/rotate.js +4 -0
  16. package/dist/commands/login.js +42 -14
  17. package/dist/commands/machines/list.js +28 -6
  18. package/dist/commands/projects/show.js +1 -1
  19. package/dist/commands/revoke-daemon.js +2 -1
  20. package/dist/commands/spaces/workflows/archive.d.ts +1 -0
  21. package/dist/commands/spaces/workflows/archive.js +1 -0
  22. package/dist/commands/spaces/workflows/artifact.d.ts +1 -0
  23. package/dist/commands/spaces/workflows/artifact.js +1 -0
  24. package/dist/commands/spaces/workflows/cancel-node.d.ts +1 -0
  25. package/dist/commands/spaces/workflows/cancel-node.js +1 -0
  26. package/dist/commands/spaces/workflows/cancel.d.ts +1 -0
  27. package/dist/commands/spaces/workflows/cancel.js +6 -2
  28. package/dist/commands/spaces/workflows/create.d.ts +1 -0
  29. package/dist/commands/spaces/workflows/create.js +7 -1
  30. package/dist/commands/spaces/workflows/events.d.ts +1 -0
  31. package/dist/commands/spaces/workflows/events.js +1 -0
  32. package/dist/commands/spaces/workflows/fixtures/create.d.ts +1 -0
  33. package/dist/commands/spaces/workflows/fixtures/create.js +1 -0
  34. package/dist/commands/spaces/workflows/fixtures/list.d.ts +1 -0
  35. package/dist/commands/spaces/workflows/fixtures/list.js +1 -0
  36. package/dist/commands/spaces/workflows/lifecycle-diagnostics.d.ts +1 -0
  37. package/dist/commands/spaces/workflows/lifecycle-diagnostics.js +1 -0
  38. package/dist/commands/spaces/workflows/list.d.ts +1 -0
  39. package/dist/commands/spaces/workflows/list.js +1 -0
  40. package/dist/commands/spaces/workflows/node-types.d.ts +2 -0
  41. package/dist/commands/spaces/workflows/node-types.js +24 -6
  42. package/dist/commands/spaces/workflows/pause.d.ts +1 -0
  43. package/dist/commands/spaces/workflows/pause.js +1 -0
  44. package/dist/commands/spaces/workflows/plan/show.d.ts +1 -0
  45. package/dist/commands/spaces/workflows/plan/show.js +1 -0
  46. package/dist/commands/spaces/workflows/plan/update.d.ts +1 -0
  47. package/dist/commands/spaces/workflows/plan/update.js +1 -0
  48. package/dist/commands/spaces/workflows/proposals/apply.d.ts +1 -0
  49. package/dist/commands/spaces/workflows/proposals/apply.js +1 -0
  50. package/dist/commands/spaces/workflows/proposals/confirm.d.ts +1 -0
  51. package/dist/commands/spaces/workflows/proposals/confirm.js +1 -0
  52. package/dist/commands/spaces/workflows/proposals/create.d.ts +1 -0
  53. package/dist/commands/spaces/workflows/proposals/create.js +1 -0
  54. package/dist/commands/spaces/workflows/proposals/dismiss.d.ts +1 -0
  55. package/dist/commands/spaces/workflows/proposals/dismiss.js +1 -0
  56. package/dist/commands/spaces/workflows/proposals/list.d.ts +1 -0
  57. package/dist/commands/spaces/workflows/proposals/list.js +1 -0
  58. package/dist/commands/spaces/workflows/publish.d.ts +1 -0
  59. package/dist/commands/spaces/workflows/publish.js +9 -0
  60. package/dist/commands/spaces/workflows/reconcile.d.ts +1 -0
  61. package/dist/commands/spaces/workflows/reconcile.js +1 -0
  62. package/dist/commands/spaces/workflows/resume.d.ts +1 -0
  63. package/dist/commands/spaces/workflows/resume.js +1 -0
  64. package/dist/commands/spaces/workflows/retry.d.ts +1 -0
  65. package/dist/commands/spaces/workflows/retry.js +1 -0
  66. package/dist/commands/spaces/workflows/run.d.ts +1 -0
  67. package/dist/commands/spaces/workflows/run.js +14 -7
  68. package/dist/commands/spaces/workflows/runs.d.ts +1 -0
  69. package/dist/commands/spaces/workflows/runs.js +1 -0
  70. package/dist/commands/spaces/workflows/show.d.ts +25 -0
  71. package/dist/commands/spaces/workflows/show.js +42 -4
  72. package/dist/commands/spaces/workflows/signal.d.ts +12 -0
  73. package/dist/commands/spaces/workflows/signal.js +40 -2
  74. package/dist/commands/spaces/workflows/start.d.ts +1 -0
  75. package/dist/commands/spaces/workflows/start.js +1 -0
  76. package/dist/commands/spaces/workflows/task-execution/create.d.ts +1 -0
  77. package/dist/commands/spaces/workflows/task-execution/create.js +1 -0
  78. package/dist/commands/spaces/workflows/test.d.ts +1 -0
  79. package/dist/commands/spaces/workflows/test.js +1 -0
  80. package/dist/commands/spaces/workflows/triggers/create.d.ts +1 -0
  81. package/dist/commands/spaces/workflows/triggers/create.js +1 -0
  82. package/dist/commands/spaces/workflows/triggers/fire.d.ts +1 -0
  83. package/dist/commands/spaces/workflows/triggers/fire.js +1 -0
  84. package/dist/commands/spaces/workflows/triggers/list.d.ts +1 -0
  85. package/dist/commands/spaces/workflows/triggers/list.js +1 -0
  86. package/dist/commands/spaces/workflows/triggers/update.d.ts +1 -0
  87. package/dist/commands/spaces/workflows/triggers/update.js +1 -0
  88. package/dist/commands/spaces/workflows/update.d.ts +1 -0
  89. package/dist/commands/spaces/workflows/update.js +4 -0
  90. package/dist/commands/spaces/workflows/validate.d.ts +1 -0
  91. package/dist/commands/spaces/workflows/validate.js +15 -0
  92. package/dist/commands/tasks/activity.js +6 -2
  93. package/dist/commands/tasks/bulk-update.d.ts +1 -0
  94. package/dist/commands/tasks/bulk-update.js +7 -0
  95. package/dist/commands/tasks/complete.js +2 -1
  96. package/dist/commands/tasks/create-tree.d.ts +69 -0
  97. package/dist/commands/tasks/create-tree.js +413 -47
  98. package/dist/commands/tasks/list.d.ts +2 -0
  99. package/dist/commands/tasks/list.js +19 -1
  100. package/dist/commands/tasks/rearm-recurrence.d.ts +22 -0
  101. package/dist/commands/tasks/rearm-recurrence.js +68 -0
  102. package/dist/commands/tasks/show.d.ts +1 -0
  103. package/dist/commands/tasks/show.js +17 -3
  104. package/dist/commands/tasks/timeline.js +4 -0
  105. package/dist/commands/tasks/update.d.ts +1 -0
  106. package/dist/commands/tasks/update.js +6 -1
  107. package/dist/commands/triggers/create.js +8 -2
  108. package/dist/commands/triggers/parse-intent.d.ts +7 -0
  109. package/dist/commands/triggers/parse-intent.js +31 -9
  110. package/dist/commands/workspaces/grants.js +23 -3
  111. package/dist/commands/workspaces/members.d.ts +20 -0
  112. package/dist/commands/workspaces/members.js +64 -0
  113. package/dist/commands/workspaces/stewardship.js +14 -2
  114. package/dist/help.js +12 -0
  115. package/dist/hooks/command-not-found.js +27 -1
  116. package/dist/lib/command-miss.d.ts +52 -2
  117. package/dist/lib/command-miss.js +187 -18
  118. package/dist/lib/commitment-loop-status.d.ts +8 -0
  119. package/dist/lib/commitment-loop-status.js +45 -0
  120. package/dist/lib/commitment-product.js +13 -1
  121. package/dist/lib/commitments.d.ts +16 -2
  122. package/dist/lib/commitments.js +48 -8
  123. package/dist/lib/daemon-command-hint.d.ts +15 -0
  124. package/dist/lib/daemon-command-hint.js +21 -0
  125. package/dist/lib/daemon-setup.js +10 -1
  126. package/dist/lib/daemonBrokerRefusal.js +3 -2
  127. package/dist/lib/daemonHandoff.js +2 -1
  128. package/dist/lib/deployed-build.d.ts +43 -0
  129. package/dist/lib/deployed-build.js +66 -0
  130. package/dist/lib/grants.d.ts +27 -1
  131. package/dist/lib/grants.js +39 -21
  132. package/dist/lib/json-mode.d.ts +11 -0
  133. package/dist/lib/json-mode.js +19 -0
  134. package/dist/lib/json-output.d.ts +25 -0
  135. package/dist/lib/json-output.js +30 -0
  136. package/dist/lib/list-output.d.ts +9 -0
  137. package/dist/lib/list-output.js +20 -1
  138. package/dist/lib/local-daemon-readiness.d.ts +90 -0
  139. package/dist/lib/local-daemon-readiness.js +143 -0
  140. package/dist/lib/machines.d.ts +117 -0
  141. package/dist/lib/machines.js +139 -0
  142. package/dist/lib/managed-instruction-local.d.ts +2 -0
  143. package/dist/lib/managed-instruction-local.js +2 -0
  144. package/dist/lib/profile-machine-identity-guard.d.ts +18 -0
  145. package/dist/lib/profile-machine-identity-guard.js +29 -0
  146. package/dist/lib/request-field-flags.d.ts +20 -0
  147. package/dist/lib/request-field-flags.js +34 -0
  148. package/dist/lib/stewardship-output.d.ts +1 -1
  149. package/dist/lib/stewardship-output.js +26 -30
  150. package/dist/lib/task-batch.d.ts +61 -4
  151. package/dist/lib/task-batch.js +316 -80
  152. package/dist/lib/task-economics.d.ts +5 -0
  153. package/dist/lib/task-economics.js +15 -0
  154. package/dist/lib/task-recurrence-outcome.d.ts +6 -6
  155. package/dist/lib/task-recurrence-outcome.js +27 -3
  156. package/dist/lib/tasks.d.ts +39 -0
  157. package/dist/lib/tasks.js +132 -0
  158. package/dist/lib/triggers.d.ts +31 -1
  159. package/dist/lib/triggers.js +55 -1
  160. package/dist/lib/workflow-issues.d.ts +14 -0
  161. package/dist/lib/workflow-issues.js +32 -0
  162. package/dist/lib/workflow-run-view.d.ts +55 -0
  163. package/dist/lib/workflow-run-view.js +105 -0
  164. package/dist/lib/workspaces.d.ts +31 -4
  165. package/dist/lib/workspaces.js +43 -0
  166. package/dist/lib/write-target.d.ts +29 -0
  167. package/dist/lib/write-target.js +72 -0
  168. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/workspaceIsolationProbeWire.d.ts +51 -0
  169. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/workspaceIsolationProbeWire.js +50 -0
  170. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/workspaceIsolationProbeWire.d.ts +51 -0
  171. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/workspaceIsolationProbeWire.js +46 -0
  172. package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
  173. package/dist/node_modules/@skrr-ai/data-provider/index.js +7896 -7524
  174. package/oclif.manifest.json +376 -52
  175. package/package.json +19 -1
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.batchRetryPolicy = void 0;
3
4
  exports.describeApiFailure = describeApiFailure;
4
5
  exports.isBatchRouteMissing = isBatchRouteMissing;
5
6
  exports.chunkItems = chunkItems;
@@ -81,31 +82,51 @@ async function reconcileSubmittedNodes(nodes, mapping, error, deadlineAt = Date.
81
82
  let timer;
82
83
  try {
83
84
  const read = (0, tasks_1.findTaskByExternalKey)({
84
- externalKey, spaceId: node.payload.spaceId, goalId: node.payload.goalId,
85
+ externalKey,
86
+ spaceId: node.payload.spaceId,
87
+ goalId: node.payload.goalId,
85
88
  });
86
- const existing = await Promise.race([read, new Promise((resolve) => {
89
+ const existing = await Promise.race([
90
+ read,
91
+ new Promise((resolve) => {
87
92
  timer = setTimeout(() => resolve(null), Math.max(1, deadlineAt - Date.now()));
88
- })]);
93
+ }),
94
+ ]);
89
95
  if (existing?.task?.externalKey === externalKey && (0, tasks_1.taskId)(existing.task)) {
90
96
  const id = (0, tasks_1.taskId)(existing.task);
91
97
  mapping[node.key] = id;
92
- rows.push({ key: node.key, id, status: 'exists', externalKey,
93
- ...(typeof existing.task.identifier === 'string' ? { identifier: existing.task.identifier } : {}),
98
+ rows.push({
99
+ key: node.key,
100
+ id,
101
+ status: 'exists',
102
+ externalKey,
103
+ ...(typeof existing.task.identifier === 'string'
104
+ ? { identifier: existing.task.identifier }
105
+ : {}),
94
106
  });
95
107
  return;
96
108
  }
97
109
  }
98
- catch { /* A failed read does not prove the write failed. */ }
110
+ catch {
111
+ /* A failed read does not prove the write failed. */
112
+ }
99
113
  finally {
100
114
  clearTimeout(timer);
101
115
  }
102
116
  }
103
- failures.push({ key: node.key, skipped: false, outcome: 'unknown',
117
+ failures.push({
118
+ key: node.key,
119
+ skipped: false,
120
+ outcome: 'unknown',
104
121
  ...(externalKey ? { externalKey } : {}),
105
- error: { ...error, code: 'BATCH_OUTCOME_UNKNOWN', retryable: Boolean(externalKey),
122
+ error: {
123
+ ...error,
124
+ code: 'BATCH_OUTCOME_UNKNOWN',
125
+ retryable: Boolean(externalKey),
106
126
  message: `The result for "${node.key}" is unknown. ` +
107
- (externalKey ? `No committed task was confirmed by external key "${externalKey}"; the original write may still finish. Re-run the same keyed tree to reconcile and resume.` :
108
- 'This node has no stable external key. Inspect the Space before another write; do not re-run --fresh blindly.'),
127
+ (externalKey
128
+ ? `No committed task was confirmed by external key "${externalKey}"; the original write may still finish. Re-run the same keyed tree to reconcile and resume.`
129
+ : 'This node has no stable external key. Inspect the Space before another write; do not re-run --fresh blindly.'),
109
130
  },
110
131
  });
111
132
  }));
@@ -116,47 +137,181 @@ async function reconcileSubmittedNodes(nodes, mapping, error, deadlineAt = Date.
116
137
  return { rows, failures };
117
138
  }
118
139
  /**
119
- * Create `nodes` (parents before children) in chunks. `mapping` holds key -> id
120
- * for nodes that already exist and is extended in place. Stops after the first
121
- * chunk that reports a failure, as the serial path stopped at its first failure.
140
+ * How a create that is not finishing is retried. A seam rather than constants so
141
+ * a test can run the whole policy without waiting through it.
142
+ *
143
+ * Each round that confirms nothing waits the next interval and counts toward the
144
+ * limit; a round that confirms anything resets it. The total wait is bounded
145
+ * (about 15 s), on purpose: this is a courtesy to a server that is briefly slow
146
+ * or rate-limited, not a promise to outlast an outage — past it the command says
147
+ * what is unknown and how to continue.
148
+ */
149
+ exports.batchRetryPolicy = {
150
+ backoffMs: [1000, 2000, 4000, 8000],
151
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
152
+ };
153
+ const SERVER_DEADLINE_CODES = new Set(['BATCH_DEADLINE_EXCEEDED']);
154
+ /** A row the server skipped because something it depends on never came to exist. */
155
+ const UPSTREAM_FAILED_CODES = new Set(['BATCH_PARENT_NOT_CREATED', 'BATCH_BLOCKER_NOT_CREATED']);
156
+ function isUncertainTransport(error) {
157
+ return (!error.status ||
158
+ error.status === 408 ||
159
+ error.status === 425 ||
160
+ error.status === 429 ||
161
+ error.status >= 500 ||
162
+ error.code === 'IDEMPOTENCY_IN_PROGRESS');
163
+ }
164
+ function stableKeyOf(node) {
165
+ const key = node.payload.externalKey;
166
+ return typeof key === 'string' && key.trim() ? key : undefined;
167
+ }
168
+ /**
169
+ * Create `nodes` — parents and blockers BEFORE what depends on them — until every
170
+ * one exists, or say precisely which do not and why.
171
+ *
172
+ * One request is a bounded unit of work, not the whole job: the server answers
173
+ * within a response budget, creating what it can and reporting the rest as not
174
+ * started (`BATCH_DEADLINE_EXCEEDED`) or still in flight (`pending`). That is a
175
+ * normal, successful answer, so this loop is what turns it into a finished tree:
176
+ * submit, record what is confirmed, submit what is left. It used to stop at the
177
+ * first row the server had not confirmed and hand the person a `--resume` to
178
+ * type — once per row or two, against a server that confirms a row or two per
179
+ * request (OSK-S-440 #1, #3, #4).
180
+ *
181
+ * The one rule that makes resubmitting safe is the one the server enforces: a
182
+ * node with a stable `externalKey` can be created once. A submitted write whose
183
+ * outcome is unknown is therefore READ first (`reconcileSubmittedNodes`), and only
184
+ * if the key is still absent is the node sent again — where, if the first write
185
+ * has landed in the meantime, the unique index turns it into `exists`. A node with
186
+ * no stable key is never resubmitted after an uncertain outcome, because nothing
187
+ * could tell the second write from a duplicate; that case stops and says so, as
188
+ * before.
189
+ *
190
+ * `mapping` holds key -> id for nodes that already exist and is extended in place.
122
191
  */
123
- async function createNodesViaBatch(nodes, mapping) {
192
+ async function createNodesViaBatch(nodes, mapping, options = {}) {
193
+ const startedAt = Date.now();
194
+ const position = new Map(nodes.map((node, index) => [node.key, index]));
124
195
  const rows = [];
125
196
  const failures = [];
126
- for (const [chunkIndex, chunk] of chunkItems(nodes).entries()) {
197
+ const chunkSize = data_provider_1.TASK_BATCH_MAX_ITEMS ?? 100;
198
+ let remaining = nodes.filter((node) => !(node.key in mapping));
199
+ /** Nodes whose last submission ended without a verdict: they may exist. */
200
+ const unknown = new Set();
201
+ let stalled = 0;
202
+ let round = 0;
203
+ let lastError;
204
+ const report = (waiting) => options.onProgress?.({
205
+ confirmed: rows.length,
206
+ total: nodes.length,
207
+ round,
208
+ elapsedMs: Date.now() - startedAt,
209
+ ...(waiting ? { waiting } : {}),
210
+ });
211
+ /** Everything still owed becomes a failure that says whether it may exist. */
212
+ const giveUp = (reason) => {
213
+ for (const node of remaining) {
214
+ const externalKey = stableKeyOf(node);
215
+ const maybeThere = unknown.has(node.key);
216
+ failures.push({
217
+ key: node.key,
218
+ skipped: !maybeThere,
219
+ outcome: maybeThere ? 'unknown' : 'not_attempted',
220
+ ...(externalKey ? { externalKey } : {}),
221
+ error: {
222
+ ...(maybeThere && lastError ? lastError : {}),
223
+ message: maybeThere
224
+ ? `The result for "${node.key}" is unknown: ${reason}` +
225
+ (externalKey
226
+ ? ' Re-run the same keyed tree with --resume to confirm it and continue.'
227
+ : ' This node has no stable external key. Inspect the Space before another write; do not re-run --fresh blindly.')
228
+ : `"${node.key}" was not started: ${reason}`,
229
+ code: maybeThere ? 'BATCH_OUTCOME_UNKNOWN' : 'BATCH_NOT_ATTEMPTED',
230
+ retryable: maybeThere ? Boolean(externalKey) : true,
231
+ },
232
+ });
233
+ }
234
+ remaining = [];
235
+ };
236
+ while (remaining.length > 0) {
237
+ round += 1;
238
+ const chunk = remaining.slice(0, chunkSize);
127
239
  const inChunk = new Map(chunk.map((node, i) => [node.key, `n${i}`]));
128
240
  const items = [];
129
241
  const sent = [];
242
+ const dropped = new Set();
130
243
  for (const [i, node] of chunk.entries()) {
131
244
  const { parentId: _ignored, ...payload } = node.payload;
132
245
  const item = { ...payload, tempId: `n${i}` };
246
+ let unresolved;
133
247
  if (node.parentKey) {
134
248
  const sameChunk = inChunk.get(node.parentKey);
135
249
  if (sameChunk)
136
250
  item.parentTempId = sameChunk;
137
251
  else if (mapping[node.parentKey])
138
252
  item.parentId = mapping[node.parentKey];
139
- else {
140
- failures.push({
141
- key: node.key,
142
- skipped: true,
143
- error: {
144
- message: `Parent "${node.parentKey}" was not created, so "${node.key}" was not attempted`,
145
- code: 'BATCH_PARENT_NOT_CREATED',
146
- retryable: false,
147
- },
148
- });
149
- continue;
150
- }
253
+ else
254
+ unresolved = `Parent "${node.parentKey}"`;
151
255
  }
152
256
  else if (node.payload.parentId) {
153
257
  item.parentId = node.payload.parentId;
154
258
  }
259
+ if (!unresolved && node.blockerKeys?.length) {
260
+ const tempIds = [];
261
+ const ids = [];
262
+ for (const blockerKey of node.blockerKeys) {
263
+ const sameChunk = inChunk.get(blockerKey);
264
+ if (sameChunk)
265
+ tempIds.push(sameChunk);
266
+ else if (mapping[blockerKey])
267
+ ids.push(mapping[blockerKey]);
268
+ else {
269
+ unresolved = `Blocker "${blockerKey}"`;
270
+ break;
271
+ }
272
+ }
273
+ if (!unresolved) {
274
+ if (tempIds.length)
275
+ item.blockedByTempIds = tempIds;
276
+ if (ids.length) {
277
+ item.blockedByIds = [
278
+ ...new Set([...(payload.blockedByIds ?? []), ...ids]),
279
+ ];
280
+ }
281
+ }
282
+ }
283
+ if (unresolved) {
284
+ dropped.add(node.key);
285
+ failures.push({
286
+ key: node.key,
287
+ skipped: true,
288
+ outcome: 'not_attempted',
289
+ error: {
290
+ message: `${unresolved} was not created, so "${node.key}" was not attempted`,
291
+ code: node.blockerKeys?.length
292
+ ? 'BATCH_BLOCKER_NOT_CREATED'
293
+ : 'BATCH_PARENT_NOT_CREATED',
294
+ retryable: false,
295
+ },
296
+ });
297
+ continue;
298
+ }
155
299
  items.push(item);
156
300
  sent.push(node);
157
301
  }
158
- if (items.length === 0)
302
+ // A dropped node is a failure already recorded; it leaves the queue now, or
303
+ // it would be examined, and recorded, again every round.
304
+ remaining = remaining.filter((node) => !dropped.has(node.key));
305
+ const upstreamBroken = dropped.size > 0;
306
+ if (items.length === 0) {
307
+ // Everything in this chunk depended on something that is gone, so nothing
308
+ // further can be built on it.
309
+ giveUp('a node it depends on was not created.');
159
310
  break;
311
+ }
312
+ let confirmedThisRound = 0;
313
+ let hardFailure = false;
314
+ let waitReason = 'the server confirmed nothing';
160
315
  let response;
161
316
  try {
162
317
  response = await data_provider_1.dataService.createTasksBatch(items, {
@@ -164,63 +319,124 @@ async function createNodesViaBatch(nodes, mapping) {
164
319
  });
165
320
  }
166
321
  catch (err) {
167
- if (chunkIndex === 0 && isBatchRouteMissing(err))
322
+ if (round === 1 && isBatchRouteMissing(err))
168
323
  return { supported: false };
169
324
  const error = describeApiFailure(err);
170
- const uncertain = !error.status || error.status === 408 || error.status >= 500 || error.code === 'IDEMPOTENCY_IN_PROGRESS';
171
- if (uncertain) {
325
+ lastError = error;
326
+ if (isUncertainTransport(error)) {
327
+ // Read before writing again: a lost response is not a lost write.
172
328
  const recovered = await reconcileSubmittedNodes(sent, mapping, error);
173
- rows.push(...recovered.rows);
174
- failures.push(...recovered.failures);
175
- // Even a fully reconciled chunk stops here: no extra write follows a
176
- // lost response during this run. Later chunks are explicitly unstarted.
177
- const remaining = nodes.slice((chunkIndex + 1) * (data_provider_1.TASK_BATCH_MAX_ITEMS ?? 100));
178
- for (const node of remaining)
179
- failures.push({ key: node.key, skipped: true,
180
- outcome: 'not_attempted', externalKey: node.payload.externalKey,
181
- error: { message: 'Not started after a batch transport failure; resume the same keyed tree.', code: 'BATCH_NOT_ATTEMPTED', retryable: true }, });
329
+ for (const row of recovered.rows) {
330
+ rows.push(row);
331
+ unknown.delete(row.key);
332
+ confirmedThisRound += 1;
333
+ }
334
+ const stillUnknown = recovered.failures.map((f) => f.key);
335
+ for (const key of stillUnknown)
336
+ unknown.add(key);
337
+ const unkeyed = sent.filter((n) => stillUnknown.includes(n.key) && !stableKeyOf(n));
338
+ remaining = remaining.filter((node) => !(node.key in mapping));
339
+ if (unkeyed.length > 0) {
340
+ // No stable key: a second write could not be told from a duplicate.
341
+ // Say what is unknown and stop, exactly as before.
342
+ for (const f of recovered.failures)
343
+ failures.push(f);
344
+ remaining = remaining.filter((node) => !recovered.failures.some((f) => f.key === node.key));
345
+ giveUp('the request was lost before the server answered.');
346
+ break;
347
+ }
348
+ waitReason = `the request did not complete (${error.status ?? 'no response'})`;
182
349
  }
183
350
  else {
184
- for (const node of sent)
351
+ // The whole request was refused (400/401/403/413/422...): a decision, not a glitch.
352
+ for (const node of sent) {
185
353
  failures.push({ key: node.key, skipped: false, outcome: 'failed', error });
186
- for (const node of nodes.slice((chunkIndex + 1) * (data_provider_1.TASK_BATCH_MAX_ITEMS ?? 100))) {
187
- failures.push({ key: node.key, skipped: true, outcome: 'not_attempted',
188
- error: { message: 'Not started after a batch refusal.', code: 'BATCH_NOT_ATTEMPTED', retryable: true } });
189
354
  }
355
+ remaining = remaining.filter((node) => !sent.includes(node));
356
+ giveUp('the batch request was refused.');
357
+ break;
190
358
  }
191
- break;
192
359
  }
193
- let failed = false;
194
- const reconciliationDeadlineAt = Date.now() + 5000;
195
- for (const [i, node] of sent.entries()) {
196
- const result = response.results?.find((r) => r.index === i);
197
- if (!result || result.status === 'pending' || (result.ok && !result.id) || (result.error?.status ?? 0) >= 500) {
198
- const recovered = await reconcileSubmittedNodes([{ ...node, payload: {
199
- ...node.payload, ...(result?.externalKey ? { externalKey: result.externalKey } : {}),
200
- } }], mapping, {
201
- message: 'Batch did not confirm this submitted write', code: 'BATCH_OUTCOME_UNKNOWN', retryable: true,
202
- }, reconciliationDeadlineAt);
203
- rows.push(...recovered.rows);
204
- failures.push(...recovered.failures);
205
- failed = true;
206
- continue;
207
- }
208
- if (result.ok && result.id) {
209
- mapping[node.key] = result.id;
210
- rows.push({
211
- key: node.key,
212
- id: result.id,
213
- ...(result.identifier ? { identifier: result.identifier } : {}),
214
- status: result.status === 'exists' ? 'exists' : 'created',
215
- ...(result.externalKey ? { externalKey: result.externalKey } : {}),
216
- });
217
- }
218
- else {
219
- failed = true;
360
+ if (response) {
361
+ const results = new Map((response.results ?? []).map((r) => [r.index, r]));
362
+ const retryLater = [];
363
+ const reconcileDeadlineAt = Date.now() + 5000;
364
+ for (const [i, node] of sent.entries()) {
365
+ const result = results.get(i);
366
+ const code = result?.error?.code;
367
+ const status = result?.error?.status ?? 0;
368
+ const confirmed = result?.ok && result.id;
369
+ if (confirmed) {
370
+ mapping[node.key] = result.id;
371
+ rows.push({
372
+ key: node.key,
373
+ id: result.id,
374
+ ...(result.identifier ? { identifier: result.identifier } : {}),
375
+ status: result.status === 'exists' ? 'exists' : 'created',
376
+ ...(result.externalKey ? { externalKey: result.externalKey } : {}),
377
+ });
378
+ unknown.delete(node.key);
379
+ confirmedThisRound += 1;
380
+ continue;
381
+ }
382
+ // Submitted, outcome not established: pending, absent from a 200, an ok
383
+ // row with no id, or a 5xx the item handler returned. Read, don't guess.
384
+ if (!result || result.status === 'pending' || result.ok || status >= 500) {
385
+ const recovered = await reconcileSubmittedNodes([
386
+ {
387
+ ...node,
388
+ payload: {
389
+ ...node.payload,
390
+ ...(result?.externalKey ? { externalKey: result.externalKey } : {}),
391
+ },
392
+ },
393
+ ], mapping, {
394
+ message: 'Batch did not confirm this submitted write',
395
+ code: 'BATCH_OUTCOME_UNKNOWN',
396
+ retryable: true,
397
+ }, reconcileDeadlineAt);
398
+ if (recovered.rows.length > 0) {
399
+ for (const row of recovered.rows)
400
+ rows.push(row);
401
+ unknown.delete(node.key);
402
+ confirmedThisRound += 1;
403
+ }
404
+ else if (stableKeyOf(node) || result?.externalKey) {
405
+ unknown.add(node.key);
406
+ retryLater.push(node);
407
+ lastError = recovered.failures[0]?.error ?? lastError;
408
+ }
409
+ else {
410
+ failures.push(...recovered.failures);
411
+ hardFailure = true;
412
+ }
413
+ continue;
414
+ }
415
+ // Never started: the response budget ran out, or the rate window did.
416
+ if (result.status === 'skipped' &&
417
+ (SERVER_DEADLINE_CODES.has(code ?? '') ||
418
+ code === 'BATCH_RATE_LIMITED' ||
419
+ status === 429 ||
420
+ status === 408)) {
421
+ retryLater.push(node);
422
+ if (code === 'BATCH_RATE_LIMITED' || status === 429)
423
+ waitReason = 'the create rate limit is spent for this minute';
424
+ continue;
425
+ }
426
+ // Something it depends on failed, or the row itself was refused for good.
427
+ const retryable = Boolean(result.error?.retryable) && result.status !== 'skipped';
428
+ if (retryable) {
429
+ retryLater.push(node);
430
+ lastError = describeApiFailure(result.error);
431
+ continue;
432
+ }
433
+ hardFailure = true;
220
434
  failures.push({
221
435
  key: node.key,
222
436
  skipped: result.status === 'skipped',
223
- outcome: result.status === 'skipped' ? 'not_attempted' : 'failed',
437
+ outcome: result.status === 'skipped' || UPSTREAM_FAILED_CODES.has(code ?? '')
438
+ ? 'not_attempted'
439
+ : 'failed',
224
440
  ...(result.externalKey ? { externalKey: result.externalKey } : {}),
225
441
  error: result.error
226
442
  ? {
@@ -232,17 +448,37 @@ async function createNodesViaBatch(nodes, mapping) {
232
448
  : { message: 'Task was not created', retryable: false },
233
449
  });
234
450
  }
235
- }
236
- if (failed) {
237
- for (const node of nodes.slice((chunkIndex + 1) * (data_provider_1.TASK_BATCH_MAX_ITEMS ?? 100))) {
238
- failures.push({ key: node.key, skipped: true, outcome: 'not_attempted',
239
- externalKey: node.payload.externalKey,
240
- error: { message: 'Not started after an incomplete batch; resume the same keyed tree.', code: 'BATCH_NOT_ATTEMPTED', retryable: true }, });
451
+ const retrySet = new Set(retryLater.map((n) => n.key));
452
+ remaining = remaining.filter((node) => !(node.key in mapping) && (retrySet.has(node.key) || !sent.includes(node)));
453
+ // Rows refused for good leave the queue; the loop stops below.
454
+ if (hardFailure) {
455
+ remaining = remaining.filter((node) => !failures.some((f) => f.key === node.key));
241
456
  }
457
+ }
458
+ report();
459
+ if (hardFailure || upstreamBroken) {
460
+ giveUp('an earlier node was refused; nothing that depends on it can be created.');
242
461
  break;
243
462
  }
463
+ if (remaining.length === 0)
464
+ break;
465
+ if (confirmedThisRound > 0) {
466
+ stalled = 0;
467
+ }
468
+ else {
469
+ stalled += 1;
470
+ if (stalled >= exports.batchRetryPolicy.backoffMs.length) {
471
+ giveUp(`no progress after ${stalled} attempts (${waitReason}${lastError?.code ? `; last: ${lastError.code}` : ''}).`);
472
+ break;
473
+ }
474
+ const ms = exports.batchRetryPolicy.backoffMs[stalled - 1];
475
+ report({ ms, reason: waitReason });
476
+ await exports.batchRetryPolicy.sleep(ms);
477
+ }
244
478
  }
245
- return { supported: true, rows, failures };
479
+ rows.sort((a, b) => (position.get(a.key) ?? 0) - (position.get(b.key) ?? 0));
480
+ failures.sort((a, b) => (position.get(a.key) ?? 0) - (position.get(b.key) ?? 0));
481
+ return { supported: true, rows, failures, rounds: round };
246
482
  }
247
483
  /**
248
484
  * Apply `updates` in chunks; `results[i]` answers `updates[i]`. A chunk the
@@ -29,3 +29,8 @@ export declare function describeEstimate(economics: unknown): string;
29
29
  * scored and sorts after every row that is.
30
30
  */
31
31
  export declare function economicsScoreText(economics: unknown): string;
32
+ /**
33
+ * One line for a `task.economics.*` Activity event (OSK-15365): the revision, the claim after the
34
+ * change and the claim it replaced, so `tasks activity` shows from-what-to-what without `--json`.
35
+ */
36
+ export declare function describeEconomicsChange(data: unknown): string;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.economicsFromFlags = economicsFromFlags;
4
4
  exports.describeEstimate = describeEstimate;
5
5
  exports.economicsScoreText = economicsScoreText;
6
+ exports.describeEconomicsChange = describeEconomicsChange;
6
7
  const data_provider_1 = require("@skrr-ai/data-provider");
7
8
  /**
8
9
  * A flag's amount as a number, or an error that names the flag and the value (OSK-15366). A raw
@@ -85,3 +86,17 @@ function economicsScoreText(economics) {
85
86
  const score = (0, data_provider_1.proposalEconomicsRank)(economics).score;
86
87
  return score === null || !Number.isFinite(score) ? '—' : String(Math.round(score));
87
88
  }
89
+ /**
90
+ * One line for a `task.economics.*` Activity event (OSK-15365): the revision, the claim after the
91
+ * change and the claim it replaced, so `tasks activity` shows from-what-to-what without `--json`.
92
+ */
93
+ function describeEconomicsChange(data) {
94
+ const value = (data && typeof data === 'object' ? data : {});
95
+ const now = value.change === 'retracted' || !value.after ? 'no estimate' : describeEstimate(value.after);
96
+ let was = 'no estimate';
97
+ if (value.before)
98
+ was = describeEstimate(value.before);
99
+ else if (value.beforeUnavailable)
100
+ was = 'not recorded';
101
+ return `r${value.revision ?? '?'} ${value.change ?? 'changed'}: ${now} (was: ${was})`;
102
+ }
@@ -1,10 +1,10 @@
1
1
  /**
2
- * What completing a recurring occurrence did to its series, as a sentence (OSK-15483).
3
- *
4
- * `tasks complete` armed the next occurrence (or deliberately did not) and printed nothing either way, so a
5
- * person could not tell that the series continued, or that the third of three runs had ended it. The server
6
- * reports `recurrence` on the completion; this words it.
2
+ * The ONE way back for a series whose next occurrence was never armed (OSK-15338), named by every
3
+ * surface that reports the failure. `tasks schedule` refuses the finished occurrence that carries
4
+ * the failure, so naming it (as `tasks show` did) sent people to a command that could never work.
5
+ * An `invalid_pattern` failure has no cure without a new pattern, so the hint carries `--cron`.
7
6
  */
7
+ export declare function recurrenceRearmHint(code: unknown, taskRef?: string, bin?: string): string;
8
8
  export type RecurrenceOutcome = {
9
9
  state?: string;
10
10
  reason?: string;
@@ -16,4 +16,4 @@ export type RecurrenceOutcome = {
16
16
  armFailure?: unknown;
17
17
  };
18
18
  /** One line, or null when the Task does not recur (or an older server did not say). */
19
- export declare function recurrenceOutcomeLine(outcome: RecurrenceOutcome | null | undefined): string | null;
19
+ export declare function recurrenceOutcomeLine(outcome: RecurrenceOutcome | null | undefined, taskRef?: string, bin?: string): string | null;
@@ -1,13 +1,32 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.recurrenceRearmHint = recurrenceRearmHint;
3
4
  exports.recurrenceOutcomeLine = recurrenceOutcomeLine;
5
+ /**
6
+ * What completing a recurring occurrence did to its series, as a sentence (OSK-15483).
7
+ *
8
+ * `tasks complete` armed the next occurrence (or deliberately did not) and printed nothing either way, so a
9
+ * person could not tell that the series continued, or that the third of three runs had ended it. The server
10
+ * reports `recurrence` on the completion; this words it.
11
+ */
12
+ const data_provider_1 = require("@skrr-ai/data-provider");
13
+ /**
14
+ * The ONE way back for a series whose next occurrence was never armed (OSK-15338), named by every
15
+ * surface that reports the failure. `tasks schedule` refuses the finished occurrence that carries
16
+ * the failure, so naming it (as `tasks show` did) sent people to a command that could never work.
17
+ * An `invalid_pattern` failure has no cure without a new pattern, so the hint carries `--cron`.
18
+ */
19
+ function recurrenceRearmHint(code, taskRef = '<task-id>', bin = 'skrr') {
20
+ const fix = code === 'invalid_pattern' ? ' --cron "<minute hour day month weekday>"' : '';
21
+ return `Re-arm the series with \`${bin} tasks rearm-recurrence ${taskRef}${fix}\`.`;
22
+ }
4
23
  const ENDED_BECAUSE = {
5
24
  max_runs: 'it reached its maximum number of runs',
6
25
  end_date: 'its end date has passed',
7
26
  no_next_occurrence: 'its pattern has no further occurrence',
8
27
  };
9
28
  /** One line, or null when the Task does not recur (or an older server did not say). */
10
- function recurrenceOutcomeLine(outcome) {
29
+ function recurrenceOutcomeLine(outcome, taskRef, bin = 'skrr') {
11
30
  if (!outcome || typeof outcome !== 'object')
12
31
  return null;
13
32
  switch (outcome.state) {
@@ -22,8 +41,13 @@ function recurrenceOutcomeLine(outcome) {
22
41
  return 'Recurrence: the series is paused, so no next occurrence was scheduled.';
23
42
  case 'routine_managed':
24
43
  return 'Recurrence: a Routine owns this series, so no next occurrence was scheduled here.';
25
- case 'not_armed':
26
- return 'Recurrence: the series is live but no next occurrence was scheduled; check it with `tasks show`.';
44
+ case 'not_armed': {
45
+ const code = outcome.armFailure?.code;
46
+ const why = typeof code === 'string' && code
47
+ ? `${(0, data_provider_1.describeRecurrenceArmFailure)(code)} (${code})`
48
+ : 'The series is live but no next occurrence was scheduled.';
49
+ return `Recurrence: ${why} ${recurrenceRearmHint(code, taskRef, bin)}`;
50
+ }
27
51
  default:
28
52
  return null;
29
53
  }
@@ -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
  *