@skrr-ai/cli 0.1.9 → 0.1.11

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 (109) hide show
  1. package/dist/base-command.d.ts +1 -13
  2. package/dist/base-command.js +78 -1
  3. package/dist/commands/browser/skill/show.js +7 -1
  4. package/dist/commands/code/index.d.ts +1 -0
  5. package/dist/commands/code/index.js +9 -1
  6. package/dist/commands/commitments/analytics/index.js +2 -0
  7. package/dist/commands/daemon/byok.d.ts +1 -0
  8. package/dist/commands/daemon/byok.js +2 -1
  9. package/dist/commands/daemon/install.d.ts +1 -0
  10. package/dist/commands/daemon/install.js +2 -1
  11. package/dist/commands/daemon/login.d.ts +31 -0
  12. package/dist/commands/daemon/login.js +56 -0
  13. package/dist/commands/daemon/restart.d.ts +9 -0
  14. package/dist/commands/daemon/restart.js +36 -0
  15. package/dist/commands/daemon/start.d.ts +1 -0
  16. package/dist/commands/daemon/start.js +2 -1
  17. package/dist/commands/daemon/status.d.ts +1 -0
  18. package/dist/commands/daemon/status.js +2 -1
  19. package/dist/commands/daemon/stop.d.ts +1 -0
  20. package/dist/commands/daemon/stop.js +2 -1
  21. package/dist/commands/daemon/uninstall.d.ts +1 -0
  22. package/dist/commands/daemon/uninstall.js +2 -1
  23. package/dist/commands/daemon/unlock.d.ts +9 -0
  24. package/dist/commands/daemon/unlock.js +33 -0
  25. package/dist/commands/goals/key-results/create.js +32 -1
  26. package/dist/commands/goals/key-results/update.d.ts +11 -0
  27. package/dist/commands/goals/key-results/update.js +80 -2
  28. package/dist/commands/goals/plan-now.d.ts +54 -2
  29. package/dist/commands/goals/plan-now.js +175 -18
  30. package/dist/commands/goals/planner-config.d.ts +60 -9
  31. package/dist/commands/goals/planner-config.js +82 -34
  32. package/dist/commands/goals/revisions.js +17 -0
  33. package/dist/commands/goals/show.d.ts +17 -0
  34. package/dist/commands/goals/show.js +90 -3
  35. package/dist/commands/login.js +36 -3
  36. package/dist/commands/spaces/create.js +2 -1
  37. package/dist/commands/spaces/index.js +9 -1
  38. package/dist/commands/spaces/list.d.ts +18 -0
  39. package/dist/commands/spaces/list.js +57 -7
  40. package/dist/commands/spaces/show.js +4 -1
  41. package/dist/commands/spaces/summary.d.ts +4 -0
  42. package/dist/commands/spaces/summary.js +77 -1
  43. package/dist/commands/spaces/update.d.ts +4 -0
  44. package/dist/commands/spaces/update.js +38 -1
  45. package/dist/commands/tasks/actionability.js +40 -1
  46. package/dist/commands/tasks/activity.d.ts +29 -0
  47. package/dist/commands/tasks/activity.js +47 -0
  48. package/dist/commands/tasks/complete.d.ts +47 -0
  49. package/dist/commands/tasks/complete.js +159 -12
  50. package/dist/commands/tasks/create.d.ts +26 -0
  51. package/dist/commands/tasks/create.js +60 -1
  52. package/dist/commands/tasks/events/append.d.ts +2 -0
  53. package/dist/commands/tasks/events/append.js +41 -10
  54. package/dist/commands/tasks/events/list.js +60 -9
  55. package/dist/commands/tasks/output.js +22 -2
  56. package/dist/commands/tasks/ready.d.ts +38 -0
  57. package/dist/commands/tasks/ready.js +37 -0
  58. package/dist/commands/tasks/runs.d.ts +22 -0
  59. package/dist/commands/tasks/runs.js +110 -2
  60. package/dist/commands/tasks/show.d.ts +28 -0
  61. package/dist/commands/tasks/show.js +61 -0
  62. package/dist/commands/tasks/timeline.d.ts +7 -0
  63. package/dist/commands/tasks/timeline.js +34 -3
  64. package/dist/commands/tasks/update.d.ts +20 -0
  65. package/dist/commands/tasks/update.js +38 -1
  66. package/dist/commands/whoami.d.ts +40 -0
  67. package/dist/commands/whoami.js +61 -10
  68. package/dist/commands/wiki/ls.d.ts +23 -0
  69. package/dist/commands/wiki/ls.js +63 -10
  70. package/dist/commands/wiki/mv.d.ts +70 -0
  71. package/dist/commands/wiki/mv.js +198 -4
  72. package/dist/commands/wiki/rm.js +12 -1
  73. package/dist/commands/wiki/write.js +35 -3
  74. package/dist/help.d.ts +27 -0
  75. package/dist/help.js +48 -0
  76. package/dist/hooks/command-not-found.d.ts +31 -0
  77. package/dist/hooks/command-not-found.js +12 -80
  78. package/dist/lib/command-miss.d.ts +60 -0
  79. package/dist/lib/command-miss.js +128 -0
  80. package/dist/lib/commitment-analytics.d.ts +10 -0
  81. package/dist/lib/commitment-analytics.js +10 -0
  82. package/dist/lib/config.d.ts +40 -0
  83. package/dist/lib/config.js +72 -9
  84. package/dist/lib/daemonBroker.d.ts +7 -1
  85. package/dist/lib/daemonBroker.js +71 -0
  86. package/dist/lib/daemonHandoff.d.ts +38 -0
  87. package/dist/lib/daemonHandoff.js +297 -0
  88. package/dist/lib/exec-oversky.d.ts +30 -0
  89. package/dist/lib/exec-oversky.js +41 -0
  90. package/dist/lib/format.d.ts +7 -0
  91. package/dist/lib/format.js +27 -5
  92. package/dist/lib/login.js +22 -2
  93. package/dist/lib/sky-code.js +9 -4
  94. package/dist/lib/task-transcript.d.ts +9 -0
  95. package/dist/lib/task-transcript.js +13 -2
  96. package/dist/lib/tasks.d.ts +34 -0
  97. package/dist/lib/tasks.js +70 -1
  98. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.d.ts +50 -0
  99. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.js +67 -0
  100. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +7 -1
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.d.ts +50 -0
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.js +59 -0
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +3 -0
  106. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  107. package/dist/node_modules/@skrr-ai/data-provider/index.js +15 -4
  108. package/oclif.manifest.json +15713 -15480
  109. package/package.json +5 -3
@@ -4,17 +4,33 @@ 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 format_1 = require("../../../lib/format");
8
+ const tasks_1 = require("../../../lib/tasks");
7
9
  class TasksEventsList extends base_command_1.BaseCommand {
8
10
  static description = 'List durable task events';
9
- static args = { id: core_1.Args.string({ required: false, ignoreStdin: true }) };
11
+ static args = {
12
+ id: core_1.Args.string({
13
+ required: false,
14
+ ignoreStdin: true,
15
+ description: 'Task ID. Optional inside a task runtime, which supplies it',
16
+ }),
17
+ };
10
18
  static flags = {
11
- task: core_1.Flags.string(),
12
- json: core_1.Flags.boolean(),
13
- limit: core_1.Flags.integer({ default: 50 }),
14
- cursor: core_1.Flags.string(),
15
- kind: core_1.Flags.string(),
16
- run: core_1.Flags.string(),
17
- actor: core_1.Flags.string(),
19
+ task: core_1.Flags.string({
20
+ description: 'Task ID; overrides the runtime context and the positional',
21
+ }),
22
+ json: core_1.Flags.boolean({ description: 'Output the raw event list as JSON' }),
23
+ limit: core_1.Flags.integer({ default: 50, description: 'Maximum events to return' }),
24
+ cursor: core_1.Flags.string({ description: 'Continue a previous page, from its next_cursor' }),
25
+ kind: core_1.Flags.string({
26
+ description: 'Show only events of this kind',
27
+ options: [...tasks_1.TASK_WORK_EVENT_KINDS],
28
+ }),
29
+ run: core_1.Flags.string({ description: 'Show only events from this execution run' }),
30
+ actor: core_1.Flags.string({
31
+ description: 'Show only events from this actor kind',
32
+ options: ['user', 'agent', 'system'],
33
+ }),
18
34
  };
19
35
  async run() {
20
36
  this.requireAuth();
@@ -33,7 +49,42 @@ class TasksEventsList extends base_command_1.BaseCommand {
33
49
  catch (error) {
34
50
  this.handleApiError(error);
35
51
  }
36
- this.log(JSON.stringify(result, null, 2));
52
+ // This printed raw Mongo documents — `_id` and `__v` included — regardless
53
+ // of `--json`, which was a byte-identical no-op: the flag advertised a mode
54
+ // the command did not have. Its two siblings, `tasks timeline` and `tasks
55
+ // activity`, both render tables, so the one command an agent is pointed at
56
+ // for the durable ledger was the one that made you read storage internals.
57
+ if (flags.json) {
58
+ this.log(JSON.stringify(result, null, 2));
59
+ return;
60
+ }
61
+ const rows = (result?.data ?? []);
62
+ if (rows.length === 0) {
63
+ this.log('No events.');
64
+ return;
65
+ }
66
+ (0, format_1.renderTable)(rows.map((event) => ({
67
+ time: String(event.occurredAt ?? '-')
68
+ .replace('T', ' ')
69
+ .slice(0, 19),
70
+ kind: String(event.kind ?? '-'),
71
+ actor: String(event.actor?.kind ?? '-'),
72
+ summary: (0, format_1.truncate)(String(event.summary ?? '-'), 80),
73
+ run: String(event.executionRunId ?? '-'),
74
+ })), [
75
+ { key: 'time', header: 'TIME' },
76
+ { key: 'kind', header: 'KIND' },
77
+ { key: 'actor', header: 'ACTOR' },
78
+ { key: 'summary', header: 'SUMMARY', maxWidth: 80 },
79
+ { key: 'run', header: 'RUN', maxWidth: 24 },
80
+ ], (line) => this.log(line));
81
+ // The fuller fields (changed / next / evidence / artifacts) are on the rows
82
+ // and deliberately not columns — five of them would not fit a terminal.
83
+ // Point at the flag that has them rather than truncating them into noise.
84
+ this.log('');
85
+ this.log('Full event bodies (changed, next, evidence, artifacts): --json');
86
+ if (result?.next_cursor)
87
+ this.log(`Next page: --cursor ${result.next_cursor}`);
37
88
  }
38
89
  }
39
90
  exports.default = TasksEventsList;
@@ -94,6 +94,9 @@ class TasksOutput extends base_command_1.BaseCommand {
94
94
  ? { report: latestReport, occurredAt: latestReportEvent?.occurredAt }
95
95
  : null,
96
96
  taskOutput: outputComment,
97
+ // Present in JSON as well, so a script can detect a task closed
98
+ // more than once without diffing comment bodies.
99
+ completionCount: outputComment ? outputComment.supersededCount + 1 : 0,
97
100
  finalMessage,
98
101
  messageCount: messages.length,
99
102
  }, null, 2));
@@ -104,12 +107,13 @@ class TasksOutput extends base_command_1.BaseCommand {
104
107
  conversationId,
105
108
  latestRun,
106
109
  latestReport,
110
+ latestReportAt: latestReportEvent?.occurredAt ? String(latestReportEvent.occurredAt) : null,
107
111
  outputComment,
108
112
  finalMessage,
109
113
  full: flags.full,
110
114
  });
111
115
  }
112
- renderHuman({ task, conversationId, latestRun, latestReport, outputComment, finalMessage, full, }) {
116
+ renderHuman({ task, conversationId, latestRun, latestReport, latestReportAt, outputComment, finalMessage, full, }) {
113
117
  const ref = String(task.identifier ?? task.id ?? '-');
114
118
  const agents = Array.isArray(task.assigneeAgentIds) ? task.assigneeAgentIds : [];
115
119
  this.log(`Task: ${ref} — ${task.title ?? '-'}`);
@@ -120,7 +124,13 @@ class TasksOutput extends base_command_1.BaseCommand {
120
124
  : 'never executed'}`);
121
125
  this.log('');
122
126
  if (latestReport) {
123
- this.log('Outcome (task report):');
127
+ // Timestamped like the Verdict below it. These two blocks are selected
128
+ // independently — the newest task-report work event and the newest
129
+ // TASK_OUTPUT comment — so on a task completed more than once they can
130
+ // come from different completions and disagree. Undated, that was
131
+ // invisible.
132
+ const reportAt = latestReportAt ? ` · ${(0, task_transcript_1.formatTime)(latestReportAt)}` : '';
133
+ this.log(`Outcome (task report${reportAt}):`);
124
134
  this.log(` ${latestReport.outcome}`);
125
135
  this.log(' findings:');
126
136
  for (const finding of latestReport.findings)
@@ -156,6 +166,16 @@ class TasksOutput extends base_command_1.BaseCommand {
156
166
  // Reconcile the three signals a parked run shows — `completed`,
157
167
  // `UNVERIFIED`, and `Last run: failed` — which otherwise read as a
158
168
  // contradiction. (OSK-4945)
169
+ if (outputComment.supersededCount > 0) {
170
+ // `tasks complete` succeeds on an already-done task and posts another
171
+ // block, so the canonical-looking screen can be assembled from several
172
+ // closeouts. Say so rather than presenting it as one record.
173
+ const total = outputComment.supersededCount + 1;
174
+ const earlier = outputComment.supersededCount === 1 ? 'block' : 'blocks';
175
+ this.log(` note: completed ${total} times — ${outputComment.supersededCount} earlier TASK_OUTPUT ${earlier} exist.`);
176
+ this.log(' The Outcome above may come from a different completion than this');
177
+ this.log(' Verdict. `skrr tasks comments list` shows every one.');
178
+ }
159
179
  const note = (0, task_transcript_1.unverifiedOutcomeNote)(parsed.verdict);
160
180
  if (note)
161
181
  this.log(` note: ${note}`);
@@ -1,4 +1,27 @@
1
1
  import { BaseCommand } from '../../base-command';
2
+ /**
3
+ * Repair oclif's required-flag copy for a flag named after a preposition. (S4 dogfood finding)
4
+ *
5
+ * oclif builds the reason as `Missing required flag ${name}` — bare, unquoted —
6
+ * and wraps it in `The following error occurred:`. For almost every flag that
7
+ * reads fine (`Missing required flag idempotency-key`). For `--for` it produces
8
+ *
9
+ * Missing required flag for
10
+ *
11
+ * which reads as a sentence cut off mid-clause: missing a required flag FOR
12
+ * WHAT? The reader has to already know `for` is the flag's name to parse it,
13
+ * and the one reader who does not is exactly the one getting this error.
14
+ *
15
+ * Restoring the `--` makes the name unmistakably a name. The rewrite is
16
+ * deliberately narrow — it matches the whole reason line and leaves anything
17
+ * else untouched — so an oclif that changes this template degrades to today's
18
+ * wording rather than mangling a different message.
19
+ *
20
+ * The general form of this belongs in `BaseCommand.catch`, where it would cover
21
+ * every command; this is the local fix for the one flag where the ambiguity is
22
+ * actually harmful.
23
+ */
24
+ export declare function clarifyMissingRequiredFlag(message: string): string;
2
25
  export default class TasksReady extends BaseCommand {
3
26
  static description: string;
4
27
  static examples: string[];
@@ -11,5 +34,20 @@ export default class TasksReady extends BaseCommand {
11
34
  limit: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
12
35
  cursor: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
13
36
  };
37
+ /**
38
+ * Rewrite the parse-error copy on the way out, then hand the error back
39
+ * unchanged in every other respect — same class, same `oclif.exit`, same
40
+ * `--json` envelope from `BaseCommand.catch`. Only the sentence changes.
41
+ */
42
+ protected catch(err: Error & {
43
+ code?: string;
44
+ exitCode?: number;
45
+ oclif?: {
46
+ exit?: number;
47
+ };
48
+ parse?: unknown;
49
+ suggestions?: unknown;
50
+ retryable?: boolean;
51
+ }): Promise<unknown>;
14
52
  run(): Promise<void>;
15
53
  }
@@ -1,10 +1,36 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.clarifyMissingRequiredFlag = clarifyMissingRequiredFlag;
3
4
  const core_1 = require("@oclif/core");
4
5
  const data_provider_1 = require("@skrr-ai/data-provider");
5
6
  const base_command_1 = require("../../base-command");
6
7
  const format_1 = require("../../lib/format");
7
8
  const web_url_1 = require("../../lib/web-url");
9
+ /**
10
+ * Repair oclif's required-flag copy for a flag named after a preposition. (S4 dogfood finding)
11
+ *
12
+ * oclif builds the reason as `Missing required flag ${name}` — bare, unquoted —
13
+ * and wraps it in `The following error occurred:`. For almost every flag that
14
+ * reads fine (`Missing required flag idempotency-key`). For `--for` it produces
15
+ *
16
+ * Missing required flag for
17
+ *
18
+ * which reads as a sentence cut off mid-clause: missing a required flag FOR
19
+ * WHAT? The reader has to already know `for` is the flag's name to parse it,
20
+ * and the one reader who does not is exactly the one getting this error.
21
+ *
22
+ * Restoring the `--` makes the name unmistakably a name. The rewrite is
23
+ * deliberately narrow — it matches the whole reason line and leaves anything
24
+ * else untouched — so an oclif that changes this template degrades to today's
25
+ * wording rather than mangling a different message.
26
+ *
27
+ * The general form of this belongs in `BaseCommand.catch`, where it would cover
28
+ * every command; this is the local fix for the one flag where the ambiguity is
29
+ * actually harmful.
30
+ */
31
+ function clarifyMissingRequiredFlag(message) {
32
+ return message.replace(/Missing required flag ([A-Za-z0-9][\w-]*)$/gm, (_full, name) => `Missing required flag --${name}`);
33
+ }
8
34
  class TasksReady extends base_command_1.BaseCommand {
9
35
  static description = 'List tasks ready for a Human or Agent, using server-owned readiness';
10
36
  static examples = [
@@ -24,6 +50,17 @@ class TasksReady extends base_command_1.BaseCommand {
24
50
  limit: core_1.Flags.integer({ description: 'Max results', default: 50 }),
25
51
  cursor: core_1.Flags.string({ description: 'Pagination cursor from a previous page' }),
26
52
  };
53
+ /**
54
+ * Rewrite the parse-error copy on the way out, then hand the error back
55
+ * unchanged in every other respect — same class, same `oclif.exit`, same
56
+ * `--json` envelope from `BaseCommand.catch`. Only the sentence changes.
57
+ */
58
+ async catch(err) {
59
+ if (typeof err?.message === 'string') {
60
+ err.message = clarifyMissingRequiredFlag(err.message);
61
+ }
62
+ return super.catch(err);
63
+ }
27
64
  async run() {
28
65
  this.requireAuth();
29
66
  const { flags } = await this.parse(TasksReady);
@@ -1,10 +1,32 @@
1
1
  import { BaseCommand } from '../../base-command';
2
+ import { type RunFailure } from './activity';
2
3
  /**
3
4
  * OSK-4836 — say WHERE a run executed. A named daemon wins over the environment
4
5
  * label; an environment with no daemon is the cloud worker (or a runtime whose
5
6
  * daemon id was not recorded). `-` when the row predates executor stamping.
6
7
  */
7
8
  export declare function formatRunExecutor(run: Record<string, unknown>): string;
9
+ /**
10
+ * Whether this run row is one whose reason the caller cannot see. (S3 dogfood finding)
11
+ *
12
+ * `{ status: 'failed', errors: 1 }` and nothing else — a count of errors with
13
+ * no error. The row is complete as far as the projection goes, so the only way
14
+ * to tell a caller why is to go and read it from somewhere the CLI can reach.
15
+ * This decides when that lookup is worth a round trip: never for a healthy
16
+ * list, always once something failed.
17
+ */
18
+ export declare function runNeedsFailureReason(run: Record<string, unknown>): boolean;
19
+ /**
20
+ * The run's OWN failure fields, when the server carries them.
21
+ *
22
+ * The projection now derives `failureReason` / `failureCode` / `failureClass`
23
+ * from the terminal event it already had in hand, so the reason travels with
24
+ * the run rather than having to be reassembled from the activity log by runId.
25
+ * Exported for `runs.spec.ts`.
26
+ */
27
+ export declare function failureFromRun(run: Record<string, unknown>): RunFailure | null;
28
+ /** The reason block printed under the table — one entry per run we could explain. */
29
+ export declare function renderRunFailures(runs: Array<Record<string, unknown>>, failures: Map<string, RunFailure>, bin?: string, taskRef?: string): string[];
8
30
  export default class TasksRuns extends BaseCommand {
9
31
  static description: string;
10
32
  static examples: string[];
@@ -1,10 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatRunExecutor = formatRunExecutor;
4
+ exports.runNeedsFailureReason = runNeedsFailureReason;
5
+ exports.failureFromRun = failureFromRun;
6
+ exports.renderRunFailures = renderRunFailures;
4
7
  const core_1 = require("@oclif/core");
5
8
  const data_provider_1 = require("@skrr-ai/data-provider");
6
9
  const base_command_1 = require("../../base-command");
7
10
  const format_1 = require("../../lib/format");
11
+ const activity_1 = require("./activity");
8
12
  function formatTime(value) {
9
13
  try {
10
14
  return new Date(String(value)).toISOString().slice(0, 19).replace('T', ' ');
@@ -29,6 +33,79 @@ function formatRunExecutor(run) {
29
33
  }
30
34
  return '-';
31
35
  }
36
+ /**
37
+ * Whether this run row is one whose reason the caller cannot see. (S3 dogfood finding)
38
+ *
39
+ * `{ status: 'failed', errors: 1 }` and nothing else — a count of errors with
40
+ * no error. The row is complete as far as the projection goes, so the only way
41
+ * to tell a caller why is to go and read it from somewhere the CLI can reach.
42
+ * This decides when that lookup is worth a round trip: never for a healthy
43
+ * list, always once something failed.
44
+ */
45
+ function runNeedsFailureReason(run) {
46
+ if (run.status === 'failed' || run.outcome === 'failed')
47
+ return true;
48
+ return typeof run.errors === 'number' && run.errors > 0;
49
+ }
50
+ /**
51
+ * The run's OWN failure fields, when the server carries them.
52
+ *
53
+ * The projection now derives `failureReason` / `failureCode` / `failureClass`
54
+ * from the terminal event it already had in hand, so the reason travels with
55
+ * the run rather than having to be reassembled from the activity log by runId.
56
+ * Exported for `runs.spec.ts`.
57
+ */
58
+ function failureFromRun(run) {
59
+ const message = typeof run.failureReason === 'string' ? run.failureReason.trim() : '';
60
+ if (!message)
61
+ return null;
62
+ return {
63
+ runId: String(run.runId ?? run.id ?? run._id ?? '-'),
64
+ message,
65
+ code: typeof run.failureCode === 'string' ? run.failureCode : null,
66
+ causeClass: typeof run.failureClass === 'string' ? run.failureClass : null,
67
+ };
68
+ }
69
+ /** The reason block printed under the table — one entry per run we could explain. */
70
+ function renderRunFailures(runs, failures, bin = 'skrr', taskRef = '<task-id>') {
71
+ const needing = runs.filter(runNeedsFailureReason);
72
+ if (needing.length === 0)
73
+ return [];
74
+ const lines = ['', 'Why they failed:'];
75
+ let explained = 0;
76
+ let usedActivity = false;
77
+ for (const run of needing) {
78
+ const runId = String(run.runId ?? run.id ?? run._id ?? '-');
79
+ // The run's own field first. The activity join stays as the fallback
80
+ // because a CLI talks to whatever server it is pointed at, and one that
81
+ // predates the projection field still has the activity log — dropping the
82
+ // fallback would make this command worse against an older deployment than
83
+ // it was before the field existed.
84
+ const own = failureFromRun(run);
85
+ const failure = own ?? failures.get(runId);
86
+ if (!failure) {
87
+ // No invented reason. Saying "unknown" is the honest row, and it is
88
+ // materially different from the row above it.
89
+ lines.push(` ${runId} no reason recorded in the activity log.`);
90
+ continue;
91
+ }
92
+ explained += 1;
93
+ if (!own)
94
+ usedActivity = true;
95
+ const tags = [failure.code, failure.causeClass].filter(Boolean).join(' · ');
96
+ lines.push(` ${runId}${tags ? ` ${tags}` : ''}`);
97
+ lines.push(` ${failure.message}`);
98
+ }
99
+ // Name the SOURCE only when it is not the run itself: a reason that came with
100
+ // the row needs no directions to somewhere else.
101
+ if (explained === 0) {
102
+ lines.push(` (the run projection carries no message; try \`${bin} tasks show ${taskRef}\` → executionError)`);
103
+ }
104
+ else if (usedActivity) {
105
+ lines.push(` (from the activity log — \`${bin} tasks activity ${taskRef} --json\` → details.error)`);
106
+ }
107
+ return lines;
108
+ }
32
109
  class TasksRuns extends base_command_1.BaseCommand {
33
110
  static description = 'List execution runs for a task';
34
111
  static examples = [
@@ -51,12 +128,26 @@ class TasksRuns extends base_command_1.BaseCommand {
51
128
  catch (err) {
52
129
  this.handleApiError(err);
53
130
  }
131
+ const r = response;
132
+ const items = r?.data ?? [];
133
+ // A failed run whose reason the ROW does not carry. Split from "failed" so
134
+ // the note below cannot keep asserting the projection has no reason field
135
+ // once the server started sending one — a warning that outlives its cause
136
+ // is just a false statement with a warning icon.
137
+ const unexplained = items.filter(runNeedsFailureReason).some((run) => !failureFromRun(run));
54
138
  if (flags.json) {
139
+ // The server payload goes out byte-identical — enriching it would put a
140
+ // CLI-derived reason under a key a caller would read as the server's.
141
+ // The pointer goes to STDERR instead, so `--json` stays parseable and a
142
+ // scripted caller still learns where the message actually lives.
143
+ if (unexplained) {
144
+ this.warn('A failed run here carries `errors` but no `failureReason` — this server predates that ' +
145
+ `field. Read it from \`${this.config.bin} tasks show ${args.id} --json\` (executionError) or ` +
146
+ `\`${this.config.bin} tasks activity ${args.id} --json\` (details.error, keyed by runId).`);
147
+ }
55
148
  this.log(JSON.stringify(response, null, 2));
56
149
  return;
57
150
  }
58
- const r = response;
59
- const items = r?.data ?? [];
60
151
  if (items.length === 0) {
61
152
  this.log('No runs.');
62
153
  return;
@@ -77,6 +168,23 @@ class TasksRuns extends base_command_1.BaseCommand {
77
168
  { key: 'duration', header: 'DURATION' },
78
169
  { key: 'tokens', header: 'TOKENS' },
79
170
  ], (line) => this.log(line));
171
+ // Only once something failed: a healthy list pays nothing for this.
172
+ if (!unexplained)
173
+ return;
174
+ let failures = new Map();
175
+ try {
176
+ const activity = (await data_provider_1.dataService.getTaskActivity(args.id, 200));
177
+ failures = (0, activity_1.runFailuresFromActivity)(activity?.data);
178
+ }
179
+ catch {
180
+ // Best effort by design. The runs listing already succeeded, and a second
181
+ // read that fails must not turn a working command into an error — the
182
+ // block below then says "no reason recorded", which is true of what we
183
+ // can see.
184
+ }
185
+ for (const line of renderRunFailures(items, failures, this.config.bin, args.id)) {
186
+ this.log(line);
187
+ }
80
188
  }
81
189
  }
82
190
  exports.default = TasksRuns;
@@ -1,4 +1,32 @@
1
1
  import { BaseCommand } from '../../base-command';
2
+ /**
3
+ * The failure rows of `tasks show`, when there are any. (S3 dogfood finding)
4
+ *
5
+ * The report behind this said a failed task reports `lastError: null` with no
6
+ * message anywhere a program looks. Half of that is true and half is a naming
7
+ * accident worth separating, because they need different fixes:
8
+ *
9
+ * - `lastError` is not a field. `jq .lastError` on a key that does not exist
10
+ * answers `null`, which is indistinguishable from a field that exists and is
11
+ * empty. The reason IS in `--json`, under `executionError`, alongside
12
+ * `lastFailedAt` and `executionAttempts`.
13
+ * - Nothing in the human view ever printed it. `tasks show` rendered `Status:
14
+ * failed` and then twenty rows that say nothing about why, so there was no
15
+ * way to LEARN the field's name short of dumping the whole object — which is
16
+ * exactly how the reader ended up guessing `lastError`.
17
+ *
18
+ * So the fix is discoverability, not a new field: print what the payload
19
+ * already carries, under a label that names the key a script should read.
20
+ *
21
+ * The rules this function will not break:
22
+ * - Never assert a reason we do not have. A task that failed with no
23
+ * `executionError` gets the pointer and no invented text.
24
+ * - Never claim a failure that is not one. Absent both a failure marker and a
25
+ * failed status, this renders nothing at all.
26
+ *
27
+ * Pure and exported so the wording is testable without an oclif runtime.
28
+ */
29
+ export declare function taskFailureLines(t: Record<string, unknown>, bin?: string): string[];
2
30
  export default class TasksShow extends BaseCommand {
3
31
  static description: string;
4
32
  static examples: string[];
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskFailureLines = taskFailureLines;
3
4
  const core_1 = require("@oclif/core");
4
5
  const data_provider_1 = require("@skrr-ai/data-provider");
5
6
  const api_fetch_1 = require("../../lib/api-fetch");
@@ -8,6 +9,62 @@ const base_command_1 = require("../../base-command");
8
9
  const task_resolver_1 = require("../../lib/task-resolver");
9
10
  const web_url_1 = require("../../lib/web-url");
10
11
  const html_text_1 = require("../../lib/html-text");
12
+ /**
13
+ * The failure rows of `tasks show`, when there are any. (S3 dogfood finding)
14
+ *
15
+ * The report behind this said a failed task reports `lastError: null` with no
16
+ * message anywhere a program looks. Half of that is true and half is a naming
17
+ * accident worth separating, because they need different fixes:
18
+ *
19
+ * - `lastError` is not a field. `jq .lastError` on a key that does not exist
20
+ * answers `null`, which is indistinguishable from a field that exists and is
21
+ * empty. The reason IS in `--json`, under `executionError`, alongside
22
+ * `lastFailedAt` and `executionAttempts`.
23
+ * - Nothing in the human view ever printed it. `tasks show` rendered `Status:
24
+ * failed` and then twenty rows that say nothing about why, so there was no
25
+ * way to LEARN the field's name short of dumping the whole object — which is
26
+ * exactly how the reader ended up guessing `lastError`.
27
+ *
28
+ * So the fix is discoverability, not a new field: print what the payload
29
+ * already carries, under a label that names the key a script should read.
30
+ *
31
+ * The rules this function will not break:
32
+ * - Never assert a reason we do not have. A task that failed with no
33
+ * `executionError` gets the pointer and no invented text.
34
+ * - Never claim a failure that is not one. Absent both a failure marker and a
35
+ * failed status, this renders nothing at all.
36
+ *
37
+ * Pure and exported so the wording is testable without an oclif runtime.
38
+ */
39
+ function taskFailureLines(t, bin = 'skrr') {
40
+ const error = typeof t.executionError === 'string' ? t.executionError.trim() : '';
41
+ const failedAt = t.lastFailedAt ? formatTime(t.lastFailedAt) : null;
42
+ const isFailed = t.status === 'failed';
43
+ if (!error && !failedAt && !isFailed)
44
+ return [];
45
+ const lines = [];
46
+ const attempts = typeof t.executionAttempts === 'number' ? t.executionAttempts : null;
47
+ const runId = typeof t.lastFinalizedRunId === 'string' ? t.lastFinalizedRunId : null;
48
+ const facts = [
49
+ failedAt,
50
+ attempts != null ? `attempt ${attempts}` : null,
51
+ runId ? `run ${runId}` : null,
52
+ ].filter(Boolean);
53
+ if (facts.length > 0)
54
+ lines.push(`Failed: ${facts.join(' · ')}`);
55
+ if (error) {
56
+ // Labelled with the JSON key, because a reader who needs this in a script
57
+ // needs the key more than they need the prose.
58
+ lines.push(`Error: ${error}`);
59
+ lines.push(' (`executionError` in --json)');
60
+ }
61
+ else if (isFailed) {
62
+ lines.push('Error: none recorded on the task.');
63
+ }
64
+ const ref = String(t.identifier ?? t.id ?? t._id ?? '<task-id>');
65
+ lines.push(`Per-run why: ${bin} tasks activity ${ref} --json (details.error, keyed by runId)`);
66
+ return lines;
67
+ }
11
68
  class TasksShow extends base_command_1.BaseCommand {
12
69
  static description = 'Show details for a single task (enriched)';
13
70
  static examples = [
@@ -112,6 +169,10 @@ class TasksShow extends base_command_1.BaseCommand {
112
169
  if (url)
113
170
  this.log(`URL: ${url}`);
114
171
  this.log(`Status: ${t.status ?? '-'}`);
172
+ // Immediately under Status, because "failed" is the row that raises the
173
+ // question and this is the answer to it.
174
+ for (const line of taskFailureLines(t, this.config.bin))
175
+ this.log(line);
115
176
  this.log(`Executor: ${t.executionMode ?? 'legacy / inferred'}`);
116
177
  this.log(`Priority: ${t.priority ?? '-'}`);
117
178
  this.log(`Parent: ${t.parentId ?? '-'}`);
@@ -1,4 +1,11 @@
1
1
  import { BaseCommand } from '../../base-command';
2
+ /**
3
+ * `<action> (<k=v ...>)`, or just the preview when there is nothing to add.
4
+ *
5
+ * Exported for `timeline.spec.ts`. `formatCell` is what `tasks activity`
6
+ * renders `details` with, so the two surfaces describe one entry the same way.
7
+ */
8
+ export declare function withDetails(item: Record<string, unknown>): string;
2
9
  export default class TasksTimeline extends BaseCommand {
3
10
  static description: string;
4
11
  static args: {
@@ -1,9 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withDetails = withDetails;
3
4
  const core_1 = require("@oclif/core");
4
5
  const data_provider_1 = require("@skrr-ai/data-provider");
5
6
  const base_command_1 = require("../../base-command");
6
7
  const format_1 = require("../../lib/format");
8
+ const tasks_1 = require("../../lib/tasks");
9
+ /**
10
+ * `<action> (<k=v ...>)`, or just the preview when there is nothing to add.
11
+ *
12
+ * Exported for `timeline.spec.ts`. `formatCell` is what `tasks activity`
13
+ * renders `details` with, so the two surfaces describe one entry the same way.
14
+ */
15
+ function withDetails(item) {
16
+ const preview = String(item.preview ?? '-');
17
+ const details = (0, format_1.formatCell)(item.details, '');
18
+ return details ? `${preview} (${details})` : preview;
19
+ }
7
20
  class TasksTimeline extends base_command_1.BaseCommand {
8
21
  static description = 'Show the unified task timeline: work log, comments, lifecycle, and execution';
9
22
  static args = {
@@ -13,7 +26,14 @@ class TasksTimeline extends base_command_1.BaseCommand {
13
26
  json: core_1.Flags.boolean({ description: 'Output JSON' }),
14
27
  limit: core_1.Flags.integer({ default: 50 }),
15
28
  cursor: core_1.Flags.string(),
16
- kind: core_1.Flags.string(),
29
+ // `--kind` names a typed WORK-EVENT kind, not one of the timeline's four
30
+ // sources. Undeclared and undescribed, it read like a source filter and
31
+ // silently accepted anything; the server now refuses an unknown kind, and
32
+ // declaring the options here refuses a typo before it costs a round trip.
33
+ kind: core_1.Flags.string({
34
+ description: 'Show only work events of this kind (excludes comments, lifecycle, execution)',
35
+ options: [...tasks_1.TASK_WORK_EVENT_KINDS],
36
+ }),
17
37
  run: core_1.Flags.string(),
18
38
  actor: core_1.Flags.string({ options: ['user', 'agent', 'system'] }),
19
39
  };
@@ -41,9 +61,20 @@ class TasksTimeline extends base_command_1.BaseCommand {
41
61
  time: String(item.occurredAt ?? '-')
42
62
  .replace('T', ' ')
43
63
  .slice(0, 19),
44
- source: String(item.kind ?? '-'),
64
+ // A work event's own kind is the thing you came to see. Showing the
65
+ // source alone collapsed blocker, decision, checkpoint and evidence
66
+ // into one indistinguishable `work_event`.
67
+ source: item.eventKind
68
+ ? `${String(item.kind ?? '-')}:${String(item.eventKind)}`
69
+ : String(item.kind ?? '-'),
45
70
  actor: String(item.actor?.kind ?? '-'),
46
- detail: (0, format_1.truncate)(String(item.preview ?? '-'), 90),
71
+ // A lifecycle row's preview is the bare action, so `status_changed`
72
+ // appeared three times in a row without ever naming a status. The
73
+ // answer is in `details` (`status=blocked`), which `tasks activity`
74
+ // has always shown off the same entries. Appended rather than
75
+ // substituted: the action is still what happened, `details` is what
76
+ // it happened to.
77
+ detail: (0, format_1.truncate)(withDetails(item), 90),
47
78
  run: String(item.runId ?? '-'),
48
79
  })), [
49
80
  { key: 'time', header: 'TIME' },
@@ -16,6 +16,26 @@ export default class TasksUpdate extends BaseCommand {
16
16
  'start-date': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
17
17
  'due-date': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
18
18
  estimate: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
19
+ /**
20
+ * Declared, hidden, and refused — on purpose. (S4 dogfood finding)
21
+ *
22
+ * `--status` is the single most likely flag a person types at `tasks
23
+ * update`, and undeclared it produced oclif's `Nonexistent flag: --status`
24
+ * followed by a ~160-line dump of this command's help. The answer is in
25
+ * that dump — this command's own description says status changes go through
26
+ * `tasks move` — buried under thirty other flags.
27
+ *
28
+ * Declaring it routes `--status` past the parser and into
29
+ * `taskUpdateMutationGuard`, which already refuses a `status` key arriving
30
+ * via `--from-json` with the exact sentence naming `tasks move`. So the two
31
+ * ways of asking for the same wrong thing now get the same right answer,
32
+ * the same `TASK_STATUS_REQUIRES_MOVE` code in `--json`, and no help dump —
33
+ * and the copy has ONE home rather than a second parallel string.
34
+ *
35
+ * `hidden` because it is not a capability: help must not advertise a flag
36
+ * whose only behaviour is to explain itself.
37
+ */
38
+ status: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
19
39
  'duplicate-of': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
20
40
  goal: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
21
41
  cycle: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;