@skrr-ai/cli 0.1.41 → 0.1.43

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 (79) hide show
  1. package/dist/commands/balance/overage.js +4 -3
  2. package/dist/commands/balance/plan.js +10 -6
  3. package/dist/commands/balance/usage/events.js +3 -2
  4. package/dist/commands/balance/usage.js +2 -1
  5. package/dist/commands/browser/install.js +3 -2
  6. package/dist/commands/browser/uninstall.js +3 -2
  7. package/dist/commands/code/install.js +6 -6
  8. package/dist/commands/followups/remind.js +4 -0
  9. package/dist/commands/harnesses/install.d.ts +29 -0
  10. package/dist/commands/harnesses/install.js +153 -25
  11. package/dist/commands/harnesses/installers.d.ts +10 -0
  12. package/dist/commands/harnesses/installers.js +31 -0
  13. package/dist/commands/harnesses/list.js +3 -2
  14. package/dist/commands/skills/import-as-actions.js +6 -15
  15. package/dist/commands/tasks/attachments/set-role.d.ts +24 -0
  16. package/dist/commands/tasks/attachments/set-role.js +60 -0
  17. package/dist/commands/tasks/attachments/upload.d.ts +1 -0
  18. package/dist/commands/tasks/attachments/upload.js +18 -52
  19. package/dist/commands/tasks/comments/add.d.ts +1 -0
  20. package/dist/commands/tasks/comments/add.js +67 -2
  21. package/dist/commands/tasks/complete.d.ts +1 -0
  22. package/dist/commands/tasks/complete.js +69 -1
  23. package/dist/commands/tasks/result/submit.d.ts +3 -0
  24. package/dist/commands/tasks/result/submit.js +78 -5
  25. package/dist/commands/tasks/update.js +4 -2
  26. package/dist/commands/tasks/updates/add.d.ts +1 -0
  27. package/dist/commands/tasks/updates/add.js +63 -0
  28. package/dist/lib/api-fetch.js +7 -2
  29. package/dist/lib/cli-installers.d.ts +63 -4
  30. package/dist/lib/cli-installers.js +98 -8
  31. package/dist/lib/dedicated-machines.js +4 -2
  32. package/dist/lib/file-mime.js +1 -1
  33. package/dist/lib/first-party-harness-agent.js +6 -5
  34. package/dist/lib/first-party-harness-doctor.js +15 -32
  35. package/dist/lib/first-party-harness-managed.d.ts +9 -4
  36. package/dist/lib/first-party-harness-managed.js +13 -10
  37. package/dist/lib/first-party-harness.d.ts +25 -21
  38. package/dist/lib/first-party-harness.js +40 -27
  39. package/dist/lib/followups.d.ts +2 -0
  40. package/dist/lib/harness-provider-input.d.ts +18 -12
  41. package/dist/lib/harness-provider-input.js +18 -12
  42. package/dist/lib/harness-tiers.d.ts +4 -3
  43. package/dist/lib/harness-tiers.js +4 -3
  44. package/dist/lib/html-text.js +25 -0
  45. package/dist/lib/local-skills.d.ts +27 -0
  46. package/dist/lib/local-skills.js +38 -0
  47. package/dist/lib/session-task-endpoints.d.ts +1 -1
  48. package/dist/lib/session-task-endpoints.js +3 -0
  49. package/dist/lib/task-asset-upload.d.ts +84 -0
  50. package/dist/lib/task-asset-upload.js +374 -0
  51. package/dist/lib/task-closure.d.ts +28 -0
  52. package/dist/lib/task-closure.js +37 -0
  53. package/dist/lib/tasks.d.ts +8 -0
  54. package/dist/lib/tasks.js +16 -0
  55. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +113 -39
  56. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +148 -71
  57. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.d.ts +21 -33
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.js +24 -50
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.d.ts +19 -7
  60. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.js +26 -10
  61. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessTrust.d.ts +9 -5
  62. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessTrust.js +9 -5
  63. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  64. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +1 -13
  65. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +113 -39
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +146 -70
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.d.ts +21 -33
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.js +24 -49
  69. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.d.ts +19 -7
  70. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.js +27 -11
  71. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessTrust.d.ts +9 -5
  72. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessTrust.js +9 -5
  73. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  74. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -4
  75. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  76. package/dist/node_modules/@skrr-ai/data-provider/index.js +4025 -3938
  77. package/dist/node_modules/@skrr-ai/data-provider/package.json +1 -1
  78. package/oclif.manifest.json +12853 -12721
  79. package/package.json +2 -2
@@ -0,0 +1,374 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.readTaskForUpload = readTaskForUpload;
37
+ exports.uploadTaskAssetFile = uploadTaskAssetFile;
38
+ exports.attachTaskAssetFiles = attachTaskAssetFiles;
39
+ exports.taskAssetMarkdownRef = taskAssetMarkdownRef;
40
+ exports.sha256OfFile = sha256OfFile;
41
+ exports.postFileToStorage = postFileToStorage;
42
+ /**
43
+ * One Task-asset upload path for every CLI surface — `tasks attachments
44
+ * upload` and the `--attach` flag on updates/result/complete/comments all
45
+ * funnel here so the routing rules live in exactly one place (OSK-9863).
46
+ *
47
+ * ≤25MB or image → buffered multipart (canonicalization needs the bytes)
48
+ * larger non-image → intent → presigned POST to S3 → complete (OSK-9867)
49
+ *
50
+ * Session routing follows the CREDENTIAL, resolved here once per file so a
51
+ * narrowed agent session lands on `task-attachments*` and a human (or a
52
+ * delegated session holding a wide credential) lands on `/api/tasks/:id/*`.
53
+ *
54
+ * Retry safety is server-side: the sha256 IS the asset identity, so a second
55
+ * upload of the same bytes returns the existing ready row instead of
56
+ * duplicating it.
57
+ *
58
+ * The digest this module REPORTS is always the server's. The server
59
+ * canonicalizes images (EXIF strip, orientation, re-encode) before storing
60
+ * them, so the bytes on disk and the bytes stored are routinely different; an
61
+ * evidence reference that named the local hash would pin a version of the
62
+ * file that was never stored (OSK-10120). The local hash is computed only
63
+ * where the protocol needs it — declaring a direct upload, which storage then
64
+ * enforces — and never leaves this module as a claim about the asset.
65
+ */
66
+ const fs = __importStar(require("node:fs"));
67
+ const http = __importStar(require("node:http"));
68
+ const https = __importStar(require("node:https"));
69
+ const path = __importStar(require("node:path"));
70
+ const node_buffer_1 = require("node:buffer");
71
+ const node_crypto_1 = require("node:crypto");
72
+ const data_provider_1 = require("@skrr-ai/data-provider");
73
+ const base_command_1 = require("../base-command");
74
+ const file_mime_1 = require("./file-mime");
75
+ const api_fetch_1 = require("./api-fetch");
76
+ const session_task_endpoints_1 = require("./session-task-endpoints");
77
+ function mb(bytes) {
78
+ return `${Math.floor(bytes / (1024 * 1024))} MB`;
79
+ }
80
+ const SHA256_HEX = /^[0-9a-f]{64}$/i;
81
+ function storedDigest(asset) {
82
+ const value = asset.contentSha256;
83
+ return typeof value === 'string' && SHA256_HEX.test(value) ? value.toLowerCase() : undefined;
84
+ }
85
+ /**
86
+ * Read the Task an upload targets, through the same credential the upload
87
+ * will use. In a session the task comes from the claim, so the read is the
88
+ * bound task and `taskId` is only reconciled against it — a scoped credential
89
+ * refuses to act on any other task before a byte leaves the machine.
90
+ */
91
+ async function readTaskForUpload(taskId) {
92
+ const readPath = await (0, session_task_endpoints_1.sessionScopedTaskPath)('read');
93
+ if (readPath) {
94
+ const bound = await (0, api_fetch_1.apiFetch)(readPath);
95
+ if (!(0, session_task_endpoints_1.requestMatchesBoundTask)(bound, taskId)) {
96
+ throw new Error(`This session is bound to task ${String(bound.identifier ?? bound.id ?? '?')} — a scoped credential cannot attach files to ${taskId}.`);
97
+ }
98
+ return bound;
99
+ }
100
+ return (await data_provider_1.dataService.getTask(taskId, { enrich: false }));
101
+ }
102
+ /**
103
+ * Upload `filePath` as a Task asset. In a session the task comes from the
104
+ * claim — `taskId` is only reconciled against the binding, never sent.
105
+ * Throws a plain Error; callers decide how to surface it.
106
+ */
107
+ async function uploadTaskAssetFile(opts) {
108
+ const { taskId, filePath, role } = opts;
109
+ if (!fs.existsSync(filePath)) {
110
+ throw new Error(`File not found: ${filePath}`);
111
+ }
112
+ const stat = fs.statSync(filePath);
113
+ if (!stat.isFile()) {
114
+ throw new Error(`Not a regular file: ${filePath}`);
115
+ }
116
+ const size = stat.size;
117
+ const filename = path.basename(filePath);
118
+ const mimeType = (0, file_mime_1.mimeTypeForFilename)(filename);
119
+ const isImage = mimeType.startsWith('image/');
120
+ if (isImage && size > data_provider_1.TASK_ATTACHMENT_MAX_BYTES) {
121
+ throw new Error(`${filename} is too large for an image (max ${mb(data_provider_1.TASK_ATTACHMENT_MAX_BYTES)})`);
122
+ }
123
+ if (size > data_provider_1.TASK_DIRECT_UPLOAD_MAX_BYTES) {
124
+ throw new Error(`${filename} exceeds the direct-upload limit (${mb(data_provider_1.TASK_DIRECT_UPLOAD_MAX_BYTES)})`);
125
+ }
126
+ const direct = !isImage && size > data_provider_1.TASK_ATTACHMENT_MAX_BYTES;
127
+ const sessionPath = await (0, session_task_endpoints_1.sessionScopedTaskPath)('attachment');
128
+ let result;
129
+ try {
130
+ if (sessionPath) {
131
+ await readTaskForUpload(taskId);
132
+ result = direct
133
+ ? await sessionDirectUpload({ filePath, size, filename, mimeType, role })
134
+ : await sessionBufferedUpload({ filePath, filename, mimeType, role });
135
+ }
136
+ else {
137
+ result = direct
138
+ ? await genericDirectUpload({ taskId, filePath, size, filename, mimeType, role })
139
+ : await genericBufferedUpload({ taskId, filePath, filename, mimeType, role });
140
+ }
141
+ }
142
+ catch (err) {
143
+ throw withServerReason(err, filename);
144
+ }
145
+ return {
146
+ id: String(result.id ?? result._id),
147
+ digest: storedDigest(result),
148
+ filename,
149
+ mimeType,
150
+ deduplicated: result.deduplicated === true,
151
+ };
152
+ }
153
+ /**
154
+ * Put the server's own reason on a refused upload.
155
+ *
156
+ * The transports keep response bodies out of `message` (a body can carry a
157
+ * stack trace), so a refusal read `HTTP 403 Forbidden — POST …` — and `--attach`
158
+ * prints only the message. Task media refusals are written to be acted on: 422
159
+ * an image whose bytes cannot be decoded, 415 a type the bytes contradict, 413
160
+ * the storage quota, 403 an agent asking for a Run input. The reason and its
161
+ * code replace the transport line; status and body stay on the error, so
162
+ * `handleApiError` still reads them. An error with no stated reason is left
163
+ * exactly as it was.
164
+ */
165
+ function withServerReason(err, filename) {
166
+ if (!(err instanceof Error))
167
+ return err;
168
+ const { code, message } = (0, base_command_1.apiRefusal)(err);
169
+ if (!message)
170
+ return err;
171
+ err.message = `${filename}: ${message}${code ? ` (${code})` : ''}`;
172
+ return err;
173
+ }
174
+ /**
175
+ * Upload every `--attach` file and return the reference objects an Update or
176
+ * Result appends to `evidenceRefs`. Serial: half a dozen files is the
177
+ * realistic case and parallel uploads would race the session rate limiter.
178
+ */
179
+ async function attachTaskAssetFiles(opts) {
180
+ const refs = [];
181
+ for (const filePath of opts.files) {
182
+ const asset = await uploadTaskAssetFile({
183
+ taskId: opts.taskId,
184
+ filePath,
185
+ role: opts.role,
186
+ });
187
+ refs.push({
188
+ kind: 'task_attachment',
189
+ id: asset.id,
190
+ ...(asset.digest ? { digest: `sha256:${asset.digest}` } : {}),
191
+ });
192
+ }
193
+ return refs;
194
+ }
195
+ /**
196
+ * The markdown image a comment body carries for an asset. The alt text is the
197
+ * filename, so its brackets are escaped: `shot [final].png` would otherwise
198
+ * close the alt early and break the reference.
199
+ */
200
+ function taskAssetMarkdownRef(asset) {
201
+ const alt = asset.filename.replace(/[\\[\]]/g, (ch) => `\\${ch}`);
202
+ return `![${alt}](task-asset:${asset.id})`;
203
+ }
204
+ /**
205
+ * The buffered path holds the file in memory by design: it is capped at the
206
+ * buffered limit, and the server needs the whole body to canonicalize it.
207
+ */
208
+ function fileFormData(filePath, filename, mimeType, role) {
209
+ const fd = new FormData();
210
+ fd.append('file', new node_buffer_1.Blob([fs.readFileSync(filePath)], { type: mimeType }), filename);
211
+ if (role)
212
+ fd.append('role', role);
213
+ return fd;
214
+ }
215
+ async function genericBufferedUpload(p) {
216
+ return data_provider_1.dataService.uploadTaskAttachment({
217
+ taskId: p.taskId,
218
+ formData: fileFormData(p.filePath, p.filename, p.mimeType, p.role),
219
+ });
220
+ }
221
+ async function sessionBufferedUpload(p) {
222
+ return (0, api_fetch_1.apiFetch)((await (0, session_task_endpoints_1.sessionScopedTaskPath)('attachment')), {
223
+ method: 'POST',
224
+ body: fileFormData(p.filePath, p.filename, p.mimeType, p.role),
225
+ });
226
+ }
227
+ async function genericDirectUpload(p) {
228
+ const intent = await data_provider_1.dataService.createTaskAttachmentUploadIntent({
229
+ taskId: p.taskId,
230
+ filename: p.filename,
231
+ mimeType: p.mimeType,
232
+ sizeBytes: p.size,
233
+ sha256: await sha256OfFile(p.filePath),
234
+ role: p.role,
235
+ });
236
+ if (!intent.uploadUrl)
237
+ return intent.attachment;
238
+ await postFileToStorage({
239
+ uploadUrl: intent.uploadUrl,
240
+ uploadFields: intent.uploadFields ?? {},
241
+ filePath: p.filePath,
242
+ size: p.size,
243
+ filename: p.filename,
244
+ mimeType: p.mimeType,
245
+ });
246
+ return data_provider_1.dataService.completeTaskAttachmentUpload({
247
+ taskId: p.taskId,
248
+ attachmentId: intent.attachment.id,
249
+ });
250
+ }
251
+ async function sessionDirectUpload(p) {
252
+ const intent = await (0, api_fetch_1.apiFetch)((await (0, session_task_endpoints_1.sessionScopedTaskPath)('attachmentIntent')), {
253
+ method: 'POST',
254
+ body: {
255
+ filename: p.filename,
256
+ mimeType: p.mimeType,
257
+ sizeBytes: p.size,
258
+ sha256: await sha256OfFile(p.filePath),
259
+ ...(p.role ? { role: p.role } : {}),
260
+ },
261
+ });
262
+ if (!intent.uploadUrl)
263
+ return intent.attachment;
264
+ await postFileToStorage({
265
+ uploadUrl: intent.uploadUrl,
266
+ uploadFields: intent.uploadFields ?? {},
267
+ filePath: p.filePath,
268
+ size: p.size,
269
+ filename: p.filename,
270
+ mimeType: p.mimeType,
271
+ });
272
+ return (0, api_fetch_1.apiFetch)((await (0, session_task_endpoints_1.sessionScopedTaskPath)('attachmentComplete')), {
273
+ method: 'POST',
274
+ body: { attachmentId: intent.attachment.id },
275
+ });
276
+ }
277
+ /** sha256 of a file, read as a stream: a 250MB recording never sits in memory. */
278
+ async function sha256OfFile(filePath) {
279
+ const hash = (0, node_crypto_1.createHash)('sha256');
280
+ for await (const chunk of fs.createReadStream(filePath)) {
281
+ hash.update(chunk);
282
+ }
283
+ return hash.digest('hex');
284
+ }
285
+ /** A form-data header value: no CR/LF, and quotes percent-encoded. */
286
+ function quotedHeaderValue(value) {
287
+ return value.replace(/[\r\n]/g, ' ').replace(/"/g, '%22');
288
+ }
289
+ /**
290
+ * POST the file to the presigned URL as multipart/form-data, streamed from
291
+ * disk.
292
+ *
293
+ * Built by hand rather than with `FormData` + `fetch` for two reasons, both
294
+ * about large files. `FormData` needs a `Blob`, which here means the whole
295
+ * file in memory — the double buffering this replaced read up to 250MB and
296
+ * then copied it again. And storage refuses a POST without a Content-Length
297
+ * (it does not take chunked uploads), which a streamed `fetch` body cannot
298
+ * promise; the exact length is known here, because every part except the file
299
+ * is a small in-memory string and the file's size came from `stat`.
300
+ *
301
+ * `file` goes LAST — S3 presigned POST evaluates fields in order and ignores
302
+ * anything after it. The fields carry `x-amz-checksum-sha256`, so storage
303
+ * itself rejects bytes that do not match the digest the intent declared,
304
+ * including a file that changed between hashing and upload.
305
+ */
306
+ async function postFileToStorage(p) {
307
+ const boundary = `----skrr-upload-${(0, node_crypto_1.randomBytes)(16).toString('hex')}`;
308
+ const fieldParts = Object.entries(p.uploadFields).map(([name, value]) => `--${boundary}\r\nContent-Disposition: form-data; name="${quotedHeaderValue(name)}"\r\n\r\n${value}\r\n`);
309
+ const head = Buffer.from(fieldParts.join('') +
310
+ `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${quotedHeaderValue(p.filename)}"\r\nContent-Type: ${quotedHeaderValue(p.mimeType)}\r\n\r\n`, 'utf8');
311
+ const tail = Buffer.from(`\r\n--${boundary}--\r\n`, 'utf8');
312
+ const url = new URL(p.uploadUrl);
313
+ const transport = url.protocol === 'http:' ? http : https;
314
+ await new Promise((resolve, reject) => {
315
+ let settled = false;
316
+ const fail = (err) => {
317
+ if (settled)
318
+ return;
319
+ settled = true;
320
+ reject(err);
321
+ };
322
+ const req = transport.request(url, {
323
+ method: 'POST',
324
+ headers: {
325
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
326
+ 'Content-Length': String(head.length + p.size + tail.length),
327
+ },
328
+ }, (res) => {
329
+ let body = '';
330
+ res.setEncoding('utf8');
331
+ res.on('data', (chunk) => {
332
+ if (body.length < 4096)
333
+ body += chunk;
334
+ });
335
+ res.on('error', fail);
336
+ res.on('end', () => {
337
+ const status = res.statusCode ?? 0;
338
+ if (status >= 200 && status < 300) {
339
+ if (!settled) {
340
+ settled = true;
341
+ resolve();
342
+ }
343
+ return;
344
+ }
345
+ // S3 names the refusal in XML (`<Code>BadDigest</Code>`,
346
+ // `EntityTooLarge`, `AccessDenied` once the POST policy expired).
347
+ // The code is the only part a person can act on.
348
+ const code = /<Code>([^<]+)<\/Code>/.exec(body)?.[1];
349
+ fail(new Error(`Storage rejected the upload (${status}${code ? ` ${code}` : ''})`));
350
+ });
351
+ });
352
+ req.on('error', fail);
353
+ req.write(head);
354
+ let sent = 0;
355
+ const stream = fs.createReadStream(p.filePath);
356
+ stream.on('data', (chunk) => {
357
+ sent += chunk.length;
358
+ });
359
+ stream.on('error', (err) => {
360
+ req.destroy(err);
361
+ fail(err);
362
+ });
363
+ stream.on('end', () => {
364
+ if (sent !== p.size) {
365
+ const err = new Error(`${p.filename} changed while it was being uploaded`);
366
+ req.destroy(err);
367
+ fail(err);
368
+ return;
369
+ }
370
+ req.end(tail);
371
+ });
372
+ stream.pipe(req, { end: false });
373
+ });
374
+ }
@@ -1,4 +1,19 @@
1
1
  import type { TaskClosureEvaluation } from '@skrr-ai/data-provider';
2
+ /**
3
+ * Is this Task SETTLED — closed in a way that refuses a new Result until it is
4
+ * reopened (`TASK_REOPEN_REQUIRED`)?
5
+ *
6
+ * Read the same way the server reads it: the immutable `statusType` when the row
7
+ * carries a known one, the legacy status id otherwise, and the `settled` band
8
+ * from the shared vocabulary rather than a column name. A command asks this
9
+ * BEFORE doing something the refusal would strand — uploading `--attach` files
10
+ * for a Result the server is about to refuse — and never instead of the
11
+ * server's own check, which stays the authority.
12
+ */
13
+ export declare function taskIsSettled(task: {
14
+ statusType?: unknown;
15
+ status?: unknown;
16
+ } | null | undefined): boolean;
2
17
  /**
3
18
  * Render "where does this Task actually stand" for every surface that should
4
19
  * say it.
@@ -28,6 +43,19 @@ export type ClosureLike = TaskClosureEvaluation | {
28
43
  * the whole point is to be able to act without a second lookup.
29
44
  */
30
45
  export declare function closureReport(closure: ClosureLike): string[];
46
+ /**
47
+ * The ways forward when a closed Task refuses a Result (`TASK_REOPEN_REQUIRED`).
48
+ *
49
+ * Both writers reach this refusal, `tasks result submit` and `tasks complete`,
50
+ * so both print from here. The first version sat in `result submit` alone and
51
+ * offered `tasks complete` as a way around it, which the same refusal stops:
52
+ * every step named here has to work on a closed Task (OSK-10031).
53
+ */
54
+ export declare function reopenRequiredLines({ bin, task, command, }: {
55
+ bin: string;
56
+ task: string;
57
+ command: 'result submit' | 'complete';
58
+ }): string[];
31
59
  /** `met (agent)` vs `met (person)` — shared with the web through data-provider. */
32
60
  export { describeAssessorKind as describeAssessor } from '@skrr-ai/data-provider';
33
61
  export { describeClosure as closureSnapshotLines, type TaskClosureSnapshot as ClosureSnapshot, } from '@skrr-ai/data-provider';
@@ -1,8 +1,29 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.closureSnapshotLines = exports.describeAssessor = void 0;
4
+ exports.taskIsSettled = taskIsSettled;
4
5
  exports.closureReport = closureReport;
6
+ exports.reopenRequiredLines = reopenRequiredLines;
5
7
  const data_provider_1 = require("@skrr-ai/data-provider");
8
+ /**
9
+ * Is this Task SETTLED — closed in a way that refuses a new Result until it is
10
+ * reopened (`TASK_REOPEN_REQUIRED`)?
11
+ *
12
+ * Read the same way the server reads it: the immutable `statusType` when the row
13
+ * carries a known one, the legacy status id otherwise, and the `settled` band
14
+ * from the shared vocabulary rather than a column name. A command asks this
15
+ * BEFORE doing something the refusal would strand — uploading `--attach` files
16
+ * for a Result the server is about to refuse — and never instead of the
17
+ * server's own check, which stays the authority.
18
+ */
19
+ function taskIsSettled(task) {
20
+ if (!task)
21
+ return false;
22
+ const type = (0, data_provider_1.isTaskWorkflowStateType)(task.statusType)
23
+ ? task.statusType
24
+ : data_provider_1.LEGACY_STATUS_TYPE_BY_ID[String(task.status ?? '')];
25
+ return Boolean(type) && data_provider_1.TASK_WORKFLOW_TYPE_BANDS.settled.includes(type);
26
+ }
6
27
  function isUnavailable(closure) {
7
28
  return Boolean(closure && closure.unavailable);
8
29
  }
@@ -82,6 +103,22 @@ function closureReport(closure) {
82
103
  : 'Closing anyway is allowed — all of this is recorded on resolution.closure.');
83
104
  return lines;
84
105
  }
106
+ /**
107
+ * The ways forward when a closed Task refuses a Result (`TASK_REOPEN_REQUIRED`).
108
+ *
109
+ * Both writers reach this refusal, `tasks result submit` and `tasks complete`,
110
+ * so both print from here. The first version sat in `result submit` alone and
111
+ * offered `tasks complete` as a way around it, which the same refusal stops:
112
+ * every step named here has to work on a closed Task (OSK-10031).
113
+ */
114
+ function reopenRequiredLines({ bin, task, command, }) {
115
+ return [
116
+ `→ Reopen it: ${bin} tasks move ${task} --status in_progress`,
117
+ command === 'complete'
118
+ ? '→ Then run this command again.'
119
+ : `→ Then submit again, or submit and close in one step: ${bin} tasks complete ${task} --summary "…"`,
120
+ ];
121
+ }
85
122
  /** `met (agent)` vs `met (person)` — shared with the web through data-provider. */
86
123
  var data_provider_2 = require("@skrr-ai/data-provider");
87
124
  Object.defineProperty(exports, "describeAssessor", { enumerable: true, get: function () { return data_provider_2.describeAssessorKind; } });
@@ -1,4 +1,12 @@
1
1
  import type { ParsedDeliverableCoordinate, ParsedExpectationSpec } from '@skrr-ai/data-provider';
2
+ /**
3
+ * `--goal` on `tasks update`. `none`, `null` and `clear` detach the goal: PATCH
4
+ * sends `goalId: null`. Anything else names a goal. `clear` is accepted because
5
+ * this same command documents it for `--expect`, so it is the spelling a user
6
+ * reaches for — and it used to be stored as a goal id (OSK-10207).
7
+ * Undefined means the flag was not passed.
8
+ */
9
+ export declare function resolveTaskGoalFlag(value: string | undefined): string | null | undefined;
2
10
  export declare const TASK_PRIORITIES: readonly ["low", "medium", "high", "critical"];
3
11
  /**
4
12
  * The kinds `tasks events append --kind` will accept.
package/dist/lib/tasks.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RECOGNIZED_TASK_VERDICTS = exports.DELIVERABLE_LIFECYCLE_VALUES = exports.EXPECT_FLAG_HELP = exports.TASK_WORK_EVENT_KINDS = exports.TASK_PRIORITIES = void 0;
4
+ exports.resolveTaskGoalFlag = resolveTaskGoalFlag;
4
5
  exports.releasePinRepairGuard = releasePinRepairGuard;
5
6
  exports.taskUpdateMutationGuard = taskUpdateMutationGuard;
6
7
  exports.executionIsolationFromWorktreeFlag = executionIsolationFromWorktreeFlag;
@@ -36,6 +37,21 @@ exports.verdictIsRecognized = verdictIsRecognized;
36
37
  const data_provider_1 = require("@skrr-ai/data-provider");
37
38
  const prompt_1 = require("./prompt");
38
39
  const projects_1 = require("./projects");
40
+ /**
41
+ * `--goal` on `tasks update`. `none`, `null` and `clear` detach the goal: PATCH
42
+ * sends `goalId: null`. Anything else names a goal. `clear` is accepted because
43
+ * this same command documents it for `--expect`, so it is the spelling a user
44
+ * reaches for — and it used to be stored as a goal id (OSK-10207).
45
+ * Undefined means the flag was not passed.
46
+ */
47
+ function resolveTaskGoalFlag(value) {
48
+ if (value === undefined)
49
+ return undefined;
50
+ const v = value.trim();
51
+ if (!v || ['none', 'null', 'clear'].includes(v.toLowerCase()))
52
+ return null;
53
+ return v;
54
+ }
39
55
  exports.TASK_PRIORITIES = ['low', 'medium', 'high', 'critical'];
40
56
  const DESCRIPTION_FORMATS = ['html', 'markdown', 'plain'];
41
57
  /**