devflow-kit 3.1.0 → 3.3.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/CHANGELOG.md +52 -0
- package/README.md +2 -2
- package/dist/cli/agents-view/render.js +69 -15
- package/dist/cli/agents-view/state.js +40 -14
- package/dist/cli/commands/agents.js +135 -45
- package/dist/cli/commands/init.js +128 -53
- package/dist/cli/commands/learning.js +61 -13
- package/dist/cli/commands/memory.js +35 -14
- package/dist/cli/commands/uninstall.js +163 -39
- package/dist/commands/code-review.md +1 -3
- package/dist/commands/debug.md +15 -12
- package/dist/commands/dynamic-build.md +172 -135
- package/dist/commands/dynamic-plan.md +9 -3
- package/dist/commands/explore.md +10 -4
- package/dist/commands/implement.md +149 -145
- package/dist/commands/plan.md +13 -9
- package/dist/commands/release.md +8 -2
- package/dist/commands/research.md +8 -2
- package/dist/commands/resolve.md +28 -19
- package/dist/commands/self-review.md +16 -13
- package/dist/core/agent-frontmatter.js +25 -0
- package/dist/core/agent-models.js +201 -36
- package/dist/core/agent-state.js +27 -5
- package/dist/core/assets.js +1 -1
- package/dist/core/feature-config.js +68 -10
- package/dist/core/flags.js +24 -0
- package/dist/core/learning-queue-cleanup.js +10 -11
- package/dist/core/learning-tuning-config.js +8 -0
- package/dist/core/linked-path.js +46 -0
- package/dist/core/plugins.js +16 -5
- package/dist/core/queue-drain.js +31 -0
- package/dist/hud/components/learning-counts.js +54 -8
- package/dist/skills/git/references/tracker/github/create-release.md +2 -2
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +1 -1
- package/dist/skills/git/references/tracker/jira/create-release.md +2 -2
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +1 -1
- package/dist/skills/git/references/tracker/linear/create-release.md +2 -2
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +1 -1
- package/dist/targets/claude-code/installer.js +36 -9
- package/dist/targets/claude-code/post-install.js +128 -38
- package/package.json +1 -1
- package/src/assets/agents/code.md +85 -35
- package/src/assets/agents/design.md +12 -0
- package/src/assets/agents/diagnose.md +18 -11
- package/src/assets/agents/evaluate.md +17 -24
- package/src/assets/agents/knowledge.md +7 -3
- package/src/assets/agents/learning.md +4 -6
- package/src/assets/agents/research.md +21 -0
- package/src/assets/agents/review.md +12 -0
- package/src/assets/agents/scrutinize.md +37 -9
- package/src/assets/agents/simplify.md +24 -0
- package/src/assets/agents/skim.md +6 -2
- package/src/assets/agents/synthesize.md +18 -0
- package/src/assets/agents/test.md +19 -11
- package/src/assets/agents/triage.md +8 -0
- package/src/assets/agents/validate.md +20 -11
- package/src/assets/commands/_partials/_engine.mds +36 -55
- package/src/assets/commands/_partials/_knowledge.mds +1 -3
- package/src/assets/commands/_partials/_plan_contract.mds +1 -1
- package/src/assets/commands/_partials/_tracker.mds +1 -1
- package/src/assets/commands/_partials/_wave.mds +8 -6
- package/src/assets/commands/code-review.mds +1 -3
- package/src/assets/commands/debug.mds +13 -8
- package/src/assets/commands/dynamic-build.mds +126 -72
- package/src/assets/commands/dynamic-plan.mds +7 -1
- package/src/assets/commands/explore.mds +9 -1
- package/src/assets/commands/implement.mds +147 -141
- package/src/assets/commands/plan.mds +12 -8
- package/src/assets/commands/release.md +8 -2
- package/src/assets/commands/research.mds +8 -2
- package/src/assets/commands/resolve.mds +27 -16
- package/src/assets/commands/self-review.mds +15 -10
- package/src/assets/mds/tracker/_common.mds +1 -1
- package/src/assets/mds/tracker/_github.mds +2 -2
- package/src/assets/mds/tracker/_jira.mds +2 -2
- package/src/assets/mds/tracker/_linear.mds +2 -2
- package/src/assets/scripts/ci-wait.cjs +636 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -3
- package/src/assets/scripts/hooks/background-memory-update +356 -17
- package/src/assets/scripts/hooks/capture-prompt +4 -3
- package/src/assets/scripts/hooks/capture-question +4 -3
- package/src/assets/scripts/hooks/capture-turn +4 -3
- package/src/assets/scripts/hooks/ensure-devflow-init +13 -1
- package/src/assets/scripts/hooks/ensure-root-gitignore +122 -10
- package/src/assets/scripts/hooks/git-marker +71 -0
- package/src/assets/scripts/hooks/json-helper.cjs +12 -145
- package/src/assets/scripts/hooks/json-parse +24 -129
- package/src/assets/scripts/hooks/lib/learning-store.cjs +169 -64
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +1 -1
- package/src/assets/scripts/hooks/memory-worker +10 -0
- package/src/assets/scripts/hooks/pre-compact-memory +66 -14
- package/src/assets/scripts/hooks/preamble +9 -1
- package/src/assets/scripts/hooks/queue-append +53 -21
- package/src/assets/scripts/hooks/session-start-context +108 -29
- package/src/assets/scripts/hooks/session-start-memory +33 -11
- package/src/assets/scripts/release-trace.cjs +27 -10
- package/src/assets/skills/accessibility/SKILL.md +1 -1
- package/src/assets/skills/apply-decisions/SKILL.md +12 -82
- package/src/assets/skills/apply-feature-knowledge/SKILL.md +8 -42
- package/src/assets/skills/architecture/SKILL.md +1 -1
- package/src/assets/skills/boundary-validation/SKILL.md +1 -1
- package/src/assets/skills/complexity/SKILL.md +1 -1
- package/src/assets/skills/compliance/SKILL.md +1 -1
- package/src/assets/skills/consistency/SKILL.md +1 -1
- package/src/assets/skills/database/SKILL.md +1 -1
- package/src/assets/skills/dependencies/SKILL.md +1 -1
- package/src/assets/skills/dependency-research/SKILL.md +3 -6
- package/src/assets/skills/design-review/SKILL.md +1 -1
- package/src/assets/skills/docs-framework/SKILL.md +1 -1
- package/src/assets/skills/documentation/SKILL.md +1 -1
- package/src/assets/skills/gap-analysis/SKILL.md +1 -1
- package/src/assets/skills/git/SKILL.md +1 -1
- package/src/assets/skills/go/SKILL.md +1 -1
- package/src/assets/skills/java/SKILL.md +1 -1
- package/src/assets/skills/patterns/SKILL.md +1 -1
- package/src/assets/skills/performance/SKILL.md +1 -1
- package/src/assets/skills/python/SKILL.md +1 -1
- package/src/assets/skills/qa/SKILL.md +1 -3
- package/src/assets/skills/quality-gates/SKILL.md +9 -12
- package/src/assets/skills/quality-gates/references/report-template.md +20 -20
- package/src/assets/skills/react/SKILL.md +1 -1
- package/src/assets/skills/regression/SKILL.md +1 -1
- package/src/assets/skills/reliability/SKILL.md +1 -1
- package/src/assets/skills/research-codebase/SKILL.md +1 -1
- package/src/assets/skills/research-competitor/SKILL.md +1 -1
- package/src/assets/skills/research-external/SKILL.md +1 -1
- package/src/assets/skills/research-technology/SKILL.md +1 -1
- package/src/assets/skills/review-methodology/SKILL.md +1 -1
- package/src/assets/skills/rust/SKILL.md +1 -1
- package/src/assets/skills/security/SKILL.md +1 -1
- package/src/assets/skills/software-design/SKILL.md +1 -1
- package/src/assets/skills/test-driven-development/SKILL.md +15 -33
- package/src/assets/skills/testing/SKILL.md +1 -1
- package/src/assets/skills/typescript/SKILL.md +1 -1
- package/src/assets/skills/ui-design/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/SKILL.md +3 -55
- package/src/assets/skills/worktree-support/references/discovery.md +48 -0
- package/src/assets/skills/worktree-support/references/roots.md +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Mapping file: ~/.devflow/agent-models.json
|
|
9
9
|
* { version: 1, agents: { [name]: { model?, effort? } } }
|
|
10
|
-
* Deviations-only: omit an agent to inherit its shipped default.
|
|
10
|
+
* Deviations-only: omit an agent to inherit its shipped default (model AND effort).
|
|
11
11
|
* Unknown agent names are tolerated and preserved on save (plugin may not be installed).
|
|
12
12
|
* Invalid effort values are dropped with a warning.
|
|
13
13
|
*
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
import { promises as fs } from 'fs';
|
|
26
26
|
import * as path from 'path';
|
|
27
27
|
import { writeFileAtomicExclusive } from './fs-atomic.js';
|
|
28
|
-
import { isDormantExternalModel } from './external-models.js';
|
|
29
|
-
import { rewriteAgentFrontmatter, readFrontmatterModel, isValidModelName } from './agent-frontmatter.js';
|
|
28
|
+
import { isDormantExternalModel, CLAUDE_MODEL_ALIASES } from './external-models.js';
|
|
29
|
+
import { rewriteAgentFrontmatter, readFrontmatterModel, readFrontmatterEffort, isValidModelName, } from './agent-frontmatter.js';
|
|
30
30
|
import { agentSourceDirs } from './assets.js';
|
|
31
31
|
import { getAllAgentNames } from './plugins.js';
|
|
32
32
|
import { mdEntryName, mdFileName } from './orphan-sweep.js';
|
|
@@ -51,6 +51,93 @@ export const EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
|
51
51
|
* EFFORT_LEVELS[number] is a literal union, not string.
|
|
52
52
|
*/
|
|
53
53
|
const EFFORT_LEVELS_SET = new Set(EFFORT_LEVELS);
|
|
54
|
+
/** True when `value` is one of EFFORT_LEVELS (narrows string to EffortLevel). */
|
|
55
|
+
export function isEffortLevel(value) {
|
|
56
|
+
return EFFORT_LEVELS_SET.has(value);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* D-SHIPPED-EFFORT: the stored effort value that drops an agent's effort line
|
|
60
|
+
* altogether. A mapping `inherit` yields no `effort:` line and does NOT fall
|
|
61
|
+
* back to the shipped effort, so the agent follows the session's effort. It is
|
|
62
|
+
* a stored value, unlike `default` (which deletes the key so the shipped
|
|
63
|
+
* effort applies again), and it is effort-only: model-side inherit semantics
|
|
64
|
+
* are a separate decision.
|
|
65
|
+
*/
|
|
66
|
+
export const EFFORT_INHERIT = 'inherit';
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Workers
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
/**
|
|
71
|
+
* D-WORKER-AGENTS: background workers whose model and effort are settable in
|
|
72
|
+
* `devflow agents` although they are not agents. A worker runs as its own
|
|
73
|
+
* `claude -p` process outside any session and has no installed agent file, so
|
|
74
|
+
* there is nothing for a reapply to rewrite.
|
|
75
|
+
*
|
|
76
|
+
* Worker entries are stored under `agents.<name>` in agent-models.json, in the
|
|
77
|
+
* same map as agent entries; an absent entry means the default declared here.
|
|
78
|
+
* Because the map is shared, every agent-only path (the reapply walk, the init
|
|
79
|
+
* reapply gate, countExternalMappedAgents, the list and TUI agent sets) goes
|
|
80
|
+
* through agentOnlyMapping, the one definition of the agent-only subset.
|
|
81
|
+
*
|
|
82
|
+
* A worker is not a DEVFLOW_PLUGINS agent: getAllAgentNames() and the roster
|
|
83
|
+
* count are unaffected.
|
|
84
|
+
*
|
|
85
|
+
* Shipped value of `memory`: `claude-sonnet-5-5` at `high` effort. User decision
|
|
86
|
+
* 2026-10-08: memory quality is worth more than the cost saving of Haiku.
|
|
87
|
+
*
|
|
88
|
+
* D-MEMORY-WORKER-LEAN: background-memory-update reads `agents.memory` from
|
|
89
|
+
* `~/.devflow/agent-models.json` at run time, validating `model` and `effort`
|
|
90
|
+
* field by field against the domain below, and passes `--effort` when the CLI
|
|
91
|
+
* version probe puts it at or above the lean-argv floor. A field it cannot use
|
|
92
|
+
* falls back to this row. The hook spells that fallback out for bash
|
|
93
|
+
* (`MEMORY_MODEL_DEFAULT`, `MEMORY_EFFORT_DEFAULT`, the alias and effort lists and
|
|
94
|
+
* the model-name rules), and tests/agent-models-worker.test.ts holds every one
|
|
95
|
+
* equal to this row and to the domain, so the two cannot drift apart.
|
|
96
|
+
*/
|
|
97
|
+
export const WORKER_AGENTS = {
|
|
98
|
+
memory: { model: 'claude-sonnet-5-5', effort: 'high' },
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* True when `name` is a worker key. Own-property check, so a hostile key such as
|
|
102
|
+
* `constructor` or `__proto__` is never answered from the prototype chain.
|
|
103
|
+
*/
|
|
104
|
+
export function isWorkerAgent(name) {
|
|
105
|
+
return Object.hasOwn(WORKER_AGENTS, name);
|
|
106
|
+
}
|
|
107
|
+
/** A full Claude model identifier: the `claude-` prefix and at least one more character. */
|
|
108
|
+
const CLAUDE_FULL_ID_RE = /^claude-.+$/;
|
|
109
|
+
/**
|
|
110
|
+
* D-WORKER-AGENTS: the worker value domain, owned here once and shared by
|
|
111
|
+
* `validateSetArgs` (the write boundary) and `readAgentMapping` (the read
|
|
112
|
+
* boundary), so a hand-edited file and a CLI argument are held to one rule.
|
|
113
|
+
*
|
|
114
|
+
* - model: `default`, a CLAUDE_MODEL_ALIASES alias, or a full identifier
|
|
115
|
+
* starting `claude-`. Anything else is rejected, `inherit` included
|
|
116
|
+
* (a worker has no session model to inherit). External models are
|
|
117
|
+
* rejected too: the worker runs `claude -p` outside a session and
|
|
118
|
+
* nothing here verifies it routes through the proxy, so allowing
|
|
119
|
+
* them later is a deliberate change to this function.
|
|
120
|
+
* - effort: `default` or an EFFORT_LEVELS level. `inherit` is rejected because
|
|
121
|
+
* a worker has no session effort to inherit.
|
|
122
|
+
*
|
|
123
|
+
* Pure function — no catalog lookup, no I/O.
|
|
124
|
+
*/
|
|
125
|
+
export function validateWorkerValue(field, value) {
|
|
126
|
+
if (field === 'effort') {
|
|
127
|
+
if (value === 'default' || isEffortLevel(value))
|
|
128
|
+
return Ok(undefined);
|
|
129
|
+
return Err(`Invalid effort "${value}" for a worker. Valid: default, ${EFFORT_LEVELS.join(', ')} ` +
|
|
130
|
+
`(a worker has no session effort to inherit)`);
|
|
131
|
+
}
|
|
132
|
+
const inDomain = isValidModelName(value) &&
|
|
133
|
+
(value === 'default' ||
|
|
134
|
+
CLAUDE_MODEL_ALIASES.includes(value) ||
|
|
135
|
+
CLAUDE_FULL_ID_RE.test(value));
|
|
136
|
+
if (inDomain)
|
|
137
|
+
return Ok(undefined);
|
|
138
|
+
return Err(`Invalid model "${value}" for a worker. Valid: default, ${CLAUDE_MODEL_ALIASES.join(', ')}, ` +
|
|
139
|
+
`or a full claude- identifier (external models are not supported for workers)`);
|
|
140
|
+
}
|
|
54
141
|
// ---------------------------------------------------------------------------
|
|
55
142
|
// Key migration
|
|
56
143
|
// ---------------------------------------------------------------------------
|
|
@@ -216,6 +303,34 @@ export async function parseAgentMappingEnvelope(filePath) {
|
|
|
216
303
|
}
|
|
217
304
|
return { kind: 'ok', envelope, rawAgents: rawAgents };
|
|
218
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* D-WORKER-AGENTS: the agent-only subset of the mapping — every entry whose key
|
|
308
|
+
* is not a worker. Unknown names stay: the file preserves entries for agents
|
|
309
|
+
* whose plugin is not installed.
|
|
310
|
+
*
|
|
311
|
+
* This is the one definition of "the agents in the mapping". The reapply walk,
|
|
312
|
+
* the init reapply gate, countExternalMappedAgents and the TUI's orphan rows all
|
|
313
|
+
* call it, so a worker entry can never be mistaken for an agent with no
|
|
314
|
+
* installed file.
|
|
315
|
+
*
|
|
316
|
+
* Pure function — returns a new record, never mutates the mapping.
|
|
317
|
+
*/
|
|
318
|
+
export function agentOnlyMapping(mapping) {
|
|
319
|
+
const agents = {};
|
|
320
|
+
for (const [name, entry] of Object.entries(mapping.agents)) {
|
|
321
|
+
if (!isWorkerAgent(name))
|
|
322
|
+
agents[name] = entry;
|
|
323
|
+
}
|
|
324
|
+
return agents;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* True when the mapping holds at least one agent entry. The `devflow init`
|
|
328
|
+
* reapply gate: an agents.memory-only mapping keeps it closed, because there is
|
|
329
|
+
* no agent file for a reapply to converge.
|
|
330
|
+
*/
|
|
331
|
+
export function hasAgentMappingEntries(mapping) {
|
|
332
|
+
return Object.keys(agentOnlyMapping(mapping)).length > 0;
|
|
333
|
+
}
|
|
219
334
|
/**
|
|
220
335
|
* Read and tolerantly parse ~/.devflow/agent-models.json.
|
|
221
336
|
* Returns an empty mapping when the file is missing.
|
|
@@ -245,24 +360,39 @@ export async function readAgentMapping(devflowDir, opts) {
|
|
|
245
360
|
continue;
|
|
246
361
|
const raw = entry;
|
|
247
362
|
const mapping = {};
|
|
363
|
+
// D-WORKER-AGENTS: a worker key is held to the worker value domain on read,
|
|
364
|
+
// by the same function the CLI write boundary uses.
|
|
365
|
+
const worker = isWorkerAgent(name);
|
|
248
366
|
if (typeof raw.model === 'string') {
|
|
249
367
|
// Tighten to the same charset used by rewriteAgentFrontmatter.
|
|
250
368
|
// The effort field is enum-validated below; model must be equally strict.
|
|
251
369
|
// An invalid entry is dropped with a warning rather than silently
|
|
252
370
|
// persisting and permanently poisoning that agent on every reapply.
|
|
253
|
-
if (isValidModelName(raw.model)) {
|
|
254
|
-
|
|
371
|
+
if (!isValidModelName(raw.model)) {
|
|
372
|
+
warn(`agent-models: invalid-model name for agent "${name}" — dropping entry`);
|
|
255
373
|
}
|
|
256
374
|
else {
|
|
257
|
-
|
|
375
|
+
const workerCheck = worker ? validateWorkerValue('model', raw.model) : undefined;
|
|
376
|
+
if (workerCheck !== undefined && !workerCheck.ok) {
|
|
377
|
+
warn(`agent-models: dropping out-of-domain model "${raw.model}" for worker "${name}" — ${workerCheck.error}`);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
mapping.model = raw.model;
|
|
381
|
+
}
|
|
258
382
|
}
|
|
259
383
|
}
|
|
260
384
|
if (typeof raw.effort === 'string') {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
385
|
+
// `inherit` is a stored agent value; a worker has no session effort to
|
|
386
|
+
// inherit, so for a worker it is out of domain like any other non-level.
|
|
387
|
+
if (isEffortLevel(raw.effort)) {
|
|
264
388
|
mapping.effort = raw.effort;
|
|
265
389
|
}
|
|
390
|
+
else if (raw.effort === EFFORT_INHERIT && !worker) {
|
|
391
|
+
mapping.effort = raw.effort;
|
|
392
|
+
}
|
|
393
|
+
else if (worker) {
|
|
394
|
+
warn(`agent-models: dropping out-of-domain effort "${raw.effort}" for worker "${name}"`);
|
|
395
|
+
}
|
|
266
396
|
else {
|
|
267
397
|
warn(`agent-models: dropping invalid effort "${raw.effort}" for agent "${name}"`);
|
|
268
398
|
}
|
|
@@ -330,41 +460,49 @@ export async function saveAgentMapping(devflowDir, mapping) {
|
|
|
330
460
|
* isDormantExternalModel) AND proxyEnabled is false → the entry is DORMANT.
|
|
331
461
|
* The shipped default model is used instead. The entry remains saved.
|
|
332
462
|
*
|
|
333
|
-
*
|
|
463
|
+
* D-SHIPPED-EFFORT — effort rule, in order:
|
|
464
|
+
* 1. a mapping effort level wins;
|
|
465
|
+
* 2. a mapping `inherit` yields no effort line and does NOT fall back to the
|
|
466
|
+
* shipped effort;
|
|
467
|
+
* 3. an absent mapping effort falls back to the shipped effort, which is
|
|
468
|
+
* undefined when the agent ships none.
|
|
469
|
+
* Effort is ALWAYS applied regardless of proxy state, a dormant external
|
|
470
|
+
* model included.
|
|
334
471
|
*
|
|
335
472
|
* Pure function — no I/O.
|
|
336
473
|
*
|
|
337
474
|
* @param agentName - The agent's short name (e.g., 'code').
|
|
338
475
|
* @param mapping - The full mapping file.
|
|
339
|
-
* @param shippedDefaults - Map of agent name → shipped
|
|
476
|
+
* @param shippedDefaults - Map of agent name → shipped model and effort.
|
|
340
477
|
* @param proxyEnabled - Whether the Devflow proxy is currently active.
|
|
341
478
|
*/
|
|
342
479
|
export function resolveEffective(agentName, mapping, shippedDefaults, proxyEnabled) {
|
|
343
480
|
const entry = mapping.agents[agentName];
|
|
481
|
+
const shipped = shippedDefaults[agentName];
|
|
344
482
|
let model;
|
|
345
483
|
if (entry?.model !== undefined) {
|
|
346
484
|
// Dormant: external model configured but proxy is off → fall back to shipped default.
|
|
347
485
|
model = isDormantExternalModel(entry.model, proxyEnabled)
|
|
348
|
-
?
|
|
486
|
+
? shipped?.model
|
|
349
487
|
: entry.model;
|
|
350
488
|
}
|
|
351
489
|
else {
|
|
352
490
|
// No mapping entry → use shipped default.
|
|
353
|
-
model =
|
|
491
|
+
model = shipped?.model;
|
|
354
492
|
}
|
|
355
|
-
const
|
|
493
|
+
const mapped = entry?.effort;
|
|
494
|
+
const effort = mapped === EFFORT_INHERIT ? undefined : (mapped ?? shipped?.effort);
|
|
356
495
|
return { model, effort };
|
|
357
496
|
}
|
|
358
|
-
// ---------------------------------------------------------------------------
|
|
359
|
-
// loadShippedDefaults
|
|
360
|
-
// ---------------------------------------------------------------------------
|
|
361
497
|
/**
|
|
362
|
-
* Parse the shipped model
|
|
498
|
+
* Parse the shipped model and effort out of every {name}.md in one directory.
|
|
363
499
|
*
|
|
364
500
|
* A missing or unreadable directory yields an empty map — dist/agents/ does not
|
|
365
501
|
* exist until a generator host does, and a source tree that produced no agents
|
|
366
502
|
* is caught by the registry-completeness guard rather than by a throw here.
|
|
367
|
-
* Unknown or malformed files are skipped individually.
|
|
503
|
+
* Unknown or malformed files are skipped individually. The effort is returned as
|
|
504
|
+
* written: only the directory that wins the merge has its effort validated, so a
|
|
505
|
+
* typo in a losing file is never reported.
|
|
368
506
|
*/
|
|
369
507
|
async function readDirDefaults(dir) {
|
|
370
508
|
let entries;
|
|
@@ -380,9 +518,10 @@ async function readDirDefaults(dir) {
|
|
|
380
518
|
return null;
|
|
381
519
|
try {
|
|
382
520
|
const content = await fs.readFile(path.join(dir, file), 'utf-8');
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
521
|
+
const model = readFrontmatterModel(content);
|
|
522
|
+
const effort = readFrontmatterEffort(content);
|
|
523
|
+
if (model.ok && model.value && effort.ok) {
|
|
524
|
+
return [agentName, { model: model.value, effort: effort.value }];
|
|
386
525
|
}
|
|
387
526
|
}
|
|
388
527
|
catch {
|
|
@@ -399,11 +538,22 @@ async function readDirDefaults(dir) {
|
|
|
399
538
|
return defaults;
|
|
400
539
|
}
|
|
401
540
|
/**
|
|
402
|
-
* Load shipped default
|
|
541
|
+
* Load the shipped default model and effort of every agent from the agent files.
|
|
542
|
+
*
|
|
543
|
+
* D-SHIPPED-EFFORT: this is the only production reader of shipped model and
|
|
544
|
+
* effort values. An agent's effort is part of its shipped default, so a reapply
|
|
545
|
+
* that did not see it would strip it (the mapping has no entry to say
|
|
546
|
+
* otherwise).
|
|
403
547
|
*
|
|
404
548
|
* Directories are MOST-PREFERRED FIRST — the convention owned by
|
|
405
549
|
* agentSourceDirs() — and the first directory to supply a name wins, so once an
|
|
406
550
|
* agent is generated into dist/agents/ its frontmatter is the shipped default.
|
|
551
|
+
* First-hit-wins applies to the whole record: an agent's effort is never read
|
|
552
|
+
* from a different file than its model.
|
|
553
|
+
*
|
|
554
|
+
* A shipped `effort:` outside EFFORT_LEVELS (`inherit` included — it is a
|
|
555
|
+
* mapping value, not a shipped one) is dropped with a warning naming the agent;
|
|
556
|
+
* its model survives.
|
|
407
557
|
*
|
|
408
558
|
* A registry agent that no directory supplies is reported through `onWarning`
|
|
409
559
|
* as ONE aggregate message naming every missing agent and the build step. The
|
|
@@ -417,17 +567,31 @@ async function readDirDefaults(dir) {
|
|
|
417
567
|
* prove the precedence against a temp tree; all real callers use the default.
|
|
418
568
|
* @param opts - Optional warning channel; the gap is silent without one.
|
|
419
569
|
*/
|
|
420
|
-
export async function
|
|
570
|
+
export async function loadShippedAgentDefaults(dirs = agentSourceDirs(), opts) {
|
|
421
571
|
const perDir = await Promise.all(dirs.map(readDirDefaults));
|
|
422
|
-
const
|
|
572
|
+
const winners = {};
|
|
423
573
|
for (const dirDefaults of perDir) {
|
|
424
|
-
for (const [agentName,
|
|
425
|
-
if (!(agentName
|
|
426
|
-
|
|
574
|
+
for (const [agentName, raw] of Object.entries(dirDefaults)) {
|
|
575
|
+
if (!Object.hasOwn(winners, agentName)) {
|
|
576
|
+
winners[agentName] = raw;
|
|
427
577
|
}
|
|
428
578
|
}
|
|
429
579
|
}
|
|
430
|
-
const
|
|
580
|
+
const defaults = {};
|
|
581
|
+
for (const [agentName, raw] of Object.entries(winners)) {
|
|
582
|
+
if (raw.effort === '') {
|
|
583
|
+
defaults[agentName] = { model: raw.model };
|
|
584
|
+
}
|
|
585
|
+
else if (isEffortLevel(raw.effort)) {
|
|
586
|
+
defaults[agentName] = { model: raw.model, effort: raw.effort };
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
defaults[agentName] = { model: raw.model };
|
|
590
|
+
opts?.onWarning?.(`Shipped agent "${agentName}" declares effort "${raw.effort}", which is not one of ` +
|
|
591
|
+
`${EFFORT_LEVELS.join(', ')} — ignoring it.`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const missing = getAllAgentNames().filter(name => !Object.hasOwn(defaults, name));
|
|
431
595
|
if (missing.length > 0) {
|
|
432
596
|
opts?.onWarning?.(`No shipped default found for declared agent(s): ${missing.join(', ')}. ` +
|
|
433
597
|
`Run \`npm run build:mds\` if they are compiled from .mds generator hosts, otherwise ` +
|
|
@@ -439,10 +603,11 @@ export async function loadShippedDefaults(dirs = agentSourceDirs(), opts) {
|
|
|
439
603
|
* Idempotent convergence function: walk every installed agent file and
|
|
440
604
|
* rewrite frontmatter model/effort to match the effective mapping.
|
|
441
605
|
*
|
|
442
|
-
* - Reads shipped defaults LIVE from the agent sources —
|
|
443
|
-
* dist/agents/ preferred over src/assets/agents/. An agent
|
|
444
|
-
* is reported through the warning channel rather than
|
|
445
|
-
* with no explanation.
|
|
606
|
+
* - Reads shipped defaults (model AND effort) LIVE from the agent sources —
|
|
607
|
+
* agentSourceDirs(), dist/agents/ preferred over src/assets/agents/. An agent
|
|
608
|
+
* no source supplies is reported through the warning channel rather than
|
|
609
|
+
* passing as 'unchanged' with no explanation. D-SHIPPED-EFFORT: an agent that
|
|
610
|
+
* ships an effort keeps it across a reapply unless its mapping says otherwise.
|
|
446
611
|
* - Gets the agent name list from the registry (getAllAgentNames()) plus
|
|
447
612
|
* any mapping entries for agents not in the registry.
|
|
448
613
|
* - Missing installed files → skip silently (recorded in skippedMissing).
|
|
@@ -461,11 +626,11 @@ export async function reapplyAgentMapping(opts) {
|
|
|
461
626
|
return { updated: [], unchanged: [], skippedMissing: [], invalidMapping: [], warnings };
|
|
462
627
|
}
|
|
463
628
|
const mapping = mappingResult.value;
|
|
464
|
-
const shippedDefaults = await
|
|
629
|
+
const shippedDefaults = await loadShippedAgentDefaults(opts.agentSourceDirs, { onWarning: warn });
|
|
465
630
|
// Build the union of: all registered agent names + all names in the mapping
|
|
466
631
|
// (so agents not yet in the registry but configured are also processed).
|
|
467
632
|
const registryNames = new Set(getAllAgentNames());
|
|
468
|
-
const mappingNames = new Set(Object.keys(mapping
|
|
633
|
+
const mappingNames = new Set(Object.keys(agentOnlyMapping(mapping)));
|
|
469
634
|
const allNames = new Set([...registryNames, ...mappingNames]);
|
|
470
635
|
// Stable iteration order — Set preserves insertion order, array fixes it for the map.
|
|
471
636
|
const allNamesList = [...allNames];
|
|
@@ -589,7 +754,7 @@ export async function revertExternalAgents(opts) {
|
|
|
589
754
|
*/
|
|
590
755
|
export function countExternalMappedAgents(mapping) {
|
|
591
756
|
let count = 0;
|
|
592
|
-
for (const entry of Object.values(mapping
|
|
757
|
+
for (const entry of Object.values(agentOnlyMapping(mapping))) {
|
|
593
758
|
if (isDormantExternalModel(entry.model, false)) {
|
|
594
759
|
count++;
|
|
595
760
|
}
|
package/dist/core/agent-state.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Agent
|
|
2
|
+
* Agent row vocabulary shared by `devflow agents --list` and the TUI.
|
|
3
3
|
*
|
|
4
|
-
* Single source of truth for the STATE column
|
|
5
|
-
* Centralised here (core layer) so neither
|
|
6
|
-
* cli/agents-view owns the vocabulary.
|
|
4
|
+
* Single source of truth for the STATE column and the EFFORT cell, so neither
|
|
5
|
+
* surface can drift from the other. Centralised here (core layer) so neither
|
|
6
|
+
* cli/commands nor cli/agents-view owns the vocabulary.
|
|
7
7
|
*
|
|
8
8
|
* Pure core-layer module, no CLI-adapter concerns.
|
|
9
9
|
*/
|
|
@@ -22,13 +22,35 @@ export const AGENT_STATE_LABELS = {
|
|
|
22
22
|
'saved-inactive': 'saved-inactive',
|
|
23
23
|
'not-installed': 'not installed',
|
|
24
24
|
'unknown': 'unknown',
|
|
25
|
+
'worker': 'worker',
|
|
25
26
|
};
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// formatEffortDisplay
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/**
|
|
31
|
+
* The text of an EFFORT cell, shared by `--list` and the TUI.
|
|
32
|
+
*
|
|
33
|
+
* D-SHIPPED-EFFORT: a configured level or `inherit` is shown as is. An
|
|
34
|
+
* unconfigured row shows `default (<shipped effort>)` when the shipped source
|
|
35
|
+
* carries an effort — the same `default (shippedDefault)` convention the MODEL
|
|
36
|
+
* cell uses — and plain `default` when it carries none.
|
|
37
|
+
*
|
|
38
|
+
* Pure function, no I/O.
|
|
39
|
+
*
|
|
40
|
+
* @param configured - The row's effort: a level, `inherit`, or `default` when unset.
|
|
41
|
+
* @param shippedEffort - The effort the shipped source carries, if any.
|
|
42
|
+
*/
|
|
43
|
+
export function formatEffortDisplay(configured, shippedEffort) {
|
|
44
|
+
if (configured !== 'default')
|
|
45
|
+
return configured;
|
|
46
|
+
return shippedEffort === undefined ? 'default' : `default (${shippedEffort})`;
|
|
47
|
+
}
|
|
26
48
|
/**
|
|
27
49
|
* Classify an agent row's install state.
|
|
28
50
|
*
|
|
29
51
|
* Single source of truth shared by `--list` and the TUI so the two surfaces
|
|
30
52
|
* cannot drift. The four-way result drives the STATE column in render.ts and
|
|
31
|
-
* the STATE column in --list output.
|
|
53
|
+
* the STATE column in --list output. Worker rows never come through here.
|
|
32
54
|
*
|
|
33
55
|
* Pure function, no I/O.
|
|
34
56
|
*/
|
package/dist/core/assets.js
CHANGED
|
@@ -75,7 +75,7 @@ export function compiledSkillRefsDir(root = getPackageRoot()) {
|
|
|
75
75
|
* The single owner of the dist-first agent-resolution policy: a generator
|
|
76
76
|
* host's compiled artifact in dist/agents/ supersedes a hand-authored file of
|
|
77
77
|
* the same name in src/assets/agents/. Every consumer reads the order from
|
|
78
|
-
* here — the installer's first-hit-wins resolve,
|
|
78
|
+
* here — the installer's first-hit-wins resolve, loadShippedAgentDefaults's
|
|
79
79
|
* first-wins merge, and the test harness's resolveAgentSource — so the
|
|
80
80
|
* convention is stated once and cannot drift apart between call sites.
|
|
81
81
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
import { promises as fs } from 'fs';
|
|
3
|
+
import { firstSymbolicLink } from './linked-path.js';
|
|
3
4
|
import { getFeatureConfigPath } from './project-paths.js';
|
|
4
5
|
import { parseTrackerId } from './tracker.js';
|
|
5
6
|
import { loadProjectConfigLib } from './evidence-policy.js';
|
|
@@ -169,17 +170,64 @@ async function readConfigBody(projectRoot, lib = loadProjectConfigLib()) {
|
|
|
169
170
|
}
|
|
170
171
|
return classifyConfigBytes(read.bytes, lib.value);
|
|
171
172
|
}
|
|
173
|
+
/** A thrown value's message, or the value itself when it is not an Error. */
|
|
174
|
+
function messageOf(err) {
|
|
175
|
+
return err instanceof Error ? err.message : String(err);
|
|
176
|
+
}
|
|
172
177
|
/**
|
|
173
|
-
* Serialise a config body to a project's config file.
|
|
178
|
+
* Serialise a config body to a project's config file. Never throws.
|
|
174
179
|
* Creates the .devflow/ directory if missing.
|
|
175
180
|
* Uses an atomic temp+rename pattern to prevent partial reads under concurrent writes.
|
|
181
|
+
* The copy is created only where nothing stands ('wx'), so an entry a repository
|
|
182
|
+
* planted at its name — a symbolic link among them — is never written through
|
|
183
|
+
* (D-CLI-NO-SYMLINK) and, not being this run's, is left where it is; the write then
|
|
184
|
+
* fails and the Result says so. Once this run has created the copy, a write, close or
|
|
185
|
+
* rename that fails removes it again, so a failed write leaves nothing beside the
|
|
186
|
+
* config; the Result names the failure, and the copy too when it could not be removed.
|
|
176
187
|
*/
|
|
177
188
|
async function writeConfigBody(projectRoot, body) {
|
|
178
189
|
const configPath = getFeatureConfigPath(projectRoot);
|
|
179
|
-
await fs.mkdir(path.join(projectRoot, '.devflow'), { recursive: true });
|
|
180
190
|
const tmpPath = configPath + '.tmp.' + process.pid;
|
|
181
|
-
|
|
182
|
-
|
|
191
|
+
let text;
|
|
192
|
+
let copy;
|
|
193
|
+
try {
|
|
194
|
+
text = JSON.stringify(body, null, 2) + '\n';
|
|
195
|
+
await fs.mkdir(path.join(projectRoot, '.devflow'), { recursive: true });
|
|
196
|
+
copy = await fs.open(tmpPath, 'wx', 0o600);
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
return { ok: false, detail: messageOf(err) };
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
try {
|
|
203
|
+
// The open handle is fs.writeFile's destination and the text its data: the same
|
|
204
|
+
// write as copy.writeFile(text), in the form a path-traversal scan reads right,
|
|
205
|
+
// since it takes a writeFile's first argument for a path.
|
|
206
|
+
await fs.writeFile(copy, text, 'utf-8');
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
await copy.close();
|
|
210
|
+
}
|
|
211
|
+
await fs.rename(tmpPath, configPath);
|
|
212
|
+
return { ok: true };
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
return { ok: false, detail: await removeCopy(tmpPath, messageOf(err)) };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Remove the copy a failed config write created, and say why the write failed:
|
|
220
|
+
* `detail`, followed by the removal's own failure when the copy stays. A copy that
|
|
221
|
+
* is already gone counts as removed (`force`).
|
|
222
|
+
*/
|
|
223
|
+
async function removeCopy(tmpPath, detail) {
|
|
224
|
+
try {
|
|
225
|
+
await fs.rm(tmpPath, { force: true });
|
|
226
|
+
return detail;
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
return `${detail}; its copy ${tmpPath} could not be removed: ${messageOf(err)}`;
|
|
230
|
+
}
|
|
183
231
|
}
|
|
184
232
|
/**
|
|
185
233
|
* Merge devflow's managed keys over the config body the file already holds.
|
|
@@ -227,21 +275,31 @@ export function mergeManagedConfig(existing, managed) {
|
|
|
227
275
|
* change. Acceptable because init is a single-threaded, user-initiated command
|
|
228
276
|
* and the window is milliseconds on a local filesystem; the file swap itself is
|
|
229
277
|
* atomic (temp + rename), so a reader never sees a partial file.
|
|
278
|
+
*
|
|
279
|
+
* D-CLI-NO-SYMLINK (firstSymbolicLink): a `.devflow` that is a symbolic link is
|
|
280
|
+
* left alone, and the Result says so; the file is neither read nor written there.
|
|
230
281
|
*/
|
|
231
282
|
export async function writeManagedConfig(projectRoot, managed, lib = loadProjectConfigLib()) {
|
|
232
283
|
const configPath = getFeatureConfigPath(projectRoot);
|
|
284
|
+
let linked;
|
|
285
|
+
try {
|
|
286
|
+
linked = await firstSymbolicLink([path.dirname(configPath)]);
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
return { ok: false, error: { kind: 'unreadable', path: configPath, detail: messageOf(err) } };
|
|
290
|
+
}
|
|
291
|
+
if (linked !== null) {
|
|
292
|
+
return { ok: false, error: { kind: 'unreadable', path: configPath, detail: `${linked} is a symbolic link, and devflow writes nothing through one` } };
|
|
293
|
+
}
|
|
233
294
|
const existing = await readConfigBody(projectRoot, lib);
|
|
234
295
|
if (existing.kind === 'malformed')
|
|
235
296
|
return { ok: false, error: { kind: 'malformed', path: configPath } };
|
|
236
297
|
if (existing.kind === 'unreadable') {
|
|
237
298
|
return { ok: false, error: { kind: 'unreadable', path: configPath, detail: existing.detail } };
|
|
238
299
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
catch (err) {
|
|
243
|
-
return { ok: false, error: { kind: 'write-failed', path: configPath, detail: err instanceof Error ? err.message : String(err) } };
|
|
244
|
-
}
|
|
300
|
+
const written = await writeConfigBody(projectRoot, mergeManagedConfig(objectOf(existing), managed));
|
|
301
|
+
if (!written.ok)
|
|
302
|
+
return { ok: false, error: { kind: 'write-failed', path: configPath, detail: written.detail } };
|
|
245
303
|
return { ok: true };
|
|
246
304
|
}
|
|
247
305
|
/**
|
package/dist/core/flags.js
CHANGED
|
@@ -399,6 +399,30 @@ export const FLAG_REGISTRY = [
|
|
|
399
399
|
integer: true,
|
|
400
400
|
upstreamDefault: 30,
|
|
401
401
|
},
|
|
402
|
+
{
|
|
403
|
+
// D-FOREGROUND-RUN: every agent runs builds and tests in the foreground under an explicit
|
|
404
|
+
// Bash `timeout` whose ceiling is Claude Code's 600000 ms, or this variable when set. A
|
|
405
|
+
// suite that cannot be split under that ceiling is reported BLOCKED with the remedy
|
|
406
|
+
// `devflow flags --set bash-max-timeout-ms=<ms>`, so this flag is that remedy.
|
|
407
|
+
// The env name was confirmed against Claude Code 2.1.294 (docs/reference/claude-code-flags-probe.md):
|
|
408
|
+
// the binary reads BASH_MAX_TIMEOUT_MS, ignores a non-positive or NaN value, and takes the
|
|
409
|
+
// larger of it and the default timeout. Neutral by default (undefined → manifest null →
|
|
410
|
+
// key deleted). min 600000 = upstream default, so the flag only ever raises the ceiling;
|
|
411
|
+
// max 7200000 (2 h) is the devflow sanity bound.
|
|
412
|
+
id: 'bash-max-timeout-ms',
|
|
413
|
+
label: 'Bash max timeout',
|
|
414
|
+
description: 'Ceiling in milliseconds for a foreground Bash command timeout',
|
|
415
|
+
hint: 'Raises the Bash timeout ceiling past 600000 ms for long-running suites',
|
|
416
|
+
blurb: 'Bash timeout ceiling',
|
|
417
|
+
kind: 'number',
|
|
418
|
+
target: { type: 'env', key: 'BASH_MAX_TIMEOUT_MS' },
|
|
419
|
+
recommended: false,
|
|
420
|
+
defaultValue: undefined,
|
|
421
|
+
min: 600000, // upstream default ceiling: lower values would only shrink it
|
|
422
|
+
max: 7200000, // devflow sanity bound: 2 hours
|
|
423
|
+
integer: true,
|
|
424
|
+
upstreamDefault: 600000,
|
|
425
|
+
},
|
|
402
426
|
{
|
|
403
427
|
// Writes as { command: value } per Claude Code spellcheck setting shape.
|
|
404
428
|
id: 'spellcheck',
|
|
@@ -5,24 +5,23 @@
|
|
|
5
5
|
* --clear` and `--disable` (src/cli/commands/learning.ts) and by the drain
|
|
6
6
|
* `devflow init` runs when learning is switched off (src/cli/commands/init.ts).
|
|
7
7
|
*/
|
|
8
|
-
import
|
|
9
|
-
import { getLearningClaimOwnerPath, getLearningPendingTurnsPath, getLearningPendingTurnsProcessingPath, } from './project-paths.js';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { getLearningClaimOwnerPath, getLearningDir, getLearningPendingTurnsPath, getLearningPendingTurnsProcessingPath, } from './project-paths.js';
|
|
10
|
+
import { drainQueueFiles } from './queue-drain.js';
|
|
10
11
|
/**
|
|
11
12
|
* Drain the learning (decisions-detection) pending-turns queue, its claimed
|
|
12
13
|
* batch and the claim's owner file so stale turns don't process later — used by
|
|
13
14
|
* both `--clear` and `--disable`. A mid-run Learning agent whose claimed batch
|
|
14
15
|
* vanishes aborts without changes, which is the desired outcome in both cases.
|
|
15
|
-
*
|
|
16
|
+
* Refused, deleting nothing, when `.devflow` or `.devflow/learning` under `gitRoot`
|
|
17
|
+
* is a symbolic link (D-CLI-NO-SYMLINK). ENOENT-tolerant; other errors propagate.
|
|
16
18
|
*/
|
|
17
19
|
export async function drainLearningQueue(gitRoot) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
unlinkIfPresent(getLearningPendingTurnsPath(gitRoot)),
|
|
24
|
-
unlinkIfPresent(getLearningPendingTurnsProcessingPath(gitRoot)),
|
|
25
|
-
unlinkIfPresent(getLearningClaimOwnerPath(gitRoot)),
|
|
20
|
+
const learningDir = getLearningDir(gitRoot);
|
|
21
|
+
return drainQueueFiles([path.dirname(learningDir), learningDir], [
|
|
22
|
+
getLearningPendingTurnsPath(gitRoot),
|
|
23
|
+
getLearningPendingTurnsProcessingPath(gitRoot),
|
|
24
|
+
getLearningClaimOwnerPath(gitRoot),
|
|
26
25
|
]);
|
|
27
26
|
}
|
|
28
27
|
//# sourceMappingURL=learning-queue-cleanup.js.map
|
|
@@ -6,6 +6,14 @@ const VALID_MODELS = new Set(['opus', 'sonnet', 'haiku']);
|
|
|
6
6
|
function isValidModel(value) {
|
|
7
7
|
return typeof value === 'string' && VALID_MODELS.has(value);
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* D-LEARNING-SHIPPED-ROW: the default model is the model the Learning agent ships
|
|
11
|
+
* with, the `model:` line of src/assets/agents/learning.md. With no layer
|
|
12
|
+
* configuring a model, the spawn directive names none and that line decides, so a
|
|
13
|
+
* different default here would describe a run that never happens.
|
|
14
|
+
* tests/decisions/config.test.ts holds this value equal to the Learning row of the
|
|
15
|
+
* agent-config table, which a change of the shipped tier edits with the frontmatter.
|
|
16
|
+
*/
|
|
9
17
|
const DEFAULTS = {
|
|
10
18
|
model: 'opus',
|
|
11
19
|
debug: false,
|