@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.
- package/README.md +1 -1
- package/dist/agent/session.js +11 -0
- package/dist/cli/command-catalog.js +5 -1
- package/dist/cli/commands/config.js +18 -3
- package/dist/cli/commands/logs.js +149 -0
- package/dist/cli/commands/pr.js +1 -10
- package/dist/cli/commands/run.js +6 -3
- package/dist/cli/commands/sessions.js +27 -2
- package/dist/cli/onboard.js +0 -9
- package/dist/cli/program.js +15 -1
- package/dist/cli/session-commands.js +45 -2
- package/dist/config/manager.js +91 -11
- package/dist/config/schema.js +53 -11
- package/dist/errors/constructors.js +35 -2
- package/dist/errors/types.js +6 -0
- package/dist/jobs/index.js +1 -0
- package/dist/jobs/log-renderer.js +10 -5
- package/dist/jobs/log-store.js +505 -0
- package/dist/jobs/manager.js +69 -1
- package/dist/routing/index.js +1 -1
- package/dist/routing/router.js +34 -14
- package/dist/routing/types.js +0 -2
- package/dist/session/prune.js +83 -23
- package/dist/subagent/orchestrator.js +16 -3
- package/dist/usage/types.js +25 -0
- package/package.json +1 -1
package/dist/config/schema.js
CHANGED
|
@@ -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
|
|
395
|
-
*
|
|
396
|
-
*
|
|
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
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
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
|
|
472
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|
package/dist/errors/types.js
CHANGED
|
@@ -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,
|
package/dist/jobs/index.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
|
98
|
-
*
|
|
99
|
-
*
|
|
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
|