@skrr-ai/cli 0.1.37 → 0.1.38

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 (174) hide show
  1. package/bin/dev-fallback.js +167 -4
  2. package/dist/base-command.js +46 -2
  3. package/dist/commands/agents/actions/create.js +18 -7
  4. package/dist/commands/agents/actions/delete.d.ts +1 -0
  5. package/dist/commands/agents/actions/delete.js +7 -0
  6. package/dist/commands/agents/actions/update.js +5 -3
  7. package/dist/commands/agents/api-actions/add.js +5 -2
  8. package/dist/commands/agents/api-actions/index.d.ts +11 -0
  9. package/dist/commands/agents/api-actions/index.js +27 -1
  10. package/dist/commands/agents/avatar/default/index.d.ts +11 -0
  11. package/dist/commands/agents/avatar/default/index.js +17 -0
  12. package/dist/commands/agents/avatar/default/set.d.ts +2 -1
  13. package/dist/commands/agents/avatar/default/set.js +32 -7
  14. package/dist/commands/agents/avatar/upload.js +7 -1
  15. package/dist/commands/agents/categories/delete.js +8 -0
  16. package/dist/commands/agents/categories/index.d.ts +21 -5
  17. package/dist/commands/agents/categories/index.js +68 -15
  18. package/dist/commands/agents/chat.d.ts +21 -0
  19. package/dist/commands/agents/chat.js +94 -7
  20. package/dist/commands/agents/create.js +11 -4
  21. package/dist/commands/agents/list.d.ts +15 -0
  22. package/dist/commands/agents/list.js +31 -1
  23. package/dist/commands/agents/scene-background/upload.d.ts +30 -0
  24. package/dist/commands/agents/scene-background/upload.js +97 -0
  25. package/dist/commands/agents/show.js +13 -0
  26. package/dist/commands/agents/tools/calls.d.ts +23 -5
  27. package/dist/commands/agents/tools/calls.js +81 -18
  28. package/dist/commands/agents/update.js +15 -5
  29. package/dist/commands/code/{oversky-agent.d.ts → skrr-agent.d.ts} +3 -3
  30. package/dist/commands/code/{oversky-agent.js → skrr-agent.js} +11 -8
  31. package/dist/commands/convos/messages.d.ts +13 -0
  32. package/dist/commands/convos/messages.js +40 -16
  33. package/dist/commands/daemon/index.js +8 -1
  34. package/dist/commands/daemon/logs.d.ts +26 -0
  35. package/dist/commands/daemon/logs.js +56 -0
  36. package/dist/commands/daemon/uninstall.js +1 -1
  37. package/dist/commands/followups/act.js +3 -2
  38. package/dist/commands/followups/cancel.d.ts +6 -3
  39. package/dist/commands/followups/cancel.js +16 -6
  40. package/dist/commands/followups/observability.d.ts +23 -0
  41. package/dist/commands/followups/observability.js +114 -0
  42. package/dist/commands/followups/reschedule.js +1 -1
  43. package/dist/commands/followups/resolve.js +3 -3
  44. package/dist/commands/followups/show.js +2 -1
  45. package/dist/commands/followups/watch.js +9 -5
  46. package/dist/commands/harnesses/forget.js +28 -0
  47. package/dist/commands/harnesses/install.d.ts +41 -0
  48. package/dist/commands/harnesses/install.js +165 -0
  49. package/dist/commands/harnesses/installers.d.ts +45 -0
  50. package/dist/commands/harnesses/installers.js +155 -0
  51. package/dist/commands/harnesses/list.d.ts +82 -1
  52. package/dist/commands/harnesses/list.js +104 -12
  53. package/dist/commands/harnesses/models.js +10 -1
  54. package/dist/commands/harnesses/show.d.ts +15 -0
  55. package/dist/commands/harnesses/show.js +44 -3
  56. package/dist/commands/harnesses/update.js +1 -1
  57. package/dist/commands/harnesses/usage.d.ts +25 -0
  58. package/dist/commands/harnesses/usage.js +24 -2
  59. package/dist/commands/instructions/status.js +13 -1
  60. package/dist/commands/machines/dedicated/catalog.js +4 -15
  61. package/dist/commands/machines/dedicated/create.d.ts +8 -0
  62. package/dist/commands/machines/dedicated/create.js +58 -1
  63. package/dist/commands/machines/dedicated/destroy.js +18 -4
  64. package/dist/commands/machines/dedicated/list.js +9 -6
  65. package/dist/commands/machines/hosted/list.js +12 -0
  66. package/dist/commands/machines/list.js +11 -0
  67. package/dist/commands/machines/show.d.ts +1 -0
  68. package/dist/commands/machines/show.js +40 -11
  69. package/dist/commands/profiles/list.d.ts +23 -0
  70. package/dist/commands/profiles/list.js +96 -0
  71. package/dist/commands/search/skills.d.ts +11 -1
  72. package/dist/commands/search/skills.js +26 -8
  73. package/dist/commands/skills/details.d.ts +2 -0
  74. package/dist/commands/skills/details.js +24 -1
  75. package/dist/commands/skills/import-as-actions.d.ts +32 -0
  76. package/dist/commands/skills/import-as-actions.js +175 -0
  77. package/dist/commands/skills/install.d.ts +2 -0
  78. package/dist/commands/skills/install.js +24 -1
  79. package/dist/commands/skills/invocable.js +6 -2
  80. package/dist/commands/skills/local.d.ts +40 -0
  81. package/dist/commands/skills/local.js +160 -0
  82. package/dist/commands/skills/search.d.ts +11 -1
  83. package/dist/commands/skills/search.js +29 -8
  84. package/dist/commands/spaces/actions/add.js +23 -5
  85. package/dist/commands/spaces/actions/available.js +3 -0
  86. package/dist/commands/spaces/actions/list.d.ts +5 -0
  87. package/dist/commands/spaces/actions/list.js +49 -3
  88. package/dist/commands/spaces/actions/run.d.ts +28 -0
  89. package/dist/commands/spaces/actions/run.js +146 -0
  90. package/dist/commands/spaces/actions/update.js +27 -5
  91. package/dist/commands/spaces/label-summaries.d.ts +27 -0
  92. package/dist/commands/spaces/label-summaries.js +64 -0
  93. package/dist/commands/spaces/task-rollups.d.ts +5 -0
  94. package/dist/commands/spaces/task-rollups.js +15 -4
  95. package/dist/commands/spaces/workflows/lifecycle-diagnostics.js +63 -3
  96. package/dist/commands/tasks/actions/attach.js +32 -7
  97. package/dist/commands/tasks/complete.js +13 -0
  98. package/dist/commands/tasks/create.js +1 -1
  99. package/dist/commands/tasks/events/append.js +29 -6
  100. package/dist/commands/tasks/expectations/assess.d.ts +1 -0
  101. package/dist/commands/tasks/expectations/assess.js +43 -5
  102. package/dist/commands/tasks/expectations.js +77 -16
  103. package/dist/commands/tasks/follow-up.js +2 -2
  104. package/dist/commands/tasks/lifecycle/definitions.js +21 -2
  105. package/dist/commands/tasks/lifecycle/detach.d.ts +7 -0
  106. package/dist/commands/tasks/lifecycle/detach.js +43 -3
  107. package/dist/commands/tasks/lifecycle/migrate.d.ts +2 -0
  108. package/dist/commands/tasks/lifecycle/migrate.js +42 -3
  109. package/dist/commands/tasks/lifecycle/reopen.d.ts +2 -0
  110. package/dist/commands/tasks/lifecycle/reopen.js +40 -3
  111. package/dist/commands/tasks/lifecycle/transition.js +33 -2
  112. package/dist/commands/tasks/list.js +16 -9
  113. package/dist/commands/tasks/move.js +11 -13
  114. package/dist/commands/tasks/review-queue.d.ts +15 -0
  115. package/dist/commands/tasks/review-queue.js +180 -31
  116. package/dist/commands/tasks/show.d.ts +11 -0
  117. package/dist/commands/tasks/show.js +105 -12
  118. package/dist/commands/tasks/trust.js +7 -2
  119. package/dist/commands/tasks/update.js +46 -10
  120. package/dist/commands/tasks/updates/add.js +1 -1
  121. package/dist/commands/token/list.d.ts +22 -0
  122. package/dist/commands/token/list.js +68 -0
  123. package/dist/commands/token/revoke.d.ts +23 -0
  124. package/dist/commands/token/revoke.js +53 -0
  125. package/dist/lib/agent-actions.d.ts +57 -2
  126. package/dist/lib/agent-actions.js +113 -21
  127. package/dist/lib/agent-pin-mute.d.ts +10 -5
  128. package/dist/lib/agent-pin-mute.js +17 -4
  129. package/dist/lib/agent-visual-refs.d.ts +27 -0
  130. package/dist/lib/agent-visual-refs.js +127 -2
  131. package/dist/lib/agentic-stream.d.ts +6 -0
  132. package/dist/lib/agentic-stream.js +34 -3
  133. package/dist/lib/assignee-resolver.d.ts +1 -1
  134. package/dist/lib/assignee-resolver.js +8 -0
  135. package/dist/lib/bulk-task-targets.d.ts +12 -0
  136. package/dist/lib/bulk-task-targets.js +34 -1
  137. package/dist/lib/cli-installers.d.ts +130 -0
  138. package/dist/lib/cli-installers.js +145 -0
  139. package/dist/lib/command-miss.js +55 -7
  140. package/dist/lib/dedicated-lease-command.d.ts +13 -0
  141. package/dist/lib/dedicated-lease-command.js +22 -1
  142. package/dist/lib/dedicated-machines.d.ts +95 -1
  143. package/dist/lib/dedicated-machines.js +236 -2
  144. package/dist/lib/failed-lookup.d.ts +3 -0
  145. package/dist/lib/failed-lookup.js +40 -0
  146. package/dist/lib/first-party-harness-agent.js +9 -2
  147. package/dist/lib/first-party-harness-doctor.d.ts +15 -0
  148. package/dist/lib/first-party-harness-doctor.js +126 -27
  149. package/dist/lib/followups.d.ts +14 -6
  150. package/dist/lib/followups.js +31 -6
  151. package/dist/lib/harnesses.d.ts +8 -0
  152. package/dist/lib/instruction-input.d.ts +19 -1
  153. package/dist/lib/instruction-input.js +52 -1
  154. package/dist/lib/label-scope.d.ts +15 -0
  155. package/dist/lib/label-scope.js +25 -5
  156. package/dist/lib/local-skills.d.ts +98 -0
  157. package/dist/lib/local-skills.js +195 -0
  158. package/dist/lib/machines.d.ts +10 -10
  159. package/dist/lib/machines.js +23 -1
  160. package/dist/lib/search-query.d.ts +19 -0
  161. package/dist/lib/search-query.js +35 -0
  162. package/dist/lib/space-custom-actions.d.ts +72 -0
  163. package/dist/lib/space-custom-actions.js +117 -0
  164. package/dist/lib/space-resolver.d.ts +1 -21
  165. package/dist/lib/space-resolver.js +19 -10
  166. package/dist/lib/task-closure.d.ts +3 -2
  167. package/dist/lib/task-closure.js +18 -20
  168. package/dist/lib/task-lifecycle-output.js +117 -9
  169. package/dist/lib/task-ref-resolver.d.ts +1 -17
  170. package/dist/lib/task-ref-resolver.js +18 -8
  171. package/dist/lib/tasks.js +9 -3
  172. package/dist/node_modules/@skrr-ai/data-provider/index.js +5076 -4947
  173. package/oclif.manifest.json +26635 -25649
  174. package/package.json +9 -3
@@ -5,15 +5,51 @@ const data_provider_1 = require("@skrr-ai/data-provider");
5
5
  const base_command_1 = require("../../../base-command");
6
6
  class TasksLifecycleMigrate extends base_command_1.BaseCommand {
7
7
  static description = 'Migrate a bound lifecycle to a newer published version, fencing the old generation';
8
- static args = { id: core_1.Args.string({ required: true, ignoreStdin: true }) };
8
+ static examples = [
9
+ '<%= config.bin %> tasks lifecycle definitions OSK-1234 # where a --version comes from',
10
+ '<%= config.bin %> tasks lifecycle migrate OSK-1234 --version wfv_… --dry-run',
11
+ ];
12
+ static args = {
13
+ id: core_1.Args.string({ required: true, ignoreStdin: true, description: 'Task ID or identifier' }),
14
+ };
9
15
  static flags = {
10
- version: core_1.Flags.string({ required: true, description: 'Target WorkflowDefinition version id' }),
16
+ version: core_1.Flags.string({
17
+ required: true,
18
+ description: 'Target published version id — `tasks lifecycle definitions <task-id>` prints it',
19
+ }),
20
+ 'dry-run': core_1.Flags.boolean({
21
+ description: 'Describe what migrating would do and exit without writing',
22
+ }),
11
23
  json: core_1.Flags.boolean({ description: 'Output JSON' }),
12
24
  };
13
25
  async run() {
14
26
  this.requireAuth();
15
27
  const { args, flags } = await this.parse(TasksLifecycleMigrate);
16
28
  const taskId = await this.resolveTaskReference(args.id);
29
+ // Moving a Task between published protocols is not obviously reversible,
30
+ // and the old output said only that a run had started. A flag rather than a
31
+ // prompt, so the command stays usable from a script.
32
+ if (flags['dry-run']) {
33
+ let current;
34
+ try {
35
+ current = await data_provider_1.dataService.getTaskLifecycle(taskId);
36
+ }
37
+ catch (error) {
38
+ this.handleApiError(error);
39
+ }
40
+ if (flags.json)
41
+ return this.log(JSON.stringify({ dryRun: true, ...current }, null, 2));
42
+ const lifecycle = current.lifecycle;
43
+ if (!lifecycle)
44
+ return this.log('No lifecycle is attached to this Task — nothing to migrate.');
45
+ this.log(`Would migrate from version ${lifecycle.versionId} to ${flags.version}, as generation ` +
46
+ `${Number(lifecycle.generation) + 1}.`);
47
+ this.log(` It re-enters at the CURRENT stage (${lifecycle.currentStageKey ?? '—'}) when that stage` +
48
+ " still exists on the target version; otherwise the protocol's entry stage decides.");
49
+ this.log(' The old generation is fenced — its run can no longer write.');
50
+ this.log(' Earlier generations stay readable in `tasks lifecycle history`.');
51
+ return;
52
+ }
17
53
  let result;
18
54
  try {
19
55
  result = await data_provider_1.dataService.migrateTaskLifecycle({ taskId, versionId: flags.version });
@@ -23,7 +59,10 @@ class TasksLifecycleMigrate extends base_command_1.BaseCommand {
23
59
  }
24
60
  if (flags.json)
25
61
  return this.log(JSON.stringify(result, null, 2));
26
- this.log(`Migrated lifecycle — run ${result.run.id} (${result.run.status}).`);
62
+ const lifecycle = result.lifecycle;
63
+ this.log(`Migrated to version ${lifecycle?.versionId ?? flags.version} — now generation ` +
64
+ `${lifecycle?.generation ?? '?'} at stage ${lifecycle?.currentStageKey ?? '—'}` +
65
+ ` (run ${result.run.id}, ${result.run.status}).`);
27
66
  }
28
67
  }
29
68
  exports.default = TasksLifecycleMigrate;
@@ -1,12 +1,14 @@
1
1
  import { BaseCommand } from '../../../base-command';
2
2
  export default class TasksLifecycleReopen extends BaseCommand {
3
3
  static description: string;
4
+ static examples: string[];
4
5
  static args: {
5
6
  id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
6
7
  };
7
8
  static flags: {
8
9
  stage: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
9
10
  reason: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
+ 'dry-run': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
12
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
11
13
  };
12
14
  run(): Promise<void>;
@@ -5,16 +5,50 @@ const data_provider_1 = require("@skrr-ai/data-provider");
5
5
  const base_command_1 = require("../../../base-command");
6
6
  class TasksLifecycleReopen extends base_command_1.BaseCommand {
7
7
  static description = 'Fence the current lifecycle generation and reopen at an earlier stage';
8
- static args = { id: core_1.Args.string({ required: true, ignoreStdin: true }) };
8
+ static examples = [
9
+ '<%= config.bin %> tasks lifecycle reopen OSK-1234 --dry-run',
10
+ '<%= config.bin %> tasks lifecycle reopen OSK-1234 --stage fix --reason "The fix did not take"',
11
+ ];
12
+ static args = {
13
+ id: core_1.Args.string({ required: true, ignoreStdin: true, description: 'Task ID or identifier' }),
14
+ };
9
15
  static flags = {
10
16
  stage: core_1.Flags.string({ description: 'Stage key to re-enter at; defaults to the entry stage' }),
11
- reason: core_1.Flags.string({ description: 'Why the lifecycle is reopening' }),
17
+ reason: core_1.Flags.string({ description: 'Why the lifecycle is reopening; recorded in history' }),
18
+ 'dry-run': core_1.Flags.boolean({
19
+ description: 'Describe what reopening would do and exit without writing',
20
+ }),
12
21
  json: core_1.Flags.boolean({ description: 'Output JSON' }),
13
22
  };
14
23
  async run() {
15
24
  this.requireAuth();
16
25
  const { args, flags } = await this.parse(TasksLifecycleReopen);
17
26
  const taskId = await this.resolveTaskReference(args.id);
27
+ // Reopen advances a generation and is not obviously reversible. A preview
28
+ // that names the target stage is cheaper than learning what it did by
29
+ // diffing state afterwards, and a flag keeps the command scriptable where
30
+ // an interactive prompt would not.
31
+ if (flags['dry-run']) {
32
+ let current;
33
+ try {
34
+ current = await data_provider_1.dataService.getTaskLifecycle(taskId);
35
+ }
36
+ catch (error) {
37
+ this.handleApiError(error);
38
+ }
39
+ if (flags.json)
40
+ return this.log(JSON.stringify({ dryRun: true, ...current }, null, 2));
41
+ const lifecycle = current.lifecycle;
42
+ if (!lifecycle)
43
+ return this.log('No lifecycle is attached to this Task — nothing to reopen.');
44
+ const entry = flags.stage || current.stages?.[0]?.key;
45
+ this.log(`Would reopen at stage ${entry ?? '(entry stage)'} as generation ${Number(lifecycle.generation) + 1}` +
46
+ ` (currently generation ${lifecycle.generation}, stage ${lifecycle.currentStageKey ?? '—'}).`);
47
+ this.log(' The current run is cancelled; its transitions stay readable in history.');
48
+ this.log(" A terminal Task is brought back to the entry stage's declared status.");
49
+ this.log(' Stages the earlier generation finished keep reading as done in that generation.');
50
+ return;
51
+ }
18
52
  let result;
19
53
  try {
20
54
  result = await data_provider_1.dataService.reopenTaskLifecycle({
@@ -28,7 +62,10 @@ class TasksLifecycleReopen extends base_command_1.BaseCommand {
28
62
  }
29
63
  if (flags.json)
30
64
  return this.log(JSON.stringify(result, null, 2));
31
- this.log(`Reopened lifecycle — run ${result.run.id} (${result.run.status}).`);
65
+ // Name where it landed, not just that it happened.
66
+ const lifecycle = result.lifecycle;
67
+ this.log(`Reopened at stage ${lifecycle?.currentStageKey ?? '—'} as generation ${lifecycle?.generation ?? '?'}` +
68
+ ` — run ${result.run.id} (${result.run.status}). The previous generation stays in history.`);
32
69
  }
33
70
  }
34
71
  exports.default = TasksLifecycleReopen;
@@ -42,6 +42,23 @@ class TasksLifecycleTransition extends base_command_1.BaseCommand {
42
42
  this.error('--payload must be a JSON object.', { exit: 2 });
43
43
  }
44
44
  }
45
+ /*
46
+ * The fence only protects the interval it is given.
47
+ *
48
+ * `--expected-revision` is the caller's claim about the state they DECIDED
49
+ * against. When it is omitted this command re-reads the lifecycle and sends
50
+ * back what it just read — and a compare-and-set whose expected value was
51
+ * fetched microseconds before the write can only catch that microsecond,
52
+ * never the interval that matters, which is between the human's read and
53
+ * their write. So the default is a convenience, not protection, and it was
54
+ * silent about that: a `confirm` decided at revision 0 applied against
55
+ * revision 1 and closed a Task, with nothing in the output saying the world
56
+ * had moved in between.
57
+ *
58
+ * Kept as the default because requiring a number on every call would be
59
+ * paid by everyone to protect a minority of flows — but it now says what it
60
+ * fenced on and how to fence properly.
61
+ */
45
62
  let expectedRevision = flags['expected-revision'];
46
63
  if (expectedRevision === undefined) {
47
64
  let current;
@@ -55,6 +72,10 @@ class TasksLifecycleTransition extends base_command_1.BaseCommand {
55
72
  this.error('No lifecycle is attached to this Task.', { exit: 2 });
56
73
  }
57
74
  expectedRevision = current.lifecycle.revision;
75
+ if (!flags.json) {
76
+ this.log(`Fencing on revision ${expectedRevision}, read just now — not the revision you saw. ` +
77
+ 'Pass --expected-revision <n> to fence on the state you decided against.');
78
+ }
58
79
  }
59
80
  const idempotencyKey = flags['idempotency-key'] ?? `cli-lifecycle:${taskId}:${args.transition}:r${expectedRevision}`;
60
81
  let response;
@@ -71,8 +92,18 @@ class TasksLifecycleTransition extends base_command_1.BaseCommand {
71
92
  this.handleApiError(error);
72
93
  }
73
94
  if (flags.json)
74
- return this.log(JSON.stringify(response, null, 2));
75
- (0, task_lifecycle_output_1.renderLifecycleTransitionResult)(response, (line) => this.log(line));
95
+ this.log(JSON.stringify(response, null, 2));
96
+ else
97
+ (0, task_lifecycle_output_1.renderLifecycleTransitionResult)(response, (line) => this.log(line));
98
+ // Exit non-zero when the write did not land. `applied` is the only verdict
99
+ // that means the stage moved; `rejected`, `superseded` and `gated` all mean
100
+ // it did not. Exiting 0 on all four made
101
+ // `transition A && transition B` run B after A had been fenced off — which
102
+ // is precisely the lost update `expectedRevision` exists to prevent, waved
103
+ // through by the shell.
104
+ if (response.status !== 'applied') {
105
+ this.exit(1);
106
+ }
76
107
  }
77
108
  }
78
109
  exports.default = TasksLifecycleTransition;
@@ -146,13 +146,13 @@ function buildListParams(flags, assigneeUserId) {
146
146
  ...(flag('actionability-reason')
147
147
  ? { actionabilityReason: flag('actionability-reason') }
148
148
  : {}),
149
- ...(flag('actionable-by') ||
150
- flag('actionable-for-agent') ||
151
- flag('needs-me') ||
152
- flag('actionability-state') ||
153
- flag('actionability-reason')
154
- ? { includeActionability: true }
155
- : {}),
149
+ // This table always renders a NEXT column, so it always has to ask for the
150
+ // data behind it. Requesting it only alongside an actionability FILTER left
151
+ // the default board printing a header with nothing under it — and a plain
152
+ // `tasks list` is exactly where a person looks to find out what anything is
153
+ // waiting for. The server computes it in one batched read, so the cost is
154
+ // a column that works rather than a column that is there.
155
+ includeActionability: true,
156
156
  ...(flag('priority') ? { priority: flag('priority') } : {}),
157
157
  ...(flag('estimate') !== undefined ? { estimate: flag('estimate') } : {}),
158
158
  ...(flag('min-estimate') !== undefined
@@ -427,8 +427,13 @@ class TasksList extends base_command_1.BaseCommand {
427
427
  priority: String(t.priority ?? '-'),
428
428
  estimate: t.estimate === null || t.estimate === undefined ? '-' : String(t.estimate),
429
429
  assignees,
430
+ // `reason` is the sentence the server wrote for a person; `reasonCode`
431
+ // is the enum it wrote for a machine. Printing the code rendered every
432
+ // stage of every lifecycle as `human:lifecycle_stage` — six different
433
+ // waits, one string — while the sentence naming the stage sat unread in
434
+ // the same payload.
430
435
  next: actionability
431
- ? `${String(actionability.nextActor ?? '-')}:${String(actionability.reasonCode ?? actionability.state ?? '-')}`
436
+ ? `${String(actionability.nextActor ?? '-')}: ${String(actionability.reason || actionability.reasonCode || actionability.state || '-')}`
432
437
  : String(t.executionMode ?? '-'),
433
438
  match: String(t._matchField ?? '-'),
434
439
  };
@@ -440,7 +445,9 @@ class TasksList extends base_command_1.BaseCommand {
440
445
  { key: 'priority', header: 'PRIORITY' },
441
446
  { key: 'estimate', header: 'ESTIMATE' },
442
447
  { key: 'assignees', header: 'ASSIGNEES' },
443
- { key: 'next', header: 'NEXT' },
448
+ // Bounded: the reason is a sentence, and an unbounded column wraps the
449
+ // table into unreadability on a narrow terminal.
450
+ { key: 'next', header: 'NEXT', maxWidth: 46 },
444
451
  // Only meaningful on a search — the server computes it from the same
445
452
  // relevance rank that orders the rows, and a plain listing has none.
446
453
  ...(flags.q ? [{ key: 'match', header: 'MATCH' }] : []),
@@ -5,6 +5,7 @@ const data_provider_1 = require("@skrr-ai/data-provider");
5
5
  const api_fetch_1 = require("../../lib/api-fetch");
6
6
  const session_task_endpoints_1 = require("../../lib/session-task-endpoints");
7
7
  const base_command_1 = require("../../base-command");
8
+ const task_closure_1 = require("../../lib/task-closure");
8
9
  const web_url_1 = require("../../lib/web-url");
9
10
  const task_resolver_1 = require("../../lib/task-resolver");
10
11
  const projects_1 = require("../../lib/projects");
@@ -155,19 +156,16 @@ class TasksMove extends base_command_1.BaseCommand {
155
156
  // the loosely-typed receipt, so the narrowing cast stays; what matters is
156
157
  // that the shape it names is the one data-provider owns.
157
158
  const resolutionClosure = t.resolution?.closure;
158
- if (resolutionClosure?.overrode?.includes('TASK_RESULT_REVIEW_REQUIRED')) {
159
- this.log(' Closed past the owner-declared required Review — the override is recorded (resolution.closure.overrode).');
160
- }
161
- if (resolutionClosure?.closable === false) {
162
- const parts = [];
163
- const unmet = resolutionClosure.expectations?.unmet?.length ?? 0;
164
- if (unmet > 0) {
165
- parts.push(`${unmet} Expectation${unmet === 1 ? '' : 's'} still unmet`);
166
- }
167
- parts.push(...(resolutionClosure.blockers || []).filter((code) => code !== 'TASK_EXPECTATIONS_NOT_MET' && code !== 'TASK_RESULT_REVIEW_REQUIRED'));
168
- if (parts.length) {
169
- this.log(` Closed over ${parts.join('; ')} (resolution.closure).`);
170
- }
159
+ /*
160
+ * Through the shared renderer. This hand-rolled the rendering and pushed
161
+ * blocker CODES at a human — `Closed over TASK_RESULT_REQUIRED`, which is a
162
+ * wire identifier and, read plainly, the opposite of what just happened. The
163
+ * worst instance was the one this batch cares most about: a Task closed over
164
+ * a reviewer's objection printed `TASK_RESULT_CHANGES_REQUESTED`, rendering
165
+ * "somebody looked and objected" as an identifier.
166
+ */
167
+ for (const sentence of (0, task_closure_1.closureSnapshotLines)(resolutionClosure)) {
168
+ this.log(` ${sentence}`);
171
169
  }
172
170
  }
173
171
  /**
@@ -23,7 +23,22 @@ export default class TasksReviewQueue extends BaseCommand {
23
23
  space: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
24
24
  actionable: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
25
25
  limit: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
26
+ 'include-closed': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
26
27
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
27
28
  };
28
29
  run(): Promise<void>;
30
+ /**
31
+ * An empty queue must not be how you learn a Space id is wrong.
32
+ *
33
+ * A UUID-shaped ref is accepted without a lookup (`base-command` short-circuits
34
+ * on the SHAPE, which is a cheap and correct optimisation nearly everywhere),
35
+ * and the list route deliberately never 404s — an unknown Space falls back to
36
+ * the ACL enumeration and returns an honest empty page. Those two reasonable
37
+ * decisions compose into "Nothing is awaiting a judgement", exit 0, for a
38
+ * Space that does not exist. A typo read as done.
39
+ *
40
+ * So the existence check is paid for ONLY on the path that is about to make
41
+ * the completeness claim — never on the path that found work.
42
+ */
43
+ private assertSpaceExists;
29
44
  }
@@ -35,6 +35,9 @@ class TasksReviewQueue extends base_command_1.BaseCommand {
35
35
  description: 'Only Tasks where a Result is already in — the ones a reviewer can act on',
36
36
  }),
37
37
  limit: core_1.Flags.integer({ description: 'Maximum Tasks to inspect (default 100)', default: 100 }),
38
+ 'include-closed': core_1.Flags.boolean({
39
+ description: 'Also list Tasks already closed — for auditing what was closed over',
40
+ }),
38
41
  json: core_1.Flags.boolean({ description: 'Output as JSON' }),
39
42
  };
40
43
  async run() {
@@ -66,7 +69,27 @@ class TasksReviewQueue extends base_command_1.BaseCommand {
66
69
  // unlooked-for empty answer this command refuses to give.
67
70
  this.error('--limit must be at least 1.', { exit: 1 });
68
71
  }
72
+ /*
73
+ * oclif's `required: true` asserts the flag was SUPPLIED, not that it says
74
+ * anything. `--space ""` — an unset shell variable, which is how it happens
75
+ * — satisfied it, and the empty string was then dropped from the query
76
+ * string as falsy, so the command silently widened from one Space to every
77
+ * Space the caller can see and reported that as the answer.
78
+ */
79
+ if (!flags.space.trim()) {
80
+ this.error('--space needs a Space id; it was given an empty value.', { exit: 1 });
81
+ }
69
82
  const tasks = [];
83
+ /*
84
+ * Whether the scan actually reached the end.
85
+ *
86
+ * The loop already followed the cursor, and then stopped at `--limit` with
87
+ * `has_more` still true and threw that fact away — so a Space with 95 owed
88
+ * Expectations reported on the first 11 and printed the same sentence it
89
+ * prints for a Space that owes nothing. The comment above states the
90
+ * principle this had to satisfy; nothing recorded whether it had.
91
+ */
92
+ let truncated = false;
70
93
  try {
71
94
  let cursor;
72
95
  for (;;) {
@@ -80,8 +103,12 @@ class TasksReviewQueue extends base_command_1.BaseCommand {
80
103
  'say whether anything is owed. Refusing to report an empty queue.', { exit: 1 });
81
104
  }
82
105
  tasks.push(...response.data);
83
- if (!response.has_more || !response.next_cursor || tasks.length >= flags.limit)
106
+ if (!response.has_more || !response.next_cursor)
107
+ break;
108
+ if (tasks.length >= flags.limit) {
109
+ truncated = true;
84
110
  break;
111
+ }
85
112
  cursor = response.next_cursor;
86
113
  }
87
114
  }
@@ -95,34 +122,101 @@ class TasksReviewQueue extends base_command_1.BaseCommand {
95
122
  * worse than no queue: the reviewer would believe they had seen everything.
96
123
  */
97
124
  const rows = [];
98
- for (const task of tasks) {
99
- const taskId = String(task.id ?? task._id ?? '');
100
- if (!taskId)
101
- continue;
102
- const base = {
103
- taskId,
104
- identifier: String(task.identifier ?? taskId),
105
- title: String(task.title ?? '(untitled)'),
106
- status: String(task.status ?? '-'),
107
- hasResult: Boolean(task.currentResultId),
108
- };
109
- try {
110
- const acceptance = await data_provider_1.dataService.getTaskAcceptance(taskId);
111
- const awaiting = acceptance.expectations.filter((e) => e.gating && !e.met);
112
- if (awaiting.length > 0)
113
- rows.push({ ...base, awaiting });
114
- }
115
- catch (error) {
116
- rows.push({ ...base, awaiting: [], error: error?.message || 'unreadable' });
125
+ /*
126
+ * A finished Task is not a worklist item.
127
+ *
128
+ * Closing is the closer's call now, so a Task can reach `done` over
129
+ * Expectations nobody judged — and those rows stayed in the queue forever,
130
+ * captioned in the present tense ("settling these will not close it") about
131
+ * something already closed. Every forced close permanently added noise to
132
+ * the list a reviewer is supposed to work down. `--include-closed` keeps the
133
+ * old view for anyone auditing what was closed over.
134
+ */
135
+ const CLOSED_STATUS_TYPES = new Set(['completed', 'cancelled']);
136
+ const candidates = tasks.filter((task) => flags['include-closed'] || !CLOSED_STATUS_TYPES.has(String(task.statusType ?? '')));
137
+ const skippedClosed = tasks.length - candidates.length;
138
+ /*
139
+ * Acceptance is one request per Task and there is no batch endpoint, so the
140
+ * serial loop this replaces took 61 seconds on 100 Tasks and printed nothing
141
+ * at all until it finished. Bounded rather than `Promise.all`: 100
142
+ * simultaneous authorized reads hit the same cascade the `/cards` endpoint
143
+ * exists to avoid. Results are written by INDEX so output order stays the
144
+ * list's order regardless of which request returns first.
145
+ */
146
+ const ACCEPTANCE_CONCURRENCY = 6;
147
+ const settled = new Array(candidates.length).fill(null);
148
+ let next = 0;
149
+ const worker = async () => {
150
+ for (;;) {
151
+ const index = next;
152
+ next += 1;
153
+ if (index >= candidates.length)
154
+ return;
155
+ const task = candidates[index];
156
+ const taskId = String(task.id ?? task._id ?? '');
157
+ if (!taskId)
158
+ continue;
159
+ const base = {
160
+ taskId,
161
+ identifier: String(task.identifier ?? taskId),
162
+ title: String(task.title ?? '(untitled)'),
163
+ status: String(task.status ?? '-'),
164
+ hasResult: Boolean(task.currentResultId),
165
+ };
166
+ try {
167
+ const acceptance = await data_provider_1.dataService.getTaskAcceptance(taskId);
168
+ const awaiting = acceptance.expectations.filter((e) => e.gating && !e.met);
169
+ if (awaiting.length > 0)
170
+ settled[index] = { ...base, awaiting };
171
+ }
172
+ catch (error) {
173
+ settled[index] = {
174
+ ...base,
175
+ awaiting: [],
176
+ error: error?.message || 'unreadable',
177
+ };
178
+ }
117
179
  }
118
- }
180
+ };
181
+ await Promise.all(Array.from({ length: Math.min(ACCEPTANCE_CONCURRENCY, candidates.length) }, worker));
182
+ rows.push(...settled.filter((row) => row !== null));
119
183
  const visible = flags.actionable ? rows.filter((row) => row.hasResult || row.error) : rows;
120
184
  if (flags.json) {
121
- this.log(JSON.stringify({ space: flags.space, tasks: visible }, null, 2));
185
+ this.log(JSON.stringify({
186
+ space: flags.space,
187
+ inspected: candidates.length,
188
+ truncated,
189
+ ...(skippedClosed > 0 ? { skippedClosed } : {}),
190
+ owedTasks: rows.length,
191
+ tasks: visible,
192
+ }, null, 2));
122
193
  return;
123
194
  }
124
- if (visible.length === 0) {
195
+ /*
196
+ * "Nothing is owed" may only be said by a scan that reached the end.
197
+ *
198
+ * One sentence used to cover four different states — a clean queue, a
199
+ * truncated scan, a filter that emptied the list, and a Space that does not
200
+ * exist — and in three of them it was false. Each now says its own thing,
201
+ * and the two that cannot support the claim refuse to make it, in the same
202
+ * shape as the misread-envelope guard above.
203
+ */
204
+ if (truncated && rows.length === 0) {
205
+ this.error(`Inspected only the first ${candidates.length} Task(s) of a longer list in ${flags.space}, ` +
206
+ 'so it cannot say whether anything is owed. Refusing to report an empty queue. ' +
207
+ 'Re-run with a larger --limit.', { exit: 1 });
208
+ }
209
+ if (rows.length === 0) {
210
+ await this.assertSpaceExists(flags.space);
125
211
  this.log(`Nothing in ${flags.space} is awaiting a judgement.`);
212
+ if (skippedClosed > 0) {
213
+ this.log(` (${skippedClosed} closed Task(s) skipped — see them with --include-closed.)`);
214
+ }
215
+ return;
216
+ }
217
+ if (visible.length === 0) {
218
+ this.log(`${rows.length} Task(s) in ${flags.space} owe judgements, but none has a Result yet — ` +
219
+ 're-run without --actionable to see them.');
126
220
  return;
127
221
  }
128
222
  let total = 0;
@@ -133,18 +227,37 @@ class TasksReviewQueue extends base_command_1.BaseCommand {
133
227
  this.log(` could not read its Expectations: ${row.error}`);
134
228
  continue;
135
229
  }
136
- // A Task with no Result yet is listed but marked: its Expectations are
137
- // owed, and settling them still leaves it unclosable. Saying so here stops
138
- // a reviewer working through a list that cannot finish.
230
+ /*
231
+ * State the fact, not a gate. This read "no Result submitted yet — settling
232
+ * these will not close it", which implied that a Result is what stands
233
+ * between these judgements and a close. Since `56a6ded703` it is not: a
234
+ * Task closes without one and the absence is recorded on
235
+ * `resolution.closure`. What a reviewer needs to know is only that there is
236
+ * no Result to judge the work against yet.
237
+ */
139
238
  if (!row.hasResult) {
140
- this.log(' no Result submitted yet — settling these will not close it');
239
+ this.log(' no Result submitted yet — nothing to judge the work against');
141
240
  }
142
241
  for (const expectation of row.awaiting) {
143
242
  total += 1;
144
- const settledBy = expectation.assessedByKind
145
- ? ` [last judged by ${(0, task_closure_1.describeAssessor)(expectation.assessedByKind)}]`
146
- : '';
147
- this.log(` ${expectation.id} ${expectation.sentence || expectation.text || ''}${settledBy}`);
243
+ this.log(` ${expectation.id} ${expectation.sentence || expectation.text || ''}`);
244
+ /*
245
+ * The judgement itself, not just the KIND of judge.
246
+ *
247
+ * `[last judged by person]` named no person, no decision, no date and no
248
+ * reason — so a reviewer working down this list could see that someone
249
+ * had looked and not what they concluded, which is the one thing they
250
+ * need before looking again. The acceptance projection carries all of it
251
+ * now; this is where it was being dropped.
252
+ */
253
+ if (expectation.assessedByKind) {
254
+ const decision = expectation.decision || 'judged';
255
+ const when = expectation.assessedAt
256
+ ? ` on ${String(expectation.assessedAt).slice(0, 10)}`
257
+ : '';
258
+ const why = expectation.reason ? `: ${expectation.reason}` : '';
259
+ this.log(` ${decision} by ${(0, task_closure_1.describeAssessor)(expectation.assessedByKind)}${when}${why}`);
260
+ }
148
261
  if (expectation.evidenceLevel) {
149
262
  this.log(` evidence: ${expectation.evidenceLevel}${expectation.requiredEvidence ? ` (asked for ${expectation.requiredEvidence})` : ''}`);
150
263
  }
@@ -155,11 +268,47 @@ class TasksReviewQueue extends base_command_1.BaseCommand {
155
268
  }
156
269
  this.log('');
157
270
  this.log(`${total} Expectation(s) awaiting a judgement across ${visible.length} Task(s).`);
271
+ if (truncated) {
272
+ this.log(`Only the first ${candidates.length} Task(s) were inspected and more remain — ` +
273
+ 're-run with a larger --limit to see the rest.');
274
+ }
275
+ if (skippedClosed > 0) {
276
+ this.log(`${skippedClosed} closed Task(s) skipped — see them with --include-closed.`);
277
+ }
158
278
  this.log('');
159
279
  this.log('Settle one: skrr tasks expectations assess <task-id> <expectation-id> \\');
160
280
  this.log(' --decision met --reason "..."');
161
281
  this.log('Settle a Task: skrr tasks expectations assess <task-id> --all \\');
162
282
  this.log(' --decision met --reason "..."');
163
283
  }
284
+ /**
285
+ * An empty queue must not be how you learn a Space id is wrong.
286
+ *
287
+ * A UUID-shaped ref is accepted without a lookup (`base-command` short-circuits
288
+ * on the SHAPE, which is a cheap and correct optimisation nearly everywhere),
289
+ * and the list route deliberately never 404s — an unknown Space falls back to
290
+ * the ACL enumeration and returns an honest empty page. Those two reasonable
291
+ * decisions compose into "Nothing is awaiting a judgement", exit 0, for a
292
+ * Space that does not exist. A typo read as done.
293
+ *
294
+ * So the existence check is paid for ONLY on the path that is about to make
295
+ * the completeness claim — never on the path that found work.
296
+ */
297
+ async assertSpaceExists(spaceId) {
298
+ try {
299
+ await data_provider_1.dataService.getSpace(spaceId);
300
+ }
301
+ catch (error) {
302
+ // The status rides the error itself here, the way `base-command` reads it
303
+ // (`e.status === 404`) — not on a nested `response`.
304
+ const status = error?.status;
305
+ if (status === 404 || status === 403) {
306
+ this.error(`No Space ${spaceId} is visible to you, so there is nothing to report on. ` +
307
+ 'Check the id with `skrr spaces list`.', { exit: 2 });
308
+ }
309
+ // Any other failure is about this read, not about the Space. Let the
310
+ // empty answer stand rather than turning a blip into a false "not found".
311
+ }
312
+ }
164
313
  }
165
314
  exports.default = TasksReviewQueue;
@@ -1,4 +1,15 @@
1
1
  import { BaseCommand } from '../../base-command';
2
+ /**
3
+ * Where to look next — as COMMANDS, not as facts about the task.
4
+ *
5
+ * These used to print as `What the agent produced: skrr tasks output <ref>`,
6
+ * in the same `Label: value` grammar as every line above them, so the line read
7
+ * as a statement of what the agent had produced. It also printed on tasks no
8
+ * agent had ever run, asserting output that did not exist (OSK-9669). They are
9
+ * now a visibly separate list, and only once a run, a conversation or a Result
10
+ * exists for them to show.
11
+ */
12
+ export declare function taskPointerLines(t: Record<string, unknown>, ref: string, bin?: string): string[];
2
13
  /**
3
14
  * The failure rows of `tasks show`, when there are any. (S3 dogfood finding)
4
15
  *