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.
Files changed (166) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +1 -1
  3. package/dist/agents/git.md +2 -2
  4. package/dist/cli/agents-view/index.js +1 -1
  5. package/dist/cli/agents-view/render.js +71 -17
  6. package/dist/cli/agents-view/state.js +42 -16
  7. package/dist/cli/agents-view/terminal.js +5 -5
  8. package/dist/cli/commands/agents.js +142 -51
  9. package/dist/cli/commands/ambient.js +1 -1
  10. package/dist/cli/commands/attribution-prompts.js +8 -8
  11. package/dist/cli/commands/capture.js +1 -1
  12. package/dist/cli/commands/compliance-prompts.js +8 -8
  13. package/dist/cli/commands/compliance.js +8 -7
  14. package/dist/cli/commands/flags.js +33 -31
  15. package/dist/cli/commands/hud.js +1 -1
  16. package/dist/cli/commands/init-seed.js +9 -9
  17. package/dist/cli/commands/init.js +162 -85
  18. package/dist/cli/commands/install-report.js +10 -10
  19. package/dist/cli/commands/learning.js +302 -136
  20. package/dist/cli/commands/memory.js +36 -15
  21. package/dist/cli/commands/proxy.js +23 -23
  22. package/dist/cli/commands/rules.js +6 -5
  23. package/dist/cli/commands/tracker-prompts.js +6 -6
  24. package/dist/cli/commands/tracker.js +9 -9
  25. package/dist/cli/commands/uninstall.js +183 -59
  26. package/dist/cli/flags-view/render.js +5 -5
  27. package/dist/cli/flags-view/state.js +9 -9
  28. package/dist/cli/flags-view/terminal.js +4 -4
  29. package/dist/cli/tui/cells.js +1 -1
  30. package/dist/cli/tui/terminal.js +6 -6
  31. package/dist/commands/code-review.md +0 -2
  32. package/dist/commands/debug.md +14 -11
  33. package/dist/commands/dynamic-build.md +51 -47
  34. package/dist/commands/dynamic-plan.md +27 -7
  35. package/dist/commands/dynamic-profile.md +17 -3
  36. package/dist/commands/dynamic-tickets.md +18 -4
  37. package/dist/commands/explore.md +9 -3
  38. package/dist/commands/implement.md +20 -16
  39. package/dist/commands/plan.md +13 -9
  40. package/dist/commands/release.md +23 -3
  41. package/dist/commands/research.md +9 -3
  42. package/dist/commands/resolve.md +9 -12
  43. package/dist/commands/self-review.md +0 -2
  44. package/dist/core/agent-frontmatter.js +28 -3
  45. package/dist/core/agent-models.js +204 -42
  46. package/dist/core/agent-state.js +28 -6
  47. package/dist/core/ansi.js +2 -2
  48. package/dist/core/assets.js +1 -1
  49. package/dist/core/cache.js +7 -8
  50. package/dist/core/codex-auth-inspect.js +4 -4
  51. package/dist/core/compliance-compose.js +3 -3
  52. package/dist/core/compliance.js +3 -4
  53. package/dist/core/evidence-policy.js +14 -13
  54. package/dist/core/external-models.js +1 -1
  55. package/dist/core/feature-config.js +71 -13
  56. package/dist/core/feature-switch.js +3 -3
  57. package/dist/core/flags.js +49 -25
  58. package/dist/core/fs-atomic.js +6 -7
  59. package/dist/core/learning-queue-cleanup.js +16 -81
  60. package/dist/core/learning-store.js +61 -0
  61. package/dist/core/linked-path.js +46 -0
  62. package/dist/core/manifest.js +5 -5
  63. package/dist/core/mds-variants.js +13 -13
  64. package/dist/core/model-discovery.js +8 -8
  65. package/dist/core/observations.js +17 -101
  66. package/dist/core/orphan-sweep.js +4 -4
  67. package/dist/core/plugins.js +13 -8
  68. package/dist/core/project-paths.js +9 -13
  69. package/dist/core/proxy-log.js +8 -8
  70. package/dist/core/proxy-state.js +3 -3
  71. package/dist/core/queue-drain.js +31 -0
  72. package/dist/core/reference-sweep.js +6 -6
  73. package/dist/core/teammate-mode-cleanup.js +1 -1
  74. package/dist/core/tracker.js +14 -14
  75. package/dist/hud/colors.js +2 -2
  76. package/dist/hud/components/learning-counts.js +54 -22
  77. package/dist/hud/components/version-badge.js +1 -1
  78. package/dist/skills/git/references/pr/resolve-review-threads.md +2 -2
  79. package/dist/skills/git/references/tracker/github/create-release.md +2 -2
  80. package/dist/skills/git/references/tracker/jira/create-release.md +2 -2
  81. package/dist/skills/git/references/tracker/linear/create-release.md +2 -2
  82. package/dist/targets/claude-code/compliance-install.js +17 -15
  83. package/dist/targets/claude-code/hooks.js +2 -2
  84. package/dist/targets/claude-code/installer.js +59 -32
  85. package/dist/targets/claude-code/legacy.js +1 -1
  86. package/dist/targets/claude-code/post-install.js +135 -45
  87. package/dist/targets/claude-code/tracker-install.js +2 -2
  88. package/package.json +1 -1
  89. package/src/assets/agents/code.md +15 -21
  90. package/src/assets/agents/design.md +4 -2
  91. package/src/assets/agents/diagnose.md +3 -1
  92. package/src/assets/agents/evaluate.md +4 -0
  93. package/src/assets/agents/git.mds +2 -2
  94. package/src/assets/agents/knowledge.md +5 -3
  95. package/src/assets/agents/learning.md +281 -196
  96. package/src/assets/agents/research.md +3 -1
  97. package/src/assets/agents/review.md +5 -3
  98. package/src/assets/agents/scrutinize.md +5 -1
  99. package/src/assets/agents/simplify.md +4 -0
  100. package/src/assets/agents/skim.md +4 -2
  101. package/src/assets/agents/synthesize.md +6 -0
  102. package/src/assets/agents/test.md +18 -10
  103. package/src/assets/agents/triage.md +11 -9
  104. package/src/assets/agents/validate.md +14 -10
  105. package/src/assets/commands/_partials/_decisions.mds +8 -3
  106. package/src/assets/commands/_partials/_docs_root.mds +3 -3
  107. package/src/assets/commands/_partials/_engine.mds +16 -32
  108. package/src/assets/commands/_partials/_knowledge.mds +0 -2
  109. package/src/assets/commands/_partials/_preamble.mds +6 -2
  110. package/src/assets/commands/_partials/_settings.mds +2 -2
  111. package/src/assets/commands/_partials/_tracker.mds +1 -1
  112. package/src/assets/commands/code-review.mds +0 -2
  113. package/src/assets/commands/debug.mds +13 -8
  114. package/src/assets/commands/dynamic-build.mds +18 -12
  115. package/src/assets/commands/dynamic-plan.mds +10 -4
  116. package/src/assets/commands/dynamic-profile.mds +1 -1
  117. package/src/assets/commands/dynamic-tickets.mds +2 -2
  118. package/src/assets/commands/explore.mds +9 -1
  119. package/src/assets/commands/implement.mds +19 -13
  120. package/src/assets/commands/plan.mds +12 -8
  121. package/src/assets/commands/release.md +23 -3
  122. package/src/assets/commands/research.mds +9 -3
  123. package/src/assets/commands/resolve.mds +9 -10
  124. package/src/assets/mds/git/_pr.mds +3 -3
  125. package/src/assets/mds/tracker/_common.mds +1 -1
  126. package/src/assets/mds/tracker/_github.mds +3 -3
  127. package/src/assets/mds/tracker/_jira.mds +3 -3
  128. package/src/assets/mds/tracker/_linear.mds +3 -3
  129. package/src/assets/mds/tracker/_mcp.mds +6 -5
  130. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -2
  131. package/src/assets/scripts/hooks/background-memory-update +97 -33
  132. package/src/assets/scripts/hooks/capture-prompt +4 -3
  133. package/src/assets/scripts/hooks/capture-question +4 -3
  134. package/src/assets/scripts/hooks/capture-turn +5 -20
  135. package/src/assets/scripts/hooks/ensure-devflow-init +14 -2
  136. package/src/assets/scripts/hooks/ensure-proxy +5 -6
  137. package/src/assets/scripts/hooks/ensure-root-gitignore +123 -11
  138. package/src/assets/scripts/hooks/git-marker +71 -0
  139. package/src/assets/scripts/hooks/is-hex-sha +1 -1
  140. package/src/assets/scripts/hooks/json-helper.cjs +345 -944
  141. package/src/assets/scripts/hooks/json-parse +25 -129
  142. package/src/assets/scripts/hooks/lib/decisions-format.cjs +205 -156
  143. package/src/assets/scripts/hooks/lib/learning-store.cjs +3207 -0
  144. package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +7 -5
  145. package/src/assets/scripts/hooks/lib/project-paths.cjs +13 -19
  146. package/src/assets/scripts/hooks/lib/render-decisions.cjs +253 -226
  147. package/src/assets/scripts/hooks/memory-worker +10 -0
  148. package/src/assets/scripts/hooks/pre-compact-memory +66 -14
  149. package/src/assets/scripts/hooks/preamble +9 -1
  150. package/src/assets/scripts/hooks/queue-append +55 -23
  151. package/src/assets/scripts/hooks/resolve-project-root +3 -4
  152. package/src/assets/scripts/hooks/session-start-context +146 -45
  153. package/src/assets/scripts/hooks/session-start-memory +33 -11
  154. package/src/assets/scripts/lib/project-config.cjs +2 -2
  155. package/src/assets/scripts/pr-evidence.cjs +3 -3
  156. package/src/assets/scripts/redact-secrets.cjs +20 -20
  157. package/src/assets/scripts/release-trace.cjs +1 -1
  158. package/src/assets/scripts/resolve-evidence-policy.cjs +3 -3
  159. package/src/assets/scripts/resolve-settings.cjs +3 -3
  160. package/src/assets/scripts/verify-evidence.cjs +2 -2
  161. package/src/assets/skills/apply-decisions/SKILL.md +37 -17
  162. package/src/assets/skills/docs-framework/SKILL.md +2 -2
  163. package/src/assets/skills/feature-knowledge/SKILL.md +6 -5
  164. package/src/assets/skills/test-driven-development/SKILL.md +6 -4
  165. package/dist/core/observation-io.js +0 -50
  166. package/src/assets/scripts/hooks/decisions-usage-scan.cjs +0 -131
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * devflow agents — Manage per-agent model/effort assignments.
3
3
  *
4
- * applies ADR-013: CLI-layer module; core logic in src/core/agent-models.ts.
5
- * avoids PF-014: never process.exit() inside a finally-guarded scope (terminal.ts
6
- * handles cleanup in Promise resolve, not process.exit).
4
+ * CLI-layer module; core logic in src/core/agent-models.ts.
5
+ * Never process.exit() inside a finally-guarded scope: it skips the finally
6
+ * (terminal.ts handles cleanup in Promise resolve, not process.exit).
7
7
  *
8
8
  * Branding note: "subswitch" must NEVER appear in user-visible strings.
9
9
  * User-facing vocabulary: "external model routing" / "Devflow proxy" /
@@ -14,14 +14,19 @@
14
14
  * devflow agents --list → tabular list (safe in non-TTY)
15
15
  * devflow agents --set <agent> --model <m> --effort <e>
16
16
  * devflow agents --reset [--yes]
17
+ *
18
+ * D-WORKER-AGENTS: `memory` is a background worker, not an agent, and is
19
+ * settable here as `--set memory` and as a row after the agents in `--list` and
20
+ * the TUI. Its state is always 'worker'; its values are held to the worker
21
+ * domain (Claude models and effort levels only) by validateWorkerValue.
17
22
  */
18
23
  import { Command } from 'commander';
19
24
  import * as path from 'path';
20
25
  import * as p from '@clack/prompts';
21
26
  import color from 'picocolors';
22
- import { EFFORT_LEVELS, LEGACY_AGENT_KEYS, readAgentMapping, saveAgentMapping, reapplyAgentMapping, loadShippedDefaults, readInstalledAgentNames, } from '../../core/agent-models.js';
27
+ import { EFFORT_INHERIT, EFFORT_LEVELS, LEGACY_AGENT_KEYS, WORKER_AGENTS, agentOnlyMapping, isEffortLevel, isWorkerAgent, readAgentMapping, saveAgentMapping, reapplyAgentMapping, loadShippedAgentDefaults, readInstalledAgentNames, validateWorkerValue, } from '../../core/agent-models.js';
23
28
  import { CLAUDE_MODEL_ALIASES, isDormantExternalModel, } from '../../core/external-models.js';
24
- import { classifyAgentState, AGENT_STATE_LABELS, } from '../../core/agent-state.js';
29
+ import { classifyAgentState, formatEffortDisplay, AGENT_STATE_LABELS, } from '../../core/agent-state.js';
25
30
  import { isValidModelName } from '../../core/agent-frontmatter.js';
26
31
  import { isProxyEnabled } from '../../core/proxy-state.js';
27
32
  import { getAllAgentNames } from '../../core/plugins.js';
@@ -47,26 +52,43 @@ function Err(error) {
47
52
  * This preserves the configure-first-then-enable provisioning flow and keeps
48
53
  * `--set` at zero subprocess cost (AC-P9: cache-only, 0 spawns).
49
54
  *
55
+ * D-WORKER-AGENTS: when `agentName` is a worker (`memory`), model and effort are
56
+ * held to the worker value domain instead — validateWorkerValue, the function
57
+ * readAgentMapping applies on read — with no catalog lookup: a worker runs
58
+ * outside any session, so an external model, `inherit` as a model and `inherit`
59
+ * as an effort are all rejected.
60
+ *
61
+ * D-SHIPPED-EFFORT: for an agent, `--effort inherit` is accepted. It stores the
62
+ * sentinel that drops the agent's effort line; `--effort default` deletes the
63
+ * key so the shipped effort applies again.
64
+ *
50
65
  * Returns Err when:
51
66
  * - neither model nor effort is provided
52
67
  * - model is unknown AND catalog is known
53
- * - effort is unknown (not in EFFORT_LEVELS ∪ 'default')
68
+ * - effort is unknown (not in EFFORT_LEVELS ∪ 'default' ∪ 'inherit')
69
+ * - agentName is a worker and a value is outside the worker domain
54
70
  */
55
- export function validateSetArgs(args, catalog = { known: false }) {
71
+ export function validateSetArgs(args, catalog = { known: false }, agentName) {
56
72
  const { model, effort } = args;
57
73
  if (model === undefined && effort === undefined) {
58
74
  return Err('Specify at least one of --model or --effort');
59
75
  }
76
+ const worker = agentName !== undefined && isWorkerAgent(agentName);
60
77
  if (model !== undefined) {
61
78
  // Charset gate: applied at every trust boundary regardless of catalog state.
62
79
  // Rejects injection payloads (newlines, YAML metacharacters) before they can
63
- // propagate to agent-models.json or agent frontmatter. avoids PF-017: allowlist
80
+ // propagate to agent-models.json or agent frontmatter. Allowlist
64
81
  // what the callee actually consumes rather than denylist individual bad chars.
65
82
  if (!isValidModelName(model)) {
66
83
  return Err(`Invalid model name "${model}". Model names must start with an alphanumeric character ` +
67
84
  `and contain only alphanumeric, dot, underscore, or hyphen characters (max 64 chars).`);
68
85
  }
69
- if (catalog.known) {
86
+ if (worker) {
87
+ const check = validateWorkerValue('model', model);
88
+ if (!check.ok)
89
+ return Err(check.error);
90
+ }
91
+ else if (catalog.known) {
70
92
  // Full validation against the discovered catalog.
71
93
  const valid = ['default', ...CLAUDE_MODEL_ALIASES, ...catalog.selectableNames];
72
94
  if (!valid.includes(model)) {
@@ -77,9 +99,16 @@ export function validateSetArgs(args, catalog = { known: false }) {
77
99
  // warning fires at agents.ts call site if the proxy is off.
78
100
  }
79
101
  if (effort !== undefined) {
80
- const valid = ['default', ...EFFORT_LEVELS];
81
- if (!valid.includes(effort)) {
82
- return Err(`Unknown effort "${effort}". Valid: ${valid.join(', ')}`);
102
+ if (worker) {
103
+ const check = validateWorkerValue('effort', effort);
104
+ if (!check.ok)
105
+ return Err(check.error);
106
+ }
107
+ else {
108
+ const valid = ['default', ...EFFORT_LEVELS, EFFORT_INHERIT];
109
+ if (!valid.includes(effort)) {
110
+ return Err(`Unknown effort "${effort}". Valid: ${valid.join(', ')}`);
111
+ }
83
112
  }
84
113
  }
85
114
  return Ok(args);
@@ -106,10 +135,9 @@ export function applySetMapping(mapping, agentName, args) {
106
135
  if (args.effort === 'default') {
107
136
  delete existing.effort;
108
137
  }
109
- else {
110
- // Sound narrowing: validateSetArgs already confirmed args.effort is a valid
111
- // EffortLevel before this function is called. SetArgs.effort is string to keep
112
- // the CLI entry type permissive; the assertion here is not a compensating cast.
138
+ else if (isEffortLevel(args.effort) || args.effort === EFFORT_INHERIT) {
139
+ // SetArgs.effort is a string to keep the CLI entry type permissive;
140
+ // validateSetArgs has already confirmed it is a level or the inherit sentinel.
113
141
  existing.effort = args.effort;
114
142
  }
115
143
  }
@@ -133,23 +161,62 @@ export async function buildListRows(input) {
133
161
  const entry = mapping.agents[name];
134
162
  const configured = entry?.model ?? 'default';
135
163
  const effort = entry?.effort ?? 'default';
136
- const defaultModel = shippedDefaults[name] ?? 'unknown';
164
+ const defaultModel = shippedDefaults[name]?.model ?? 'unknown';
165
+ const shippedEffort = shippedDefaults[name]?.effort;
137
166
  const installed = installedNames.has(name);
138
167
  // inRegistry is always true here — agentNames comes from the registry.
139
168
  const state = classifyAgentState({ configured, proxyEnabled, installed, inRegistry: true });
140
- return { name, defaultModel, configured, effort, state };
169
+ return { name, defaultModel, configured, effort, shippedEffort, state };
141
170
  });
142
171
  return rows;
143
172
  }
173
+ /**
174
+ * Build the list rows of the background workers (D-WORKER-AGENTS), which follow
175
+ * the agent rows in `--list`.
176
+ *
177
+ * A worker has no installed agent file, so its row never consults the install
178
+ * directory: its state is always 'worker', never classified (it can be neither
179
+ * not installed nor dormant — validateSetArgs rejects an external model for a
180
+ * worker). DEFAULT is the model WORKER_AGENTS ships and EFFORT shows the shipped
181
+ * effort while unconfigured, the same convention as an agent row.
182
+ *
183
+ * Pure function, no I/O.
184
+ */
185
+ export function buildWorkerListRows(mapping) {
186
+ return Object.entries(WORKER_AGENTS).map(([name, shipped]) => {
187
+ const entry = mapping.agents[name];
188
+ return {
189
+ name,
190
+ defaultModel: shipped.model,
191
+ configured: entry?.model ?? 'default',
192
+ effort: entry?.effort ?? 'default',
193
+ shippedEffort: shipped.effort,
194
+ state: 'worker',
195
+ };
196
+ });
197
+ }
144
198
  // ---------------------------------------------------------------------------
145
199
  // --list output formatting
146
200
  // ---------------------------------------------------------------------------
201
+ /**
202
+ * Width of the EFFORT column: the longest `default (<level>)` an unconfigured row
203
+ * can show, derived from EFFORT_LEVELS so a new level cannot truncate the cell.
204
+ */
205
+ const EFFORT_COLUMN_WIDTH = Math.max(...EFFORT_LEVELS.map(level => formatEffortDisplay('default', level).length), EFFORT_INHERIT.length);
206
+ /** Narrowest the DEFAULT column of `--list` renders; it widens to fit a longer shipped default. */
207
+ const DEFAULT_COLUMN_MIN_WIDTH = 10;
208
+ /** Narrowest the CONFIGURED column of `--list` renders; it widens to fit a longer configured model. */
209
+ const CONFIGURED_COLUMN_MIN_WIDTH = 16;
147
210
  export function formatListOutput(rows, proxyEnabled) {
148
211
  const lines = [];
149
212
  const AGENT_W = 20;
150
- const DEFAULT_W = 10;
151
- const CONFIGURED_W = 16;
152
- const EFFORT_W = 12;
213
+ // DEFAULT and CONFIGURED grow to the longest value in their column, so a full
214
+ // model identifier (the memory worker ships claude-sonnet-5-5; a user maps
215
+ // claude-sonnet-4-6) is shown whole, not cut to a fixed width and read as a
216
+ // different model.
217
+ const DEFAULT_W = Math.max(DEFAULT_COLUMN_MIN_WIDTH, ...rows.map(row => stripAnsi(row.defaultModel).length));
218
+ const CONFIGURED_W = Math.max(CONFIGURED_COLUMN_MIN_WIDTH, ...rows.map(row => stripAnsi(row.configured).length));
219
+ const EFFORT_W = EFFORT_COLUMN_WIDTH;
153
220
  // Header
154
221
  lines.push([
155
222
  color.gray('AGENT'.padEnd(AGENT_W)),
@@ -176,6 +243,9 @@ export function formatListOutput(rows, proxyEnabled) {
176
243
  case 'unknown':
177
244
  stateStr = color.dim(AGENT_STATE_LABELS['unknown']);
178
245
  break;
246
+ case 'worker':
247
+ stateStr = color.dim(AGENT_STATE_LABELS['worker']);
248
+ break;
179
249
  default: {
180
250
  const _ = row.state;
181
251
  void _;
@@ -188,15 +258,18 @@ export function formatListOutput(rows, proxyEnabled) {
188
258
  stripAnsi(row.name).padEnd(AGENT_W).slice(0, AGENT_W),
189
259
  stripAnsi(row.defaultModel).padEnd(DEFAULT_W).slice(0, DEFAULT_W),
190
260
  stripAnsi(row.configured).padEnd(CONFIGURED_W).slice(0, CONFIGURED_W),
191
- stripAnsi(row.effort).padEnd(EFFORT_W).slice(0, EFFORT_W),
261
+ stripAnsi(formatEffortDisplay(row.effort, row.shippedEffort)).padEnd(EFFORT_W).slice(0, EFFORT_W),
192
262
  stateStr,
193
263
  ].join(' '));
194
264
  }
195
- const installed = rows.filter(r => r.state !== 'not-installed').length;
196
- const configured = rows.filter(r => r.configured !== 'default' || r.effort !== 'default').length;
265
+ // The footer counts agent rows only: a worker row is neither installed nor
266
+ // not installed, and configuring it changes no agent (D-WORKER-AGENTS).
267
+ const agentRows = rows.filter(r => r.state !== 'worker');
268
+ const installed = agentRows.filter(r => r.state !== 'not-installed').length;
269
+ const configured = agentRows.filter(r => r.configured !== 'default' || r.effort !== 'default').length;
197
270
  const proxyLabel = proxyEnabled ? color.green('enabled') : color.yellow('disabled');
198
271
  lines.push('');
199
- lines.push(`${installed}/${rows.length} installed · ${configured} configured · proxy: ${proxyLabel}`);
272
+ lines.push(`${installed}/${agentRows.length} installed · ${configured} configured · proxy: ${proxyLabel}`);
200
273
  return lines.join('\n');
201
274
  }
202
275
  // ---------------------------------------------------------------------------
@@ -241,7 +314,8 @@ async function buildTuiState(agentNames, mapping, shippedDefaults, proxyEnabled,
241
314
  const entry = mapping.agents[name];
242
315
  return buildRow({
243
316
  name,
244
- shippedDefault: shippedDefaults[name] ?? 'unknown',
317
+ shippedDefault: shippedDefaults[name]?.model ?? 'unknown',
318
+ shippedEffort: shippedDefaults[name]?.effort,
245
319
  savedModel: entry?.model,
246
320
  savedEffort: entry?.effort,
247
321
  proxyEnabled,
@@ -251,12 +325,31 @@ async function buildTuiState(agentNames, mapping, shippedDefaults, proxyEnabled,
251
325
  inRegistry: true,
252
326
  });
253
327
  });
254
- // Orphan rows: keys in agent-models.json not present in the registry.
328
+ // Worker rows (D-WORKER-AGENTS) follow the agents. A worker has no installed
329
+ // file and takes only the worker value domain, so it is built with its own
330
+ // shipped defaults and never classified as an orphan (state 'unknown').
331
+ for (const [workerName, shipped] of Object.entries(WORKER_AGENTS)) {
332
+ const entry = mapping.agents[workerName];
333
+ rows.push(buildRow({
334
+ name: workerName,
335
+ shippedDefault: shipped.model,
336
+ shippedEffort: shipped.effort,
337
+ savedModel: entry?.model,
338
+ savedEffort: entry?.effort,
339
+ proxyEnabled,
340
+ installed: false,
341
+ inRegistry: false,
342
+ worker: true,
343
+ }));
344
+ }
345
+ // Orphan rows: agent keys in agent-models.json not present in the registry.
255
346
  // Appended at the end so they are visually separated from known agents.
256
- for (const orphanKey of Object.keys(mapping.agents)) {
347
+ // agentOnlyMapping leaves out the worker keys, which have their own rows above.
348
+ const agentEntries = agentOnlyMapping(mapping);
349
+ for (const orphanKey of Object.keys(agentEntries)) {
257
350
  if (registrySet.has(orphanKey))
258
351
  continue;
259
- const entry = mapping.agents[orphanKey];
352
+ const entry = agentEntries[orphanKey];
260
353
  rows.push(buildRow({
261
354
  name: orphanKey,
262
355
  shippedDefault: 'unknown',
@@ -352,14 +445,15 @@ export const agentsCommand = new Command('agents')
352
445
  .option('--list', 'List all agents with their current configuration')
353
446
  .option('--set <agent>', 'Set model/effort for a specific agent')
354
447
  .option('--model <model>', 'Model to assign (use with --set)')
355
- .option('--effort <level>', 'Effort level to assign (use with --set)')
448
+ .option('--effort <level>', `Effort level to assign (use with --set): ${EFFORT_LEVELS.join(', ')}, ` +
449
+ `default (use the shipped effort), or ${EFFORT_INHERIT} (drop the effort line, follow the session)`)
356
450
  .option('--reset', 'Clear all agent customisations and restore defaults')
357
451
  .option('--yes', 'Skip confirmation prompt (use with --reset)')
358
452
  .action(async (options) => {
359
453
  const claudeDir = getClaudeDirectory();
360
454
  const devflowDir = getDevFlowDirectory();
361
455
  const installDir = path.join(claudeDir, 'agents', 'devflow');
362
- // Cache directory for model discovery — authoritative path from cache.ts (avoids PF-013).
456
+ // Cache directory for model discovery — authoritative path from cache.ts.
363
457
  const cacheDir = modelCacheDir(devflowDir);
364
458
  // Log path mirrors proxy.ts for unified proxy diagnostics.
365
459
  const logPath = path.join(devflowDir, 'logs', 'proxy.log');
@@ -376,20 +470,23 @@ export const agentsCommand = new Command('agents')
376
470
  // An agent with no shipped default renders a blank DEFAULT column and can
377
471
  // never be reverted off an external model; surface the gap rather than
378
472
  // letting the table imply the agent simply ships without one.
379
- const shippedDefaults = await loadShippedDefaults(undefined, {
473
+ const shippedDefaults = await loadShippedAgentDefaults(undefined, {
380
474
  onWarning: (msg) => p.log.warn(msg),
381
475
  });
382
- // ── --list ──────────────────────────────────────────────────────────────
383
- if (options.list) {
384
- const agentNames = getAllAgentNames().sort();
385
- const rows = await buildListRows({
386
- agentNames,
476
+ // Agent rows in registry order, then the worker rows (D-WORKER-AGENTS).
477
+ const loadListRows = async () => {
478
+ const agentRows = await buildListRows({
479
+ agentNames: getAllAgentNames().sort(),
387
480
  mapping,
388
481
  installDir,
389
482
  shippedDefaults,
390
483
  proxyEnabled,
391
484
  });
392
- process.stdout.write(formatListOutput(rows, proxyEnabled) + '\n');
485
+ return [...agentRows, ...buildWorkerListRows(mapping)];
486
+ };
487
+ // ── --list ──────────────────────────────────────────────────────────────
488
+ if (options.list) {
489
+ process.stdout.write(formatListOutput(await loadListRows(), proxyEnabled) + '\n');
393
490
  return;
394
491
  }
395
492
  // ── --reset ─────────────────────────────────────────────────────────────
@@ -447,10 +544,11 @@ export const agentsCommand = new Command('agents')
447
544
  p.log.info(`Agent '${agentName}' has been renamed to '${canonical}' — using canonical name.`);
448
545
  agentName = canonical;
449
546
  }
450
- // Validate agent name
547
+ // Validate agent name. D-WORKER-AGENTS: the workers are settable too, so a
548
+ // first `--set memory` is accepted although nothing in the mapping names it yet.
451
549
  const knownAgents = getAllAgentNames();
452
550
  const knownMapping = Object.keys(mapping.agents);
453
- const allKnown = new Set([...knownAgents, ...knownMapping]);
551
+ const allKnown = new Set([...knownAgents, ...Object.keys(WORKER_AGENTS), ...knownMapping]);
454
552
  if (!allKnown.has(agentName)) {
455
553
  p.log.error(`Unknown agent "${agentName}". Valid: ${[...allKnown].sort().join(', ')}`);
456
554
  process.exitCode = 1;
@@ -461,7 +559,7 @@ export const agentsCommand = new Command('agents')
461
559
  // dormancy warning below fires if needed). This preserves the
462
560
  // configure-first-then-enable provisioning flow.
463
561
  const setCatalog = getExternalModelsCached(cacheDir);
464
- const validation = validateSetArgs({ model: options.model, effort: options.effort }, setCatalog);
562
+ const validation = validateSetArgs({ model: options.model, effort: options.effort }, setCatalog, agentName);
465
563
  if (!validation.ok) {
466
564
  p.log.error(validation.error);
467
565
  process.exitCode = 1;
@@ -510,15 +608,7 @@ export const agentsCommand = new Command('agents')
510
608
  const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
511
609
  if (!isInteractive) {
512
610
  // Non-TTY: print list and exit 1 with note
513
- const agentNames = getAllAgentNames().sort();
514
- const rows = await buildListRows({
515
- agentNames,
516
- mapping,
517
- installDir,
518
- shippedDefaults,
519
- proxyEnabled,
520
- });
521
- process.stdout.write(formatListOutput(rows, proxyEnabled) + '\n');
611
+ process.stdout.write(formatListOutput(await loadListRows(), proxyEnabled) + '\n');
522
612
  process.stderr.write('Note: interactive view requires a terminal. Use --list for non-TTY output.\n');
523
613
  process.exitCode = 1;
524
614
  return;
@@ -569,7 +659,8 @@ export const agentsCommand = new Command('agents')
569
659
  // Lazy-import terminal to avoid loading readline/tty in non-TTY paths
570
660
  const { runAgentsTui } = await import('../agents-view/terminal.js');
571
661
  // Wrap: runTui rejects on initial-render failure or handler throw.
572
- // On rejection: log and bail — no partial write (avoids PF-014 process.exit).
662
+ // On rejection: log and bail — no partial write; set process.exitCode
663
+ // rather than calling process.exit(), which would skip pending cleanup.
573
664
  let result;
574
665
  try {
575
666
  result = await runAgentsTui(tuiState);
@@ -17,7 +17,7 @@ const ORCHESTRATOR_HOOK_MARKER = 'session-start-orchestrator';
17
17
  * hook is devflow's when its command ENDS in one of these, under any directory — so
18
18
  * installs made under a custom or repo-local devflow directory are still recognised —
19
19
  * and never because it merely contains a marker word: a user's
20
- * `~/bin/preamble-logger.sh` or `echo preamble` is theirs (applies ADR-024). The
20
+ * `~/bin/preamble-logger.sh` or `echo preamble` is theirs. The
21
21
  * legacy forms are the pre-preamble `ambient-prompt` hook (first a bare
22
22
  * `ambient-prompt.sh`, then through `run-hook`) and the retired
23
23
  * `session-start-classification` hook, both still swept on enable and disable.
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Attribution prompt helpers for devflow init.
3
3
  *
4
- * CLI-layer module (ADR-013): prompt-rendering logic lives in src/cli/commands/,
4
+ * CLI-layer module: prompt-rendering logic lives in src/cli/commands/,
5
5
  * core business logic stays in src/core/.
6
6
  *
7
- * Applies PF-029: the gate is an exported pure predicate with an explicit isTTY guard,
7
+ * The gate is an exported pure predicate with an explicit isTTY guard,
8
8
  * so --recommended (flag, no prompt) and the non-TTY fallback keep their promptless
9
9
  * contracts and the reachability rule is unit-testable without a terminal.
10
- * Applies PF-014: runAttributionStep never calls process.exit() or throws — callers
10
+ * runAttributionStep never calls process.exit() or throws — callers
11
11
  * own the cancel idiom (p.cancel + process.exit(0)), keeping try/finally cleanup safe.
12
12
  *
13
13
  * D27: suppress-attribution flag — gates Claude Code's AI-attribution injection.
@@ -37,7 +37,7 @@ import { clackNote, clackSelect } from './prompt-io.js';
37
37
  * Gating on the mode name is sound here because 'advanced' is only ever resolved on an
38
38
  * interactive path (the Advanced branch exit-1s on non-TTY), and the explicit isTTY guard
39
39
  * keeps the promptless contracts of --recommended and the non-TTY fallback pinned
40
- * regardless (PF-029).
40
+ * regardless.
41
41
  *
42
42
  * There is no CLI override for attribution — it is toggled post-install via
43
43
  * `devflow flags --enable/--disable suppress-attribution`.
@@ -65,7 +65,7 @@ export function buildClackAttributionPrompts() {
65
65
  *
66
66
  * Returns true only when `suppress-attribution` is explicitly set to the boolean
67
67
  * true — undefined, null, and false all map to false, giving `p.select` a real
68
- * boolean rather than an unchecked cast (PF-018: non-vacuous path).
68
+ * boolean rather than an unchecked cast.
69
69
  *
70
70
  * Pure function — no side effects, fully testable without a TTY.
71
71
  */
@@ -94,14 +94,14 @@ export function applyAttributionAnswer(flags, outcome) {
94
94
  * 1. Note — "Current setting: …" header with context about what the flag does.
95
95
  * 2. Enable select — labeled Yes / No with hints (seeded from prior state);
96
96
  * p.select is immune to Enter-through muscle memory while still preserving
97
- * the seeded value (ambient-prompt style — per PF-029).
97
+ * the seeded value (ambient-prompt style).
98
98
  *
99
99
  * Returns:
100
100
  * {kind:'resolved', suppress, messages} — step completed; `suppress` is the chosen
101
101
  * boolean; `messages` are emitted by the caller.
102
102
  * {kind:'cancelled'} — user pressed Escape; caller runs p.cancel + process.exit(0).
103
103
  *
104
- * Invariants (PF-014):
104
+ * Invariants:
105
105
  * - Never calls process.exit(), never throws.
106
106
  * - All I/O is routed through the `prompts` parameter (injectable for tests).
107
107
  */
@@ -109,7 +109,7 @@ export async function runAttributionStep(opts) {
109
109
  const { seed, prompts } = opts;
110
110
  const currentStr = seed ? 'suppressed' : 'shown (default)';
111
111
  // security-02: name the destructive branch (Yes) BEFORE the user consents.
112
- // ADR-024 corollary (b): turning the flag ON replaces any existing attribution
112
+ // Turning the flag ON replaces any existing attribution
113
113
  // value, including a custom one — this is deliberate. Only the exact
114
114
  // devflow-managed shape {"commit":"","pr":""} is removed on disable.
115
115
  // security-04: surface the org AI-disclosure-policy dimension as a note (not a gate).
@@ -76,7 +76,7 @@ export function removeCaptureHooks(input) {
76
76
  const settings = typeof input === 'string' ? JSON.parse(input) : structuredClone(input);
77
77
  let changed = false;
78
78
  for (const [hookType, marker] of Object.entries(CAPTURE_HOOK_CONFIG)) {
79
- // Evaluate every removal — never short-circuit (PF-015).
79
+ // Evaluate every removal — never short-circuit.
80
80
  const removed = removeHooks(settings, hookType, isCaptureHook(marker));
81
81
  changed = changed || removed;
82
82
  }
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Compliance prompt helpers for devflow init.
3
3
  *
4
- * CLI-layer module (ADR-013): prompt-rendering logic lives in src/cli/commands/,
4
+ * CLI-layer module: prompt-rendering logic lives in src/cli/commands/,
5
5
  * core business logic stays in src/core/compliance.ts.
6
6
  *
7
- * Applies PF-029: every wizard gate keys on `modePromptShown` (was the Setup-mode
7
+ * Every wizard gate keys on `modePromptShown` (was the Setup-mode
8
8
  * p.select prompt actually shown?), never on the mode name, so --recommended (flag,
9
9
  * no prompt) and the non-TTY fallback preserve their promptless contracts.
10
- * Applies PF-014: runComplianceStep never calls process.exit() or throws — callers
10
+ * runComplianceStep never calls process.exit() or throws — callers
11
11
  * own the cancel idiom (p.cancel + process.exit(0)), keeping try/finally cleanup safe.
12
12
  *
13
13
  * Shared DI seam (PromptOutcome, WizardPromptIO, clackNote, clackSelect) lives in
@@ -53,7 +53,7 @@ export function formatComplianceSummary(enabled, frameworks) {
53
53
  /**
54
54
  * Determines whether the compliance wizard step should run for a given init invocation.
55
55
  *
56
- * Gate table (per PF-029: key on modePromptShown, never on the mode name):
56
+ * Gate table (key on modePromptShown, never on the mode name):
57
57
  *
58
58
  * --recommended flag / !isTTY fallback → no (promptless contract preserved)
59
59
  * Interactive mode-prompt → Recommended → yes (modePromptShown=true)
@@ -106,7 +106,7 @@ export function buildClackCompliancePrompts() {
106
106
  * 1. Note — "Current setting: …" header then the framework catalogue.
107
107
  * 2. Enable select — labeled Yes / No with hints (seeded from prior state);
108
108
  * p.select is immune to Enter-through muscle memory while still preserving
109
- * the seeded value (ambient-prompt style — per PF-029).
109
+ * the seeded value (ambient-prompt style).
110
110
  * 3. If Yes — framework multiselect (seeded, required:false).
111
111
  *
112
112
  * Returns:
@@ -114,7 +114,7 @@ export function buildClackCompliancePrompts() {
114
114
  * ComplianceFeatureState; `messages` are emitted by the caller.
115
115
  * {kind:'cancelled'} — user pressed Escape; caller runs p.cancel + process.exit(0).
116
116
  *
117
- * Invariants (PF-014):
117
+ * Invariants:
118
118
  * - Never calls process.exit(), never throws.
119
119
  * - Returned arrays never alias seed arrays (defensive copies throughout).
120
120
  * - All I/O is routed through the `prompts` parameter (injectable for tests).
@@ -141,7 +141,7 @@ export async function runComplianceStep(opts) {
141
141
  if (!enabled) {
142
142
  return {
143
143
  kind: 'resolved',
144
- // Never alias seed.frameworks — defensive copy (PF-014).
144
+ // Never alias seed.frameworks — defensive copy.
145
145
  state: { enabled: false, frameworks: [...seed.frameworks] },
146
146
  messages: [{
147
147
  level: 'info',
@@ -153,7 +153,7 @@ export async function runComplianceStep(opts) {
153
153
  const multiselectOutcome = await prompts.multiselect({
154
154
  message: FRAMEWORK_SELECT_MESSAGE,
155
155
  options: frameworkChoices(),
156
- // Defensive copy: never alias seed.frameworks (PF-014).
156
+ // Defensive copy: never alias seed.frameworks.
157
157
  initialValues: [...seed.frameworks],
158
158
  required: false,
159
159
  });
@@ -1,15 +1,16 @@
1
1
  /**
2
2
  * devflow compliance — Enable, disable, set, and check status of the compliance feature.
3
3
  *
4
- * Applies ADR-013: CLI-layer module; pure helpers in src/core/compliance.ts,
4
+ * CLI-layer module; pure helpers in src/core/compliance.ts,
5
5
  * I/O orchestration in src/targets/claude-code/compliance-install.ts.
6
- * Applies ADR-001: compliance is manifest-group (like proxy), not config.json-gated.
7
- * Avoids PF-009: per-artifact failures are warn-not-throw.
8
- * Avoids PF-015: enable/disable each converge BOTH artifacts unconditionally.
6
+ * Compliance is manifest-group (like proxy), not config.json-gated.
7
+ * Per-artifact failures are warn-not-throw, so one failed artifact never aborts the rest.
8
+ * Enable/disable each converge BOTH artifacts unconditionally, so neither is left
9
+ * half-converged.
9
10
  * The evidence-policy lines (--status, and the --enable/--set suggestion) come
10
11
  * from src/core/evidence-policy.ts, the seam onto the package's own resolvers;
11
12
  * the CLI prints the keys to add to .devflow/project.json and never writes it
12
- * (applies ADR-024).
13
+ * (the file is team-owned).
13
14
  */
14
15
  import { Command } from 'commander';
15
16
  import { promises as fs } from 'fs';
@@ -277,7 +278,7 @@ export const complianceCommand = new Command('compliance')
277
278
  action = 'disable';
278
279
  }
279
280
  const resolved = resolveComplianceCliAction(current, action, setFrameworks);
280
- // Converge artifacts (PF-015: always converge, never short-circuit)
281
+ // Converge artifacts (always converge, never short-circuit)
281
282
  await convergeFromManifest({
282
283
  claudeDir,
283
284
  devflowDir,
@@ -310,7 +311,7 @@ export const complianceCommand = new Command('compliance')
310
311
  'run `devflow rules --enable` to install the stamped rule'));
311
312
  }
312
313
  // Suggest the team file compliance now implies. Printed, never written:
313
- // .devflow/project.json is team-owned (D-POLICY-NO-WRITE, applies ADR-024).
314
+ // .devflow/project.json is team-owned (D-POLICY-NO-WRITE).
314
315
  if (resolved.nextState.enabled) {
315
316
  const policyModule = loadEvidencePolicyModule();
316
317
  const settingsModule = loadSettingsModule();