devflow-kit 3.4.0 → 3.6.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 +127 -0
- package/README.md +12 -8
- package/dist/agents/code.md +39 -182
- package/dist/agents/{diagnose.md → debug.md} +4 -4
- package/dist/agents/design.md +0 -1
- package/dist/agents/fix.md +165 -0
- package/dist/agents/git.md +31 -693
- package/dist/agents/{scrutinize.md → harden.md} +9 -6
- package/dist/agents/knowledge.md +8 -2
- package/dist/agents/refactor.md +133 -0
- package/dist/agents/research.md +6 -0
- package/dist/agents/review.md +0 -4
- package/dist/agents/skim.md +4 -0
- package/dist/agents/triage.md +8 -1
- package/dist/cli/agents-view/render.js +1 -1
- package/dist/cli/commands/behavior-prompts.js +77 -0
- package/dist/cli/commands/behavior.js +123 -0
- package/dist/cli/commands/flags.js +8 -2
- package/dist/cli/commands/init-seed.js +7 -3
- package/dist/cli/commands/init.js +75 -8
- package/dist/cli/commands/learning.js +12 -12
- package/dist/cli/commands/legacy-hooks.js +1 -1
- package/dist/cli/commands/rules.js +16 -0
- package/dist/cli/commands/tracker.js +112 -15
- package/dist/cli/commands/uninstall.js +111 -5
- package/dist/cli.js +2 -0
- package/dist/commands/bug-analysis.md +22 -18
- package/dist/commands/code-review.md +25 -29
- package/dist/commands/debug.md +36 -15
- package/dist/commands/dynamic-build.md +64 -78
- package/dist/commands/dynamic-plan.md +18 -14
- package/dist/commands/dynamic-profile.md +8 -6
- package/dist/commands/dynamic-tickets.md +19 -15
- package/dist/commands/explore.md +14 -14
- package/dist/commands/implement.md +122 -225
- package/dist/commands/plan.md +32 -47
- package/dist/commands/release.md +12 -7
- package/dist/commands/research.md +26 -15
- package/dist/commands/resolve.md +53 -92
- package/dist/commands/self-review.md +42 -42
- package/dist/core/agent-frontmatter.js +101 -0
- package/dist/core/agent-models.js +47 -11
- package/dist/core/compliance-compose.js +6 -4
- package/dist/core/{learning-tuning-config.js → curate-tuning-config.js} +14 -14
- package/dist/core/feature-switch.js +23 -2
- package/dist/core/flags.js +111 -0
- package/dist/core/learning-queue-cleanup.js +1 -1
- package/dist/core/manifest.js +11 -3
- package/dist/core/mds-variants.js +68 -26
- package/dist/core/migrations.js +63 -44
- package/dist/core/plugins.js +49 -70
- package/dist/core/project-paths.js +3 -7
- package/dist/core/proxy-log.js +1 -1
- package/dist/core/proxy-state.js +1 -1
- package/dist/core/tracker.js +120 -2
- package/dist/hud/codex-usage.js +247 -0
- package/dist/hud/components/codex-quota.js +37 -0
- package/dist/hud/components/usage-quota.js +1 -1
- package/dist/hud/config.js +1 -0
- package/dist/hud/index.js +16 -3
- package/dist/hud/render.js +3 -1
- package/dist/learning-off/agents/code.md +37 -180
- package/dist/learning-off/agents/{diagnose.md → debug.md} +4 -4
- package/dist/learning-off/agents/design.md +0 -1
- package/dist/learning-off/agents/fix.md +161 -0
- package/dist/learning-off/agents/{scrutinize.md → harden.md} +9 -6
- package/dist/learning-off/agents/knowledge.md +8 -2
- package/dist/learning-off/agents/refactor.md +129 -0
- package/dist/learning-off/agents/research.md +6 -0
- package/dist/learning-off/agents/review.md +0 -4
- package/dist/learning-off/agents/skim.md +4 -0
- package/dist/learning-off/agents/triage.md +8 -1
- package/dist/learning-off/commands/bug-analysis.md +12 -12
- package/dist/learning-off/commands/code-review.md +14 -22
- package/dist/learning-off/commands/debug.md +24 -8
- package/dist/learning-off/commands/dynamic-build.md +58 -74
- package/dist/learning-off/commands/dynamic-plan.md +12 -10
- package/dist/learning-off/commands/dynamic-profile.md +2 -2
- package/dist/learning-off/commands/dynamic-tickets.md +13 -11
- package/dist/learning-off/commands/explore.md +3 -7
- package/dist/learning-off/commands/implement.md +110 -217
- package/dist/learning-off/commands/plan.md +21 -39
- package/dist/learning-off/commands/release.md +7 -4
- package/dist/learning-off/commands/research.md +16 -9
- package/dist/learning-off/commands/resolve.md +42 -85
- package/dist/learning-off/commands/self-review.md +30 -34
- package/dist/skills/git/references/learn-conventions.md +27 -3
- package/dist/skills/git/references/pr/check-ci-status.md +19 -0
- package/dist/skills/git/references/pr/check-merge-readiness.md +21 -0
- package/dist/skills/git/references/pr/fetch-review-threads.md +30 -0
- package/dist/skills/git/references/pr/post-resolution-summary.md +18 -0
- package/dist/skills/git/references/pr/post-review-summary.md +20 -0
- package/dist/skills/git/references/pr/resolve-review-threads.md +28 -4
- package/dist/skills/git/references/pr/update-pr-evidence.md +17 -6
- package/dist/skills/git/references/pr/validate-branch.md +26 -0
- package/dist/skills/git/references/tracker/github/associate-release.md +24 -0
- package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +31 -1
- package/dist/skills/git/references/tracker/github/create-release.md +30 -1
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +89 -2
- package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +16 -1
- package/dist/skills/git/references/tracker/github/fetch-issue.md +36 -1
- package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +47 -1
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +25 -0
- package/dist/skills/git/references/tracker/github/manage-debt.md +21 -1
- package/dist/skills/git/references/tracker/github/post-wave-report.md +15 -1
- package/dist/skills/git/references/tracker/github/setup-task.md +41 -1
- package/dist/skills/git/references/tracker/jira/associate-release.md +24 -0
- package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +31 -1
- package/dist/skills/git/references/tracker/jira/create-release.md +30 -1
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +89 -2
- package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +16 -1
- package/dist/skills/git/references/tracker/jira/fetch-issue.md +36 -1
- package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +47 -1
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +25 -0
- package/dist/skills/git/references/tracker/jira/manage-debt.md +21 -1
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +15 -1
- package/dist/skills/git/references/tracker/jira/setup-task.md +41 -1
- package/dist/skills/git/references/tracker/linear/associate-release.md +24 -0
- package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +31 -1
- package/dist/skills/git/references/tracker/linear/create-release.md +30 -1
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +89 -2
- package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +16 -1
- package/dist/skills/git/references/tracker/linear/fetch-issue.md +36 -1
- package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +47 -1
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +25 -0
- package/dist/skills/git/references/tracker/linear/manage-debt.md +21 -1
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +15 -1
- package/dist/skills/git/references/tracker/linear/setup-task.md +41 -1
- package/dist/targets/claude-code/agent-type-owner.js +164 -0
- package/dist/targets/claude-code/behavior-install.js +111 -0
- package/dist/targets/claude-code/installer.js +177 -77
- package/dist/targets/claude-code/learning-install.js +9 -1
- package/dist/targets/claude-code/tracker-install.js +33 -16
- package/dist/targets/claude-code/tracker-servers.js +107 -0
- package/package.json +4 -3
- package/src/assets/agents/_partials/_code.mds +109 -0
- package/src/assets/agents/code.mds +18 -222
- package/src/assets/agents/{learning.md → curate.md} +22 -72
- package/src/assets/agents/{diagnose.mds → debug.mds} +4 -4
- package/src/assets/agents/design.mds +0 -1
- package/src/assets/agents/evaluate.md +11 -5
- package/src/assets/agents/explore.md +58 -0
- package/src/assets/agents/fix.mds +113 -0
- package/src/assets/agents/git.mds +31 -693
- package/src/assets/agents/{scrutinize.mds → harden.mds} +9 -6
- package/src/assets/agents/knowledge.mds +8 -2
- package/src/assets/agents/{simplify.md → polish.md} +7 -4
- package/src/assets/agents/refactor.mds +81 -0
- package/src/assets/agents/research.mds +6 -0
- package/src/assets/agents/review.mds +0 -4
- package/src/assets/agents/skim.mds +4 -0
- package/src/assets/agents/synthesize.md +26 -352
- package/src/assets/agents/test.md +10 -2
- package/src/assets/agents/tracker.md +20 -7
- package/src/assets/agents/triage.mds +8 -1
- package/src/assets/agents/validate.md +5 -0
- package/src/assets/commands/_partials/_compliance.mds +36 -0
- package/src/assets/commands/_partials/_decisions.mds +26 -6
- package/src/assets/commands/_partials/_engine.mds +17 -17
- package/src/assets/commands/_partials/_evidence_policy.mds +0 -17
- package/src/assets/commands/_partials/_knowledge.mds +14 -9
- package/src/assets/commands/_partials/_preamble.mds +5 -3
- package/src/assets/commands/_partials/_roster.mds +6 -5
- package/src/assets/commands/_partials/_wave.mds +2 -2
- package/src/assets/commands/bug-analysis.mds +10 -10
- package/src/assets/commands/code-review.mds +12 -22
- package/src/assets/commands/debug.mds +24 -2
- package/src/assets/commands/dynamic-build.mds +34 -51
- package/src/assets/commands/dynamic-plan.mds +4 -3
- package/src/assets/commands/dynamic-tickets.mds +5 -4
- package/src/assets/commands/explore.mds +1 -1
- package/src/assets/commands/implement.mds +108 -201
- package/src/assets/commands/plan.mds +19 -44
- package/src/assets/commands/release.mds +8 -3
- package/src/assets/commands/research.mds +14 -7
- package/src/assets/commands/resolve.mds +37 -82
- package/src/assets/commands/self-review.mds +27 -27
- package/src/assets/mds/.mdsroot +4 -0
- package/src/assets/mds/git/_ops.mds +383 -0
- package/src/assets/mds/git/_ops_pr.mds +201 -0
- package/src/assets/mds/git/_ops_steps.mds +147 -0
- package/src/assets/mds/git/_pr.mds +45 -50
- package/src/assets/mds/git/_references.mds +6 -3
- package/src/assets/mds/tracker/_common.mds +5 -5
- package/src/assets/mds/tracker/_contract.mds +4 -3
- package/src/assets/mds/tracker/_github.mds +60 -14
- package/src/assets/mds/tracker/_jira.mds +60 -14
- package/src/assets/mds/tracker/_linear.mds +60 -14
- package/src/assets/mds/tracker/_steps.mds +10 -9
- package/src/assets/rules/behavior.md +6 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -3
- package/src/assets/scripts/hooks/capture-prompt +1 -1
- package/src/assets/scripts/hooks/capture-question +1 -1
- package/src/assets/scripts/hooks/capture-turn +1 -1
- package/src/assets/scripts/hooks/json-helper.cjs +2 -2
- package/src/assets/scripts/hooks/lib/learning-store.cjs +1 -1
- package/src/assets/scripts/hooks/lib/project-paths.cjs +4 -10
- package/src/assets/scripts/hooks/preamble +1 -1
- package/src/assets/scripts/hooks/session-start-context +58 -45
- package/src/assets/scripts/verify-evidence.cjs +726 -9
- package/src/assets/skills/agent-references/SKILL.md +37 -0
- package/src/assets/skills/agent-references/references/commands/implement-evidence-asks.md +35 -0
- package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +15 -0
- package/src/assets/skills/agent-references/references/curate/maintain.md +71 -0
- package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +71 -0
- package/src/assets/skills/agent-references/references/synthesize/design.md +36 -0
- package/src/assets/skills/agent-references/references/synthesize/exploration.md +38 -0
- package/src/assets/skills/agent-references/references/synthesize/planning.md +60 -0
- package/src/assets/skills/agent-references/references/synthesize/research.md +61 -0
- package/src/assets/skills/agent-references/references/synthesize/review.md +76 -0
- package/src/assets/skills/apply-decisions/SKILL.md +6 -4
- package/src/assets/skills/compliance/SKILL.md +1 -1
- package/src/assets/skills/docs-framework/SKILL.md +81 -84
- package/src/assets/skills/docs-framework/references/patterns.md +14 -25
- package/src/assets/skills/docs-framework/references/violations.md +7 -7
- package/src/assets/skills/gap-analysis/SKILL.md +0 -16
- package/src/assets/skills/gap-analysis/references/compliance.md +19 -0
- package/src/assets/skills/qa/SKILL.md +1 -1
- package/src/assets/skills/quality-gates/SKILL.md +7 -7
- package/src/assets/skills/quality-gates/references/report-template.md +7 -7
- package/dist/skills/git/references/pr/ensure-pr-ready.md +0 -24
|
@@ -231,4 +231,105 @@ export function rewriteAgentFrontmatter(content, opts) {
|
|
|
231
231
|
const changed = newContent !== content;
|
|
232
232
|
return Ok({ content: newContent, changed });
|
|
233
233
|
}
|
|
234
|
+
const WHOLE_SERVER_PREFIX = 'mcp__';
|
|
235
|
+
/** A server name as it appears in a whole-server entry: the charset Claude Code keeps, no `__`. */
|
|
236
|
+
const WHOLE_SERVER_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
237
|
+
/**
|
|
238
|
+
* True when `tool` is a whole-server allowlist entry: `mcp__` and a server name of
|
|
239
|
+
* 1-64 characters from `[A-Za-z0-9_-]` with no `__` (D-WHOLE-SERVER-ENTRY).
|
|
240
|
+
*/
|
|
241
|
+
export function isWholeServerTool(tool) {
|
|
242
|
+
if (!tool.startsWith(WHOLE_SERVER_PREFIX))
|
|
243
|
+
return false;
|
|
244
|
+
const name = tool.slice(WHOLE_SERVER_PREFIX.length);
|
|
245
|
+
return WHOLE_SERVER_NAME_RE.test(name) && !name.includes('__');
|
|
246
|
+
}
|
|
247
|
+
/** The single-line inline form `tools: ["A", "B"]`, within a frontmatter body. */
|
|
248
|
+
const INLINE_TOOLS_RE = /^tools:[ \t]*\[([^\r\n]*)\][ \t]*(?=\r?$)/m;
|
|
249
|
+
function toolItems(inner) {
|
|
250
|
+
return inner.split(',').filter(item => item.trim() !== '');
|
|
251
|
+
}
|
|
252
|
+
function unquoteTool(item) {
|
|
253
|
+
return item.trim().replace(/^(["'])(.*)\1$/, '$2');
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Replace the whole-server entries of the first frontmatter block's inline `tools:` line
|
|
257
|
+
* with `extraTools`, and touch nothing else.
|
|
258
|
+
*
|
|
259
|
+
* Idempotent: stamping the same list twice yields the same bytes, and stamping a new list
|
|
260
|
+
* replaces the previous one rather than adding to it. Every other entry of the line keeps
|
|
261
|
+
* its original text, and every other byte of the file (other keys, the body, CRLF or LF)
|
|
262
|
+
* is untouched. An empty `extraTools` removes the stamp.
|
|
263
|
+
*
|
|
264
|
+
* Rejects, rather than repairs: an entry that is not a whole-server entry (`invalid-tool`;
|
|
265
|
+
* it would otherwise reach the frontmatter as free text), a file with no inline
|
|
266
|
+
* `tools: [...]` line (`no-tools-line`; a block-list `tools:` is not edited) and a file
|
|
267
|
+
* with no well-formed frontmatter.
|
|
268
|
+
*
|
|
269
|
+
* Pure function — no I/O.
|
|
270
|
+
*/
|
|
271
|
+
export function stampAgentTools(content, extraTools) {
|
|
272
|
+
for (const tool of extraTools) {
|
|
273
|
+
if (!isWholeServerTool(tool))
|
|
274
|
+
return Err('invalid-tool');
|
|
275
|
+
}
|
|
276
|
+
const parts = parseFrontmatter(content);
|
|
277
|
+
if (!parts.ok)
|
|
278
|
+
return Err(parts.error);
|
|
279
|
+
const { fmBody, openDelim, closeDelim, afterClose, eol } = parts.value;
|
|
280
|
+
const match = INLINE_TOOLS_RE.exec(fmBody);
|
|
281
|
+
if (!match)
|
|
282
|
+
return Err('no-tools-line');
|
|
283
|
+
const rawItems = toolItems(match[1]);
|
|
284
|
+
const kept = rawItems.filter(item => !isWholeServerTool(unquoteTool(item)));
|
|
285
|
+
const stamped = [...new Set(extraTools)].map(tool => `"${tool}"`);
|
|
286
|
+
// Nothing to remove and nothing to add: the line, whatever its spacing, is already right.
|
|
287
|
+
if (kept.length === rawItems.length && stamped.length === 0)
|
|
288
|
+
return Ok({ content, changed: false });
|
|
289
|
+
// The kept entries keep their own text; only the edges lose the padding that no longer
|
|
290
|
+
// borders a neighbour, so the appended entries land after `, ` and the line stays one shape.
|
|
291
|
+
const keptText = kept
|
|
292
|
+
.map((item, i) => {
|
|
293
|
+
const lead = i === 0 ? item.trimStart() : item;
|
|
294
|
+
return i === kept.length - 1 ? lead.trimEnd() : lead;
|
|
295
|
+
})
|
|
296
|
+
.join(',');
|
|
297
|
+
const inner = [keptText, stamped.join(', ')].filter(text => text !== '').join(', ');
|
|
298
|
+
const line = `tools: [${inner}]`;
|
|
299
|
+
const newBody = fmBody.slice(0, match.index) + line + fmBody.slice(match.index + match[0].length);
|
|
300
|
+
const newContent = openDelim + newBody + eol + closeDelim + afterClose;
|
|
301
|
+
return Ok({ content: newContent, changed: newContent !== content });
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The whole-server entries of the first frontmatter block's inline `tools:` line, in
|
|
305
|
+
* order. Ok([]) when the line carries none; an error when there is no line to read.
|
|
306
|
+
* Pure function — no I/O.
|
|
307
|
+
*/
|
|
308
|
+
export function readStampedTools(content) {
|
|
309
|
+
const parts = parseFrontmatter(content);
|
|
310
|
+
if (!parts.ok)
|
|
311
|
+
return Err(parts.error);
|
|
312
|
+
const match = INLINE_TOOLS_RE.exec(parts.value.fmBody);
|
|
313
|
+
if (!match)
|
|
314
|
+
return Err('no-tools-line');
|
|
315
|
+
return Ok(toolItems(match[1]).map(unquoteTool).filter(isWholeServerTool));
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The text a converge installs for an agent: the variant `source` wearing the whole-server
|
|
319
|
+
* entries the installed copy carries. The stamp is applied at install and by
|
|
320
|
+
* `devflow tracker --servers`, never by a converge, so a converge only CARRIES it. Without
|
|
321
|
+
* the carry, switching an agent between prompt variants would drop its stamp and compare
|
|
322
|
+
* unequal to the installed copy on every run.
|
|
323
|
+
*
|
|
324
|
+
* `source` is returned unchanged when there is nothing safe to carry: either side has no
|
|
325
|
+
* well-formed inline `tools:` line, or the installed copy carries no entries.
|
|
326
|
+
* Pure function — no I/O.
|
|
327
|
+
*/
|
|
328
|
+
export function carryStampedTools(source, installed) {
|
|
329
|
+
const carried = readStampedTools(installed);
|
|
330
|
+
if (!carried.ok || carried.value.length === 0)
|
|
331
|
+
return source;
|
|
332
|
+
const stamped = stampAgentTools(source, carried.value);
|
|
333
|
+
return stamped.ok ? stamped.value.content : source;
|
|
334
|
+
}
|
|
234
335
|
//# sourceMappingURL=agent-frontmatter.js.map
|
|
@@ -142,38 +142,56 @@ export function validateWorkerValue(field, value) {
|
|
|
142
142
|
// Key migration
|
|
143
143
|
// ---------------------------------------------------------------------------
|
|
144
144
|
/**
|
|
145
|
-
* Maps
|
|
145
|
+
* Maps every retired agent key to the live slug that replaces it. Two renames feed it:
|
|
146
146
|
*
|
|
147
|
-
*
|
|
148
|
-
* (
|
|
149
|
-
*
|
|
150
|
-
*
|
|
147
|
+
* - The 2.0.0 action-verb rename (coder, reviewer, …) and the 1.x names that rename
|
|
148
|
+
* retired (scrutinizer, simplifier, bug-analyzer).
|
|
149
|
+
* - The rename of six agents to the names that state their job (code-fix, code-edit,
|
|
150
|
+
* scrutinize, simplify, diagnose, learning).
|
|
151
|
+
*
|
|
152
|
+
* SINGLE HOP: every value is a live slug and no value is itself a key. A 1.x name
|
|
153
|
+
* therefore points straight at its final slug (scrutinizer → harden, never
|
|
154
|
+
* scrutinizer → scrutinize → harden), so one pass of canonicaliseAgentKeys() lands
|
|
155
|
+
* every key. The two canonicalise-agent-keys migrations in migrations.ts call it to
|
|
156
|
+
* rewrite ~/.devflow/agent-models.json; readAgentMapping() calls it on every read.
|
|
151
157
|
*
|
|
152
158
|
* PROTOTYPE SAFETY: Object.create(null) prevents prototype-pollution.
|
|
153
159
|
* Every lookup MUST use Object.hasOwn — never key in LEGACY_AGENT_KEYS or
|
|
154
160
|
* direct bracket access. `--set constructor` would otherwise return a function
|
|
155
161
|
* that flows into path.join (the object-literal lookup on argv trap).
|
|
156
162
|
*
|
|
163
|
+
* ORDER: entries are declared oldest first. canonicaliseAgentKeys() reads that order
|
|
164
|
+
* when two keys meet at one slug (scrutinizer and scrutinize both mean harden): the
|
|
165
|
+
* entry declared later is the more recent name and wins.
|
|
166
|
+
*
|
|
157
167
|
* DORMANCY: entries here are stored whether or not the key rename has been
|
|
158
168
|
* applied to the file yet. canonicaliseAgentKeys() is idempotent, so
|
|
159
169
|
* double-execution from concurrent sessions is a harmless no-op — the second
|
|
160
170
|
* run finds the file already migrated and makes zero writes.
|
|
161
171
|
*/
|
|
162
172
|
export const LEGACY_AGENT_KEYS = Object.assign(Object.create(null), {
|
|
163
|
-
//
|
|
173
|
+
// 1.x names (noun form), retargeted to the live slug of their final rename.
|
|
174
|
+
'scrutinizer': 'harden',
|
|
175
|
+
'simplifier': 'polish',
|
|
176
|
+
'bug-analyzer': 'debug',
|
|
177
|
+
// 2.0.0 action-verb rename (old slug → new slug).
|
|
164
178
|
'coder': 'code',
|
|
165
179
|
'designer': 'design',
|
|
166
180
|
'evaluator': 'evaluate',
|
|
167
181
|
'researcher': 'research',
|
|
168
182
|
'reviewer': 'review',
|
|
169
|
-
'scrutinizer': 'scrutinize',
|
|
170
|
-
'simplifier': 'simplify',
|
|
171
183
|
'skimmer': 'skim',
|
|
172
184
|
'synthesizer': 'synthesize',
|
|
173
185
|
'tester': 'test',
|
|
174
186
|
'triager': 'triage',
|
|
175
187
|
'validator': 'validate',
|
|
176
|
-
|
|
188
|
+
// Rename of six agents to the names that state their job.
|
|
189
|
+
'code-fix': 'fix',
|
|
190
|
+
'code-edit': 'refactor',
|
|
191
|
+
'scrutinize': 'harden',
|
|
192
|
+
'simplify': 'polish',
|
|
193
|
+
'diagnose': 'debug',
|
|
194
|
+
'learning': 'curate',
|
|
177
195
|
});
|
|
178
196
|
/**
|
|
179
197
|
* Canonicalise agent keys in a raw agents map by applying LEGACY_AGENT_KEYS.
|
|
@@ -184,6 +202,10 @@ export const LEGACY_AGENT_KEYS = Object.assign(Object.create(null), {
|
|
|
184
202
|
*
|
|
185
203
|
* Collision detection is against the ORIGINAL map so results are order-independent.
|
|
186
204
|
*
|
|
205
|
+
* Two legacy keys that name one agent (scrutinizer and scrutinize both mean harden) meet
|
|
206
|
+
* at one slug. The key declared later in LEGACY_AGENT_KEYS, the more recent name, wins;
|
|
207
|
+
* the other is dropped. The pick reads the declaration order, never the file order.
|
|
208
|
+
*
|
|
187
209
|
* The function is pure and idempotent: calling it twice on the same input
|
|
188
210
|
* produces the same result as calling it once (idempotent under concurrent execution).
|
|
189
211
|
*
|
|
@@ -208,6 +230,19 @@ export function canonicaliseAgentKeys(rawAgents, onWarning) {
|
|
|
208
230
|
}
|
|
209
231
|
// Snapshot the original keys for collision detection (order-independent result).
|
|
210
232
|
const originalKeys = new Set(Object.keys(rawAgents));
|
|
233
|
+
// When several legacy keys in the file name one agent, the one declared latest in
|
|
234
|
+
// LEGACY_AGENT_KEYS is the one that is renamed; the others are dropped below.
|
|
235
|
+
const declared = Object.keys(LEGACY_AGENT_KEYS);
|
|
236
|
+
const winnerByTarget = new Map();
|
|
237
|
+
for (const oldKey of originalKeys) {
|
|
238
|
+
if (oldKey === '__proto__' || !Object.hasOwn(LEGACY_AGENT_KEYS, oldKey))
|
|
239
|
+
continue;
|
|
240
|
+
const target = LEGACY_AGENT_KEYS[oldKey];
|
|
241
|
+
const held = winnerByTarget.get(target);
|
|
242
|
+
if (held === undefined || declared.indexOf(oldKey) > declared.indexOf(held)) {
|
|
243
|
+
winnerByTarget.set(target, oldKey);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
211
246
|
const result = { ...rawAgents };
|
|
212
247
|
let didMutate = false;
|
|
213
248
|
for (const oldKey of originalKeys) {
|
|
@@ -237,8 +272,9 @@ export function canonicaliseAgentKeys(rawAgents, onWarning) {
|
|
|
237
272
|
didMutate = true;
|
|
238
273
|
continue;
|
|
239
274
|
}
|
|
240
|
-
if (originalKeys.has(newKey)) {
|
|
241
|
-
// Collision: new (canonical) key already present
|
|
275
|
+
if (originalKeys.has(newKey) || winnerByTarget.get(newKey) !== oldKey) {
|
|
276
|
+
// Collision: new (canonical) key already present, or a more recent legacy key
|
|
277
|
+
// names the same agent → that value wins, old dropped.
|
|
242
278
|
// Warning is surfaced by the caller's structured dropped-block (e.g. migrations.ts)
|
|
243
279
|
// to avoid printing the same collision twice in two different vocabularies (F8).
|
|
244
280
|
dropped.push(oldKey);
|
|
@@ -172,13 +172,15 @@ function collectMissingFragmentWarnings(fnName, activeFrameworks, fragments, omi
|
|
|
172
172
|
*
|
|
173
173
|
* Every framework reference is installed on every machine
|
|
174
174
|
* (D-COMPLIANCE-INSTALL-ALWAYS), so file presence no longer says which
|
|
175
|
-
* frameworks apply. The
|
|
176
|
-
*
|
|
177
|
-
*
|
|
175
|
+
* frameworks apply. The command names the ids in the agent's prompt
|
|
176
|
+
* (D-COMPLIANCE-INJECT: the compliance directive) — the `COMPLIANCE` field of the
|
|
177
|
+
* settings line, which folds this machine's selection with the repository's
|
|
178
|
+
* `.devflow/project.json` — and those ids alone are in force. No agent takes a
|
|
179
|
+
* named lens input, so the stamp names the prompt, not a variable.
|
|
178
180
|
*/
|
|
179
181
|
const RUNTIME_SELECTION_LINES = [
|
|
180
182
|
'Every `references/{id}.md` is installed, so presence decides nothing: the ids you were given',
|
|
181
|
-
"(
|
|
183
|
+
"(named in your prompt; this machine's plus the repository's) are the frameworks in force.",
|
|
182
184
|
'Load `references/{id}.md` for each given id and no other; `none` means generic controls only.',
|
|
183
185
|
'Apply generic controls always.',
|
|
184
186
|
'',
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { getDevFlowDirectory } from '../targets/claude-code/claude-paths.js';
|
|
4
|
-
import {
|
|
4
|
+
import { getCurateTuningConfigPath } from './project-paths.js';
|
|
5
5
|
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
9
|
/**
|
|
10
|
-
* D-LEARNING-SHIPPED-ROW: the default model is the model the
|
|
11
|
-
* with, the `model:` line of src/assets/agents/
|
|
10
|
+
* D-LEARNING-SHIPPED-ROW: the default model is the model the Curate agent ships
|
|
11
|
+
* with, the `model:` line of src/assets/agents/curate.md. With no layer
|
|
12
12
|
* configuring a model, the spawn directive names none and that line decides, so a
|
|
13
13
|
* different default here would describe a run that never happens.
|
|
14
|
-
* tests/decisions/config.test.ts holds this value equal to the
|
|
14
|
+
* tests/decisions/config.test.ts holds this value equal to the Curate row of the
|
|
15
15
|
* agent-config table, which a change of the shipped tier edits with the frontmatter.
|
|
16
16
|
*/
|
|
17
17
|
const DEFAULTS = {
|
|
@@ -19,12 +19,12 @@ const DEFAULTS = {
|
|
|
19
19
|
debug: false,
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
|
-
* Apply a single JSON config layer onto a
|
|
22
|
+
* Apply a single JSON config layer onto a CurateTuningConfig, returning a new object.
|
|
23
23
|
* Skips fields with wrong types. Swallows parse errors — callers see defaults.
|
|
24
24
|
* Unknown fields (e.g. an old config still on disk with extra knobs) are
|
|
25
25
|
* silently ignored, not an error.
|
|
26
26
|
*/
|
|
27
|
-
export function
|
|
27
|
+
export function applyCurateTuningConfigLayer(config, json) {
|
|
28
28
|
try {
|
|
29
29
|
const parsed = JSON.parse(json);
|
|
30
30
|
// Object-shape guard mirrors coerceConfig in feature-config.ts: a JSON
|
|
@@ -34,7 +34,7 @@ export function applyLearningTuningConfigLayer(config, json) {
|
|
|
34
34
|
}
|
|
35
35
|
const raw = parsed;
|
|
36
36
|
return {
|
|
37
|
-
// Only accept a model value that belongs to the closed
|
|
37
|
+
// Only accept a model value that belongs to the closed CurateModelAlias domain.
|
|
38
38
|
model: isValidModel(raw.model) ? raw.model : config.model,
|
|
39
39
|
debug: typeof raw.debug === 'boolean' ? raw.debug : config.debug,
|
|
40
40
|
};
|
|
@@ -54,13 +54,13 @@ function readConfigFile(filePath) {
|
|
|
54
54
|
catch (err) {
|
|
55
55
|
if (err.code !== 'ENOENT') {
|
|
56
56
|
// Warn but don't crash — callers fall back to defaults.
|
|
57
|
-
console.warn(`[
|
|
57
|
+
console.warn(`[curate-tuning-config] warning: could not read ${filePath}: ${err.message}`);
|
|
58
58
|
}
|
|
59
59
|
return null;
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
|
-
* Load and merge
|
|
63
|
+
* Load and merge curate agent tuning configuration.
|
|
64
64
|
*
|
|
65
65
|
* Priority (highest wins): project config → global config → defaults.
|
|
66
66
|
*
|
|
@@ -71,18 +71,18 @@ function readConfigFile(filePath) {
|
|
|
71
71
|
*
|
|
72
72
|
* Invalid JSON in either file is silently ignored and treated as absent.
|
|
73
73
|
*/
|
|
74
|
-
export function
|
|
74
|
+
export function loadCurateTuningConfig(ledgerRoot) {
|
|
75
75
|
const globalConfigPath = path.join(getDevFlowDirectory(), 'learning.json');
|
|
76
|
-
const projectConfigPath =
|
|
76
|
+
const projectConfigPath = getCurateTuningConfigPath(ledgerRoot);
|
|
77
77
|
let config = { ...DEFAULTS };
|
|
78
78
|
const globalJson = readConfigFile(globalConfigPath);
|
|
79
79
|
if (globalJson !== null) {
|
|
80
|
-
config =
|
|
80
|
+
config = applyCurateTuningConfigLayer(config, globalJson);
|
|
81
81
|
}
|
|
82
82
|
const projectJson = readConfigFile(projectConfigPath);
|
|
83
83
|
if (projectJson !== null) {
|
|
84
|
-
config =
|
|
84
|
+
config = applyCurateTuningConfigLayer(config, projectJson);
|
|
85
85
|
}
|
|
86
86
|
return config;
|
|
87
87
|
}
|
|
88
|
-
//# sourceMappingURL=
|
|
88
|
+
//# sourceMappingURL=curate-tuning-config.js.map
|
|
@@ -106,6 +106,27 @@ async function readRawManifest(devflowDir) {
|
|
|
106
106
|
export async function readMachineFeature(devflowDir, feature) {
|
|
107
107
|
return isMachineFeatureOn(await readRawManifest(devflowDir), feature);
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Whether a RAW parsed manifest has an opt-in `feature` switched on. Pure.
|
|
111
|
+
*
|
|
112
|
+
* D-BEHAVIOR-OPT-IN: only an explicit boolean `true` is on. A missing key, a
|
|
113
|
+
* non-boolean value, a manifest without a `features` record and anything that is not an
|
|
114
|
+
* object all read as OFF — fail-closed, the mirror of {@link isMachineFeatureOn}. Like
|
|
115
|
+
* that reader it is deliberately not built on readManifest(): a read-only status must
|
|
116
|
+
* not heal-write, and a manifest missing a required field is still a manifest here.
|
|
117
|
+
*/
|
|
118
|
+
export function isOptInFeatureOn(rawManifest, feature) {
|
|
119
|
+
if (!isJsonObject(rawManifest) || !isJsonObject(rawManifest.features))
|
|
120
|
+
return false;
|
|
121
|
+
return rawManifest.features[feature] === true;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Read an opt-in machine switch from `<devflowDir>/manifest.json`. Read-only (no heal
|
|
125
|
+
* write); an absent, unreadable or malformed manifest reads as OFF.
|
|
126
|
+
*/
|
|
127
|
+
export async function readOptInFeature(devflowDir, feature) {
|
|
128
|
+
return isOptInFeatureOn(await readRawManifest(devflowDir), feature);
|
|
129
|
+
}
|
|
109
130
|
/**
|
|
110
131
|
* Write the machine-wide switch to `<devflowDir>/manifest.json` (atomic
|
|
111
132
|
* temp + rename). Refuses with `not-installed` when there is no manifest to
|
|
@@ -115,8 +136,8 @@ export async function readMachineFeature(devflowDir, feature) {
|
|
|
115
136
|
* D-NOOP-TOGGLE: a toggle to the value the manifest already records writes
|
|
116
137
|
* nothing. The only bytes such a write could change are `updatedAt`, and that
|
|
117
138
|
* field means "the manifest's content last changed", so a no-op must not move
|
|
118
|
-
* it. This is the one write point of `devflow memory|learning|knowledge
|
|
119
|
-
* --enable/--disable`, so the rule holds for all
|
|
139
|
+
* it. This is the one write point of `devflow memory|learning|knowledge|behavior
|
|
140
|
+
* --enable/--disable`, so the rule holds for all four at once.
|
|
120
141
|
*/
|
|
121
142
|
export async function writeMachineFeature(devflowDir, feature, enabled) {
|
|
122
143
|
const raw = await readRawManifest(devflowDir);
|
package/dist/core/flags.js
CHANGED
|
@@ -88,6 +88,45 @@ export const FLAG_REGISTRY = [
|
|
|
88
88
|
recommended: true,
|
|
89
89
|
defaultValue: true,
|
|
90
90
|
},
|
|
91
|
+
{
|
|
92
|
+
// D-SUBAGENT-CACHE-5M: subagents write their prompt cache with the 5-minute TTL
|
|
93
|
+
// while the main session keeps the 1-hour TTL of `prompt-caching-1h` (untouched).
|
|
94
|
+
//
|
|
95
|
+
// Why: the wave-4 token audit (2026-10-10) found 99.998% of subagent cache writes
|
|
96
|
+
// are 1-hour writes, yet fewer than 0.1% of subagent requests idle longer than
|
|
97
|
+
// 5 minutes - a subagent finishes or is resumed long before a 5-minute entry
|
|
98
|
+
// lapses, so the 1-hour premium buys nothing. Modelled effect of 5m: -8.9% on
|
|
99
|
+
// subagent cache-equivalent cost, -7.7% on total.
|
|
100
|
+
//
|
|
101
|
+
// Mechanism (VENDOR BEHAVIOUR, probed 2026-10-11 on Claude Code 2.1.296 in a
|
|
102
|
+
// sandboxed session): with ENABLE_PROMPT_CACHING_1H=true and this setting at
|
|
103
|
+
// "5m", subagent requests wrote only ephemeral_5m cache (36,387 tokens, none at
|
|
104
|
+
// 1h) while the main session wrote 100% 1h (35,209). Precedence of the subagent
|
|
105
|
+
// TTL in the binary, highest first: an env var that forces 5m, env
|
|
106
|
+
// CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL, this setting, a per-agent frontmatter
|
|
107
|
+
// `experimental.cacheTtl`, ENABLE_PROMPT_CACHING_1H. The 1-hour TTL is ignored
|
|
108
|
+
// while a subscription is in overage. A Claude Code that predates the setting
|
|
109
|
+
// ignores the key, so subagents then follow ENABLE_PROMPT_CACHING_1H as before.
|
|
110
|
+
// Re-probe when the precedence matters again; this is an observation, not a spec.
|
|
111
|
+
//
|
|
112
|
+
// Rejected: per-agent `experimental.cacheTtl` - it would spread one policy over
|
|
113
|
+
// every agent file; one devflow-managed flag keeps the policy in one place.
|
|
114
|
+
//
|
|
115
|
+
// Delete guard: only "5m" is deleted on disable or uninstall, so a user's own
|
|
116
|
+
// "1h" stays (D-GUARD-SCALAR). Adoption: init seeds this flag OFF when a value
|
|
117
|
+
// that is not "5m" is already on disk (D-ADOPT-RESPECTS-USER, respectUserOwnedSettings).
|
|
118
|
+
id: 'subagent-cache-5m',
|
|
119
|
+
label: 'Subagent 5-min cache',
|
|
120
|
+
description: 'Write subagent prompt-cache entries with the 5-minute TTL; the main session keeps 1 hour',
|
|
121
|
+
hint: 'Subagents rarely idle past 5 min, so 5-min cache writes cost less',
|
|
122
|
+
blurb: '5-min cache TTL for subagents',
|
|
123
|
+
kind: 'boolean',
|
|
124
|
+
target: { type: 'setting', key: 'subagentPromptCacheTtl' },
|
|
125
|
+
onPayload: '5m',
|
|
126
|
+
settingDeleteGuard: '5m',
|
|
127
|
+
recommended: true,
|
|
128
|
+
defaultValue: true,
|
|
129
|
+
},
|
|
91
130
|
{
|
|
92
131
|
id: 'show-turn-duration',
|
|
93
132
|
label: 'Show turn duration',
|
|
@@ -969,6 +1008,78 @@ export function settingHoldsManagedShape(settingsJson, flagId) {
|
|
|
969
1008
|
return false;
|
|
970
1009
|
}
|
|
971
1010
|
}
|
|
1011
|
+
/**
|
|
1012
|
+
* D-ADOPT-RESPECTS-USER predicate: the mirror of `settingHoldsManagedShape`.
|
|
1013
|
+
*
|
|
1014
|
+
* Returns true when the settings.json string carries a value at the guarded flag's
|
|
1015
|
+
* target key that is NOT the flag's managed shape - a value the user (or another
|
|
1016
|
+
* tool) owns, which enabling the flag would overwrite. Presence is decided with
|
|
1017
|
+
* `hasOwnProperty`, so an explicit `null` counts as a value; the shape decision is
|
|
1018
|
+
* delegated to `settingValueHoldsManagedShape`, never compared here.
|
|
1019
|
+
*
|
|
1020
|
+
* Returns false when:
|
|
1021
|
+
* - settingsJson is malformed
|
|
1022
|
+
* - root is not a plain object
|
|
1023
|
+
* - flagId is not in the registry
|
|
1024
|
+
* - the flag has no settingDeleteGuard, or is not a setting-target boolean
|
|
1025
|
+
* - the key is absent
|
|
1026
|
+
* - the on-disk value holds the managed shape
|
|
1027
|
+
*/
|
|
1028
|
+
export function settingHoldsUserValue(settingsJson, flagId) {
|
|
1029
|
+
try {
|
|
1030
|
+
const parsed = JSON.parse(settingsJson);
|
|
1031
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
1032
|
+
return false;
|
|
1033
|
+
const flag = FLAG_REGISTRY_MAP.get(flagId);
|
|
1034
|
+
if (!flag || flag.kind !== 'boolean' || flag.target.type !== 'setting')
|
|
1035
|
+
return false;
|
|
1036
|
+
if (flag.settingDeleteGuard === undefined)
|
|
1037
|
+
return false;
|
|
1038
|
+
const root = parsed;
|
|
1039
|
+
if (!Object.prototype.hasOwnProperty.call(root, flag.target.key))
|
|
1040
|
+
return false;
|
|
1041
|
+
return !settingValueHoldsManagedShape(flag, root[flag.target.key]);
|
|
1042
|
+
}
|
|
1043
|
+
catch {
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* D-ADOPT-RESPECTS-USER: seed a guarded setting flag OFF when adopting it ON would
|
|
1049
|
+
* overwrite a value the user already owns.
|
|
1050
|
+
*
|
|
1051
|
+
* A flag the manifest does not record is adopted at its `defaultValue`. For a flag whose
|
|
1052
|
+
* target is a settings.json key with a `settingDeleteGuard`, adopting ON writes its
|
|
1053
|
+
* managed payload over whatever is on disk (the guard protects deletion, not writes).
|
|
1054
|
+
* When the key already holds a value that is not the managed shape - a user's own `"1h"`
|
|
1055
|
+
* under `subagentPromptCacheTtl` - that write would silently replace a deliberate choice.
|
|
1056
|
+
* Such a flag is set `false` instead; with the record then claiming `false`, the converge
|
|
1057
|
+
* pipeline neither writes nor deletes the user's value.
|
|
1058
|
+
*
|
|
1059
|
+
* Scope: only flags ABSENT from `manifestFlags` (or all of them on a fresh install, when
|
|
1060
|
+
* it is null). A flag the manifest records keeps its value in `flags`, so an explicit
|
|
1061
|
+
* enable still wins. `devflow init --reset` hands this an empty settings snapshot
|
|
1062
|
+
* (resolveResetGatedInputs), so nothing is detected and the factory default stands. The rule is generic over guarded
|
|
1063
|
+
* setting flags: no flag id is special-cased, and `settingHoldsUserValue` is the one place
|
|
1064
|
+
* that decides "the disk holds something that is not ours". For a flag whose default is
|
|
1065
|
+
* already OFF (suppress-attribution) it changes nothing.
|
|
1066
|
+
*
|
|
1067
|
+
* Two callers apply the same rule: the init seed, and the flags TUI's starting rows (an
|
|
1068
|
+
* upgrade that has not run init yet would otherwise offer a Save that overwrites the value).
|
|
1069
|
+
*
|
|
1070
|
+
* Pure function - the settings snapshot is read by the caller and passed in.
|
|
1071
|
+
*/
|
|
1072
|
+
export function respectUserOwnedSettings(flags, manifestFlags, settingsSnapshot) {
|
|
1073
|
+
const result = { ...flags };
|
|
1074
|
+
for (const flag of FLAG_REGISTRY) {
|
|
1075
|
+
const adoptedAtDefault = manifestFlags === null || !(flag.id in manifestFlags);
|
|
1076
|
+
if (!adoptedAtDefault)
|
|
1077
|
+
continue;
|
|
1078
|
+
if (settingHoldsUserValue(settingsSnapshot, flag.id))
|
|
1079
|
+
result[flag.id] = false;
|
|
1080
|
+
}
|
|
1081
|
+
return result;
|
|
1082
|
+
}
|
|
972
1083
|
/**
|
|
973
1084
|
* D-ATTR-GUARD: returns true when it is safe to delete a flag's target key from
|
|
974
1085
|
* settings. For flags with a settingDeleteGuard the key is only deleted when the
|
|
@@ -11,7 +11,7 @@ import { drainQueueFiles } from './queue-drain.js';
|
|
|
11
11
|
/**
|
|
12
12
|
* Drain the learning (decisions-detection) pending-turns queue, its claimed
|
|
13
13
|
* batch and the claim's owner file so stale turns don't process later — used by
|
|
14
|
-
* both `--clear` and `--disable`. A mid-run
|
|
14
|
+
* both `--clear` and `--disable`. A mid-run Curate agent whose claimed batch
|
|
15
15
|
* vanishes aborts without changes, which is the desired outcome in both cases.
|
|
16
16
|
* Refused, deleting nothing, when `.devflow` or `.devflow/learning` under `gitRoot`
|
|
17
17
|
* is a symbolic link (D-CLI-NO-SYMLINK). ENOENT-tolerant; other errors propagate.
|
package/dist/core/manifest.js
CHANGED
|
@@ -62,12 +62,15 @@ function parseManifestFlags(features, knownFlags) {
|
|
|
62
62
|
* - features.knownFlags stripped from result (folded into FlagsRecord key-presence)
|
|
63
63
|
* - features.proxy absent → false
|
|
64
64
|
* - features.compliance absent/malformed → {enabled:false, frameworks:[]}
|
|
65
|
+
* - features.behavior absent → false; malformed → false, healed on disk (D-BEHAVIOR-OPT-IN)
|
|
65
66
|
* - features.tracker absent/malformed → {provider:'github'}
|
|
67
|
+
* - features.tracker.servers absent → the default list; invalid → the default list, reported
|
|
68
|
+
* through `opts.onWarning` when the caller supplies one (this reader has no output channel)
|
|
66
69
|
*
|
|
67
70
|
* D39: heal-write failure returns the migrated in-memory manifest (not null).
|
|
68
71
|
* The on-disk format remains unhealed; next read triggers another attempt.
|
|
69
72
|
*/
|
|
70
|
-
export async function readManifest(devflowDir) {
|
|
73
|
+
export async function readManifest(devflowDir, opts) {
|
|
71
74
|
const manifestPath = path.join(devflowDir, 'manifest.json');
|
|
72
75
|
try {
|
|
73
76
|
const content = await fs.readFile(manifestPath, 'utf-8');
|
|
@@ -129,7 +132,10 @@ export async function readManifest(devflowDir) {
|
|
|
129
132
|
features.decisions !== undefined ||
|
|
130
133
|
flagsWereLegacy ||
|
|
131
134
|
features.knownFlags !== undefined ||
|
|
132
|
-
features.viewMode !== undefined
|
|
135
|
+
features.viewMode !== undefined ||
|
|
136
|
+
// D-BEHAVIOR-OPT-IN: a malformed switch (present, not a boolean) is rewritten as
|
|
137
|
+
// the false it reads as; an absent one is not a defect and owes no write.
|
|
138
|
+
(features.behavior !== undefined && typeof features.behavior !== 'boolean');
|
|
133
139
|
const SECURITY_MODES = ['none', 'user', 'managed'];
|
|
134
140
|
const manifest = {
|
|
135
141
|
version: data.version,
|
|
@@ -158,12 +164,14 @@ export async function readManifest(devflowDir) {
|
|
|
158
164
|
proxy: typeof features.proxy === 'boolean' ? features.proxy : false,
|
|
159
165
|
// Self-heal: absent/malformed compliance → {enabled:false, frameworks:[]}
|
|
160
166
|
compliance: normalizeComplianceFeature(features.compliance),
|
|
167
|
+
// D-BEHAVIOR-OPT-IN: absent/malformed → false (opt-in; the inverse of knowledge/learning).
|
|
168
|
+
behavior: features.behavior === true,
|
|
161
169
|
// Self-heal: absent/malformed/unknown tracker → {provider:'github'} (AC-3.21).
|
|
162
170
|
// Deliberately NOT in the hard-null set above — see the field's doc comment.
|
|
163
171
|
// Healing here is silent and emits no DEGRADED: that is the correct
|
|
164
172
|
// self-heal behaviour, and a different condition from a per-repo config
|
|
165
173
|
// value outside the domain (which does emit `unknown tracker provider`).
|
|
166
|
-
tracker: normalizeTrackerFeature(features.tracker),
|
|
174
|
+
tracker: normalizeTrackerFeature(features.tracker, opts?.onWarning),
|
|
167
175
|
},
|
|
168
176
|
installedAt: data.installedAt,
|
|
169
177
|
updatedAt: data.updatedAt,
|