@skrr-ai/cli 0.1.39 → 0.1.40
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/README.md +1 -1
- package/dist/base-command.js +8 -0
- package/dist/commands/agents/create.d.ts +7 -0
- package/dist/commands/agents/create.js +13 -1
- package/dist/commands/agents/triggers/policy.d.ts +1 -0
- package/dist/commands/agents/triggers/policy.js +32 -0
- package/dist/commands/agents/update.js +4 -3
- package/dist/commands/followups/observability.js +6 -2
- package/dist/commands/followups/remind.js +5 -2
- package/dist/commands/followups/reschedule.js +2 -1
- package/dist/commands/followups/show.js +4 -0
- package/dist/commands/followups/watch.d.ts +1 -0
- package/dist/commands/followups/watch.js +9 -3
- package/dist/commands/tasks/lifecycle/detach.js +26 -5
- package/dist/lib/dedicated-lease-command.js +4 -1
- package/dist/lib/dedicated-machines.d.ts +8 -1
- package/dist/lib/dedicated-machines.js +22 -6
- package/dist/lib/dedicated-wait.js +13 -0
- package/dist/lib/followups.d.ts +8 -0
- package/dist/lib/label-ref.d.ts +8 -1
- package/dist/lib/label-ref.js +12 -2
- package/dist/lib/option-hint.d.ts +15 -0
- package/dist/lib/option-hint.js +75 -0
- package/dist/lib/workspaces.d.ts +3 -0
- package/dist/lib/workspaces.js +38 -0
- package/oclif.manifest.json +26312 -26304
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ observability), and shared links.
|
|
|
38
38
|
| **spaces widgets** | `list`, `add`, `remove` |
|
|
39
39
|
| **spaces skills** | `list`, `add`, `remove`, `pin` |
|
|
40
40
|
| **spaces labels** | `list`, `add`, `remove` (attach/detach; filter with `spaces list --label`) |
|
|
41
|
-
| **labels** | `list`, `palette`, `create`, `update`, `archive`, `restore`, `delete`, `reorder` (unified labels across `user
|
|
41
|
+
| **labels** | `list`, `palette`, `create`, `update`, `archive`, `restore`, `delete`, `reorder` (unified labels across `user` and `workspace` scopes) |
|
|
42
42
|
| **chats triggers** | `show`, `set` |
|
|
43
43
|
| **dm** | `list`, `show`, `messages`, `threads`, `thread show/messages/follow/mute/share`, `send` (agent 1:1 main chat and Slack-style Thread controls) |
|
|
44
44
|
| **diagnostics** | `context`, `doctor`, `usage`, `commands` (agent-friendly runtime identity, readiness, progressive token-budgeted discovery, and complete machine-readable schemas) |
|
package/dist/base-command.js
CHANGED
|
@@ -16,6 +16,7 @@ exports.isTypedServerRefusal = isTypedServerRefusal;
|
|
|
16
16
|
exports.isRetryableFailure = isRetryableFailure;
|
|
17
17
|
const core_1 = require("@oclif/core");
|
|
18
18
|
Object.defineProperty(exports, "Flags", { enumerable: true, get: function () { return core_1.Flags; } });
|
|
19
|
+
const option_hint_1 = require("./lib/option-hint");
|
|
19
20
|
const failed_lookup_1 = require("./lib/failed-lookup");
|
|
20
21
|
// Re-exported from its home in `lib/keychain`, beside the other profile
|
|
21
22
|
// functions. It lived here while `init()` was its only caller; a lib module
|
|
@@ -788,6 +789,13 @@ class BaseCommand extends core_1.Command {
|
|
|
788
789
|
if (retiredHint) {
|
|
789
790
|
err.message = `${err.message}\n\n${retiredHint}`;
|
|
790
791
|
}
|
|
792
|
+
// Sibling domains spell one lifecycle differently (`in_progress` for a
|
|
793
|
+
// space, `active` for a goal), and oclif's enum refusal named neither the
|
|
794
|
+
// difference nor the value to use (OSK-9735).
|
|
795
|
+
const optionHint = (0, option_hint_1.invalidOptionHint)(err.message || '');
|
|
796
|
+
if (optionHint) {
|
|
797
|
+
err.message = `${err.message}\n\n${optionHint}`;
|
|
798
|
+
}
|
|
791
799
|
if (this.wantsJsonOutput()) {
|
|
792
800
|
const exit = err.exitCode ?? err.oclif?.exit ?? 1;
|
|
793
801
|
process.exitCode = exit;
|
|
@@ -64,6 +64,13 @@ export declare function agentCreateMissingFields(payload: Record<string, unknown
|
|
|
64
64
|
* a failed create would be worse than the silence.
|
|
65
65
|
*/
|
|
66
66
|
export declare function unappliedPayloadKeys(sent: Record<string, unknown>, created: unknown, ignore?: readonly string[]): string[];
|
|
67
|
+
/**
|
|
68
|
+
* Why a key can come back missing. Both readings are live: a typo of a real
|
|
69
|
+
* field (`max_turns` for `maxTurns`), and a real field only the server sets
|
|
70
|
+
* (`nativeRole`, `author`). Naming only the first told a caller who had tried
|
|
71
|
+
* to claim a platform role that they had misspelled something.
|
|
72
|
+
*/
|
|
73
|
+
export declare function unappliedPayloadKeysWarning(keys: readonly string[]): string;
|
|
67
74
|
export declare function backgroundLoopLines(agentId: string, relationshipLoopsSkipped: boolean): string[];
|
|
68
75
|
export default class AgentsCreate extends BaseCommand {
|
|
69
76
|
static description: string;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.agentCreateMissingFieldsMessage = agentCreateMissingFieldsMessage;
|
|
4
4
|
exports.agentCreateMissingFields = agentCreateMissingFields;
|
|
5
5
|
exports.unappliedPayloadKeys = unappliedPayloadKeys;
|
|
6
|
+
exports.unappliedPayloadKeysWarning = unappliedPayloadKeysWarning;
|
|
6
7
|
exports.backgroundLoopLines = backgroundLoopLines;
|
|
7
8
|
const core_1 = require("@oclif/core");
|
|
8
9
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
@@ -105,6 +106,17 @@ function unappliedPayloadKeys(sent, created, ignore = []) {
|
|
|
105
106
|
.filter((key) => !Object.prototype.hasOwnProperty.call(back, key))
|
|
106
107
|
.sort();
|
|
107
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Why a key can come back missing. Both readings are live: a typo of a real
|
|
111
|
+
* field (`max_turns` for `maxTurns`), and a real field only the server sets
|
|
112
|
+
* (`nativeRole`, `author`). Naming only the first told a caller who had tried
|
|
113
|
+
* to claim a platform role that they had misspelled something.
|
|
114
|
+
*/
|
|
115
|
+
function unappliedPayloadKeysWarning(keys) {
|
|
116
|
+
const one = keys.length === 1;
|
|
117
|
+
return (`The server did not keep ${keys.join(', ')}: ${one ? 'it is not an agent field' : 'they are not agent fields'}, ` +
|
|
118
|
+
`or only the server sets ${one ? 'it' : 'them'}. \`skrr agents show <id> --json\` shows what the agent stores.`);
|
|
119
|
+
}
|
|
108
120
|
function backgroundLoopLines(agentId, relationshipLoopsSkipped) {
|
|
109
121
|
if (relationshipLoopsSkipped) {
|
|
110
122
|
return [
|
|
@@ -368,7 +380,7 @@ class AgentsCreate extends base_command_1.BaseCommand {
|
|
|
368
380
|
// Agent document field and is intentionally absent from the response.
|
|
369
381
|
const unapplied = unappliedPayloadKeys(payload, created, ['relationshipLoops']);
|
|
370
382
|
if (unapplied.length > 0) {
|
|
371
|
-
this.warn(
|
|
383
|
+
this.warn(unappliedPayloadKeysWarning(unapplied));
|
|
372
384
|
}
|
|
373
385
|
if (flags.json) {
|
|
374
386
|
this.log(JSON.stringify((0, web_url_1.withAgentUrl)(this.cliConfig.baseURL, created), null, 2));
|
|
@@ -15,6 +15,7 @@ export default class AgentsTriggersPolicy extends BaseCommand {
|
|
|
15
15
|
'max-per-hour': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
16
16
|
'working-hours': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
17
17
|
'quiet-hours': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
18
|
+
'followup-budget': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
18
19
|
};
|
|
19
20
|
run(): Promise<void>;
|
|
20
21
|
}
|
|
@@ -8,6 +8,28 @@ const triggers_1 = require("../../../lib/triggers");
|
|
|
8
8
|
// used to carry its own copy that stopped one rung short, so `auto_run` was
|
|
9
9
|
// accepted by the API and unreachable from the CLI.
|
|
10
10
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
11
|
+
/** `watch=10 checkin=default` → `{ watch: 10, checkin: null }`; the server validates the pools. */
|
|
12
|
+
function parseFollowUpBudget(values) {
|
|
13
|
+
if (!values || values.length === 0)
|
|
14
|
+
return undefined;
|
|
15
|
+
const budget = {};
|
|
16
|
+
for (const entry of (0, triggers_1.splitRepeated)(values) ?? []) {
|
|
17
|
+
const [pool, raw] = entry.split('=');
|
|
18
|
+
if (!pool || raw === undefined || raw === '') {
|
|
19
|
+
throw new Error(`--followup-budget expects pool=count, got "${entry}"`);
|
|
20
|
+
}
|
|
21
|
+
if (raw === 'default') {
|
|
22
|
+
budget[pool] = null;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const count = Number(raw);
|
|
26
|
+
if (!Number.isInteger(count) || count < 0) {
|
|
27
|
+
throw new Error(`--followup-budget ${pool} must be a non-negative integer or "default"`);
|
|
28
|
+
}
|
|
29
|
+
budget[pool] = count;
|
|
30
|
+
}
|
|
31
|
+
return budget;
|
|
32
|
+
}
|
|
11
33
|
class AgentsTriggersPolicy extends base_command_1.BaseCommand {
|
|
12
34
|
static description = "Show or update an agent's initiative policy for agent-scoped triggers";
|
|
13
35
|
static examples = [
|
|
@@ -38,6 +60,10 @@ class AgentsTriggersPolicy extends base_command_1.BaseCommand {
|
|
|
38
60
|
'max-per-hour': core_1.Flags.integer({ description: 'Agent initiative hourly fire cap' }),
|
|
39
61
|
'working-hours': core_1.Flags.string({ description: 'Policy workingHours JSON object' }),
|
|
40
62
|
'quiet-hours': core_1.Flags.string({ description: 'Policy quietHours JSON object' }),
|
|
63
|
+
'followup-budget': core_1.Flags.string({
|
|
64
|
+
description: 'Daily follow-up creation cap for this agent, as pool=count (pools: watch, checkin, act; count "default" clears it). Repeatable',
|
|
65
|
+
multiple: true,
|
|
66
|
+
}),
|
|
41
67
|
};
|
|
42
68
|
async run() {
|
|
43
69
|
this.requireAuth();
|
|
@@ -52,6 +78,7 @@ class AgentsTriggersPolicy extends base_command_1.BaseCommand {
|
|
|
52
78
|
maxPerHour: flags['max-per-hour'],
|
|
53
79
|
workingHours: (0, triggers_1.parseJsonObject)(flags['working-hours'], '--working-hours'),
|
|
54
80
|
quietHours: (0, triggers_1.parseJsonObject)(flags['quiet-hours'], '--quiet-hours'),
|
|
81
|
+
followUpBudget: parseFollowUpBudget(flags['followup-budget']),
|
|
55
82
|
});
|
|
56
83
|
}
|
|
57
84
|
catch (err) {
|
|
@@ -77,6 +104,11 @@ class AgentsTriggersPolicy extends base_command_1.BaseCommand {
|
|
|
77
104
|
this.log(`Allowed kinds: ${Array.isArray(policy.allowedKinds) ? policy.allowedKinds.join(', ') : '-'}`);
|
|
78
105
|
this.log(`Max fires/day: ${String(policy.maxFiresPerDay ?? '-')}`);
|
|
79
106
|
this.log(`Max/hour: ${String(policy.maxPerHour ?? '-')}`);
|
|
107
|
+
const followUpBudget = policy.followUpBudget;
|
|
108
|
+
const budgetEntries = Object.entries(followUpBudget ?? {}).filter(([, v]) => v != null);
|
|
109
|
+
this.log(`Follow-up caps: ${budgetEntries.length > 0
|
|
110
|
+
? budgetEntries.map(([pool, cap]) => `${pool} ${String(cap)}/day`).join(', ')
|
|
111
|
+
: 'defaults'}`);
|
|
80
112
|
if (Object.keys(counts).length > 0) {
|
|
81
113
|
this.log(`Triggers: ${String(counts.total ?? 0)} total, ${String(counts.enabled ?? 0)} enabled`);
|
|
82
114
|
}
|
|
@@ -186,13 +186,14 @@ class AgentsUpdate extends base_command_1.BaseCommand {
|
|
|
186
186
|
this.handleApiError(err);
|
|
187
187
|
}
|
|
188
188
|
// The same check `agents create` makes. Without it, a key the server does
|
|
189
|
-
// not model
|
|
190
|
-
// was discarded with the same "Updated agent"
|
|
189
|
+
// not model, a typo of one that it does (`max_turns` for `maxTurns`), or a
|
|
190
|
+
// field only the server sets was discarded with the same "Updated agent"
|
|
191
|
+
// line as a real change.
|
|
191
192
|
// `removeProjectIds` is an instruction, not a stored field, so it never
|
|
192
193
|
// comes back and is not reported.
|
|
193
194
|
const unapplied = (0, create_1.unappliedPayloadKeys)(payload, updated, ['removeProjectIds']);
|
|
194
195
|
if (unapplied.length > 0) {
|
|
195
|
-
this.warn(
|
|
196
|
+
this.warn((0, create_1.unappliedPayloadKeysWarning)(unapplied));
|
|
196
197
|
}
|
|
197
198
|
if (flags.json) {
|
|
198
199
|
this.log(JSON.stringify((0, web_url_1.withAgentUrl)(this.cliConfig.baseURL, updated), null, 2));
|
|
@@ -24,7 +24,7 @@ const outcomes = (terminal) => Object.entries(terminal)
|
|
|
24
24
|
* every agent only with `--all-agents`.
|
|
25
25
|
*/
|
|
26
26
|
class FollowupsObservability extends base_command_1.BaseCommand {
|
|
27
|
-
static description = 'Per-agent, per-kind rollup — created, outcomes, escalation, budget, check-in silence';
|
|
27
|
+
static description = 'Per-agent, per-kind rollup — created, outcomes, escalation, spend, budget, check-in silence';
|
|
28
28
|
static examples = [
|
|
29
29
|
'<%= config.bin %> followups observability',
|
|
30
30
|
'<%= config.bin %> followups observability --agent agent_1',
|
|
@@ -93,6 +93,9 @@ class FollowupsObservability extends base_command_1.BaseCommand {
|
|
|
93
93
|
outcomes: outcomes(kind.terminal),
|
|
94
94
|
escalated: rate(kind.escalationRate),
|
|
95
95
|
fires: String(kind.fires),
|
|
96
|
+
spend: kind.spend
|
|
97
|
+
? `$${kind.spend.today.toFixed(2)} / $${kind.spend.total.toFixed(2)}`
|
|
98
|
+
: '—',
|
|
96
99
|
budget: kind.budget ? `${kind.budget.used}/${kind.budget.limit ?? '∞'}` : '—',
|
|
97
100
|
silence: rate(kind.silenceRate),
|
|
98
101
|
})), [
|
|
@@ -104,11 +107,12 @@ class FollowupsObservability extends base_command_1.BaseCommand {
|
|
|
104
107
|
{ key: 'outcomes', header: 'OUTCOMES' },
|
|
105
108
|
{ key: 'escalated', header: 'ESCALATED' },
|
|
106
109
|
{ key: 'fires', header: 'FIRES' },
|
|
110
|
+
{ key: 'spend', header: 'SPENT TODAY/ALL' },
|
|
107
111
|
{ key: 'budget', header: 'TODAY/LIMIT' },
|
|
108
112
|
{ key: 'silence', header: 'SILENT' },
|
|
109
113
|
], (line) => this.log(line));
|
|
110
114
|
});
|
|
111
|
-
this.log(`\nAs of ${report.generatedAt ?? 'now'}. TODAY/LIMIT is the daily budget (reminders are not metered); SILENT is how often a check-in chose to stay quiet.`);
|
|
115
|
+
this.log(`\nAs of ${report.generatedAt ?? 'now'}. SPENT is the agent turns these follow-ups woke (USD, today / all time); TODAY/LIMIT is the daily creation budget (reminders are not metered); SILENT is how often a check-in chose to stay quiet.`);
|
|
112
116
|
}
|
|
113
117
|
}
|
|
114
118
|
exports.default = FollowupsObservability;
|
|
@@ -15,11 +15,14 @@ class FollowupsRemind extends base_command_1.BaseCommand {
|
|
|
15
15
|
static description = 'Remind the owner at a time — the reminder follow-up';
|
|
16
16
|
static examples = [
|
|
17
17
|
'<%= config.bin %> followups remind --subject task:OSK-42 --content "pick the deploy up" --in 2h',
|
|
18
|
-
'<%= config.bin %> followups remind --subject conversation:conv_9 --content "ask how the move went" --at 2026-09-20T09:00:00Z',
|
|
18
|
+
'<%= config.bin %> followups remind --subject conversation:conv_9 --content "ask how the move went" --at 2026-09-20T09:00:00Z --active --agent agent_1',
|
|
19
19
|
'<%= config.bin %> followups remind --subject task:OSK-42 --content "ping me" --at 2026-09-20T09:00:00Z --active --agent agent_1',
|
|
20
20
|
];
|
|
21
21
|
static flags = {
|
|
22
|
-
subject: core_1.Flags.string({
|
|
22
|
+
subject: core_1.Flags.string({
|
|
23
|
+
description: 'Subject ref: type:externalKey. A passive reminder attaches to a task, project, goal or wiki_page; use --active for a conversation',
|
|
24
|
+
required: true,
|
|
25
|
+
}),
|
|
23
26
|
content: core_1.Flags.string({ description: "The reminder's own words", required: true }),
|
|
24
27
|
at: core_1.Flags.string({ description: 'ISO-8601 instant to remind at', exclusive: ['in'] }),
|
|
25
28
|
in: core_1.Flags.string({
|
|
@@ -42,7 +42,8 @@ class FollowupsReschedule extends base_command_1.BaseCommand {
|
|
|
42
42
|
this.log(JSON.stringify(result, null, 2));
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
const next = result.followup?.nextRunAt;
|
|
46
|
+
this.log(`Follow-up ${args.id} rescheduled to ${typeof next === 'string' ? next : at}.`);
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
exports.default = FollowupsReschedule;
|
|
@@ -35,6 +35,10 @@ class FollowupsShow extends base_command_1.BaseCommand {
|
|
|
35
35
|
['intent', row.intentKey],
|
|
36
36
|
['producer', row.producer],
|
|
37
37
|
['terminal', row.terminal ? `${row.terminal} (${row.terminalReason ?? '—'})` : undefined],
|
|
38
|
+
[
|
|
39
|
+
'settled',
|
|
40
|
+
row.terminal ? `${row.terminalAt ?? '—'} by ${row.terminalBy ?? 'unknown'}` : undefined,
|
|
41
|
+
],
|
|
38
42
|
['deadline', row.deadlineAt],
|
|
39
43
|
['escalated to', row.escalatedInitiativeId],
|
|
40
44
|
]) {
|
|
@@ -21,7 +21,11 @@ const triggers_1 = require("../../lib/triggers");
|
|
|
21
21
|
* cannot stop is a leak.
|
|
22
22
|
*/
|
|
23
23
|
class FollowupsWatch extends base_command_1.BaseCommand {
|
|
24
|
-
static
|
|
24
|
+
static summary = 'Watch an asynchronous tail until it settles — the watch follow-up';
|
|
25
|
+
static description = [
|
|
26
|
+
'Two forms, and they cost different amounts. Prefer the predicate form whenever the state is readable over HTTPS: --url + --until is evaluated by the server, wakes the agent once when the condition settles, and always ends at --deadline.',
|
|
27
|
+
"The recipe form (--check / --success / --abort) wakes the agent for one turn per check, and every turn counts against the agent's watch budget. Use it only for a check a predicate cannot express.",
|
|
28
|
+
].join('\n\n');
|
|
25
29
|
static examples = [
|
|
26
30
|
'<%= config.bin %> followups watch --subject task:OSK-42 --why "deploy still running" --url https://ci.example.com/api/build/9 --until \'{"path":"status","op":"equals","value":"done"}\' --deadline 6h',
|
|
27
31
|
'<%= config.bin %> followups watch --subject task:OSK-42 --why "pr review" --check "gh pr view 42 --json state" --success "state == MERGED" --abort "state == CLOSED" --wait event --wait-entity github --wait-event pull_request_merged --wait-fallback 2h',
|
|
@@ -34,7 +38,7 @@ class FollowupsWatch extends base_command_1.BaseCommand {
|
|
|
34
38
|
// predicate form
|
|
35
39
|
url: core_1.Flags.string({ description: 'HTTPS endpoint to observe (predicate form)' }),
|
|
36
40
|
until: core_1.Flags.string({
|
|
37
|
-
description: 'Predicate tree as JSON (predicate form): a leaf {"path","op","value"} or {"all"|"any": [...]}. op is equals, not_equals, contains, starts_with, in, gt, lt, exists, … — an unknown op is refused with the full list',
|
|
41
|
+
description: 'Predicate tree as JSON (predicate form — the server evaluates it; no agent turn per check): a leaf {"path","op","value"} or {"all"|"any": [...]}. op is equals, not_equals, contains, starts_with, in, gt, lt, exists, … — an unknown op is refused with the full list',
|
|
38
42
|
}),
|
|
39
43
|
grant: core_1.Flags.string({ description: 'Grant id supplying the watch credentials' }),
|
|
40
44
|
deadline: core_1.Flags.string({ description: 'When the watch must stop (predicate form, required)' }),
|
|
@@ -43,7 +47,9 @@ class FollowupsWatch extends base_command_1.BaseCommand {
|
|
|
43
47
|
'root-path': core_1.Flags.string({ description: 'JSON root path inside the response' }),
|
|
44
48
|
'resume-prompt': core_1.Flags.string({ description: 'What to tell the settling turn' }),
|
|
45
49
|
// recipe form
|
|
46
|
-
check: core_1.Flags.string({
|
|
50
|
+
check: core_1.Flags.string({
|
|
51
|
+
description: 'The check the agent runs (recipe form) — every check wakes the agent for one turn, counted against the watch budget',
|
|
52
|
+
}),
|
|
47
53
|
success: core_1.Flags.string({ description: 'What state means the wait resolved (recipe form)' }),
|
|
48
54
|
abort: core_1.Flags.string({ description: 'When to stop and escalate (recipe form)' }),
|
|
49
55
|
wait: core_1.Flags.string({
|
|
@@ -43,11 +43,26 @@ class TasksLifecycleDetach extends base_command_1.BaseCommand {
|
|
|
43
43
|
return this.log('No lifecycle is attached to this Task — nothing to detach.');
|
|
44
44
|
this.log(`Would detach ${lifecycle.packId ? `pack ${lifecycle.packId}` : `definition ${lifecycle.definitionId}`}` +
|
|
45
45
|
` at generation ${lifecycle.generation}, stage ${lifecycle.currentStageKey ?? '—'}.`);
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
if (lifecycle.runId) {
|
|
47
|
+
this.log(' The run is cancelled and kept as history; nothing already recorded is removed.');
|
|
48
|
+
this.log(' The generation is bumped, so no in-flight writer from the old run can land.');
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
this.log(' It has not started, so there is no run to cancel.');
|
|
52
|
+
}
|
|
48
53
|
this.log(' Re-attaching afterwards starts a NEW generation — it does not resume this one.');
|
|
49
54
|
return;
|
|
50
55
|
}
|
|
56
|
+
// The server clears runId on detach, so whether there WAS a run to cancel
|
|
57
|
+
// is only knowable before the write. Best-effort: a failed read must not
|
|
58
|
+
// block the detach, it only costs the sentence its precision.
|
|
59
|
+
let hadRun;
|
|
60
|
+
try {
|
|
61
|
+
hadRun = Boolean((await data_provider_1.dataService.getTaskLifecycle(taskId)).lifecycle?.runId);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
hadRun = undefined;
|
|
65
|
+
}
|
|
51
66
|
let response;
|
|
52
67
|
try {
|
|
53
68
|
response = await data_provider_1.dataService.detachTaskLifecycle({
|
|
@@ -62,9 +77,15 @@ class TasksLifecycleDetach extends base_command_1.BaseCommand {
|
|
|
62
77
|
return this.log(JSON.stringify(response, null, 2));
|
|
63
78
|
const lifecycle = response.lifecycle;
|
|
64
79
|
// Name what changed. "Detached lifecycle from the Task." left the reader to
|
|
65
|
-
// diff the state afterwards to learn what had happened
|
|
66
|
-
|
|
67
|
-
|
|
80
|
+
// diff the state afterwards to learn what had happened — and saying a run
|
|
81
|
+
// was cancelled when none had started is the same problem the other way.
|
|
82
|
+
const runSentence = hadRun === false
|
|
83
|
+
? 'it had not started, so there was no run to cancel'
|
|
84
|
+
: hadRun
|
|
85
|
+
? 'the run is cancelled and kept as history'
|
|
86
|
+
: 'any run is cancelled and kept as history';
|
|
87
|
+
this.log(`Detached${lifecycle?.packId ? ` pack ${lifecycle.packId}` : ''} — ${runSentence},` +
|
|
88
|
+
` and the binding is now generation ${lifecycle?.generation ?? '?'}` +
|
|
68
89
|
` (${lifecycle?.status ?? 'detached'}).`);
|
|
69
90
|
this.log('Re-attach with: skrr tasks lifecycle attach <task-id> --pack <pack>');
|
|
70
91
|
}
|
|
@@ -127,7 +127,10 @@ class DedicatedLeaseCommand extends base_command_1.BaseCommand {
|
|
|
127
127
|
return await fn();
|
|
128
128
|
}
|
|
129
129
|
catch (err) {
|
|
130
|
-
const message = (0, dedicated_machines_1.formatDedicatedRuntimeApiError)(err
|
|
130
|
+
const message = (0, dedicated_machines_1.formatDedicatedRuntimeApiError)(err, {
|
|
131
|
+
command: this.id,
|
|
132
|
+
bin: this.config.bin,
|
|
133
|
+
});
|
|
131
134
|
if (message) {
|
|
132
135
|
this.failWithCliError({
|
|
133
136
|
message,
|
|
@@ -643,5 +643,12 @@ export declare function dedicatedRuntimeApiErrorCode(err: unknown): string | und
|
|
|
643
643
|
* second list would only drift from the first.
|
|
644
644
|
*/
|
|
645
645
|
export declare function dedicatedRuntimeApiErrorDetails(err: unknown): Record<string, unknown> | undefined;
|
|
646
|
-
|
|
646
|
+
/** What a refused command knows about itself, so advice can be one it can follow. */
|
|
647
|
+
export interface DedicatedApiErrorContext {
|
|
648
|
+
/** oclif command id, e.g. `machines:dedicated:create`. */
|
|
649
|
+
command?: string;
|
|
650
|
+
bin?: string;
|
|
651
|
+
leaseId?: string;
|
|
652
|
+
}
|
|
653
|
+
export declare function formatDedicatedRuntimeApiError(err: unknown, context?: DedicatedApiErrorContext): string | null;
|
|
647
654
|
export {};
|
|
@@ -1134,9 +1134,9 @@ function dedicatedRuntimeApiErrorDetails(err) {
|
|
|
1134
1134
|
* say nothing to the person running it (OSK-8768); `--json` keeps them in
|
|
1135
1135
|
* `error.details.reasons`.
|
|
1136
1136
|
*/
|
|
1137
|
-
function describeRefusalReasons(err) {
|
|
1137
|
+
function describeRefusalReasons(err, context = {}) {
|
|
1138
1138
|
const details = dedicatedRuntimeApiErrorDetails(err);
|
|
1139
|
-
const spend = describeSpendingCapRefusal(err.body, details);
|
|
1139
|
+
const spend = describeSpendingCapRefusal(err.body, details, context);
|
|
1140
1140
|
if (spend)
|
|
1141
1141
|
return spend;
|
|
1142
1142
|
const sentences = (0, harnesses_1.describeHealthReasonSentences)(details?.reasons, details?.state);
|
|
@@ -1148,7 +1148,7 @@ function describeRefusalReasons(err) {
|
|
|
1148
1148
|
* the cap. Without them "would exceed the monthly cap" left a user guessing by
|
|
1149
1149
|
* how much, and which cap (OSK-9552).
|
|
1150
1150
|
*/
|
|
1151
|
-
function describeSpendingCapRefusal(body, details) {
|
|
1151
|
+
function describeSpendingCapRefusal(body, details, context = {}) {
|
|
1152
1152
|
let code;
|
|
1153
1153
|
try {
|
|
1154
1154
|
code = JSON.parse(body || '{}').code;
|
|
@@ -1172,9 +1172,25 @@ function describeSpendingCapRefusal(body, details) {
|
|
|
1172
1172
|
].filter(Boolean);
|
|
1173
1173
|
if (parts.length === 0)
|
|
1174
1174
|
return '';
|
|
1175
|
-
return ` ${parts.join('; ')}.
|
|
1175
|
+
return ` ${parts.join('; ')}. ${raiseCapHint(context)}`;
|
|
1176
1176
|
}
|
|
1177
|
-
|
|
1177
|
+
/**
|
|
1178
|
+
* How to get past a cap refusal, for the command that was refused. A create or
|
|
1179
|
+
* a restore makes a NEW machine and takes its cap as a flag; only an existing
|
|
1180
|
+
* machine has a lease id to raise. Every refusal used to say
|
|
1181
|
+
* `machines dedicated spending-limit <lease-id>`, which a user refused on create
|
|
1182
|
+
* had no lease id to run (OSK-9552).
|
|
1183
|
+
*/
|
|
1184
|
+
function raiseCapHint(context) {
|
|
1185
|
+
const bin = context.bin || 'skrr';
|
|
1186
|
+
const creates = context.command === 'machines:dedicated:create' ||
|
|
1187
|
+
context.command === 'machines:dedicated:restore';
|
|
1188
|
+
if (creates) {
|
|
1189
|
+
return 'Pass a higher --spending-limit-cents: the cap is compared with the whole month, not with this machine alone.';
|
|
1190
|
+
}
|
|
1191
|
+
return `Raise the cap with \`${bin} machines dedicated spending-limit ${context.leaseId || '<lease-id>'} --cents <n>\`.`;
|
|
1192
|
+
}
|
|
1193
|
+
function formatDedicatedRuntimeApiError(err, context = {}) {
|
|
1178
1194
|
if (!(err instanceof api_fetch_1.ApiFetchError) || !err.body) {
|
|
1179
1195
|
return null;
|
|
1180
1196
|
}
|
|
@@ -1202,7 +1218,7 @@ function formatDedicatedRuntimeApiError(err) {
|
|
|
1202
1218
|
// Machine noun even though the caller named a Dedicated Runtime. Do not
|
|
1203
1219
|
// repeat that contradictory product name while a mixed deployment rolls.
|
|
1204
1220
|
const sentence = (parsed.message || parsed.error || parsed.code)?.replace(/(?:skrr )?Hosted Machine/g, 'Dedicated Runtime');
|
|
1205
|
-
return sentence ? `${sentence}${describeRefusalReasons(err)}` : null;
|
|
1221
|
+
return sentence ? `${sentence}${describeRefusalReasons(err, context)}` : null;
|
|
1206
1222
|
}
|
|
1207
1223
|
catch {
|
|
1208
1224
|
return err.body.trim() || null;
|
|
@@ -17,8 +17,21 @@ function backupIsNewer(current, previous) {
|
|
|
17
17
|
const b = Date.parse(previous);
|
|
18
18
|
return Number.isFinite(a) && Number.isFinite(b) ? a > b : current !== previous;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* `recovering` is also the state an image move replaces the machine through, on
|
|
22
|
+
* purpose. Reading every `recovering` as a failed action being retried told an
|
|
23
|
+
* owner watching an ordinary `update-image --wait` that destroy was available
|
|
24
|
+
* "if it never settles".
|
|
25
|
+
*/
|
|
26
|
+
const PLANNED_REPLACEMENT_PROGRESS = {
|
|
27
|
+
dedicated_runtime_image_update: 'replacing the machine',
|
|
28
|
+
dedicated_runtime_image_update_rolling_back: 'replacing the machine again',
|
|
29
|
+
};
|
|
20
30
|
function progressOf(lease) {
|
|
21
31
|
const state = (0, dedicated_machines_1.dedicatedLeaseState)(lease);
|
|
32
|
+
const planned = lease.stateReason ? PLANNED_REPLACEMENT_PROGRESS[lease.stateReason] : undefined;
|
|
33
|
+
if (state === 'recovering' && planned)
|
|
34
|
+
return planned;
|
|
22
35
|
if (state === 'recovering') {
|
|
23
36
|
// `recovering` is where a failed provision or action lands, retried with
|
|
24
37
|
// backoff. It can settle — or never, which is why destroy is allowed from it.
|
package/dist/lib/followups.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface FollowUpRow {
|
|
|
34
34
|
terminal?: string | null;
|
|
35
35
|
terminalAt?: string | null;
|
|
36
36
|
terminalReason?: string | null;
|
|
37
|
+
/** Who settled it: `trigger_fire` for the runtime, otherwise the actor. */
|
|
38
|
+
terminalBy?: string | null;
|
|
37
39
|
escalatedInitiativeId?: string | null;
|
|
38
40
|
capsule?: unknown;
|
|
39
41
|
nextRunAt?: string | null;
|
|
@@ -62,6 +64,12 @@ export interface FollowUpObservability {
|
|
|
62
64
|
terminal: Record<string, number>;
|
|
63
65
|
escalationRate: number | null;
|
|
64
66
|
fires: number;
|
|
67
|
+
/** USD from the fires' UsageLog rows; null when the ledger could not be read. */
|
|
68
|
+
spend?: {
|
|
69
|
+
today: number;
|
|
70
|
+
total: number;
|
|
71
|
+
currency: string;
|
|
72
|
+
} | null;
|
|
65
73
|
budget: {
|
|
66
74
|
pool: string | null;
|
|
67
75
|
used: number;
|
package/dist/lib/label-ref.d.ts
CHANGED
|
@@ -59,12 +59,19 @@ export declare function closestNames(typed: string, names: string[], limit?: num
|
|
|
59
59
|
export interface LabelVocabularyContext {
|
|
60
60
|
/** Narrow to labels attachable to this entity type. Omit for all of them. */
|
|
61
61
|
applicableTo?: LabelApplicable;
|
|
62
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Narrows to this workspace scope. When omitted the AMBIENT workspace is
|
|
64
|
+
* resolved (`--workspace` / OVERSKY_WORKSPACE_ID / the server's default
|
|
65
|
+
* workspace): a session that never selected a workspace still writes to
|
|
66
|
+
* one, and a vocabulary that cannot see it refuses names that exist.
|
|
67
|
+
*/
|
|
63
68
|
workspaceId?: string;
|
|
64
69
|
/** Archived labels are excluded unless asked for. */
|
|
65
70
|
includeArchived?: boolean;
|
|
66
71
|
/** Injected in tests. */
|
|
67
72
|
getLabels?: typeof dataService.getLabels;
|
|
73
|
+
/** Injected in tests; defaults to `workspaces.ambientWorkspaceId`. */
|
|
74
|
+
resolveWorkspaceId?: () => Promise<string | undefined>;
|
|
68
75
|
}
|
|
69
76
|
export interface LabelRefContext extends LabelVocabularyContext {
|
|
70
77
|
/** Which entity the resolved labels will be attached to. */
|
package/dist/lib/label-ref.js
CHANGED
|
@@ -39,6 +39,7 @@ exports.closestNames = closestNames;
|
|
|
39
39
|
exports.collectLabelVocabulary = collectLabelVocabulary;
|
|
40
40
|
exports.resolveLabelRefs = resolveLabelRefs;
|
|
41
41
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
42
|
+
const workspaces_1 = require("./workspaces");
|
|
42
43
|
/** A canonical v4 uuid, which is what every label id is. */
|
|
43
44
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
44
45
|
function looksLikeLabelId(ref) {
|
|
@@ -89,9 +90,18 @@ function closestNames(typed, names, limit = 3) {
|
|
|
89
90
|
*/
|
|
90
91
|
async function collectLabelVocabulary(context) {
|
|
91
92
|
const getLabels = context.getLabels ?? data_provider_1.dataService.getLabels;
|
|
93
|
+
let workspaceId = context.workspaceId;
|
|
94
|
+
if (!workspaceId) {
|
|
95
|
+
try {
|
|
96
|
+
workspaceId = await (context.resolveWorkspaceId ?? workspaces_1.ambientWorkspaceId)();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
workspaceId = undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
92
102
|
const requests = [{ scope: 'user' }];
|
|
93
|
-
if (
|
|
94
|
-
requests.push({ scope: 'workspace', scopeId:
|
|
103
|
+
if (workspaceId)
|
|
104
|
+
requests.push({ scope: 'workspace', scopeId: workspaceId });
|
|
95
105
|
const settled = await Promise.allSettled(requests.map((request) => getLabels({
|
|
96
106
|
...request,
|
|
97
107
|
...(context.applicableTo ? { applicableTo: context.applicableTo } : {}),
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A "did you mean" for oclif's enum refusal (OSK-9735).
|
|
3
|
+
*
|
|
4
|
+
* Tasks, spaces, goals and key results each spell one lifecycle their own way —
|
|
5
|
+
* "under way" is `in_progress` for a space and `active` for a goal; "finished"
|
|
6
|
+
* is `done`, `completed` or `achieved` — and oclif's
|
|
7
|
+
* `Expected --status=in_progress to be one of: …` was the only place a user
|
|
8
|
+
* learned that. This names the value the command uses for the word that was
|
|
9
|
+
* typed, and otherwise the nearest spelling.
|
|
10
|
+
*
|
|
11
|
+
* It suggests and never substitutes. Some of the differences are deliberate (a
|
|
12
|
+
* key result's status is a health reading, not a lifecycle), so writing a mapped
|
|
13
|
+
* value would record a status nobody chose.
|
|
14
|
+
*/
|
|
15
|
+
export declare function invalidOptionHint(message: string): string | null;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* A "did you mean" for oclif's enum refusal (OSK-9735).
|
|
4
|
+
*
|
|
5
|
+
* Tasks, spaces, goals and key results each spell one lifecycle their own way —
|
|
6
|
+
* "under way" is `in_progress` for a space and `active` for a goal; "finished"
|
|
7
|
+
* is `done`, `completed` or `achieved` — and oclif's
|
|
8
|
+
* `Expected --status=in_progress to be one of: …` was the only place a user
|
|
9
|
+
* learned that. This names the value the command uses for the word that was
|
|
10
|
+
* typed, and otherwise the nearest spelling.
|
|
11
|
+
*
|
|
12
|
+
* It suggests and never substitutes. Some of the differences are deliberate (a
|
|
13
|
+
* key result's status is a health reading, not a lifecycle), so writing a mapped
|
|
14
|
+
* value would record a status nobody chose.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.invalidOptionHint = invalidOptionHint;
|
|
18
|
+
/**
|
|
19
|
+
* Spellings of one concept across the CLI's vocabularies. A value is suggested
|
|
20
|
+
* only when it shares a group with what was typed.
|
|
21
|
+
*/
|
|
22
|
+
const CONCEPT_GROUPS = [
|
|
23
|
+
new Set(['in_progress', 'inprogress', 'active', 'started', 'ongoing', 'running', 'doing']),
|
|
24
|
+
new Set(['done', 'completed', 'complete', 'achieved', 'finished', 'closed', 'resolved']),
|
|
25
|
+
new Set(['cancelled', 'canceled', 'abandoned']),
|
|
26
|
+
new Set(['in_review', 'inreview', 'review', 'reviewing']),
|
|
27
|
+
new Set(['planned', 'todo', 'to_do', 'not_started']),
|
|
28
|
+
];
|
|
29
|
+
const REFUSAL = /Expected (--[\w-]+)=(.+?) to be one of: ([^\n]+)/;
|
|
30
|
+
const normalize = (value) => value
|
|
31
|
+
.trim()
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.replace(/[\s-]+/g, '_');
|
|
34
|
+
function editDistance(a, b) {
|
|
35
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
36
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
37
|
+
let diagonal = row[0];
|
|
38
|
+
row[0] = i;
|
|
39
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
40
|
+
const above = row[j];
|
|
41
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
42
|
+
diagonal = above;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return row[b.length];
|
|
46
|
+
}
|
|
47
|
+
function invalidOptionHint(message) {
|
|
48
|
+
const match = REFUSAL.exec(message || '');
|
|
49
|
+
if (!match)
|
|
50
|
+
return null;
|
|
51
|
+
const [, flag, input, list] = match;
|
|
52
|
+
const options = list
|
|
53
|
+
.split(',')
|
|
54
|
+
.map((option) => option.trim())
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
const typed = normalize(input);
|
|
57
|
+
const exact = options.find((option) => normalize(option) === typed);
|
|
58
|
+
if (exact)
|
|
59
|
+
return ` Did you mean ${flag} ${exact}?`;
|
|
60
|
+
const group = CONCEPT_GROUPS.find((concept) => concept.has(typed));
|
|
61
|
+
const synonym = group && options.find((option) => group.has(normalize(option)));
|
|
62
|
+
if (synonym) {
|
|
63
|
+
return ` Did you mean ${flag} ${synonym}? Here "${input}" is spelled "${synonym}".`;
|
|
64
|
+
}
|
|
65
|
+
let nearest = null;
|
|
66
|
+
for (const option of options) {
|
|
67
|
+
const distance = editDistance(typed, normalize(option));
|
|
68
|
+
if (!nearest || distance < nearest.distance)
|
|
69
|
+
nearest = { option, distance };
|
|
70
|
+
}
|
|
71
|
+
if (nearest && nearest.distance <= Math.max(2, Math.floor(typed.length / 4))) {
|
|
72
|
+
return ` Did you mean ${flag} ${nearest.option}?`;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
package/dist/lib/workspaces.d.ts
CHANGED
|
@@ -56,6 +56,9 @@ export interface WorkspaceListing {
|
|
|
56
56
|
*/
|
|
57
57
|
export declare function describeUnselectedWorkspace(listing: WorkspaceListing): string;
|
|
58
58
|
export declare function listWorkspaces(): Promise<WorkspaceRow[]>;
|
|
59
|
+
export declare function ambientWorkspaceId(env?: NodeJS.ProcessEnv): Promise<string | undefined>;
|
|
60
|
+
/** Test seam — a memoized network result must not leak between specs. */
|
|
61
|
+
export declare function clearAmbientWorkspaceCache(): void;
|
|
59
62
|
/** `listWorkspaces`, keeping the envelope's caller-level fields. */
|
|
60
63
|
export declare function listWorkspaceListing(): Promise<WorkspaceListing>;
|
|
61
64
|
export declare function getWorkspace(workspaceId: string): Promise<WorkspaceRow>;
|