devflow-kit 3.0.1 → 3.1.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 (133) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/agents/git.md +2 -2
  3. package/dist/cli/agents-view/index.js +1 -1
  4. package/dist/cli/agents-view/render.js +2 -2
  5. package/dist/cli/agents-view/state.js +2 -2
  6. package/dist/cli/agents-view/terminal.js +5 -5
  7. package/dist/cli/commands/agents.js +7 -6
  8. package/dist/cli/commands/ambient.js +1 -1
  9. package/dist/cli/commands/attribution-prompts.js +8 -8
  10. package/dist/cli/commands/capture.js +1 -1
  11. package/dist/cli/commands/compliance-prompts.js +8 -8
  12. package/dist/cli/commands/compliance.js +8 -7
  13. package/dist/cli/commands/flags.js +33 -31
  14. package/dist/cli/commands/hud.js +1 -1
  15. package/dist/cli/commands/init-seed.js +9 -9
  16. package/dist/cli/commands/init.js +34 -32
  17. package/dist/cli/commands/install-report.js +10 -10
  18. package/dist/cli/commands/learning.js +267 -129
  19. package/dist/cli/commands/memory.js +1 -1
  20. package/dist/cli/commands/proxy.js +23 -23
  21. package/dist/cli/commands/rules.js +6 -5
  22. package/dist/cli/commands/tracker-prompts.js +6 -6
  23. package/dist/cli/commands/tracker.js +9 -9
  24. package/dist/cli/commands/uninstall.js +20 -20
  25. package/dist/cli/flags-view/render.js +5 -5
  26. package/dist/cli/flags-view/state.js +9 -9
  27. package/dist/cli/flags-view/terminal.js +4 -4
  28. package/dist/cli/tui/cells.js +1 -1
  29. package/dist/cli/tui/terminal.js +6 -6
  30. package/dist/commands/dynamic-build.md +18 -4
  31. package/dist/commands/dynamic-plan.md +19 -5
  32. package/dist/commands/dynamic-profile.md +17 -3
  33. package/dist/commands/dynamic-tickets.md +18 -4
  34. package/dist/commands/release.md +15 -1
  35. package/dist/commands/research.md +1 -1
  36. package/dist/commands/resolve.md +8 -9
  37. package/dist/core/agent-frontmatter.js +3 -3
  38. package/dist/core/agent-models.js +6 -6
  39. package/dist/core/agent-state.js +2 -2
  40. package/dist/core/ansi.js +2 -2
  41. package/dist/core/cache.js +7 -8
  42. package/dist/core/codex-auth-inspect.js +4 -4
  43. package/dist/core/compliance-compose.js +3 -3
  44. package/dist/core/compliance.js +3 -4
  45. package/dist/core/evidence-policy.js +14 -13
  46. package/dist/core/external-models.js +1 -1
  47. package/dist/core/feature-config.js +3 -3
  48. package/dist/core/feature-switch.js +3 -3
  49. package/dist/core/flags.js +25 -25
  50. package/dist/core/fs-atomic.js +6 -7
  51. package/dist/core/learning-queue-cleanup.js +16 -80
  52. package/dist/core/learning-store.js +61 -0
  53. package/dist/core/manifest.js +5 -5
  54. package/dist/core/mds-variants.js +13 -13
  55. package/dist/core/model-discovery.js +8 -8
  56. package/dist/core/observations.js +17 -101
  57. package/dist/core/orphan-sweep.js +4 -4
  58. package/dist/core/plugins.js +4 -5
  59. package/dist/core/project-paths.js +9 -13
  60. package/dist/core/proxy-log.js +8 -8
  61. package/dist/core/proxy-state.js +3 -3
  62. package/dist/core/reference-sweep.js +6 -6
  63. package/dist/core/teammate-mode-cleanup.js +1 -1
  64. package/dist/core/tracker.js +14 -14
  65. package/dist/hud/colors.js +2 -2
  66. package/dist/hud/components/learning-counts.js +2 -16
  67. package/dist/hud/components/version-badge.js +1 -1
  68. package/dist/skills/git/references/pr/resolve-review-threads.md +2 -2
  69. package/dist/targets/claude-code/compliance-install.js +17 -15
  70. package/dist/targets/claude-code/hooks.js +2 -2
  71. package/dist/targets/claude-code/installer.js +24 -24
  72. package/dist/targets/claude-code/legacy.js +1 -1
  73. package/dist/targets/claude-code/post-install.js +7 -7
  74. package/dist/targets/claude-code/tracker-install.js +2 -2
  75. package/package.json +1 -1
  76. package/src/assets/agents/code.md +1 -4
  77. package/src/assets/agents/design.md +2 -2
  78. package/src/assets/agents/diagnose.md +1 -1
  79. package/src/assets/agents/git.mds +2 -2
  80. package/src/assets/agents/knowledge.md +3 -3
  81. package/src/assets/agents/learning.md +281 -196
  82. package/src/assets/agents/research.md +1 -1
  83. package/src/assets/agents/review.md +3 -3
  84. package/src/assets/agents/scrutinize.md +1 -1
  85. package/src/assets/agents/skim.md +1 -1
  86. package/src/assets/agents/triage.md +9 -9
  87. package/src/assets/commands/_partials/_decisions.mds +8 -3
  88. package/src/assets/commands/_partials/_docs_root.mds +3 -3
  89. package/src/assets/commands/_partials/_engine.mds +1 -1
  90. package/src/assets/commands/_partials/_preamble.mds +6 -2
  91. package/src/assets/commands/_partials/_settings.mds +2 -2
  92. package/src/assets/commands/dynamic-build.mds +1 -1
  93. package/src/assets/commands/dynamic-plan.mds +3 -3
  94. package/src/assets/commands/dynamic-profile.mds +1 -1
  95. package/src/assets/commands/dynamic-tickets.mds +2 -2
  96. package/src/assets/commands/release.md +15 -1
  97. package/src/assets/commands/research.mds +1 -1
  98. package/src/assets/commands/resolve.mds +8 -9
  99. package/src/assets/mds/git/_pr.mds +3 -3
  100. package/src/assets/mds/tracker/_common.mds +1 -1
  101. package/src/assets/mds/tracker/_github.mds +1 -1
  102. package/src/assets/mds/tracker/_jira.mds +1 -1
  103. package/src/assets/mds/tracker/_linear.mds +1 -1
  104. package/src/assets/mds/tracker/_mcp.mds +6 -5
  105. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +1 -0
  106. package/src/assets/scripts/hooks/background-memory-update +28 -22
  107. package/src/assets/scripts/hooks/capture-turn +1 -17
  108. package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
  109. package/src/assets/scripts/hooks/ensure-proxy +5 -6
  110. package/src/assets/scripts/hooks/ensure-root-gitignore +1 -1
  111. package/src/assets/scripts/hooks/is-hex-sha +1 -1
  112. package/src/assets/scripts/hooks/json-helper.cjs +348 -814
  113. package/src/assets/scripts/hooks/json-parse +3 -2
  114. package/src/assets/scripts/hooks/lib/decisions-format.cjs +205 -156
  115. package/src/assets/scripts/hooks/lib/learning-store.cjs +3102 -0
  116. package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +7 -5
  117. package/src/assets/scripts/hooks/lib/project-paths.cjs +13 -19
  118. package/src/assets/scripts/hooks/lib/render-decisions.cjs +253 -226
  119. package/src/assets/scripts/hooks/queue-append +2 -2
  120. package/src/assets/scripts/hooks/resolve-project-root +3 -4
  121. package/src/assets/scripts/hooks/session-start-context +40 -18
  122. package/src/assets/scripts/lib/project-config.cjs +2 -2
  123. package/src/assets/scripts/pr-evidence.cjs +3 -3
  124. package/src/assets/scripts/redact-secrets.cjs +20 -20
  125. package/src/assets/scripts/release-trace.cjs +1 -1
  126. package/src/assets/scripts/resolve-evidence-policy.cjs +3 -3
  127. package/src/assets/scripts/resolve-settings.cjs +3 -3
  128. package/src/assets/scripts/verify-evidence.cjs +2 -2
  129. package/src/assets/skills/apply-decisions/SKILL.md +37 -17
  130. package/src/assets/skills/docs-framework/SKILL.md +2 -2
  131. package/src/assets/skills/feature-knowledge/SKILL.md +6 -5
  132. package/dist/core/observation-io.js +0 -50
  133. package/src/assets/scripts/hooks/decisions-usage-scan.cjs +0 -131
@@ -14,7 +14,7 @@ import { loadProjectConfigLib } from './evidence-policy.js';
14
14
  * carrying it would leave a `learning: false` in the file that no longer does
15
15
  * what it says. `decisions` is the pre-rename spelling of `learning`;
16
16
  * `autoCommit` is inert. `features` is deliberately NOT here — it is a live key,
17
- * carried like any other unmanaged key (avoids PF-071).
17
+ * carried like any other unmanaged key.
18
18
  */
19
19
  const RETIRED_CONFIG_KEYS = new Set([
20
20
  'memory', 'learning', 'knowledge', 'decisions', 'autoCommit',
@@ -77,7 +77,7 @@ function coerceConfig(parsed) {
77
77
  if (!isJsonObject(parsed))
78
78
  return null;
79
79
  const p = parsed;
80
- // Coerce reviewPublication: any invalid or absent value → 'auto' (self-heal, ADR-014 idiom).
80
+ // Coerce reviewPublication: any invalid or absent value → 'auto' (self-heal).
81
81
  const rp = p.reviewPublication;
82
82
  const reviewPublication = rp === 'auto' || rp === 'full' || rp === 'off' ? rp : 'auto';
83
83
  // The per-repo tracker override is carried through VERBATIM — never coerced,
@@ -185,7 +185,7 @@ async function writeConfigBody(projectRoot, body) {
185
185
  * Merge devflow's managed keys over the config body the file already holds.
186
186
  * Pure — returns a new object and never mutates `existing`.
187
187
  *
188
- * D-CONFIG-PRESERVE-UNMANAGED (avoids PF-071): `.devflow/config.json` is a
188
+ * D-CONFIG-PRESERVE-UNMANAGED: `.devflow/config.json` is a
189
189
  * user-editable file that devflow only PARTLY owns. The managed keys come from
190
190
  * `managed`; every other key comes from the file, verbatim and by key presence
191
191
  * — the hand-written per-repo `tracker` override (whose invalid values must
@@ -6,7 +6,7 @@ function isJsonObject(value) {
6
6
  }
7
7
  /**
8
8
  * The pre-rename key each machine feature was stored under, where one exists:
9
- * `learning` was `decisions` (ADR-011) and `knowledge` was `kb`. A manifest no
9
+ * `learning` was `decisions` and `knowledge` was `kb`. A manifest no
10
10
  * command has rewritten since the rename can still hold only the legacy key.
11
11
  * `memory` was never renamed.
12
12
  */
@@ -19,7 +19,7 @@ const LEGACY_KEYS = {
19
19
  *
20
20
  * Only an explicit boolean `false` switches a feature off. A missing key, a
21
21
  * non-boolean value, or anything that is not a manifest-shaped object reads as
22
- * ON — fail-open (ADR-028), and the exact rule `queue_read_gates` applies in the
22
+ * ON — fail-open, and the exact rule `queue_read_gates` applies in the
23
23
  * shell hooks, so the CLI's status and the runtime never disagree about the
24
24
  * same file.
25
25
  *
@@ -37,7 +37,7 @@ const LEGACY_KEYS = {
37
37
  * precedence exactly, so `devflow knowledge --status` reports a `kb: false`
38
38
  * as disabled. queue_read_gates never reads knowledge, so there is no shell
39
39
  * mirror. The knowledge write-back prose gate deliberately does not learn the
40
- * legacy key (ADR-028: no prompt text for a state only an un-upgraded install
40
+ * legacy key (no prompt text for a state only an un-upgraded install
41
41
  * can hold); readManifest rewrites `kb` to `knowledge` on the next CLI run
42
42
  * that loads the manifest, after which that gate reads the healed key.
43
43
  *
@@ -7,7 +7,7 @@
7
7
  * D14: Typed registry — flags carry kind (boolean|enum|number|string), target
8
8
  * (env|setting), and per-kind defaultValue. Neutral values delete their target
9
9
  * key; active values write the appropriate payload. Number 0 is ACTIVE. Sink
10
- * validation via coerceFlagValue (applies PF-023: validate at the convergence
10
+ * validation via coerceFlagValue (validate at the convergence
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
  */
@@ -139,7 +139,7 @@ export const FLAG_REGISTRY = [
139
139
  {
140
140
  // Devflow fan-outs routinely exceed the upstream default of 20.
141
141
  // Set to 40 by default so parallel Code/Review/Research waves don't
142
- // silently queue. upstreamDefault recorded for display. (applies PF-023 bounds)
142
+ // silently queue. upstreamDefault recorded for display.
143
143
  id: 'max-concurrent-subagents',
144
144
  label: 'Max concurrent subagents',
145
145
  description: 'Maximum number of subagents Claude Code will spawn concurrently',
@@ -150,7 +150,7 @@ export const FLAG_REGISTRY = [
150
150
  recommended: true,
151
151
  defaultValue: 40,
152
152
  min: 1,
153
- max: 100, // devflow sanity bound (applies PF-023)
153
+ max: 100, // devflow sanity bound
154
154
  integer: true,
155
155
  upstreamDefault: 20,
156
156
  },
@@ -341,7 +341,7 @@ export const FLAG_REGISTRY = [
341
341
  // ── Valued flags (number/enum/string) ────────────────────────────────────
342
342
  {
343
343
  // Domain: unset by default; set only when users want a non-default spawn depth.
344
- // upstreamDefault: 3 (recorded for display). PF-023 bounds: max 10.
344
+ // upstreamDefault: 3 (recorded for display). Sanity bound: max 10.
345
345
  id: 'subagent-spawn-depth',
346
346
  label: 'Max subagent spawn depth',
347
347
  description: 'Maximum depth of nested subagent spawning',
@@ -352,7 +352,7 @@ export const FLAG_REGISTRY = [
352
352
  recommended: false,
353
353
  defaultValue: undefined,
354
354
  min: 1,
355
- max: 10, // devflow sanity bound (applies PF-023)
355
+ max: 10, // devflow sanity bound
356
356
  integer: true,
357
357
  upstreamDefault: 3,
358
358
  },
@@ -384,7 +384,7 @@ export const FLAG_REGISTRY = [
384
384
  },
385
385
  {
386
386
  // Upstream default: 30 min. 0 = disabled (still ACTIVE — written to env).
387
- // PF-023 bounds: max 1440 (24h). min 0 (0 = off, explicit value not neutral).
387
+ // Sanity bounds: max 1440 (24h). min 0 (0 = off, explicit value not neutral).
388
388
  id: 'goal-checkin-minutes',
389
389
  label: 'Goal check-in interval',
390
390
  description: 'Interval in minutes for Claude to check in on task goals',
@@ -395,7 +395,7 @@ export const FLAG_REGISTRY = [
395
395
  recommended: false,
396
396
  defaultValue: undefined,
397
397
  min: 0, // 0 = off (ACTIVE, not neutral — written as "0")
398
- max: 1440, // devflow sanity bound: 24 hours (applies PF-023)
398
+ max: 1440, // devflow sanity bound: 24 hours
399
399
  integer: true,
400
400
  upstreamDefault: 30,
401
401
  },
@@ -411,7 +411,7 @@ export const FLAG_REGISTRY = [
411
411
  recommended: false,
412
412
  defaultValue: undefined,
413
413
  wrapKey: 'command',
414
- maxLength: 256, // devflow sanity bound (applies PF-023)
414
+ maxLength: 256, // devflow sanity bound
415
415
  },
416
416
  {
417
417
  // view-mode folded into the registry; neutralValue 'default' deletes the viewMode key.
@@ -474,7 +474,7 @@ export function isNeutral(flag, value) {
474
474
  /**
475
475
  * Map a record value to a TUI value.
476
476
  *
477
- * viewMode GLUE RULE (PF-017 one-shared-definition corollary): the mapping lives here,
477
+ * viewMode GLUE RULE (one shared definition): the mapping lives here,
478
478
  * next to neutralValueOf — the definition it depends on — not across a module boundary.
479
479
  * enum with neutralValue: neutralValue → null in TUI (null is the TUI representation
480
480
  * of "use the default"; the key is deleted when persisted).
@@ -494,7 +494,7 @@ export function recordToTui(flag, v) {
494
494
  /**
495
495
  * Map a TUI value back to a record value.
496
496
  *
497
- * viewMode GLUE RULE (PF-017 one-shared-definition corollary): inverse of recordToTui,
497
+ * viewMode GLUE RULE (one shared definition): inverse of recordToTui,
498
498
  * co-located with that function so the round-trip contract is auditable in one place.
499
499
  * enum with neutralValue: null → neutralValue (e.g. 'default').
500
500
  * All other values pass through unchanged.
@@ -509,7 +509,7 @@ export function tuiToRecord(flag, v) {
509
509
  }
510
510
  /**
511
511
  * Validate and coerce `raw` to a safe value for `flag` at the sink.
512
- * Returns null when the value is invalid (hostile-value defence — applies PF-023).
512
+ * Returns null when the value is invalid (hostile-value defence).
513
513
  *
514
514
  * Number invariants: finite, within [min, max], integer when required.
515
515
  * String invariants: within maxLength, no control characters.
@@ -567,7 +567,7 @@ export function coerceFlagValue(flag, raw) {
567
567
  * Parse a CLI text input to a FlagsRecordValue.
568
568
  * 'unset' (literal) → null for any flag.
569
569
  *
570
- * Number branch uses strict decimal grammar (applies PF-023 — invariant at the sink
570
+ * Number branch uses strict decimal grammar (invariant at the sink
571
571
  * every caller reaches, not per-caller): rejects empty, padded, hex, exponent,
572
572
  * and leading-zero forms. Equivalent to the TUI's strict parsing so both entry
573
573
  * points share one grammar.
@@ -749,7 +749,7 @@ export function readViewMode(record) {
749
749
  * Sanitize a FlagsRecord by coercing each known flag's value through
750
750
  * coerceFlagValue.
751
751
  *
752
- * Known flag IDs (applies ADR-014 key-presence semantics):
752
+ * Known flag IDs (key-presence semantics):
753
753
  * - explicit null input → kept as null (deliberately unset)
754
754
  * - valid non-null input → kept as coerced value
755
755
  * - invalid non-null input → KEY DROPPED (absent = adopt default on next init,
@@ -758,7 +758,7 @@ export function readViewMode(record) {
758
758
  * Unknown flag IDs (forward-compat):
759
759
  * - primitive values (boolean, number, string, null) → kept as-is
760
760
  * - non-primitive values (objects, arrays) → DROPPED to avoid laundering
761
- * untrusted shapes into FlagsRecordValue (applies PF-023)
761
+ * untrusted shapes into FlagsRecordValue
762
762
  *
763
763
  * D39: `__proto__`, `constructor`, `prototype` are always skipped.
764
764
  */
@@ -823,7 +823,7 @@ export function getDefaultFlagsRecord() {
823
823
  * Migrate a legacy (string-array) enabled-flags manifest to a typed FlagsRecord.
824
824
  * Called by manifest.ts self-healing when it encounters an old string-array manifest.
825
825
  *
826
- * Contract (applies ADR-014 transition semantics):
826
+ * Contract:
827
827
  * - knownIds defined → knownSet = knownIds ∪ enabledIds
828
828
  * - knownIds undefined → knownSet = full current registry ∪ enabledIds
829
829
  * (pre-knownFlags manifests: all flags known, so adopt-nothing is expressed
@@ -884,7 +884,7 @@ export function settingValueHoldsManagedShape(flag, value) {
884
884
  return isDeepStrictEqual(value, guard);
885
885
  }
886
886
  /**
887
- * D-ATTR-GUARD single-source predicate (consistency-01 / ADR-024).
887
+ * D-ATTR-GUARD single-source predicate (consistency-01).
888
888
  *
889
889
  * Returns true when the settings.json string contains a value at the flag's
890
890
  * target key that equals the flag's managed shape (`settingDeleteGuard`).
@@ -926,7 +926,7 @@ export function settingHoldsManagedShape(settingsJson, flagId) {
926
926
  *
927
927
  * Delegates to `settingValueHoldsManagedShape` — the single equality oracle for
928
928
  * managed-shape comparisons — so there is exactly one `isDeepStrictEqual` call
929
- * across the entire apply/strip pipeline (ADR-024 mechanism 3).
929
+ * across the entire apply/strip pipeline.
930
930
  *
931
931
  * Single-source invariant: both the disable path (applyFlags neutral branch) and
932
932
  * the uninstall path (stripFlags) collapse to this predicate.
@@ -980,7 +980,7 @@ function buildPayload(flag, value) {
980
980
  * Apply a FlagsRecord to a settings JSON string.
981
981
  *
982
982
  * - Unknown flag IDs are skipped (forward-compatible with future flags).
983
- * - `coerceFlagValue` is called at the sink before applying (applies PF-023).
983
+ * - `coerceFlagValue` is called at the sink before applying.
984
984
  * - Neutral values delete their target key.
985
985
  * - Env payloads for number flags are stringified ('40', never 40).
986
986
  * - Setting payloads for string flags with wrapKey are shaped ({ command: v }).
@@ -988,7 +988,7 @@ function buildPayload(flag, value) {
988
988
  * - `__proto__`, `constructor`, `prototype` keys are silently skipped.
989
989
  */
990
990
  export function applyFlags(settingsJson, flags) {
991
- // REL-M2 sink guard (applies PF-023): a non-plain-object root (null, array, scalar)
991
+ // REL-M2 sink guard: a non-plain-object root (null, array, scalar)
992
992
  // would cause a silent no-op or a confusing TypeError deep inside the loop.
993
993
  // Throw early with a clear message so every caller path is self-guarding.
994
994
  const root = JSON.parse(settingsJson);
@@ -1003,7 +1003,7 @@ export function applyFlags(settingsJson, flags) {
1003
1003
  const flag = FLAG_REGISTRY_MAP.get(id);
1004
1004
  if (!flag)
1005
1005
  continue; // unknown id — skip for forward compat
1006
- // Coerce at the sink (applies PF-023: validate at the convergence point)
1006
+ // Coerce at the sink (validate at the convergence point)
1007
1007
  const safe = coerceFlagValue(flag, value);
1008
1008
  if (isNeutral(flag, safe)) {
1009
1009
  // Neutral → delete the target key
@@ -1045,7 +1045,7 @@ export function applyFlags(settingsJson, flags) {
1045
1045
  * Cleans up empty env object. Strip-then-apply idempotence preserved (INV-1).
1046
1046
  */
1047
1047
  export function stripFlags(settingsJson) {
1048
- // REL-M2 sink guard (applies PF-023): mirror of applyFlags — throw early on a
1048
+ // REL-M2 sink guard: mirror of applyFlags — throw early on a
1049
1049
  // non-plain-object root so every caller path is self-guarding.
1050
1050
  const root = JSON.parse(settingsJson);
1051
1051
  if (root === null || typeof root !== 'object' || Array.isArray(root)) {
@@ -1122,7 +1122,7 @@ export function resolveFinalViewMode(current, selected, explicit) {
1122
1122
  // ─── Fold-before-strip pipeline ───────────────────────────────────────────────
1123
1123
  /**
1124
1124
  * Fold-before-strip pipeline — the single authoritative entry point for all
1125
- * settings.json mutation paths (applies PF-015, PF-017, ADR-014).
1125
+ * settings.json mutation paths.
1126
1126
  *
1127
1127
  * Both `init.ts` and `persistFlagConfig` (flags.ts) MUST call this instead of
1128
1128
  * invoking `stripFlags` + `applyFlags` directly; the invariant lives in the
@@ -1143,7 +1143,7 @@ export function resolveFinalViewMode(current, selected, explicit) {
1143
1143
  * A flag is "claimed" when it is present and non-null in the claimed set.
1144
1144
  * Claimed: record value wins (devflow previously set this value).
1145
1145
  * Unclaimed: fold from settings — if the user has a value in settings.json,
1146
- * adopt it into the record (ADR-014 adoption, devflow takes ownership).
1146
+ * adopt it into the record (devflow takes ownership).
1147
1147
  *
1148
1148
  * Boolean flags: never folded — on/off is always record-driven.
1149
1149
  *
@@ -1166,7 +1166,7 @@ export function resolveFinalViewMode(current, selected, explicit) {
1166
1166
  */
1167
1167
  export function convergeFlagsIntoSettings(settingsJson, record, opts) {
1168
1168
  // ── Step 1: fold view-mode (must read pre-strip) ──────────────────────────
1169
- // PF-015: resolveExistingViewMode reads the viewMode key. stripFlags removes
1169
+ // resolveExistingViewMode reads the viewMode key. stripFlags removes
1170
1170
  // it as part of the view-mode registry entry. Reading after strip silently
1171
1171
  // reverts an externally-set /focus.
1172
1172
  const folded = {
@@ -1229,7 +1229,7 @@ export function convergeFlagsIntoSettings(settingsJson, record, opts) {
1229
1229
  }
1230
1230
  }
1231
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,
1232
+ // D-ATTR-ADOPT: a delete guard is evidence about the VALUE,
1233
1233
  // never about the record — a pre-existing on-disk key whose value matches the
1234
1234
  // managed shape means devflow wrote it, so adopt it into the record now, before
1235
1235
  // stripFlags can unconditionally remove it on the next line.
@@ -5,11 +5,10 @@ import { promises as fs } from 'fs';
5
5
  * D34: Canonical atomic-write helper for the TypeScript CLI surface.
6
6
  *
7
7
  * Call sites: used by the CLI's exclusive-write call sites (migrations, init, post-install,
8
- * uninstall, security, ambient, memory, HUD, observation I/O).
9
- * The CJS counterpart (`writeExclusive` in `src/assets/scripts/hooks/json-helper.cjs` and
10
- * `src/assets/scripts/hooks/decisions-usage-scan.cjs`) intentionally remains a separate
11
- * implementation — same semantics, different module system. Any change to the
12
- * retry logic here MUST be mirrored in both CJS files.
8
+ * uninstall, security, ambient, memory, HUD).
9
+ * The CJS counterpart (`writeExclusive` in `src/assets/scripts/hooks/lib/learning-store.cjs`)
10
+ * intentionally remains a separate implementation — same semantics, different module
11
+ * system. Any change to the retry logic here MUST be mirrored there.
13
12
  */
14
13
  /**
15
14
  * Atomically write `filePath` by writing to a sibling `.tmp` then renaming.
@@ -34,7 +33,7 @@ export async function writeFileAtomicExclusive(filePath, data) {
34
33
  // PID-scope the tmp name so concurrent writers from different processes
35
34
  // (e.g., two Claude Code sessions) never collide on the same .tmp path.
36
35
  // mirrors proxy-log.ts rotation at src/core/proxy-log.ts which PID-scopes
37
- // for the same reason. avoids PF-011.
36
+ // for the same reason.
38
37
  const tmp = `${filePath}.tmp.${process.pid}`;
39
38
  try {
40
39
  await fs.writeFile(tmp, data, { encoding: 'utf-8', flag: 'wx' });
@@ -58,7 +57,7 @@ export async function writeFileAtomicExclusive(filePath, data) {
58
57
  //
59
58
  // Non-fatal path: if stat fails (ENOENT → fresh file, or any other I/O
60
59
  // error), skip chmod and keep the umask default — the write must still
61
- // complete correctly (avoids PF-009 failure-isolation principle).
60
+ // complete correctly.
62
61
  try {
63
62
  const { mode } = await fs.stat(filePath);
64
63
  // mode includes file-type bits; mask to permission bits only for chmod.
@@ -1,92 +1,28 @@
1
1
  /**
2
2
  * @file learning-queue-cleanup.ts
3
3
  *
4
- * Shared cleanup helpers for `.devflow/learning/`. Imported solely by
5
- * `src/cli/commands/learning.ts`:
6
- * - `sweepLegacyDreamMarkers` — used by `devflow learning --reset`
7
- * - `drainLearningQueue` — used by `devflow learning --clear` / `--disable`
4
+ * `drainLearningQueue`, the learning queue drain shared by `devflow learning
5
+ * --clear` and `--disable` (src/cli/commands/learning.ts) and by the drain
6
+ * `devflow init` runs when learning is switched off (src/cli/commands/init.ts).
8
7
  */
9
8
  import { promises as fs } from 'fs';
10
- import * as path from 'path';
11
- import { getLearningPendingTurnsPath, getLearningPendingTurnsProcessingPath, } from './project-paths.js';
12
- // ---------------------------------------------------------------------------
13
- // Legacy marker-pipeline sweep
14
- // ---------------------------------------------------------------------------
15
- /** Fixed-name stamps left by the retired dream marker pipeline. */
16
- const LEGACY_FIXED_STAMPS = ['.decisions-runs-today', '.curation-last', '.processor-spawned-at'];
17
- /** Per-session marker variants: (decisions|curation).*.{json,processing,retries,failed} */
18
- function isLegacyPerSessionMarker(name) {
19
- return ((name.startsWith('decisions.') || name.startsWith('curation.')) &&
20
- (name.endsWith('.json') || name.endsWith('.processing') || name.endsWith('.retries') || name.endsWith('.failed')));
21
- }
22
- /**
23
- * Sweep legacy marker-pipeline files from a `.devflow/learning/` directory:
24
- * the fixed-name stamps above, plus per-session `decisions.*`/`curation.*`
25
- * markers. Never touches `learning.json` or the live
26
- * `.pending-turns.jsonl`/`.pending-turns.processing` queue files.
27
- *
28
- * ENOENT-idempotent (missing learning dir or already-removed files are not
29
- * errors). Non-ENOENT errors are rethrown — callers that need best-effort
30
- * semantics (e.g. `--reset`, which must still finish releasing its lock)
31
- * should wrap the call in their own try/catch.
32
- *
33
- * @returns number of files removed
34
- */
35
- export async function sweepLegacyDreamMarkers(learningDir) {
36
- let removed = 0;
37
- for (const name of LEGACY_FIXED_STAMPS) {
38
- try {
39
- await fs.unlink(path.join(learningDir, name));
40
- removed++;
41
- }
42
- catch (err) {
43
- const code = err.code;
44
- if (code !== 'ENOENT')
45
- throw err;
46
- }
47
- }
48
- try {
49
- const entries = await fs.readdir(learningDir);
50
- for (const entry of entries) {
51
- if (isLegacyPerSessionMarker(entry)) {
52
- try {
53
- await fs.unlink(path.join(learningDir, entry));
54
- removed++;
55
- }
56
- catch (err) {
57
- const code = err.code;
58
- if (code !== 'ENOENT')
59
- throw err;
60
- }
61
- }
62
- }
63
- }
64
- catch (err) {
65
- const code = err.code;
66
- if (code !== 'ENOENT')
67
- throw err;
68
- }
69
- return removed;
70
- }
71
- // ---------------------------------------------------------------------------
72
- // Live queue drain
73
- // ---------------------------------------------------------------------------
9
+ import { getLearningClaimOwnerPath, getLearningPendingTurnsPath, getLearningPendingTurnsProcessingPath, } from './project-paths.js';
74
10
  /**
75
- * Drain the learning (decisions-detection) pending-turns queue so stale turns
76
- * don't process later — used by both `--clear` and `--disable`. A mid-run
77
- * Learning agent whose claimed batch vanishes aborts without changes, which is
78
- * the desired outcome in both cases. ENOENT-tolerant; other errors propagate.
11
+ * Drain the learning (decisions-detection) pending-turns queue, its claimed
12
+ * batch and the claim's owner file so stale turns don't process later — used by
13
+ * both `--clear` and `--disable`. A mid-run Learning agent whose claimed batch
14
+ * vanishes aborts without changes, which is the desired outcome in both cases.
15
+ * ENOENT-tolerant; other errors propagate.
79
16
  */
80
17
  export async function drainLearningQueue(gitRoot) {
18
+ const unlinkIfPresent = (file) => fs.unlink(file).catch((e) => {
19
+ if (e.code !== 'ENOENT')
20
+ throw e;
21
+ });
81
22
  await Promise.all([
82
- fs.unlink(getLearningPendingTurnsPath(gitRoot)).catch((e) => {
83
- if (e.code !== 'ENOENT')
84
- throw e;
85
- }),
86
- fs.unlink(getLearningPendingTurnsProcessingPath(gitRoot)).catch((e) => {
87
- if (e.code !== 'ENOENT')
88
- throw e;
89
- }),
23
+ unlinkIfPresent(getLearningPendingTurnsPath(gitRoot)),
24
+ unlinkIfPresent(getLearningPendingTurnsProcessingPath(gitRoot)),
25
+ unlinkIfPresent(getLearningClaimOwnerPath(gitRoot)),
90
26
  ]);
91
27
  }
92
28
  //# sourceMappingURL=learning-queue-cleanup.js.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The CLI's view of the learning store — a typed seam onto the package's own
3
+ * `hooks/lib/learning-store.cjs`, never a second implementation of it.
4
+ *
5
+ * D-LEARNING-STORE-SEAM: `devflow learning` reads and writes the learning tree only
6
+ * through the store module json-helper's ops and the hooks run, loaded with the
7
+ * evidence-policy seam's `loadScript` from the package's own scripts directory and
8
+ * shape-checked against LEARNING_STORE_SURFACE; the interfaces below are
9
+ * TRANSCRIBED from the store's JSDoc and are this side's only shape authority.
10
+ * Reason: the CLI's own TypeScript reader judged every row by the v1 shape, so it
11
+ * counted v2 observations as invalid, and its `--clear` truncated the log without
12
+ * the lock every learning writer takes.
13
+ *
14
+ * As with the evidence resolver (D-POLICY-CJS-SEAM), the CLI loads the package
15
+ * copy, which `npm` installs with this CLI, not `~/.devflow/scripts`, which
16
+ * `devflow init` refreshes: the two can differ until the next init.
17
+ */
18
+ import { join } from 'path';
19
+ import { scriptsDir } from './assets.js';
20
+ import { loadScript } from './evidence-policy.js';
21
+ /** The store, relative to src/assets/scripts/ (and ~/.devflow/scripts/). */
22
+ export const LEARNING_STORE_SCRIPT_NAME = join('hooks', 'lib', 'learning-store.cjs');
23
+ /**
24
+ * Every key of LearningStoreModule and the runtime kind the loader requires of
25
+ * it. `satisfies` makes the compiler reject an interface key missing here.
26
+ */
27
+ export const LEARNING_STORE_SURFACE = Object.freeze({
28
+ INACTIVE_STATUSES: 'string-array',
29
+ ANCHOR_ID_RE: 'regexp',
30
+ readLearningState: 'function',
31
+ buildListing: 'function',
32
+ readListing: 'function',
33
+ formatListing: 'function',
34
+ showByKey: 'function',
35
+ restoreAnchor: 'function',
36
+ clearUnreferenced: 'function',
37
+ resetLearning: 'function',
38
+ });
39
+ /**
40
+ * Load the learning store from `dir` (default: the package's own scripts
41
+ * directory) and shape-check its surface. Never throws: a missing file is
42
+ * `not-found`; a module that throws on load or lacks a surface key is `unusable`.
43
+ */
44
+ export function loadLearningStore(dir = scriptsDir()) {
45
+ return loadScript(join(dir, LEARNING_STORE_SCRIPT_NAME), LEARNING_STORE_SURFACE);
46
+ }
47
+ /**
48
+ * Why the store cannot be used, and the remedy: a package reinstall, since the
49
+ * CLI loads the package's own copy, which `devflow init` does not restore.
50
+ */
51
+ export function formatLearningStoreUnavailable(error) {
52
+ switch (error.kind) {
53
+ case 'not-found': return 'learning store not found — reinstall devflow-kit';
54
+ case 'unusable': return 'learning store failed to load — reinstall devflow-kit';
55
+ default: {
56
+ const exhaustive = error;
57
+ return exhaustive;
58
+ }
59
+ }
60
+ }
61
+ //# sourceMappingURL=learning-store.js.map
@@ -54,7 +54,7 @@ function parseManifestFlags(features, knownFlags) {
54
54
  /**
55
55
  * Read and parse the manifest file. Returns null if missing or corrupt.
56
56
  *
57
- * Self-heals the following on-disk inconsistencies (applies ADR-014):
57
+ * Self-heals the following on-disk inconsistencies:
58
58
  * - features.kb → features.knowledge rename (both absent → true, D-FEATURES-ABSENT-ON)
59
59
  * - features.decisions → features.learning rename (both absent → true, D-FEATURES-ABSENT-ON)
60
60
  * - features.flags as string[] → FlagsRecord (via migrateLegacyFlagsToRecord)
@@ -103,7 +103,7 @@ export async function readManifest(devflowDir) {
103
103
  const knowledge = typeof features.knowledge === 'boolean' ? features.knowledge
104
104
  : typeof features.kb === 'boolean' ? features.kb
105
105
  : true;
106
- // Self-heal: rename features.decisions → features.learning on disk (ADR-011).
106
+ // Self-heal: rename features.decisions → features.learning on disk.
107
107
  // Coalesce: features.learning wins; fall back to features.decisions; default ON.
108
108
  // D-LEARNING-LEGACY-DECISIONS: isMachineFeatureOn and queue_read_gates apply
109
109
  // this exact precedence, so the legacy key is honoured before the heal lands.
@@ -122,7 +122,7 @@ export async function readManifest(devflowDir) {
122
122
  // `flagsWereLegacy` is true only when the on-disk shape was a string[] (Case A),
123
123
  // keeping the needsHeal predicate in lockstep with the parse branch above.
124
124
  const { flags: parsedFlags, legacy: flagsWereLegacy } = parseManifestFlags(features, knownFlags);
125
- // PF-023 + D39: sanitize all values; block prototype pollution keys.
125
+ // D39: sanitize all values; block prototype pollution keys.
126
126
  const sanitizedFlags = sanitizeFlagsRecord(parsedFlags);
127
127
  // needsHeal when any legacy artifact is present on disk
128
128
  const needsHeal = features.kb !== undefined ||
@@ -154,14 +154,14 @@ export async function readManifest(devflowDir) {
154
154
  security: typeof features.security === 'string' && SECURITY_MODES.includes(features.security)
155
155
  ? features.security
156
156
  : undefined,
157
- // Self-heal: absent proxy field defaults to false (applies ADR-014 self-heal idiom)
157
+ // Self-heal: absent proxy field defaults to false
158
158
  proxy: typeof features.proxy === 'boolean' ? features.proxy : false,
159
159
  // Self-heal: absent/malformed compliance → {enabled:false, frameworks:[]}
160
160
  compliance: normalizeComplianceFeature(features.compliance),
161
161
  // Self-heal: absent/malformed/unknown tracker → {provider:'github'} (AC-3.21).
162
162
  // Deliberately NOT in the hard-null set above — see the field's doc comment.
163
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
164
+ // self-heal behaviour, and a different condition from a per-repo config
165
165
  // value outside the domain (which does emit `unknown tracker provider`).
166
166
  tracker: normalizeTrackerFeature(features.tracker),
167
167
  },
@@ -4,11 +4,11 @@
4
4
  * Pure module — zero I/O. Every question a caller asks about a HOST is answered
5
5
  * with a Result; callers own every filesystem call and every process exit.
6
6
  *
7
- * applies ADR-013: pure core-layer module, no build-script or adapter concerns.
7
+ * Pure core-layer module, no build-script or adapter concerns.
8
8
  * The registries below are agent-neutral, so what is DERIVED from them is derived
9
9
  * here rather than inside an install target — a target adapter computing a build
10
10
  * fact, with tests importing that adapter to learn it, is the seam inverting.
11
- * avoids PF-014: no process.exit(); every fallible path returns Result. The
11
+ * No process.exit(); every fallible path returns Result. The
12
12
  * exiting shell is scripts/build-mds.ts, which renders these errors into its
13
13
  * pre-existing messages.
14
14
  *
@@ -85,7 +85,7 @@ export function validateOutputName(name) {
85
85
  * `tracker/jira/`, and `tracker/mcp.md` would read as a third provider.
86
86
  * Relaxing the shared rule instead would have admitted `_anything.md`
87
87
  * as a command or an agent basename too — a widening across all three build
88
- * destinations to buy a property only this one needs (ADR-025: classify the case,
88
+ * destinations to buy a property only this one needs (classify the case,
89
89
  * never blanket-widen).
90
90
  *
91
91
  * Every other guarantee is inherited by delegation, so the dot-segment,
@@ -127,8 +127,8 @@ export const AGENTS_OUTPUT_DIR = 'dist/agents';
127
127
  * the installer decides which skill install triggers the reference overlay from
128
128
  * it, and the init summary renders `prefixSkillName()` of it. Retyped at each of
129
129
  * those three sites, moving the references to another skill would mean finding
130
- * all three spellings with nothing failing if only two were found — the PF-013
131
- * shape, a hardcoded spelling that still resolves.
130
+ * all three spellings with nothing failing if only two were found — a hardcoded
131
+ * spelling that still resolves.
132
132
  *
133
133
  * Bare, not `devflow:`-prefixed: the build writes to `dist/skills/git/…` while
134
134
  * the install target is `skills/devflow:git/`. prefixSkillName is what spans that
@@ -162,7 +162,7 @@ const ALLOWED_OUTPUT_DIRS = [
162
162
  *
163
163
  * Exported so guards assert the build's refusal text against the table itself
164
164
  * rather than against a retyped literal: adding a destination then rewrites both
165
- * the message and its assertion from one edit (PF-018 — the expectation must
165
+ * the message and its assertion from one edit (the expectation must
166
166
  * come from the thing under test, not a copy of it).
167
167
  */
168
168
  export const ALLOWED_OUTPUT_DIR_NAMES = ALLOWED_OUTPUT_DIRS.map(entry => entry.dir);
@@ -223,7 +223,7 @@ export function resolveOutputDir(root, declared) {
223
223
  * reverse alone lets a listed op silently emit nothing.
224
224
  *
225
225
  * The list is long from its first commit on purpose. A one- or two-element list
226
- * makes every parity assertion over it vacuous (GAP-42, the PF-018 trap) and is
226
+ * makes every parity assertion over it vacuous (GAP-42) and is
227
227
  * structurally identical to the single-arm conditional AC-1.2 forbids, so
228
228
  * expandVariants refuses a pair list below MIN_VARIANT_PAIRS.
229
229
  */
@@ -283,8 +283,8 @@ export const TRACKER_GITHUB_OPS = TRACKER_OPS;
283
283
  * carries one pointer to each.
284
284
  *
285
285
  * 9 entries, one above {@link MIN_VARIANT_PAIRS}, which is a floor and not a
286
- * target: a shorter roster makes every parity assertion over it vacuous (GAP-42,
287
- * the PF-018 trap) and `expandVariants` refuses the build, so the roster can grow
286
+ * target: a shorter roster makes every parity assertion over it vacuous (GAP-42)
287
+ * and `expandVariants` refuses the build, so the roster can grow
288
288
  * but never drop below 8. `update-pr-evidence` (#363) is the ninth — it edits the
289
289
  * PR body and comments on the PR, both GitHub whatever the tracker is.
290
290
  */
@@ -558,7 +558,7 @@ export const MIN_VARIANT_PAIRS = 8;
558
558
  * writes.
559
559
  *
560
560
  * Pure and total: every refusal is a Result, so the build shell keeps its single
561
- * exit (avoids PF-014). The expansion is deliberately flat rather than nested —
561
+ * exit. The expansion is deliberately flat rather than nested —
562
562
  * one list of destinations is what the plan pass needs to detect two hosts
563
563
  * claiming one file, and a nested shape would have to be flattened there anyway.
564
564
  *
@@ -642,14 +642,14 @@ export function expandVariants(modules = resolveVariantModules()) {
642
642
  * Lives beside the registry it reads rather than in the Claude Code installer that
643
643
  * consumes it: nothing about the answer is Claude-Code-specific, and the packaging
644
644
  * and containment tests that read it are asking the BUILD what it emits, not
645
- * asking an install target (applies ADR-013).
645
+ * asking an install target.
646
646
  *
647
647
  * Asserts where its siblings return a Result. The registry is a compile-time
648
648
  * constant, so a refusal is a programming error rather than an install-time
649
649
  * degradation: no caller could sensibly continue, and every caller would otherwise
650
650
  * carry the same impossible branch. The full refusal is rendered and not just its
651
651
  * `kind` — the payload is what names the offending module and op, and a payload
652
- * nothing reads is a payload nothing maintains (avoids PF-041). Same rendering the
652
+ * nothing reads is a payload nothing maintains. Same rendering the
653
653
  * build's own refusal sinks use (scripts/build-mds.ts).
654
654
  */
655
655
  export function generatedReferenceManifest() {
@@ -721,7 +721,7 @@ export const VARIANT_SECTION_MARKER_RE = /^<!-- op: (_?[a-z0-9][a-z0-9._-]{0,63}
721
721
  *
722
722
  * Bidirectional, and both directions are load-bearing:
723
723
  * - unknown-section — the body carries a section for an op the registry does
724
- * not name, so a file would ship that nothing loads (ADR-003);
724
+ * not name, so a file would ship that nothing loads;
725
725
  * - missing-section — the registry names an op the body does not cover, so the
726
726
  * preamble's load instruction resolves to nothing at runtime.
727
727
  * A forward-only check passes on either half of that pair.