@skrr-ai/cli 0.1.130 → 0.1.132

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.
@@ -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,6 +763,14 @@ 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' && (overlayThreadTs || bodyDestination?.threadTs)) {
767
+ throw invalidIntent('--thread-ts / destination.threadTs only applies to slack_channel.');
768
+ }
769
+ if (destinationKind !== 'agent_email' &&
770
+ (merged.recipients !== undefined || merged.subject !== undefined ||
771
+ bodyDestination?.recipients !== undefined || bodyDestination?.subject !== undefined)) {
772
+ throw invalidIntent('--to / destination.recipients and --subject / destination.subject only apply to agent_email.');
773
+ }
750
774
  const destination = buildDestination({
751
775
  kind: destinationKind,
752
776
  bodyDestination,
@@ -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
  }
@@ -25,7 +25,7 @@ class AgentsScheduleTrace extends base_command_1.BaseCommand {
25
25
  };
26
26
  static flags = {
27
27
  json: core_1.Flags.boolean({ description: 'Output as JSON' }),
28
- date: core_1.Flags.string({ description: 'Plan date (YYYY-MM-DD). Defaults to today.' }),
28
+ date: core_1.Flags.string({ description: 'Plan date (YYYY-MM-DD). Defaults to today in the plan timezone.' }),
29
29
  todo: core_1.Flags.string({ description: 'Scope the trace to one to-do' }),
30
30
  timezone: core_1.Flags.string({ description: 'IANA timezone for the plan day' }),
31
31
  limit: core_1.Flags.integer({ description: 'Events to return (1-200)' }),
@@ -34,16 +34,25 @@ class AgentsScheduleTrace extends base_command_1.BaseCommand {
34
34
  async run() {
35
35
  this.requireAuth();
36
36
  const { args, flags } = await this.parse(AgentsScheduleTrace);
37
- // The server requires a plan date. Defaulting to today locally keeps the
38
- // common call a single argument rather than a date the caller must know.
39
- const date = flags.date ?? new Date().toISOString().slice(0, 10);
37
+ // Use the same zone for choosing the day and querying its events. UTC can
38
+ // already be tomorrow while it is still evening for the reader.
39
+ const timezone = flags.timezone ?? (flags.date ? undefined : Intl.DateTimeFormat().resolvedOptions().timeZone);
40
+ let date = flags.date;
41
+ if (!date) {
42
+ try {
43
+ date = (0, schedule_1.scheduleTraceToday)(new Date(), timezone);
44
+ }
45
+ catch {
46
+ this.error(`Invalid IANA timezone: ${timezone}`, { exit: 1 });
47
+ }
48
+ }
40
49
  let trace;
41
50
  try {
42
51
  trace = await schedule_1.scheduleApi.scheduleTrace({
43
52
  agentId: args.agent,
44
53
  date,
45
54
  todoId: flags.todo,
46
- timezone: flags.timezone,
55
+ timezone,
47
56
  limit: flags.limit,
48
57
  cursor: flags.cursor,
49
58
  });
@@ -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
  }
@@ -21,14 +21,15 @@ class AgentsTodosList extends base_command_1.BaseCommand {
21
21
  };
22
22
  static flags = {
23
23
  json: core_1.Flags.boolean({ description: 'Output as JSON' }),
24
- date: core_1.Flags.string({ description: 'Day to list (YYYY-MM-DD, UTC). Defaults to today.' }),
24
+ date: core_1.Flags.string({ description: 'Day to list (YYYY-MM-DD). Defaults to today in the selected timezone.' }),
25
+ timezone: core_1.Flags.string({ description: 'IANA timezone for the day (defaults to UTC)' }),
25
26
  };
26
27
  async run() {
27
28
  this.requireAuth();
28
29
  const { args, flags } = await this.parse(AgentsTodosList);
29
30
  let response;
30
31
  try {
31
- response = await schedule_1.scheduleApi.listTodos(args.id, flags.date);
32
+ response = await schedule_1.scheduleApi.listTodos(args.id, flags.date, flags.timezone);
32
33
  }
33
34
  catch (err) {
34
35
  this.handleApiError(err);
@@ -37,6 +38,7 @@ class AgentsTodosList extends base_command_1.BaseCommand {
37
38
  this.printJsonResult((0, list_output_1.listResult)(response, 'todos'));
38
39
  return;
39
40
  }
41
+ this.log(`Day timezone: ${flags.timezone || 'UTC'}`);
40
42
  (0, schedule_1.renderTodoList)(response.todos ?? [], (line) => this.log(line));
41
43
  }
42
44
  }
@@ -107,6 +107,19 @@ function assertEventDrivenWatchedSource(payload) {
107
107
  hasEventCapableWatchedSource(payload)) {
108
108
  return;
109
109
  }
110
+ const sources = Array.isArray(payload.watchedSources) ? payload.watchedSources : [];
111
+ const hasInvalidEvents = sources.some((value) => {
112
+ const source = record(value);
113
+ const config = record(source?.config);
114
+ return source?.enabled !== false && Array.isArray(config?.events) &&
115
+ !hasStructurallyValidExplicitEvent(config);
116
+ });
117
+ if (hasInvalidEvents) {
118
+ throw new Error('Event-driven cadence requires at least one event-capable watched source. ' +
119
+ 'An explicit config.events list must contain event objects with non-empty entityType and eventType, ' +
120
+ 'for example {"entityType":"github","eventType":"check_run_failed"}. ' +
121
+ 'Supply valid bindings, or remove config.events to use the source defaults (which may include more events).');
122
+ }
110
123
  throw new Error('Event-driven cadence (--event-driven or --cadence-mode event_plus_interval) requires at least one event-capable watched source. ' +
111
124
  'Add --watch github (or another event-capable source), or choose an interval-only cadence.');
112
125
  }
@@ -56,6 +56,15 @@ class DaemonInstall extends core_1.Command {
56
56
  // own comment records `skrr code run -m <model>` dying the same way. Same
57
57
  // mistake, one directory over.
58
58
  const argv = this.argv;
59
+ // A forwarded help request is read-only. Do not run installation preflight,
60
+ // bind a profile, or download software just to show the runtime's options.
61
+ if (argv.some((arg) => arg === '--help' || arg === '-h')) {
62
+ if (!(0, exec_runtime_binary_1.isCurrentRuntimeBinary)((0, exec_runtime_binary_1.findRuntimeBinary)())) {
63
+ this.log('The background service is not installed, so its detailed install options are unavailable. Run `skrr daemon install` to install it.');
64
+ return;
65
+ }
66
+ this.exit(await (0, exec_runtime_binary_1.execRuntimeBinary)(['install', '--help']));
67
+ }
59
68
  // Before fetching anything: a machine still carrying the previous product
60
69
  // identity's state is refused by the runtime's own install, so downloading a
61
70
  // release first only delays the same refusal (OSK-6895).
@@ -18,6 +18,7 @@ export default class HarnessesPrune extends BaseCommand {
18
18
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
19
  limit: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
20
  'older-than': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ 'include-gone-leases': import("@oclif/core/interfaces").BooleanFlag<boolean>;
21
22
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
22
23
  };
23
24
  run(): Promise<void>;
@@ -30,12 +30,20 @@ class HarnessesPrune extends base_command_1.BaseCommand {
30
30
  'pinned row is KEPT, with the agents named. Move the pin (`agents attach-harness`) and run this again.\n\n' +
31
31
  'At most --limit rows are removed per run, oldest first; the rest are counted and the next run takes ' +
32
32
  'them. Running it again when nothing qualifies removes nothing. A daemon that is still running ' +
33
- 're-registers on its next connection, so removing a row never stops a machine.';
33
+ 're-registers on its next connection, so removing a row never stops a machine.\n\n' +
34
+ 'A hosted, dedicated or cloud machine belongs to its lease, so by default it is never touched. ' +
35
+ '--include-gone-leases also considers the rows of such a machine whose lease is GONE (archived, ' +
36
+ 'terminating, terminated, failed or lost): no compute runs on it again, so the row can never come ' +
37
+ 'back online. A row with no lease, a lease that can come back (stopped, ready, active) and a row ' +
38
+ 'with usage recorded against it are kept, and so is everything the rules above keep. Use --older-than ' +
39
+ 'to leave recent ones, for example 30d.';
34
40
  static examples = [
35
41
  '<%= config.bin %> harnesses prune',
36
42
  '<%= config.bin %> harnesses prune --yes',
37
43
  '<%= config.bin %> harnesses prune --yes --limit 20',
38
44
  '<%= config.bin %> harnesses prune --older-than 7d --json',
45
+ '<%= config.bin %> harnesses prune --include-gone-leases --older-than 30d',
46
+ '<%= config.bin %> harnesses prune --include-gone-leases --older-than 30d --yes --limit 200',
39
47
  ];
40
48
  static flags = {
41
49
  yes: core_1.Flags.boolean({
@@ -50,6 +58,10 @@ class HarnessesPrune extends base_command_1.BaseCommand {
50
58
  'older-than': core_1.Flags.string({
51
59
  description: 'Only rows unseen for at least this long: 90m, 36h or 7d (default 24h). The server never goes below its offline window.',
52
60
  }),
61
+ 'include-gone-leases': core_1.Flags.boolean({
62
+ description: 'Also consider the rows of hosted, dedicated or cloud machines whose lease is gone. Off by default.',
63
+ default: false,
64
+ }),
53
65
  json: core_1.Flags.boolean({ description: 'Output as JSON' }),
54
66
  };
55
67
  async run() {
@@ -71,6 +83,7 @@ class HarnessesPrune extends base_command_1.BaseCommand {
71
83
  dryRun: !flags.yes,
72
84
  ...(flags.limit !== undefined ? { limit: flags.limit } : {}),
73
85
  ...(minUnseenMs !== undefined ? { minUnseenMs } : {}),
86
+ ...(flags['include-gone-leases'] ? { includeGoneLeases: true } : {}),
74
87
  });
75
88
  }
76
89
  catch (err) {
@@ -86,7 +99,9 @@ class HarnessesPrune extends base_command_1.BaseCommand {
86
99
  render(result) {
87
100
  const { dryRun } = result;
88
101
  const removing = dryRun ? result.remove.length : result.removedCount;
89
- const noun = (n) => `${n} superseded harness row${n === 1 ? '' : 's'}`;
102
+ // "superseded" is only true of the personal-machine rows; a gone-lease row was not replaced by anything.
103
+ const kind = result.remove.some((row) => row.goneLease) ? '' : 'superseded ';
104
+ const noun = (n) => `${n} ${kind}harness row${n === 1 ? '' : 's'}`;
90
105
  if (dryRun) {
91
106
  this.log('Dry run - nothing was removed.');
92
107
  }
@@ -141,6 +156,9 @@ class HarnessesPrune extends base_command_1.BaseCommand {
141
156
  if (count('not_personal_machine') > 0) {
142
157
  passedOver.push(`${count('not_personal_machine')} hosted, dedicated, cloud or of unknown origin`);
143
158
  }
159
+ if (count('lease_not_gone') > 0) {
160
+ passedOver.push(`${count('lease_not_gone')} of a machine whose lease is not gone`);
161
+ }
144
162
  if (count('no_machine_identity') > 0) {
145
163
  passedOver.push(`${count('no_machine_identity')} that name no machine`);
146
164
  }
@@ -159,13 +177,15 @@ class HarnessesPrune extends base_command_1.BaseCommand {
159
177
  provider: (0, format_1.formatCell)(row.provider),
160
178
  daemon: (0, format_1.formatCell)(row.daemonId),
161
179
  seen: (0, format_1.localStamp)(row.lastSeenAt),
162
- replacedBy: (0, format_1.formatCell)(row.supersededBy?.daemonId),
180
+ replacedBy: row.goneLease
181
+ ? `lease gone (${row.goneLease})`
182
+ : (0, format_1.formatCell)(row.supersededBy?.daemonId),
163
183
  })), [
164
184
  { key: 'id', header: 'HARNESS ID' },
165
185
  { key: 'provider', header: 'PROVIDER' },
166
186
  { key: 'daemon', header: 'DAEMON ID', verbatim: true },
167
187
  { key: 'seen', header: `LAST SEEN (${(0, format_1.localZone)()})` },
168
- { key: 'replacedBy', header: 'REPLACED BY (ONLINE)', verbatim: true },
188
+ { key: 'replacedBy', header: 'REPLACED BY (ONLINE) / LEASE', verbatim: true },
169
189
  ], (line) => this.log(line));
170
190
  }
171
191
  }