@skrr-ai/cli 0.1.10 → 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 (108) 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 +30 -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/daemonBroker.d.ts +7 -1
  84. package/dist/lib/daemonBroker.js +71 -0
  85. package/dist/lib/daemonHandoff.d.ts +38 -0
  86. package/dist/lib/daemonHandoff.js +297 -0
  87. package/dist/lib/exec-oversky.d.ts +30 -0
  88. package/dist/lib/exec-oversky.js +41 -0
  89. package/dist/lib/format.d.ts +7 -0
  90. package/dist/lib/format.js +27 -5
  91. package/dist/lib/login.js +22 -2
  92. package/dist/lib/sky-code.js +1 -1
  93. package/dist/lib/task-transcript.d.ts +9 -0
  94. package/dist/lib/task-transcript.js +13 -2
  95. package/dist/lib/tasks.d.ts +34 -0
  96. package/dist/lib/tasks.js +70 -1
  97. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.d.ts +50 -0
  98. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.js +67 -0
  99. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
  100. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +7 -1
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.d.ts +50 -0
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.js +59 -0
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +3 -0
  105. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  106. package/dist/node_modules/@skrr-ai/data-provider/index.js +15 -4
  107. package/oclif.manifest.json +15771 -15538
  108. package/package.json +5 -3
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.describeDoneDisagreement = describeDoneDisagreement;
4
+ exports.describeField = describeField;
3
5
  const core_1 = require("@oclif/core");
4
6
  const data_provider_1 = require("@skrr-ai/data-provider");
5
7
  const base_command_1 = require("../../../base-command");
@@ -35,7 +37,16 @@ class GoalsKeyResultsUpdate extends base_command_1.BaseCommand {
35
37
  description: 'Clear the description (sends null)',
36
38
  exclusive: ['description'],
37
39
  }),
38
- progress: core_1.Flags.integer({ description: 'Progress 0–100 (used in manual mode)' }),
40
+ // The bound is declared, not just described. `--progress 150` and
41
+ // `--progress -5` both came back as a bare `Invalid request data /
42
+ // HTTP_400` from the server — byte-identical for two different mistakes,
43
+ // in a CLI that gives excellent field-level errors for enums. oclif
44
+ // refuses an out-of-range value naming the range, before the round trip.
45
+ progress: core_1.Flags.integer({
46
+ description: 'Progress 0–100 (used in manual mode)',
47
+ min: 0,
48
+ max: 100,
49
+ }),
39
50
  'progress-mode': core_1.Flags.string({
40
51
  description: 'How progress is tracked',
41
52
  options: [...data_provider_1.KEY_RESULT_PROGRESS_MODES],
@@ -86,7 +97,74 @@ class GoalsKeyResultsUpdate extends base_command_1.BaseCommand {
86
97
  return;
87
98
  }
88
99
  const k = updated;
89
- this.log(`Updated key result ${k.id ?? args.krId} on goal ${args.goalId}`);
100
+ // `Updated key result <uuid> on goal <uuid>` echoed two ids the caller had
101
+ // just typed and said nothing about what changed — so a status-only update
102
+ // never confirmed that the value passed is the value that stuck, and the
103
+ // documented next move was to run it again "purely as insurance". Echo the
104
+ // patched fields as the SERVER now holds them, not as they were sent.
105
+ const echo = Object.keys(data)
106
+ .map((field) => `${field}=${describeField(k, field)}`)
107
+ .join(' ');
108
+ this.log(`Updated key result ${k.id ?? args.krId} on goal ${args.goalId}: ${echo}`);
109
+ // The write echoes the number back, so without this the caller has every
110
+ // reason to believe it took — and then `key-results list` shows `-`.
111
+ if (k.progressMode === 'auto' && flags.progress !== undefined) {
112
+ this.warn(`The value is stored. ${AUTO_MODE_NOTE}`);
113
+ }
114
+ // `done` beside `25%` on one row, with nothing reconciling them. The two
115
+ // ARE independent axes — a key result can be closed out incomplete, and
116
+ // pulling progress to 100 here would be the CLI inventing a policy the
117
+ // server does not have, destroying the recorded number in the process. So
118
+ // say which one is which instead, at the moment they are made to disagree,
119
+ // rather than leaving it to be discovered from a list days later.
120
+ const progress = (0, data_provider_1.resolveKeyResultProgress)(k);
121
+ const disagreement = describeDoneDisagreement(flags.status, progress);
122
+ if (disagreement)
123
+ this.warn(disagreement);
90
124
  }
91
125
  }
92
126
  exports.default = GoalsKeyResultsUpdate;
127
+ /**
128
+ * The sentence to print when `done` and a sub-100 progress are made to
129
+ * disagree, or `null` when they do not. Exported for `update.spec.ts`.
130
+ *
131
+ * Returns a warning rather than reconciling the two: pulling progress to 100
132
+ * would be the CLI inventing a policy the server does not have, and it would
133
+ * destroy the recorded number on the way.
134
+ */
135
+ function describeDoneDisagreement(status, progress) {
136
+ if (status !== 'done' || typeof progress !== 'number' || progress >= 100)
137
+ return null;
138
+ return (`Status is 'done' but progress is ${progress}%. They are independent: status is the verdict, ` +
139
+ `progress is the measure, and a key result may legitimately be closed out incomplete. ` +
140
+ `If you meant it to read as finished, also pass --progress 100.`);
141
+ }
142
+ /** How the server now holds one patched field, for the confirmation line. */
143
+ function describeField(kr, field) {
144
+ const value = kr[field];
145
+ if (value === null)
146
+ return 'cleared';
147
+ if (value === undefined)
148
+ return '(not returned)';
149
+ return typeof value === 'string' ? value : JSON.stringify(value);
150
+ }
151
+ /**
152
+ * Why an `auto` key result reads as `-` from the CLI.
153
+ *
154
+ * Auto progress is computed from tasks linked to THIS key result
155
+ * (`Task.keyResultIds`), and the task↔key-result link is a DOCUMENTED
156
+ * omission from the CLI surface — it sits in `domain-api-parity.spec.ts`'s
157
+ * allowlist with a stated reason. So `--progress-mode auto` is offered by a
158
+ * surface that cannot make it move, and `resolveKeyResultProgress` renders
159
+ * `_computedProgress ?? null` for auto, i.e. `-`, while a `--progress` value
160
+ * you set is stored and returned by `update`.
161
+ *
162
+ * The result was one CLI reporting two different values for one field: the
163
+ * write echoed 40 and the list showed `-`. Saying so at the point of use is
164
+ * the honest fix; silently rendering the stored number instead would hide a
165
+ * real distinction (a manual value is not a computed one), and adding the
166
+ * link command would reverse a recorded decision.
167
+ */
168
+ const AUTO_MODE_NOTE = 'Auto progress is computed from tasks linked to this key result, and linking a task to a key ' +
169
+ 'result is deliberately not in the CLI — so this reads as "-" here until it is linked ' +
170
+ 'elsewhere. Use --progress-mode manual to track a number from the CLI.';
@@ -1,4 +1,50 @@
1
+ import type { GoalPlanNowResponse } from '@skrr-ai/data-provider';
1
2
  import { BaseCommand } from '../../base-command';
3
+ /**
4
+ * Turn the planner's internal `reason` enum into a sentence.
5
+ *
6
+ * These strings are produced by `GoalPlanner` and are not declared anywhere in
7
+ * `@skrr-ai/data-provider`, so this is a rendering table and deliberately NOT a
8
+ * second vocabulary: an unrecognised reason is returned verbatim rather than
9
+ * hidden, which is what keeps a new server reason visible instead of silently
10
+ * dropping to "the planner did something".
11
+ *
12
+ * Dogfooded: `plan-now` printed a bare `note: lock_held`, which reads as "the
13
+ * planner ran and found nothing", when in fact the planner never started.
14
+ */
15
+ export declare function describePlannerReason(reason: string | null | undefined): string | null;
16
+ /**
17
+ * Did the tick fail to run? `processed` is the server's own answer, and it is
18
+ * the field the old renderer ignored while printing three zeros and exiting 0.
19
+ *
20
+ * Only an EXPLICIT `false` counts, on either field. An absent `processed` is
21
+ * treated as "it ran", so a CLI pointed at a server that predates the field
22
+ * keeps working — the old renderer made the same assumption, just without
23
+ * saying so.
24
+ */
25
+ export declare function plannerTickDidNotRun(result: GoalPlanNowResponse | undefined): boolean;
26
+ /** Did the REQUEST give up, as opposed to the tick failing? */
27
+ export declare function isPlannerRequestTimeout(err: unknown): boolean;
28
+ /**
29
+ * What a gateway timeout on a planner tick actually licenses us to say.
30
+ *
31
+ * The shared error path maps every 502/503/504 to "this is a service outage …
32
+ * Nothing was changed", and for a planner tick both halves were wrong when
33
+ * dogfooded: the very next command against the same host answered instantly,
34
+ * and a retry two seconds later came back `lock_held` — proof the first request
35
+ * had been accepted and was still running. "Nothing was changed" is the
36
+ * dangerous half, because it invites a retry of a non-idempotent, task-creating,
37
+ * billable operation.
38
+ *
39
+ * A 504 is a gateway giving up. It carries no information about whether the
40
+ * origin finished the work, so this says exactly that and names the commands
41
+ * that CAN answer it.
42
+ */
43
+ export declare function plannerTimeoutMessage(opts: {
44
+ goalId: string;
45
+ status: number;
46
+ bin: string;
47
+ }): string;
2
48
  /**
3
49
  * `skrr goals plan-now <goal-id>` — run a single planner tick immediately,
4
50
  * independent of the configured cadence (or even when no cadence is set). The
@@ -8,8 +54,12 @@ import { BaseCommand } from '../../base-command';
8
54
  * response reports what the tick did: tasks created, existing tasks refined, and
9
55
  * how many created tasks were also scheduled for auto-execution (only when the
10
56
  * goal's `autoExecute` is on and an assignee resolves). `--space` scopes the
11
- * created tasks to a Mission Space and backfills spaceId on the goal's existing
12
- * tasks.
57
+ * created tasks to a space and backfills spaceId on the goal's existing tasks.
58
+ *
59
+ * The route answers HTTP 200 with `processed: false` when the tick was SKIPPED
60
+ * (a lock held by a running tick, a cadence that is not due, an archived goal).
61
+ * That is not a success, so this command does not report it as one — see the
62
+ * `processed` branch in `run()`.
13
63
  */
14
64
  export default class GoalsPlanNow extends BaseCommand {
15
65
  static description: string;
@@ -22,4 +72,6 @@ export default class GoalsPlanNow extends BaseCommand {
22
72
  space: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
23
73
  };
24
74
  run(): Promise<void>;
75
+ /** A gateway timeout on THIS request, reported as what it is. */
76
+ private failIfRequestTimedOut;
25
77
  }
@@ -1,8 +1,119 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.describePlannerReason = describePlannerReason;
4
+ exports.plannerTickDidNotRun = plannerTickDidNotRun;
5
+ exports.isPlannerRequestTimeout = isPlannerRequestTimeout;
6
+ exports.plannerTimeoutMessage = plannerTimeoutMessage;
3
7
  const core_1 = require("@oclif/core");
4
8
  const data_provider_1 = require("@skrr-ai/data-provider");
5
9
  const base_command_1 = require("../../base-command");
10
+ /**
11
+ * Statuses that mean "this REQUEST gave up", not "the work failed".
12
+ *
13
+ * Same set `agents plan-day` uses, and for the same reason: a planner tick is a
14
+ * live agent turn that routinely outlives the edge's request budget, so the
15
+ * gateway answers while the server keeps running. A 4xx means the tick never
16
+ * started and a 500 means the route itself threw — neither is a timeout, and
17
+ * both are reported by the shared error path.
18
+ */
19
+ const TIMEOUT_STATUSES = new Set([408, 504, 524]);
20
+ /**
21
+ * Reasons whose no-op is CONTENTION or timing — the same call can be expected
22
+ * to run later. Everything else (`goal_archived`, `goal_planner_disabled`, …)
23
+ * needs the user to change something first, so a script retrying it would spin.
24
+ */
25
+ const TRANSIENT_SKIP_REASONS = new Set(['lock_held', 'not_due', 'schedule_superseded']);
26
+ /**
27
+ * Turn the planner's internal `reason` enum into a sentence.
28
+ *
29
+ * These strings are produced by `GoalPlanner` and are not declared anywhere in
30
+ * `@skrr-ai/data-provider`, so this is a rendering table and deliberately NOT a
31
+ * second vocabulary: an unrecognised reason is returned verbatim rather than
32
+ * hidden, which is what keeps a new server reason visible instead of silently
33
+ * dropping to "the planner did something".
34
+ *
35
+ * Dogfooded: `plan-now` printed a bare `note: lock_held`, which reads as "the
36
+ * planner ran and found nothing", when in fact the planner never started.
37
+ */
38
+ function describePlannerReason(reason) {
39
+ if (!reason)
40
+ return null;
41
+ switch (reason) {
42
+ case 'lock_held':
43
+ return 'a planner tick for this goal is already running, so this one did not start';
44
+ case 'not_due':
45
+ return "the goal's next tick is not due yet";
46
+ case 'schedule_superseded':
47
+ return "the goal's cadence changed after this tick was scheduled, so it was dropped";
48
+ case 'goal_deleted':
49
+ return 'the goal no longer exists';
50
+ case 'goal_archived':
51
+ return 'the goal is archived';
52
+ case 'goal_planner_disabled':
53
+ return "the goal's auto-planner has no cadence set";
54
+ case 'missing_goal_id':
55
+ return 'the request carried no goal id';
56
+ case 'at_capacity':
57
+ return 'the goal is already at its maxActiveTasks cap, so no new tasks were created';
58
+ case 'at_capacity_refined':
59
+ return 'the goal is at its maxActiveTasks cap, so existing tasks were refined instead of new ones created';
60
+ case 'coordinator_suppressed':
61
+ return 'another proactive surface had just raised this goal, so no new tasks were created';
62
+ case 'all_creates_failed':
63
+ return 'the planner proposed tasks but none of them could be created';
64
+ case 'no_changes':
65
+ return 'the planner ran and found nothing to add or refine';
66
+ case 'one_voice_busy':
67
+ return 'another agent was already speaking for this owner, so the tick stayed quiet';
68
+ default:
69
+ // `one_voice_<something>` is a family the server composes at run time.
70
+ if (reason.startsWith('one_voice_')) {
71
+ return `the one-voice coordinator suppressed this tick (${reason.slice('one_voice_'.length)})`;
72
+ }
73
+ return reason;
74
+ }
75
+ }
76
+ /**
77
+ * Did the tick fail to run? `processed` is the server's own answer, and it is
78
+ * the field the old renderer ignored while printing three zeros and exiting 0.
79
+ *
80
+ * Only an EXPLICIT `false` counts, on either field. An absent `processed` is
81
+ * treated as "it ran", so a CLI pointed at a server that predates the field
82
+ * keeps working — the old renderer made the same assumption, just without
83
+ * saying so.
84
+ */
85
+ function plannerTickDidNotRun(result) {
86
+ return result?.ok === false || result?.processed === false;
87
+ }
88
+ /** Did the REQUEST give up, as opposed to the tick failing? */
89
+ function isPlannerRequestTimeout(err) {
90
+ const status = err?.status;
91
+ return typeof status === 'number' && TIMEOUT_STATUSES.has(status);
92
+ }
93
+ /**
94
+ * What a gateway timeout on a planner tick actually licenses us to say.
95
+ *
96
+ * The shared error path maps every 502/503/504 to "this is a service outage …
97
+ * Nothing was changed", and for a planner tick both halves were wrong when
98
+ * dogfooded: the very next command against the same host answered instantly,
99
+ * and a retry two seconds later came back `lock_held` — proof the first request
100
+ * had been accepted and was still running. "Nothing was changed" is the
101
+ * dangerous half, because it invites a retry of a non-idempotent, task-creating,
102
+ * billable operation.
103
+ *
104
+ * A 504 is a gateway giving up. It carries no information about whether the
105
+ * origin finished the work, so this says exactly that and names the commands
106
+ * that CAN answer it.
107
+ */
108
+ function plannerTimeoutMessage(opts) {
109
+ const { goalId, status, bin } = opts;
110
+ return (`The planner tick for goal ${goalId} outlived this request (HTTP ${status}). ` +
111
+ 'That is the gateway giving up, not a verdict on the tick: the run may still be ' +
112
+ 'executing and may still create tasks. Nothing here observed whether it finished. ' +
113
+ `Check with \`${bin} goals plan-now ${goalId}\` — a "lock_held" answer means it is still ` +
114
+ `running — or \`${bin} goals tasks ${goalId}\` to see what it produced. ` +
115
+ 'Do not retry blindly: a second tick is a second billable agent run.');
116
+ }
6
117
  /**
7
118
  * `skrr goals plan-now <goal-id>` — run a single planner tick immediately,
8
119
  * independent of the configured cadence (or even when no cadence is set). The
@@ -12,11 +123,15 @@ const base_command_1 = require("../../base-command");
12
123
  * response reports what the tick did: tasks created, existing tasks refined, and
13
124
  * how many created tasks were also scheduled for auto-execution (only when the
14
125
  * goal's `autoExecute` is on and an assignee resolves). `--space` scopes the
15
- * created tasks to a Mission Space and backfills spaceId on the goal's existing
16
- * tasks.
126
+ * created tasks to a space and backfills spaceId on the goal's existing tasks.
127
+ *
128
+ * The route answers HTTP 200 with `processed: false` when the tick was SKIPPED
129
+ * (a lock held by a running tick, a cadence that is not due, an archived goal).
130
+ * That is not a success, so this command does not report it as one — see the
131
+ * `processed` branch in `run()`.
17
132
  */
18
133
  class GoalsPlanNow extends base_command_1.BaseCommand {
19
- static description = 'Run one auto-planner tick for a goal right now';
134
+ static description = 'Run one auto-planner tick for a goal right now (a live agent turn that creates and refines tasks)';
20
135
  static examples = [
21
136
  '<%= config.bin %> goals plan-now <goal-id>',
22
137
  '<%= config.bin %> goals plan-now <goal-id> --space <space-id>',
@@ -28,41 +143,83 @@ class GoalsPlanNow extends base_command_1.BaseCommand {
28
143
  static flags = {
29
144
  json: core_1.Flags.boolean({ description: 'Output the planner result as JSON' }),
30
145
  space: core_1.Flags.string({
31
- description: 'Scope created tasks to this Mission Space (backfills spaceId on existing tasks)',
146
+ description: "Scope created tasks to this space (also backfills spaceId on the goal's existing tasks)",
32
147
  }),
33
148
  };
34
149
  async run() {
35
150
  this.requireAuth();
36
151
  const { args, flags } = await this.parse(GoalsPlanNow);
152
+ // A tick is an LLM run; minutes of silence is normal and used to be
153
+ // indistinguishable from a hung command. stderr, so `--json` stdout stays a
154
+ // single parseable document.
155
+ this.logToStderr(`Running one planner tick for goal ${args.id}. This is a live agent turn and can take minutes.`);
37
156
  let result;
38
157
  try {
39
158
  result = await data_provider_1.dataService.planGoalNow(args.id, flags.space ? { spaceId: flags.space } : {});
40
159
  }
41
160
  catch (err) {
161
+ this.failIfRequestTimedOut(err, args.id);
42
162
  this.handleApiError(err);
43
163
  }
44
- if (flags.json) {
45
- this.log(JSON.stringify(result, null, 2));
46
- return;
47
- }
48
164
  const r = result;
49
- if (!r?.ok) {
50
- this.log(`Planner did not run${r?.reason ? `: ${r.reason}` : '.'}`);
165
+ const reasonText = describePlannerReason(r?.reason);
166
+ if (plannerTickDidNotRun(r)) {
167
+ // The backfill runs BEFORE planning and unconditionally, so a skipped
168
+ // tick can still have moved rows. Saying "nothing happened" would be the
169
+ // same class of lie this whole command is being fixed for.
170
+ const backfilled = r?.backfilledCount
171
+ ? ` ${r.backfilledCount} existing task(s) were still backfilled to the given space.`
172
+ : '';
173
+ const transient = TRANSIENT_SKIP_REASONS.has(r?.reason ?? '');
174
+ const retry = transient
175
+ ? ` Re-run \`${this.config.bin} goals plan-now ${args.id}\` to try again.`
176
+ : '';
177
+ this.failWithCliError({
178
+ message: `The planner did not run for goal ${args.id}` +
179
+ (reasonText ? `: ${reasonText}.` : '.') +
180
+ backfilled +
181
+ retry,
182
+ code: 'PLANNER_TICK_NOT_RUN',
183
+ exit: 1,
184
+ retryable: transient,
185
+ details: r,
186
+ });
187
+ }
188
+ if (flags.json) {
189
+ this.log(JSON.stringify(r, null, 2));
51
190
  return;
52
191
  }
53
192
  this.log(`Planner tick for goal ${args.id}:`);
54
- this.log(` created: ${r.createdCount ?? 0}`);
55
- this.log(` refined: ${r.refinedCount ?? 0}`);
56
- this.log(` scheduled: ${r.scheduledCount ?? 0}`);
57
- if (r.backfilledCount) {
58
- this.log(` backfilled: ${r.backfilledCount}`);
193
+ this.log(` created: ${r?.createdCount ?? 0}`);
194
+ this.log(` refined: ${r?.refinedCount ?? 0}`);
195
+ this.log(` scheduled: ${r?.scheduledCount ?? 0}`);
196
+ if (flags.space || r?.backfilledCount) {
197
+ this.log(` backfilled: ${r?.backfilledCount ?? 0}`);
59
198
  }
60
- if (r.resolvedSpaceId) {
199
+ if (r?.resolvedSpaceId) {
61
200
  this.log(` space: ${r.resolvedSpaceId}`);
62
201
  }
63
- if (r.reason) {
64
- this.log(` note: ${r.reason}`);
202
+ if (reasonText) {
203
+ // The raw enum stays in parentheses: it is what the server logs and what
204
+ // an existing script may already grep for.
205
+ this.log(` note: ${reasonText} (${r?.reason}).`);
65
206
  }
66
207
  }
208
+ /** A gateway timeout on THIS request, reported as what it is. */
209
+ failIfRequestTimedOut(err, goalId) {
210
+ if (!isPlannerRequestTimeout(err))
211
+ return;
212
+ const status = err.status;
213
+ this.failWithCliError({
214
+ message: plannerTimeoutMessage({ goalId, status, bin: this.config.bin }),
215
+ code: 'PLANNER_TICK_TIMED_OUT',
216
+ status,
217
+ exit: 5,
218
+ // Deliberately false. The request may be repeated by a HUMAN who has
219
+ // checked, but `plan-now` is not idempotent, so a script must not treat
220
+ // this as "safe to send again".
221
+ retryable: false,
222
+ });
223
+ }
67
224
  }
68
225
  exports.default = GoalsPlanNow;
@@ -1,16 +1,67 @@
1
1
  import { BaseCommand } from '../../base-command';
2
2
  /**
3
- * `skrr goals planner-config <goal-id>` configure (or disable) the goal's
4
- * auto-planner: the scheduled tick that runs a planner agent to create and
5
- * refine tasks toward the goal.
3
+ * The goal's auto-planner state, as the CLI is willing to state it.
4
+ *
5
+ * Two fields are DERIVED rather than passed through, and both exist because the
6
+ * stored document can disagree with itself:
7
+ *
8
+ * - `enabled` reads `tickIntervalMs`, the one field the server's own disable
9
+ * path clears (`$unset tickIntervalMs` + `$unset nextTickAt`). It is the
10
+ * single source of "is this armed".
11
+ * - `nextTickAt` is forced to null whenever the planner is off. The stored
12
+ * timestamp can SURVIVE a disable: `GoalPlanner.planForGoal` writes
13
+ * `nextTickAt` unconditionally at claim time and again at completion, so a
14
+ * tick that was already in flight when `--disable` ran re-creates the field
15
+ * afterwards. Dogfooded — `goals show --json` reported a `nextTickAt` in the
16
+ * past on a disabled planner while the plain view said `interval: disabled`,
17
+ * i.e. the two renderings of one command disagreed about whether a tick was
18
+ * scheduled. A disabled planner has no next tick; saying so is the only
19
+ * reading that is true from either side.
20
+ *
21
+ * The raw server fields are never rewritten — `goals show --json` still carries
22
+ * `tickIntervalMs` / `nextTickAt` verbatim, and this view sits beside them.
23
+ */
24
+ export type GoalPlannerView = {
25
+ enabled: boolean;
26
+ tickIntervalMs: number | null;
27
+ maxActiveTasks: number | null;
28
+ autoExecute: boolean | null;
29
+ nextTickAt: string | null;
30
+ lastTickedAt: string | null;
31
+ /**
32
+ * `tickLockedAt` — set when a tick claims the goal, unset when it finishes or
33
+ * fails. A value here means a claimed tick has not released the lock: it is
34
+ * either still running or it crashed. We report the timestamp and decline to
35
+ * choose between those two, because the CLI cannot tell them apart.
36
+ */
37
+ tickLockedAt: string | null;
38
+ };
39
+ /** Read the planner state off a Goal document (lean or typed). */
40
+ export declare function goalPlannerView(goal: Record<string, unknown> | undefined | null): GoalPlannerView;
41
+ /**
42
+ * The plain-text planner block, shared by `planner-config` and `goals show` so
43
+ * the two commands cannot drift into describing the same state differently.
44
+ */
45
+ export declare function renderGoalPlannerLines(view: GoalPlannerView): string[];
46
+ /**
47
+ * `skrr goals planner-config <goal-id>` — read, configure, or disable the
48
+ * goal's auto-planner: the scheduled tick that runs a planner agent to create
49
+ * and refine tasks toward the goal.
50
+ *
51
+ * With no flags this READS the config. It used to refuse, which made the only
52
+ * way to answer "is the planner on for this goal?" a write — and for `--disable`
53
+ * that meant turning the automation off to find out it was on. A recurring,
54
+ * task-creating, optionally agent-starting automation must be readable without
55
+ * changing it.
6
56
  *
7
57
  * Wraps `PATCH /api/goals/:goalId/planner-config` (data-provider
8
- * `updateGoalPlannerConfig`). Setting a cadence enables the planner; `--disable`
9
- * (which sends `tickIntervalMs: null`) turns it off. The interval flags mirror
10
- * the trigger-create convention elsewhere in this CLI: `--interval-minutes` for
11
- * the ergonomic path, `--interval-ms` for the raw value. The server owns the
12
- * bounds (interval 60s–30d, maxActiveTasks 1–50) and rejects out-of-range
13
- * values, surfaced here via the standard error path.
58
+ * `updateGoalPlannerConfig`) for writes and `GET /api/goals/:goalId` for the
59
+ * read. Setting a cadence enables the planner; `--disable` (which sends
60
+ * `tickIntervalMs: null`) turns it off. The interval flags mirror the
61
+ * trigger-create convention elsewhere in this CLI: `--interval-minutes` for the
62
+ * ergonomic path, `--interval-ms` for the raw value. The server owns the bounds
63
+ * (interval 60s–30d, maxActiveTasks 1–50) and rejects out-of-range values,
64
+ * surfaced here via the standard error path.
14
65
  */
15
66
  export default class GoalsPlannerConfig extends BaseCommand {
16
67
  static description: string;
@@ -1,24 +1,78 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.goalPlannerView = goalPlannerView;
4
+ exports.renderGoalPlannerLines = renderGoalPlannerLines;
3
5
  const core_1 = require("@oclif/core");
4
6
  const data_provider_1 = require("@skrr-ai/data-provider");
5
7
  const base_command_1 = require("../../base-command");
8
+ function str(value) {
9
+ return typeof value === 'string' && value ? value : null;
10
+ }
11
+ /** Read the planner state off a Goal document (lean or typed). */
12
+ function goalPlannerView(goal) {
13
+ const g = (goal ?? {});
14
+ const tickIntervalMs = typeof g.tickIntervalMs === 'number' ? g.tickIntervalMs : null;
15
+ const enabled = tickIntervalMs != null;
16
+ return {
17
+ enabled,
18
+ tickIntervalMs,
19
+ maxActiveTasks: typeof g.maxActiveTasks === 'number' ? g.maxActiveTasks : null,
20
+ autoExecute: typeof g.autoExecute === 'boolean' ? g.autoExecute : null,
21
+ nextTickAt: enabled ? str(g.nextTickAt) : null,
22
+ lastTickedAt: str(g.lastTickedAt),
23
+ tickLockedAt: str(g.tickLockedAt),
24
+ };
25
+ }
6
26
  /**
7
- * `skrr goals planner-config <goal-id>` configure (or disable) the goal's
8
- * auto-planner: the scheduled tick that runs a planner agent to create and
9
- * refine tasks toward the goal.
27
+ * The plain-text planner block, shared by `planner-config` and `goals show` so
28
+ * the two commands cannot drift into describing the same state differently.
29
+ */
30
+ function renderGoalPlannerLines(view) {
31
+ const interval = view.enabled
32
+ ? `${Math.round(view.tickIntervalMs / 60_000)}m (${view.tickIntervalMs}ms)`
33
+ : 'disabled';
34
+ let autoExecute = '-';
35
+ if (view.autoExecute !== null) {
36
+ autoExecute = view.autoExecute ? 'on' : 'off';
37
+ }
38
+ const lines = [
39
+ ` interval: ${interval}`,
40
+ ` maxActiveTasks: ${view.maxActiveTasks ?? '-'}`,
41
+ ` autoExecute: ${autoExecute}`,
42
+ ];
43
+ if (view.nextTickAt)
44
+ lines.push(` nextTickAt: ${view.nextTickAt}`);
45
+ if (view.lastTickedAt)
46
+ lines.push(` lastTickedAt: ${view.lastTickedAt}`);
47
+ if (view.tickLockedAt) {
48
+ lines.push(` tickLockedAt: ${view.tickLockedAt} (a claimed tick has not released this lock)`);
49
+ }
50
+ return lines;
51
+ }
52
+ /**
53
+ * `skrr goals planner-config <goal-id>` — read, configure, or disable the
54
+ * goal's auto-planner: the scheduled tick that runs a planner agent to create
55
+ * and refine tasks toward the goal.
56
+ *
57
+ * With no flags this READS the config. It used to refuse, which made the only
58
+ * way to answer "is the planner on for this goal?" a write — and for `--disable`
59
+ * that meant turning the automation off to find out it was on. A recurring,
60
+ * task-creating, optionally agent-starting automation must be readable without
61
+ * changing it.
10
62
  *
11
63
  * Wraps `PATCH /api/goals/:goalId/planner-config` (data-provider
12
- * `updateGoalPlannerConfig`). Setting a cadence enables the planner; `--disable`
13
- * (which sends `tickIntervalMs: null`) turns it off. The interval flags mirror
14
- * the trigger-create convention elsewhere in this CLI: `--interval-minutes` for
15
- * the ergonomic path, `--interval-ms` for the raw value. The server owns the
16
- * bounds (interval 60s–30d, maxActiveTasks 1–50) and rejects out-of-range
17
- * values, surfaced here via the standard error path.
64
+ * `updateGoalPlannerConfig`) for writes and `GET /api/goals/:goalId` for the
65
+ * read. Setting a cadence enables the planner; `--disable` (which sends
66
+ * `tickIntervalMs: null`) turns it off. The interval flags mirror the
67
+ * trigger-create convention elsewhere in this CLI: `--interval-minutes` for the
68
+ * ergonomic path, `--interval-ms` for the raw value. The server owns the bounds
69
+ * (interval 60s–30d, maxActiveTasks 1–50) and rejects out-of-range values,
70
+ * surfaced here via the standard error path.
18
71
  */
19
72
  class GoalsPlannerConfig extends base_command_1.BaseCommand {
20
- static description = "Configure or disable a goal's auto-planner";
73
+ static description = "Read, configure, or disable a goal's auto-planner (a recurring agent tick that CREATES and refines tasks under the goal)";
21
74
  static examples = [
75
+ '<%= config.bin %> goals planner-config <goal-id>',
22
76
  '<%= config.bin %> goals planner-config <goal-id> --interval-minutes 60',
23
77
  '<%= config.bin %> goals planner-config <goal-id> --interval-minutes 60 --max-active-tasks 5 --auto-execute',
24
78
  '<%= config.bin %> goals planner-config <goal-id> --no-auto-execute',
@@ -28,24 +82,24 @@ class GoalsPlannerConfig extends base_command_1.BaseCommand {
28
82
  id: core_1.Args.string({ description: 'Goal ID', required: true, ignoreStdin: true }),
29
83
  };
30
84
  static flags = {
31
- json: core_1.Flags.boolean({ description: 'Output the updated goal as JSON' }),
85
+ json: core_1.Flags.boolean({ description: 'Output the goal as JSON' }),
32
86
  'interval-minutes': core_1.Flags.integer({
33
- description: 'Planner tick cadence in minutes (enables the planner)',
87
+ description: 'How often the planner runs, in minutes. Setting it arms the planner: each tick creates and refines tasks under this goal',
34
88
  exclusive: ['interval-ms', 'disable'],
35
89
  }),
36
90
  'interval-ms': core_1.Flags.integer({
37
- description: 'Planner tick cadence in milliseconds (enables the planner)',
91
+ description: 'Same cadence in milliseconds (raw value; arms the planner)',
38
92
  exclusive: ['interval-minutes', 'disable'],
39
93
  }),
40
94
  disable: core_1.Flags.boolean({
41
- description: 'Disable the auto-planner (clears the tick cadence)',
95
+ description: 'Disarm the auto-planner clears the cadence so no further ticks run',
42
96
  exclusive: ['interval-minutes', 'interval-ms'],
43
97
  }),
44
98
  'max-active-tasks': core_1.Flags.integer({
45
- description: 'Cap on concurrently-active planner tasks (server bounds: 1–50)',
99
+ description: 'Stop creating new tasks once this many are active under the goal (server bounds: 1–50)',
46
100
  }),
47
101
  'auto-execute': core_1.Flags.boolean({
48
- description: 'Auto-schedule planner-created tasks that have an assignee (use --no-auto-execute to turn off)',
102
+ description: 'Also START the agent on each planner-created task that has an assignee, without asking (use --no-auto-execute to turn off)',
49
103
  allowNo: true,
50
104
  }),
51
105
  };
@@ -68,34 +122,28 @@ class GoalsPlannerConfig extends base_command_1.BaseCommand {
68
122
  if (flags['auto-execute'] !== undefined) {
69
123
  data.autoExecute = flags['auto-execute'];
70
124
  }
71
- if (Object.keys(data).length === 0) {
72
- this.error('Nothing to update. Pass --interval-minutes/--interval-ms, --disable, --max-active-tasks, or --auto-execute/--no-auto-execute.', { exit: 1 });
73
- }
125
+ const isWrite = Object.keys(data).length > 0;
74
126
  let result;
75
127
  try {
76
- result = await data_provider_1.dataService.updateGoalPlannerConfig({ goalId: args.id, data });
128
+ result = isWrite
129
+ ? await data_provider_1.dataService.updateGoalPlannerConfig({ goalId: args.id, data })
130
+ : await data_provider_1.dataService.getGoal(args.id);
77
131
  }
78
132
  catch (err) {
79
133
  this.handleApiError(err);
80
134
  }
81
135
  if (flags.json) {
82
- this.log(JSON.stringify(result, null, 2));
136
+ this.log(JSON.stringify({ ...result, planner: goalPlannerView(result) }, null, 2));
83
137
  return;
84
138
  }
85
- const g = result;
86
- const interval = g.tickIntervalMs == null
87
- ? 'disabled'
88
- : `${Math.round(g.tickIntervalMs / 60_000)}m (${g.tickIntervalMs}ms)`;
89
- let autoExecute = '-';
90
- if (g.autoExecute !== undefined) {
91
- autoExecute = g.autoExecute ? 'on' : 'off';
92
- }
93
139
  this.log(`Planner config for goal ${args.id}:`);
94
- this.log(` interval: ${interval}`);
95
- this.log(` maxActiveTasks: ${g.maxActiveTasks ?? '-'}`);
96
- this.log(` autoExecute: ${autoExecute}`);
97
- if (g.nextTickAt) {
98
- this.log(` nextTickAt: ${g.nextTickAt}`);
140
+ for (const line of renderGoalPlannerLines(goalPlannerView(result))) {
141
+ this.log(line);
142
+ }
143
+ if (!isWrite) {
144
+ this.log('');
145
+ this.log(`Change it with \`${this.config.bin} goals planner-config ${args.id} --interval-minutes <n>\`, ` +
146
+ 'or `--disable` to turn it off.');
99
147
  }
100
148
  }
101
149
  }