@aragon-agent/cli 0.6.39 → 0.6.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/CHANGELOG.md +2589 -2523
- package/README.md +3706 -3614
- package/dist/agent/controller.d.ts +103 -9
- package/dist/agent/controller.js +196 -19
- package/dist/agent/model-profile-settings.js +12 -1
- package/dist/agent/plan-prompt.d.ts +1 -3
- package/dist/agent/plan-prompt.js +13 -7
- package/dist/agent/reducer.d.ts +29 -1
- package/dist/agent/reducer.js +34 -6
- package/dist/agent/skill-triage-prompt.d.ts +42 -0
- package/dist/agent/skill-triage-prompt.js +74 -0
- package/dist/agent/system-prompt.d.ts +14 -2
- package/dist/agent/system-prompt.js +7 -4
- package/dist/cli.js +21 -5
- package/dist/commands/builtins.js +12 -3
- package/dist/commands/skills.js +86 -1
- package/dist/commands/subagent-command.d.ts +25 -0
- package/dist/commands/subagent-command.js +242 -0
- package/dist/compaction/command.js +16 -3
- package/dist/compaction/compactor.d.ts +25 -0
- package/dist/compaction/compactor.js +111 -10
- package/dist/compaction/digest.d.ts +24 -1
- package/dist/compaction/digest.js +139 -17
- package/dist/compaction/limits.d.ts +21 -0
- package/dist/compaction/limits.js +21 -0
- package/dist/compaction/reason-text.d.ts +36 -0
- package/dist/compaction/reason-text.js +78 -0
- package/dist/compaction/types.d.ts +82 -14
- package/dist/compaction/wiring.d.ts +13 -8
- package/dist/compaction/wiring.js +1 -0
- package/dist/config/load.js +50 -3
- package/dist/config/schema.d.ts +82 -6
- package/dist/config/schema.js +107 -9
- package/dist/config/store.js +7 -2
- package/dist/proc/log-mirror.d.ts +21 -4
- package/dist/proc/log-mirror.js +29 -7
- package/dist/skills/service.d.ts +41 -0
- package/dist/skills/service.js +62 -0
- package/dist/skills/triage.d.ts +194 -0
- package/dist/skills/triage.js +393 -0
- package/dist/team/forced-dispatch.d.ts +95 -0
- package/dist/team/forced-dispatch.js +208 -0
- package/dist/team/limits.d.ts +21 -1
- package/dist/team/limits.js +21 -1
- package/dist/team/normalize.d.ts +15 -0
- package/dist/team/normalize.js +21 -4
- package/dist/team/prompt.d.ts +6 -3
- package/dist/team/prompt.js +9 -5
- package/dist/team/task-tool.js +11 -7
- package/dist/tools/human-input.d.ts +35 -2
- package/dist/tools/human-input.js +38 -0
- package/dist/tools/plan-tools.d.ts +20 -12
- package/dist/tools/plan-tools.js +24 -17
- package/dist/ui/App.js +14 -1
- package/dist/ui/StatusBar.d.ts +1 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/entries/CompactionCard.d.ts +11 -1
- package/dist/ui/entries/CompactionCard.js +65 -4
- package/dist/ui/layout/status-layout.d.ts +10 -0
- package/dist/ui/layout/status-layout.js +18 -1
- package/dist/ui/overlays/SettingsScreen.js +29 -5
- package/dist/ui/overlays/TeamFanoutPage.d.ts +35 -0
- package/dist/ui/overlays/TeamFanoutPage.js +78 -0
- package/dist/ui/overlays/model-profile-state.d.ts +19 -1
- package/dist/ui/overlays/model-profile-state.js +12 -0
- package/dist/ui/overlays/team-fanout-state.d.ts +73 -0
- package/dist/ui/overlays/team-fanout-state.js +97 -0
- package/package.json +2 -2
package/dist/config/schema.d.ts
CHANGED
|
@@ -206,15 +206,15 @@ export declare const DEFAULT_SCROLL_RESUME_MS = 5000;
|
|
|
206
206
|
* Exactly the trap `retry.maxRetries` needed this helper for.
|
|
207
207
|
*/
|
|
208
208
|
export declare function clampScrollResumeMs(v: unknown, fallback: number): number;
|
|
209
|
-
/** `ask_user` calls allowed per user turn. */
|
|
210
|
-
export declare const DEFAULT_PLAN_MAX_ASK_ROUNDS = 4;
|
|
211
209
|
/**
|
|
212
210
|
* Ceiling on ONE human wait, in ms. Thirty minutes is far outside "the user
|
|
213
211
|
* went to get coffee", but a ceiling has to exist: an unattended terminal must
|
|
214
212
|
* not wedge a CI job or a stale ssh session forever.
|
|
213
|
+
*
|
|
214
|
+
* THIS IS NOT A ROUND LIMIT and the distinction matters. It bounds a single wait
|
|
215
|
+
* (liveness); it says nothing about how many times the model may ask.
|
|
215
216
|
*/
|
|
216
217
|
export declare const DEFAULT_PLAN_HUMAN_TIMEOUT_MS = 1800000;
|
|
217
|
-
export declare function clampAskRounds(v: unknown, fallback: number): number;
|
|
218
218
|
export declare function clampHumanTimeout(v: unknown, fallback: number): number;
|
|
219
219
|
export interface SkillsConfig {
|
|
220
220
|
enabled: boolean;
|
|
@@ -259,6 +259,18 @@ export interface SkillsConfig {
|
|
|
259
259
|
* just be a second decorative field.
|
|
260
260
|
*/
|
|
261
261
|
toolPolicy: SkillsToolPolicyMode;
|
|
262
|
+
/**
|
|
263
|
+
* Automatic skill triage (skill-triage-and-unbounded-ask §4.2.9): compute a
|
|
264
|
+
* short candidate list for each new task and put it in the system prompt, so
|
|
265
|
+
* "is a skill applicable?" becomes a question the protocol forces the model to
|
|
266
|
+
* answer rather than one it has to remember to ask.
|
|
267
|
+
*
|
|
268
|
+
* DEFAULT ON, and OFF must be byte-identical to the pre-feature prompt. That is
|
|
269
|
+
* what the block's conditional splice in `buildSystemPrompt` guarantees
|
|
270
|
+
* (invariant I-T1) — a permanently-on block would be a prompt change nobody
|
|
271
|
+
* could turn off.
|
|
272
|
+
*/
|
|
273
|
+
triage: boolean;
|
|
262
274
|
}
|
|
263
275
|
export declare const SKILLS_INTEGRITY_MODES: readonly ["off", "warn", "strict"];
|
|
264
276
|
export type SkillsIntegrityMode = (typeof SKILLS_INTEGRITY_MODES)[number];
|
|
@@ -343,6 +355,16 @@ export interface TeamConfig {
|
|
|
343
355
|
* failed subagent.
|
|
344
356
|
*/
|
|
345
357
|
maxConcurrent: number;
|
|
358
|
+
/**
|
|
359
|
+
* The one-shot marker `migrateLegacyTeamFanout` writes (subagent-fanout
|
|
360
|
+
* D-3), and NOTHING else may ever write it.
|
|
361
|
+
*
|
|
362
|
+
* It exists because the fan-out default moved 5/3 -> 8/8 while every save
|
|
363
|
+
* materializes the whole section: without a marker, a user who deliberately
|
|
364
|
+
* returns to 5/3 is migrated back to 8/8 on the next launch, for ever. Absent
|
|
365
|
+
* means "not yet migrated"; it is never written as `false`.
|
|
366
|
+
*/
|
|
367
|
+
fanoutMigrated?: boolean;
|
|
346
368
|
/**
|
|
347
369
|
* One child's wall clock before its own abort. `0` (the default) means NO
|
|
348
370
|
* ceiling: a subagent runs until it finishes or is aborted, exactly like
|
|
@@ -465,6 +487,40 @@ export declare const LEGACY_TEAM_CEILING_DEFAULTS: Readonly<{
|
|
|
465
487
|
* equality test lives beside the constants it reads.
|
|
466
488
|
*/
|
|
467
489
|
export declare function legacyTeamCeilingPatch(raw: unknown): Partial<TeamConfig> | null;
|
|
490
|
+
/**
|
|
491
|
+
* The fan-out defaults cli <= 0.6.x persisted as its OWN defaults
|
|
492
|
+
* (subagent-fanout F-2 / R-1).
|
|
493
|
+
*
|
|
494
|
+
* THE SAME TRAP AS THE CEILINGS ABOVE, ONE SECTION OVER. `mergePersistedConfig`
|
|
495
|
+
* writes the WHOLE `team` section on every save, so a user who has ever touched
|
|
496
|
+
* any setting carries this exact pair in `config.json` - changing
|
|
497
|
+
* `DEFAULT_TEAM_CONFIG` alone would never reach them, and they would keep
|
|
498
|
+
* seeing "only 3 subagents at once" for ever.
|
|
499
|
+
*/
|
|
500
|
+
export declare const LEGACY_TEAM_FANOUT_DEFAULTS: {
|
|
501
|
+
readonly maxSubagents: 5;
|
|
502
|
+
readonly maxConcurrent: 3;
|
|
503
|
+
};
|
|
504
|
+
/**
|
|
505
|
+
* The `{ maxSubagents, maxConcurrent, fanoutMigrated }` patch for the legacy
|
|
506
|
+
* default pair, or `null` when nothing matches (subagent-fanout D-3).
|
|
507
|
+
*
|
|
508
|
+
* EXACT-EQUALITY ONLY, the rule `legacyTeamCeilingPatch` records: a value that
|
|
509
|
+
* differs was a deliberate user choice and survives untouched. Two differences
|
|
510
|
+
* from that function, both deliberate:
|
|
511
|
+
*
|
|
512
|
+
* - `fanoutMigrated: true` short-circuits to `null`. That is what makes "the
|
|
513
|
+
* user changed their mind and went back to 5/3" stick: without it the next
|
|
514
|
+
* launch would migrate them again, every launch, for ever.
|
|
515
|
+
* - `maxConcurrent` is allowed to be ABSENT as well as 3, because a
|
|
516
|
+
* hand-written section may name only one key and a half-written pair is
|
|
517
|
+
* still the legacy intent, not a user decision.
|
|
518
|
+
*
|
|
519
|
+
* Pure on purpose: `loadConfig` decides WHERE the patch is applied and
|
|
520
|
+
* persisted; this function only answers WHICH keys match, so the equality test
|
|
521
|
+
* lives beside the constants it reads.
|
|
522
|
+
*/
|
|
523
|
+
export declare function legacyTeamFanoutPatch(raw: unknown): Partial<TeamConfig> | null;
|
|
468
524
|
export interface TodoConfig {
|
|
469
525
|
/** Register `todo_write` at all. Default TRUE. */
|
|
470
526
|
enabled: boolean;
|
|
@@ -1064,8 +1120,6 @@ export interface PersistedConfig {
|
|
|
1064
1120
|
scrollResumeMs: number;
|
|
1065
1121
|
/** Start every session in PLAN mode (plan-mode §4.4). */
|
|
1066
1122
|
planModeDefault: boolean;
|
|
1067
|
-
/** `ask_user` rounds allowed per user turn; clamped to [1, 10]. */
|
|
1068
|
-
planModeMaxAskRounds: number;
|
|
1069
1123
|
/** Ceiling on a single human wait in ms; clamped to [60_000, 7_200_000]. */
|
|
1070
1124
|
planModeHumanTimeoutMs: number;
|
|
1071
1125
|
/** Skill system settings. The SECOND nested object in this file after apiKeys. */
|
|
@@ -1118,6 +1172,29 @@ export declare const LEGACY_STATE_KEYS: readonly ["promptHistory", "submitCount"
|
|
|
1118
1172
|
* one door for both directions.
|
|
1119
1173
|
*/
|
|
1120
1174
|
export declare function stripLegacyStateKeys<T extends object>(raw: T): T;
|
|
1175
|
+
/**
|
|
1176
|
+
* Config keys whose MEANING was removed, mapped to the explanation a user gets if
|
|
1177
|
+
* they still try to set one.
|
|
1178
|
+
*
|
|
1179
|
+
* SEPARATE FROM `LEGACY_STATE_KEYS`, because the disposition differs: those keys'
|
|
1180
|
+
* values were MOVED somewhere else, while these have nowhere to go — the feature
|
|
1181
|
+
* they configured no longer exists. Merging the two lists would erase that
|
|
1182
|
+
* difference at exactly the moment someone is reading the file to find out what
|
|
1183
|
+
* happened to their setting.
|
|
1184
|
+
*
|
|
1185
|
+
* Stripped on the READ side for the same reason `stripLegacyStateKeys` is:
|
|
1186
|
+
* `loadPersistedConfig()` merges `{ ...DEFAULT_CONFIG, ...partial }`, so a key
|
|
1187
|
+
* left on disk rides that spread into memory and `updatePersistedConfig` writes it
|
|
1188
|
+
* straight back. The user deletes the line, launches once, and watches it return.
|
|
1189
|
+
*/
|
|
1190
|
+
export declare const RETIRED_CONFIG_KEYS: Readonly<Record<string, string>>;
|
|
1191
|
+
/**
|
|
1192
|
+
* THE single gate that keeps retired keys out of `config.json`. Applied on the
|
|
1193
|
+
* read side, so it is self-healing: every write is read-strip-merge-write, and a
|
|
1194
|
+
* hand-edited file, a downgrade to an older CLI and back, or a half-finished
|
|
1195
|
+
* migration all converge on a clean file at the next write.
|
|
1196
|
+
*/
|
|
1197
|
+
export declare function stripRetiredConfigKeys<T extends object>(raw: T): T;
|
|
1121
1198
|
export interface CliConfig {
|
|
1122
1199
|
/** A committed model snapshot failed to apply; no further requests may start. */
|
|
1123
1200
|
modelSettingsRestartRequired?: boolean;
|
|
@@ -1312,7 +1389,6 @@ export interface CliConfig {
|
|
|
1312
1389
|
* live mode from here on belongs to the controller.
|
|
1313
1390
|
*/
|
|
1314
1391
|
startInPlanMode: boolean;
|
|
1315
|
-
planModeMaxAskRounds: number;
|
|
1316
1392
|
planModeHumanTimeoutMs: number;
|
|
1317
1393
|
skills: SkillsConfig;
|
|
1318
1394
|
/** Flag/env-only skill switches for this run (§7.5). Never persisted. */
|
package/dist/config/schema.js
CHANGED
|
@@ -273,20 +273,23 @@ export function clampScrollResumeMs(v, fallback) {
|
|
|
273
273
|
// nested-object merge is hand-written and capped at one level; three scalars do
|
|
274
274
|
// not justify a second `clampXConfig` gate plus two more merge sites, and flat
|
|
275
275
|
// keys mean `store.ts` needs ZERO changes.
|
|
276
|
+
//
|
|
277
|
+
// ONE OF THOSE KEYS IS GONE: `planModeMaxAskRounds` capped `ask_user` at four
|
|
278
|
+
// rounds per turn. There is no cap any more, so there is no number to store, no
|
|
279
|
+
// range to clamp and nothing to configure. It is retired through
|
|
280
|
+
// `RETIRED_CONFIG_KEYS` below rather than defaulted to a large number — a
|
|
281
|
+
// ninety-nine in the config file is the same second source of truth as a four.
|
|
276
282
|
// ---------------------------------------------------------------------------
|
|
277
|
-
/** `ask_user` calls allowed per user turn. */
|
|
278
|
-
export const DEFAULT_PLAN_MAX_ASK_ROUNDS = 4;
|
|
279
|
-
const ASK_ROUNDS_RANGE = { min: 1, max: 10 };
|
|
280
283
|
/**
|
|
281
284
|
* Ceiling on ONE human wait, in ms. Thirty minutes is far outside "the user
|
|
282
285
|
* went to get coffee", but a ceiling has to exist: an unattended terminal must
|
|
283
286
|
* not wedge a CI job or a stale ssh session forever.
|
|
287
|
+
*
|
|
288
|
+
* THIS IS NOT A ROUND LIMIT and the distinction matters. It bounds a single wait
|
|
289
|
+
* (liveness); it says nothing about how many times the model may ask.
|
|
284
290
|
*/
|
|
285
291
|
export const DEFAULT_PLAN_HUMAN_TIMEOUT_MS = 1_800_000;
|
|
286
292
|
const HUMAN_TIMEOUT_RANGE = { min: 60_000, max: 7_200_000 };
|
|
287
|
-
export function clampAskRounds(v, fallback) {
|
|
288
|
-
return clampInt(v, fallback, ASK_ROUNDS_RANGE);
|
|
289
|
-
}
|
|
290
293
|
export function clampHumanTimeout(v, fallback) {
|
|
291
294
|
return clampInt(v, fallback, HUMAN_TIMEOUT_RANGE);
|
|
292
295
|
}
|
|
@@ -322,6 +325,9 @@ export const DEFAULT_SKILLS_CONFIG = {
|
|
|
322
325
|
integrity: 'warn',
|
|
323
326
|
usageTracking: true,
|
|
324
327
|
toolPolicy: 'enforce',
|
|
328
|
+
// ON by default: the capability this key controls is the feature's whole point,
|
|
329
|
+
// and it costs zero model calls (local matching over in-memory records).
|
|
330
|
+
triage: true,
|
|
325
331
|
};
|
|
326
332
|
const CATALOG_BYTES_RANGE = { min: 500, max: 40_000 };
|
|
327
333
|
/**
|
|
@@ -406,6 +412,7 @@ export function clampSkillsConfig(raw) {
|
|
|
406
412
|
integrity: clampSkillsIntegrity(src.integrity, DEFAULT_SKILLS_CONFIG.integrity),
|
|
407
413
|
usageTracking: bool(src.usageTracking, DEFAULT_SKILLS_CONFIG.usageTracking),
|
|
408
414
|
toolPolicy: clampSkillsToolPolicy(src.toolPolicy, DEFAULT_SKILLS_CONFIG.toolPolicy),
|
|
415
|
+
triage: bool(src.triage, DEFAULT_SKILLS_CONFIG.triage),
|
|
409
416
|
};
|
|
410
417
|
}
|
|
411
418
|
export const DEFAULT_SKILLS_RUNTIME = { forcedSkills: [], approveAll: false };
|
|
@@ -474,8 +481,12 @@ export function clampLogConfig(raw) {
|
|
|
474
481
|
export const HARD_MAX_SUBAGENTS = TEAM_LIMITS.hardMaxSubagents;
|
|
475
482
|
export const DEFAULT_TEAM_CONFIG = {
|
|
476
483
|
enabled: true,
|
|
477
|
-
|
|
478
|
-
maxConcurrent
|
|
484
|
+
// 8 IS THE DEFAULT, 10 IS THE CEILING, and they are not the same number
|
|
485
|
+
// (subagent-fanout D-1 / D-2). `maxConcurrent` moves WITH `maxSubagents`
|
|
486
|
+
// because a wider population behind a 3-wide pool shows the user nothing:
|
|
487
|
+
// children 4..8 simply queue (`team-bus.test.ts` records the queued phase).
|
|
488
|
+
maxSubagents: 8,
|
|
489
|
+
maxConcurrent: 8,
|
|
479
490
|
// 0 = NO LIMIT. Subagents run under the same regime as the lead: no
|
|
480
491
|
// wall clock, no turn cap; only the idle watchdog and the user's Esc.
|
|
481
492
|
subagentTimeoutMs: 0,
|
|
@@ -534,6 +545,13 @@ export function clampTeamConfig(raw) {
|
|
|
534
545
|
&& [...src.modelProfileId].length <= 128
|
|
535
546
|
? src.modelProfileId
|
|
536
547
|
: null,
|
|
548
|
+
// TRANSPARENT, and only ever `true` (subagent-fanout D-3 / R-6). The flag is
|
|
549
|
+
// not a setting: it is the one-shot marker the migration leaves behind, so a
|
|
550
|
+
// clamp that dropped it would re-migrate the user on every launch. Written
|
|
551
|
+
// as a spread rather than `=== true ? true : undefined` so the key stays
|
|
552
|
+
// ABSENT (not `undefined`) in the clamped result - `toEqual(DEFAULT_TEAM_CONFIG)`
|
|
553
|
+
// and the on-disk shape both depend on that.
|
|
554
|
+
...(src.fanoutMigrated === true ? { fanoutMigrated: true } : {}),
|
|
537
555
|
};
|
|
538
556
|
}
|
|
539
557
|
/**
|
|
@@ -574,6 +592,56 @@ export function legacyTeamCeilingPatch(raw) {
|
|
|
574
592
|
}
|
|
575
593
|
return matched ? patch : null;
|
|
576
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* The fan-out defaults cli <= 0.6.x persisted as its OWN defaults
|
|
597
|
+
* (subagent-fanout F-2 / R-1).
|
|
598
|
+
*
|
|
599
|
+
* THE SAME TRAP AS THE CEILINGS ABOVE, ONE SECTION OVER. `mergePersistedConfig`
|
|
600
|
+
* writes the WHOLE `team` section on every save, so a user who has ever touched
|
|
601
|
+
* any setting carries this exact pair in `config.json` - changing
|
|
602
|
+
* `DEFAULT_TEAM_CONFIG` alone would never reach them, and they would keep
|
|
603
|
+
* seeing "only 3 subagents at once" for ever.
|
|
604
|
+
*/
|
|
605
|
+
export const LEGACY_TEAM_FANOUT_DEFAULTS = {
|
|
606
|
+
maxSubagents: 5,
|
|
607
|
+
maxConcurrent: 3,
|
|
608
|
+
};
|
|
609
|
+
/**
|
|
610
|
+
* The `{ maxSubagents, maxConcurrent, fanoutMigrated }` patch for the legacy
|
|
611
|
+
* default pair, or `null` when nothing matches (subagent-fanout D-3).
|
|
612
|
+
*
|
|
613
|
+
* EXACT-EQUALITY ONLY, the rule `legacyTeamCeilingPatch` records: a value that
|
|
614
|
+
* differs was a deliberate user choice and survives untouched. Two differences
|
|
615
|
+
* from that function, both deliberate:
|
|
616
|
+
*
|
|
617
|
+
* - `fanoutMigrated: true` short-circuits to `null`. That is what makes "the
|
|
618
|
+
* user changed their mind and went back to 5/3" stick: without it the next
|
|
619
|
+
* launch would migrate them again, every launch, for ever.
|
|
620
|
+
* - `maxConcurrent` is allowed to be ABSENT as well as 3, because a
|
|
621
|
+
* hand-written section may name only one key and a half-written pair is
|
|
622
|
+
* still the legacy intent, not a user decision.
|
|
623
|
+
*
|
|
624
|
+
* Pure on purpose: `loadConfig` decides WHERE the patch is applied and
|
|
625
|
+
* persisted; this function only answers WHICH keys match, so the equality test
|
|
626
|
+
* lives beside the constants it reads.
|
|
627
|
+
*/
|
|
628
|
+
export function legacyTeamFanoutPatch(raw) {
|
|
629
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
630
|
+
return null;
|
|
631
|
+
const src = raw;
|
|
632
|
+
if (src.fanoutMigrated === true)
|
|
633
|
+
return null;
|
|
634
|
+
if (src.maxSubagents !== LEGACY_TEAM_FANOUT_DEFAULTS.maxSubagents)
|
|
635
|
+
return null;
|
|
636
|
+
if (src.maxConcurrent !== undefined
|
|
637
|
+
&& src.maxConcurrent !== LEGACY_TEAM_FANOUT_DEFAULTS.maxConcurrent)
|
|
638
|
+
return null;
|
|
639
|
+
return {
|
|
640
|
+
maxSubagents: DEFAULT_TEAM_CONFIG.maxSubagents,
|
|
641
|
+
maxConcurrent: DEFAULT_TEAM_CONFIG.maxConcurrent,
|
|
642
|
+
fanoutMigrated: true,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
577
645
|
export const DEFAULT_TODO_CONFIG = {
|
|
578
646
|
enabled: true,
|
|
579
647
|
panel: true,
|
|
@@ -1069,7 +1137,6 @@ export const DEFAULT_CONFIG = {
|
|
|
1069
1137
|
keyboardEnhancement: true,
|
|
1070
1138
|
scrollResumeMs: DEFAULT_SCROLL_RESUME_MS,
|
|
1071
1139
|
planModeDefault: false,
|
|
1072
|
-
planModeMaxAskRounds: DEFAULT_PLAN_MAX_ASK_ROUNDS,
|
|
1073
1140
|
planModeHumanTimeoutMs: DEFAULT_PLAN_HUMAN_TIMEOUT_MS,
|
|
1074
1141
|
skills: DEFAULT_SKILLS_CONFIG,
|
|
1075
1142
|
log: DEFAULT_LOG_CONFIG,
|
|
@@ -1162,6 +1229,37 @@ export function stripLegacyStateKeys(raw) {
|
|
|
1162
1229
|
delete out[key];
|
|
1163
1230
|
return out;
|
|
1164
1231
|
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Config keys whose MEANING was removed, mapped to the explanation a user gets if
|
|
1234
|
+
* they still try to set one.
|
|
1235
|
+
*
|
|
1236
|
+
* SEPARATE FROM `LEGACY_STATE_KEYS`, because the disposition differs: those keys'
|
|
1237
|
+
* values were MOVED somewhere else, while these have nowhere to go — the feature
|
|
1238
|
+
* they configured no longer exists. Merging the two lists would erase that
|
|
1239
|
+
* difference at exactly the moment someone is reading the file to find out what
|
|
1240
|
+
* happened to their setting.
|
|
1241
|
+
*
|
|
1242
|
+
* Stripped on the READ side for the same reason `stripLegacyStateKeys` is:
|
|
1243
|
+
* `loadPersistedConfig()` merges `{ ...DEFAULT_CONFIG, ...partial }`, so a key
|
|
1244
|
+
* left on disk rides that spread into memory and `updatePersistedConfig` writes it
|
|
1245
|
+
* straight back. The user deletes the line, launches once, and watches it return.
|
|
1246
|
+
*/
|
|
1247
|
+
export const RETIRED_CONFIG_KEYS = {
|
|
1248
|
+
planModeMaxAskRounds: 'ask_user has no round limit any more, so this key is gone. Nothing to set; ' +
|
|
1249
|
+
'the next config write removes it from config.json.',
|
|
1250
|
+
};
|
|
1251
|
+
/**
|
|
1252
|
+
* THE single gate that keeps retired keys out of `config.json`. Applied on the
|
|
1253
|
+
* read side, so it is self-healing: every write is read-strip-merge-write, and a
|
|
1254
|
+
* hand-edited file, a downgrade to an older CLI and back, or a half-finished
|
|
1255
|
+
* migration all converge on a clean file at the next write.
|
|
1256
|
+
*/
|
|
1257
|
+
export function stripRetiredConfigKeys(raw) {
|
|
1258
|
+
const out = { ...raw };
|
|
1259
|
+
for (const key of Object.keys(RETIRED_CONFIG_KEYS))
|
|
1260
|
+
delete out[key];
|
|
1261
|
+
return out;
|
|
1262
|
+
}
|
|
1165
1263
|
// ---------------------------------------------------------------------------
|
|
1166
1264
|
// Coercion / validation helpers
|
|
1167
1265
|
// ---------------------------------------------------------------------------
|
package/dist/config/store.js
CHANGED
|
@@ -17,7 +17,7 @@ import { getConfigPath, getSessionsDir } from './app-paths.js';
|
|
|
17
17
|
import { registerProfileSecrets, registerSecretsFrom } from '../logging/secret-registry.js';
|
|
18
18
|
import { adaptLegacyConnectionPatch, ModelProfileConfigError, readModelSettingsDisk } from './model-profile-store.js';
|
|
19
19
|
import { normalizeModelProfiles } from './model-profiles.js';
|
|
20
|
-
import { clampBashConfig, clampJobsConfig, clampPromptsConfig, clampTaskSummaryConfig, CONFIG_VERSION, DEFAULT_CONFIG, clampCompactionConfig, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampContextWindow, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
|
|
20
|
+
import { clampBashConfig, clampJobsConfig, clampPromptsConfig, clampTaskSummaryConfig, CONFIG_VERSION, DEFAULT_CONFIG, clampCompactionConfig, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampContextWindow, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, stripRetiredConfigKeys, } from './schema.js';
|
|
21
21
|
/**
|
|
22
22
|
* Normalize `maxTokens` for both directions of the config file.
|
|
23
23
|
*
|
|
@@ -124,7 +124,12 @@ export function normalizePersistedConfig(input) {
|
|
|
124
124
|
// on disk would otherwise ride the spread into `merged` and be written back
|
|
125
125
|
// out by `updatePersistedConfig` — the "I deleted it and it grew back" bug
|
|
126
126
|
// that `stripLegacyStateKeys` exists to make impossible (§4.5).
|
|
127
|
-
|
|
127
|
+
//
|
|
128
|
+
// TWO PASSES, DELIBERATELY: `stripLegacyStateKeys` handles keys whose value
|
|
129
|
+
// MOVED, `stripRetiredConfigKeys` handles keys whose FEATURE was removed
|
|
130
|
+
// (`planModeMaxAskRounds`). Same door, different reasons, and each list keeps
|
|
131
|
+
// growing on its own schedule.
|
|
132
|
+
const partial = stripRetiredConfigKeys(stripLegacyStateKeys(input));
|
|
128
133
|
delete partial.fullscreen;
|
|
129
134
|
return {
|
|
130
135
|
...DEFAULT_CONFIG,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `read_file` and `grep`): it is the full log on disk plus the PATH in the
|
|
10
10
|
* truncation note.
|
|
11
11
|
*
|
|
12
|
-
* THREE RULES, each of which is about not becoming a new failure mode:
|
|
12
|
+
* THREE RULES PLUS A SCOPE, each of which is about not becoming a new failure mode:
|
|
13
13
|
*
|
|
14
14
|
* - ASYNC AND COALESCED. `appendFile` with a <=4 writes/second buffer, so a
|
|
15
15
|
* webpack build emitting thousands of lines cannot turn logging into the
|
|
@@ -19,19 +19,36 @@
|
|
|
19
19
|
* - FAILURES ARE DEBUG LOGS, NEVER THROWS. A full disk must not kill a training
|
|
20
20
|
* run; the ring, the tools and the job state machine do not depend on this
|
|
21
21
|
* module at all.
|
|
22
|
+
* - SCOPED TO ITS OWN ROOT. The retention sweep deletes session directories under
|
|
23
|
+
* the jobs root the CALLER names, never under a parent derived from it: a
|
|
24
|
+
* derived parent is other people's directory the moment the mirror is handed a
|
|
25
|
+
* scratch path.
|
|
22
26
|
*/
|
|
23
27
|
export interface JobLogMirrorOptions {
|
|
24
28
|
/** `${aragonHome}/jobs/${sessionTag}`. A closure so a test can hand in a temp dir. */
|
|
25
29
|
root: () => string;
|
|
30
|
+
/**
|
|
31
|
+
* THE DIRECTORY WHOSE SESSION SUBDIRECTORIES MAY BE SWEPT - passed, never derived.
|
|
32
|
+
*
|
|
33
|
+
* `dirname(root())` used to stand in for this, and it was wrong in the only way that
|
|
34
|
+
* matters: for any caller whose `root()` does not sit exactly one level below the jobs
|
|
35
|
+
* directory (a scratch dir in a test is the OS temp root's child) it handed the sweep a
|
|
36
|
+
* directory full of OTHER people's data and deleted everything but the newest few
|
|
37
|
+
* entries. A release run paid 40 s for that sweep and lost the temp directories of the
|
|
38
|
+
* test files running beside it. Omitted means NO sweep at all: growth is bounded only
|
|
39
|
+
* for callers that name the root they own.
|
|
40
|
+
*/
|
|
41
|
+
jobsRoot?: () => string;
|
|
26
42
|
maxBytes?: number;
|
|
27
43
|
flushMs?: number;
|
|
28
44
|
}
|
|
29
45
|
/**
|
|
30
46
|
* Delete all but the newest `PROC_LIMITS.mirrorRetainDirs` session directories.
|
|
31
47
|
*
|
|
32
|
-
* BEST EFFORT AND SILENT. This runs once per
|
|
33
|
-
*
|
|
34
|
-
*
|
|
48
|
+
* BEST EFFORT AND SILENT. This runs once per mirror at first use, and its whole job is to
|
|
49
|
+
* stop `~/.aragon-agent/jobs/` from growing without bound; a directory that cannot be read
|
|
50
|
+
* is not a reason to refuse to start. `jobsRoot` MUST be the directory this module owns -
|
|
51
|
+
* see `JobLogMirrorOptions.jobsRoot` for what deriving it cost.
|
|
35
52
|
*/
|
|
36
53
|
export declare function pruneJobLogSessions(jobsRoot: string): void;
|
|
37
54
|
export declare class JobLogMirror {
|
package/dist/proc/log-mirror.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `read_file` and `grep`): it is the full log on disk plus the PATH in the
|
|
10
10
|
* truncation note.
|
|
11
11
|
*
|
|
12
|
-
* THREE RULES, each of which is about not becoming a new failure mode:
|
|
12
|
+
* THREE RULES PLUS A SCOPE, each of which is about not becoming a new failure mode:
|
|
13
13
|
*
|
|
14
14
|
* - ASYNC AND COALESCED. `appendFile` with a <=4 writes/second buffer, so a
|
|
15
15
|
* webpack build emitting thousands of lines cannot turn logging into the
|
|
@@ -19,18 +19,34 @@
|
|
|
19
19
|
* - FAILURES ARE DEBUG LOGS, NEVER THROWS. A full disk must not kill a training
|
|
20
20
|
* run; the ring, the tools and the job state machine do not depend on this
|
|
21
21
|
* module at all.
|
|
22
|
+
* - SCOPED TO ITS OWN ROOT. The retention sweep deletes session directories under
|
|
23
|
+
* the jobs root the CALLER names, never under a parent derived from it: a
|
|
24
|
+
* derived parent is other people's directory the moment the mirror is handed a
|
|
25
|
+
* scratch path.
|
|
22
26
|
*/
|
|
23
27
|
import { appendFile, appendFileSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
24
28
|
import { existsSync } from 'node:fs';
|
|
25
29
|
import { dirname, join } from 'node:path';
|
|
26
30
|
import { getLogger } from '../logging/logger.js';
|
|
27
31
|
import { PROC_LIMITS } from './limits.js';
|
|
32
|
+
/**
|
|
33
|
+
* True when `path` holds nothing but `<jobId>.log` files, or nothing at all.
|
|
34
|
+
*
|
|
35
|
+
* DEFENSE IN DEPTH, NOT THE PRIMARY GUARD. The primary guard is that the caller names
|
|
36
|
+
* the jobs root instead of having it derived. This one means a sweep handed a directory
|
|
37
|
+
* that is not a mirror root is a no-op rather than a data loss - an empty directory and a
|
|
38
|
+
* directory of job logs are the only two shapes this module ever creates.
|
|
39
|
+
*/
|
|
40
|
+
function isMirrorDirectory(path) {
|
|
41
|
+
return readdirSync(path, { withFileTypes: true }).every((entry) => entry.isFile() && entry.name.endsWith('.log'));
|
|
42
|
+
}
|
|
28
43
|
/**
|
|
29
44
|
* Delete all but the newest `PROC_LIMITS.mirrorRetainDirs` session directories.
|
|
30
45
|
*
|
|
31
|
-
* BEST EFFORT AND SILENT. This runs once per
|
|
32
|
-
*
|
|
33
|
-
*
|
|
46
|
+
* BEST EFFORT AND SILENT. This runs once per mirror at first use, and its whole job is to
|
|
47
|
+
* stop `~/.aragon-agent/jobs/` from growing without bound; a directory that cannot be read
|
|
48
|
+
* is not a reason to refuse to start. `jobsRoot` MUST be the directory this module owns -
|
|
49
|
+
* see `JobLogMirrorOptions.jobsRoot` for what deriving it cost.
|
|
34
50
|
*/
|
|
35
51
|
export function pruneJobLogSessions(jobsRoot) {
|
|
36
52
|
try {
|
|
@@ -41,14 +57,16 @@ export function pruneJobLogSessions(jobsRoot) {
|
|
|
41
57
|
.map((entry) => {
|
|
42
58
|
const path = join(jobsRoot, entry.name);
|
|
43
59
|
try {
|
|
44
|
-
return { path, mtime: statSync(path).mtimeMs };
|
|
60
|
+
return { path, mtime: statSync(path).mtimeMs, prunable: isMirrorDirectory(path) };
|
|
45
61
|
}
|
|
46
62
|
catch {
|
|
47
|
-
return { path, mtime: 0 };
|
|
63
|
+
return { path, mtime: 0, prunable: false };
|
|
48
64
|
}
|
|
49
65
|
})
|
|
50
66
|
.sort((a, b) => b.mtime - a.mtime);
|
|
51
67
|
for (const stale of dirs.slice(PROC_LIMITS.mirrorRetainDirs)) {
|
|
68
|
+
if (!stale.prunable)
|
|
69
|
+
continue;
|
|
52
70
|
try {
|
|
53
71
|
rmSync(stale.path, { recursive: true, force: true });
|
|
54
72
|
}
|
|
@@ -107,7 +125,11 @@ export class JobLogMirror {
|
|
|
107
125
|
this.files.set(jobId, { path, bytes, capped: bytes >= this.maxBytes() });
|
|
108
126
|
if (!this.pruned) {
|
|
109
127
|
this.pruned = true;
|
|
110
|
-
|
|
128
|
+
// ONLY THE ROOT THE CALLER NAMED. See `JobLogMirrorOptions.jobsRoot` for why
|
|
129
|
+
// `dirname(root())` is not a substitute for it.
|
|
130
|
+
const jobsRoot = this.opts.jobsRoot?.();
|
|
131
|
+
if (jobsRoot !== undefined)
|
|
132
|
+
pruneJobLogSessions(jobsRoot);
|
|
111
133
|
}
|
|
112
134
|
}
|
|
113
135
|
catch (err) {
|
package/dist/skills/service.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { SkillRegistry, type SkillFileRef, type SkillHost, type SkillPlatform, t
|
|
|
9
9
|
import type { NoticeLevel } from '../agent/reducer.js';
|
|
10
10
|
import type { SkillsConfig, SkillsRuntimeOptions, SkillsToolPolicyMode } from '../config/schema.js';
|
|
11
11
|
import { type ConfirmRequest } from '../tools/index.js';
|
|
12
|
+
import { type SkillRoutingReport } from './triage.js';
|
|
12
13
|
/**
|
|
13
14
|
* Capability-probing approval gate (D17 / §8.3.1).
|
|
14
15
|
*
|
|
@@ -229,6 +230,46 @@ export declare class SkillService {
|
|
|
229
230
|
/** Warn once about `--skill <name>` values that match nothing (§7.5). */
|
|
230
231
|
reportUnknownForcedSkills(): void;
|
|
231
232
|
setDisabled(name: string, disabled: boolean): void;
|
|
233
|
+
/**
|
|
234
|
+
* Route one task against the installed skills — PURE COMPUTATION (I-T2).
|
|
235
|
+
*
|
|
236
|
+
* Reads in-memory records only: no file, no body, no network, no model call. The
|
|
237
|
+
* whole "which skills could apply here" question therefore costs zero context
|
|
238
|
+
* bytes, which is what makes the disclosure ladder honest — Level 2 is paid for
|
|
239
|
+
* only when the model actually opens a skill.
|
|
240
|
+
*
|
|
241
|
+
* Returns `null` when triage is switched off, so every caller has one shape to
|
|
242
|
+
* check rather than an empty report that looks like "no match".
|
|
243
|
+
*
|
|
244
|
+
* Writes NO usage counter. An automatic proposal is not a user's choice, which
|
|
245
|
+
* is the same reason `loadBody({ countUse: false })` exists for always-on
|
|
246
|
+
* skills: counting it would pin an auto-suggested skill to the top of the
|
|
247
|
+
* ranking forever and destroy the signal the ranking is there to carry.
|
|
248
|
+
*/
|
|
249
|
+
triage(task: string): SkillRoutingReport | null;
|
|
250
|
+
/**
|
|
251
|
+
* `/skills route <text>` — the same computation, WITHOUT the on/off gate.
|
|
252
|
+
*
|
|
253
|
+
* The gate exists to keep the block out of the prompt; it must not make the
|
|
254
|
+
* explanation unreachable, because "why did nothing match" is the question a user
|
|
255
|
+
* asks precisely when they suspect the switch is off.
|
|
256
|
+
*/
|
|
257
|
+
route(task: string): SkillRoutingReport;
|
|
258
|
+
/**
|
|
259
|
+
* `/skills triage on|off` — persist the switch AND rebuild the prompt now.
|
|
260
|
+
*
|
|
261
|
+
* `updateConfig` deliberately only replaces this object's config and calls
|
|
262
|
+
* `persist`; it does NOT call `deps.onChange`. Every other mutator supplies that
|
|
263
|
+
* call itself (`setDisabled` above, the directory-trust path below), and `onChange`
|
|
264
|
+
* is the sole route to `controller.refreshSkills()` → `rebuildSystemPrompt()`.
|
|
265
|
+
* Without it the setting would be written to disk and the running session would
|
|
266
|
+
* keep the old prompt until the next launch — which reads as a broken switch.
|
|
267
|
+
*
|
|
268
|
+
* The early return is not an optimization: `updateConfig` persists, so flipping a
|
|
269
|
+
* switch to the value it already has would produce a pointless disk write and a
|
|
270
|
+
* pointless prompt rebuild on every keystroke.
|
|
271
|
+
*/
|
|
272
|
+
setTriageEnabled(enabled: boolean): void;
|
|
232
273
|
getConfig(): SkillsConfig;
|
|
233
274
|
/** Local use counters backing `--sort=recent`; empty when tracking is off. */
|
|
234
275
|
getUsage(): SkillUsageMap;
|
package/dist/skills/service.js
CHANGED
|
@@ -14,6 +14,7 @@ import { currentPlatform } from './node-host.js';
|
|
|
14
14
|
import { MANIFEST_FILENAME, readManifest, sha256Buffer, sha256File } from './manifest.js';
|
|
15
15
|
import { isDirectory, normalizeTrustPath, resolveSkillRoots, getStagingDir, } from './paths.js';
|
|
16
16
|
import { USAGE_MAX_ENTRIES, loadUsage, pruneUsage, recordUse } from './usage.js';
|
|
17
|
+
import { buildRoutingReport } from './triage.js';
|
|
17
18
|
const ENTRY_FILE = 'SKILL.md';
|
|
18
19
|
/**
|
|
19
20
|
* Re-exported from `manifest.ts` rather than spelled out again. The two copies
|
|
@@ -615,6 +616,67 @@ export class SkillService {
|
|
|
615
616
|
this.updateConfig({ disabled: [...next] });
|
|
616
617
|
this.deps.onChange?.();
|
|
617
618
|
}
|
|
619
|
+
/**
|
|
620
|
+
* Route one task against the installed skills — PURE COMPUTATION (I-T2).
|
|
621
|
+
*
|
|
622
|
+
* Reads in-memory records only: no file, no body, no network, no model call. The
|
|
623
|
+
* whole "which skills could apply here" question therefore costs zero context
|
|
624
|
+
* bytes, which is what makes the disclosure ladder honest — Level 2 is paid for
|
|
625
|
+
* only when the model actually opens a skill.
|
|
626
|
+
*
|
|
627
|
+
* Returns `null` when triage is switched off, so every caller has one shape to
|
|
628
|
+
* check rather than an empty report that looks like "no match".
|
|
629
|
+
*
|
|
630
|
+
* Writes NO usage counter. An automatic proposal is not a user's choice, which
|
|
631
|
+
* is the same reason `loadBody({ countUse: false })` exists for always-on
|
|
632
|
+
* skills: counting it would pin an auto-suggested skill to the top of the
|
|
633
|
+
* ranking forever and destroy the signal the ranking is there to carry.
|
|
634
|
+
*/
|
|
635
|
+
triage(task) {
|
|
636
|
+
if (!this.config.enabled || !this.config.triage)
|
|
637
|
+
return null;
|
|
638
|
+
return buildRoutingReport({
|
|
639
|
+
task,
|
|
640
|
+
records: this.registry.list(),
|
|
641
|
+
activeNames: this.registry.activeNames,
|
|
642
|
+
now: Date.now(),
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* `/skills route <text>` — the same computation, WITHOUT the on/off gate.
|
|
647
|
+
*
|
|
648
|
+
* The gate exists to keep the block out of the prompt; it must not make the
|
|
649
|
+
* explanation unreachable, because "why did nothing match" is the question a user
|
|
650
|
+
* asks precisely when they suspect the switch is off.
|
|
651
|
+
*/
|
|
652
|
+
route(task) {
|
|
653
|
+
return buildRoutingReport({
|
|
654
|
+
task,
|
|
655
|
+
records: this.registry.list(),
|
|
656
|
+
activeNames: this.registry.activeNames,
|
|
657
|
+
now: Date.now(),
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* `/skills triage on|off` — persist the switch AND rebuild the prompt now.
|
|
662
|
+
*
|
|
663
|
+
* `updateConfig` deliberately only replaces this object's config and calls
|
|
664
|
+
* `persist`; it does NOT call `deps.onChange`. Every other mutator supplies that
|
|
665
|
+
* call itself (`setDisabled` above, the directory-trust path below), and `onChange`
|
|
666
|
+
* is the sole route to `controller.refreshSkills()` → `rebuildSystemPrompt()`.
|
|
667
|
+
* Without it the setting would be written to disk and the running session would
|
|
668
|
+
* keep the old prompt until the next launch — which reads as a broken switch.
|
|
669
|
+
*
|
|
670
|
+
* The early return is not an optimization: `updateConfig` persists, so flipping a
|
|
671
|
+
* switch to the value it already has would produce a pointless disk write and a
|
|
672
|
+
* pointless prompt rebuild on every keystroke.
|
|
673
|
+
*/
|
|
674
|
+
setTriageEnabled(enabled) {
|
|
675
|
+
if (this.config.triage === enabled)
|
|
676
|
+
return;
|
|
677
|
+
this.updateConfig({ triage: enabled });
|
|
678
|
+
this.deps.onChange?.();
|
|
679
|
+
}
|
|
618
680
|
getConfig() {
|
|
619
681
|
return this.config;
|
|
620
682
|
}
|