@skrr-ai/cli 0.1.131 → 0.1.133

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 (33) hide show
  1. package/dist/commands/agenda/plan.d.ts +4 -4
  2. package/dist/commands/agenda/plan.js +121 -9
  3. package/dist/commands/agent/message/send.d.ts +2 -0
  4. package/dist/commands/agent/message/send.js +49 -24
  5. package/dist/commands/agents/intuitions/generate.js +25 -6
  6. package/dist/commands/agents/intuitions/list.js +7 -2
  7. package/dist/commands/agents/plan-day.d.ts +4 -4
  8. package/dist/commands/agents/plan-day.js +36 -10
  9. package/dist/commands/agents/schedule-overview.d.ts +14 -0
  10. package/dist/commands/agents/schedule-overview.js +11 -1
  11. package/dist/commands/agents/spawn.js +6 -0
  12. package/dist/commands/agents/todos/edit.d.ts +14 -0
  13. package/dist/commands/agents/todos/edit.js +43 -0
  14. package/dist/commands/agents/todos/list.d.ts +1 -0
  15. package/dist/commands/agents/todos/list.js +4 -2
  16. package/dist/commands/agents/triggers/fire.d.ts +2 -2
  17. package/dist/commands/agents/triggers/fire.js +20 -3
  18. package/dist/commands/agents/triggers/policy.d.ts +6 -0
  19. package/dist/commands/agents/triggers/policy.js +22 -0
  20. package/dist/commands/commitments/create.js +13 -0
  21. package/dist/commands/daemon/install.js +9 -0
  22. package/dist/commands/goals/briefing/enable.js +1 -2
  23. package/dist/commands/goals/briefing/status.d.ts +8 -0
  24. package/dist/commands/goals/briefing/status.js +35 -0
  25. package/dist/commands/memory/list.js +9 -3
  26. package/dist/generated/agent-guides.json +19 -19
  27. package/dist/lib/schedule.d.ts +20 -1
  28. package/dist/lib/schedule.js +7 -3
  29. package/dist/lib/scoped-memory-command.d.ts +10 -0
  30. package/dist/lib/scoped-memory-command.js +21 -2
  31. package/dist/node_modules/@skrr-ai/data-provider/index.js +774 -774
  32. package/oclif.manifest.json +196 -3
  33. package/package.json +1 -1
@@ -7,10 +7,8 @@ import { BaseCommand } from '../../base-command';
7
7
  * server, and enqueues the agent turn through the canonical agentic
8
8
  * pipeline (preflight → BullMQ → AgentWorker → AgenticClient → SDK).
9
9
  *
10
- * The CLI returns the resolved `conversationId` + `sessionId` immediately
11
- * after the HTTP accept. Live streaming continues over `/ws/agentic` —
12
- * not subscribed here. Pair with `skrr convos show <conversationId>` to
13
- * inspect the resulting agenda once it persists.
10
+ * Interactive terminals follow the accepted session, including permission
11
+ * requests. JSON and non-interactive invocations remain detached by default.
14
12
  */
15
13
  export default class AgendaPlan extends BaseCommand {
16
14
  static description: string;
@@ -22,6 +20,8 @@ export default class AgendaPlan extends BaseCommand {
22
20
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
23
21
  'space-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
22
  timezone: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
23
+ follow: import("@oclif/core/interfaces").BooleanFlag<boolean>;
24
+ 'approve-tools': import("@oclif/core/interfaces").BooleanFlag<boolean>;
25
25
  };
26
26
  run(): Promise<void>;
27
27
  }
@@ -3,6 +3,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  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
+ const api_fetch_1 = require("../../lib/api-fetch");
7
+ const agentic_stream_1 = require("../../lib/agentic-stream");
8
+ const chat_1 = require("../agents/chat");
9
+ function describeRuntime(runtime) {
10
+ if (typeof runtime === 'string')
11
+ return runtime;
12
+ if (!runtime || typeof runtime !== 'object')
13
+ return null;
14
+ const record = runtime;
15
+ const plan = record.executionPlan && typeof record.executionPlan === 'object'
16
+ ? record.executionPlan : null;
17
+ const selected = typeof plan?.selectedKind === 'string' ? plan.selectedKind : null;
18
+ if (selected)
19
+ return `selected ${selected}${selected === 'agent-worker' && record.daemonId ? '; local daemon available as tool bridge' : ''}`;
20
+ return typeof record.mode === 'string' ? record.mode : null;
21
+ }
6
22
  /**
7
23
  * `skrr agenda plan <agent-id>` — kick off a one-shot Agenda Planning turn
8
24
  * for an agent. The server-side controller resolves the agent's persistent
@@ -11,10 +27,8 @@ const base_command_1 = require("../../base-command");
11
27
  * server, and enqueues the agent turn through the canonical agentic
12
28
  * pipeline (preflight → BullMQ → AgentWorker → AgenticClient → SDK).
13
29
  *
14
- * The CLI returns the resolved `conversationId` + `sessionId` immediately
15
- * after the HTTP accept. Live streaming continues over `/ws/agentic` —
16
- * not subscribed here. Pair with `skrr convos show <conversationId>` to
17
- * inspect the resulting agenda once it persists.
30
+ * Interactive terminals follow the accepted session, including permission
31
+ * requests. JSON and non-interactive invocations remain detached by default.
18
32
  */
19
33
  class AgendaPlan extends base_command_1.BaseCommand {
20
34
  static description = 'Trigger an Agenda Planning turn for an agent. Returns the conversation id where the streaming response will land.';
@@ -34,10 +48,17 @@ class AgendaPlan extends base_command_1.BaseCommand {
34
48
  timezone: core_1.Flags.string({
35
49
  description: 'IANA timezone hint for the agenda turn (defaults to UTC server-side)',
36
50
  }),
51
+ follow: core_1.Flags.boolean({ allowNo: true, description: 'Follow the session (default on an interactive terminal)' }),
52
+ 'approve-tools': core_1.Flags.boolean({
53
+ description: 'Allow tool prompts; on a non-interactive follow this grants requested tools for the turn',
54
+ }),
37
55
  };
38
56
  async run() {
39
57
  this.requireAuth();
40
58
  const { args, flags } = await this.parse(AgendaPlan);
59
+ if (flags.json && flags.follow)
60
+ this.error('--json is detached; use --follow without --json to stream.', { exit: 1 });
61
+ const follow = flags.follow ?? (Boolean(process.stdin.isTTY) && !flags.json);
41
62
  let response;
42
63
  try {
43
64
  response = await data_provider_1.dataService.planAgenda({
@@ -57,12 +78,103 @@ class AgendaPlan extends base_command_1.BaseCommand {
57
78
  this.log(`Conversation ID: ${response.conversationId}`);
58
79
  this.log(`Session ID: ${response.sessionId}`);
59
80
  this.log(`New conversation: ${response.isNewConversation ? 'yes' : 'no'}`);
60
- if (response.runtime)
61
- this.log(`Runtime: ${response.runtime}`);
81
+ const runtimeDescription = describeRuntime(response.runtime);
82
+ if (runtimeDescription)
83
+ this.log(`Runtime: ${runtimeDescription}`);
62
84
  this.log('');
63
- this.log('Streaming continues over /ws/agentic. Use `skrr convos show ' +
64
- response.conversationId +
65
- '` to inspect the agenda once it completes.');
85
+ if (!follow) {
86
+ this.log('The turn is running detached. It may wait for permission; inspect it in the app or conversation ' + response.conversationId + '.');
87
+ return;
88
+ }
89
+ const readMessages = async () => {
90
+ const messages = await data_provider_1.dataService.getMessagesByConvoId(response.conversationId);
91
+ return Array.isArray(messages) ? messages : [];
92
+ };
93
+ let detach;
94
+ const detached = new Promise((resolve) => { detach = () => resolve('detached'); });
95
+ let disconnectTimer;
96
+ const client = new agentic_stream_1.AgenticStreamClient({
97
+ baseURL: this.cliConfig.baseURL,
98
+ token: this.resolvedCredential.token,
99
+ workspaceId: process.env.OVERSKY_WORKSPACE_ID,
100
+ sessionId: response.sessionId,
101
+ conversationId: response.conversationId,
102
+ approveTools: Boolean(process.stdin.isTTY || flags['approve-tools']),
103
+ onEvent: (event) => {
104
+ if (event.type === 'transport.connected' && disconnectTimer) {
105
+ clearTimeout(disconnectTimer);
106
+ disconnectTimer = undefined;
107
+ }
108
+ else if (event.type === 'transport.disconnected' && !disconnectTimer) {
109
+ // An established Socket.IO connection can be disconnected without an
110
+ // automatic retry. Bound the lost view and reconcile its durable row.
111
+ disconnectTimer = setTimeout(detach, 30_000);
112
+ }
113
+ },
114
+ reconcileTerminal: async () => (0, chat_1.reconcileTerminalFromMessages)(await readMessages(), response.responseMessageId),
115
+ awaitHandover: {
116
+ read: async () => (0, chat_1.handoverStateFromMessages)(await readMessages(), response.responseMessageId),
117
+ responseMessageId: response.responseMessageId,
118
+ conversationId: response.conversationId,
119
+ maxWaitMs: 5 * 60_000,
120
+ },
121
+ replayEnvelopes: (from) => (0, api_fetch_1.apiFetch)(`/api/agentic/sessions/${encodeURIComponent(response.sessionId)}/envelopes?from=${from}&limit=500`, { credential: this.resolvedCredential, signal: AbortSignal.timeout(5000) }),
122
+ });
123
+ let interruptedBy = null;
124
+ let timer;
125
+ const onInterrupt = (signal) => {
126
+ if (interruptedBy) {
127
+ detach();
128
+ return;
129
+ }
130
+ interruptedBy = signal;
131
+ client.cancel();
132
+ if (signal !== 'SIGHUP')
133
+ process.stderr.write(`\nCancellation requested for ${response.sessionId}; waiting up to 10 seconds. The turn may still be running if it does not stop.\n`);
134
+ timer = setTimeout(detach, 10_000);
135
+ };
136
+ const onSigint = () => onInterrupt('SIGINT');
137
+ const onSigterm = () => onInterrupt('SIGTERM');
138
+ const onSighup = () => onInterrupt('SIGHUP');
139
+ process.on('SIGINT', onSigint);
140
+ process.on('SIGTERM', onSigterm);
141
+ process.on('SIGHUP', onSighup);
142
+ try {
143
+ const outcome = await Promise.race([
144
+ client.run().catch((error) => {
145
+ if (error?.code === 'STREAM_ATTACH_TIMEOUT')
146
+ return 'detached';
147
+ throw error;
148
+ }),
149
+ detached,
150
+ ]);
151
+ const result = outcome === 'detached' && await client.probeTerminal('detach')
152
+ ? client.settledResult() : outcome === 'detached' ? null : outcome;
153
+ if (!result) {
154
+ process.stderr.write(`Detached from ${response.sessionId}; the turn may still be running. Check conversation ${response.conversationId}.\n`);
155
+ process.exitCode = interruptedBy === 'SIGTERM' ? 143 : interruptedBy === 'SIGHUP' ? 129 : interruptedBy ? 130 : 75;
156
+ }
157
+ else if (result.status === 'failed') {
158
+ process.exitCode = 1;
159
+ }
160
+ else if (result.status === 'aborted' && interruptedBy !== 'SIGHUP') {
161
+ this.log(`\nCancellation confirmed for ${response.sessionId}.`);
162
+ }
163
+ if (interruptedBy === 'SIGTERM')
164
+ process.exitCode = 143;
165
+ if (interruptedBy === 'SIGHUP')
166
+ process.exitCode = 129;
167
+ }
168
+ finally {
169
+ if (timer)
170
+ clearTimeout(timer);
171
+ if (disconnectTimer)
172
+ clearTimeout(disconnectTimer);
173
+ process.removeListener('SIGINT', onSigint);
174
+ process.removeListener('SIGTERM', onSigterm);
175
+ process.removeListener('SIGHUP', onSighup);
176
+ client.close();
177
+ }
66
178
  }
67
179
  }
68
180
  exports.default = AgendaPlan;
@@ -118,6 +118,8 @@ export default class AgentMessageSend extends BaseCommand {
118
118
  private submitAsSession;
119
119
  private submitAsHuman;
120
120
  }
121
+ /** Keep stdout machine-readable while making a slow delivery visible. */
122
+ export declare function withSendWaitNotice<T>(send: () => Promise<T>): Promise<T>;
121
123
  export type NormalizedIntentPayload = {
122
124
  destination: CanonicalMessageDestination;
123
125
  content: {
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.EXIT_DELIVERY_FAILED = exports.EXIT_DELIVERY_RETRYABLE = exports.OWNER_DM_SEGMENTATION_SOFT_LIMIT = void 0;
37
+ exports.withSendWaitNotice = withSendWaitNotice;
37
38
  exports.messageSegmentationAdvisory = messageSegmentationAdvisory;
38
39
  exports.buildSequencedRequests = buildSequencedRequests;
39
40
  exports.deliveryExitCode = deliveryExitCode;
@@ -308,30 +309,33 @@ class AgentMessageSend extends base_command_1.BaseCommand {
308
309
  'session claim, not the request. To change it, run the task from a space in ' +
309
310
  'the intended workspace.');
310
311
  }
311
- const envelopes = [];
312
- let halt;
313
- for (const [index, requestPart] of requests.entries()) {
314
- const envelope = sessionContext
315
- ? await this.submitAsSession({
316
- sessionContext,
317
- request: requestPart,
318
- agentIdentity: agentIdentity,
319
- principalSource,
320
- })
321
- : await this.submitAsHuman({
322
- request: requestPart,
323
- agentIdentity: agentIdentity,
324
- principalSource,
325
- });
326
- envelopes.push(envelope);
327
- // A part that did not reach the user must stop the sequence. Continuing
328
- // would deliver thought 3 while thought 2 sits held for approval, and the
329
- // user would read the update out of order — worse than not receiving the
330
- // tail at all.
331
- halt = sequenceHalt({ envelope, sentCount: index + 1, totalParts: requests.length });
332
- if (halt)
333
- break;
334
- }
312
+ const { envelopes, halt } = await withSendWaitNotice(async () => {
313
+ const envelopes = [];
314
+ let halt;
315
+ for (const [index, requestPart] of requests.entries()) {
316
+ const envelope = sessionContext
317
+ ? await this.submitAsSession({
318
+ sessionContext,
319
+ request: requestPart,
320
+ agentIdentity: agentIdentity,
321
+ principalSource,
322
+ })
323
+ : await this.submitAsHuman({
324
+ request: requestPart,
325
+ agentIdentity: agentIdentity,
326
+ principalSource,
327
+ });
328
+ envelopes.push(envelope);
329
+ // A part that did not reach the user must stop the sequence. Continuing
330
+ // would deliver thought 3 while thought 2 sits held for approval, and the
331
+ // user would read the update out of order — worse than not receiving the
332
+ // tail at all.
333
+ halt = sequenceHalt({ envelope, sentCount: index + 1, totalParts: requests.length });
334
+ if (halt)
335
+ break;
336
+ }
337
+ return { envelopes, halt };
338
+ });
335
339
  if (flags.json) {
336
340
  // Branch on the REQUESTED part count, not the sent count: a sequence that
337
341
  // halted on its first part still has to report the parts it skipped.
@@ -524,6 +528,18 @@ class AgentMessageSend extends base_command_1.BaseCommand {
524
528
  }
525
529
  }
526
530
  exports.default = AgentMessageSend;
531
+ /** Keep stdout machine-readable while making a slow delivery visible. */
532
+ async function withSendWaitNotice(send) {
533
+ const timer = setTimeout(() => {
534
+ process.stderr.write('Waiting for the message outcome; the request is still in progress.\n');
535
+ }, 3000);
536
+ try {
537
+ return await send();
538
+ }
539
+ finally {
540
+ clearTimeout(timer);
541
+ }
542
+ }
527
543
  // Re-export the module-scope `request.post` under a local name so the transport
528
544
  // helper stays testable without pulling the whole data-provider surface into
529
545
  // scope inside every helper file.
@@ -747,9 +763,18 @@ function normalizeIntentPayload({ agentId, jsonBody, merged, }) {
747
763
  const overlayChannel = optionalString(merged.channelId);
748
764
  const overlayThreadTs = optionalString(merged.threadTs);
749
765
  const destinationKind = pickDestinationKind({ bodyDestination, overlayKind });
766
+ if (destinationKind !== 'slack_channel' &&
767
+ (merged.channelId !== undefined || bodyDestination?.channelId !== undefined)) {
768
+ throw invalidIntent('--channel / destination.channelId only applies to slack_channel.');
769
+ }
750
770
  if (destinationKind !== 'slack_channel' && (overlayThreadTs || bodyDestination?.threadTs)) {
751
771
  throw invalidIntent('--thread-ts / destination.threadTs only applies to slack_channel.');
752
772
  }
773
+ if (destinationKind !== 'agent_email' &&
774
+ (merged.recipients !== undefined || merged.subject !== undefined ||
775
+ bodyDestination?.recipients !== undefined || bodyDestination?.subject !== undefined)) {
776
+ throw invalidIntent('--to / destination.recipients and --subject / destination.subject only apply to agent_email.');
777
+ }
753
778
  const destination = buildDestination({
754
779
  kind: destinationKind,
755
780
  bodyDestination,
@@ -13,12 +13,31 @@ class AgentsIntuitionsGenerate extends base_command_1.BaseCommand {
13
13
  async run() {
14
14
  this.requireAuth();
15
15
  const { args, flags } = await this.parse(AgentsIntuitionsGenerate);
16
- const result = await data_provider_1.dataService.generateAgentIntuitions(args.agentId, flags.force);
17
- this.log(flags.json
18
- ? this.agentJson(args.agentId, result)
19
- : `Generated intuitions for agent ${args.agentId}`);
20
- if (!flags.json)
21
- this.printAgentUrl(args.agentId);
16
+ let result;
17
+ try {
18
+ result = await data_provider_1.dataService.generateAgentIntuitions(args.agentId, flags.force);
19
+ }
20
+ catch (err) {
21
+ this.handleApiError(err);
22
+ }
23
+ if (flags.json) {
24
+ this.log(this.agentJson(args.agentId, result));
25
+ return;
26
+ }
27
+ const response = result;
28
+ if (response.outcome === 'insufficient_context') {
29
+ this.log('No intuitions yet: add relevant agent memories or activity, then try again.');
30
+ }
31
+ else if (response.outcome === 'no_suggestions') {
32
+ this.log('No intuitions were suggested from the available context. Try again after more activity.');
33
+ }
34
+ else if (response.intuitions?.length === 0) {
35
+ this.log('No intuitions were returned. Add relevant memories or activity, then try again.');
36
+ }
37
+ else {
38
+ this.log(`Generated intuitions for agent ${args.agentId}`);
39
+ }
40
+ this.printAgentUrl(args.agentId);
22
41
  }
23
42
  }
24
43
  exports.default = AgentsIntuitionsGenerate;
@@ -16,8 +16,13 @@ class AgentsIntuitionsList extends base_command_1.BaseCommand {
16
16
  this.printJsonResult((0, list_output_1.listResult)(this.withAgentContext(args.agentId, result), 'intuitions'));
17
17
  return;
18
18
  }
19
- const text = JSON.stringify(result.intuitions ?? result, null, 2);
20
- this.log(text);
19
+ const intuitions = result.intuitions ?? result;
20
+ if (Array.isArray(intuitions) && intuitions.length === 0) {
21
+ this.log(`No active intuitions. Run skrr agents intuitions generate ${args.agentId} to look for suggestions.`);
22
+ }
23
+ else {
24
+ this.log(JSON.stringify(intuitions, null, 2));
25
+ }
21
26
  this.printAgentUrl(args.agentId);
22
27
  }
23
28
  }
@@ -28,6 +28,7 @@ export default class AgentsPlanDay extends BaseCommand {
28
28
  'hours-start': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
29
29
  'hours-end': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
30
30
  timezone: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
31
+ 'approve-next-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
31
32
  'save-hours': import("@oclif/core/interfaces").BooleanFlag<boolean>;
32
33
  };
33
34
  run(): Promise<void>;
@@ -44,10 +45,9 @@ export default class AgentsPlanDay extends BaseCommand {
44
45
  private fetchPending;
45
46
  private workingHoursOverride;
46
47
  /**
47
- * Statuses that mean "the request ran out of time", not "the request was
48
- * refused". Only these are recoverable by reading the durable draft: a 4xx
49
- * means no generation started, and a 500 means the route itself failed, so
50
- * polling would just report an absence as if it were a timeout.
48
+ * Gateway failures can interrupt a response after the planner started.
49
+ * Check the durable draft before suggesting another billable request; a
50
+ * 502 can also mean the planner never started, so do not assert that it did.
51
51
  */
52
52
  private static readonly TIMEOUT_STATUSES;
53
53
  /** Bounded, honest recovery poll. A daemon-run plan lands in ~2-4 minutes. */
@@ -45,6 +45,10 @@ class AgentsPlanDay extends base_command_1.BaseCommand {
45
45
  'hours-start': core_1.Flags.string({ description: 'Working-hours start (HH:MM) for the proposal' }),
46
46
  'hours-end': core_1.Flags.string({ description: 'Working-hours end (HH:MM) for the proposal' }),
47
47
  timezone: core_1.Flags.string({ description: 'IANA timezone for the working-hours window' }),
48
+ 'approve-next-run': core_1.Flags.boolean({
49
+ description: 'With --approve, schedule the suggested next planning run',
50
+ dependsOn: ['approve'],
51
+ }),
48
52
  'save-hours': core_1.Flags.boolean({
49
53
  description: 'On --approve, persist the working-hours window as the agent default',
50
54
  }),
@@ -53,6 +57,14 @@ class AgentsPlanDay extends base_command_1.BaseCommand {
53
57
  this.requireAuth();
54
58
  const { args, flags } = await this.parse(AgentsPlanDay);
55
59
  const agentId = args.id;
60
+ if (!flags.dismiss && !flags.approve && flags.timezone) {
61
+ try {
62
+ new Intl.DateTimeFormat('en', { timeZone: flags.timezone });
63
+ }
64
+ catch {
65
+ this.error(`Invalid IANA timezone: ${flags.timezone}`, { exit: 1 });
66
+ }
67
+ }
56
68
  try {
57
69
  if (flags.dismiss) {
58
70
  await this.runDismiss(agentId, flags.json);
@@ -120,12 +132,11 @@ class AgentsPlanDay extends base_command_1.BaseCommand {
120
132
  };
121
133
  }
122
134
  /**
123
- * Statuses that mean "the request ran out of time", not "the request was
124
- * refused". Only these are recoverable by reading the durable draft: a 4xx
125
- * means no generation started, and a 500 means the route itself failed, so
126
- * polling would just report an absence as if it were a timeout.
135
+ * Gateway failures can interrupt a response after the planner started.
136
+ * Check the durable draft before suggesting another billable request; a
137
+ * 502 can also mean the planner never started, so do not assert that it did.
127
138
  */
128
- static TIMEOUT_STATUSES = new Set([408, 504, 524]);
139
+ static TIMEOUT_STATUSES = new Set([408, 502, 504, 524]);
129
140
  /** Bounded, honest recovery poll. A daemon-run plan lands in ~2-4 minutes. */
130
141
  static RECOVERY_ATTEMPTS = 12;
131
142
  static RECOVERY_INTERVAL_MS = 15_000;
@@ -141,9 +152,8 @@ class AgentsPlanDay extends base_command_1.BaseCommand {
141
152
  return null;
142
153
  }
143
154
  // stderr, so `--json` stdout stays a single parseable document.
144
- this.warn(`Plan generation for ${agentId} outlived the request (HTTP ${status}). ` +
145
- 'The run is still finishing on the agent runtime and the draft is persisted when it ' +
146
- 'completes — waiting for it rather than starting a second run.');
155
+ this.warn(`The plan request for ${agentId} was interrupted (HTTP ${status}). ` +
156
+ 'The run may still be finishing; checking for its saved draft before starting another.');
147
157
  for (let attempt = 1; attempt <= AgentsPlanDay.RECOVERY_ATTEMPTS; attempt += 1) {
148
158
  await new Promise((resolve) => setTimeout(resolve, AgentsPlanDay.RECOVERY_INTERVAL_MS));
149
159
  let pending = null;
@@ -181,6 +191,7 @@ class AgentsPlanDay extends base_command_1.BaseCommand {
181
191
  try {
182
192
  generated = await schedule_1.scheduleApi.proposeSmartSchedule({
183
193
  agentId,
194
+ ...(flags.timezone ? { timezone: flags.timezone } : {}),
184
195
  ...(this.workingHoursOverride(flags)
185
196
  ? { workingHoursOverride: this.workingHoursOverride(flags) }
186
197
  : {}),
@@ -216,6 +227,9 @@ class AgentsPlanDay extends base_command_1.BaseCommand {
216
227
  this.log('');
217
228
  this.log('This is a draft — nothing is scheduled yet.');
218
229
  this.log(` Approve: ${this.config.bin} agents plan-day ${agentId} --approve`);
230
+ if (proposal.nextRunSuggestion) {
231
+ this.log(` Include next run: ${this.config.bin} agents plan-day ${agentId} --approve --approve-next-run`);
232
+ }
219
233
  this.log(` Discard: ${this.config.bin} agents plan-day ${agentId} --dismiss`);
220
234
  }
221
235
  async runApprove(agentId, flags) {
@@ -224,13 +238,20 @@ class AgentsPlanDay extends base_command_1.BaseCommand {
224
238
  this.error(`No pending plan for agent ${agentId}. Run \`${this.config.bin} agents plan-day ${agentId}\` first.`, { exit: 4 });
225
239
  }
226
240
  const todos = (0, schedule_1.proposalToApproveTodos)(proposal);
227
- if (todos.length === 0) {
228
- this.error('The pending proposal has no schedulable to-dos to approve.', { exit: 1 });
241
+ if (flags['approve-next-run'] && !proposal.nextRunSuggestion) {
242
+ this.error('The pending proposal has no suggested next run to approve.', { exit: 1 });
243
+ }
244
+ if (todos.length === 0 && !flags['approve-next-run']) {
245
+ const next = proposal.nextRunSuggestion
246
+ ? ` Use --approve --approve-next-run to schedule its suggested next run, or --dismiss to discard it.`
247
+ : ' Use --dismiss to discard it.';
248
+ this.error(`The pending proposal has no schedulable to-dos.${next}`, { exit: 1 });
229
249
  }
230
250
  const result = await schedule_1.scheduleApi.approveSmartSchedule({
231
251
  agentId,
232
252
  proposalId: proposal.proposalId,
233
253
  todos,
254
+ ...(flags['approve-next-run'] ? { approveNextRunSuggestion: true } : {}),
234
255
  ...(flags['save-hours']
235
256
  ? { saveHoursAsDefault: true, workingHours: proposal.workingHours }
236
257
  : {}),
@@ -254,6 +275,11 @@ class AgentsPlanDay extends base_command_1.BaseCommand {
254
275
  }
255
276
  this.log(`Approved plan ${proposal.proposalId}: created ${result.created.length} to-do(s).`);
256
277
  (0, schedule_1.renderCapacityWarning)(result.capacity, (line) => this.log(line));
278
+ if (flags['approve-next-run']) {
279
+ this.log(result.scheduledNextRun?.created
280
+ ? `Next planning run scheduled${result.scheduledNextRun.triggerId ? ` (${result.scheduledNextRun.triggerId})` : ''}.`
281
+ : `Next planning run was not created${result.scheduledNextRun?.message || result.scheduledNextRun?.skippedReason ? `: ${result.scheduledNextRun.message || result.scheduledNextRun.skippedReason}` : '.'}`);
282
+ }
257
283
  if (result.failed.length > 0) {
258
284
  this.log(`${result.failed.length} failed:`);
259
285
  for (const f of result.failed) {
@@ -42,3 +42,17 @@ export declare function reflectionLine(reflection: TriggerDefinition): string;
42
42
  * else null. "active · next <time>" only says the loop is armed.
43
43
  */
44
44
  export declare function reflectionNoteLine(reflection: TriggerDefinition, bin: string): string | null;
45
+ /** A trigger the Always-On control fence withholds, as `/api/upcoming` reports it. */
46
+ export interface WithheldTrigger {
47
+ label?: string;
48
+ reason?: string;
49
+ reasonText?: string;
50
+ indeterminate?: boolean;
51
+ }
52
+ /**
53
+ * One withheld trigger as the text view prints it. The sentence the server wrote
54
+ * (`reasonText`: "Always-On is off for this agent") is printed rather than the code,
55
+ * because `disabled` alone reads as a property of the trigger when the cause is the
56
+ * agent's Always-On switch. A server that predates `reasonText` still prints the code.
57
+ */
58
+ export declare function withheldLine(trigger: WithheldTrigger): string;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.reflectionSummary = reflectionSummary;
4
4
  exports.reflectionLine = reflectionLine;
5
5
  exports.reflectionNoteLine = reflectionNoteLine;
6
+ exports.withheldLine = withheldLine;
6
7
  const core_1 = require("@oclif/core");
7
8
  const data_provider_1 = require("@skrr-ai/data-provider");
8
9
  const base_command_1 = require("../../base-command");
@@ -126,7 +127,7 @@ class AgentsScheduleOverview extends base_command_1.BaseCommand {
126
127
  this.log('');
127
128
  this.log(` ${blockedUpcoming.length} trigger(s) withheld — they will not fire as configured:`);
128
129
  for (const b of blockedUpcoming.slice(0, 8)) {
129
- this.log(` ${b.label ?? '(unlabeled)'} — ${b.reason ?? 'blocked'}`);
130
+ this.log(withheldLine(b));
130
131
  }
131
132
  if (blockedUpcoming.length > 8) {
132
133
  this.log(` … and ${blockedUpcoming.length - 8} more`);
@@ -214,6 +215,15 @@ function reflectionNoteLine(reflection, bin) {
214
215
  const note = (0, reflection_1.reflectionAutonomyNote)(reflection, bin);
215
216
  return note ? ` Note: ${note}` : null;
216
217
  }
218
+ /**
219
+ * One withheld trigger as the text view prints it. The sentence the server wrote
220
+ * (`reasonText`: "Always-On is off for this agent") is printed rather than the code,
221
+ * because `disabled` alone reads as a property of the trigger when the cause is the
222
+ * agent's Always-On switch. A server that predates `reasonText` still prints the code.
223
+ */
224
+ function withheldLine(trigger) {
225
+ return ` ${trigger.label ?? '(unlabeled)'} — ${trigger.reasonText ?? trigger.reason ?? 'blocked'}`;
226
+ }
217
227
  function settledValue(result) {
218
228
  return result.status === 'fulfilled' ? result.value : undefined;
219
229
  }
@@ -17,6 +17,12 @@ function proactiveInstallReadiness(result, bin) {
17
17
  if (result.warnings.length === 0) {
18
18
  return { status: 'ready', message: 'All requested setup steps completed.' };
19
19
  }
20
+ if (result.warnings.some((warning) => warning.code === 'always_on_off')) {
21
+ return {
22
+ status: 'degraded',
23
+ message: 'The Agent is off. Review the setup warnings and enable Always-On explicitly if you want it to run.',
24
+ };
25
+ }
20
26
  // A full trigger budget must not make a new persona-only Agent look broken
21
27
  // when its core system commitments, local execution policy, and Always-On
22
28
  // state installed successfully. The optional backlog watcher is deferred;
@@ -0,0 +1,14 @@
1
+ import { BaseCommand } from '../../../base-command';
2
+ export default class AgentsTodosEdit extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ at: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_command_1 = require("../../../base-command");
5
+ const schedule_1 = require("../../../lib/schedule");
6
+ class AgentsTodosEdit extends base_command_1.BaseCommand {
7
+ static description = 'Change the name or time of a pending to-do';
8
+ static examples = [
9
+ '<%= config.bin %> agents todos edit <todo-id> --name "Review launch"',
10
+ '<%= config.bin %> agents todos edit <todo-id> --at 2026-11-05T18:00:00Z',
11
+ ];
12
+ static args = { id: core_1.Args.string({ description: 'To-Do ID', required: true, ignoreStdin: true }) };
13
+ static flags = {
14
+ json: core_1.Flags.boolean({ description: 'Output as JSON' }),
15
+ name: core_1.Flags.string({ description: 'New name' }),
16
+ at: core_1.Flags.string({ description: 'New ISO-8601 run time' }),
17
+ };
18
+ async run() {
19
+ this.requireAuth();
20
+ const { args, flags } = await this.parse(AgentsTodosEdit);
21
+ if (flags.name === undefined && flags.at === undefined)
22
+ this.error('Supply --name or --at.', { exit: 1 });
23
+ let todo;
24
+ try {
25
+ const current = await schedule_1.scheduleApi.getTodo(args.id);
26
+ if (!current.updatedAt)
27
+ this.error('The to-do has no version; refresh before editing.', { exit: 1 });
28
+ todo = await schedule_1.scheduleApi.updateTodo(args.id, {
29
+ ...(flags.name !== undefined ? { name: flags.name } : {}),
30
+ ...(flags.at !== undefined ? { runAt: flags.at } : {}),
31
+ expectedUpdatedAt: current.updatedAt,
32
+ });
33
+ }
34
+ catch (err) {
35
+ this.handleApiError(err);
36
+ }
37
+ if (flags.json)
38
+ this.log(JSON.stringify(todo, null, 2));
39
+ else
40
+ this.log(`Updated to-do ${todo.id} — "${todo.name}" at ${todo.runAt}`);
41
+ }
42
+ }
43
+ exports.default = AgentsTodosEdit;
@@ -13,6 +13,7 @@ export default class AgentsTodosList extends BaseCommand {
13
13
  static flags: {
14
14
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
15
  date: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ timezone: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
17
  };
17
18
  run(): Promise<void>;
18
19
  }