@cruxy/cli 1.10.0 → 1.11.0

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.
@@ -391,13 +391,47 @@ export const JobsConfigSchema = z
391
391
  */
392
392
  maxJobs: z.number().int().positive().default(5),
393
393
  /**
394
- * How many of a job's most-recent log lines are retained in its ring buffer
395
- * for `cruxy logs <id>`. Bounded so a chatty job can't grow memory without
396
- * limit; older lines roll off oldest-first. Default 1000.
394
+ * How many of a job's most-recent log lines are retained in its IN-MEMORY
395
+ * ring buffer, which is what `/logs <id>` and the Tasks view read while the
396
+ * session is alive. Bounded so a chatty job can't grow memory without
397
+ * limit; older lines roll off oldest-first and `/logs` says how many.
398
+ * Default 1000.
399
+ *
400
+ * This is no longer the only copy: {@link logFileLines} bounds the
401
+ * persisted one, which `cruxy logs <id>` reads back after the fact.
397
402
  */
398
403
  logBufferLines: z.number().int().positive().default(1000),
404
+ /**
405
+ * How many lines of a job's output are written to its file under
406
+ * `~/.cruxy/projects/<project>/subagents/` (#172 item 1). Past this the log
407
+ * records one honest `truncated` marker and stops; the job runs on.
408
+ *
409
+ * WHY A SECOND, LARGER BOUND rather than reusing {@link logBufferLines}.
410
+ * The file exists precisely to remove the ring buffer's drop-on-overflow,
411
+ * so a file bounded at the buffer's size would persist the same truncated
412
+ * tail and buy nothing — which is why this is FLOORED at `logBufferLines`
413
+ * rather than allowed to sink below it (see the refinement below).
414
+ *
415
+ * WHY IT IS BOUNDED AT ALL. `sessions.retention` bounds how many session-
416
+ * shaped things the tree keeps; nothing bounds how big ONE of them gets,
417
+ * and a background job is the first writer here that can produce unbounded
418
+ * output unattended — it runs a full agent loop off screen, and a job stuck
419
+ * in a tool-call loop emits lines for as long as its budget lasts with
420
+ * nobody watching. Streaming that to disk uncapped is the second unbounded
421
+ * writer under `~/.cruxy` that #257 exists to prevent.
422
+ *
423
+ * 20000 is 20x the in-memory buffer: roughly 2 MB for a worst-case job, and
424
+ * the sweep drops a `done` job's log at the next start, so the steady-state
425
+ * cost of the default is near zero.
426
+ */
427
+ logFileLines: z.number().int().positive().default(20000),
399
428
  })
400
- .strict();
429
+ .strict()
430
+ .refine((j) => j.logFileLines >= j.logBufferLines, {
431
+ message: "jobs.logFileLines must be at least jobs.logBufferLines — the persisted log " +
432
+ "exists to remove the ring buffer's drop-on-overflow, and a file that kept " +
433
+ "fewer lines than memory would make persisting strictly worse than not persisting",
434
+ });
401
435
  /**
402
436
  * Sandbox / container execution (C.16): defense-in-depth beneath the U.3 gate.
403
437
  * When enabled, `run_command` and `run_tests` execute inside an isolated,
@@ -461,17 +495,25 @@ export const HooksConfigSchema = z
461
495
  /**
462
496
  * Multi-model routing (C.30): map declared task classes to tiers so mechanical
463
497
  * work runs on a cheap tier and hard reasoning on a strong one. Fully opt-in —
464
- * with an empty `map` and no `default`, every task class routes to the session's
465
- * single tier and behavior is unchanged (routing stays inert until configured).
466
- * Only tier names appear here; upstream model names never do (U.8). Keys are the
467
- * fixed {@link TASK_CLASSES}, so a mistyped class is rejected at config load.
498
+ * with an empty `map` and no `default` the whole table is inert and nothing
499
+ * about a session changes. Only tier names appear here; upstream model names
500
+ * never do (U.8). Keys are the fixed {@link TASK_CLASSES}, so a mistyped class
501
+ * is rejected at config load.
502
+ *
503
+ * A PARTIAL TABLE STAYS PARTIAL. Writing `map` without `default` routes exactly
504
+ * the classes named and leaves every other one to the gateway (`auto`) — it does
505
+ * not pin them to some tier chosen on your behalf. The exception is a
506
+ * `model.model` that names a tier: that is a session-wide model choice, and the
507
+ * unnamed classes inherit it rather than being handed back to the gateway.
468
508
  */
469
509
  export const RoutingConfigSchema = z
470
510
  .object({
471
- /** Tier for any task class not in `map`. Unset → the tier implied by
472
- * `model.model` (a real tier, or the auto-fallback tier). */
511
+ /** Tier for any task class not in `map`. Unset → the tier `model.model`
512
+ * names, if it names one; otherwise the class is not routed here at all and
513
+ * goes out as `auto` for the gateway to route. */
473
514
  default: z.enum(MODEL_TIERS).optional(),
474
- /** Per-task-class tier overrides; anything omitted takes `default`. */
515
+ /** Per-task-class tier overrides; anything omitted takes `default`, or
516
+ * `auto` when there is no `default` to take. */
475
517
  map: z.record(z.enum(TASK_CLASSES), z.enum(MODEL_TIERS)).default({}),
476
518
  })
477
519
  .strict();
@@ -109,14 +109,47 @@ export function configParse(path, underlying) {
109
109
  meta: { path },
110
110
  });
111
111
  }
112
+ /**
113
+ * `--config <path>` named a file that does not exist.
114
+ *
115
+ * Loud by construction: the alternative is falling back to discovery or to
116
+ * defaults, which produces exactly the behaviour the user would have got
117
+ * without the flag, and so is indistinguishable from success (#289). Raised
118
+ * only for an EXPLICIT path — a missing global or project config is ordinary.
119
+ */
120
+ export function configNotFound(path) {
121
+ return new CruxyError({
122
+ code: ErrorCode.ConfigNotFound,
123
+ title: `config file not found: ${path}`,
124
+ cause: "`--config` named a file that does not exist",
125
+ nextSteps: [
126
+ "check the path (it is resolved as given, relative to the current directory)",
127
+ "omit `--config` to use the discovered project config, or `~/.cruxy/config.json`",
128
+ ],
129
+ meta: { path },
130
+ });
131
+ }
112
132
  export function configInvalid(issues, path) {
113
133
  return new CruxyError({
114
134
  code: ErrorCode.ConfigInvalid,
115
- title: "the configuration is invalid",
135
+ // NAME THE FILE, as `configParse` above already does. The path was carried
136
+ // in `meta` alone, and the terminal formatter renders title/cause/steps/code
137
+ // — not meta — so the caller's choice of which file to blame reached nobody
138
+ // and "correct the reported field(s)" left the user to guess between the
139
+ // global, project and explicit configs (#289).
140
+ //
141
+ // It is the HIGHEST-PRECEDENCE file in effect, not necessarily the one
142
+ // holding the bad key: the config is a merge, and a resolved value cannot
143
+ // be traced to a layer from here. Hence "start with" and the pointer to
144
+ // `config path` — an honest lead, not a claim about which line to edit.
145
+ title: path
146
+ ? `the configuration is invalid (${path})`
147
+ : "the configuration is invalid",
116
148
  cause: issues,
117
149
  nextSteps: [
118
150
  "correct the reported field(s)",
119
- "see valid keys with `cruxy config list`",
151
+ ...(path ? [`start with ${path}, the last file merged`] : []),
152
+ "see valid keys with `cruxy config list`, or the files in effect with `cruxy config path`",
120
153
  ],
121
154
  meta: path ? { path } : undefined,
122
155
  });
@@ -29,6 +29,11 @@ export const ErrorCode = {
29
29
  // config (exit 3)
30
30
  ConfigParse: "CRUXY_E_CONFIG_PARSE",
31
31
  ConfigInvalid: "CRUXY_E_CONFIG_INVALID",
32
+ /** A config file named with `--config` does not exist. DISTINCT from
33
+ * {@link ConfigParse}: nothing was malformed, the file simply is not there,
34
+ * and the advice is to check the path rather than the JSON. Only an EXPLICIT
35
+ * path can raise it — a missing global or project file is the normal case. */
36
+ ConfigNotFound: "CRUXY_E_CONFIG_NOT_FOUND",
32
37
  // auth (exit 4)
33
38
  AuthMissingKey: "CRUXY_E_AUTH_MISSING_KEY",
34
39
  AuthInvalid: "CRUXY_E_AUTH_INVALID",
@@ -311,6 +316,7 @@ const EXIT_CODES = {
311
316
  [ErrorCode.RoutingTierUnavailable]: 2,
312
317
  [ErrorCode.ConfigParse]: 3,
313
318
  [ErrorCode.ConfigInvalid]: 3,
319
+ [ErrorCode.ConfigNotFound]: 3,
314
320
  [ErrorCode.AuthMissingKey]: 4,
315
321
  [ErrorCode.AuthInvalid]: 4,
316
322
  [ErrorCode.AuthExpired]: 4,
@@ -1,5 +1,6 @@
1
1
  export * from "./types.js";
2
2
  export * from "./log-buffer.js";
3
+ export * from "./log-store.js";
3
4
  export * from "./log-renderer.js";
4
5
  export * from "./approval-queue.js";
5
6
  export * from "./manager.js";
@@ -18,9 +18,14 @@ const OFFSCREEN_CAPS = {
18
18
  };
19
19
  /**
20
20
  * A {@link StreamRenderer} for a background job (C.28) that captures activity into
21
- * the job's log buffer and writes NOTHING to any terminal — a job runs
21
+ * the job's log sink and writes NOTHING to any terminal — a job runs
22
22
  * non-interactively, off screen, and its foreground session owns the terminal.
23
- * `cruxy logs <id>` (a REPL `/logs`) reads back what was captured here.
23
+ *
24
+ * What was captured here is read back from two different copies: the in-session
25
+ * `/logs <id>` and the Tasks view read the ring buffer while the session lives,
26
+ * and `cruxy logs <id>` reads the persisted file afterwards (#172 item 1).
27
+ * Everything this renderer emits goes through the one `JobManager.log` sink, so
28
+ * both copies see exactly the same lines.
24
29
  *
25
30
  * Assistant text is accumulated and flushed a line at a time on `endSegment`;
26
31
  * committed chrome notes and tool-call completions are captured verbatim. The
@@ -94,9 +99,9 @@ export class JobLogRenderer {
94
99
  this.planSteps = steps.map((s) => ({ ...s }));
95
100
  }
96
101
  /**
97
- * A job's test run is exactly the kind of outcome `cruxy logs` exists to
98
- * show. Plain text, no theme glyphs, matching this log's `[ok]`/`[fail]`
99
- * style — and no count this renderer was not given.
102
+ * A job's test run is exactly the kind of outcome a job log exists to show.
103
+ * Plain text, no theme glyphs, matching this log's `[ok]`/`[fail]` style —
104
+ * and no count this renderer was not given.
100
105
  */
101
106
  testResult(report) {
102
107
  const counted = report.total !== undefined