@skrr-ai/cli 0.1.104 → 0.1.106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/base-command.js +6 -2
  2. package/dist/commands/agenda/trigger/list.js +2 -0
  3. package/dist/commands/agents/chat.js +4 -1
  4. package/dist/commands/agents/operating-plan.js +5 -2
  5. package/dist/commands/agents/triggers/create.js +3 -1
  6. package/dist/commands/agents/triggers/fire.js +2 -0
  7. package/dist/commands/agents/triggers/replay.js +2 -0
  8. package/dist/commands/agents/triggers/update.js +12 -4
  9. package/dist/commands/commitments/action-history.js +7 -2
  10. package/dist/commands/commitments/cadence-decisions.js +6 -1
  11. package/dist/commands/commitments/cadence-requests/decide.d.ts +1 -1
  12. package/dist/commands/commitments/cadence-requests/decide.js +1 -1
  13. package/dist/commands/commitments/doctor.js +7 -1
  14. package/dist/commands/commitments/observations.js +7 -1
  15. package/dist/commands/commitments/operating-review.d.ts +15 -0
  16. package/dist/commands/commitments/operating-review.js +23 -2
  17. package/dist/commands/commitments/preflight.d.ts +12 -0
  18. package/dist/commands/commitments/preflight.js +23 -9
  19. package/dist/commands/commitments/reflection.js +6 -1
  20. package/dist/commands/commitments/show.js +3 -1
  21. package/dist/commands/commitments/trigger-status.js +2 -22
  22. package/dist/commands/commitments/wake-capabilities.d.ts +13 -0
  23. package/dist/commands/commitments/wake-capabilities.js +39 -3
  24. package/dist/commands/daemon/logs.d.ts +73 -3
  25. package/dist/commands/daemon/logs.js +304 -17
  26. package/dist/commands/followups/act.js +13 -1
  27. package/dist/commands/followups/checkin.js +11 -2
  28. package/dist/commands/followups/reschedule.js +13 -1
  29. package/dist/commands/proposals/discard.d.ts +1 -1
  30. package/dist/commands/proposals/keep.d.ts +1 -1
  31. package/dist/commands/proposals/list.d.ts +7 -0
  32. package/dist/commands/proposals/list.js +31 -5
  33. package/dist/commands/proposals/show.d.ts +2 -1
  34. package/dist/commands/proposals/show.js +31 -4
  35. package/dist/commands/proposals/snooze.d.ts +1 -1
  36. package/dist/commands/requests/create.js +8 -3
  37. package/dist/commands/spaces/workflows/create.js +3 -3
  38. package/dist/commands/tasks/complete.js +4 -0
  39. package/dist/commands/tasks/create.d.ts +2 -0
  40. package/dist/commands/tasks/create.js +17 -3
  41. package/dist/commands/tasks/deliverable/add.d.ts +2 -0
  42. package/dist/commands/tasks/deliverable/add.js +22 -7
  43. package/dist/commands/tasks/deliverable/list.d.ts +13 -0
  44. package/dist/commands/tasks/deliverable/list.js +34 -1
  45. package/dist/commands/tasks/economics-compare.js +7 -1
  46. package/dist/commands/tasks/expectations.d.ts +10 -0
  47. package/dist/commands/tasks/expectations.js +31 -3
  48. package/dist/commands/tasks/list.d.ts +9 -0
  49. package/dist/commands/tasks/list.js +40 -5
  50. package/dist/commands/tasks/report.js +1 -1
  51. package/dist/commands/tasks/result/show.d.ts +7 -0
  52. package/dist/commands/tasks/result/show.js +32 -1
  53. package/dist/commands/tasks/result/submit.js +1 -1
  54. package/dist/commands/tasks/retry-schedule.js +11 -1
  55. package/dist/commands/tasks/show.d.ts +6 -0
  56. package/dist/commands/tasks/show.js +23 -0
  57. package/dist/commands/tasks/update.d.ts +3 -0
  58. package/dist/commands/tasks/update.js +23 -3
  59. package/dist/commands/triggers/audit.js +6 -2
  60. package/dist/commands/triggers/create.js +3 -1
  61. package/dist/commands/triggers/fire.d.ts +1 -0
  62. package/dist/commands/triggers/fire.js +16 -1
  63. package/dist/commands/triggers/preflight.js +6 -5
  64. package/dist/commands/triggers/replay.js +2 -0
  65. package/dist/commands/triggers/update.js +12 -4
  66. package/dist/commands/workspaces/stewardship.js +5 -1
  67. package/dist/lib/agentic-stream.d.ts +9 -0
  68. package/dist/lib/agentic-stream.js +38 -18
  69. package/dist/lib/commitment-event-wakes.d.ts +8 -0
  70. package/dist/lib/commitment-event-wakes.js +39 -0
  71. package/dist/lib/commitment-ledgers.d.ts +9 -0
  72. package/dist/lib/commitment-ledgers.js +126 -0
  73. package/dist/lib/commitments.d.ts +102 -2
  74. package/dist/lib/commitments.js +208 -8
  75. package/dist/lib/delegated-cli.js +13 -2
  76. package/dist/lib/followups.d.ts +6 -0
  77. package/dist/lib/followups.js +12 -0
  78. package/dist/lib/message-content.d.ts +6 -0
  79. package/dist/lib/message-content.js +24 -0
  80. package/dist/lib/operating-plan-loops.d.ts +8 -0
  81. package/dist/lib/operating-plan-loops.js +32 -0
  82. package/dist/lib/preflight-copy.d.ts +27 -0
  83. package/dist/lib/preflight-copy.js +89 -0
  84. package/dist/lib/proposal-decision-command.d.ts +1 -1
  85. package/dist/lib/proposal-decision-command.js +27 -3
  86. package/dist/lib/proposals.d.ts +17 -0
  87. package/dist/lib/proposals.js +41 -0
  88. package/dist/lib/stewardship-output.js +57 -12
  89. package/dist/lib/task-economics-line.d.ts +30 -0
  90. package/dist/lib/task-economics-line.js +49 -0
  91. package/dist/lib/task-economics.d.ts +15 -0
  92. package/dist/lib/task-economics.js +73 -8
  93. package/dist/lib/task-recurrence-outcome.d.ts +19 -0
  94. package/dist/lib/task-recurrence-outcome.js +30 -0
  95. package/dist/lib/triggers.d.ts +42 -0
  96. package/dist/lib/triggers.js +136 -2
  97. package/dist/lib/widgetTerminalText.d.ts +8 -0
  98. package/dist/lib/widgetTerminalText.js +116 -0
  99. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +33 -0
  100. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +27 -0
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.d.ts +28 -5
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.js +50 -5
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +33 -0
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +26 -0
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.d.ts +28 -5
  106. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.js +50 -5
  107. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  108. package/dist/node_modules/@skrr-ai/data-provider/index.js +7261 -7056
  109. package/docs/v1/operations.md +1 -1
  110. package/oclif.manifest.json +116 -72
  111. package/package.json +1 -1
@@ -19,6 +19,7 @@ export interface DaemonLogsFlags {
19
19
  stderr?: boolean;
20
20
  path?: boolean;
21
21
  profile?: string;
22
+ rotated?: boolean;
22
23
  }
23
24
  /**
24
25
  * Older daemons already kept logs in this profile-scoped location, even
@@ -116,6 +117,69 @@ export declare function resolveForwardedLogPath(flags: Pick<DaemonLogsFlags, 'st
116
117
  * forwarding to the bug, so the CLI forwards only at or above this version.
117
118
  */
118
119
  export declare const LIVE_LOG_RESOLUTION_RUNTIME_VERSION = "0.8.119";
120
+ /**
121
+ * The first runtime whose `logs` knows `--rotated` (OSK-16214). An older one
122
+ * rejects it as an unknown option, so the CLI never forwards the flag below
123
+ * this version: it asks the runtime which file it reads (`--path`) and reads
124
+ * that file and its rotated siblings itself.
125
+ */
126
+ export declare const ROTATED_LOGS_RUNTIME_VERSION = "0.8.120";
127
+ /**
128
+ * The first runtime whose `logs --rotated --lines N` says where a window that
129
+ * cut older lines starts, and whose `logs --path` names the rotated siblings
130
+ * on stderr (OSK-16228). 0.8.120 knows `--rotated` but says neither, so for it
131
+ * the CLI reads the files itself, exactly as it does for an older runtime.
132
+ */
133
+ export declare const ROTATED_WINDOW_NOTE_RUNTIME_VERSION = "0.8.121";
134
+ /**
135
+ * Rotated siblings of a log, OLDEST FIRST (OSK-16214). The daemon's logger
136
+ * renames `daemon.app.log` to `daemon.app.1.log`, `.1` to `.2` and so on at
137
+ * 10 MB (`daemon/src/logger.ts` `maybeRotate`), and `logs` never reached them —
138
+ * `--lines 5000 | grep <run id>` found nothing while `daemon.app.1.log` held the
139
+ * run. A copy of `daemon/src/rotated-log-files.ts`, for runtimes that predate
140
+ * `--rotated`. Never throws.
141
+ */
142
+ export declare function rotatedLogSiblings(filePath: string, readdir?: (dir: string) => string[]): string[];
143
+ /** The sentence shown when rotated siblings hold older lines the view leaves out. */
144
+ export declare function describeRotatedSiblings(filePath: string, siblings: readonly string[]): string | null;
145
+ /**
146
+ * The last `maxLines` lines of one file, read backwards in chunks so the cost
147
+ * follows the lines asked for, not the file's size. Bytes are decoded once, so
148
+ * a multi-byte character split across a chunk boundary survives.
149
+ */
150
+ export declare function readLastLines(filePath: string, maxLines: number): string[];
151
+ /**
152
+ * The last `maxLines` lines across `files` (OLDEST FIRST) as if they were one
153
+ * file. A file renamed away between the listing and the read is skipped.
154
+ */
155
+ export declare function readLastLinesAcross(files: readonly string[], maxLines: number): string[];
156
+ /**
157
+ * Where a `--rotated --lines N` window began when it left older lines out
158
+ * (OSK-16228). `startLine` is 1-based within `file`; `totalLines` counts the
159
+ * whole set. A copy of `daemon/src/rotated-log-files.ts`, like the helpers above.
160
+ */
161
+ export interface LogWindowCut {
162
+ file: string;
163
+ startLine: number;
164
+ totalLines: number;
165
+ }
166
+ /** Lines in a file, split as `readLastLines` splits them; read in chunks. */
167
+ export declare function countLines(filePath: string): number;
168
+ /**
169
+ * `readLastLinesAcross`, plus where the window began when it cut older lines.
170
+ * Without it, `--rotated --lines 20000 | grep <id>` for an id 90k lines back
171
+ * printed nothing and said nothing, which reads as "never logged". Lines are
172
+ * counted only once the window is full, and only from the file it stopped in
173
+ * back.
174
+ */
175
+ export declare function readLogWindow(files: readonly string[], maxLines: number): {
176
+ lines: string[];
177
+ cut: LogWindowCut | null;
178
+ };
179
+ /** The stderr sentence for a window that left older lines out. */
180
+ export declare function describeLogWindowCut(cut: LogWindowCut, shown: number): string;
181
+ /** Every byte of `files`, in order, to `out` — streamed, honouring back-pressure. */
182
+ export declare function streamLogFiles(files: readonly string[], out: NodeJS.WritableStream): Promise<void>;
119
183
  /** `x.y.z` from a version string, or null. A prerelease suffix is ignored. */
120
184
  export declare function parseRuntimeVersion(text: string): [number, number, number] | null;
121
185
  /** Whether `version` is at least `floor`; null when either is not a version. */
@@ -130,8 +194,11 @@ export declare function probeRuntimeVersion(capture?: typeof execRuntimeBinaryCa
130
194
  */
131
195
  export declare function probeStatusLogPath(flags: Pick<DaemonLogsFlags, 'profile'>, capture?: typeof execRuntimeBinaryCapture): Promise<string | null>;
132
196
  /** What `skrr daemon logs` does: forward to the runtime, or read a file itself. */
133
- export type DaemonLogsPlan = {
197
+ export type DaemonLogsPlan =
198
+ /** `runtimeVersion` decides whether a flag newer than 0.8.119 may be forwarded. */
199
+ {
134
200
  kind: 'forward';
201
+ runtimeVersion: string | null;
135
202
  }
136
203
  /** `note` goes to stderr before any log content; never printed for `--path`. */
137
204
  | {
@@ -171,13 +238,15 @@ export declare function readDaemonLogTail(filePath: string, maxLines: number): s
171
238
  * or it parses successfully and is then silently dropped.
172
239
  */
173
240
  export declare function buildDaemonLogsArgs(flags: DaemonLogsFlags): string[];
241
+ /** `--stderr` help, word for word the runtime's own (`daemon/src/index.ts`). */
242
+ export declare const STDERR_FLAG_DESCRIPTION = "Read the daemon's raw stderr instead of its log: the file its installed service redirects stderr to, or the profile's daemon.err.log. A daemon started with --log-file by anything else has no known stderr file, so its live log (which carries every WARN and ERROR) is shown instead, with a note";
174
243
  export default class DaemonLogs extends Command {
175
244
  static aliases: string[];
176
245
  static description: string;
177
246
  /**
178
- * Mirrors the four options `oversky logs` actually implements
247
+ * Mirrors the options `oversky logs` actually implements
179
248
  * (`daemon/src/index.ts` — `-n/--lines`, `-f/--follow`, `--stderr`,
180
- * `--path`). Typed flags are deliberate — they give `skrr daemon logs --help`
249
+ * `--path`, and since 0.8.120 `--rotated`). Typed flags are deliberate — they give `skrr daemon logs --help`
181
250
  * something real to print, which raw `strict = false` passthrough never did.
182
251
  * The trade is that anything NOT declared here becomes unreachable, so this
183
252
  * list has to stay in step with the daemon command.
@@ -187,6 +256,7 @@ export default class DaemonLogs extends Command {
187
256
  lines: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
188
257
  stderr: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
189
258
  path: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
259
+ rotated: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
190
260
  profile: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
191
261
  };
192
262
  static examples: string[];
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.LIVE_LOG_RESOLUTION_RUNTIME_VERSION = void 0;
6
+ exports.STDERR_FLAG_DESCRIPTION = exports.ROTATED_WINDOW_NOTE_RUNTIME_VERSION = exports.ROTATED_LOGS_RUNTIME_VERSION = exports.LIVE_LOG_RESOLUTION_RUNTIME_VERSION = void 0;
7
7
  exports.describeLogAge = describeLogAge;
8
8
  exports.resolveLegacyDaemonLogPath = resolveLegacyDaemonLogPath;
9
9
  exports.resolveDaemonLogsProfile = resolveDaemonLogsProfile;
@@ -11,6 +11,14 @@ exports.installedServiceLogDestinations = installedServiceLogDestinations;
11
11
  exports.installedServiceLogCandidates = installedServiceLogCandidates;
12
12
  exports.chooseLegacyFallbackLog = chooseLegacyFallbackLog;
13
13
  exports.resolveForwardedLogPath = resolveForwardedLogPath;
14
+ exports.rotatedLogSiblings = rotatedLogSiblings;
15
+ exports.describeRotatedSiblings = describeRotatedSiblings;
16
+ exports.readLastLines = readLastLines;
17
+ exports.readLastLinesAcross = readLastLinesAcross;
18
+ exports.countLines = countLines;
19
+ exports.readLogWindow = readLogWindow;
20
+ exports.describeLogWindowCut = describeLogWindowCut;
21
+ exports.streamLogFiles = streamLogFiles;
14
22
  exports.parseRuntimeVersion = parseRuntimeVersion;
15
23
  exports.runtimeVersionAtLeast = runtimeVersionAtLeast;
16
24
  exports.probeRuntimeVersion = probeRuntimeVersion;
@@ -22,6 +30,7 @@ const core_1 = require("@oclif/core");
22
30
  const node_fs_1 = require("node:fs");
23
31
  const node_os_1 = require("node:os");
24
32
  const node_path_1 = __importDefault(require("node:path"));
33
+ const node_events_1 = require("node:events");
25
34
  const exec_runtime_binary_1 = require("../../lib/exec-runtime-binary");
26
35
  const MAX_FALLBACK_LOG_BYTES = 1024 * 1024;
27
36
  const PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
@@ -188,7 +197,9 @@ function chooseLegacyFallbackLog(args) {
188
197
  */
189
198
  async function resolveForwardedLogPath(flags, capture = exec_runtime_binary_1.execRuntimeBinaryCapture) {
190
199
  try {
191
- const out = await capture(buildDaemonLogsArgs({ ...flags, path: true }), {
200
+ // Only what decides the file: --rotated would list siblings first, and the
201
+ // last line is still the current file, but there is no reason to ask.
202
+ const out = await capture(buildDaemonLogsArgs({ stderr: flags.stderr, profile: flags.profile, path: true }), {
192
203
  timeoutMs: 5_000,
193
204
  });
194
205
  if (out.code !== 0)
@@ -213,6 +224,197 @@ async function resolveForwardedLogPath(flags, capture = exec_runtime_binary_1.ex
213
224
  * forwarding to the bug, so the CLI forwards only at or above this version.
214
225
  */
215
226
  exports.LIVE_LOG_RESOLUTION_RUNTIME_VERSION = '0.8.119';
227
+ /**
228
+ * The first runtime whose `logs` knows `--rotated` (OSK-16214). An older one
229
+ * rejects it as an unknown option, so the CLI never forwards the flag below
230
+ * this version: it asks the runtime which file it reads (`--path`) and reads
231
+ * that file and its rotated siblings itself.
232
+ */
233
+ exports.ROTATED_LOGS_RUNTIME_VERSION = '0.8.120';
234
+ /**
235
+ * The first runtime whose `logs --rotated --lines N` says where a window that
236
+ * cut older lines starts, and whose `logs --path` names the rotated siblings
237
+ * on stderr (OSK-16228). 0.8.120 knows `--rotated` but says neither, so for it
238
+ * the CLI reads the files itself, exactly as it does for an older runtime.
239
+ */
240
+ exports.ROTATED_WINDOW_NOTE_RUNTIME_VERSION = '0.8.121';
241
+ /**
242
+ * Rotated siblings of a log, OLDEST FIRST (OSK-16214). The daemon's logger
243
+ * renames `daemon.app.log` to `daemon.app.1.log`, `.1` to `.2` and so on at
244
+ * 10 MB (`daemon/src/logger.ts` `maybeRotate`), and `logs` never reached them —
245
+ * `--lines 5000 | grep <run id>` found nothing while `daemon.app.1.log` held the
246
+ * run. A copy of `daemon/src/rotated-log-files.ts`, for runtimes that predate
247
+ * `--rotated`. Never throws.
248
+ */
249
+ function rotatedLogSiblings(filePath, readdir = (dir) => (0, node_fs_1.readdirSync)(dir)) {
250
+ const dir = node_path_1.default.dirname(filePath);
251
+ const ext = node_path_1.default.extname(filePath);
252
+ const base = node_path_1.default.basename(filePath, ext);
253
+ const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
254
+ const pattern = new RegExp(`^${escape(base)}\\.(\\d+)${escape(ext)}$`);
255
+ let names;
256
+ try {
257
+ names = readdir(dir);
258
+ }
259
+ catch {
260
+ return [];
261
+ }
262
+ return names
263
+ .map((name) => ({ name, index: Number(pattern.exec(name)?.[1] ?? NaN) }))
264
+ .filter(({ index }) => Number.isSafeInteger(index) && index >= 1)
265
+ .sort((a, b) => b.index - a.index)
266
+ .map(({ name }) => node_path_1.default.join(dir, name));
267
+ }
268
+ /** The sentence shown when rotated siblings hold older lines the view leaves out. */
269
+ function describeRotatedSiblings(filePath, siblings) {
270
+ if (siblings.length === 0)
271
+ return null;
272
+ const names = siblings.map((s) => node_path_1.default.basename(s));
273
+ const listed = names.length === 1 ? names[0] : `${names.at(-1)} … ${names[0]}`;
274
+ return (`Older lines are in ${siblings.length} rotated file${siblings.length === 1 ? '' : 's'} ` +
275
+ `beside ${filePath} (${listed}). Add --rotated to read them too, oldest first.`);
276
+ }
277
+ /**
278
+ * The last `maxLines` lines of one file, read backwards in chunks so the cost
279
+ * follows the lines asked for, not the file's size. Bytes are decoded once, so
280
+ * a multi-byte character split across a chunk boundary survives.
281
+ */
282
+ function readLastLines(filePath, maxLines) {
283
+ if (maxLines <= 0)
284
+ return [];
285
+ const { size } = (0, node_fs_1.statSync)(filePath);
286
+ const descriptor = (0, node_fs_1.openSync)(filePath, 'r');
287
+ try {
288
+ let start = size;
289
+ const chunks = [];
290
+ let newlines = 0;
291
+ while (start > 0 && newlines <= maxLines) {
292
+ const length = Math.min(64 * 1024, start);
293
+ start -= length;
294
+ const buffer = Buffer.alloc(length);
295
+ (0, node_fs_1.readSync)(descriptor, buffer, 0, length, start);
296
+ chunks.unshift(buffer);
297
+ for (const byte of buffer)
298
+ if (byte === 0x0a)
299
+ newlines += 1;
300
+ }
301
+ const lines = Buffer.concat(chunks).toString('utf8').split('\n');
302
+ if (lines.at(-1) === '')
303
+ lines.pop();
304
+ if (start > 0)
305
+ lines.shift();
306
+ return lines.slice(-maxLines);
307
+ }
308
+ finally {
309
+ (0, node_fs_1.closeSync)(descriptor);
310
+ }
311
+ }
312
+ /**
313
+ * The last `maxLines` lines across `files` (OLDEST FIRST) as if they were one
314
+ * file. A file renamed away between the listing and the read is skipped.
315
+ */
316
+ function readLastLinesAcross(files, maxLines) {
317
+ return readLogWindow(files, maxLines).lines;
318
+ }
319
+ /** Lines in a file, split as `readLastLines` splits them; read in chunks. */
320
+ function countLines(filePath) {
321
+ const descriptor = (0, node_fs_1.openSync)(filePath, 'r');
322
+ try {
323
+ const buffer = Buffer.alloc(64 * 1024);
324
+ let count = 0;
325
+ let last = -1;
326
+ let position = 0;
327
+ for (;;) {
328
+ const read = (0, node_fs_1.readSync)(descriptor, buffer, 0, buffer.length, position);
329
+ if (read === 0)
330
+ break;
331
+ position += read;
332
+ for (let i = 0; i < read; i += 1)
333
+ if (buffer[i] === 0x0a)
334
+ count += 1;
335
+ last = buffer[read - 1];
336
+ }
337
+ return last === -1 || last === 0x0a ? count : count + 1;
338
+ }
339
+ finally {
340
+ (0, node_fs_1.closeSync)(descriptor);
341
+ }
342
+ }
343
+ /**
344
+ * `readLastLinesAcross`, plus where the window began when it cut older lines.
345
+ * Without it, `--rotated --lines 20000 | grep <id>` for an id 90k lines back
346
+ * printed nothing and said nothing, which reads as "never logged". Lines are
347
+ * counted only once the window is full, and only from the file it stopped in
348
+ * back.
349
+ */
350
+ function readLogWindow(files, maxLines) {
351
+ const collected = [];
352
+ let remaining = maxLines;
353
+ let boundary = null;
354
+ for (let i = files.length - 1; i >= 0 && remaining > 0; i -= 1) {
355
+ try {
356
+ const lines = readLastLines(files[i], remaining);
357
+ collected.unshift(lines);
358
+ remaining -= lines.length;
359
+ if (remaining <= 0)
360
+ boundary = { index: i, taken: lines.length };
361
+ }
362
+ catch {
363
+ // Rotated away mid-read; the rest are still worth showing.
364
+ }
365
+ }
366
+ const lines = collected.flat();
367
+ if (boundary === null)
368
+ return { lines, cut: null };
369
+ let boundaryLines;
370
+ try {
371
+ boundaryLines = countLines(files[boundary.index]);
372
+ }
373
+ catch {
374
+ return { lines, cut: null };
375
+ }
376
+ let olderLines = 0;
377
+ for (const file of files.slice(0, boundary.index)) {
378
+ try {
379
+ olderLines += countLines(file);
380
+ }
381
+ catch {
382
+ // Rotated away since the read; it is not in the window either way.
383
+ }
384
+ }
385
+ const startLine = Math.max(1, boundaryLines - boundary.taken + 1);
386
+ if (startLine === 1 && olderLines === 0)
387
+ return { lines, cut: null };
388
+ return {
389
+ lines,
390
+ cut: {
391
+ file: files[boundary.index],
392
+ startLine,
393
+ totalLines: olderLines + (startLine - 1) + lines.length,
394
+ },
395
+ };
396
+ }
397
+ /** The stderr sentence for a window that left older lines out. */
398
+ function describeLogWindowCut(cut, shown) {
399
+ return (`Showing the last ${shown} of ${cut.totalLines} lines; the window starts at line ` +
400
+ `${cut.startLine} of ${node_path_1.default.basename(cut.file)}. Drop --lines to read the full history.`);
401
+ }
402
+ /** Every byte of `files`, in order, to `out` — streamed, honouring back-pressure. */
403
+ async function streamLogFiles(files, out) {
404
+ for (const file of files) {
405
+ try {
406
+ for await (const chunk of (0, node_fs_1.createReadStream)(file)) {
407
+ if (!out.write(chunk))
408
+ await (0, node_events_1.once)(out, 'drain');
409
+ }
410
+ }
411
+ catch (err) {
412
+ if (err?.code === 'ENOENT')
413
+ continue;
414
+ throw err;
415
+ }
416
+ }
417
+ }
216
418
  /** `x.y.z` from a version string, or null. A prerelease suffix is ignored. */
217
419
  function parseRuntimeVersion(text) {
218
420
  const m = /(\d+)\.(\d+)\.(\d+)/.exec(text);
@@ -300,13 +502,13 @@ async function planDaemonLogs(flags, deps) {
300
502
  }
301
503
  // Help could not be read: nothing to go on, keep the old behaviour.
302
504
  if (supports === null)
303
- return { kind: 'forward' };
505
+ return { kind: 'forward', runtimeVersion: null };
304
506
  const version = await deps.runtimeVersion();
305
507
  const current = version
306
508
  ? runtimeVersionAtLeast(version, exports.LIVE_LOG_RESOLUTION_RUNTIME_VERSION)
307
509
  : null;
308
510
  if (current === true)
309
- return { kind: 'forward' };
511
+ return { kind: 'forward', runtimeVersion: version };
310
512
  const live = await deps.statusLogPath();
311
513
  const predates = version !== null
312
514
  ? `The installed runtime (${version}) predates \`logs\` following the live daemon's ` +
@@ -315,12 +517,12 @@ async function planDaemonLogs(flags, deps) {
315
517
  `${deps.profilePath} instead of the live log.`;
316
518
  if (!live) {
317
519
  if (current === null)
318
- return { kind: 'forward' };
520
+ return { kind: 'forward', runtimeVersion: version };
319
521
  return legacy(`${predates} Its \`status\` did not name the live log either.`);
320
522
  }
321
523
  // The live daemon writes the profile log itself: the runtime's answer is right.
322
524
  if (sameLogFile(live, node_path_1.default.join(node_path_1.default.dirname(deps.profilePath), 'daemon.log'))) {
323
- return { kind: 'forward' };
525
+ return { kind: 'forward', runtimeVersion: version };
324
526
  }
325
527
  const missingHint = 'The live daemon has not written it yet; `skrr daemon status` shows whether one is running.';
326
528
  if (!flags.stderr) {
@@ -391,8 +593,13 @@ function buildDaemonLogsArgs(flags) {
391
593
  ...(flags.lines !== undefined ? ['--lines', String(flags.lines)] : []),
392
594
  ...(flags.stderr ? ['--stderr'] : []),
393
595
  ...(flags.path ? ['--path'] : []),
596
+ // Only ever reached for a runtime at ROTATED_LOGS_RUNTIME_VERSION or later;
597
+ // `run` reads the files itself for an older one.
598
+ ...(flags.rotated ? ['--rotated'] : []),
394
599
  ];
395
600
  }
601
+ /** `--stderr` help, word for word the runtime's own (`daemon/src/index.ts`). */
602
+ exports.STDERR_FLAG_DESCRIPTION = "Read the daemon's raw stderr instead of its log: the file its installed service redirects stderr to, or the profile's daemon.err.log. A daemon started with --log-file by anything else has no known stderr file, so its live log (which carries every WARN and ERROR) is shown instead, with a note";
396
603
  class DaemonLogs extends core_1.Command {
397
604
  // `skrr daemons <sub>` — the plural is advertised as an alias for the
398
605
  // whole topic, and without a per-command alias only the bare topic
@@ -400,9 +607,9 @@ class DaemonLogs extends core_1.Command {
400
607
  static aliases = ['daemons:logs'];
401
608
  static description = 'Tail or print recent skrr daemon logs';
402
609
  /**
403
- * Mirrors the four options `oversky logs` actually implements
610
+ * Mirrors the options `oversky logs` actually implements
404
611
  * (`daemon/src/index.ts` — `-n/--lines`, `-f/--follow`, `--stderr`,
405
- * `--path`). Typed flags are deliberate — they give `skrr daemon logs --help`
612
+ * `--path`, and since 0.8.120 `--rotated`). Typed flags are deliberate — they give `skrr daemon logs --help`
406
613
  * something real to print, which raw `strict = false` passthrough never did.
407
614
  * The trade is that anything NOT declared here becomes unreachable, so this
408
615
  * list has to stay in step with the daemon command.
@@ -414,17 +621,26 @@ class DaemonLogs extends core_1.Command {
414
621
  }),
415
622
  lines: core_1.Flags.integer({
416
623
  char: 'n',
417
- description: 'Number of recent log lines to print',
624
+ description: 'Number of recent log lines to print (default 200; every line with --rotated)',
418
625
  // `oversky logs` validates `>= 0`; keep the two in step so `-n 0` is not
419
626
  // rejected here only to be accepted there.
420
627
  min: 0,
421
628
  }),
629
+ // OSK-16213: this said "daemon.err.log — every WARN and ERROR". Since
630
+ // OSK-15993 it is the process's raw stderr as the service redirects it
631
+ // (`planDaemonLogs` here, `resolveDaemonLogTarget` in the runtime), and a
632
+ // structured --log-file is what carries WARN and ERROR. The runtime's own
633
+ // `--stderr` help says the same words; logs.spec.ts compares the two.
422
634
  stderr: core_1.Flags.boolean({
423
- description: 'Read daemon.err.log — every WARN and ERROR — instead of daemon.log',
635
+ description: exports.STDERR_FLAG_DESCRIPTION,
424
636
  }),
425
637
  path: core_1.Flags.boolean({
426
638
  description: 'Print the resolved log file path and exit',
427
639
  }),
640
+ // OSK-16214: the rotated files (`daemon.app.1.log` …) were unreachable.
641
+ rotated: core_1.Flags.boolean({
642
+ description: 'Include the rotated files (daemon.app.1.log …) before the current one, oldest first; with --path, list them all',
643
+ }),
428
644
  // Every other daemon verb reaches a non-default profile with --profile; this
429
645
  // one could only be steered by OVERSKY_PROFILE (OSK-10001).
430
646
  profile: core_1.Flags.string({
@@ -438,6 +654,7 @@ class DaemonLogs extends core_1.Command {
438
654
  '<%= config.bin %> daemon logs --lines 500',
439
655
  '<%= config.bin %> daemon logs --stderr',
440
656
  '<%= config.bin %> daemon logs --path',
657
+ '<%= config.bin %> daemon logs --rotated | grep arun_',
441
658
  '<%= config.bin %> daemon logs --profile dogfood-laptop --lines 50',
442
659
  ];
443
660
  async run() {
@@ -466,7 +683,7 @@ class DaemonLogs extends core_1.Command {
466
683
  // with it — 0.8.112–0.8.118 read the profile log even when the live
467
684
  // daemon writes elsewhere. `planDaemonLogs` forwards only to a runtime
468
685
  // that resolves the live log itself.
469
- const plan = await planDaemonLogs(flags, {
686
+ let plan = await planDaemonLogs(flags, {
470
687
  supportsLogs: () => (0, exec_runtime_binary_1.runtimeBinarySupportsSubcommand)('logs'),
471
688
  runtimeVersion: () => probeRuntimeVersion(),
472
689
  statusLogPath: () => probeStatusLogPath(flags),
@@ -481,22 +698,84 @@ class DaemonLogs extends core_1.Command {
481
698
  }
482
699
  },
483
700
  });
701
+ // OSK-16214: `--rotated` is new in ROTATED_LOGS_RUNTIME_VERSION, and an
702
+ // older runtime rejects it as an unknown option. Its `--path` is still
703
+ // the live-aware answer, so read that file and its siblings here.
704
+ const runtimeKnowsRotated = plan.kind === 'forward' &&
705
+ plan.runtimeVersion !== null &&
706
+ runtimeVersionAtLeast(plan.runtimeVersion, exports.ROTATED_LOGS_RUNTIME_VERSION) === true;
707
+ // OSK-16228: a `--rotated --lines N` window that cuts older lines, and
708
+ // `--path` with rotated siblings beside it, are said on stderr only from
709
+ // ROTATED_WINDOW_NOTE_RUNTIME_VERSION. Below it, read the files here.
710
+ const runtimeSaysWindow = plan.kind === 'forward' &&
711
+ plan.runtimeVersion !== null &&
712
+ runtimeVersionAtLeast(plan.runtimeVersion, exports.ROTATED_WINDOW_NOTE_RUNTIME_VERSION) === true;
713
+ if (plan.kind === 'forward' && !runtimeSaysWindow && (flags.rotated || flags.path)) {
714
+ const resolved = await resolveForwardedLogPath(flags);
715
+ if (resolved) {
716
+ plan = {
717
+ kind: 'read',
718
+ path: resolved,
719
+ note: null,
720
+ missingHint: 'The live daemon has not written it yet; `skrr daemon status` shows whether one is running.',
721
+ };
722
+ }
723
+ else if (flags.rotated && !runtimeKnowsRotated) {
724
+ this.error(`The installed runtime${plan.runtimeVersion ? ` (${plan.runtimeVersion})` : ''} predates ` +
725
+ `--rotated (${exports.ROTATED_LOGS_RUNTIME_VERSION}) and did not name its log file, so its ` +
726
+ 'rotated files cannot be found. Upgrade it (`skrr daemon update`), or list the ' +
727
+ 'directory `skrr daemon logs --path` prints.', { exit: 1 });
728
+ }
729
+ // Otherwise forward: the runtime still answers, just without the note.
730
+ }
484
731
  if (plan.kind === 'read') {
485
732
  const logPath = plan.path;
486
- // `--path` prints the one path and nothing else, on either stream.
733
+ const siblings = rotatedLogSiblings(logPath);
734
+ // `--path` prints the one path and nothing else on stdout — under
735
+ // --rotated, every file oldest first, the current one last. Without
736
+ // --rotated, stderr names the siblings a grep of that path would miss
737
+ // (OSK-16228).
487
738
  if (flags.path) {
739
+ for (const file of flags.rotated ? siblings : [])
740
+ this.log(file);
488
741
  this.log(logPath);
742
+ if (!flags.rotated) {
743
+ const hint = describeRotatedSiblings(logPath, siblings);
744
+ if (hint)
745
+ this.logToStderr(hint);
746
+ }
489
747
  return;
490
748
  }
491
749
  if (plan.note)
492
750
  this.logToStderr(plan.note);
751
+ if (flags.rotated && siblings.length === 0) {
752
+ this.logToStderr(`No rotated files beside ${logPath}.`);
753
+ }
754
+ else if (!flags.rotated) {
755
+ const hint = describeRotatedSiblings(logPath, siblings);
756
+ if (hint)
757
+ this.logToStderr(hint);
758
+ }
493
759
  showAge(logPath);
494
760
  if (!(0, node_fs_1.existsSync)(logPath)) {
495
761
  this.error(`No daemon log at ${logPath}. ${plan.missingHint}`, { exit: 1 });
496
762
  }
497
- const lines = readDaemonLogTail(logPath, flags.lines ?? 200);
498
- if (lines.length > 0)
499
- this.log(lines.join('\n'));
763
+ if (flags.rotated && flags.lines === undefined) {
764
+ // Every line of every file, streamed rather than held whole.
765
+ await streamLogFiles([...siblings, logPath], process.stdout);
766
+ }
767
+ else {
768
+ const window = flags.rotated
769
+ ? readLogWindow([...siblings, logPath], flags.lines ?? 200)
770
+ : null;
771
+ const lines = window ? window.lines : readDaemonLogTail(logPath, flags.lines ?? 200);
772
+ if (lines.length > 0)
773
+ this.log(lines.join('\n'));
774
+ // OSK-16228: after the content, so it is the last thing a terminal
775
+ // shows, and on stderr, so a grep over stdout is unchanged.
776
+ if (window?.cut)
777
+ this.logToStderr(describeLogWindowCut(window.cut, lines.length));
778
+ }
500
779
  if (flags.follow) {
501
780
  let position = (0, node_fs_1.statSync)(logPath).size;
502
781
  (0, node_fs_1.watchFile)(logPath, { interval: 500 }, () => {
@@ -528,8 +807,16 @@ class DaemonLogs extends core_1.Command {
528
807
  }
529
808
  // The banner must describe the file the runtime will actually print —
530
809
  // the live daemon's log (OSK-15993) — not the profile default.
531
- if (!flags.path)
532
- showAge(await resolveForwardedLogPath(flags));
810
+ if (!flags.path) {
811
+ const forwardedPath = await resolveForwardedLogPath(flags);
812
+ // A runtime that knows --rotated says this itself; an older one cannot.
813
+ if (!runtimeKnowsRotated && forwardedPath) {
814
+ const hint = describeRotatedSiblings(forwardedPath, rotatedLogSiblings(forwardedPath));
815
+ if (hint)
816
+ this.logToStderr(hint);
817
+ }
818
+ showAge(forwardedPath);
819
+ }
533
820
  const code = await (0, exec_runtime_binary_1.execRuntimeBinary)(args);
534
821
  this.exit(code);
535
822
  }
@@ -23,7 +23,10 @@ class FollowupsAct extends base_command_1.BaseCommand {
23
23
  required: true,
24
24
  }),
25
25
  at: core_1.Flags.string({ description: 'ISO-8601 instant to act at', exclusive: ['in'] }),
26
- in: core_1.Flags.string({ description: 'Duration from now (2h, 3d)', exclusive: ['at'] }),
26
+ in: core_1.Flags.string({
27
+ description: 'Duration from now (2h, 3d), between 1m and 30d',
28
+ exclusive: ['at'],
29
+ }),
27
30
  agent: core_1.Flags.string({ description: 'Agent ID (default: bound session agent)' }),
28
31
  endpoint: core_1.Flags.string({ description: 'Delivery endpoint id (EndpointRegistry)' }),
29
32
  'endpoint-target': core_1.Flags.string({ description: 'Channel target for channel endpoints' }),
@@ -42,6 +45,15 @@ class FollowupsAct extends base_command_1.BaseCommand {
42
45
  retryable: false,
43
46
  });
44
47
  }
48
+ const problem = (0, followups_1.whenFlagProblem)(flags.at ? 'at' : 'in', at);
49
+ if (problem) {
50
+ this.failWithCliError({
51
+ message: problem,
52
+ code: 'FOLLOWUP_INVALID_FIELD',
53
+ exit: 2,
54
+ retryable: false,
55
+ });
56
+ }
45
57
  const action = (0, triggers_1.parseJsonObject)(flags.action, '--action');
46
58
  if (typeof action?.builtinType !== 'string' || !action.builtinType) {
47
59
  this.failWithCliError({
@@ -23,7 +23,7 @@ class FollowupsCheckin extends base_command_1.BaseCommand {
23
23
  subject: core_1.Flags.string({ description: followups_1.FOLLOWUP_SUBJECT_FLAG_HELP, required: true }),
24
24
  why: core_1.Flags.string({ description: 'Why this wake exists, in one line', required: true }),
25
25
  in: core_1.Flags.string({
26
- description: 'Duration from now (90m, 2h) — the once form',
26
+ description: 'Duration from now (90m, 2h), between 1m and 30d — the once form',
27
27
  exclusive: ['frequency'],
28
28
  }),
29
29
  frequency: core_1.Flags.string({
@@ -64,6 +64,15 @@ class FollowupsCheckin extends base_command_1.BaseCommand {
64
64
  retryable: false,
65
65
  });
66
66
  }
67
+ const problem = (0, followups_1.whenFlagProblem)('in', flags.in);
68
+ if (problem) {
69
+ this.failWithCliError({
70
+ message: problem,
71
+ code: 'FOLLOWUP_INVALID_FIELD',
72
+ exit: 2,
73
+ retryable: false,
74
+ });
75
+ }
67
76
  let agent;
68
77
  try {
69
78
  agent = (0, followups_1.resolveFollowUpAgent)(flags.agent);
@@ -106,7 +115,7 @@ class FollowupsCheckin extends base_command_1.BaseCommand {
106
115
  if (response.dropped && producer === 'user_explicit') {
107
116
  this.failWithCliError({
108
117
  message: budgetRefusalMessage(agent.id, response.budget),
109
- code: 'FOLLOWUP_BUDGET_EXHAUSTED',
118
+ code: 'FOLLOWUP_BUDGET_EXCEEDED',
110
119
  exit: 1,
111
120
  retryable: false,
112
121
  details: { budget: response.budget ?? null, response },
@@ -15,7 +15,10 @@ class FollowupsReschedule extends base_command_1.BaseCommand {
15
15
  };
16
16
  static flags = {
17
17
  at: core_1.Flags.string({ description: 'New instant (ISO-8601)', exclusive: ['in'] }),
18
- in: core_1.Flags.string({ description: 'New offset from now (90m, 2h)', exclusive: ['at'] }),
18
+ in: core_1.Flags.string({
19
+ description: 'New offset from now (90m, 2h); between 1m and 30d',
20
+ exclusive: ['at'],
21
+ }),
19
22
  json: core_1.Flags.boolean({ description: 'Output as JSON' }),
20
23
  };
21
24
  async run() {
@@ -30,6 +33,15 @@ class FollowupsReschedule extends base_command_1.BaseCommand {
30
33
  retryable: false,
31
34
  });
32
35
  }
36
+ const problem = (0, followups_1.whenFlagProblem)(flags.at ? 'at' : 'in', at);
37
+ if (problem) {
38
+ this.failWithCliError({
39
+ message: problem,
40
+ code: 'FOLLOWUP_INVALID_FIELD',
41
+ exit: 2,
42
+ retryable: false,
43
+ });
44
+ }
33
45
  let result;
34
46
  try {
35
47
  result = (await followups_1.followupsApi.reschedule(args.id, { at }));
@@ -3,7 +3,7 @@ export default class ProposalsDiscard extends ProposalDecisionCommand {
3
3
  static description: string;
4
4
  static args: {
5
5
  kind: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
6
- id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
6
+ id: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
7
7
  };
8
8
  static decision: "discard";
9
9
  }