@skrr-ai/cli 0.1.10 → 0.1.12

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 (120) 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 +15 -0
  10. package/dist/commands/daemon/install.js +39 -2
  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 +53 -0
  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 +57 -9
  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.js +6 -2
  83. package/dist/lib/daemon-installer.d.ts +62 -0
  84. package/dist/lib/daemon-installer.js +247 -0
  85. package/dist/lib/daemon-setup.d.ts +49 -0
  86. package/dist/lib/daemon-setup.js +103 -0
  87. package/dist/lib/daemonBroker.d.ts +7 -1
  88. package/dist/lib/daemonBroker.js +71 -0
  89. package/dist/lib/daemonHandoff.d.ts +47 -0
  90. package/dist/lib/daemonHandoff.js +304 -0
  91. package/dist/lib/exec-oversky.d.ts +30 -0
  92. package/dist/lib/exec-oversky.js +41 -0
  93. package/dist/lib/format.d.ts +7 -0
  94. package/dist/lib/format.js +27 -5
  95. package/dist/lib/login.js +22 -2
  96. package/dist/lib/sky-code.js +1 -1
  97. package/dist/lib/task-transcript.d.ts +9 -0
  98. package/dist/lib/task-transcript.js +13 -2
  99. package/dist/lib/tasks.d.ts +34 -0
  100. package/dist/lib/tasks.js +70 -1
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.d.ts +50 -0
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.js +67 -0
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -0
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +21 -1
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseKeys.d.ts +87 -0
  106. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseKeys.js +94 -0
  107. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.d.ts +161 -0
  108. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.js +235 -0
  109. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.d.ts +50 -0
  110. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.js +59 -0
  111. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -0
  112. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -0
  113. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseKeys.d.ts +87 -0
  114. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseKeys.js +91 -0
  115. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.d.ts +161 -0
  116. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.js +227 -0
  117. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  118. package/dist/node_modules/@skrr-ai/data-provider/index.js +15 -4
  119. package/oclif.manifest.json +1831 -1598
  120. package/package.json +5 -3
@@ -56,8 +56,47 @@ class TasksActionability extends base_command_1.BaseCommand {
56
56
  this.log(`Eligible: ${a.actorRefs.map((ref) => `${ref.kind}:${ref.id}`).join(', ')}`);
57
57
  }
58
58
  if (a.allowedActions.length) {
59
- this.log(`Actions: ${a.allowedActions.map((action) => action.action).join(', ')}`);
59
+ // These are SERVER action names, not commands, and printing them bare as
60
+ // `Actions: retry_task, resolve_task_hold` reads as a list of things to
61
+ // run — so a reader tries `skrr resolve_task_hold` and finds nothing.
62
+ // That gap is what OSK-3815 fixed for one of them by adding `tasks
63
+ // retry`; naming the verb beside each is the rest of it.
64
+ //
65
+ // Unmapped actions print bare on purpose. Inventing a plausible verb for
66
+ // an action whose resolution is genuinely a review conversation is worse
67
+ // than saying nothing — the reader would chase a command that does not
68
+ // do what the name suggests.
69
+ const width = Math.max(...a.allowedActions.map((action) => action.action.length));
70
+ this.log('Actions:');
71
+ for (const { action } of a.allowedActions) {
72
+ const verb = ACTION_VERBS[action];
73
+ this.log(` ${action.padEnd(width)}${verb ? ` → ${verb}` : ''}`);
74
+ }
60
75
  }
61
76
  }
62
77
  }
63
78
  exports.default = TasksActionability;
79
+ /**
80
+ * Server action name → the `skrr` command that performs it.
81
+ *
82
+ * Only mappings verified against the command surface are listed. Four actions
83
+ * are deliberately absent: `request_changes` and `inspect_workflow` resolve
84
+ * through a review conversation rather than one verb, and `inspect_dependency`
85
+ * / `inspect_workflow` are readings rather than actions — the blocker they
86
+ * point at is already printed above as the reason.
87
+ */
88
+ const ACTION_VERBS = {
89
+ answer_human_input: 'skrr tasks input answer <id>',
90
+ assign_task: 'skrr tasks assign <id> --agent <agent-id>',
91
+ complete_task: 'skrr tasks complete <id>',
92
+ dismiss_human_input: 'skrr tasks input dismiss <id>',
93
+ interrupt_task: 'skrr tasks interrupt <id>',
94
+ request_approval: 'skrr tasks approvals create <id>',
95
+ resolve_approval: 'skrr tasks approvals resolve <id>',
96
+ resolve_task_hold: 'skrr tasks move <id> --status <status>',
97
+ retry_task: 'skrr tasks retry <id>',
98
+ set_execution_mode: 'skrr tasks update <id> --execution-mode <agent|human>',
99
+ start_task: 'skrr tasks start <id>',
100
+ triage_task: 'skrr tasks triage <id>',
101
+ unschedule_task: 'skrr tasks unschedule <id>',
102
+ };
@@ -17,6 +17,35 @@ export type ActivityRow = {
17
17
  details: string;
18
18
  };
19
19
  export declare function activityRow(entry: ActivityEntry): ActivityRow;
20
+ export type RunFailure = {
21
+ runId: string;
22
+ message: string;
23
+ code: string | null;
24
+ causeClass: string | null;
25
+ };
26
+ /**
27
+ * The failure reasons the activity log carries, indexed by run. (S3 dogfood finding)
28
+ *
29
+ * `tasks runs` shows `errors: 1` beside a `failed` run and no message, because
30
+ * the run projection does not carry one — that gap is server-side and this
31
+ * cannot close it. What the activity log DOES carry is the full string, on the
32
+ * `execution_failed` entry, with the `runId` right beside it:
33
+ *
34
+ * { action: 'execution_failed',
35
+ * details: { runId, error, code: 'MCP_CONNECTION_UNHEALTHY', causeClass: 'agent_error' } }
36
+ *
37
+ * so the join is exact rather than a guess. Reading it is the honest half of
38
+ * the fix: the reason is surfaced where it is genuinely known, and never
39
+ * synthesised where it is not.
40
+ *
41
+ * Deliberately keyed ONLY on an explicit `details.runId`. An entry without one
42
+ * is skipped rather than attributed to the newest run — a failure pinned to the
43
+ * wrong run reads exactly like a true one, and is worse than none.
44
+ *
45
+ * Last entry wins per run: activity is append-ordered, so a re-finalization
46
+ * that recorded a better reason should not be shadowed by the first attempt.
47
+ */
48
+ export declare function runFailuresFromActivity(entries: unknown): Map<string, RunFailure>;
20
49
  export default class TasksActivity extends BaseCommand {
21
50
  static description: string;
22
51
  static examples: string[];
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.activityRow = activityRow;
4
+ exports.runFailuresFromActivity = runFailuresFromActivity;
4
5
  const core_1 = require("@oclif/core");
5
6
  const data_provider_1 = require("@skrr-ai/data-provider");
6
7
  const base_command_1 = require("../../base-command");
@@ -13,6 +14,52 @@ function activityRow(entry) {
13
14
  details: (0, format_1.formatCell)(entry.details ?? entry.metadata),
14
15
  };
15
16
  }
17
+ /**
18
+ * The failure reasons the activity log carries, indexed by run. (S3 dogfood finding)
19
+ *
20
+ * `tasks runs` shows `errors: 1` beside a `failed` run and no message, because
21
+ * the run projection does not carry one — that gap is server-side and this
22
+ * cannot close it. What the activity log DOES carry is the full string, on the
23
+ * `execution_failed` entry, with the `runId` right beside it:
24
+ *
25
+ * { action: 'execution_failed',
26
+ * details: { runId, error, code: 'MCP_CONNECTION_UNHEALTHY', causeClass: 'agent_error' } }
27
+ *
28
+ * so the join is exact rather than a guess. Reading it is the honest half of
29
+ * the fix: the reason is surfaced where it is genuinely known, and never
30
+ * synthesised where it is not.
31
+ *
32
+ * Deliberately keyed ONLY on an explicit `details.runId`. An entry without one
33
+ * is skipped rather than attributed to the newest run — a failure pinned to the
34
+ * wrong run reads exactly like a true one, and is worse than none.
35
+ *
36
+ * Last entry wins per run: activity is append-ordered, so a re-finalization
37
+ * that recorded a better reason should not be shadowed by the first attempt.
38
+ */
39
+ function runFailuresFromActivity(entries) {
40
+ const out = new Map();
41
+ if (!Array.isArray(entries))
42
+ return out;
43
+ for (const raw of entries) {
44
+ const entry = raw;
45
+ if (!entry || typeof entry !== 'object')
46
+ continue;
47
+ const details = entry.details;
48
+ if (!details || typeof details !== 'object')
49
+ continue;
50
+ const runId = typeof details.runId === 'string' ? details.runId.trim() : '';
51
+ const message = typeof details.error === 'string' ? details.error.trim() : '';
52
+ if (!runId || !message)
53
+ continue;
54
+ out.set(runId, {
55
+ runId,
56
+ message,
57
+ code: typeof details.code === 'string' ? details.code : null,
58
+ causeClass: typeof details.causeClass === 'string' ? details.causeClass : null,
59
+ });
60
+ }
61
+ return out;
62
+ }
16
63
  /** Same contract as `tasks monitor`'s timestamp column — never throws, never prints "undefined". */
17
64
  function formatTime(value) {
18
65
  if (!value)
@@ -41,6 +41,52 @@ export interface DurableCompletionResult {
41
41
  * old server and fall through to the legacy comment-first path.
42
42
  */
43
43
  export declare function resolveCanonicalCompletionTaskId(taskRef: string, lookup: (taskRef: string) => Promise<unknown>): Promise<string>;
44
+ /**
45
+ * The canonical id AND the status it is in.
46
+ *
47
+ * The status comes free: this lookup already happens on the user path, so the
48
+ * re-completion guard below costs no extra round trip on the common case. It is
49
+ * also the only way to make the success line honest — `tasks complete` printed
50
+ * "Moved task X to done" unconditionally, so a task that was ALREADY done got a
51
+ * report of a state transition that did not happen.
52
+ */
53
+ export declare function resolveCompletionTarget(taskRef: string, lookup: (taskRef: string) => Promise<unknown>): Promise<{
54
+ id: string;
55
+ status: string | null;
56
+ }>;
57
+ /**
58
+ * What a second `tasks complete` on an already-closed task should do.
59
+ *
60
+ * The repo contract calls this "the only terminal closeout", and a task has one
61
+ * canonical outcome — but a second run used to succeed silently, post another
62
+ * TASK_OUTPUT block, and leave `tasks output` rendering the throwaway one as
63
+ * authoritative. The real summary and its artifacts link were still on the task
64
+ * but no longer canonical, which is the worst of the available failures: no
65
+ * error, no warning, exit 0, and a record that now says something different.
66
+ *
67
+ * Refusing outright would break the case that makes retries safe. A completion
68
+ * whose response is lost — a flaky connection, a re-run script — SHOULD be able
69
+ * to run again, and today it collapses server-side on the shared
70
+ * `X-Idempotency-Key`. Turning that into a hard failure would force every
71
+ * caller to write its own is-it-already-done check.
72
+ *
73
+ * So the axis is not "again or not", it is "the SAME again, or something
74
+ * different":
75
+ *
76
+ * - identical report → `noop`, exit 0. Nothing to do, and saying so is more
77
+ * honest than re-posting an identical block.
78
+ * - differing report → `refuse`. Superseding a canonical outcome is a real
79
+ * decision, so it takes `--amend` and says so.
80
+ * - not yet terminal → `proceed`, unchanged.
81
+ *
82
+ * Pure, so the rule is testable without a server.
83
+ */
84
+ export declare function decideRecompletion(opts: {
85
+ priorStatus: string | null;
86
+ canonicalBody: string | null;
87
+ incomingBody: string;
88
+ amend: boolean;
89
+ }): 'proceed' | 'noop' | 'refuse';
44
90
  /** A 404 from the /complete endpoint means an old server without the route. */
45
91
  export declare function isCompleteEndpointMissing(err: unknown): boolean;
46
92
  /**
@@ -90,6 +136,7 @@ export default class TasksComplete extends BaseCommand {
90
136
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
91
137
  task: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
92
138
  'from-json': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
139
+ amend: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
93
140
  status: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
94
141
  summary: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
95
142
  artifacts: import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TaskCompletionMismatchError = void 0;
4
4
  exports.resolveCanonicalCompletionTaskId = resolveCanonicalCompletionTaskId;
5
+ exports.resolveCompletionTarget = resolveCompletionTarget;
6
+ exports.decideRecompletion = decideRecompletion;
5
7
  exports.isCompleteEndpointMissing = isCompleteEndpointMissing;
6
8
  exports.completeTaskDurably = completeTaskDurably;
7
9
  const core_1 = require("@oclif/core");
@@ -16,7 +18,9 @@ const prompt_1 = require("../../lib/prompt");
16
18
  const task_report_1 = require("../../lib/task-report");
17
19
  const task_resolver_1 = require("../../lib/task-resolver");
18
20
  const task_instruction_offer_1 = require("../../lib/task-instruction-offer");
21
+ const task_transcript_1 = require("../../lib/task-transcript");
19
22
  const web_url_1 = require("../../lib/web-url");
23
+ const tasks_1 = require("../../lib/tasks");
20
24
  /**
21
25
  * Mutation endpoints require the task's canonical UUID even though the CLI
22
26
  * accepts human-readable refs and titles. Resolve the display reference before
@@ -24,12 +28,65 @@ const web_url_1 = require("../../lib/web-url");
24
28
  * old server and fall through to the legacy comment-first path.
25
29
  */
26
30
  async function resolveCanonicalCompletionTaskId(taskRef, lookup) {
31
+ return (await resolveCompletionTarget(taskRef, lookup)).id;
32
+ }
33
+ /** Terminal workflow states — a task in one of these has already been closed out. */
34
+ const TERMINAL_STATUSES = new Set(['done', 'failed']);
35
+ /**
36
+ * The canonical id AND the status it is in.
37
+ *
38
+ * The status comes free: this lookup already happens on the user path, so the
39
+ * re-completion guard below costs no extra round trip on the common case. It is
40
+ * also the only way to make the success line honest — `tasks complete` printed
41
+ * "Moved task X to done" unconditionally, so a task that was ALREADY done got a
42
+ * report of a state transition that did not happen.
43
+ */
44
+ async function resolveCompletionTarget(taskRef, lookup) {
27
45
  const task = (await lookup(taskRef));
28
46
  const id = typeof task?.id === 'string' ? task.id.trim() : '';
29
47
  if (!id) {
30
48
  throw new Error(`Task ${taskRef} did not resolve to a canonical id.`);
31
49
  }
32
- return id;
50
+ return { id, status: typeof task?.status === 'string' ? task.status : null };
51
+ }
52
+ /**
53
+ * What a second `tasks complete` on an already-closed task should do.
54
+ *
55
+ * The repo contract calls this "the only terminal closeout", and a task has one
56
+ * canonical outcome — but a second run used to succeed silently, post another
57
+ * TASK_OUTPUT block, and leave `tasks output` rendering the throwaway one as
58
+ * authoritative. The real summary and its artifacts link were still on the task
59
+ * but no longer canonical, which is the worst of the available failures: no
60
+ * error, no warning, exit 0, and a record that now says something different.
61
+ *
62
+ * Refusing outright would break the case that makes retries safe. A completion
63
+ * whose response is lost — a flaky connection, a re-run script — SHOULD be able
64
+ * to run again, and today it collapses server-side on the shared
65
+ * `X-Idempotency-Key`. Turning that into a hard failure would force every
66
+ * caller to write its own is-it-already-done check.
67
+ *
68
+ * So the axis is not "again or not", it is "the SAME again, or something
69
+ * different":
70
+ *
71
+ * - identical report → `noop`, exit 0. Nothing to do, and saying so is more
72
+ * honest than re-posting an identical block.
73
+ * - differing report → `refuse`. Superseding a canonical outcome is a real
74
+ * decision, so it takes `--amend` and says so.
75
+ * - not yet terminal → `proceed`, unchanged.
76
+ *
77
+ * Pure, so the rule is testable without a server.
78
+ */
79
+ function decideRecompletion(opts) {
80
+ const { priorStatus, canonicalBody, incomingBody, amend } = opts;
81
+ if (!priorStatus || !TERMINAL_STATUSES.has(priorStatus))
82
+ return 'proceed';
83
+ if (amend)
84
+ return 'proceed';
85
+ // No canonical block to disagree with — a task closed by a path that skipped
86
+ // it, so writing one is an addition rather than a replacement.
87
+ if (canonicalBody === null)
88
+ return 'proceed';
89
+ return canonicalBody.trim() === incomingBody.trim() ? 'noop' : 'refuse';
33
90
  }
34
91
  /** A 404 from the /complete endpoint means an old server without the route. */
35
92
  function isCompleteEndpointMissing(err) {
@@ -144,6 +201,9 @@ class TasksComplete extends base_command_1.BaseCommand {
144
201
  'from-json': core_1.Flags.string({
145
202
  description: 'Path to JSON with taskId, status, summary, artifacts, and verdict; "-" for stdin',
146
203
  }),
204
+ amend: core_1.Flags.boolean({
205
+ description: 'Supersede the canonical outcome of an already-completed task. Without it, a differing report on a closed task is refused',
206
+ }),
147
207
  status: core_1.Flags.string({
148
208
  description: 'TASK_OUTPUT status',
149
209
  options: ['completed', 'failed'],
@@ -221,6 +281,16 @@ class TasksComplete extends base_command_1.BaseCommand {
221
281
  ]) || 'none',
222
282
  verdict: flags.verdict,
223
283
  });
284
+ // `--verdict` is deliberately not enumerated — a reviewer task passes a
285
+ // JSON posture object, and `normalizeReviewPosture` accepts synonyms. But
286
+ // when it recognises nothing it derives the posture from the criteria
287
+ // rather than failing, so a typo is recorded and silently means something
288
+ // else. Warn over the set the server actually matches; never refuse.
289
+ if (!(0, tasks_1.verdictIsRecognized)(input.verdict)) {
290
+ this.warn(`Verdict "${input.verdict}" is not one the server recognises ` +
291
+ `(${tasks_1.RECOGNIZED_TASK_VERDICTS.join(', ')}, or a JSON review posture). ` +
292
+ 'It will be recorded as written, and a review will fall back to what the criteria imply.');
293
+ }
224
294
  let taskIdentity;
225
295
  try {
226
296
  taskIdentity = (0, task_resolver_1.resolveTaskIdentity)({
@@ -316,18 +386,88 @@ class TasksComplete extends base_command_1.BaseCommand {
316
386
  noMove: !!flags['no-move'],
317
387
  };
318
388
  const completionIdempotencyKey = (0, outbox_1.idempotencyKeyFor)('task-complete', taskIdentity.id, JSON.stringify(completionPayload));
389
+ const sessionCompletionPath = autonomousSessionCompletionPath();
390
+ // Resolved OUTSIDE the write's try/catch. It is a GET, so a failure here is
391
+ // not a queueable write — and the re-completion guard has to be able to
392
+ // refuse without `queueWriteOrFail` treating the refusal as an outage worth
393
+ // replaying later.
394
+ //
395
+ // The session endpoint derives its task from the trusted run claim and does
396
+ // not need (or permit) a broad task lookup. The normal user and
397
+ // delegated-daemon path does: task routes read OSK refs, while mutation
398
+ // services key on UUIDs. Passing the ref straight through caused the atomic
399
+ // endpoint's task-level 404 to look like "endpoint missing"; legacy fallback
400
+ // then posted TASK_OUTPUT before its move 404'd.
401
+ let completionTaskId = taskIdentity.id;
402
+ // Captured at resolution so the guard and the success line can both tell
403
+ // "already closed" from "just closed".
404
+ let priorStatus = null;
405
+ if (!sessionCompletionPath) {
406
+ try {
407
+ const target = await resolveCompletionTarget(await this.resolveTaskReference(taskIdentity.id), (taskRef) => data_provider_1.dataService.getTask(taskRef, { enrich: false }));
408
+ completionTaskId = target.id;
409
+ priorStatus = target.status;
410
+ }
411
+ catch (err) {
412
+ this.handleApiError(err);
413
+ }
414
+ }
415
+ // A task that is already closed gets one extra read, and only then — the
416
+ // first completion of a task never pays for this.
417
+ if (decideRecompletion({
418
+ priorStatus,
419
+ canonicalBody: '',
420
+ incomingBody: body,
421
+ amend: flags.amend,
422
+ }) !== 'proceed') {
423
+ let canonicalBody = null;
424
+ try {
425
+ canonicalBody = (await (0, task_transcript_1.fetchLatestTaskOutputComment)(completionTaskId))?.body ?? null;
426
+ }
427
+ catch {
428
+ // Unreadable history must not block a completion: falling through to
429
+ // `proceed` is exactly today's behaviour, which is the safe direction
430
+ // for a guard whose whole job is to protect a record it cannot see.
431
+ canonicalBody = null;
432
+ }
433
+ const decision = decideRecompletion({
434
+ priorStatus,
435
+ canonicalBody,
436
+ incomingBody: body,
437
+ amend: flags.amend,
438
+ });
439
+ if (decision === 'noop') {
440
+ const url = (0, web_url_1.taskUrlFrom)(this.cliConfig.baseURL, { id: completionTaskId });
441
+ if (flags.json) {
442
+ this.log(JSON.stringify({
443
+ taskId: completionTaskId,
444
+ status: priorStatus,
445
+ alreadyCompleted: true,
446
+ changed: false,
447
+ url: url ? `${url}#task-outcome` : url,
448
+ }, null, 2));
449
+ return;
450
+ }
451
+ this.log(`Already ${priorStatus}; this report is identical to the canonical one — nothing to do.`);
452
+ if (url)
453
+ this.log(`Outcome: ${url}#task-outcome`);
454
+ return;
455
+ }
456
+ if (decision === 'refuse') {
457
+ this.failWithCliError({
458
+ message: `Task ${completionTaskId} is already ${priorStatus}, and this report differs from its canonical ` +
459
+ `outcome — completing again would silently replace it (artifacts included).\n` +
460
+ ` → re-run with \`--amend\` to supersede it deliberately, or read what is there now with ` +
461
+ `\`${this.config.bin} tasks output ${completionTaskId}\`.`,
462
+ code: 'TASK_ALREADY_COMPLETED',
463
+ exit: 1,
464
+ retryable: false,
465
+ details: { taskId: completionTaskId, status: priorStatus },
466
+ });
467
+ }
468
+ }
319
469
  let result;
320
470
  try {
321
- const sessionCompletionPath = autonomousSessionCompletionPath();
322
- // The session endpoint derives its task from the trusted run claim and
323
- // does not need (or permit) a broad task lookup. The normal user and
324
- // delegated-daemon path does: task routes read OSK refs, while mutation
325
- // services key on UUIDs. Passing the ref straight through caused the
326
- // atomic endpoint's task-level 404 to look like "endpoint missing";
327
- // legacy fallback then posted TASK_OUTPUT before its move 404'd.
328
- const completionTaskId = sessionCompletionPath
329
- ? taskIdentity.id
330
- : await resolveCanonicalCompletionTaskId(await this.resolveTaskReference(taskIdentity.id), (taskRef) => data_provider_1.dataService.getTask(taskRef, { enrich: false }));
331
471
  result = await completeTaskDurably({
332
472
  taskId: completionTaskId,
333
473
  status,
@@ -409,7 +549,14 @@ class TasksComplete extends base_command_1.BaseCommand {
409
549
  : undefined;
410
550
  this.log(`Posted TASK_OUTPUT comment ${commentId ?? '?'} on task ${completedTaskId}`);
411
551
  if (movedTask) {
412
- this.log(`Moved task ${completedTaskId} to ${movedTask.status}`);
552
+ // Only claim a transition that happened. This printed unconditionally, so
553
+ // `tasks complete` on an already-done task reported "Moved task X to
554
+ // done" when nothing moved — a state change asserted by the one tool the
555
+ // reader is trusting to tell them the state.
556
+ const newStatus = movedTask.status;
557
+ this.log(priorStatus && priorStatus === newStatus
558
+ ? `Task ${completedTaskId} was already ${newStatus}; the outcome was amended.`
559
+ : `Moved task ${completedTaskId} to ${newStatus}`);
413
560
  }
414
561
  if (report) {
415
562
  const url = (0, web_url_1.taskUrlFrom)(this.cliConfig.baseURL, movedTask ?? { id: completedTaskId });
@@ -1,4 +1,30 @@
1
1
  import { BaseCommand } from '../../base-command';
2
+ /**
3
+ * Whether the estimate we asked for survived the write. (S3 dogfood finding)
4
+ *
5
+ * `--estimate 3` exited 0 with empty stderr and stored `null`. The value is
6
+ * dropped because an estimate is a SPACE-owned vocabulary: `assertTaskEstimateAllowed`
7
+ * resolves the scale up the Space's ancestor chain, and a chain with no
8
+ * `estimateScale` anywhere resolves to the `none` default — which allows no
9
+ * value, so the field is simply not persisted. It does not throw, because
10
+ * "this Space does not track estimates" is a legitimate configuration rather
11
+ * than a bad request.
12
+ *
13
+ * That is defensible server-side. What is not defensible is the CLI sending a
14
+ * value, receiving a response that plainly does not contain it, and reporting
15
+ * success — the caller cannot tell "stored" from "discarded" without a second
16
+ * read they have no reason to make.
17
+ *
18
+ * So this is a POSTCONDITION check on the response we already have: no extra
19
+ * round trip, and exact rather than predicted — it reports what the server
20
+ * actually did, including snapping a value onto a coarser scale. It warns and
21
+ * does not fail: the task WAS created, and exiting non-zero over one dropped
22
+ * field would be a worse lie than the silence it replaces.
23
+ *
24
+ * Same shape as `warnDroppedDefaultAssignees` below, which does this for a
25
+ * silently-dropped assignee (OSK-4857).
26
+ */
27
+ export declare function droppedEstimateWarning(requested: number | undefined, written: Record<string, unknown> | null | undefined, spaceId?: string | null): string | null;
2
28
  export default class TasksCreate extends BaseCommand {
3
29
  static description: string;
4
30
  static examples: string[];
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.droppedEstimateWarning = droppedEstimateWarning;
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");
@@ -7,6 +8,49 @@ const prompt_1 = require("../../lib/prompt");
7
8
  const tasks_1 = require("../../lib/tasks");
8
9
  const task_instruction_offer_1 = require("../../lib/task-instruction-offer");
9
10
  const web_url_1 = require("../../lib/web-url");
11
+ /**
12
+ * Whether the estimate we asked for survived the write. (S3 dogfood finding)
13
+ *
14
+ * `--estimate 3` exited 0 with empty stderr and stored `null`. The value is
15
+ * dropped because an estimate is a SPACE-owned vocabulary: `assertTaskEstimateAllowed`
16
+ * resolves the scale up the Space's ancestor chain, and a chain with no
17
+ * `estimateScale` anywhere resolves to the `none` default — which allows no
18
+ * value, so the field is simply not persisted. It does not throw, because
19
+ * "this Space does not track estimates" is a legitimate configuration rather
20
+ * than a bad request.
21
+ *
22
+ * That is defensible server-side. What is not defensible is the CLI sending a
23
+ * value, receiving a response that plainly does not contain it, and reporting
24
+ * success — the caller cannot tell "stored" from "discarded" without a second
25
+ * read they have no reason to make.
26
+ *
27
+ * So this is a POSTCONDITION check on the response we already have: no extra
28
+ * round trip, and exact rather than predicted — it reports what the server
29
+ * actually did, including snapping a value onto a coarser scale. It warns and
30
+ * does not fail: the task WAS created, and exiting non-zero over one dropped
31
+ * field would be a worse lie than the silence it replaces.
32
+ *
33
+ * Same shape as `warnDroppedDefaultAssignees` below, which does this for a
34
+ * silently-dropped assignee (OSK-4857).
35
+ */
36
+ function droppedEstimateWarning(requested, written, spaceId) {
37
+ if (requested === undefined || requested === null)
38
+ return null;
39
+ if (!written || typeof written !== 'object')
40
+ return null;
41
+ const stored = written.estimate;
42
+ if (stored === requested)
43
+ return null;
44
+ const subject = spaceId ? `space ${spaceId}` : 'this Space';
45
+ if (typeof stored === 'number') {
46
+ return (`--estimate ${requested} was stored as ${stored}: the estimate scale on ${subject} does ` +
47
+ `not allow ${requested}, so the server snapped it to the nearest value it does.`);
48
+ }
49
+ return (`--estimate ${requested} was NOT stored — the task came back with no estimate. ` +
50
+ `Estimates are owned by the Space: ${subject} (and every ancestor it inherits from) has no ` +
51
+ 'estimate scale, so the value is discarded rather than rejected. ' +
52
+ 'Set one with `spaces update <space> --estimate-scale <scale> --estimate-migration <clear|migrate>`.');
53
+ }
10
54
  class TasksCreate extends base_command_1.BaseCommand {
11
55
  static description = 'Create a new task';
12
56
  static examples = [
@@ -100,7 +144,17 @@ class TasksCreate extends base_command_1.BaseCommand {
100
144
  }),
101
145
  'start-date': core_1.Flags.string({ description: 'Planned start date (ISO string)' }),
102
146
  'due-date': core_1.Flags.string({ description: 'Due date (ISO string)' }),
103
- estimate: core_1.Flags.integer({ description: 'Estimate value from the Space estimate scale' }),
147
+ estimate: core_1.Flags.integer({
148
+ // Names the prerequisite AND how to satisfy it. The first version of this
149
+ // help said the scale was reachable only from the web UI, which was true
150
+ // when written and stopped being true the moment `spaces update` grew the
151
+ // flag — a help string asserting a limitation has to be revisited when the
152
+ // limitation is lifted, or it becomes the most authoritative wrong answer
153
+ // in the product.
154
+ description: 'Estimate value from the Space estimate scale. Requires a scale on the Space (or an ' +
155
+ 'ancestor it inherits from); with none set, the server drops the value. Set one with ' +
156
+ '`spaces update <space> --estimate-scale <scale> --estimate-migration <clear|migrate>`.',
157
+ }),
104
158
  tag: core_1.Flags.string({ description: 'Tag (repeatable)', multiple: true }),
105
159
  'label-id': core_1.Flags.string({
106
160
  description: 'Attach a task label by id (repeatable). Manage labels with `skrr tasks labels`. ' +
@@ -333,6 +387,11 @@ class TasksCreate extends base_command_1.BaseCommand {
333
387
  }
334
388
  }
335
389
  const url = (0, web_url_1.taskUrlFrom)(this.cliConfig.baseURL, created);
390
+ // Before either output path, and via `warn` so it reaches stderr — `--json`
391
+ // stdout stays a parseable task row.
392
+ const estimateWarning = droppedEstimateWarning(flags.estimate, created, payload.spaceId);
393
+ if (estimateWarning)
394
+ this.warn(estimateWarning);
336
395
  if (flags.json) {
337
396
  // The raw task row alone cannot tell a script the assignee it implicitly
338
397
  // asked for was dropped (`assigneeAgentIds: []`) from "nobody was ever
@@ -1,6 +1,8 @@
1
1
  import { BaseCommand } from '../../../base-command';
2
2
  export default class TasksEventsAppend extends BaseCommand {
3
+ static summary: string;
3
4
  static description: string;
5
+ static examples: string[];
4
6
  static args: {
5
7
  id: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
6
8
  };
@@ -8,12 +8,38 @@ const api_fetch_1 = require("../../../lib/api-fetch");
8
8
  const session_task_endpoints_1 = require("../../../lib/session-task-endpoints");
9
9
  const tasks_1 = require("../../../lib/tasks");
10
10
  class TasksEventsAppend extends base_command_1.BaseCommand {
11
- static description = 'Append a typed, durable task event';
12
- static args = { id: core_1.Args.string({ required: false, ignoreStdin: true }) };
11
+ static summary = 'Append a typed, durable task event';
12
+ static description = 'Records one meaningful work boundary — a decision, a material change, evidence, a blocker, ' +
13
+ 'or a handoff — on the task you are executing. Not a log of tool calls: an event that is ' +
14
+ 'worth reading six weeks from now. `skrr tasks timeline <id>` reads them back.';
15
+ static examples = [
16
+ '<%= config.bin %> tasks events append <task-id> --kind checkpoint --idempotency-key "<task-id>:first-pass" --summary "Parser handles nested fences"',
17
+ '<%= config.bin %> tasks events append <task-id> --kind decision --idempotency-key "<task-id>:storage-choice" --summary "Chose the CRDT path" --evidence "A direct contentHtml write is reverted by the next render tick"',
18
+ '<%= config.bin %> tasks events append <task-id> --kind verification --idempotency-key "<task-id>:verified" --summary "Repro no longer reproduces" --artifact <commit-sha>',
19
+ ];
20
+ static args = {
21
+ id: core_1.Args.string({
22
+ required: false,
23
+ ignoreStdin: true,
24
+ description: 'Task ID. Optional inside a task runtime, which supplies it',
25
+ }),
26
+ };
13
27
  static flags = {
14
- task: core_1.Flags.string(),
15
- json: core_1.Flags.boolean(),
16
- 'idempotency-key': core_1.Flags.string({ required: true }),
28
+ task: core_1.Flags.string({
29
+ description: 'Task ID; overrides the runtime context and the positional',
30
+ }),
31
+ json: core_1.Flags.boolean({ description: 'Output the created event as JSON' }),
32
+ // The semantics here are the ones that actually bite, and they lived only
33
+ // in the platform's instruction block: a key names ONE update permanently.
34
+ // Retried with the same text the server silently returns the original; with
35
+ // DIFFERENT text it refuses with 409 TASK_EVENT_IDEMPOTENCY_CONFLICT. A
36
+ // caller who does not know that reads a dedupe as a successful write.
37
+ 'idempotency-key': core_1.Flags.string({
38
+ required: true,
39
+ description: 'Stable key naming THIS update permanently, e.g. "<task-id>:verified". Reuse it only to ' +
40
+ 'retry the same update — reused with identical text the original is returned, with ' +
41
+ 'different text the server refuses (409 TASK_EVENT_IDEMPOTENCY_CONFLICT)',
42
+ }),
17
43
  // Enumerated so `--help` lists the kinds and a bad value is rejected by the
18
44
  // flag parser (naming the choices) rather than by the server's opaque
19
45
  // `Invalid request data` / HTTP_400. (OSK-4888)
@@ -22,11 +48,16 @@ class TasksEventsAppend extends base_command_1.BaseCommand {
22
48
  description: 'Event kind at a meaningful work boundary',
23
49
  options: [...tasks_1.TASK_WORK_EVENT_KINDS],
24
50
  }),
25
- summary: core_1.Flags.string({ required: true }),
26
- changed: core_1.Flags.string(),
27
- next: core_1.Flags.string(),
28
- evidence: core_1.Flags.string(),
29
- artifact: core_1.Flags.string({ multiple: true, description: 'Artifact reference; repeatable' }),
51
+ summary: core_1.Flags.string({ required: true, description: 'What is newly true, in one sentence' }),
52
+ changed: core_1.Flags.string({ description: 'What moved — the concrete change this event records' }),
53
+ next: core_1.Flags.string({ description: 'What happens next, or who owns the next decision' }),
54
+ evidence: core_1.Flags.string({
55
+ description: 'Why it is true — an observation, command output, or a before → after',
56
+ }),
57
+ artifact: core_1.Flags.string({
58
+ multiple: true,
59
+ description: 'Durable reference: a file path, commit sha, or URL. Repeatable. "in the conversation" is not an artifact',
60
+ }),
30
61
  };
31
62
  async run() {
32
63
  this.requireAuth();