@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.
- package/dist/commands/agenda/plan.d.ts +4 -4
- package/dist/commands/agenda/plan.js +121 -9
- package/dist/commands/agent/message/send.d.ts +2 -0
- package/dist/commands/agent/message/send.js +49 -24
- package/dist/commands/agents/intuitions/generate.js +25 -6
- package/dist/commands/agents/intuitions/list.js +7 -2
- package/dist/commands/agents/plan-day.d.ts +4 -4
- package/dist/commands/agents/plan-day.js +36 -10
- package/dist/commands/agents/schedule-overview.d.ts +14 -0
- package/dist/commands/agents/schedule-overview.js +11 -1
- package/dist/commands/agents/spawn.js +6 -0
- package/dist/commands/agents/todos/edit.d.ts +14 -0
- package/dist/commands/agents/todos/edit.js +43 -0
- package/dist/commands/agents/todos/list.d.ts +1 -0
- package/dist/commands/agents/todos/list.js +4 -2
- package/dist/commands/agents/triggers/fire.d.ts +2 -2
- package/dist/commands/agents/triggers/fire.js +20 -3
- package/dist/commands/agents/triggers/policy.d.ts +6 -0
- package/dist/commands/agents/triggers/policy.js +22 -0
- package/dist/commands/commitments/create.js +13 -0
- package/dist/commands/daemon/install.js +9 -0
- package/dist/commands/goals/briefing/enable.js +1 -2
- package/dist/commands/goals/briefing/status.d.ts +8 -0
- package/dist/commands/goals/briefing/status.js +35 -0
- package/dist/commands/memory/list.js +9 -3
- package/dist/generated/agent-guides.json +19 -19
- package/dist/lib/schedule.d.ts +20 -1
- package/dist/lib/schedule.js +7 -3
- package/dist/lib/scoped-memory-command.d.ts +10 -0
- package/dist/lib/scoped-memory-command.js +21 -2
- package/dist/node_modules/@skrr-ai/data-provider/index.js +774 -774
- package/oclif.manifest.json +196 -3
- 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
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
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
|
-
|
|
61
|
-
|
|
81
|
+
const runtimeDescription = describeRuntime(response.runtime);
|
|
82
|
+
if (runtimeDescription)
|
|
83
|
+
this.log(`Runtime: ${runtimeDescription}`);
|
|
62
84
|
this.log('');
|
|
63
|
-
|
|
64
|
-
response.conversationId +
|
|
65
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this.
|
|
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
|
|
20
|
-
|
|
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
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
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(`
|
|
145
|
-
'The run
|
|
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 (
|
|
228
|
-
this.error('The pending proposal has no
|
|
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(
|
|
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
|
}
|