@skrr-ai/cli 0.1.23 → 0.1.24

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 (67) hide show
  1. package/bin/run.js +54 -1
  2. package/dist/base-command.js +74 -1
  3. package/dist/commands/harnesses/leases/list.js +1 -1
  4. package/dist/commands/harnesses/products.js +1 -0
  5. package/dist/commands/machines/dedicated/create.d.ts +18 -0
  6. package/dist/commands/machines/dedicated/create.js +90 -0
  7. package/dist/commands/machines/dedicated/destroy.d.ts +14 -0
  8. package/dist/commands/machines/dedicated/destroy.js +56 -0
  9. package/dist/commands/machines/dedicated/grow.d.ts +13 -0
  10. package/dist/commands/machines/dedicated/grow.js +46 -0
  11. package/dist/commands/machines/dedicated/health-check.d.ts +12 -0
  12. package/dist/commands/machines/dedicated/health-check.js +42 -0
  13. package/dist/commands/machines/dedicated/incident-close.d.ts +12 -0
  14. package/dist/commands/machines/dedicated/incident-close.js +42 -0
  15. package/dist/commands/machines/dedicated/index.d.ts +6 -0
  16. package/dist/commands/machines/dedicated/index.js +39 -0
  17. package/dist/commands/machines/dedicated/list.d.ts +11 -0
  18. package/dist/commands/machines/dedicated/list.js +58 -0
  19. package/dist/commands/machines/dedicated/restart.d.ts +12 -0
  20. package/dist/commands/machines/dedicated/restart.js +42 -0
  21. package/dist/commands/machines/dedicated/show.d.ts +12 -0
  22. package/dist/commands/machines/dedicated/show.js +58 -0
  23. package/dist/commands/machines/dedicated/snapshot.d.ts +12 -0
  24. package/dist/commands/machines/dedicated/snapshot.js +42 -0
  25. package/dist/commands/machines/dedicated/start.d.ts +12 -0
  26. package/dist/commands/machines/dedicated/start.js +42 -0
  27. package/dist/commands/machines/dedicated/stop.d.ts +12 -0
  28. package/dist/commands/machines/dedicated/stop.js +42 -0
  29. package/dist/commands/machines/hosted/index.js +4 -1
  30. package/dist/commands/spaces/update.js +16 -12
  31. package/dist/commands/spaces/work-sync/reconcile.d.ts +38 -0
  32. package/dist/commands/spaces/work-sync/reconcile.js +90 -0
  33. package/dist/commands/spaces/work-sync/telemetry.d.ts +29 -0
  34. package/dist/commands/spaces/work-sync/telemetry.js +96 -0
  35. package/dist/commands/tasks/activity.js +5 -1
  36. package/dist/commands/tasks/complete.d.ts +5 -1
  37. package/dist/commands/tasks/complete.js +17 -3
  38. package/dist/commands/tasks/create.d.ts +1 -0
  39. package/dist/commands/tasks/create.js +7 -0
  40. package/dist/commands/tasks/deliverable/add.js +22 -4
  41. package/dist/commands/tasks/deliverable/list.js +23 -2
  42. package/dist/commands/tasks/events/append.js +31 -3
  43. package/dist/commands/tasks/expectations/assess.d.ts +1 -1
  44. package/dist/commands/tasks/expectations/assess.js +33 -7
  45. package/dist/commands/tasks/expectations.js +110 -10
  46. package/dist/commands/tasks/output.d.ts +0 -1
  47. package/dist/commands/tasks/output.js +21 -1
  48. package/dist/commands/tasks/report.js +3 -1
  49. package/dist/commands/tasks/result/show.js +20 -2
  50. package/dist/commands/tasks/resume/save.js +11 -1
  51. package/dist/commands/tasks/self-schedule.js +7 -4
  52. package/dist/commands/tasks/show.js +19 -0
  53. package/dist/commands/tasks/timeline.js +9 -1
  54. package/dist/commands/tasks/updates/add.js +23 -2
  55. package/dist/lib/dedicated-machines.d.ts +66 -0
  56. package/dist/lib/dedicated-machines.js +161 -0
  57. package/dist/lib/harnesses.d.ts +5 -0
  58. package/dist/lib/harnesses.js +5 -2
  59. package/dist/lib/task-legacy.d.ts +24 -0
  60. package/dist/lib/task-legacy.js +48 -0
  61. package/dist/lib/tasks.d.ts +32 -0
  62. package/dist/lib/tasks.js +47 -1
  63. package/dist/lib/work-sync.d.ts +70 -0
  64. package/dist/lib/work-sync.js +29 -0
  65. package/dist/node_modules/@skrr-ai/data-provider/index.js +2079 -2055
  66. package/oclif.manifest.json +12229 -11213
  67. package/package.json +1 -1
@@ -4,6 +4,7 @@ const core_1 = require("@oclif/core");
4
4
  const data_provider_1 = require("@skrr-ai/data-provider");
5
5
  const base_command_1 = require("../../../base-command");
6
6
  const task_resolver_1 = require("../../../lib/task-resolver");
7
+ const task_legacy_1 = require("../../../lib/task-legacy");
7
8
  class TasksResultShow extends base_command_1.BaseCommand {
8
9
  static description = 'Show the current Task Result and its exact Review';
9
10
  static args = {
@@ -29,12 +30,29 @@ class TasksResultShow extends base_command_1.BaseCommand {
29
30
  return this.log(JSON.stringify(result, null, 2));
30
31
  const current = result.results.find((row) => row.id === result.currentResultId);
31
32
  if (!current)
32
- return this.log('No Result submitted.');
33
+ return this.log(await (0, task_legacy_1.emptyCanonicalView)(task.id, 'Result'));
33
34
  const review = result.reviews.find((row) => row.resultId === current.id);
34
35
  this.log(`Result v${current.version}\n${current.summary}`);
35
36
  if (current.conclusion)
36
37
  this.log(`Conclusion (${current.conclusion.type}): ${JSON.stringify(current.conclusion.value)}`);
37
- this.log(`Review: ${review ? review.decision : 'awaiting review'}`);
38
+ // Only "awaiting" while a review can still be recorded. A closed Task
39
+ // refuses one outright (`TASK_REOPEN_REQUIRED`), so the old wording named a
40
+ // state nothing could ever leave — and a reader could not tell it from a
41
+ // review that is genuinely pending. The extra read happens only on the
42
+ // no-review path, where the distinction is the whole point.
43
+ let reviewLine = 'awaiting review';
44
+ if (review) {
45
+ reviewLine = review.decision;
46
+ }
47
+ else {
48
+ const full = (await data_provider_1.dataService
49
+ .getTask(task.id, { enrich: false })
50
+ .catch(() => undefined));
51
+ if (['completed', 'cancelled'].includes(String(full?.statusType ?? ''))) {
52
+ reviewLine = 'none recorded — the Task is closed';
53
+ }
54
+ }
55
+ this.log(`Review: ${reviewLine}`);
38
56
  if (flags.history && result.results.length > 1) {
39
57
  this.log(`Superseded versions: ${result.results
40
58
  .slice(1)
@@ -15,7 +15,17 @@ class TasksResumeSave extends base_command_1.BaseCommand {
15
15
  static flags = {
16
16
  task: core_1.Flags.string({ description: 'Task id; overrides runtime context' }),
17
17
  'run-id': core_1.Flags.string({ required: true, description: 'Canonical Task Run' }),
18
- snapshot: core_1.Flags.string({ required: true, description: 'Resume Point JSON snapshot' }),
18
+ snapshot: core_1.Flags.string({
19
+ required: true,
20
+ description: 'Resume Point JSON snapshot. ' +
21
+ 'Every field is optional. `completed`, `nextActions` and `currentFocus` accept either a ' +
22
+ 'plain sentence or {summary,sourceRefs} — the object form is canonical and a bare string ' +
23
+ 'is lifted into it, so the same sentence you put in an Update’s nextActions works here ' +
24
+ 'unchanged. Shape: {completed:[], currentFocus, nextActions:[], ' +
25
+ 'openQuestions:[{question,sourceRef?}], relevantUpdateIds:[], evidenceRefs:[], ' +
26
+ 'deliverableRefs:[], childHeads:[{runId,resumePointId,status}], workspaceStateRef}. ' +
27
+ 'Never put a credential, token or lease in a snapshot — the server refuses one.',
28
+ }),
19
29
  'spec-version': core_1.Flags.integer({ description: 'Task specification version' }),
20
30
  'idempotency-key': core_1.Flags.string({ description: 'Stable retry key' }),
21
31
  json: core_1.Flags.boolean({ description: 'Output JSON' }),
@@ -31,10 +31,13 @@ class TasksSelfSchedule extends base_command_1.BaseCommand {
31
31
  at: core_1.Flags.string({ description: 'Absolute return time (ISO-8601 UTC)' }),
32
32
  in: core_1.Flags.string({ description: 'Relative return time such as 90m, 2h, 3d' }),
33
33
  note: core_1.Flags.string({
34
- // Was "handed to the agent as the return prompt", which is not true:
35
- // `task_execute` builds the run's prompt from the Task, so this string is
36
- // recorded and never read by the run. See `TaskScheduler/selfSchedule.js`.
37
- description: 'Why you are coming back recorded on the return for you to read back',
34
+ // This DOES reach the run now. It is read by `buildTaskExecutionPrompt`,
35
+ // the one place every dispatch path assembles a task prompt, and rendered
36
+ // as `### Why You Parked This Task` alongside the instant it was parked
37
+ // for. For a while it was recorded and never delivered, and the help said
38
+ // so; both halves are fixed rather than only the sentence.
39
+ description: 'Why you are coming back. Delivered to the agent on the return as its own ' +
40
+ 'prompt section, and readable any time with --show',
38
41
  }),
39
42
  'max-per-day': core_1.Flags.integer({
40
43
  description: 'Cap on self-initiated returns per day (1-24, default 4)',
@@ -262,6 +262,25 @@ class TasksShow extends base_command_1.BaseCommand {
262
262
  this.log(`Created: ${formatTime(t.createdAt)}`);
263
263
  if (t.updatedAt)
264
264
  this.log(`Updated: ${formatTime(t.updatedAt)}`);
265
+ /**
266
+ * Say which Task model this row belongs to, on the one surface everybody
267
+ * reads first.
268
+ *
269
+ * Canonical Task Model v1 deliberately migrated nothing, so a Task created
270
+ * before it is a second class of object with visibly different behaviour —
271
+ * no Updates, no Result, no Activity, no timeline. The only marker was
272
+ * `specVersion` being ABSENT under `--json`, and absence-as-a-marker is not
273
+ * something a reader can notice: you have to already know the field exists
274
+ * to observe that it does not. So `tasks show` said nothing, and the user
275
+ * found out by watching four other commands report emptiness about a Task
276
+ * with years of history.
277
+ *
278
+ * Printed only for the legacy class. A canonical Task needs no line saying
279
+ * it is ordinary.
280
+ */
281
+ if (t.specVersion === undefined || t.specVersion === null) {
282
+ this.log('Model: predates the canonical Task model — no Result, Updates, Activity or timeline');
283
+ }
265
284
  if (t.description) {
266
285
  this.log('');
267
286
  this.log('Description:');
@@ -5,6 +5,7 @@ const core_1 = require("@oclif/core");
5
5
  const data_provider_1 = require("@skrr-ai/data-provider");
6
6
  const base_command_1 = require("../../base-command");
7
7
  const format_1 = require("../../lib/format");
8
+ const task_legacy_1 = require("../../lib/task-legacy");
8
9
  /**
9
10
  * `<action> (<k=v ...>)`, or just the preview when there is nothing to add.
10
11
  *
@@ -49,7 +50,14 @@ class TasksTimeline extends base_command_1.BaseCommand {
49
50
  this.log(JSON.stringify(result, null, 2));
50
51
  return;
51
52
  }
52
- (0, format_1.renderTable)((result.data ?? []).map((item) => ({
53
+ const rows = result.data ?? [];
54
+ if (rows.length === 0) {
55
+ // `renderTable([])` prints NOTHING — no header, no empty state, exit 0 —
56
+ // which is indistinguishable from a broken command. Say what happened.
57
+ this.log(await (0, task_legacy_1.emptyCanonicalView)(args.id, 'timeline entries'));
58
+ return;
59
+ }
60
+ (0, format_1.renderTable)(rows.map((item) => ({
53
61
  time: String(item.occurredAt ?? '-')
54
62
  .replace('T', ' ')
55
63
  .slice(0, 19),
@@ -21,6 +21,9 @@ class TasksUpdatesAdd extends base_command_1.BaseCommand {
21
21
  static examples = [
22
22
  '<%= config.bin %> tasks updates add <task> --type progress --summary "Migration review complete" --data \'{"completedSinceLast":["Checked migrations 001-014"],"nextActions":["Review authorization"]}\'',
23
23
  '<%= config.bin %> tasks updates add <task> --type decision --summary "Use a resource object" --data \'{"decision":"Use WorkResource","rationale":"Identity must survive URL changes"}\'',
24
+ '<%= config.bin %> tasks updates add <task> --type finding --summary "Cross-tenant leak" --data \'{"finding":"Space ACL is not applied on the list path","impact":"Any member can read another space","severity":"high","evidenceRefs":[]}\'',
25
+ '<%= config.bin %> tasks updates add <task> --type blocker --summary "Needs a prod credential" --data \'{"cause":"No Atlas API key in this runtime","requiredAction":"Provision a read-only key","nextOwner":{"kind":"human","userId":"<user-id>"}}\'',
26
+ '<%= config.bin %> tasks updates add <task> --type verification --summary "Repro is gone" --data \'{"claim":"The 409 no longer reproduces","result":"passed","evidenceRefs":[]}\'',
24
27
  ];
25
28
  static args = {
26
29
  task: core_1.Args.string({ required: false, ignoreStdin: true, description: 'Task id or ref' }),
@@ -33,9 +36,27 @@ class TasksUpdatesAdd extends base_command_1.BaseCommand {
33
36
  description: 'What changed in the shared understanding of the Task',
34
37
  }),
35
38
  summary: core_1.Flags.string({ required: true, description: 'One sentence saying what became true' }),
36
- data: core_1.Flags.string({ required: true, description: 'Kind-specific JSON payload' }),
39
+ data: core_1.Flags.string({
40
+ required: true,
41
+ description: 'Kind-specific JSON payload. The server enforces one shape per --type, and the array ' +
42
+ 'fields are required even when empty. ' +
43
+ 'progress {completedSinceLast:[],nextActions:[],currentFocus?} · ' +
44
+ 'finding {finding,evidenceRefs:[],impact?,severity?} · ' +
45
+ 'decision {decision,rationale,alternativesConsidered?} · ' +
46
+ 'blocker {cause,requiredAction,nextOwner} · ' +
47
+ 'verification {claim,result:passed|failed|inconclusive,evidenceRefs:[]}. ' +
48
+ 'nextOwner is a principal object like {"kind":"human","userId":"..."}, not a name. ' +
49
+ "Note --reference sets the Update's own references, NOT data.evidenceRefs.",
50
+ }),
37
51
  resume: core_1.Flags.string({
38
- description: 'JSON recovery snapshot; requires --run-id and is committed with the Update',
52
+ description: 'JSON recovery snapshot; requires --run-id and is committed with the Update. ' +
53
+ 'Every field is optional. `completed`, `nextActions` and `currentFocus` accept either a ' +
54
+ 'plain sentence or {summary,sourceRefs} — the object form is canonical and a bare string ' +
55
+ 'is lifted into it, so the same sentence you put in an Update’s nextActions works here ' +
56
+ 'unchanged. Shape: {completed:[], currentFocus, nextActions:[], ' +
57
+ 'openQuestions:[{question,sourceRef?}], relevantUpdateIds:[], evidenceRefs:[], ' +
58
+ 'deliverableRefs:[], childHeads:[{runId,resumePointId,status}], workspaceStateRef}. ' +
59
+ 'Never put a credential, token or lease in a snapshot — the server refuses one.',
39
60
  }),
40
61
  'run-id': core_1.Flags.string({ description: 'Canonical Task Run that performed this work' }),
41
62
  'spec-version': core_1.Flags.integer({
@@ -0,0 +1,66 @@
1
+ import { type HarnessLease } from './harnesses';
2
+ export type DedicatedLeaseAction = 'start' | 'stop' | 'restart' | 'snapshot' | 'health-check';
3
+ export declare const DEDICATED_LEASE_ACTIONS: DedicatedLeaseAction[];
4
+ export interface DedicatedRuntimeScope {
5
+ workspaceId?: string;
6
+ }
7
+ export interface CreateDedicatedRuntimeInput extends DedicatedRuntimeScope {
8
+ requestId: string;
9
+ displayName: string;
10
+ sizePreset: string;
11
+ regionClass?: string;
12
+ storageGb?: number;
13
+ retentionClass?: string;
14
+ backupProfile?: 'crash-consistent' | 'application-consistent';
15
+ monthlySpendingLimitCents: number;
16
+ repositoryRef?: string;
17
+ }
18
+ export interface DedicatedRuntimeResponse {
19
+ lease?: HarnessLease;
20
+ }
21
+ /**
22
+ * Flags in, request body out — and the point of it being a function is the
23
+ * OMISSION.
24
+ *
25
+ * A field the user did not set must be ABSENT from the body, not present as
26
+ * `undefined`, because the server reaches its own default through
27
+ * `input.x || <default>`. Restating those defaults on the client looks
28
+ * harmless and is not: `--storage-gb` shipped with a client default of 100
29
+ * against a product default of 50, so a user who never chose a disk size was
30
+ * billed for twice the documented one and `workspaceDefaultGb` was unreachable
31
+ * through the CLI. The three neighbouring flags happened to agree, which only
32
+ * means the same bug was invisible in them.
33
+ */
34
+ export declare function dedicatedRuntimeCreateInput(input: {
35
+ requestId: string;
36
+ displayName: string;
37
+ sizePreset: string;
38
+ monthlySpendingLimitCents: number;
39
+ regionClass?: string;
40
+ storageGb?: number;
41
+ retentionClass?: string;
42
+ backupProfile?: string;
43
+ workspaceId?: string;
44
+ }): CreateDedicatedRuntimeInput;
45
+ export declare function createDedicatedRuntime(input: CreateDedicatedRuntimeInput): Promise<DedicatedRuntimeResponse>;
46
+ export declare function listDedicatedRuntimes(options?: {
47
+ limit?: number;
48
+ state?: string;
49
+ }): Promise<{
50
+ leases?: HarnessLease[];
51
+ }>;
52
+ export declare function getDedicatedRuntime(leaseId: string): Promise<DedicatedRuntimeResponse>;
53
+ export declare function performDedicatedLeaseAction(leaseId: string, action: DedicatedLeaseAction): Promise<DedicatedRuntimeResponse>;
54
+ export declare function growDedicatedRuntime(leaseId: string, storageGb: number, options?: {
55
+ requestId?: string;
56
+ }): Promise<DedicatedRuntimeResponse>;
57
+ export declare function closeDedicatedRuntimeIncidentHold(leaseId: string, options?: {
58
+ requestId?: string;
59
+ }): Promise<DedicatedRuntimeResponse>;
60
+ export declare function destroyDedicatedRuntime(leaseId: string, options?: {
61
+ retentionIntent?: 'retain' | 'purge';
62
+ }): Promise<DedicatedRuntimeResponse>;
63
+ export declare function dedicatedLeaseId(lease: HarnessLease | undefined | null): string;
64
+ export declare function dedicatedLeaseState(lease: HarnessLease | undefined | null): string;
65
+ export declare function dedicatedLeaseDisplayName(lease: HarnessLease | undefined | null): string;
66
+ export declare function formatDedicatedRuntimeApiError(err: unknown): string | null;
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEDICATED_LEASE_ACTIONS = void 0;
4
+ exports.dedicatedRuntimeCreateInput = dedicatedRuntimeCreateInput;
5
+ exports.createDedicatedRuntime = createDedicatedRuntime;
6
+ exports.listDedicatedRuntimes = listDedicatedRuntimes;
7
+ exports.getDedicatedRuntime = getDedicatedRuntime;
8
+ exports.performDedicatedLeaseAction = performDedicatedLeaseAction;
9
+ exports.growDedicatedRuntime = growDedicatedRuntime;
10
+ exports.closeDedicatedRuntimeIncidentHold = closeDedicatedRuntimeIncidentHold;
11
+ exports.destroyDedicatedRuntime = destroyDedicatedRuntime;
12
+ exports.dedicatedLeaseId = dedicatedLeaseId;
13
+ exports.dedicatedLeaseState = dedicatedLeaseState;
14
+ exports.dedicatedLeaseDisplayName = dedicatedLeaseDisplayName;
15
+ exports.formatDedicatedRuntimeApiError = formatDedicatedRuntimeApiError;
16
+ const api_fetch_1 = require("./api-fetch");
17
+ const harnesses_1 = require("./harnesses");
18
+ exports.DEDICATED_LEASE_ACTIONS = [
19
+ 'start',
20
+ 'stop',
21
+ 'restart',
22
+ 'snapshot',
23
+ 'health-check',
24
+ ];
25
+ /**
26
+ * Flags in, request body out — and the point of it being a function is the
27
+ * OMISSION.
28
+ *
29
+ * A field the user did not set must be ABSENT from the body, not present as
30
+ * `undefined`, because the server reaches its own default through
31
+ * `input.x || <default>`. Restating those defaults on the client looks
32
+ * harmless and is not: `--storage-gb` shipped with a client default of 100
33
+ * against a product default of 50, so a user who never chose a disk size was
34
+ * billed for twice the documented one and `workspaceDefaultGb` was unreachable
35
+ * through the CLI. The three neighbouring flags happened to agree, which only
36
+ * means the same bug was invisible in them.
37
+ */
38
+ function dedicatedRuntimeCreateInput(input) {
39
+ return {
40
+ requestId: input.requestId,
41
+ displayName: input.displayName,
42
+ sizePreset: input.sizePreset,
43
+ monthlySpendingLimitCents: input.monthlySpendingLimitCents,
44
+ ...(input.regionClass ? { regionClass: input.regionClass } : {}),
45
+ ...(input.storageGb !== undefined ? { storageGb: input.storageGb } : {}),
46
+ ...(input.retentionClass ? { retentionClass: input.retentionClass } : {}),
47
+ ...(input.backupProfile
48
+ ? { backupProfile: input.backupProfile }
49
+ : {}),
50
+ ...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
51
+ };
52
+ }
53
+ async function createDedicatedRuntime(input) {
54
+ const { requestId, workspaceId, ...body } = input;
55
+ if (workspaceId) {
56
+ return (0, api_fetch_1.apiFetch)(`/api/machines/dedicated?workspaceId=${encodeURIComponent(workspaceId)}`, {
57
+ method: 'POST',
58
+ body,
59
+ headers: { 'Idempotency-Key': requestId },
60
+ });
61
+ }
62
+ return (0, api_fetch_1.apiFetch)('/api/machines/dedicated', {
63
+ method: 'POST',
64
+ body,
65
+ headers: { 'Idempotency-Key': requestId },
66
+ });
67
+ }
68
+ async function listDedicatedRuntimes(options = {}) {
69
+ return (0, harnesses_1.listHarnessLeases)({
70
+ ...options,
71
+ machineProduct: 'dedicated-machine',
72
+ });
73
+ }
74
+ async function getDedicatedRuntime(leaseId) {
75
+ return (0, harnesses_1.getHarnessLease)(leaseId);
76
+ }
77
+ async function performDedicatedLeaseAction(leaseId, action) {
78
+ return (0, harnesses_1.performHarnessLeaseAction)(leaseId, action);
79
+ }
80
+ async function growDedicatedRuntime(leaseId, storageGb, options = {}) {
81
+ return (0, api_fetch_1.apiFetch)(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/storage`, {
82
+ method: 'POST',
83
+ body: { storageGb },
84
+ ...(options.requestId ? { headers: { 'Idempotency-Key': options.requestId } } : {}),
85
+ });
86
+ }
87
+ async function closeDedicatedRuntimeIncidentHold(leaseId, options = {}) {
88
+ return (0, api_fetch_1.apiFetch)(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/incident-hold/close`, {
89
+ method: 'POST',
90
+ body: {},
91
+ ...(options.requestId ? { headers: { 'Idempotency-Key': options.requestId } } : {}),
92
+ });
93
+ }
94
+ async function destroyDedicatedRuntime(leaseId, options = {}) {
95
+ const query = options.retentionIntent
96
+ ? `?retentionIntent=${encodeURIComponent(options.retentionIntent)}`
97
+ : '';
98
+ return (0, api_fetch_1.apiFetch)(`/api/machines/leases/${encodeURIComponent(leaseId)}${query}`, { method: 'DELETE' });
99
+ }
100
+ function dedicatedLeaseId(lease) {
101
+ return String(lease?.id ?? '');
102
+ }
103
+ function dedicatedLeaseState(lease) {
104
+ return String(lease?.state ?? 'unknown');
105
+ }
106
+ function dedicatedLeaseDisplayName(lease) {
107
+ return lease?.displayName ? String(lease.displayName) : '-';
108
+ }
109
+ /**
110
+ * Dedicated Runtime commands are unusually likely to meet a server that has
111
+ * never heard of them.
112
+ *
113
+ * The CLI ships independently of the API (`@skrr-ai/cli` on npm), so "CLI newer
114
+ * than the deployed server" is the normal state for anyone who runs
115
+ * `npm update`, and this feature widened that window: twelve commands arrived at
116
+ * once, and three of them call endpoints that land in a later deploy. The server
117
+ * redacts an unknown-route 404 to "An unexpected error occurred.", so every one
118
+ * of those users got eight words that cannot be told apart from a wrong lease
119
+ * id or a broken server.
120
+ *
121
+ * A 404 carrying no product `code` is that case. Naming it is the difference
122
+ * between "wait for the deploy" and "open a bug".
123
+ */
124
+ function describeMissingEndpoint(err) {
125
+ if (err.status !== 404) {
126
+ return null;
127
+ }
128
+ try {
129
+ const parsed = JSON.parse(err.body || '');
130
+ // A product code means the server DID handle the route and refused on the
131
+ // merits — that message is the useful one and must not be overwritten.
132
+ if (parsed.code) {
133
+ return null;
134
+ }
135
+ }
136
+ catch {
137
+ // Unparseable body: no product code by definition.
138
+ }
139
+ return ('This server does not have that Dedicated Runtime endpoint. Your CLI is probably ahead of ' +
140
+ 'the deployment — `skrr context` names the server it is talking to, and `skrr machines ' +
141
+ 'dedicated list` will still work if the feature is deployed at all.');
142
+ }
143
+ function formatDedicatedRuntimeApiError(err) {
144
+ if (!(err instanceof api_fetch_1.ApiFetchError) || !err.body) {
145
+ return null;
146
+ }
147
+ if (err.status === 401) {
148
+ return null;
149
+ }
150
+ const missingEndpoint = describeMissingEndpoint(err);
151
+ if (missingEndpoint) {
152
+ return missingEndpoint;
153
+ }
154
+ try {
155
+ const parsed = JSON.parse(err.body);
156
+ return parsed.message || parsed.error || parsed.code || null;
157
+ }
158
+ catch {
159
+ return err.body.trim() || null;
160
+ }
161
+ }
@@ -97,6 +97,11 @@ export interface HarnessLease {
97
97
  assignable?: boolean;
98
98
  reasons?: string[];
99
99
  } & Record<string, unknown>;
100
+ storage?: {
101
+ retentionClass?: string;
102
+ lastBackupAt?: string;
103
+ backupProfile?: string;
104
+ };
100
105
  createdAt?: string;
101
106
  updatedAt?: string;
102
107
  }
@@ -177,8 +177,11 @@ async function listHarnessLeases(options = {}) {
177
177
  query.set('state', options.state);
178
178
  if (options.machineProduct)
179
179
  query.set('machineProduct', options.machineProduct);
180
- const suffix = query.toString() ? `?${query.toString()}` : '';
181
- return (0, api_fetch_1.apiFetch)(`/api/machines/leases${suffix}`);
180
+ const encoded = query.toString();
181
+ if (encoded) {
182
+ return (0, api_fetch_1.apiFetch)(`/api/machines/leases?${encoded}`);
183
+ }
184
+ return (0, api_fetch_1.apiFetch)('/api/machines/leases');
182
185
  }
183
186
  async function getHarnessLease(leaseId) {
184
187
  return (0, api_fetch_1.apiFetch)(`/api/machines/leases/${encodeURIComponent(leaseId)}`);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Tell a reader when an empty canonical view means "nothing happened" and when
3
+ * it means "this Task predates the records this view reads".
4
+ *
5
+ * Canonical Task Model v1 shipped with an explicit decision not to migrate
6
+ * legacy data, so a Task created before 2026-09-11 has no TaskUpdate, TaskResult
7
+ * or TaskActivityEvent rows — its history is in `activityLog`, `TaskWorkEvent`
8
+ * and TASK_OUTPUT comments. Every canonical read surface therefore answered
9
+ * "No activity." / "Result: none submitted" / nothing at all for Tasks with
10
+ * years of history, stating absence as fact.
11
+ *
12
+ * The marker is that `specVersion` is ABSENT — a field the caller can only
13
+ * notice by its own missingness, which is why this belongs in one helper rather
14
+ * than in each command.
15
+ */
16
+ export declare function isLegacyTask(taskId: string): Promise<boolean>;
17
+ /**
18
+ * The sentence to print instead of claiming emptiness. `surface` names what the
19
+ * reader just asked for, so the message says which view is empty rather than
20
+ * implying the Task is.
21
+ */
22
+ export declare function legacyEmptyStateMessage(surface: string): string;
23
+ /** Render the right empty state for a canonical view that came back empty. */
24
+ export declare function emptyCanonicalView(taskId: string, surface: string): Promise<string>;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isLegacyTask = isLegacyTask;
4
+ exports.legacyEmptyStateMessage = legacyEmptyStateMessage;
5
+ exports.emptyCanonicalView = emptyCanonicalView;
6
+ const data_provider_1 = require("@skrr-ai/data-provider");
7
+ /**
8
+ * Tell a reader when an empty canonical view means "nothing happened" and when
9
+ * it means "this Task predates the records this view reads".
10
+ *
11
+ * Canonical Task Model v1 shipped with an explicit decision not to migrate
12
+ * legacy data, so a Task created before 2026-09-11 has no TaskUpdate, TaskResult
13
+ * or TaskActivityEvent rows — its history is in `activityLog`, `TaskWorkEvent`
14
+ * and TASK_OUTPUT comments. Every canonical read surface therefore answered
15
+ * "No activity." / "Result: none submitted" / nothing at all for Tasks with
16
+ * years of history, stating absence as fact.
17
+ *
18
+ * The marker is that `specVersion` is ABSENT — a field the caller can only
19
+ * notice by its own missingness, which is why this belongs in one helper rather
20
+ * than in each command.
21
+ */
22
+ async function isLegacyTask(taskId) {
23
+ try {
24
+ const task = (await data_provider_1.dataService.getTask(taskId));
25
+ return task !== undefined && task !== null && task.specVersion === undefined;
26
+ }
27
+ catch {
28
+ // Never turn a diagnostic into a failure: the caller is rendering an empty
29
+ // state, not making a decision.
30
+ return false;
31
+ }
32
+ }
33
+ /**
34
+ * The sentence to print instead of claiming emptiness. `surface` names what the
35
+ * reader just asked for, so the message says which view is empty rather than
36
+ * implying the Task is.
37
+ */
38
+ function legacyEmptyStateMessage(surface) {
39
+ return (`No ${surface} recorded under the canonical Task model.\n` +
40
+ 'This Task predates it, so its history was never migrated — read it with ' +
41
+ '`skrr tasks show`, `skrr tasks comments list`, or `skrr tasks transcript`.');
42
+ }
43
+ /** Render the right empty state for a canonical view that came back empty. */
44
+ async function emptyCanonicalView(taskId, surface) {
45
+ return (await isLegacyTask(taskId))
46
+ ? legacyEmptyStateMessage(surface)
47
+ : `No ${surface} recorded yet.`;
48
+ }
@@ -59,6 +59,7 @@ export type TaskCreateFlagValues = {
59
59
  description?: string;
60
60
  presetKey?: string;
61
61
  }>;
62
+ 'review-policy'?: string;
62
63
  'start-date'?: string;
63
64
  'due-date'?: string;
64
65
  estimate?: number;
@@ -98,6 +99,37 @@ export declare function parseExpectations(specs: string[] | undefined): {
98
99
  error: string;
99
100
  };
100
101
  export declare const EXPECT_FLAG_HELP: string;
102
+ /**
103
+ * What `--default-expect` means, as a value rather than a side effect.
104
+ *
105
+ * `clear` REMOVES the Space default, and it has to exist. Every Expectation gates
106
+ * closure and a Space default is inherited by every task created afterwards, so a
107
+ * default set by mistake — or one that outlived its project — gated the whole
108
+ * board with no way back. `tasks update --expect` already had this sentinel; the
109
+ * Space flag, the one the design calls "configured once and then holds", did not.
110
+ * The only exit was to waive the expectation on every task, which writes `dropped`
111
+ * into the ledger about a requirement that was never real. That is a false record,
112
+ * not a workaround.
113
+ *
114
+ * `null` rather than `[]`: the route declares the field `.nullable()` and the API
115
+ * turns null into an `$unset`, so the Space stops carrying a default instead of
116
+ * carrying an empty one. `undefined` means the flag was not passed and the
117
+ * existing default must be left exactly as it is.
118
+ *
119
+ * `clear` must be the SOLE value. Combined with an expectation it is two
120
+ * contradictory instructions, and honouring one of them silently is how a caller
121
+ * ends up with a contract they did not ask for.
122
+ *
123
+ * Extracted so the three outcomes can be tested without a command harness — the
124
+ * bug being fixed was in this decision, not in the HTTP call after it.
125
+ */
126
+ export declare function resolveDefaultExpectations(values: string[] | undefined): {
127
+ ok: true;
128
+ value: Array<Record<string, unknown>> | null | undefined;
129
+ } | {
130
+ ok: false;
131
+ error: string;
132
+ };
101
133
  /**
102
134
  * Parse a deliverable coordinate. Also delegated — `:` and `@` and now `/` all
103
135
  * mean different things, and a second implementation of that grammar is how the
package/dist/lib/tasks.js CHANGED
@@ -6,6 +6,7 @@ exports.taskUpdateMutationGuard = taskUpdateMutationGuard;
6
6
  exports.executionIsolationFromWorktreeFlag = executionIsolationFromWorktreeFlag;
7
7
  exports.parseExpectation = parseExpectation;
8
8
  exports.parseExpectations = parseExpectations;
9
+ exports.resolveDefaultExpectations = resolveDefaultExpectations;
9
10
  exports.parseDeliverableCoordinate = parseDeliverableCoordinate;
10
11
  exports.buildTaskCreatePayload = buildTaskCreatePayload;
11
12
  exports.boundSessionAgentId = boundSessionAgentId;
@@ -156,10 +157,54 @@ const kindsForHelp = Array.isArray(data_provider_1.TASK_OUTCOME_KINDS)
156
157
  const presetsForHelp = Array.isArray(data_provider_1.EXPECTATION_PRESETS)
157
158
  ? data_provider_1.EXPECTATION_PRESETS.map((preset) => `@${preset.key}`).join(', ')
158
159
  : '@merged-to-main, @pdf-delivered';
159
- exports.EXPECT_FLAG_HELP = 'What this task must produce, as <kind>[/<format>][:<target>] or @<preset> ' +
160
+ exports.EXPECT_FLAG_HELP = 'What this task must produce. Each one GATES closure: the Task cannot enter a ' +
161
+ 'completed state until every Expectation is assessed met or waived ' +
162
+ '(`tasks expectations assess`). Form: <kind>[/<format>][:<target>] or @<preset> — ' +
160
163
  `e.g. 'code:origin/main', 'file/pdf:Drive/Reports', '@merged-to-main'. ` +
161
164
  `Kinds: ${kindsForHelp}. Presets: ${presetsForHelp}. ` +
162
165
  'Anything else is stored as a plain-language expectation a person settles. Repeatable.';
166
+ /**
167
+ * What `--default-expect` means, as a value rather than a side effect.
168
+ *
169
+ * `clear` REMOVES the Space default, and it has to exist. Every Expectation gates
170
+ * closure and a Space default is inherited by every task created afterwards, so a
171
+ * default set by mistake — or one that outlived its project — gated the whole
172
+ * board with no way back. `tasks update --expect` already had this sentinel; the
173
+ * Space flag, the one the design calls "configured once and then holds", did not.
174
+ * The only exit was to waive the expectation on every task, which writes `dropped`
175
+ * into the ledger about a requirement that was never real. That is a false record,
176
+ * not a workaround.
177
+ *
178
+ * `null` rather than `[]`: the route declares the field `.nullable()` and the API
179
+ * turns null into an `$unset`, so the Space stops carrying a default instead of
180
+ * carrying an empty one. `undefined` means the flag was not passed and the
181
+ * existing default must be left exactly as it is.
182
+ *
183
+ * `clear` must be the SOLE value. Combined with an expectation it is two
184
+ * contradictory instructions, and honouring one of them silently is how a caller
185
+ * ends up with a contract they did not ask for.
186
+ *
187
+ * Extracted so the three outcomes can be tested without a command harness — the
188
+ * bug being fixed was in this decision, not in the HTTP call after it.
189
+ */
190
+ function resolveDefaultExpectations(values) {
191
+ if (values === undefined || values.length === 0)
192
+ return { ok: true, value: undefined };
193
+ const clears = values.filter((value) => value.trim().toLowerCase() === 'clear');
194
+ if (clears.length > 0 && values.length > 1) {
195
+ return {
196
+ ok: false,
197
+ error: '--default-expect clear removes the space default and cannot be combined with an ' +
198
+ 'expectation. Run it alone, then set the new default.',
199
+ };
200
+ }
201
+ if (clears.length > 0)
202
+ return { ok: true, value: null };
203
+ const parsed = parseExpectations(values);
204
+ if (!parsed.ok)
205
+ return { ok: false, error: parsed.error };
206
+ return { ok: true, value: parsed.value };
207
+ }
163
208
  /**
164
209
  * Parse a deliverable coordinate. Also delegated — `:` and `@` and now `/` all
165
210
  * mean different things, and a second implementation of that grammar is how the
@@ -214,6 +259,7 @@ function buildTaskCreatePayload(body, flags = {}) {
214
259
  assigneeUserIds: flags['assignee-user'],
215
260
  executionMode: flags['execution-mode'],
216
261
  expectations: flags.expectations,
262
+ reviewPolicy: flags['review-policy'],
217
263
  startDate: flags['start-date'],
218
264
  dueDate: flags['due-date'],
219
265
  estimate: flags.estimate,