devflow-kit 3.0.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +1 -1
  3. package/dist/agents/git.md +2 -2
  4. package/dist/cli/agents-view/index.js +1 -1
  5. package/dist/cli/agents-view/render.js +71 -17
  6. package/dist/cli/agents-view/state.js +42 -16
  7. package/dist/cli/agents-view/terminal.js +5 -5
  8. package/dist/cli/commands/agents.js +142 -51
  9. package/dist/cli/commands/ambient.js +1 -1
  10. package/dist/cli/commands/attribution-prompts.js +8 -8
  11. package/dist/cli/commands/capture.js +1 -1
  12. package/dist/cli/commands/compliance-prompts.js +8 -8
  13. package/dist/cli/commands/compliance.js +8 -7
  14. package/dist/cli/commands/flags.js +33 -31
  15. package/dist/cli/commands/hud.js +1 -1
  16. package/dist/cli/commands/init-seed.js +9 -9
  17. package/dist/cli/commands/init.js +162 -85
  18. package/dist/cli/commands/install-report.js +10 -10
  19. package/dist/cli/commands/learning.js +302 -136
  20. package/dist/cli/commands/memory.js +36 -15
  21. package/dist/cli/commands/proxy.js +23 -23
  22. package/dist/cli/commands/rules.js +6 -5
  23. package/dist/cli/commands/tracker-prompts.js +6 -6
  24. package/dist/cli/commands/tracker.js +9 -9
  25. package/dist/cli/commands/uninstall.js +183 -59
  26. package/dist/cli/flags-view/render.js +5 -5
  27. package/dist/cli/flags-view/state.js +9 -9
  28. package/dist/cli/flags-view/terminal.js +4 -4
  29. package/dist/cli/tui/cells.js +1 -1
  30. package/dist/cli/tui/terminal.js +6 -6
  31. package/dist/commands/code-review.md +0 -2
  32. package/dist/commands/debug.md +14 -11
  33. package/dist/commands/dynamic-build.md +51 -47
  34. package/dist/commands/dynamic-plan.md +27 -7
  35. package/dist/commands/dynamic-profile.md +17 -3
  36. package/dist/commands/dynamic-tickets.md +18 -4
  37. package/dist/commands/explore.md +9 -3
  38. package/dist/commands/implement.md +20 -16
  39. package/dist/commands/plan.md +13 -9
  40. package/dist/commands/release.md +23 -3
  41. package/dist/commands/research.md +9 -3
  42. package/dist/commands/resolve.md +9 -12
  43. package/dist/commands/self-review.md +0 -2
  44. package/dist/core/agent-frontmatter.js +28 -3
  45. package/dist/core/agent-models.js +204 -42
  46. package/dist/core/agent-state.js +28 -6
  47. package/dist/core/ansi.js +2 -2
  48. package/dist/core/assets.js +1 -1
  49. package/dist/core/cache.js +7 -8
  50. package/dist/core/codex-auth-inspect.js +4 -4
  51. package/dist/core/compliance-compose.js +3 -3
  52. package/dist/core/compliance.js +3 -4
  53. package/dist/core/evidence-policy.js +14 -13
  54. package/dist/core/external-models.js +1 -1
  55. package/dist/core/feature-config.js +71 -13
  56. package/dist/core/feature-switch.js +3 -3
  57. package/dist/core/flags.js +49 -25
  58. package/dist/core/fs-atomic.js +6 -7
  59. package/dist/core/learning-queue-cleanup.js +16 -81
  60. package/dist/core/learning-store.js +61 -0
  61. package/dist/core/linked-path.js +46 -0
  62. package/dist/core/manifest.js +5 -5
  63. package/dist/core/mds-variants.js +13 -13
  64. package/dist/core/model-discovery.js +8 -8
  65. package/dist/core/observations.js +17 -101
  66. package/dist/core/orphan-sweep.js +4 -4
  67. package/dist/core/plugins.js +13 -8
  68. package/dist/core/project-paths.js +9 -13
  69. package/dist/core/proxy-log.js +8 -8
  70. package/dist/core/proxy-state.js +3 -3
  71. package/dist/core/queue-drain.js +31 -0
  72. package/dist/core/reference-sweep.js +6 -6
  73. package/dist/core/teammate-mode-cleanup.js +1 -1
  74. package/dist/core/tracker.js +14 -14
  75. package/dist/hud/colors.js +2 -2
  76. package/dist/hud/components/learning-counts.js +54 -22
  77. package/dist/hud/components/version-badge.js +1 -1
  78. package/dist/skills/git/references/pr/resolve-review-threads.md +2 -2
  79. package/dist/skills/git/references/tracker/github/create-release.md +2 -2
  80. package/dist/skills/git/references/tracker/jira/create-release.md +2 -2
  81. package/dist/skills/git/references/tracker/linear/create-release.md +2 -2
  82. package/dist/targets/claude-code/compliance-install.js +17 -15
  83. package/dist/targets/claude-code/hooks.js +2 -2
  84. package/dist/targets/claude-code/installer.js +59 -32
  85. package/dist/targets/claude-code/legacy.js +1 -1
  86. package/dist/targets/claude-code/post-install.js +135 -45
  87. package/dist/targets/claude-code/tracker-install.js +2 -2
  88. package/package.json +1 -1
  89. package/src/assets/agents/code.md +15 -21
  90. package/src/assets/agents/design.md +4 -2
  91. package/src/assets/agents/diagnose.md +3 -1
  92. package/src/assets/agents/evaluate.md +4 -0
  93. package/src/assets/agents/git.mds +2 -2
  94. package/src/assets/agents/knowledge.md +5 -3
  95. package/src/assets/agents/learning.md +281 -196
  96. package/src/assets/agents/research.md +3 -1
  97. package/src/assets/agents/review.md +5 -3
  98. package/src/assets/agents/scrutinize.md +5 -1
  99. package/src/assets/agents/simplify.md +4 -0
  100. package/src/assets/agents/skim.md +4 -2
  101. package/src/assets/agents/synthesize.md +6 -0
  102. package/src/assets/agents/test.md +18 -10
  103. package/src/assets/agents/triage.md +11 -9
  104. package/src/assets/agents/validate.md +14 -10
  105. package/src/assets/commands/_partials/_decisions.mds +8 -3
  106. package/src/assets/commands/_partials/_docs_root.mds +3 -3
  107. package/src/assets/commands/_partials/_engine.mds +16 -32
  108. package/src/assets/commands/_partials/_knowledge.mds +0 -2
  109. package/src/assets/commands/_partials/_preamble.mds +6 -2
  110. package/src/assets/commands/_partials/_settings.mds +2 -2
  111. package/src/assets/commands/_partials/_tracker.mds +1 -1
  112. package/src/assets/commands/code-review.mds +0 -2
  113. package/src/assets/commands/debug.mds +13 -8
  114. package/src/assets/commands/dynamic-build.mds +18 -12
  115. package/src/assets/commands/dynamic-plan.mds +10 -4
  116. package/src/assets/commands/dynamic-profile.mds +1 -1
  117. package/src/assets/commands/dynamic-tickets.mds +2 -2
  118. package/src/assets/commands/explore.mds +9 -1
  119. package/src/assets/commands/implement.mds +19 -13
  120. package/src/assets/commands/plan.mds +12 -8
  121. package/src/assets/commands/release.md +23 -3
  122. package/src/assets/commands/research.mds +9 -3
  123. package/src/assets/commands/resolve.mds +9 -10
  124. package/src/assets/mds/git/_pr.mds +3 -3
  125. package/src/assets/mds/tracker/_common.mds +1 -1
  126. package/src/assets/mds/tracker/_github.mds +3 -3
  127. package/src/assets/mds/tracker/_jira.mds +3 -3
  128. package/src/assets/mds/tracker/_linear.mds +3 -3
  129. package/src/assets/mds/tracker/_mcp.mds +6 -5
  130. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -2
  131. package/src/assets/scripts/hooks/background-memory-update +97 -33
  132. package/src/assets/scripts/hooks/capture-prompt +4 -3
  133. package/src/assets/scripts/hooks/capture-question +4 -3
  134. package/src/assets/scripts/hooks/capture-turn +5 -20
  135. package/src/assets/scripts/hooks/ensure-devflow-init +14 -2
  136. package/src/assets/scripts/hooks/ensure-proxy +5 -6
  137. package/src/assets/scripts/hooks/ensure-root-gitignore +123 -11
  138. package/src/assets/scripts/hooks/git-marker +71 -0
  139. package/src/assets/scripts/hooks/is-hex-sha +1 -1
  140. package/src/assets/scripts/hooks/json-helper.cjs +345 -944
  141. package/src/assets/scripts/hooks/json-parse +25 -129
  142. package/src/assets/scripts/hooks/lib/decisions-format.cjs +205 -156
  143. package/src/assets/scripts/hooks/lib/learning-store.cjs +3207 -0
  144. package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +7 -5
  145. package/src/assets/scripts/hooks/lib/project-paths.cjs +13 -19
  146. package/src/assets/scripts/hooks/lib/render-decisions.cjs +253 -226
  147. package/src/assets/scripts/hooks/memory-worker +10 -0
  148. package/src/assets/scripts/hooks/pre-compact-memory +66 -14
  149. package/src/assets/scripts/hooks/preamble +9 -1
  150. package/src/assets/scripts/hooks/queue-append +55 -23
  151. package/src/assets/scripts/hooks/resolve-project-root +3 -4
  152. package/src/assets/scripts/hooks/session-start-context +146 -45
  153. package/src/assets/scripts/hooks/session-start-memory +33 -11
  154. package/src/assets/scripts/lib/project-config.cjs +2 -2
  155. package/src/assets/scripts/pr-evidence.cjs +3 -3
  156. package/src/assets/scripts/redact-secrets.cjs +20 -20
  157. package/src/assets/scripts/release-trace.cjs +1 -1
  158. package/src/assets/scripts/resolve-evidence-policy.cjs +3 -3
  159. package/src/assets/scripts/resolve-settings.cjs +3 -3
  160. package/src/assets/scripts/verify-evidence.cjs +2 -2
  161. package/src/assets/skills/apply-decisions/SKILL.md +37 -17
  162. package/src/assets/skills/docs-framework/SKILL.md +2 -2
  163. package/src/assets/skills/feature-knowledge/SKILL.md +6 -5
  164. package/src/assets/skills/test-driven-development/SKILL.md +6 -4
  165. package/dist/core/observation-io.js +0 -50
  166. package/src/assets/scripts/hooks/decisions-usage-scan.cjs +0 -131
@@ -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
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @file linked-path.ts
3
+ *
4
+ * `firstSymbolicLink`, the check the CLI makes before it writes or deletes under a
5
+ * project's `.devflow/`, or under its `.claude/` when uninstall removes a legacy
6
+ * local install (D-CLI-NO-SYMLINK).
7
+ */
8
+ import { promises as fs } from 'fs';
9
+ /**
10
+ * The first of `paths` that is itself a symbolic link, or null when none is.
11
+ *
12
+ * D-CLI-NO-SYMLINK: the CLI writes or deletes under a project's `.devflow/` only
13
+ * where neither `.devflow` nor the entry it acts on through it is a symbolic link:
14
+ * `devflow init` stamps its carve-out marker, removes the legacy markers and
15
+ * writes `.devflow/config.json` only then, `devflow learning --configure` writes
16
+ * the project's `learning.json` only then, and the queue drains (`devflow learning
17
+ * --clear|--disable`, `devflow memory --disable|--clear` and `devflow init
18
+ * --no-learning|--no-memory`) delete nothing when `.devflow` or the queue's folder
19
+ * is one. `devflow uninstall`, removing a legacy local install, deletes or rewrites
20
+ * nothing under the project's `.devflow` or `.claude` where either, or anything
21
+ * below it on the way to what it removes, is one (legacyLocalChangeGuard in
22
+ * uninstall.ts). Reason: a repository can commit `.devflow`, or a folder or file in
23
+ * it, as a link to any place on the machine, and a write or delete through one lands
24
+ * wherever it points. The hooks hold the same rule (D-HOOKS-NO-SYMLINK, git-marker)
25
+ * and so do the learning ops (D-NO-LINKED-TREE). Only paths below the project root
26
+ * are passed here: the root and the folders above it are the user's choice.
27
+ *
28
+ * Each path is checked with lstat, so a link is seen rather than followed. Nothing at
29
+ * a path, or a file where a folder was expected on the way to it, is no link; any
30
+ * other lstat failure is thrown, so a caller that cannot check acts on nothing.
31
+ */
32
+ export async function firstSymbolicLink(paths) {
33
+ for (const candidate of paths) {
34
+ try {
35
+ if ((await fs.lstat(candidate)).isSymbolicLink())
36
+ return candidate;
37
+ }
38
+ catch (error) {
39
+ const code = error.code;
40
+ if (code !== 'ENOENT' && code !== 'ENOTDIR')
41
+ throw error;
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+ //# sourceMappingURL=linked-path.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.
@@ -4,14 +4,14 @@
4
4
  * Discovers routable external models from the routing runtime.
5
5
  * The only impure piece of the external-model-routing design.
6
6
  *
7
- * applies ADR-013: pure core-layer module — no adapter imports.
8
- * applies PF-013: cwd = os.tmpdir() so discovery never assumes the devflow
7
+ * Pure core-layer module — no adapter imports.
8
+ * cwd = os.tmpdir() so discovery never assumes the devflow
9
9
  * directory exists (the --set read path never mkdirs, so the dir may not
10
10
  * exist; os.tmpdir() also prevents a stale subswitch.config.json in any
11
11
  * particular working directory from causing exit 1 in the routing runtime).
12
- * avoids PF-009: discoverExternalModels never throws or rejects — every
12
+ * discoverExternalModels never throws or rejects — every
13
13
  * failure path returns { known: false }.
14
- * avoids PF-016: real-binary tests (T1–T4) live in tests/, never in
14
+ * Real-binary tests (T1–T4) live in tests/, never in
15
15
  * tests/integration/ which is excluded from npm test by vitest.config.ts.
16
16
  *
17
17
  * Branding constraint: the routing runtime package name ("subswitch") must
@@ -508,9 +508,9 @@ function buildRealSpawnAndCollect(logPath) {
508
508
  * - parse failure on live payload → log reason to proxy.log.
509
509
  * - All failures degrade to stale-cache then { known: false } — never throw.
510
510
  *
511
- * applies PF-013: cwd = os.tmpdir() (not devflow dir, which may not exist on
512
- * the cold --set path; also prevents legacy subswitch.config.json in cwd
513
- * from causing exit 1 in the routing runtime).
511
+ * cwd = os.tmpdir() (not devflow dir, which may not exist on
512
+ * the cold --set path; also prevents legacy subswitch.config.json in cwd
513
+ * from causing exit 1 in the routing runtime).
514
514
  *
515
515
  * AC-C7: resolveProxyBin() is called live every invocation, never proxy.json.binPath.
516
516
  * AC-C8: never throws or rejects.
@@ -524,7 +524,7 @@ export async function discoverExternalModels(cacheDir, logPath, deps) {
524
524
  return await _discoverInternal(cacheDir, logPath, deps);
525
525
  }
526
526
  catch {
527
- // Catch-all: discoverExternalModels MUST NOT throw (avoids PF-009).
527
+ // Catch-all: discoverExternalModels MUST NOT throw.
528
528
  return { known: false };
529
529
  }
530
530
  }
@@ -1,109 +1,25 @@
1
1
  /**
2
2
  * @file observations.ts
3
3
  *
4
- * Core observation type, type guard, parsing, and pure formatting.
5
- * No I/O, no UI dependencies — pure data module.
4
+ * The ledger entry statuses on the TypeScript side. Pure data: no I/O and no
5
+ * imports, so the HUD can load it without growing its import closure.
6
6
  */
7
7
  /**
8
- * D201: Canonical status vocabulary for rendered decisions.md / pitfalls.md entries.
9
- *
10
- * Derived from an `as const` literal array so the union type, the runtime set,
11
- * and the VALID_DECISIONS_STATUSES guard below are always in sync — no manual
12
- * duplication. `Retired` is the output of the `retire-anchor` op and MUST be
13
- * present; `Unknown` was never produced by any operation and has been removed.
14
- *
15
- * Defined here (pure data module) so both observation-io.ts and learning.ts
16
- * can import without creating a utility→command circular dependency.
17
- * Re-exported through src/cli/commands/learning.ts for external consumers.
18
- * Consumed by LedgerRow (this file) and LearningObservation.decisions_status (this file).
19
- */
20
- export const DECISIONS_ENTRY_STATUSES = [
21
- 'Accepted', 'Active', 'Deprecated', 'Superseded', 'Retired',
22
- ];
23
- /** Valid values for the decisions_status optional field — derived from DECISIONS_ENTRY_STATUSES. */
24
- const VALID_DECISIONS_STATUSES = new Set(DECISIONS_ENTRY_STATUSES);
25
- /**
26
- * Type guard for validating raw JSON as a LearningObservation.
27
- * Accepts all 4 types (v2: decision + pitfall added) and all statuses including deprecated.
28
- * New optional fields (anchor_id, date, decisions_status, amendments, raw_body) are
29
- * validated when present but their absence never causes rejection — backward compatible.
30
- */
31
- export function isLearningObservation(obj) {
32
- if (typeof obj !== 'object' || obj === null)
33
- return false;
34
- const o = obj;
35
- // Required fields
36
- if (!(typeof o.id === 'string' && o.id.length > 0))
37
- return false;
38
- if (!(o.type === 'workflow' || o.type === 'procedural' || o.type === 'decision' || o.type === 'pitfall'))
39
- return false;
40
- if (!(typeof o.pattern === 'string' && o.pattern.length > 0))
41
- return false;
42
- if (typeof o.confidence !== 'number')
43
- return false;
44
- if (typeof o.observations !== 'number')
45
- return false;
46
- if (typeof o.first_seen !== 'string')
47
- return false;
48
- if (typeof o.last_seen !== 'string')
49
- return false;
50
- if (!(o.status === 'observing' || o.status === 'ready' || o.status === 'created' || o.status === 'deprecated'))
51
- return false;
52
- if (!Array.isArray(o.evidence))
53
- return false;
54
- if (typeof o.details !== 'string')
55
- return false;
56
- // Optional ledger fields: validate type when present, reject if wrong type
57
- if (o.anchor_id !== undefined && typeof o.anchor_id !== 'string')
58
- return false;
59
- if (o.date !== undefined && typeof o.date !== 'string')
60
- return false;
61
- if (o.decisions_status !== undefined && !VALID_DECISIONS_STATUSES.has(o.decisions_status))
62
- return false;
63
- if (o.amendments !== undefined) {
64
- if (!Array.isArray(o.amendments))
65
- return false;
66
- for (const a of o.amendments) {
67
- if (typeof a !== 'object' || a === null)
68
- return false;
69
- const am = a;
70
- if (typeof am.date !== 'string' || typeof am.note !== 'string')
71
- return false;
72
- }
73
- }
74
- if (o.raw_body !== undefined && typeof o.raw_body !== 'string')
75
- return false;
76
- return true;
77
- }
78
- /**
79
- * Parse a JSONL learning log into typed observations.
80
- * Skips empty and malformed lines.
81
- */
82
- export function parseLearningLog(logContent) {
83
- const observations = [];
84
- for (const line of logContent.split('\n')) {
85
- const trimmed = line.trim();
86
- if (!trimmed)
87
- continue;
88
- try {
89
- const parsed = JSON.parse(trimmed);
90
- if (isLearningObservation(parsed)) {
91
- observations.push(parsed);
92
- }
93
- }
94
- catch {
95
- // Skip malformed lines
96
- }
97
- }
98
- return observations;
99
- }
100
- /**
101
- * Parse a JSONL log and return valid observations plus the count of invalid entries.
102
- * Centralises the raw-line-count + parse pattern used by --status, --list, and --purge.
8
+ * D201: a ledger entry's `decisions_status` takes one of DECISIONS_ENTRY_STATUSES.
9
+ * `Accepted` (decisions) and `Active` (pitfalls) are active and render; the
10
+ * INACTIVE_DECISIONS_STATUSES stay in the ledger and leave the rendered files and
11
+ * the HUD counts; an absent or unknown status counts as active. These lists
12
+ * mirror ACTIVE_STATUSES / INACTIVE_STATUSES in
13
+ * src/assets/scripts/hooks/lib/learning-store.cjs, which the hooks and the
14
+ * renderer use, and the parity test in tests/decisions/learning-store.test.ts
15
+ * pins the two equal. Reason: the renderer, the HUD and this module each kept a
16
+ * list of their own, so a status added to one counted as active in the others.
103
17
  */
104
- export function loadAndCountObservations(logContent) {
105
- const rawLines = logContent.split('\n').filter(l => l.trim()).length;
106
- const observations = parseLearningLog(logContent);
107
- return { observations, invalidCount: rawLines - observations.length };
18
+ export const INACTIVE_DECISIONS_STATUSES = ['Encoded', 'Superseded', 'Retired', 'Deprecated'];
19
+ export const DECISIONS_ENTRY_STATUSES = ['Accepted', 'Active', ...INACTIVE_DECISIONS_STATUSES];
20
+ const INACTIVE_DECISIONS_STATUS_SET = new Set(INACTIVE_DECISIONS_STATUSES);
21
+ /** True unless `status` is one of INACTIVE_DECISIONS_STATUSES (D201). */
22
+ export function isActiveDecisionsStatus(status) {
23
+ return !status || !INACTIVE_DECISIONS_STATUS_SET.has(status);
108
24
  }
109
25
  //# sourceMappingURL=observations.js.map
@@ -61,8 +61,8 @@ export function mdEntryName(entry) {
61
61
  *
62
62
  * Per-item failure isolation: both the outer readdir and the inner rm are
63
63
  * independently try/caught — a missing directory is a no-op and a failed
64
- * individual removal is recorded in `failed` without aborting the sweep (avoids PF-009).
65
- * Never writes, only removes (avoids PF-011).
64
+ * individual removal is recorded in `failed` without aborting the sweep.
65
+ * Never writes, only removes — no delete-then-write window.
66
66
  */
67
67
  export async function sweepOrphanedAssets(dir, knownNames, extractRegistryName) {
68
68
  const removed = [];
@@ -81,12 +81,12 @@ export async function sweepOrphanedAssets(dir, knownNames, extractRegistryName)
81
81
  removed.push(registryName);
82
82
  }
83
83
  catch (err) {
84
- failed.push({ name: registryName, error: err }); /* per-item isolation (avoids PF-009) */
84
+ failed.push({ name: registryName, error: err }); /* per-item isolation */
85
85
  }
86
86
  }
87
87
  }
88
88
  }
89
- catch { /* directory absent or unreadable — not an error (avoids PF-009) */ }
89
+ catch { /* directory absent or unreadable — not an error */ }
90
90
  return { scanned, removed, failed };
91
91
  }
92
92
  //# sourceMappingURL=orphan-sweep.js.map
@@ -72,6 +72,10 @@ export const DEVFLOW_PLUGINS = [
72
72
  commands: ['/plan'],
73
73
  agents: ['git', 'skim', 'synthesize', 'design'],
74
74
  skills: ['gap-analysis', 'design-review', 'patterns', 'worktree-support', 'feature-knowledge', 'apply-feature-knowledge'],
75
+ // D-CHARTER-BOUNDED-INLINE: /plan's main thread orchestrates and loads no
76
+ // companion skills, so `requires:` lists only skills this plugin's command,
77
+ // agents and skills reference. `software-design` and `test-driven-development`
78
+ // have no reader here; the closure guard's reverse arm rejects an unread entry.
75
79
  requires: [
76
80
  'apply-decisions',
77
81
  'architecture',
@@ -87,8 +91,6 @@ export const DEVFLOW_PLUGINS = [
87
91
  'reliability',
88
92
  'review-methodology',
89
93
  'security',
90
- 'software-design',
91
- 'test-driven-development',
92
94
  'testing',
93
95
  ],
94
96
  rules: [],
@@ -127,7 +129,11 @@ export const DEVFLOW_PLUGINS = [
127
129
  commands: ['/code-review'],
128
130
  agents: ['git', 'review', 'synthesize'],
129
131
  skills: ['architecture', 'complexity', 'consistency', 'database', 'dependencies', 'documentation', 'performance', 'regression', 'reliability', 'review-methodology', 'security', 'testing', 'worktree-support', 'apply-feature-knowledge'],
130
- requires: ['apply-decisions', 'docs-framework', 'git', 'quality-gates', 'software-design'],
132
+ // D-CHARTER-BOUNDED-INLINE: /code-review's main thread orchestrates and loads
133
+ // no companion skills, so `requires:` lists only skills this plugin's command,
134
+ // agents and skills reference. `quality-gates` and `software-design` have no
135
+ // reader here; the closure guard's reverse arm rejects an unread entry.
136
+ requires: ['apply-decisions', 'docs-framework', 'git'],
131
137
  rules: [],
132
138
  },
133
139
  {
@@ -465,7 +471,7 @@ export const DELETED_PLUGIN_NAMES = [
465
471
  * - uninstall.ts sweepDevflowNamespaces: union into knownNames to spare devflow:compliance
466
472
  * from the post-selective-uninstall sweep (nothing converges after selective uninstall)
467
473
  * - skills.ts: union into allSkills for shadow/unshadow/list
468
- * - tests: independent literal ['compliance'] (avoids EXCLUDED-as-oracle trap, PF-018)
474
+ * - tests: independent literal ['compliance'] (avoids EXCLUDED-as-oracle trap)
469
475
  *
470
476
  * D-FO-1: FEATURE_OWNED_SKILLS must be disjoint from getAllSkillNames()
471
477
  * (guarded by plugins.test.ts FEATURE_OWNED constants describe block).
@@ -478,7 +484,7 @@ export const FEATURE_OWNED_SKILLS = ['compliance'];
478
484
  *
479
485
  * Used by:
480
486
  * - rules.ts: union into allRules for shadow/unshadow/list
481
- * - tests: independent literal ['compliance'] (avoids EXCLUDED-as-oracle trap, PF-018)
487
+ * - tests: independent literal ['compliance'] (avoids EXCLUDED-as-oracle trap)
482
488
  *
483
489
  * D-FO-2: FEATURE_OWNED_RULES must be disjoint from getAllRuleNames()
484
490
  * (guarded by plugins.test.ts FEATURE_OWNED constants describe block).
@@ -511,7 +517,7 @@ export const PRESENCE_GATED_SKILLS = [
511
517
  * The classified exception to the closure guard — declared HERE, at the
512
518
  * declaration site of the field it exempts, and imported by the guard.
513
519
  *
514
- * D-TEMPLATE-EXCEPTION (applies PF-067: one authority for a prohibition and its
520
+ * D-TEMPLATE-EXCEPTION (one authority for a prohibition and its
515
521
  * exemptions). Every other templated reference in the corpus carries a literal
516
522
  * prefix and resolves through it — `devflow:research-{RESEARCH_TYPE}` resolves
517
523
  * because five in-scope skills start with `research-`. The Review focus skill is
@@ -745,8 +751,7 @@ export function buildFullSkillsMap() {
745
751
  * add X, not a statement that X is the whole selection, so it may never remove
746
752
  * what another plugin contributed (AC-22). {@link FEATURE_OWNED_SKILLS} is
747
753
  * subtracted unconditionally: those install and uninstall with their feature,
748
- * and sweeping them here would delete an artifact this code does not own
749
- * (applies ADR-024).
754
+ * and sweeping them here would delete an artifact this code does not own.
750
755
  *
751
756
  * A shadow is NEVER removed, whatever the selection — `~/.devflow/skills/` is
752
757
  * user content. One that falls outside the install set simply applies to
@@ -46,10 +46,14 @@ export function getFeatureConfigPath(projectRoot) {
46
46
  export function getLearningPendingTurnsPath(projectRoot) {
47
47
  return path.join(projectRoot, '.devflow', 'learning', '.pending-turns.jsonl');
48
48
  }
49
- /** .devflow/learning/.pending-turns.processing — atomic claim held by the Learning agent while processing */
49
+ /** .devflow/learning/.pending-turns.processing — the claimed batch a Learning run holds while it processes it */
50
50
  export function getLearningPendingTurnsProcessingPath(projectRoot) {
51
51
  return path.join(projectRoot, '.devflow', 'learning', '.pending-turns.processing');
52
52
  }
53
+ /** .devflow/learning/.pending-turns.owner — the token of the run that holds the claim (learning-store.cjs) */
54
+ export function getLearningClaimOwnerPath(projectRoot) {
55
+ return path.join(projectRoot, '.devflow', 'learning', '.pending-turns.owner');
56
+ }
53
57
  // ---------------------------------------------------------------------------
54
58
  // Learning content files
55
59
  // ---------------------------------------------------------------------------
@@ -77,26 +81,18 @@ export function getDecisionsLogPath(projectRoot) {
77
81
  export function getDecisionsArchivePath(projectRoot) {
78
82
  return path.join(projectRoot, '.devflow', 'learning', 'decisions-log.archive.jsonl');
79
83
  }
84
+ /** .devflow/learning/decisions-history.jsonl — prior content versions of rewritten entries (learning-store.cjs) */
85
+ export function getDecisionsHistoryPath(projectRoot) {
86
+ return path.join(projectRoot, '.devflow', 'learning', 'decisions-history.jsonl');
87
+ }
80
88
  /** .devflow/learning/.decisions.lock — mkdir-based lock directory */
81
89
  export function getDecisionsLockDir(projectRoot) {
82
90
  return path.join(projectRoot, '.devflow', 'learning', '.decisions.lock');
83
91
  }
84
- /** .devflow/learning/.decisions-usage.json */
85
- export function getDecisionsUsagePath(projectRoot) {
86
- return path.join(projectRoot, '.devflow', 'learning', '.decisions-usage.json');
87
- }
88
- /** .devflow/learning/.decisions-usage.lock/ — mkdir-based lock directory for usage file */
89
- export function getDecisionsUsageLockDir(projectRoot) {
90
- return path.join(projectRoot, '.devflow', 'learning', '.decisions-usage.lock');
91
- }
92
92
  /** .devflow/learning/index.md — pre-rendered compact index written by render-decisions.cjs */
93
93
  export function getDecisionsIndexPath(projectRoot) {
94
94
  return path.join(projectRoot, '.devflow', 'learning', 'index.md');
95
95
  }
96
- /** .devflow/learning/.observations.lock — mkdir-based lock directory for observation log writes */
97
- export function getObservationsLockDir(projectRoot) {
98
- return path.join(projectRoot, '.devflow', 'learning', '.observations.lock');
99
- }
100
96
  // ---------------------------------------------------------------------------
101
97
  // Memory / working-memory files
102
98
  // ---------------------------------------------------------------------------
@@ -10,8 +10,8 @@ import * as path from 'path';
10
10
  * - openProxyLog — 0700-parent + 0600-file open with best-effort chmod (SEC-2)
11
11
  * - rotateProxyLogIfLarge — pre-spawn-only 2MB→1MB rotation that preserves 0600 mode
12
12
  *
13
- * avoids PF-009: every failure path is non-fatal (wrapped in try/catch); one bad
14
- * chmod or rotation error must never abort the enable flow.
13
+ * Every failure path is non-fatal (wrapped in try/catch); one bad
14
+ * chmod or rotation error must never abort the enable flow.
15
15
  */
16
16
  /** 2 MB max log size before rotation (mirrors ensure-proxy _LOG_MAX_BYTES). */
17
17
  export const PROXY_LOG_MAX_BYTES = 2_097_152;
@@ -28,7 +28,7 @@ export const PROXY_LOG_TAIL_BYTES = 1_048_576;
28
28
  * Call sites compose on top of this result to add process-specific vars
29
29
  * (e.g. SUBSWITCH_CONFIG for the relay spawn and doctor spawn).
30
30
  *
31
- * applies ADR-003: the prior denylist rationale is gone — on the paths devflow
31
+ * On the paths devflow
32
32
  * invokes (serve, doctor, models) the routing runtime reads ANTHROPIC_API_KEY,
33
33
  * FORCE_COLOR, NO_COLOR (dist/tty.js, presence semantics), SUBSWITCH_CONFIG and
34
34
  * XDG_CONFIG_HOME (user-config lookup, reached only on an implicit config load —
@@ -42,8 +42,8 @@ export const PROXY_LOG_TAIL_BYTES = 1_048_576;
42
42
  * NODE_EXTRA_CA_CERTS is included so corporate-TLS deployments can supply a CA
43
43
  * bundle; it is omitted when unset (the absent-→-omit loop handles this).
44
44
  * NODE_OPTIONS is deliberately excluded: it permits arbitrary code execution via
45
- * --require/--import. Mirrored in the ensure-proxy bash hook's _RELAY_ENV array
46
- * (avoids PF-017); both allowlists must be kept in sync.
45
+ * --require/--import. Mirrored in the ensure-proxy bash hook's _RELAY_ENV array;
46
+ * both allowlists must be kept in sync.
47
47
  */
48
48
  export function scrubChildEnv() {
49
49
  const POSIX_ALLOWLIST = ['PATH', 'HOME', 'TMPDIR', 'LANG', 'LC_ALL', 'NODE_EXTRA_CA_CERTS'];
@@ -71,7 +71,7 @@ export function scrubChildEnv() {
71
71
  * 3. best-effort fs.chmod(logPath, 0o600) — widens a pre-existing file
72
72
  * that was created before this guard existed (e.g. mode 0644 on disk).
73
73
  * Non-fatal: a chmod failure (EPERM, ENOENT race) must never prevent the
74
- * handle from being returned — avoids PF-009 failure-isolation principle.
74
+ * handle from being returned.
75
75
  * Follows the SEC-1 precedent in src/core/fs-atomic.ts (commit 5755d56):
76
76
  * chmod in try/catch, never fatal, with a comment citing the rationale.
77
77
  *
@@ -88,7 +88,7 @@ export async function openProxyLog(logPath) {
88
88
  const handle = await fs.open(logPath, 'a', 0o600);
89
89
  // Step 3: best-effort chmod for a pre-existing file that has a wider mode.
90
90
  // Non-fatal: chmod failure must not prevent the handle from being returned.
91
- // avoids PF-009: failure-isolation — one bad chmod must never abort the enable flow.
91
+ // Failure isolation — one bad chmod must never abort the enable flow.
92
92
  try {
93
93
  await fs.chmod(logPath, 0o600);
94
94
  }
@@ -120,7 +120,7 @@ export async function openProxyLog(logPath) {
120
120
  *
121
121
  * Non-fatal: rotation failure (disk full, EPERM, read error) leaves the
122
122
  * original log untouched and the enable proceeds with an oversized log.
123
- * avoids PF-009: one rotation error must never abort the enable flow.
123
+ * One rotation error must never abort the enable flow.
124
124
  *
125
125
  * @param logPath - Absolute path to proxy.log.
126
126
  */
@@ -2,8 +2,8 @@
2
2
  * Proxy state persistence and routing config helpers for the Devflow external
3
3
  * model routing feature.
4
4
  *
5
- * applies ADR-013: pure core-layer module — no Claude Code adapter concerns.
6
- * avoids PF-014: never call process.exit() inside finally-guarded scopes; all
5
+ * Pure core-layer module — no Claude Code adapter concerns.
6
+ * Never call process.exit() inside finally-guarded scopes; all
7
7
  * fallible operations return Result instead of throwing.
8
8
  *
9
9
  * State file: ~/.devflow/proxy.json
@@ -155,7 +155,7 @@ const ROUTING_CONFIG_REJECTED_SUBKEYS = {
155
155
  *
156
156
  * If `existingContent` is missing or malformed, falls back to a port-only config.
157
157
  *
158
- * applies ADR-013: pure core-layer function — no I/O.
158
+ * Pure core-layer function — no I/O.
159
159
  * @D-EFR-4: see note above for the strict top-key constraint.
160
160
  */
161
161
  export function buildRoutingConfigJson(port, existingContent) {