@skrr-ai/cli 0.1.118 → 0.1.120

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 (285) hide show
  1. package/dist/base-command.d.ts +32 -0
  2. package/dist/base-command.js +113 -5
  3. package/dist/commands/commands.js +11 -0
  4. package/dist/commands/commitments/action-proposal.d.ts +13 -0
  5. package/dist/commands/commitments/action-proposal.js +39 -0
  6. package/dist/commands/commitments/action-proposals/decide.js +8 -1
  7. package/dist/commands/commitments/action-proposals/execute.js +11 -1
  8. package/dist/commands/commitments/create.js +2 -2
  9. package/dist/commands/commitments/doctor.d.ts +8 -0
  10. package/dist/commands/commitments/doctor.js +22 -0
  11. package/dist/commands/commitments/execute-remediation.js +12 -1
  12. package/dist/commands/commitments/observations.js +2 -2
  13. package/dist/commands/commitments/pack/bind.d.ts +18 -1
  14. package/dist/commands/commitments/pack/bind.js +66 -8
  15. package/dist/commands/commitments/pack/plan.d.ts +4 -1
  16. package/dist/commands/commitments/pack/plan.js +30 -2
  17. package/dist/commands/commitments/update.js +8 -2
  18. package/dist/commands/completion/index.js +1 -1
  19. package/dist/commands/completion/values.js +1 -1
  20. package/dist/commands/computer-use/audio.d.ts +1 -0
  21. package/dist/commands/computer-use/audio.js +1 -0
  22. package/dist/commands/computer-use/browser.d.ts +1 -0
  23. package/dist/commands/computer-use/browser.js +1 -0
  24. package/dist/commands/computer-use/disable.d.ts +1 -0
  25. package/dist/commands/computer-use/disable.js +1 -0
  26. package/dist/commands/computer-use/enable.d.ts +1 -0
  27. package/dist/commands/computer-use/enable.js +1 -0
  28. package/dist/commands/computer-use/index.d.ts +1 -0
  29. package/dist/commands/computer-use/index.js +1 -0
  30. package/dist/commands/computer-use/install.d.ts +1 -0
  31. package/dist/commands/computer-use/install.js +1 -0
  32. package/dist/commands/computer-use/recover.d.ts +1 -0
  33. package/dist/commands/computer-use/recover.js +1 -0
  34. package/dist/commands/computer-use/status.d.ts +1 -0
  35. package/dist/commands/computer-use/status.js +1 -0
  36. package/dist/commands/computer-use/stop.d.ts +1 -0
  37. package/dist/commands/computer-use/stop.js +1 -0
  38. package/dist/commands/context.js +16 -11
  39. package/dist/commands/convos/recovery.js +1 -1
  40. package/dist/commands/daemon/diagnostics.d.ts +9 -0
  41. package/dist/commands/daemon/diagnostics.js +53 -0
  42. package/dist/commands/daemon/index.js +2 -0
  43. package/dist/commands/daemon/maintenance/attest.d.ts +15 -0
  44. package/dist/commands/daemon/maintenance/attest.js +52 -0
  45. package/dist/commands/daemon/maintenance/hold.d.ts +17 -0
  46. package/dist/commands/daemon/maintenance/hold.js +70 -0
  47. package/dist/commands/daemon/maintenance/index.d.ts +12 -0
  48. package/dist/commands/daemon/maintenance/index.js +30 -0
  49. package/dist/commands/daemon/maintenance/resume.d.ts +13 -0
  50. package/dist/commands/daemon/maintenance/resume.js +36 -0
  51. package/dist/commands/daemon/maintenance/status.d.ts +13 -0
  52. package/dist/commands/daemon/maintenance/status.js +36 -0
  53. package/dist/commands/doctor.d.ts +4 -1
  54. package/dist/commands/doctor.js +50 -6
  55. package/dist/commands/followups/plans/cancel-batch.js +6 -3
  56. package/dist/commands/followups/plans/capacity.js +7 -3
  57. package/dist/commands/followups/plans/observability.js +11 -3
  58. package/dist/commands/followups/plans/preferences/set.js +26 -8
  59. package/dist/commands/followups/plans/preferences.js +1 -1
  60. package/dist/commands/followups/plans/show.js +1 -1
  61. package/dist/commands/goals/key-results/assess.d.ts +36 -0
  62. package/dist/commands/goals/key-results/assess.js +110 -0
  63. package/dist/commands/goals/key-results/history.d.ts +30 -0
  64. package/dist/commands/goals/key-results/history.js +104 -0
  65. package/dist/commands/goals/key-results/list.d.ts +5 -4
  66. package/dist/commands/goals/key-results/list.js +12 -6
  67. package/dist/commands/goals/key-results/show.d.ts +4 -2
  68. package/dist/commands/goals/key-results/show.js +25 -4
  69. package/dist/commands/goals/key-results/update.d.ts +4 -0
  70. package/dist/commands/goals/key-results/update.js +18 -24
  71. package/dist/commands/goals/plan-now.js +12 -1
  72. package/dist/commands/goals/planner-compare.d.ts +30 -0
  73. package/dist/commands/goals/planner-compare.js +136 -0
  74. package/dist/commands/machines/dedicated/service/exec.d.ts +3 -0
  75. package/dist/commands/machines/dedicated/service/exec.js +70 -11
  76. package/dist/commands/machines/hosted/destroy.d.ts +1 -0
  77. package/dist/commands/machines/hosted/destroy.js +11 -1
  78. package/dist/commands/machines/hosted/exec.d.ts +1 -0
  79. package/dist/commands/machines/hosted/exec.js +12 -1
  80. package/dist/commands/machines/hosted/index.js +1 -0
  81. package/dist/commands/machines/hosted/operation.d.ts +14 -0
  82. package/dist/commands/machines/hosted/operation.js +51 -0
  83. package/dist/commands/machines/hosted/pull.js +1 -1
  84. package/dist/commands/machines/hosted/start.js +6 -2
  85. package/dist/commands/memory/versions/list.d.ts +2 -0
  86. package/dist/commands/memory/versions/list.js +3 -2
  87. package/dist/commands/memory/versions/show.d.ts +2 -0
  88. package/dist/commands/memory/versions/show.js +3 -2
  89. package/dist/commands/spaces/auto-executor/compare.d.ts +18 -0
  90. package/dist/commands/spaces/auto-executor/compare.js +139 -0
  91. package/dist/commands/spaces/auto-executor/runs.d.ts +3 -0
  92. package/dist/commands/spaces/auto-executor/runs.js +13 -1
  93. package/dist/commands/spaces/charter/create-from-description.d.ts +21 -0
  94. package/dist/commands/spaces/charter/create-from-description.js +77 -0
  95. package/dist/commands/spaces/charter/show.d.ts +22 -0
  96. package/dist/commands/spaces/charter/show.js +61 -0
  97. package/dist/commands/spaces/heartbeats/list.d.ts +1 -16
  98. package/dist/commands/spaces/heartbeats/list.js +13 -0
  99. package/dist/commands/spaces/install-template.js +3 -0
  100. package/dist/commands/spaces/loops/migrate.d.ts +32 -0
  101. package/dist/commands/spaces/loops/migrate.js +103 -0
  102. package/dist/commands/spaces/loops/release.d.ts +14 -0
  103. package/dist/commands/spaces/loops/release.js +59 -0
  104. package/dist/commands/spaces/notebook.d.ts +25 -0
  105. package/dist/commands/spaces/notebook.js +75 -0
  106. package/dist/commands/spaces/proposals/discard.d.ts +17 -0
  107. package/dist/commands/spaces/proposals/discard.js +17 -0
  108. package/dist/commands/spaces/proposals/keep.d.ts +17 -0
  109. package/dist/commands/spaces/proposals/keep.js +17 -0
  110. package/dist/commands/spaces/proposals/list.d.ts +24 -0
  111. package/dist/commands/spaces/proposals/list.js +89 -0
  112. package/dist/commands/spaces/skills/propose.d.ts +29 -0
  113. package/dist/commands/spaces/skills/propose.js +82 -0
  114. package/dist/commands/spaces/templates/fork.js +3 -0
  115. package/dist/commands/spaces/triggers/show.js +6 -0
  116. package/dist/commands/spaces/workflows/retry.d.ts +1 -0
  117. package/dist/commands/spaces/workflows/retry.js +8 -1
  118. package/dist/commands/spaces/workflows/triggers/create.js +3 -2
  119. package/dist/commands/spaces/workflows/triggers/fire.js +4 -0
  120. package/dist/commands/spaces/workflows/triggers/update.js +2 -1
  121. package/dist/commands/spaces/write-policy/set.d.ts +22 -0
  122. package/dist/commands/spaces/write-policy/set.js +82 -0
  123. package/dist/commands/spaces/write-policy/show.d.ts +19 -0
  124. package/dist/commands/spaces/write-policy/show.js +49 -0
  125. package/dist/commands/store/artifacts/receipt.d.ts +14 -0
  126. package/dist/commands/store/artifacts/receipt.js +34 -0
  127. package/dist/commands/store/artifacts/release.d.ts +2 -0
  128. package/dist/commands/store/artifacts/release.js +11 -0
  129. package/dist/commands/store/governance/publish-release.d.ts +2 -0
  130. package/dist/commands/store/governance/publish-release.js +30 -1
  131. package/dist/commands/store/governance/release-receipt.d.ts +12 -0
  132. package/dist/commands/store/governance/release-receipt.js +32 -0
  133. package/dist/commands/tasks/approvals/list.d.ts +11 -5
  134. package/dist/commands/tasks/approvals/list.js +13 -6
  135. package/dist/commands/tasks/attachments/facts.d.ts +12 -0
  136. package/dist/commands/tasks/attachments/facts.js +29 -0
  137. package/dist/commands/tasks/attachments/upload.d.ts +1 -0
  138. package/dist/commands/tasks/attachments/upload.js +16 -0
  139. package/dist/commands/tasks/bulk-update.js +35 -1
  140. package/dist/commands/tasks/context.d.ts +15 -0
  141. package/dist/commands/tasks/context.js +69 -0
  142. package/dist/commands/tasks/copy.d.ts +18 -0
  143. package/dist/commands/tasks/copy.js +85 -0
  144. package/dist/commands/tasks/create-tree.js +97 -23
  145. package/dist/commands/tasks/create.js +9 -1
  146. package/dist/commands/tasks/deliverable/add.js +20 -10
  147. package/dist/commands/tasks/move.js +8 -1
  148. package/dist/commands/tasks/operations/show.d.ts +16 -0
  149. package/dist/commands/tasks/operations/show.js +98 -0
  150. package/dist/commands/tasks/operations/wait.d.ts +22 -0
  151. package/dist/commands/tasks/operations/wait.js +114 -0
  152. package/dist/commands/tasks/preflight.d.ts +1 -0
  153. package/dist/commands/tasks/preflight.js +11 -1
  154. package/dist/commands/tasks/rearm-recurrence.js +4 -0
  155. package/dist/commands/tasks/result/show.js +22 -0
  156. package/dist/commands/tasks/result/submit.d.ts +1 -0
  157. package/dist/commands/tasks/result/submit.js +3 -0
  158. package/dist/commands/tasks/retry-receipt.d.ts +15 -0
  159. package/dist/commands/tasks/retry-receipt.js +89 -0
  160. package/dist/commands/tasks/retry-review.d.ts +16 -0
  161. package/dist/commands/tasks/retry-review.js +66 -0
  162. package/dist/commands/tasks/retry.d.ts +4 -8
  163. package/dist/commands/tasks/retry.js +34 -68
  164. package/dist/commands/tasks/runs.d.ts +21 -0
  165. package/dist/commands/tasks/runs.js +95 -0
  166. package/dist/commands/tasks/show.d.ts +2 -0
  167. package/dist/commands/tasks/show.js +15 -1
  168. package/dist/commands/tasks/start-receipt.d.ts +15 -0
  169. package/dist/commands/tasks/start-receipt.js +95 -0
  170. package/dist/commands/tasks/start.d.ts +4 -0
  171. package/dist/commands/tasks/start.js +49 -3
  172. package/dist/commands/tasks/update.js +3 -0
  173. package/dist/commands/tasks/upsert.js +17 -3
  174. package/dist/commands/tasks/watch.d.ts +3 -0
  175. package/dist/commands/tasks/watch.js +85 -20
  176. package/dist/commands/wiki/history.d.ts +33 -0
  177. package/dist/commands/wiki/history.js +134 -0
  178. package/dist/commands/wiki/ls.d.ts +2 -0
  179. package/dist/commands/wiki/ls.js +8 -0
  180. package/dist/commands/wiki/update.d.ts +3 -1
  181. package/dist/commands/wiki/update.js +24 -2
  182. package/dist/commands/wiki/write.d.ts +22 -0
  183. package/dist/commands/wiki/write.js +144 -6
  184. package/dist/generated/agent-guides.json +81 -81
  185. package/dist/generated/managed-instruction-guides.json +1 -1
  186. package/dist/lib/agent-guides.d.ts +9 -0
  187. package/dist/lib/agent-guides.js +23 -1
  188. package/dist/lib/agentic-stream.d.ts +5 -0
  189. package/dist/lib/agentic-stream.js +122 -3
  190. package/dist/lib/api-fetch.d.ts +4 -0
  191. package/dist/lib/api-fetch.js +25 -7
  192. package/dist/lib/auth-diagnostics.d.ts +39 -0
  193. package/dist/lib/auth-diagnostics.js +71 -0
  194. package/dist/lib/cli-client-identity.d.ts +6 -0
  195. package/dist/lib/cli-client-identity.js +16 -0
  196. package/dist/lib/cli-installation-diagnostics.d.ts +52 -0
  197. package/dist/lib/cli-installation-diagnostics.js +254 -0
  198. package/dist/lib/command-hygiene.d.ts +45 -0
  199. package/dist/lib/command-hygiene.js +413 -0
  200. package/dist/lib/commitments.d.ts +64 -3
  201. package/dist/lib/commitments.js +34 -6
  202. package/dist/lib/completion-index.d.ts +16 -4
  203. package/dist/lib/completion-index.js +50 -14
  204. package/dist/lib/config.js +7 -1
  205. package/dist/lib/daemon-maintenance-command.d.ts +22 -0
  206. package/dist/lib/daemon-maintenance-command.js +39 -0
  207. package/dist/lib/daemon-maintenance.d.ts +275 -0
  208. package/dist/lib/daemon-maintenance.js +1035 -0
  209. package/dist/lib/dedicated-service.d.ts +3 -2
  210. package/dist/lib/dedicated-service.js +11 -3
  211. package/dist/lib/governed-store.d.ts +8 -0
  212. package/dist/lib/governed-store.js +5 -0
  213. package/dist/lib/hosted-machines.d.ts +33 -2
  214. package/dist/lib/hosted-machines.js +19 -1
  215. package/dist/lib/json-output.d.ts +1 -1
  216. package/dist/lib/machines.d.ts +9 -1
  217. package/dist/lib/machines.js +15 -1
  218. package/dist/lib/memory-history-session.d.ts +14 -0
  219. package/dist/lib/memory-history-session.js +60 -0
  220. package/dist/lib/node-adapter.js +13 -0
  221. package/dist/lib/record-output.d.ts +22 -0
  222. package/dist/lib/record-output.js +78 -0
  223. package/dist/lib/roles.d.ts +3 -3
  224. package/dist/lib/scoped-memory-command.d.ts +24 -0
  225. package/dist/lib/scoped-memory-command.js +87 -5
  226. package/dist/lib/space-charter.d.ts +49 -0
  227. package/dist/lib/space-charter.js +78 -0
  228. package/dist/lib/space-loop-migration.d.ts +18 -0
  229. package/dist/lib/space-loop-migration.js +128 -0
  230. package/dist/lib/space-notebook.d.ts +22 -0
  231. package/dist/lib/space-notebook.js +140 -0
  232. package/dist/lib/space-proposal-decision-command.d.ts +36 -0
  233. package/dist/lib/space-proposal-decision-command.js +108 -0
  234. package/dist/lib/space-proposals.d.ts +44 -0
  235. package/dist/lib/space-proposals.js +186 -0
  236. package/dist/lib/space-templates.d.ts +8 -0
  237. package/dist/lib/space-templates.js +17 -0
  238. package/dist/lib/space-write-policy.d.ts +24 -0
  239. package/dist/lib/space-write-policy.js +103 -0
  240. package/dist/lib/task-agent-context-capabilities.d.ts +10 -0
  241. package/dist/lib/task-agent-context-capabilities.js +22 -0
  242. package/dist/lib/task-agent-context-compatibility.d.ts +9 -0
  243. package/dist/lib/task-agent-context-compatibility.js +30 -0
  244. package/dist/lib/task-asset-upload.d.ts +22 -0
  245. package/dist/lib/task-asset-upload.js +257 -29
  246. package/dist/lib/task-batch.d.ts +15 -1
  247. package/dist/lib/task-batch.js +35 -2
  248. package/dist/lib/task-operation-watch.d.ts +66 -0
  249. package/dist/lib/task-operation-watch.js +159 -0
  250. package/dist/lib/task-resolver.d.ts +1 -1
  251. package/dist/lib/task-resolver.js +4 -3
  252. package/dist/lib/task-runtime-headers.js +13 -3
  253. package/dist/lib/task-work.d.ts +1 -1
  254. package/dist/lib/task-work.js +35 -1
  255. package/dist/lib/task-write-attribution.d.ts +14 -0
  256. package/dist/lib/task-write-attribution.js +35 -2
  257. package/dist/lib/triggers.d.ts +11 -0
  258. package/dist/lib/triggers.js +7 -0
  259. package/dist/lib/wiki-update.d.ts +1 -0
  260. package/dist/lib/wiki-update.js +10 -1
  261. package/dist/lib/wiki.js +4 -1
  262. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.d.ts +9 -1
  263. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.js +31 -15
  264. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +1 -0
  265. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonWire.js +13 -7
  266. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/guidedUpdate.d.ts +370 -0
  267. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/guidedUpdate.js +315 -0
  268. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/maintenanceReportWire.d.ts +178 -0
  269. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/maintenanceReportWire.js +262 -0
  270. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/toolApprovalSignature.d.ts +51 -0
  271. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/toolApprovalSignature.js +106 -0
  272. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.d.ts +9 -1
  273. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.js +32 -5
  274. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +1 -0
  275. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonWire.js +33 -14
  276. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/guidedUpdate.d.ts +370 -0
  277. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/guidedUpdate.js +404 -0
  278. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/maintenanceReportWire.d.ts +178 -0
  279. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/maintenanceReportWire.js +342 -0
  280. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/toolApprovalSignature.d.ts +51 -0
  281. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/toolApprovalSignature.js +138 -0
  282. package/dist/node_modules/@skrr-ai/auth-core/package.json +20 -0
  283. package/dist/node_modules/@skrr-ai/data-provider/index.js +27085 -20250
  284. package/oclif.manifest.json +10570 -7247
  285. package/package.json +20 -2
@@ -37,6 +37,8 @@ exports.readTaskForUpload = readTaskForUpload;
37
37
  exports.uploadTaskAssetFile = uploadTaskAssetFile;
38
38
  exports.attachTaskAssetFiles = attachTaskAssetFiles;
39
39
  exports.taskAssetMarkdownRef = taskAssetMarkdownRef;
40
+ exports.taskAssetUploadRecovery = taskAssetUploadRecovery;
41
+ exports.putFilePartToStorage = putFilePartToStorage;
40
42
  exports.sha256OfFile = sha256OfFile;
41
43
  exports.postFileToStorage = postFileToStorage;
42
44
  /**
@@ -45,7 +47,7 @@ exports.postFileToStorage = postFileToStorage;
45
47
  * funnel here so the routing rules live in exactly one place (OSK-9863).
46
48
  *
47
49
  * ≤25MB or image → buffered multipart (canonicalization needs the bytes)
48
- * larger non-image → intent → presigned POST to S3 → complete (OSK-9867)
50
+ * larger non-image → intent → granted single/multipart transport → complete
49
51
  *
50
52
  * Agent uploads retain the authenticated session producer even beside a wide
51
53
  * delegated credential. Human uploads use `/api/tasks/:id/*`.
@@ -100,6 +102,11 @@ async function readTaskForUpload(taskId) {
100
102
  */
101
103
  async function uploadTaskAssetFile(opts) {
102
104
  const { taskId, filePath, role, ledger } = opts;
105
+ if (opts.resumeAttachmentId !== undefined &&
106
+ (!opts.resumeAttachmentId || opts.resumeAttachmentId.trim() !== opts.resumeAttachmentId ||
107
+ opts.resumeAttachmentId.length > 200 || ledger !== undefined)) {
108
+ throw new Error('Resume requires one exact attachment id and retains its original ledger declaration.');
109
+ }
103
110
  if (!fs.existsSync(filePath)) {
104
111
  throw new Error(`File not found: ${filePath}`);
105
112
  }
@@ -121,7 +128,11 @@ async function uploadTaskAssetFile(opts) {
121
128
  const sessionTarget = await (0, session_task_write_1.sessionTaskWrite)('attachment', taskId);
122
129
  let result;
123
130
  try {
124
- if (sessionTarget) {
131
+ if (opts.resumeAttachmentId !== undefined) {
132
+ result = await resumeDirectUpload({ taskId, filePath, size, filename, mimeType, role,
133
+ attachmentId: opts.resumeAttachmentId }, sessionTarget);
134
+ }
135
+ else if (sessionTarget) {
125
136
  result = direct
126
137
  ? await sessionDirectUpload({ filePath, size, filename, mimeType, role, ledger }, sessionTarget)
127
138
  : await sessionBufferedUpload({ filePath, filename, mimeType, role, ledger }, sessionTarget);
@@ -135,7 +146,9 @@ async function uploadTaskAssetFile(opts) {
135
146
  catch (err) {
136
147
  throw withServerReason(err, filename);
137
148
  }
149
+ requireReadyAsset(result);
138
150
  return {
151
+ status: 'ready',
139
152
  id: String(result.id ?? result._id),
140
153
  digest: storedDigest(result),
141
154
  filename,
@@ -144,6 +157,12 @@ async function uploadTaskAssetFile(opts) {
144
157
  ...(result.resourceReceipt ? { resourceReceipt: result.resourceReceipt } : {}),
145
158
  };
146
159
  }
160
+ function requireReadyAsset(asset) {
161
+ const id = asset?.id ?? asset?._id;
162
+ if (typeof id !== 'string' || !id.trim() || asset.status !== 'ready') {
163
+ throw new Error('The server did not confirm a ready Task attachment; the upload is not complete.');
164
+ }
165
+ }
147
166
  /**
148
167
  * Put the server's own reason on a refused upload.
149
168
  *
@@ -163,6 +182,9 @@ function withServerReason(err, filename) {
163
182
  if (!message)
164
183
  return err;
165
184
  err.message = `${filename}: ${message}${code ? ` (${code})` : ''}`;
185
+ const recovery = taskAssetUploadRecovery(err);
186
+ if (recovery)
187
+ err.message += ` ${resumeHint(recovery.attachmentId)}`;
166
188
  return err;
167
189
  }
168
190
  /**
@@ -236,6 +258,7 @@ async function genericDirectUpload(p) {
236
258
  mimeType: p.mimeType,
237
259
  sizeBytes: p.size,
238
260
  sha256: await sha256OfFile(p.filePath),
261
+ transport: 'auto',
239
262
  role: p.role,
240
263
  ...(p.ledger?.identity
241
264
  ? {
@@ -246,20 +269,7 @@ async function genericDirectUpload(p) {
246
269
  }
247
270
  : {}),
248
271
  });
249
- if (!intent.uploadUrl)
250
- return intent.attachment;
251
- await postFileToStorage({
252
- uploadUrl: intent.uploadUrl,
253
- uploadFields: intent.uploadFields ?? {},
254
- filePath: p.filePath,
255
- size: p.size,
256
- filename: p.filename,
257
- mimeType: p.mimeType,
258
- });
259
- return data_provider_1.dataService.completeTaskAttachmentUpload({
260
- taskId: p.taskId,
261
- attachmentId: intent.attachment.id,
262
- });
272
+ return uploadDirectIntent(p, intent, directUploadProtocol(p.taskId, directAttachmentId(intent)));
263
273
  }
264
274
  async function sessionDirectUpload(p, target) {
265
275
  const intent = await (0, api_fetch_1.apiFetch)(`${target.path}/intent`, {
@@ -270,6 +280,7 @@ async function sessionDirectUpload(p, target) {
270
280
  mimeType: p.mimeType,
271
281
  sizeBytes: p.size,
272
282
  sha256: await sha256OfFile(p.filePath),
283
+ transport: 'auto',
273
284
  ...(p.role ? { role: p.role } : {}),
274
285
  ...(p.ledger?.identity
275
286
  ? {
@@ -281,20 +292,237 @@ async function sessionDirectUpload(p, target) {
281
292
  : {}),
282
293
  },
283
294
  });
284
- if (!intent.uploadUrl)
295
+ return uploadDirectIntent(p, intent, directUploadProtocol(target.taskId, directAttachmentId(intent), target));
296
+ }
297
+ function directUploadProtocol(taskId, attachmentId, target) {
298
+ const recovery = { taskId, attachmentId, transport: 'multipart' };
299
+ const identity = { taskId, attachmentId };
300
+ if (!target)
301
+ return {
302
+ recovery,
303
+ status: () => data_provider_1.dataService.getTaskAssetUploadStatus(identity),
304
+ sign: (partNumbers) => data_provider_1.dataService.signTaskAssetUploadParts({ ...identity, partNumbers }),
305
+ record: (part) => data_provider_1.dataService.recordTaskAssetUploadedPart({ ...identity, ...part }),
306
+ complete: () => data_provider_1.dataService.completeTaskAttachmentUpload(identity),
307
+ };
308
+ const uploadPath = `${target.path}/${encodeURIComponent(attachmentId)}`;
309
+ return {
310
+ recovery,
311
+ status: () => (0, api_fetch_1.apiFetch)(`${uploadPath}/upload-status`, {
312
+ credential: target.credential,
313
+ }),
314
+ sign: (partNumbers) => (0, api_fetch_1.apiFetch)(`${uploadPath}/upload-parts/sign`, {
315
+ method: 'POST', credential: target.credential, body: { partNumbers },
316
+ }),
317
+ record: (part) => (0, api_fetch_1.apiFetch)(`${uploadPath}/upload-parts/complete`, {
318
+ method: 'POST', credential: target.credential, body: part,
319
+ }),
320
+ complete: () => (0, api_fetch_1.apiFetch)(`${target.path}/complete`, {
321
+ method: 'POST', credential: target.credential, body: { attachmentId },
322
+ }),
323
+ };
324
+ }
325
+ /** A bounded recovery receipt, never a storage credential or claim that bytes are ready. */
326
+ function taskAssetUploadRecovery(error) {
327
+ if (!error || typeof error !== 'object')
328
+ return undefined;
329
+ const value = error.uploadRecovery;
330
+ if (!value || typeof value.taskId !== 'string' || typeof value.attachmentId !== 'string' ||
331
+ value.transport !== 'multipart')
332
+ return undefined;
333
+ return { taskId: value.taskId, attachmentId: value.attachmentId, transport: 'multipart',
334
+ ...(typeof value.sessionId === 'string' ? { sessionId: value.sessionId } : {}) };
335
+ }
336
+ function resumeHint(attachmentId) {
337
+ return `Upload attachment ${attachmentId} is not confirmed complete. ` +
338
+ `Use --resume-attachment ${attachmentId} with tasks attachments upload and the same file to recover this exact upload.`;
339
+ }
340
+ async function resumeDirectUpload(file, target) {
341
+ const taskId = target?.taskId ?? file.taskId;
342
+ const protocol = directUploadProtocol(taskId, file.attachmentId, target);
343
+ const progress = await protocol.status();
344
+ const rawDeclaration = progress?.declaredAttachment;
345
+ const declaration = rawDeclaration && typeof rawDeclaration === 'object' && !Array.isArray(rawDeclaration)
346
+ ? rawDeclaration : undefined;
347
+ const sha256 = await sha256OfFile(file.filePath);
348
+ if (!declaration || file.size < 1 || declaration.basis !== 'declared_upload_intent' ||
349
+ declaration.attachmentId !== file.attachmentId || declaration.taskId !== taskId ||
350
+ declaration.filename !== file.filename || declaration.mimeType !== file.mimeType ||
351
+ declaration.sizeBytes !== file.size || declaration.sha256 !== sha256 ||
352
+ typeof declaration.role !== 'string' || !['input', 'evidence', 'deliverable'].includes(declaration.role) ||
353
+ (file.role !== undefined && declaration.role !== file.role)) {
354
+ throw new Error('This file does not match the existing upload declaration, or the server cannot verify it. No replacement upload was created.');
355
+ }
356
+ if (progress.transport !== 'multipart' || typeof progress.expiresAt !== 'string' ||
357
+ !Number.isFinite(Date.parse(progress.expiresAt)) ||
358
+ Date.parse(progress.expiresAt) <= Date.now()) {
359
+ throw new Error('Only an unexpired multipart upload can be resumed. No replacement upload was created.');
360
+ }
361
+ const intent = {
362
+ attachment: { id: file.attachmentId, status: 'pending' }, uploadUrl: null, uploadFields: null,
363
+ uploadTransport: 'multipart', partSize: progress.partSize, partCount: progress.expectedParts,
364
+ };
365
+ // Even a ready session goes through canonical completion to recover its
366
+ // verified attachment and original ledger receipt, never its declaration.
367
+ return uploadDirectIntent(file, intent, protocol);
368
+ }
369
+ function directAttachmentId(intent) {
370
+ const id = intent?.attachment?.id;
371
+ if (typeof id !== 'string' || !id.trim()) {
372
+ throw new Error('The upload intent did not identify a Task attachment.');
373
+ }
374
+ return id;
375
+ }
376
+ /** Shared transport mechanics; each caller retains its own authenticated API door. */
377
+ async function uploadDirectIntent(file, intent, protocol) {
378
+ directAttachmentId(intent);
379
+ if (intent.attachment.status === 'ready')
285
380
  return intent.attachment;
286
- await postFileToStorage({
287
- uploadUrl: intent.uploadUrl,
288
- uploadFields: intent.uploadFields ?? {},
289
- filePath: p.filePath,
290
- size: p.size,
291
- filename: p.filename,
292
- mimeType: p.mimeType,
293
- });
294
- return (0, api_fetch_1.apiFetch)(`${target.path}/complete`, {
295
- method: 'POST',
296
- credential: target.credential,
297
- body: { attachmentId: intent.attachment.id },
381
+ if ((intent.attachment.status !== undefined && intent.attachment.status !== 'pending') ||
382
+ (intent.uploadTransport !== undefined && !['single', 'multipart'].includes(intent.uploadTransport))) {
383
+ throw new Error('The server returned an unusable Task attachment upload intent.');
384
+ }
385
+ try {
386
+ if (intent.uploadTransport === 'multipart') {
387
+ await uploadMultipartFile(file, intent, protocol);
388
+ }
389
+ else {
390
+ if (!intent.uploadUrl || !intent.uploadFields) {
391
+ throw new Error('The pending Task attachment has no usable storage grant; it was not uploaded.');
392
+ }
393
+ await postFileToStorage({ ...file, uploadUrl: intent.uploadUrl, uploadFields: intent.uploadFields });
394
+ }
395
+ const completed = await protocol.complete();
396
+ requireReadyAsset(completed);
397
+ if ((completed.id ?? completed._id) !== intent.attachment.id) {
398
+ throw new Error('Upload completion returned a different Task attachment.');
399
+ }
400
+ return completed;
401
+ }
402
+ catch (error) {
403
+ if (intent.uploadTransport !== 'multipart' || !(error instanceof Error))
404
+ throw error;
405
+ Object.assign(error, { uploadRecovery: { ...protocol.recovery } });
406
+ error.message += ` ${resumeHint(intent.attachment.id)}`;
407
+ throw error;
408
+ }
409
+ }
410
+ /** Resume only the parts this exact upload still owes. Completion remains server-verified. */
411
+ async function uploadMultipartFile(file, intent, protocol) {
412
+ const partSize = intent.partSize;
413
+ if (!Number.isSafeInteger(partSize) || !partSize || partSize < 1) {
414
+ throw new Error('The multipart grant has no valid part size.');
415
+ }
416
+ const partCount = Math.ceil(file.size / partSize);
417
+ if (partCount > 10000 || (intent.partCount !== undefined && intent.partCount !== partCount)) {
418
+ throw new Error('The multipart grant does not match the file size.');
419
+ }
420
+ const lengthOf = (partNumber) => Math.min(partSize, file.size - (partNumber - 1) * partSize);
421
+ let sessionId;
422
+ const inspect = (progress) => {
423
+ if (!progress || typeof progress.sessionId !== 'string' || !progress.sessionId ||
424
+ (sessionId && progress.sessionId !== sessionId) || progress.transport !== 'multipart' ||
425
+ progress.partSize !== partSize || progress.expectedSize !== file.size ||
426
+ progress.expectedParts !== partCount || !Array.isArray(progress.receivedParts) ||
427
+ !['created', 'receiving', 'landed', 'verified', 'scanning', 'canonicalizing', 'ready'].includes(progress.state)) {
428
+ throw new Error('The server returned inconsistent multipart upload progress.');
429
+ }
430
+ sessionId = progress.sessionId;
431
+ const received = new Set();
432
+ let bytes = 0;
433
+ for (const part of progress.receivedParts) {
434
+ if (!Number.isInteger(part.partNumber) || part.partNumber < 1 || part.partNumber > partCount ||
435
+ received.has(part.partNumber) || part.byteLength !== lengthOf(part.partNumber)) {
436
+ throw new Error('The server returned an invalid multipart part receipt.');
437
+ }
438
+ received.add(part.partNumber);
439
+ bytes += part.byteLength;
440
+ }
441
+ if (progress.uploadedBytes !== bytes) {
442
+ throw new Error('The server returned an inconsistent multipart byte count.');
443
+ }
444
+ return received;
445
+ };
446
+ let progress = await protocol.status();
447
+ const received = inspect(progress);
448
+ protocol.recovery.sessionId = progress.sessionId;
449
+ for (let partNumber = 1; partNumber <= partCount; partNumber += 1) {
450
+ if (received.has(partNumber))
451
+ continue;
452
+ if (!['created', 'receiving'].includes(progress.state)) {
453
+ throw new Error('The multipart upload cannot receive its missing parts.');
454
+ }
455
+ const signed = await protocol.sign([partNumber]);
456
+ inspect(signed?.progress);
457
+ if (signed.sessionId !== sessionId || !Array.isArray(signed.parts) || signed.parts.length !== 1 ||
458
+ signed.parts[0].partNumber !== partNumber || typeof signed.parts[0].url !== 'string') {
459
+ throw new Error('The server did not grant the requested multipart part.');
460
+ }
461
+ const receipt = await putFilePartToStorage({
462
+ uploadUrl: signed.parts[0].url, filePath: file.filePath,
463
+ offset: (partNumber - 1) * partSize, byteLength: lengthOf(partNumber),
464
+ });
465
+ const recorded = await protocol.record({ partNumber, ...receipt });
466
+ const landed = inspect(recorded?.progress);
467
+ if (recorded.sessionId !== sessionId || recorded.recorded !== true || !landed.has(partNumber)) {
468
+ throw new Error('The server did not acknowledge the uploaded multipart part.');
469
+ }
470
+ progress = recorded.progress;
471
+ }
472
+ const final = await protocol.status();
473
+ if (inspect(final).size !== partCount || final.uploadedBytes !== file.size ||
474
+ fs.statSync(file.filePath).size !== file.size) {
475
+ throw new Error('The multipart upload is incomplete or the file changed during upload.');
476
+ }
477
+ }
478
+ /** PUT one bounded file range and report only the actual storage receipt and streamed bytes. */
479
+ async function putFilePartToStorage(p) {
480
+ const url = new URL(p.uploadUrl);
481
+ if (!['http:', 'https:'].includes(url.protocol))
482
+ throw new Error('Unsupported storage upload protocol.');
483
+ return new Promise((resolve, reject) => {
484
+ let settled = false;
485
+ let sent = 0;
486
+ let ended = false;
487
+ let etag;
488
+ const stream = fs.createReadStream(p.filePath, { start: p.offset, end: p.offset + p.byteLength - 1 });
489
+ const fail = (message) => {
490
+ if (settled)
491
+ return;
492
+ settled = true;
493
+ stream.destroy();
494
+ req.destroy();
495
+ reject(new Error(message));
496
+ };
497
+ const done = () => {
498
+ if (settled || !ended || !etag)
499
+ return;
500
+ if (sent !== p.byteLength)
501
+ return fail('The file changed while its multipart part was uploaded.');
502
+ settled = true;
503
+ resolve({ etag, byteLength: sent });
504
+ };
505
+ const req = (url.protocol === 'http:' ? http : https).request(url, {
506
+ method: 'PUT', headers: { 'Content-Length': String(p.byteLength) },
507
+ }, (res) => {
508
+ const status = res.statusCode ?? 0;
509
+ const actualEtag = res.headers.etag;
510
+ if (status < 200 || status >= 300)
511
+ return fail(`Storage rejected the multipart part (${status}).`);
512
+ if (typeof actualEtag !== 'string' || !actualEtag.trim() || actualEtag.length > 220) {
513
+ return fail('Storage did not return a valid multipart part receipt.');
514
+ }
515
+ res.on('error', () => fail('Storage multipart response failed.'));
516
+ res.on('aborted', () => fail('Storage multipart response was interrupted.'));
517
+ res.on('end', () => { etag = actualEtag; done(); });
518
+ res.resume();
519
+ });
520
+ req.on('error', () => fail('Storage multipart upload failed.'));
521
+ req.setTimeout(60000, () => fail('Storage multipart upload timed out.'));
522
+ stream.on('data', (chunk) => { sent += chunk.length; });
523
+ stream.on('error', () => fail('The multipart file could not be read.'));
524
+ stream.on('end', () => { ended = true; done(); });
525
+ stream.pipe(req);
298
526
  });
299
527
  }
300
528
  /** sha256 of a file, read as a stream: a 250MB recording never sits in memory. */
@@ -8,7 +8,7 @@
8
8
  * sends a command back to its serial path. The shared transport owns each
9
9
  * request's fresh idempotency key.
10
10
  */
11
- import { type TaskBatchItemResult, type TaskBatchPatchItem } from '@skrr-ai/data-provider';
11
+ import { type TaskBatchItemResult, type TaskBatchPatchItem, type SpaceWriteProposalRef } from '@skrr-ai/data-provider';
12
12
  export type ApiFailure = {
13
13
  message: string;
14
14
  status?: number;
@@ -45,6 +45,18 @@ export type BatchCreatedRow = {
45
45
  status: 'created' | 'exists';
46
46
  externalKey?: string;
47
47
  };
48
+ /**
49
+ * A node the Space write policy held for approval (OSK-17988): nothing was
50
+ * written, and a person approves it from the named bundle. A node that needs a
51
+ * held one as its parent or blocker is held with it (`depends_on_held`): it
52
+ * cannot be placed until the held one exists, and is not a failure.
53
+ */
54
+ export type BatchHeldRow = {
55
+ key: string;
56
+ reason: 'pending_approval' | 'depends_on_held';
57
+ proposal?: SpaceWriteProposalRef;
58
+ externalKey?: string;
59
+ };
48
60
  export type BatchCreateFailure = {
49
61
  key: string;
50
62
  /** Not attempted because its parent was not created (or an earlier chunk failed). */
@@ -65,6 +77,8 @@ export type BatchCreateOutcome = {
65
77
  supported: true;
66
78
  rows: BatchCreatedRow[];
67
79
  failures: BatchCreateFailure[];
80
+ /** Held for approval by the Space write policy, and the nodes waiting on them. */
81
+ held: BatchHeldRow[];
68
82
  /** Requests made to the batch route, retries included. */
69
83
  rounds: number;
70
84
  };
@@ -183,6 +183,8 @@ async function createNodesViaBatch(nodes, mapping, options = {}) {
183
183
  const position = new Map(nodes.map((node, index) => [node.key, index]));
184
184
  const rows = [];
185
185
  const failures = [];
186
+ const held = [];
187
+ const heldKeys = new Set();
186
188
  const chunkSize = data_provider_1.TASK_BATCH_MAX_ITEMS ?? 100;
187
189
  let remaining = nodes.filter((node) => !(node.key in mapping));
188
190
  /** Nodes whose last submission ended without a verdict: they may exist. */
@@ -230,6 +232,7 @@ async function createNodesViaBatch(nodes, mapping, options = {}) {
230
232
  const items = [];
231
233
  const sent = [];
232
234
  const dropped = new Set();
235
+ const heldDropped = new Set();
233
236
  for (const [i, node] of chunk.entries()) {
234
237
  const { parentId: _ignored, ...payload } = node.payload;
235
238
  const item = { ...payload, tempId: `n${i}` };
@@ -270,6 +273,14 @@ async function createNodesViaBatch(nodes, mapping, options = {}) {
270
273
  }
271
274
  }
272
275
  }
276
+ const waitsOnHeld = (node.parentKey && heldKeys.has(node.parentKey)) ||
277
+ (node.blockerKeys ?? []).some((key) => heldKeys.has(key));
278
+ if (unresolved && waitsOnHeld) {
279
+ heldDropped.add(node.key);
280
+ heldKeys.add(node.key);
281
+ held.push({ key: node.key, reason: 'depends_on_held' });
282
+ continue;
283
+ }
273
284
  if (unresolved) {
274
285
  dropped.add(node.key);
275
286
  failures.push({
@@ -291,8 +302,11 @@ async function createNodesViaBatch(nodes, mapping, options = {}) {
291
302
  }
292
303
  // A dropped node is a failure already recorded; it leaves the queue now, or
293
304
  // it would be examined, and recorded, again every round.
294
- remaining = remaining.filter((node) => !dropped.has(node.key));
305
+ remaining = remaining.filter((node) => !dropped.has(node.key) && !heldDropped.has(node.key));
295
306
  const upstreamBroken = dropped.size > 0;
307
+ // Every node of this chunk waits on a held one; the queue already moved on.
308
+ if (items.length === 0 && !upstreamBroken)
309
+ continue;
296
310
  if (items.length === 0) {
297
311
  // Everything in this chunk depended on something that is gone, so nothing
298
312
  // further can be built on it.
@@ -354,6 +368,24 @@ async function createNodesViaBatch(nodes, mapping, options = {}) {
354
368
  const result = results.get(i);
355
369
  const code = result?.error?.code;
356
370
  const status = result?.error?.status ?? 0;
371
+ // Held for approval: nothing was written, and resubmitting would only
372
+ // find the same approval item. Not a failure, and not retried.
373
+ if (result?.status === 'pending_approval') {
374
+ held.push({
375
+ key: node.key,
376
+ reason: 'pending_approval',
377
+ ...(result.proposal ? { proposal: result.proposal } : {}),
378
+ ...(result.externalKey ? { externalKey: result.externalKey } : {}),
379
+ });
380
+ heldKeys.add(node.key);
381
+ continue;
382
+ }
383
+ if (result?.status === 'skipped' &&
384
+ (code === 'BATCH_PARENT_PENDING_APPROVAL' || code === 'BATCH_BLOCKER_PENDING_APPROVAL')) {
385
+ held.push({ key: node.key, reason: 'depends_on_held' });
386
+ heldKeys.add(node.key);
387
+ continue;
388
+ }
357
389
  const confirmed = result?.ok && result.id;
358
390
  if (confirmed) {
359
391
  mapping[node.key] = result.id;
@@ -467,7 +499,8 @@ async function createNodesViaBatch(nodes, mapping, options = {}) {
467
499
  }
468
500
  rows.sort((a, b) => (position.get(a.key) ?? 0) - (position.get(b.key) ?? 0));
469
501
  failures.sort((a, b) => (position.get(a.key) ?? 0) - (position.get(b.key) ?? 0));
470
- return { supported: true, rows, failures, rounds: round };
502
+ held.sort((a, b) => (position.get(a.key) ?? 0) - (position.get(b.key) ?? 0));
503
+ return { supported: true, rows, failures, held, rounds: round };
471
504
  }
472
505
  /**
473
506
  * Apply `updates` in chunks; `results[i]` answers `updates[i]`. A chunk the
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Read-only waiting for a server-authoritative Task Operation.
3
+ *
4
+ * A Task status is intentionally not used as a completion signal here. One
5
+ * Task can have several sequential or competing operations, so only the
6
+ * operation's immutable id and server projection may settle this wait.
7
+ */
8
+ export type TaskOperationProjection = {
9
+ object?: string;
10
+ schemaVersion?: number;
11
+ operationId?: string;
12
+ taskId?: string;
13
+ intent?: string;
14
+ state?: string;
15
+ phase?: string;
16
+ terminal?: boolean;
17
+ outcome?: string;
18
+ updatedAt?: string;
19
+ recovery?: unknown;
20
+ [key: string]: unknown;
21
+ };
22
+ export type TaskOperationCheckpoint = {
23
+ object: 'task_operation_watch_checkpoint';
24
+ schemaVersion: 1;
25
+ observedAt: string;
26
+ taskId: string;
27
+ operationId: string;
28
+ observation: 'observed' | 'timeout';
29
+ changed: boolean;
30
+ terminal: boolean;
31
+ result: 'pending' | 'succeeded' | 'failed' | 'unknown';
32
+ operation: TaskOperationProjection | null;
33
+ };
34
+ export type TaskOperationWatchOptions = {
35
+ taskId: string;
36
+ operationId: string;
37
+ intervalMs: number;
38
+ timeoutMs: number | null;
39
+ read: (taskId: string, operationId: string) => Promise<TaskOperationProjection>;
40
+ onCheckpoint: (checkpoint: TaskOperationCheckpoint) => void | Promise<void>;
41
+ sleep?: (milliseconds: number) => Promise<void>;
42
+ now?: () => number;
43
+ };
44
+ export type TaskOperationWatchResult = {
45
+ terminal: boolean;
46
+ result: 'succeeded' | 'failed';
47
+ operation: TaskOperationProjection;
48
+ };
49
+ /** Parse the same bounded duration grammar used by `tasks watch`. */
50
+ export declare function parseTaskOperationWatchDuration(value: string | undefined): number | null;
51
+ /**
52
+ * Classify only server-declared terminal status or the narrow, stable terminal
53
+ * vocabulary. An absent/malformed/unknown projection never becomes success by
54
+ * inference, and neither does a Task's own workflow status.
55
+ */
56
+ export declare function classifyTaskOperation(operation: TaskOperationProjection | null): {
57
+ terminal: boolean;
58
+ result: 'pending' | 'succeeded' | 'failed' | 'unknown';
59
+ };
60
+ /**
61
+ * Poll one immutable operation until it reaches an authoritative terminal
62
+ * result. Every read is emitted as a checkpoint so JSON consumers can prove
63
+ * liveness even where a server does not yet offer an event stream.
64
+ */
65
+ export declare function watchTaskOperation(options: TaskOperationWatchOptions): Promise<TaskOperationWatchResult>;
66
+ export declare function taskOperationCheckpointLine(checkpoint: TaskOperationCheckpoint): string;