devflow-kit 2.3.0 → 2.5.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 (169) hide show
  1. package/CHANGELOG.md +177 -0
  2. package/README.md +86 -18
  3. package/dist/agents/git.md +824 -0
  4. package/dist/cli/commands/agents.js +6 -1
  5. package/dist/cli/commands/attribution-prompts.js +144 -0
  6. package/dist/cli/commands/compliance-prompts.js +7 -11
  7. package/dist/cli/commands/compliance.js +23 -1
  8. package/dist/cli/commands/init-seed.js +69 -30
  9. package/dist/cli/commands/init.js +551 -77
  10. package/dist/cli/commands/install-report.js +205 -0
  11. package/dist/cli/commands/knowledge/index.js +2 -2
  12. package/dist/cli/commands/knowledge/toggle.js +27 -37
  13. package/dist/cli/commands/learning.js +37 -30
  14. package/dist/cli/commands/memory.js +79 -69
  15. package/dist/cli/commands/prompt-io.js +38 -0
  16. package/dist/cli/commands/proxy.js +91 -31
  17. package/dist/cli/commands/security.js +76 -16
  18. package/dist/cli/commands/skills.js +53 -7
  19. package/dist/cli/commands/tracker-prompts.js +145 -0
  20. package/dist/cli/commands/tracker.js +405 -0
  21. package/dist/cli/commands/uninstall.js +233 -77
  22. package/dist/cli.js +2 -0
  23. package/dist/commands/bug-analysis.md +22 -4
  24. package/dist/commands/code-review.md +44 -15
  25. package/dist/commands/debug.md +20 -6
  26. package/dist/commands/dynamic-build.md +289 -67
  27. package/dist/commands/dynamic-plan.md +60 -21
  28. package/dist/commands/dynamic-profile.md +1 -1
  29. package/dist/commands/dynamic-tickets.md +58 -8
  30. package/dist/commands/explore.md +2 -2
  31. package/dist/commands/implement.md +241 -53
  32. package/dist/commands/plan.md +88 -17
  33. package/dist/commands/release.md +64 -17
  34. package/dist/commands/resolve.md +138 -58
  35. package/dist/commands/self-review.md +2 -2
  36. package/dist/core/agent-models.js +55 -12
  37. package/dist/core/assets.js +58 -2
  38. package/dist/core/evidence-policy.js +147 -0
  39. package/dist/core/feature-config.js +130 -64
  40. package/dist/core/feature-switch.js +112 -0
  41. package/dist/core/flags.js +149 -8
  42. package/dist/core/manifest.js +33 -7
  43. package/dist/core/mds-variants.js +861 -0
  44. package/dist/core/model-discovery.js +12 -1
  45. package/dist/core/plugins.js +357 -9
  46. package/dist/core/project-paths.js +1 -1
  47. package/dist/core/proxy-log.js +8 -4
  48. package/dist/core/proxy-state.js +100 -43
  49. package/dist/core/reference-sweep.js +136 -0
  50. package/dist/core/tracker.js +407 -0
  51. package/dist/skills/git/references/decision-markers.md +19 -0
  52. package/dist/skills/git/references/learn-conventions.md +56 -0
  53. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  54. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  55. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  56. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  57. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  58. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  59. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  60. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  61. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  62. package/dist/skills/git/references/publication-gate.md +13 -0
  63. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  64. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  65. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  66. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  67. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  68. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  69. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  70. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  71. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  72. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  73. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  74. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  75. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  76. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  77. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  78. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  79. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  80. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  81. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  82. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  83. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  84. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  85. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  86. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  87. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  88. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  89. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  90. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  91. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  92. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  93. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  94. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  95. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  96. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  97. package/dist/skills/git/references/trust-rule.md +7 -0
  98. package/dist/targets/claude-code/installer.js +1213 -31
  99. package/dist/targets/claude-code/legacy.js +5 -0
  100. package/dist/targets/claude-code/post-install.js +303 -104
  101. package/dist/targets/claude-code/tracker-install.js +161 -0
  102. package/package.json +4 -3
  103. package/src/assets/agents/code.md +42 -4
  104. package/src/assets/agents/design.md +1 -1
  105. package/src/assets/agents/git.mds +827 -0
  106. package/src/assets/agents/knowledge.md +1 -1
  107. package/src/assets/agents/learning.md +11 -0
  108. package/src/assets/agents/synthesize.md +1 -1
  109. package/src/assets/agents/test.md +16 -5
  110. package/src/assets/agents/tracker.md +467 -0
  111. package/src/assets/agents/validate.md +7 -5
  112. package/src/assets/commands/_partials/_engine.mds +11 -9
  113. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  114. package/src/assets/commands/_partials/_knowledge.mds +2 -2
  115. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  116. package/src/assets/commands/_partials/_preamble.mds +1 -1
  117. package/src/assets/commands/_partials/_publication.mds +3 -1
  118. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  119. package/src/assets/commands/_partials/_tracker.mds +18 -0
  120. package/src/assets/commands/_partials/_wave.mds +16 -10
  121. package/src/assets/commands/bug-analysis.mds +15 -5
  122. package/src/assets/commands/code-review.mds +34 -14
  123. package/src/assets/commands/debug.mds +11 -4
  124. package/src/assets/commands/dynamic-build.mds +227 -41
  125. package/src/assets/commands/dynamic-plan.mds +35 -13
  126. package/src/assets/commands/dynamic-tickets.mds +47 -5
  127. package/src/assets/commands/implement.mds +206 -52
  128. package/src/assets/commands/plan.mds +70 -17
  129. package/src/assets/commands/release.md +64 -17
  130. package/src/assets/commands/resolve.mds +126 -56
  131. package/src/assets/mds/git/_pr.mds +331 -0
  132. package/src/assets/mds/git/_references.mds +135 -0
  133. package/src/assets/mds/tracker/_common.mds +156 -0
  134. package/src/assets/mds/tracker/_github.mds +472 -0
  135. package/src/assets/mds/tracker/_jira.mds +407 -0
  136. package/src/assets/mds/tracker/_linear.mds +449 -0
  137. package/src/assets/mds/tracker/_mcp.mds +299 -0
  138. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  139. package/src/assets/scripts/hooks/background-memory-update +14 -9
  140. package/src/assets/scripts/hooks/capture-prompt +6 -2
  141. package/src/assets/scripts/hooks/capture-question +6 -2
  142. package/src/assets/scripts/hooks/capture-turn +6 -2
  143. package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
  144. package/src/assets/scripts/hooks/ensure-proxy +64 -5
  145. package/src/assets/scripts/hooks/ensure-root-gitignore +161 -60
  146. package/src/assets/scripts/hooks/hook-log-init +3 -1
  147. package/src/assets/scripts/hooks/json-helper.cjs +223 -5
  148. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -1
  149. package/src/assets/scripts/hooks/memory-worker +15 -8
  150. package/src/assets/scripts/hooks/pre-compact-memory +12 -8
  151. package/src/assets/scripts/hooks/preamble +1 -4
  152. package/src/assets/scripts/hooks/queue-append +68 -24
  153. package/src/assets/scripts/hooks/session-start-context +355 -8
  154. package/src/assets/scripts/hooks/session-start-memory +12 -8
  155. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  156. package/src/assets/scripts/redact-secrets.cjs +490 -62
  157. package/src/assets/scripts/release-trace.cjs +1143 -0
  158. package/src/assets/scripts/resolve-evidence-policy.cjs +1065 -0
  159. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  160. package/src/assets/skills/compliance/SKILL.md +2 -0
  161. package/src/assets/skills/docs-framework/SKILL.md +5 -3
  162. package/src/assets/skills/git/SKILL.md +8 -82
  163. package/src/assets/skills/git/references/github-api.md +179 -141
  164. package/src/assets/skills/git/references/patterns.md +11 -12
  165. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  166. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  167. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  168. package/src/targets/claude-code/templates/settings.json +0 -4
  169. package/src/assets/agents/git.md +0 -938
@@ -11,6 +11,17 @@
11
11
  * point every caller reaches). applyFlags(settingsJson, FlagsRecord) is the
12
12
  * sole API; init.ts works directly with FlagsRecord (no legacy string[] bridge).
13
13
  */
14
+ import { isDeepStrictEqual } from 'node:util';
15
+ /**
16
+ * Type predicate that narrows `ClaudeCodeFlag` to `EnvBooleanFlagDef`.
17
+ * Narrows `f.onPayload` to `string` so callers writing to the env string-map
18
+ * avoid an `unknown` hop without an unsafe cast.
19
+ *
20
+ * Use in `buildPayload` and `applyFlags` only where the string type is load-bearing.
21
+ */
22
+ export function isEnvBooleanFlag(f) {
23
+ return f.kind === 'boolean' && f.target.type === 'env';
24
+ }
14
25
  // ─── Registry ─────────────────────────────────────────────────────────────────
15
26
  // Phase 0 probe findings: see docs/reference/claude-code-flags-probe.md
16
27
  /**
@@ -110,8 +121,8 @@ export const FLAG_REGISTRY = [
110
121
  kind: 'boolean',
111
122
  target: { type: 'setting', key: 'disableBundledSkills' },
112
123
  onPayload: true,
113
- recommended: true,
114
- defaultValue: true,
124
+ recommended: false,
125
+ defaultValue: false,
115
126
  },
116
127
  {
117
128
  id: 'pin-sonnet-4-6',
@@ -122,8 +133,8 @@ export const FLAG_REGISTRY = [
122
133
  kind: 'boolean',
123
134
  target: { type: 'env', key: 'ANTHROPIC_DEFAULT_SONNET_MODEL' },
124
135
  onPayload: 'claude-sonnet-4-6',
125
- recommended: true,
126
- defaultValue: true,
136
+ recommended: false,
137
+ defaultValue: false,
127
138
  },
128
139
  {
129
140
  // Devflow fan-outs routinely exceed the upstream default of 20.
@@ -309,6 +320,24 @@ export const FLAG_REGISTRY = [
309
320
  recommended: false,
310
321
  defaultValue: false,
311
322
  },
323
+ {
324
+ // D27: gates Claude Code's AI-attribution injection into git commits and PRs.
325
+ // When true, writes {"commit":"","pr":""} to settings.json which suppresses attribution.
326
+ // When false/null (neutral), removes the key ONLY when the current value is the exact
327
+ // devflow-managed shape (settingDeleteGuard). A custom attribution value (e.g. an org
328
+ // name) is never deleted — shape guard prevents erasure (D-ATTR-GUARD).
329
+ id: 'suppress-attribution',
330
+ label: 'Suppress AI attribution',
331
+ description: 'Remove AI-attribution labels from git commits and pull requests',
332
+ hint: 'Suppresses Claude attribution labels in git commits and pull requests',
333
+ blurb: 'hide AI attribution labels',
334
+ kind: 'boolean',
335
+ target: { type: 'setting', key: 'attribution' },
336
+ onPayload: { commit: '', pr: '' },
337
+ settingDeleteGuard: { commit: '', pr: '' },
338
+ recommended: false,
339
+ defaultValue: false,
340
+ },
312
341
  // ── Valued flags (number/enum/string) ────────────────────────────────────
313
342
  {
314
343
  // Domain: unset by default; set only when users want a non-default spawn depth.
@@ -837,6 +866,78 @@ export function migrateLegacyFlagsToRecord(enabledIds, knownIds, legacyViewMode)
837
866
  return result;
838
867
  }
839
868
  // ─── Apply / Strip ────────────────────────────────────────────────────────────
869
+ /**
870
+ * Returns true when `value` equals the managed shape declared by `flag.settingDeleteGuard`.
871
+ * Returns false when: the flag has no guard, is not a setting-target boolean, or the value
872
+ * does not deep-equal the guard.
873
+ *
874
+ * Single equality oracle for managed-shape comparisons — used by `canDeleteSettingKey`,
875
+ * `settingHoldsManagedShape`, and the Step 2b adoption fold in `convergeFlagsIntoSettings`.
876
+ * All three delegate here so there is exactly one `isDeepStrictEqual` call for guard matching.
877
+ */
878
+ export function settingValueHoldsManagedShape(flag, value) {
879
+ if (flag.kind !== 'boolean' || flag.target.type !== 'setting')
880
+ return false;
881
+ const guard = flag.settingDeleteGuard;
882
+ if (guard === undefined)
883
+ return false;
884
+ return isDeepStrictEqual(value, guard);
885
+ }
886
+ /**
887
+ * D-ATTR-GUARD single-source predicate (consistency-01 / ADR-024).
888
+ *
889
+ * Returns true when the settings.json string contains a value at the flag's
890
+ * target key that equals the flag's managed shape (`settingDeleteGuard`).
891
+ *
892
+ * This is the ONE place that decides "on-disk value equals the managed shape".
893
+ * All consumers — `resolveExistingAttributionSuppression` (seeding priority),
894
+ * and the guarded-boolean adoption fold in `convergeFlagsIntoSettings` — delegate
895
+ * here rather than hand-rolling their own comparison.
896
+ *
897
+ * Returns false when:
898
+ * - settingsJson is malformed
899
+ * - root is not a plain object
900
+ * - flagId is not in the registry
901
+ * - the flag has no settingDeleteGuard
902
+ * - the flag is not a setting-target boolean
903
+ * - the on-disk value does not deep-equal the guard
904
+ */
905
+ export function settingHoldsManagedShape(settingsJson, flagId) {
906
+ try {
907
+ const parsed = JSON.parse(settingsJson);
908
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
909
+ return false;
910
+ const flag = FLAG_REGISTRY_MAP.get(flagId);
911
+ if (!flag)
912
+ return false;
913
+ const value = parsed[flag.target.key];
914
+ return settingValueHoldsManagedShape(flag, value);
915
+ }
916
+ catch {
917
+ return false;
918
+ }
919
+ }
920
+ /**
921
+ * D-ATTR-GUARD: returns true when it is safe to delete a flag's target key from
922
+ * settings. For flags with a settingDeleteGuard the key is only deleted when the
923
+ * current on-disk value deep-equals the managed shape, preventing erasure of
924
+ * user-customized values (e.g. an organisation attribution block) on flag
925
+ * disable or uninstall. Flags without a guard are always safe to delete.
926
+ *
927
+ * Delegates to `settingValueHoldsManagedShape` — the single equality oracle for
928
+ * managed-shape comparisons — so there is exactly one `isDeepStrictEqual` call
929
+ * across the entire apply/strip pipeline (ADR-024 mechanism 3).
930
+ *
931
+ * Single-source invariant: both the disable path (applyFlags neutral branch) and
932
+ * the uninstall path (stripFlags) collapse to this predicate.
933
+ */
934
+ function canDeleteSettingKey(flag, settings) {
935
+ if (flag.kind !== 'boolean')
936
+ return true;
937
+ if (flag.settingDeleteGuard === undefined)
938
+ return true;
939
+ return settingValueHoldsManagedShape(flag, settings[flag.target.key]);
940
+ }
840
941
  /**
841
942
  * Return `v` as a `Record<string, unknown>` only when it is a plain object.
842
943
  * Returns undefined for arrays, null, or non-objects.
@@ -853,8 +954,17 @@ function asPlainObject(v) {
853
954
  /** Compute the value to write to settings.json for an active flag. */
854
955
  function buildPayload(flag, value) {
855
956
  switch (flag.kind) {
856
- case 'boolean':
857
- return flag.onPayload;
957
+ case 'boolean': {
958
+ // D-PAYLOAD-CLONE: return a structural clone for object payloads so the
959
+ // FLAG_REGISTRY entry is never aliased into the caller's settings tree.
960
+ // Primitives (string, boolean) are values and require no clone.
961
+ // isEnvBooleanFlag narrows onPayload to string for env flags — no clone needed,
962
+ // and the string type avoids an `unknown` hop into the env string-map.
963
+ if (isEnvBooleanFlag(flag))
964
+ return flag.onPayload; // string — no clone needed
965
+ const p = flag.onPayload;
966
+ return typeof p === 'object' && p !== null ? structuredClone(p) : p;
967
+ }
858
968
  case 'enum':
859
969
  return value;
860
970
  case 'number':
@@ -904,7 +1014,8 @@ export function applyFlags(settingsJson, flags) {
904
1014
  delete env[flag.target.key];
905
1015
  }
906
1016
  else {
907
- delete settings[flag.target.key];
1017
+ if (canDeleteSettingKey(flag, settings))
1018
+ delete settings[flag.target.key];
908
1019
  }
909
1020
  }
910
1021
  else {
@@ -949,7 +1060,8 @@ export function stripFlags(settingsJson) {
949
1060
  delete env[flag.target.key];
950
1061
  }
951
1062
  else {
952
- delete settings[flag.target.key];
1063
+ if (canDeleteSettingKey(flag, settings))
1064
+ delete settings[flag.target.key];
953
1065
  }
954
1066
  }
955
1067
  if (env && Object.keys(env).length === 0) {
@@ -1116,6 +1228,35 @@ export function convergeFlagsIntoSettings(settingsJson, record, opts) {
1116
1228
  folded[flag.id] = coerced;
1117
1229
  }
1118
1230
  }
1231
+ // ── Step 2b: adopt guarded boolean settings before the strip ─────────────
1232
+ // D-ATTR-ADOPT (PF-050 / ADR-024): a delete guard is evidence about the VALUE,
1233
+ // never about the record — a pre-existing on-disk key whose value matches the
1234
+ // managed shape means devflow wrote it, so adopt it into the record now, before
1235
+ // stripFlags can unconditionally remove it on the next line.
1236
+ //
1237
+ // Mirror of the view-mode fold (Step 1): adopt only when:
1238
+ // (a) the flag has a settingDeleteGuard (guarded boolean only)
1239
+ // (b) the record does NOT already claim the key — a claimed false or null still
1240
+ // deletes the managed block (the user intentionally disabled it)
1241
+ // (c) the pre-strip on-disk value holds the managed shape
1242
+ //
1243
+ // This fold runs before stripFlags so applyFlags rewrites the block from the
1244
+ // now-claimed record and the block survives on BOTH the init and flags paths.
1245
+ for (const flag of FLAG_REGISTRY) {
1246
+ if (flag.kind !== 'boolean')
1247
+ continue;
1248
+ if (flag.target.type !== 'setting')
1249
+ continue;
1250
+ // Skip when the record already claims this flag (claimed false/null deletes the block)
1251
+ const claimed = claimedIn !== null &&
1252
+ Object.prototype.hasOwnProperty.call(claimedIn, flag.id);
1253
+ if (claimed)
1254
+ continue;
1255
+ // Adopt only when the on-disk value matches the managed shape exactly
1256
+ if (settingValueHoldsManagedShape(flag, parsed[flag.target.key])) {
1257
+ folded[flag.id] = true;
1258
+ }
1259
+ }
1119
1260
  // ── Step 3: strip all managed keys, then apply the folded record ──────────
1120
1261
  const stripped = stripFlags(settingsJson);
1121
1262
  const settings = applyFlags(stripped, folded);
@@ -3,6 +3,7 @@ import * as path from 'path';
3
3
  import { LEGACY_PLUGIN_NAMES, DELETED_PLUGIN_NAMES } from './plugins.js';
4
4
  import { VIEW_MODES, migrateLegacyFlagsToRecord, sanitizeFlagsRecord, } from './flags.js';
5
5
  import { normalizeComplianceFeature } from './compliance.js';
6
+ import { normalizeTrackerFeature } from './tracker.js';
6
7
  import { writeFileAtomicExclusive } from './fs-atomic.js';
7
8
  /**
8
9
  * Parse features.flags across the three on-disk shapes; reports whether a heal is owed.
@@ -54,13 +55,14 @@ function parseManifestFlags(features, knownFlags) {
54
55
  * Read and parse the manifest file. Returns null if missing or corrupt.
55
56
  *
56
57
  * Self-heals the following on-disk inconsistencies (applies ADR-014):
57
- * - features.kb → features.knowledge rename
58
- * - features.decisions → features.learning rename
58
+ * - features.kb → features.knowledge rename (both absent → true, D-FEATURES-ABSENT-ON)
59
+ * - features.decisions → features.learning rename (both absent → true, D-FEATURES-ABSENT-ON)
59
60
  * - features.flags as string[] → FlagsRecord (via migrateLegacyFlagsToRecord)
60
61
  * - features.viewMode folded into flags['view-mode'] and stripped from result
61
62
  * - features.knownFlags stripped from result (folded into FlagsRecord key-presence)
62
63
  * - features.proxy absent → false
63
64
  * - features.compliance absent/malformed → {enabled:false, frameworks:[]}
65
+ * - features.tracker absent/malformed → {provider:'github'}
64
66
  *
65
67
  * D39: heal-write failure returns the migrated in-memory manifest (not null).
66
68
  * The on-disk format remains unhealed; next read triggers another attempt.
@@ -82,15 +84,33 @@ export async function readManifest(devflowDir) {
82
84
  typeof data.updatedAt !== 'string') {
83
85
  return null;
84
86
  }
85
- // Self-heal: rename features.kb → features.knowledge on disk
87
+ // D-FEATURES-ABSENT-ON (a sub-decision of D-FEATURES-MACHINE-WIDE,
88
+ // src/core/feature-switch.ts): knowledge and learning are machine-wide
89
+ // switches, and every runtime gate — queue_read_gates in the hooks,
90
+ // isMachineFeatureOn in the CLI, the knowledge write-back prose gate — reads
91
+ // an ABSENT key as ON. This reader must agree: init seeds from it and every
92
+ // syncManifestFeature toggle writes its result back, so a `false` default
93
+ // here would persist "off" for a feature the runtime is running — a plain
94
+ // re-init or `devflow hud --enable` on an older manifest silently switching
95
+ // it off machine-wide. Explicit booleans (and their legacy names) are kept
96
+ // as they are. memory needs no default: it is in the hard-null set above,
97
+ // so a manifest without it reads as no prior install and init seeds
98
+ // FEATURE_DEFAULTS.memory (true).
99
+ //
100
+ // Self-heal: rename features.kb → features.knowledge on disk.
101
+ // Coalesce: features.knowledge wins; fall back to features.kb; default ON.
102
+ // D-KNOWLEDGE-LEGACY-KB: isMachineFeatureOn applies this exact precedence,
103
+ // so the legacy key is honoured before the heal lands.
86
104
  const knowledge = typeof features.knowledge === 'boolean' ? features.knowledge
87
105
  : typeof features.kb === 'boolean' ? features.kb
88
- : false;
89
- // Self-heal: rename features.decisions → features.learning on disk
90
- // Coalesce: features.learning wins; fall back to features.decisions; default false.
106
+ : true;
107
+ // Self-heal: rename features.decisions → features.learning on disk (ADR-011).
108
+ // Coalesce: features.learning wins; fall back to features.decisions; default ON.
109
+ // D-LEARNING-LEGACY-DECISIONS: isMachineFeatureOn and queue_read_gates apply
110
+ // this exact precedence, so the legacy key is honoured before the heal lands.
91
111
  const learning = typeof features.learning === 'boolean' ? features.learning
92
112
  : typeof features.decisions === 'boolean' ? features.decisions
93
- : false;
113
+ : true;
94
114
  // Self-heal: non-string-array or absent knownFlags/knownPlugins → undefined (never partial/garbage)
95
115
  const asStringArray = (val) => Array.isArray(val) && val.every(e => typeof e === 'string')
96
116
  ? val
@@ -138,6 +158,12 @@ export async function readManifest(devflowDir) {
138
158
  proxy: typeof features.proxy === 'boolean' ? features.proxy : false,
139
159
  // Self-heal: absent/malformed compliance → {enabled:false, frameworks:[]}
140
160
  compliance: normalizeComplianceFeature(features.compliance),
161
+ // Self-heal: absent/malformed/unknown tracker → {provider:'github'} (AC-3.21).
162
+ // Deliberately NOT in the hard-null set above — see the field's doc comment.
163
+ // Healing here is silent and emits no DEGRADED: that is the correct
164
+ // ADR-014 behaviour, and a different condition from a per-repo config
165
+ // value outside the domain (which does emit `unknown tracker provider`).
166
+ tracker: normalizeTrackerFeature(features.tracker),
141
167
  },
142
168
  installedAt: data.installedAt,
143
169
  updatedAt: data.updatedAt,