devflow-kit 3.0.1 → 3.2.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 +49 -0
- package/README.md +1 -1
- package/dist/agents/git.md +2 -2
- package/dist/cli/agents-view/index.js +1 -1
- package/dist/cli/agents-view/render.js +71 -17
- package/dist/cli/agents-view/state.js +42 -16
- package/dist/cli/agents-view/terminal.js +5 -5
- package/dist/cli/commands/agents.js +142 -51
- package/dist/cli/commands/ambient.js +1 -1
- package/dist/cli/commands/attribution-prompts.js +8 -8
- package/dist/cli/commands/capture.js +1 -1
- package/dist/cli/commands/compliance-prompts.js +8 -8
- package/dist/cli/commands/compliance.js +8 -7
- package/dist/cli/commands/flags.js +33 -31
- package/dist/cli/commands/hud.js +1 -1
- package/dist/cli/commands/init-seed.js +9 -9
- package/dist/cli/commands/init.js +162 -85
- package/dist/cli/commands/install-report.js +10 -10
- package/dist/cli/commands/learning.js +302 -136
- package/dist/cli/commands/memory.js +36 -15
- package/dist/cli/commands/proxy.js +23 -23
- package/dist/cli/commands/rules.js +6 -5
- package/dist/cli/commands/tracker-prompts.js +6 -6
- package/dist/cli/commands/tracker.js +9 -9
- package/dist/cli/commands/uninstall.js +183 -59
- package/dist/cli/flags-view/render.js +5 -5
- package/dist/cli/flags-view/state.js +9 -9
- package/dist/cli/flags-view/terminal.js +4 -4
- package/dist/cli/tui/cells.js +1 -1
- package/dist/cli/tui/terminal.js +6 -6
- package/dist/commands/code-review.md +0 -2
- package/dist/commands/debug.md +14 -11
- package/dist/commands/dynamic-build.md +51 -47
- package/dist/commands/dynamic-plan.md +27 -7
- package/dist/commands/dynamic-profile.md +17 -3
- package/dist/commands/dynamic-tickets.md +18 -4
- package/dist/commands/explore.md +9 -3
- package/dist/commands/implement.md +20 -16
- package/dist/commands/plan.md +13 -9
- package/dist/commands/release.md +23 -3
- package/dist/commands/research.md +9 -3
- package/dist/commands/resolve.md +9 -12
- package/dist/commands/self-review.md +0 -2
- package/dist/core/agent-frontmatter.js +28 -3
- package/dist/core/agent-models.js +204 -42
- package/dist/core/agent-state.js +28 -6
- package/dist/core/ansi.js +2 -2
- package/dist/core/assets.js +1 -1
- package/dist/core/cache.js +7 -8
- package/dist/core/codex-auth-inspect.js +4 -4
- package/dist/core/compliance-compose.js +3 -3
- package/dist/core/compliance.js +3 -4
- package/dist/core/evidence-policy.js +14 -13
- package/dist/core/external-models.js +1 -1
- package/dist/core/feature-config.js +71 -13
- package/dist/core/feature-switch.js +3 -3
- package/dist/core/flags.js +49 -25
- package/dist/core/fs-atomic.js +6 -7
- package/dist/core/learning-queue-cleanup.js +16 -81
- package/dist/core/learning-store.js +61 -0
- package/dist/core/linked-path.js +46 -0
- package/dist/core/manifest.js +5 -5
- package/dist/core/mds-variants.js +13 -13
- package/dist/core/model-discovery.js +8 -8
- package/dist/core/observations.js +17 -101
- package/dist/core/orphan-sweep.js +4 -4
- package/dist/core/plugins.js +13 -8
- package/dist/core/project-paths.js +9 -13
- package/dist/core/proxy-log.js +8 -8
- package/dist/core/proxy-state.js +3 -3
- package/dist/core/queue-drain.js +31 -0
- package/dist/core/reference-sweep.js +6 -6
- package/dist/core/teammate-mode-cleanup.js +1 -1
- package/dist/core/tracker.js +14 -14
- package/dist/hud/colors.js +2 -2
- package/dist/hud/components/learning-counts.js +54 -22
- package/dist/hud/components/version-badge.js +1 -1
- package/dist/skills/git/references/pr/resolve-review-threads.md +2 -2
- package/dist/skills/git/references/tracker/github/create-release.md +2 -2
- package/dist/skills/git/references/tracker/jira/create-release.md +2 -2
- package/dist/skills/git/references/tracker/linear/create-release.md +2 -2
- package/dist/targets/claude-code/compliance-install.js +17 -15
- package/dist/targets/claude-code/hooks.js +2 -2
- package/dist/targets/claude-code/installer.js +59 -32
- package/dist/targets/claude-code/legacy.js +1 -1
- package/dist/targets/claude-code/post-install.js +135 -45
- package/dist/targets/claude-code/tracker-install.js +2 -2
- package/package.json +1 -1
- package/src/assets/agents/code.md +15 -21
- package/src/assets/agents/design.md +4 -2
- package/src/assets/agents/diagnose.md +3 -1
- package/src/assets/agents/evaluate.md +4 -0
- package/src/assets/agents/git.mds +2 -2
- package/src/assets/agents/knowledge.md +5 -3
- package/src/assets/agents/learning.md +281 -196
- package/src/assets/agents/research.md +3 -1
- package/src/assets/agents/review.md +5 -3
- package/src/assets/agents/scrutinize.md +5 -1
- package/src/assets/agents/simplify.md +4 -0
- package/src/assets/agents/skim.md +4 -2
- package/src/assets/agents/synthesize.md +6 -0
- package/src/assets/agents/test.md +18 -10
- package/src/assets/agents/triage.md +11 -9
- package/src/assets/agents/validate.md +14 -10
- package/src/assets/commands/_partials/_decisions.mds +8 -3
- package/src/assets/commands/_partials/_docs_root.mds +3 -3
- package/src/assets/commands/_partials/_engine.mds +16 -32
- package/src/assets/commands/_partials/_knowledge.mds +0 -2
- package/src/assets/commands/_partials/_preamble.mds +6 -2
- package/src/assets/commands/_partials/_settings.mds +2 -2
- package/src/assets/commands/_partials/_tracker.mds +1 -1
- package/src/assets/commands/code-review.mds +0 -2
- package/src/assets/commands/debug.mds +13 -8
- package/src/assets/commands/dynamic-build.mds +18 -12
- package/src/assets/commands/dynamic-plan.mds +10 -4
- package/src/assets/commands/dynamic-profile.mds +1 -1
- package/src/assets/commands/dynamic-tickets.mds +2 -2
- package/src/assets/commands/explore.mds +9 -1
- package/src/assets/commands/implement.mds +19 -13
- package/src/assets/commands/plan.mds +12 -8
- package/src/assets/commands/release.md +23 -3
- package/src/assets/commands/research.mds +9 -3
- package/src/assets/commands/resolve.mds +9 -10
- package/src/assets/mds/git/_pr.mds +3 -3
- package/src/assets/mds/tracker/_common.mds +1 -1
- package/src/assets/mds/tracker/_github.mds +3 -3
- package/src/assets/mds/tracker/_jira.mds +3 -3
- package/src/assets/mds/tracker/_linear.mds +3 -3
- package/src/assets/mds/tracker/_mcp.mds +6 -5
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -2
- package/src/assets/scripts/hooks/background-memory-update +97 -33
- 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 +5 -20
- package/src/assets/scripts/hooks/ensure-devflow-init +14 -2
- package/src/assets/scripts/hooks/ensure-proxy +5 -6
- package/src/assets/scripts/hooks/ensure-root-gitignore +123 -11
- package/src/assets/scripts/hooks/git-marker +71 -0
- package/src/assets/scripts/hooks/is-hex-sha +1 -1
- package/src/assets/scripts/hooks/json-helper.cjs +345 -944
- package/src/assets/scripts/hooks/json-parse +25 -129
- package/src/assets/scripts/hooks/lib/decisions-format.cjs +205 -156
- package/src/assets/scripts/hooks/lib/learning-store.cjs +3207 -0
- package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +7 -5
- package/src/assets/scripts/hooks/lib/project-paths.cjs +13 -19
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +253 -226
- 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 +55 -23
- package/src/assets/scripts/hooks/resolve-project-root +3 -4
- package/src/assets/scripts/hooks/session-start-context +146 -45
- package/src/assets/scripts/hooks/session-start-memory +33 -11
- package/src/assets/scripts/lib/project-config.cjs +2 -2
- package/src/assets/scripts/pr-evidence.cjs +3 -3
- package/src/assets/scripts/redact-secrets.cjs +20 -20
- package/src/assets/scripts/release-trace.cjs +1 -1
- package/src/assets/scripts/resolve-evidence-policy.cjs +3 -3
- package/src/assets/scripts/resolve-settings.cjs +3 -3
- package/src/assets/scripts/verify-evidence.cjs +2 -2
- package/src/assets/skills/apply-decisions/SKILL.md +37 -17
- package/src/assets/skills/docs-framework/SKILL.md +2 -2
- package/src/assets/skills/feature-knowledge/SKILL.md +6 -5
- package/src/assets/skills/test-driven-development/SKILL.md +6 -4
- package/dist/core/observation-io.js +0 -50
- package/src/assets/scripts/hooks/decisions-usage-scan.cjs +0 -131
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Agent model mapping engine — schema, persistence, and convergence for the
|
|
3
3
|
* per-agent model configuration feature.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Pure core-layer module, no Claude Code adapter concerns.
|
|
6
|
+
* All fallible operations return Result, no process.exit().
|
|
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';
|
|
@@ -48,9 +48,93 @@ export const EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
|
48
48
|
/**
|
|
49
49
|
* Membership set typed as ReadonlySet<string> so .has() accepts a plain
|
|
50
50
|
* string argument without a cast — TypeScript's Set<T>.has() requires T, and
|
|
51
|
-
* EFFORT_LEVELS[number] is a literal union, not string
|
|
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, which
|
|
87
|
+
* replaces the plan's Haiku default. The worker does not read `agents.memory`
|
|
88
|
+
* yet. Until it does, background-memory-update names this model as a literal in
|
|
89
|
+
* its `claude -p --model` argument, and tests/agent-models-worker.test.ts holds
|
|
90
|
+
* that literal equal to this row so the two cannot drift apart. Wiring the
|
|
91
|
+
* worker to this map, and `--effort` behind a CLI version probe, is a separate
|
|
92
|
+
* change.
|
|
93
|
+
*/
|
|
94
|
+
export const WORKER_AGENTS = {
|
|
95
|
+
memory: { model: 'claude-sonnet-5-5', effort: 'high' },
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* True when `name` is a worker key. Own-property check, so a hostile key such as
|
|
99
|
+
* `constructor` or `__proto__` is never answered from the prototype chain.
|
|
100
|
+
*/
|
|
101
|
+
export function isWorkerAgent(name) {
|
|
102
|
+
return Object.hasOwn(WORKER_AGENTS, name);
|
|
103
|
+
}
|
|
104
|
+
/** A full Claude model identifier: the `claude-` prefix and at least one more character. */
|
|
105
|
+
const CLAUDE_FULL_ID_RE = /^claude-.+$/;
|
|
106
|
+
/**
|
|
107
|
+
* D-WORKER-AGENTS: the worker value domain, owned here once and shared by
|
|
108
|
+
* `validateSetArgs` (the write boundary) and `readAgentMapping` (the read
|
|
109
|
+
* boundary), so a hand-edited file and a CLI argument are held to one rule.
|
|
110
|
+
*
|
|
111
|
+
* - model: `default`, a CLAUDE_MODEL_ALIASES alias, or a full identifier
|
|
112
|
+
* starting `claude-`. Anything else is rejected, `inherit` included
|
|
113
|
+
* (a worker has no session model to inherit). External models are
|
|
114
|
+
* rejected too: the worker runs `claude -p` outside a session and
|
|
115
|
+
* nothing here verifies it routes through the proxy, so allowing
|
|
116
|
+
* them later is a deliberate change to this function.
|
|
117
|
+
* - effort: `default` or an EFFORT_LEVELS level. `inherit` is rejected because
|
|
118
|
+
* a worker has no session effort to inherit.
|
|
119
|
+
*
|
|
120
|
+
* Pure function — no catalog lookup, no I/O.
|
|
121
|
+
*/
|
|
122
|
+
export function validateWorkerValue(field, value) {
|
|
123
|
+
if (field === 'effort') {
|
|
124
|
+
if (value === 'default' || isEffortLevel(value))
|
|
125
|
+
return Ok(undefined);
|
|
126
|
+
return Err(`Invalid effort "${value}" for a worker. Valid: default, ${EFFORT_LEVELS.join(', ')} ` +
|
|
127
|
+
`(a worker has no session effort to inherit)`);
|
|
128
|
+
}
|
|
129
|
+
const inDomain = isValidModelName(value) &&
|
|
130
|
+
(value === 'default' ||
|
|
131
|
+
CLAUDE_MODEL_ALIASES.includes(value) ||
|
|
132
|
+
CLAUDE_FULL_ID_RE.test(value));
|
|
133
|
+
if (inDomain)
|
|
134
|
+
return Ok(undefined);
|
|
135
|
+
return Err(`Invalid model "${value}" for a worker. Valid: default, ${CLAUDE_MODEL_ALIASES.join(', ')}, ` +
|
|
136
|
+
`or a full claude- identifier (external models are not supported for workers)`);
|
|
137
|
+
}
|
|
54
138
|
// ---------------------------------------------------------------------------
|
|
55
139
|
// Key migration
|
|
56
140
|
// ---------------------------------------------------------------------------
|
|
@@ -216,6 +300,34 @@ export async function parseAgentMappingEnvelope(filePath) {
|
|
|
216
300
|
}
|
|
217
301
|
return { kind: 'ok', envelope, rawAgents: rawAgents };
|
|
218
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* D-WORKER-AGENTS: the agent-only subset of the mapping — every entry whose key
|
|
305
|
+
* is not a worker. Unknown names stay: the file preserves entries for agents
|
|
306
|
+
* whose plugin is not installed.
|
|
307
|
+
*
|
|
308
|
+
* This is the one definition of "the agents in the mapping". The reapply walk,
|
|
309
|
+
* the init reapply gate, countExternalMappedAgents and the TUI's orphan rows all
|
|
310
|
+
* call it, so a worker entry can never be mistaken for an agent with no
|
|
311
|
+
* installed file.
|
|
312
|
+
*
|
|
313
|
+
* Pure function — returns a new record, never mutates the mapping.
|
|
314
|
+
*/
|
|
315
|
+
export function agentOnlyMapping(mapping) {
|
|
316
|
+
const agents = {};
|
|
317
|
+
for (const [name, entry] of Object.entries(mapping.agents)) {
|
|
318
|
+
if (!isWorkerAgent(name))
|
|
319
|
+
agents[name] = entry;
|
|
320
|
+
}
|
|
321
|
+
return agents;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* True when the mapping holds at least one agent entry. The `devflow init`
|
|
325
|
+
* reapply gate: an agents.memory-only mapping keeps it closed, because there is
|
|
326
|
+
* no agent file for a reapply to converge.
|
|
327
|
+
*/
|
|
328
|
+
export function hasAgentMappingEntries(mapping) {
|
|
329
|
+
return Object.keys(agentOnlyMapping(mapping)).length > 0;
|
|
330
|
+
}
|
|
219
331
|
/**
|
|
220
332
|
* Read and tolerantly parse ~/.devflow/agent-models.json.
|
|
221
333
|
* Returns an empty mapping when the file is missing.
|
|
@@ -245,24 +357,39 @@ export async function readAgentMapping(devflowDir, opts) {
|
|
|
245
357
|
continue;
|
|
246
358
|
const raw = entry;
|
|
247
359
|
const mapping = {};
|
|
360
|
+
// D-WORKER-AGENTS: a worker key is held to the worker value domain on read,
|
|
361
|
+
// by the same function the CLI write boundary uses.
|
|
362
|
+
const worker = isWorkerAgent(name);
|
|
248
363
|
if (typeof raw.model === 'string') {
|
|
249
364
|
// Tighten to the same charset used by rewriteAgentFrontmatter.
|
|
250
365
|
// The effort field is enum-validated below; model must be equally strict.
|
|
251
366
|
// An invalid entry is dropped with a warning rather than silently
|
|
252
367
|
// persisting and permanently poisoning that agent on every reapply.
|
|
253
|
-
if (isValidModelName(raw.model)) {
|
|
254
|
-
|
|
368
|
+
if (!isValidModelName(raw.model)) {
|
|
369
|
+
warn(`agent-models: invalid-model name for agent "${name}" — dropping entry`);
|
|
255
370
|
}
|
|
256
371
|
else {
|
|
257
|
-
|
|
372
|
+
const workerCheck = worker ? validateWorkerValue('model', raw.model) : undefined;
|
|
373
|
+
if (workerCheck !== undefined && !workerCheck.ok) {
|
|
374
|
+
warn(`agent-models: dropping out-of-domain model "${raw.model}" for worker "${name}" — ${workerCheck.error}`);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
mapping.model = raw.model;
|
|
378
|
+
}
|
|
258
379
|
}
|
|
259
380
|
}
|
|
260
381
|
if (typeof raw.effort === 'string') {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
382
|
+
// `inherit` is a stored agent value; a worker has no session effort to
|
|
383
|
+
// inherit, so for a worker it is out of domain like any other non-level.
|
|
384
|
+
if (isEffortLevel(raw.effort)) {
|
|
264
385
|
mapping.effort = raw.effort;
|
|
265
386
|
}
|
|
387
|
+
else if (raw.effort === EFFORT_INHERIT && !worker) {
|
|
388
|
+
mapping.effort = raw.effort;
|
|
389
|
+
}
|
|
390
|
+
else if (worker) {
|
|
391
|
+
warn(`agent-models: dropping out-of-domain effort "${raw.effort}" for worker "${name}"`);
|
|
392
|
+
}
|
|
266
393
|
else {
|
|
267
394
|
warn(`agent-models: dropping invalid effort "${raw.effort}" for agent "${name}"`);
|
|
268
395
|
}
|
|
@@ -288,7 +415,7 @@ export async function readAgentMapping(devflowDir, opts) {
|
|
|
288
415
|
* A missing directory returns an empty set rather than throwing (ENOENT is
|
|
289
416
|
* not an error — the install dir may not exist on a fresh machine before
|
|
290
417
|
* `devflow init` runs). Any other OS error also returns an empty set
|
|
291
|
-
* (degrade-not-throw
|
|
418
|
+
* (degrade-not-throw) — a transient or misconfigured path must
|
|
292
419
|
* not prevent the TUI or --list from starting.
|
|
293
420
|
*
|
|
294
421
|
* @param installDir - Path to ~/.claude/agents/devflow (or equivalent).
|
|
@@ -330,41 +457,49 @@ export async function saveAgentMapping(devflowDir, mapping) {
|
|
|
330
457
|
* isDormantExternalModel) AND proxyEnabled is false → the entry is DORMANT.
|
|
331
458
|
* The shipped default model is used instead. The entry remains saved.
|
|
332
459
|
*
|
|
333
|
-
*
|
|
460
|
+
* D-SHIPPED-EFFORT — effort rule, in order:
|
|
461
|
+
* 1. a mapping effort level wins;
|
|
462
|
+
* 2. a mapping `inherit` yields no effort line and does NOT fall back to the
|
|
463
|
+
* shipped effort;
|
|
464
|
+
* 3. an absent mapping effort falls back to the shipped effort, which is
|
|
465
|
+
* undefined when the agent ships none.
|
|
466
|
+
* Effort is ALWAYS applied regardless of proxy state, a dormant external
|
|
467
|
+
* model included.
|
|
334
468
|
*
|
|
335
469
|
* Pure function — no I/O.
|
|
336
470
|
*
|
|
337
471
|
* @param agentName - The agent's short name (e.g., 'code').
|
|
338
472
|
* @param mapping - The full mapping file.
|
|
339
|
-
* @param shippedDefaults - Map of agent name → shipped
|
|
473
|
+
* @param shippedDefaults - Map of agent name → shipped model and effort.
|
|
340
474
|
* @param proxyEnabled - Whether the Devflow proxy is currently active.
|
|
341
475
|
*/
|
|
342
476
|
export function resolveEffective(agentName, mapping, shippedDefaults, proxyEnabled) {
|
|
343
477
|
const entry = mapping.agents[agentName];
|
|
478
|
+
const shipped = shippedDefaults[agentName];
|
|
344
479
|
let model;
|
|
345
480
|
if (entry?.model !== undefined) {
|
|
346
481
|
// Dormant: external model configured but proxy is off → fall back to shipped default.
|
|
347
482
|
model = isDormantExternalModel(entry.model, proxyEnabled)
|
|
348
|
-
?
|
|
483
|
+
? shipped?.model
|
|
349
484
|
: entry.model;
|
|
350
485
|
}
|
|
351
486
|
else {
|
|
352
487
|
// No mapping entry → use shipped default.
|
|
353
|
-
model =
|
|
488
|
+
model = shipped?.model;
|
|
354
489
|
}
|
|
355
|
-
const
|
|
490
|
+
const mapped = entry?.effort;
|
|
491
|
+
const effort = mapped === EFFORT_INHERIT ? undefined : (mapped ?? shipped?.effort);
|
|
356
492
|
return { model, effort };
|
|
357
493
|
}
|
|
358
|
-
// ---------------------------------------------------------------------------
|
|
359
|
-
// loadShippedDefaults
|
|
360
|
-
// ---------------------------------------------------------------------------
|
|
361
494
|
/**
|
|
362
|
-
* Parse the shipped model
|
|
495
|
+
* Parse the shipped model and effort out of every {name}.md in one directory.
|
|
363
496
|
*
|
|
364
497
|
* A missing or unreadable directory yields an empty map — dist/agents/ does not
|
|
365
498
|
* exist until a generator host does, and a source tree that produced no agents
|
|
366
499
|
* is caught by the registry-completeness guard rather than by a throw here.
|
|
367
|
-
* Unknown or malformed files are skipped individually.
|
|
500
|
+
* Unknown or malformed files are skipped individually. The effort is returned as
|
|
501
|
+
* written: only the directory that wins the merge has its effort validated, so a
|
|
502
|
+
* typo in a losing file is never reported.
|
|
368
503
|
*/
|
|
369
504
|
async function readDirDefaults(dir) {
|
|
370
505
|
let entries;
|
|
@@ -380,9 +515,10 @@ async function readDirDefaults(dir) {
|
|
|
380
515
|
return null;
|
|
381
516
|
try {
|
|
382
517
|
const content = await fs.readFile(path.join(dir, file), 'utf-8');
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
518
|
+
const model = readFrontmatterModel(content);
|
|
519
|
+
const effort = readFrontmatterEffort(content);
|
|
520
|
+
if (model.ok && model.value && effort.ok) {
|
|
521
|
+
return [agentName, { model: model.value, effort: effort.value }];
|
|
386
522
|
}
|
|
387
523
|
}
|
|
388
524
|
catch {
|
|
@@ -399,11 +535,22 @@ async function readDirDefaults(dir) {
|
|
|
399
535
|
return defaults;
|
|
400
536
|
}
|
|
401
537
|
/**
|
|
402
|
-
* Load shipped default
|
|
538
|
+
* Load the shipped default model and effort of every agent from the agent files.
|
|
539
|
+
*
|
|
540
|
+
* D-SHIPPED-EFFORT: this is the only production reader of shipped model and
|
|
541
|
+
* effort values. An agent's effort is part of its shipped default, so a reapply
|
|
542
|
+
* that did not see it would strip it (the mapping has no entry to say
|
|
543
|
+
* otherwise).
|
|
403
544
|
*
|
|
404
545
|
* Directories are MOST-PREFERRED FIRST — the convention owned by
|
|
405
546
|
* agentSourceDirs() — and the first directory to supply a name wins, so once an
|
|
406
547
|
* agent is generated into dist/agents/ its frontmatter is the shipped default.
|
|
548
|
+
* First-hit-wins applies to the whole record: an agent's effort is never read
|
|
549
|
+
* from a different file than its model.
|
|
550
|
+
*
|
|
551
|
+
* A shipped `effort:` outside EFFORT_LEVELS (`inherit` included — it is a
|
|
552
|
+
* mapping value, not a shipped one) is dropped with a warning naming the agent;
|
|
553
|
+
* its model survives.
|
|
407
554
|
*
|
|
408
555
|
* A registry agent that no directory supplies is reported through `onWarning`
|
|
409
556
|
* as ONE aggregate message naming every missing agent and the build step. The
|
|
@@ -411,23 +558,37 @@ async function readDirDefaults(dir) {
|
|
|
411
558
|
* keep rendering (`devflow agents --list`), so it warns instead. Staying silent
|
|
412
559
|
* is what makes the gap dangerous: resolveEffective returns an undefined model,
|
|
413
560
|
* reapplyAgentMapping buckets the agent 'unchanged', and disabling the proxy
|
|
414
|
-
* leaves an externally-pinned agent unreverted with nothing said
|
|
561
|
+
* leaves an externally-pinned agent unreverted with nothing said.
|
|
415
562
|
*
|
|
416
563
|
* @param dirs - Agent directories, most-preferred first. Injectable so tests can
|
|
417
564
|
* prove the precedence against a temp tree; all real callers use the default.
|
|
418
565
|
* @param opts - Optional warning channel; the gap is silent without one.
|
|
419
566
|
*/
|
|
420
|
-
export async function
|
|
567
|
+
export async function loadShippedAgentDefaults(dirs = agentSourceDirs(), opts) {
|
|
421
568
|
const perDir = await Promise.all(dirs.map(readDirDefaults));
|
|
422
|
-
const
|
|
569
|
+
const winners = {};
|
|
423
570
|
for (const dirDefaults of perDir) {
|
|
424
|
-
for (const [agentName,
|
|
425
|
-
if (!(agentName
|
|
426
|
-
|
|
571
|
+
for (const [agentName, raw] of Object.entries(dirDefaults)) {
|
|
572
|
+
if (!Object.hasOwn(winners, agentName)) {
|
|
573
|
+
winners[agentName] = raw;
|
|
427
574
|
}
|
|
428
575
|
}
|
|
429
576
|
}
|
|
430
|
-
const
|
|
577
|
+
const defaults = {};
|
|
578
|
+
for (const [agentName, raw] of Object.entries(winners)) {
|
|
579
|
+
if (raw.effort === '') {
|
|
580
|
+
defaults[agentName] = { model: raw.model };
|
|
581
|
+
}
|
|
582
|
+
else if (isEffortLevel(raw.effort)) {
|
|
583
|
+
defaults[agentName] = { model: raw.model, effort: raw.effort };
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
defaults[agentName] = { model: raw.model };
|
|
587
|
+
opts?.onWarning?.(`Shipped agent "${agentName}" declares effort "${raw.effort}", which is not one of ` +
|
|
588
|
+
`${EFFORT_LEVELS.join(', ')} — ignoring it.`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const missing = getAllAgentNames().filter(name => !Object.hasOwn(defaults, name));
|
|
431
592
|
if (missing.length > 0) {
|
|
432
593
|
opts?.onWarning?.(`No shipped default found for declared agent(s): ${missing.join(', ')}. ` +
|
|
433
594
|
`Run \`npm run build:mds\` if they are compiled from .mds generator hosts, otherwise ` +
|
|
@@ -439,10 +600,11 @@ export async function loadShippedDefaults(dirs = agentSourceDirs(), opts) {
|
|
|
439
600
|
* Idempotent convergence function: walk every installed agent file and
|
|
440
601
|
* rewrite frontmatter model/effort to match the effective mapping.
|
|
441
602
|
*
|
|
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.
|
|
603
|
+
* - Reads shipped defaults (model AND effort) LIVE from the agent sources —
|
|
604
|
+
* agentSourceDirs(), dist/agents/ preferred over src/assets/agents/. An agent
|
|
605
|
+
* no source supplies is reported through the warning channel rather than
|
|
606
|
+
* passing as 'unchanged' with no explanation. D-SHIPPED-EFFORT: an agent that
|
|
607
|
+
* ships an effort keeps it across a reapply unless its mapping says otherwise.
|
|
446
608
|
* - Gets the agent name list from the registry (getAllAgentNames()) plus
|
|
447
609
|
* any mapping entries for agents not in the registry.
|
|
448
610
|
* - Missing installed files → skip silently (recorded in skippedMissing).
|
|
@@ -461,11 +623,11 @@ export async function reapplyAgentMapping(opts) {
|
|
|
461
623
|
return { updated: [], unchanged: [], skippedMissing: [], invalidMapping: [], warnings };
|
|
462
624
|
}
|
|
463
625
|
const mapping = mappingResult.value;
|
|
464
|
-
const shippedDefaults = await
|
|
626
|
+
const shippedDefaults = await loadShippedAgentDefaults(opts.agentSourceDirs, { onWarning: warn });
|
|
465
627
|
// Build the union of: all registered agent names + all names in the mapping
|
|
466
628
|
// (so agents not yet in the registry but configured are also processed).
|
|
467
629
|
const registryNames = new Set(getAllAgentNames());
|
|
468
|
-
const mappingNames = new Set(Object.keys(mapping
|
|
630
|
+
const mappingNames = new Set(Object.keys(agentOnlyMapping(mapping)));
|
|
469
631
|
const allNames = new Set([...registryNames, ...mappingNames]);
|
|
470
632
|
// Stable iteration order — Set preserves insertion order, array fixes it for the map.
|
|
471
633
|
const allNamesList = [...allNames];
|
|
@@ -480,7 +642,7 @@ export async function reapplyAgentMapping(opts) {
|
|
|
480
642
|
// Guard: reject mapping keys that would read/write outside the install directory.
|
|
481
643
|
// A corrupted or adversarial agent-models.json could contain path-traversal keys
|
|
482
644
|
// such as '../../etc/passwd'; this check prevents any filesystem access beyond
|
|
483
|
-
// opts.installDir.
|
|
645
|
+
// opts.installDir. Degrade-not-throw: this emits a warning and skips gracefully.
|
|
484
646
|
if (!isContainedIn(opts.installDir, mdFileName(agentName))) {
|
|
485
647
|
localWarn(`reapplyAgentMapping: agent name "${agentName}" resolves outside the install ` +
|
|
486
648
|
`directory — skipped (containment guard)`);
|
|
@@ -589,7 +751,7 @@ export async function revertExternalAgents(opts) {
|
|
|
589
751
|
*/
|
|
590
752
|
export function countExternalMappedAgents(mapping) {
|
|
591
753
|
let count = 0;
|
|
592
|
-
for (const entry of Object.values(mapping
|
|
754
|
+
for (const entry of Object.values(agentOnlyMapping(mapping))) {
|
|
593
755
|
if (isDormantExternalModel(entry.model, false)) {
|
|
594
756
|
count++;
|
|
595
757
|
}
|
package/dist/core/agent-state.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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)
|
|
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
|
*/
|
|
10
10
|
import { isDormantExternalModel } from './external-models.js';
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
@@ -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/ansi.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* feature module (src/hud/). Re-exported verbatim from src/hud/colors.ts
|
|
7
7
|
* so all existing HUD call sites remain untouched.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* src/core/ = agent-neutral logic; ANSI primitives have no
|
|
10
10
|
* feature coupling and belong here, not in a feature module.
|
|
11
|
-
*
|
|
11
|
+
* One shared definition over per-consumer copies.
|
|
12
12
|
*/
|
|
13
13
|
const ESC = '\x1b[';
|
|
14
14
|
const RESET = `${ESC}0m`;
|
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
|
*
|
package/dist/core/cache.js
CHANGED
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
* path.join()'s normalization of ".." components. safeEntryPath() enforces
|
|
16
16
|
* containment and returns null on violation; all callers treat null as a miss.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* Core-layer module, no Claude Code adapter concerns.
|
|
19
|
+
* Entries are written via tmp→rename (writeFileAtomicExclusive), so a reader
|
|
20
|
+
* sees the old entry or the new one, never a missing or partial file.
|
|
20
21
|
*/
|
|
21
22
|
import * as fs from 'node:fs';
|
|
22
23
|
import { promises as fsAsync } from 'node:fs';
|
|
@@ -38,9 +39,9 @@ export const MAX_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
38
39
|
// All callers that write or read the model-discovery catalog AND the uninstall
|
|
39
40
|
// removal target derive their directory paths from these functions. Keeping
|
|
40
41
|
// write-site and removal-site in the same module prevents silent orphaning of
|
|
41
|
-
// cache data on future relocations
|
|
42
|
+
// cache data on future relocations.
|
|
42
43
|
//
|
|
43
|
-
//
|
|
44
|
+
// Path layout owned by the core module, not scattered across
|
|
44
45
|
// callers in src/cli/ or src/hud/.
|
|
45
46
|
/**
|
|
46
47
|
* Returns the model-discovery cache directory for the given devflowDir.
|
|
@@ -137,8 +138,6 @@ export function readCache(cacheDir, key, validate) {
|
|
|
137
138
|
* The single canonical envelope parser — used by readCacheEntry (which adds
|
|
138
139
|
* the expiry check on top) and exported for model-discovery.ts (stale-fallback
|
|
139
140
|
* and prune sorters) so all callers share one parser and cannot drift.
|
|
140
|
-
*
|
|
141
|
-
* applies ADR-003: eliminated private parseEnvelope duplicate; one parser, one truth.
|
|
142
141
|
*/
|
|
143
142
|
export function parseRawEnvelope(raw) {
|
|
144
143
|
let parsed;
|
|
@@ -166,7 +165,7 @@ export function parseRawEnvelope(raw) {
|
|
|
166
165
|
* Write a value to cache with a TTL in milliseconds.
|
|
167
166
|
*
|
|
168
167
|
* - Creates cacheDir at mode 0700 if absent (owner-only access).
|
|
169
|
-
* - Writes the entry via atomic tmp→rename (
|
|
168
|
+
* - Writes the entry via atomic tmp→rename (no delete-then-write window).
|
|
170
169
|
* - Hardens the entry to 0600 after the write (owner-only read/write for cache data
|
|
171
170
|
* that will feed agent frontmatter in later phases).
|
|
172
171
|
* - TTL is clamped to MAX_TTL_MS before storage.
|
|
@@ -196,7 +195,7 @@ export async function writeCache(cacheDir, key, data, ttlMs) {
|
|
|
196
195
|
}
|
|
197
196
|
// Harden entry to 0600 after the atomic write. writeFileAtomicExclusive
|
|
198
197
|
// preserves the existing mode on re-writes; this chmod bootstraps 0600 on
|
|
199
|
-
// the first write to a fresh entry. Best-effort, non-fatal
|
|
198
|
+
// the first write to a fresh entry. Best-effort, non-fatal.
|
|
200
199
|
try {
|
|
201
200
|
await fsAsync.chmod(filePath, 0o600);
|
|
202
201
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Codex credential-file inspection for `devflow proxy --status`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Pure core-layer module — no Claude Code adapter concerns and
|
|
5
|
+
* no presentation (colour/format lives with the other CLI formatters).
|
|
6
6
|
*
|
|
7
7
|
* WHY THIS EXISTS RATHER THAN IMPORTING THE ROUTING RUNTIME
|
|
8
8
|
* The routing runtime validates this same file and exposes an equivalent
|
|
@@ -62,8 +62,8 @@ function jwtAccountId(token) {
|
|
|
62
62
|
/**
|
|
63
63
|
* Classify an I/O error from reading ~/.codex/auth.json into a CodexAuthState.
|
|
64
64
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
65
|
+
* Pure classification logic belongs in src/core/, not in the
|
|
66
|
+
* CLI presentation layer that calls it.
|
|
67
67
|
*
|
|
68
68
|
* ENOENT is the ordinary "not signed in" case: the file has never been written
|
|
69
69
|
* by `codex login`. Every other error (EACCES, EISDIR, EMFILE, …) is a real
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* fragment files, so installed artifacts differ by selection rather than being
|
|
6
6
|
* static all-six blobs.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Pure helpers in src/core/, no I/O.
|
|
9
|
+
* Warn-not-throw for per-item failures.
|
|
10
10
|
*/
|
|
11
11
|
import { COMPLIANCE_FRAMEWORKS, stampComplianceRule } from './compliance.js';
|
|
12
12
|
// ── Constants ──────────────────────────────────────────────────────────────────
|
|
@@ -278,7 +278,7 @@ function buildReferences(activeFrameworks, fragments) {
|
|
|
278
278
|
* Strict boundary parser: validates all required sections (## Mapping / ##
|
|
279
279
|
* Reference / ## Checklist / ## Rule) and their structural constraints.
|
|
280
280
|
* CRLF-tolerant: Windows line endings are normalised before parsing.
|
|
281
|
-
* Never throws
|
|
281
|
+
* Never throws: all errors return { ok: false, error }.
|
|
282
282
|
*/
|
|
283
283
|
export function parseComplianceFragment(id, raw) {
|
|
284
284
|
// CRLF tolerance
|
package/dist/core/compliance.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Core compliance framework registry and utilities.
|
|
3
3
|
*
|
|
4
4
|
* Pure module — no I/O, no side effects.
|
|
5
|
-
*
|
|
5
|
+
* Pure helpers in src/core/, I/O orchestration in src/targets/.
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
8
|
* Canonical compliance framework registry.
|
|
@@ -26,7 +26,7 @@ export const COMPLIANCE_RULE_PLACEHOLDER = '${DEVFLOW_COMPLIANCE_FRAMEWORKS}';
|
|
|
26
26
|
* detection.md — generic detection heuristics
|
|
27
27
|
* sources.md — authoritative source index
|
|
28
28
|
*
|
|
29
|
-
* Exported (
|
|
29
|
+
* Exported (pure constant in src/core/) so both compliance-install.ts
|
|
30
30
|
* (install) and compliance.ts CLI (status/drift detection) share a single
|
|
31
31
|
* definition. Adding a third always-present ref requires only one change here.
|
|
32
32
|
*/
|
|
@@ -60,7 +60,7 @@ function normalizeId(s) {
|
|
|
60
60
|
* This is the trust boundary for framework IDs that did not come through
|
|
61
61
|
* `parseFrameworkList` — most importantly `manifest.features.compliance.frameworks`,
|
|
62
62
|
* which `normalizeComplianceFeature` only type-checks (it cannot reject unknown IDs
|
|
63
|
-
* without violating the
|
|
63
|
+
* without violating the self-heal contract). Every framework ID that is about
|
|
64
64
|
* to become an fs path segment or be written into an installed artifact must pass
|
|
65
65
|
* through here first (AC-35, AC-36).
|
|
66
66
|
*
|
|
@@ -111,7 +111,6 @@ export function parseFrameworkList(input) {
|
|
|
111
111
|
*
|
|
112
112
|
* Absent, null, malformed, or partially-valid → {enabled:false, frameworks:[]}.
|
|
113
113
|
* Preserves valid {enabled: boolean, frameworks: string[]}.
|
|
114
|
-
* (Applies ADR-014 self-heal idiom)
|
|
115
114
|
*/
|
|
116
115
|
export function normalizeComplianceFeature(raw) {
|
|
117
116
|
const DEFAULT = { enabled: false, frameworks: [] };
|