devflow-kit 3.3.0 → 3.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 (220) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +8 -4
  3. package/dist/agents/code-edit.md +133 -0
  4. package/dist/agents/code-fix.md +165 -0
  5. package/dist/agents/code.md +187 -0
  6. package/{src/assets → dist}/agents/design.md +1 -2
  7. package/{src/assets → dist}/agents/diagnose.md +1 -2
  8. package/dist/agents/git.md +33 -722
  9. package/{src/assets → dist}/agents/knowledge.md +12 -5
  10. package/{src/assets → dist}/agents/research.md +8 -2
  11. package/{src/assets → dist}/agents/review.md +8 -11
  12. package/{src/assets → dist}/agents/scrutinize.md +4 -1
  13. package/dist/agents/skim.md +152 -0
  14. package/{src/assets → dist}/agents/triage.md +9 -2
  15. package/dist/cli/commands/behavior-prompts.js +77 -0
  16. package/dist/cli/commands/behavior.js +123 -0
  17. package/dist/cli/commands/flags.js +8 -2
  18. package/dist/cli/commands/init-seed.js +7 -3
  19. package/dist/cli/commands/init.js +134 -5
  20. package/dist/cli/commands/learning.js +38 -3
  21. package/dist/cli/commands/rules.js +16 -0
  22. package/dist/cli/commands/tracker.js +112 -15
  23. package/dist/cli/commands/uninstall.js +42 -1
  24. package/dist/cli.js +2 -0
  25. package/dist/commands/bug-analysis.md +35 -13
  26. package/dist/commands/code-review.md +155 -69
  27. package/dist/commands/debug.md +39 -16
  28. package/dist/commands/dynamic-build.md +61 -72
  29. package/dist/commands/dynamic-plan.md +39 -25
  30. package/dist/commands/dynamic-profile.md +28 -14
  31. package/dist/commands/dynamic-tickets.md +38 -22
  32. package/dist/commands/explore.md +18 -16
  33. package/dist/commands/implement.md +121 -150
  34. package/dist/commands/plan.md +51 -31
  35. package/dist/commands/release.md +72 -6
  36. package/dist/commands/research.md +49 -20
  37. package/dist/commands/resolve.md +54 -46
  38. package/dist/commands/self-review.md +38 -25
  39. package/dist/core/agent-frontmatter.js +101 -0
  40. package/dist/core/agent-models.js +43 -0
  41. package/dist/core/assets.js +55 -10
  42. package/dist/core/claude-md-audit.js +190 -0
  43. package/dist/core/compliance-compose.js +6 -4
  44. package/dist/core/feature-switch.js +41 -1
  45. package/dist/core/flags.js +139 -0
  46. package/dist/core/fs-atomic.js +8 -3
  47. package/dist/core/learning-variants.js +213 -0
  48. package/dist/core/manifest.js +73 -3
  49. package/dist/core/mds-variants.js +98 -19
  50. package/dist/core/plugins.js +111 -72
  51. package/dist/core/project-paths.js +0 -4
  52. package/dist/core/proxy-log.js +1 -1
  53. package/dist/core/proxy-state.js +1 -1
  54. package/dist/core/tracker.js +120 -2
  55. package/dist/learning-off/agents/code-edit.md +129 -0
  56. package/dist/learning-off/agents/code-fix.md +161 -0
  57. package/dist/learning-off/agents/code.md +183 -0
  58. package/dist/learning-off/agents/design.md +118 -0
  59. package/dist/learning-off/agents/diagnose.md +210 -0
  60. package/dist/learning-off/agents/knowledge.md +96 -0
  61. package/dist/learning-off/agents/research.md +155 -0
  62. package/dist/learning-off/agents/review.md +224 -0
  63. package/dist/learning-off/agents/scrutinize.md +120 -0
  64. package/{src/assets → dist/learning-off}/agents/skim.md +5 -8
  65. package/dist/learning-off/agents/triage.md +170 -0
  66. package/dist/learning-off/commands/bug-analysis.md +420 -0
  67. package/dist/learning-off/commands/code-review.md +530 -0
  68. package/dist/learning-off/commands/debug.md +314 -0
  69. package/dist/learning-off/commands/dynamic-build.md +1245 -0
  70. package/dist/learning-off/commands/dynamic-plan.md +426 -0
  71. package/dist/learning-off/commands/dynamic-profile.md +214 -0
  72. package/dist/learning-off/commands/dynamic-tickets.md +634 -0
  73. package/dist/learning-off/commands/explore.md +210 -0
  74. package/dist/learning-off/commands/implement.md +774 -0
  75. package/dist/learning-off/commands/plan.md +671 -0
  76. package/dist/learning-off/commands/release.md +311 -0
  77. package/dist/learning-off/commands/research.md +229 -0
  78. package/dist/learning-off/commands/resolve.md +842 -0
  79. package/dist/learning-off/commands/self-review.md +266 -0
  80. package/dist/skills/git/references/learn-conventions.md +27 -3
  81. package/dist/skills/git/references/pr/check-ci-status.md +19 -0
  82. package/dist/skills/git/references/pr/check-merge-readiness.md +19 -0
  83. package/dist/skills/git/references/pr/fetch-review-threads.md +30 -0
  84. package/dist/skills/git/references/pr/post-resolution-summary.md +18 -0
  85. package/dist/skills/git/references/pr/post-review-summary.md +20 -0
  86. package/dist/skills/git/references/pr/resolve-review-threads.md +28 -4
  87. package/dist/skills/git/references/pr/update-pr-evidence.md +13 -0
  88. package/dist/skills/git/references/pr/validate-branch.md +26 -0
  89. package/dist/skills/git/references/tracker/_contract.md +33 -0
  90. package/dist/skills/git/references/tracker/github/associate-release.md +24 -0
  91. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +31 -1
  92. package/dist/skills/git/references/tracker/github/create-release.md +30 -1
  93. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +89 -2
  94. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +16 -1
  95. package/dist/skills/git/references/tracker/github/fetch-issue.md +38 -1
  96. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +49 -1
  97. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +29 -0
  98. package/dist/skills/git/references/tracker/github/manage-debt.md +21 -1
  99. package/dist/skills/git/references/tracker/github/post-wave-report.md +17 -1
  100. package/dist/skills/git/references/tracker/github/setup-task.md +53 -1
  101. package/dist/skills/git/references/tracker/jira/associate-release.md +25 -1
  102. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +31 -1
  103. package/dist/skills/git/references/tracker/jira/create-release.md +30 -1
  104. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +89 -2
  105. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +16 -1
  106. package/dist/skills/git/references/tracker/jira/fetch-issue.md +38 -1
  107. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +49 -1
  108. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +29 -0
  109. package/dist/skills/git/references/tracker/jira/manage-debt.md +21 -1
  110. package/dist/skills/git/references/tracker/jira/post-wave-report.md +17 -1
  111. package/dist/skills/git/references/tracker/jira/setup-task.md +55 -3
  112. package/dist/skills/git/references/tracker/linear/associate-release.md +25 -1
  113. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +31 -1
  114. package/dist/skills/git/references/tracker/linear/create-release.md +30 -1
  115. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +89 -2
  116. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +16 -1
  117. package/dist/skills/git/references/tracker/linear/fetch-issue.md +38 -1
  118. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +49 -1
  119. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +29 -0
  120. package/dist/skills/git/references/tracker/linear/manage-debt.md +21 -1
  121. package/dist/skills/git/references/tracker/linear/post-wave-report.md +17 -1
  122. package/dist/skills/git/references/tracker/linear/setup-task.md +55 -3
  123. package/dist/targets/claude-code/agent-type-owner.js +164 -0
  124. package/dist/targets/claude-code/behavior-install.js +111 -0
  125. package/dist/targets/claude-code/installer.js +140 -37
  126. package/dist/targets/claude-code/language-stamp.js +185 -0
  127. package/dist/targets/claude-code/learning-install.js +497 -0
  128. package/dist/targets/claude-code/tracker-install.js +33 -16
  129. package/dist/targets/claude-code/tracker-servers.js +107 -0
  130. package/package.json +4 -3
  131. package/src/assets/agents/_partials/_code.mds +109 -0
  132. package/src/assets/agents/code-edit.mds +81 -0
  133. package/src/assets/agents/code-fix.mds +113 -0
  134. package/src/assets/agents/code.mds +135 -0
  135. package/src/assets/agents/design.mds +148 -0
  136. package/src/assets/agents/diagnose.mds +225 -0
  137. package/src/assets/agents/evaluate.md +8 -4
  138. package/src/assets/agents/explore.md +58 -0
  139. package/src/assets/agents/git.mds +33 -722
  140. package/src/assets/agents/knowledge.mds +131 -0
  141. package/src/assets/agents/learning.md +19 -69
  142. package/src/assets/agents/research.mds +182 -0
  143. package/src/assets/agents/review.mds +282 -0
  144. package/src/assets/agents/scrutinize.mds +135 -0
  145. package/src/assets/agents/simplify.md +3 -0
  146. package/src/assets/agents/skim.mds +165 -0
  147. package/src/assets/agents/synthesize.md +26 -352
  148. package/src/assets/agents/test.md +8 -0
  149. package/src/assets/agents/tracker.md +20 -7
  150. package/src/assets/agents/triage.mds +201 -0
  151. package/src/assets/agents/validate.md +13 -6
  152. package/src/assets/commands/_partials/_compliance.mds +41 -4
  153. package/src/assets/commands/_partials/_decisions.mds +33 -2
  154. package/src/assets/commands/_partials/_engine.mds +14 -6
  155. package/src/assets/commands/_partials/_knowledge.mds +25 -12
  156. package/src/assets/commands/_partials/_preamble.mds +34 -10
  157. package/src/assets/commands/_partials/_publication.mds +5 -4
  158. package/src/assets/commands/_partials/_roster.mds +4 -3
  159. package/src/assets/commands/_partials/_settings.mds +13 -5
  160. package/src/assets/commands/_partials/_wave.mds +9 -1
  161. package/src/assets/commands/bug-analysis.mds +27 -5
  162. package/src/assets/commands/code-review.mds +158 -52
  163. package/src/assets/commands/debug.mds +41 -3
  164. package/src/assets/commands/dynamic-build.mds +47 -27
  165. package/src/assets/commands/dynamic-plan.mds +40 -9
  166. package/src/assets/commands/dynamic-profile.mds +9 -1
  167. package/src/assets/commands/dynamic-tickets.mds +21 -6
  168. package/src/assets/commands/explore.mds +28 -2
  169. package/src/assets/commands/implement.mds +126 -129
  170. package/src/assets/commands/plan.mds +58 -14
  171. package/src/assets/commands/{release.md → release.mds} +30 -26
  172. package/src/assets/commands/research.mds +42 -11
  173. package/src/assets/commands/resolve.mds +59 -15
  174. package/src/assets/commands/self-review.mds +30 -5
  175. package/src/assets/mds/.mdsroot +4 -0
  176. package/src/assets/mds/git/_ops.mds +383 -0
  177. package/src/assets/mds/git/_ops_pr.mds +201 -0
  178. package/src/assets/mds/git/_ops_steps.mds +147 -0
  179. package/src/assets/mds/git/_pr.mds +39 -44
  180. package/src/assets/mds/git/_references.mds +6 -3
  181. package/src/assets/mds/tracker/_common.mds +5 -5
  182. package/src/assets/mds/tracker/_contract.mds +73 -0
  183. package/src/assets/mds/tracker/_github.mds +73 -16
  184. package/src/assets/mds/tracker/_jira.mds +77 -19
  185. package/src/assets/mds/tracker/_linear.mds +77 -19
  186. package/src/assets/mds/tracker/_mcp.mds +2 -2
  187. package/src/assets/mds/tracker/_steps.mds +98 -0
  188. package/src/assets/rules/behavior.md +6 -0
  189. package/src/assets/rules/context-economy.md +10 -0
  190. package/src/assets/rules/go.md +1 -0
  191. package/src/assets/rules/java.md +1 -0
  192. package/src/assets/rules/python.md +1 -0
  193. package/src/assets/rules/rust.md +1 -0
  194. package/src/assets/rules/typescript.md +1 -0
  195. package/src/assets/scripts/claude-md-audit.cjs +611 -0
  196. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +3 -3
  197. package/src/assets/scripts/hooks/json-helper.cjs +13 -5
  198. package/src/assets/scripts/hooks/json-parse +34 -10
  199. package/src/assets/scripts/hooks/lib/project-paths.cjs +0 -6
  200. package/src/assets/scripts/hooks/session-start-context +315 -7
  201. package/src/assets/skills/agent-references/SKILL.md +33 -0
  202. package/src/assets/skills/agent-references/references/learning/maintain.md +71 -0
  203. package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +71 -0
  204. package/src/assets/skills/agent-references/references/synthesize/design.md +36 -0
  205. package/src/assets/skills/agent-references/references/synthesize/exploration.md +38 -0
  206. package/src/assets/skills/agent-references/references/synthesize/planning.md +60 -0
  207. package/src/assets/skills/agent-references/references/synthesize/research.md +61 -0
  208. package/src/assets/skills/agent-references/references/synthesize/review.md +76 -0
  209. package/src/assets/skills/apply-decisions/SKILL.md +1 -1
  210. package/src/assets/skills/apply-feature-knowledge/SKILL.md +5 -5
  211. package/src/assets/skills/compliance/SKILL.md +1 -1
  212. package/src/assets/skills/docs-framework/SKILL.md +80 -83
  213. package/src/assets/skills/docs-framework/references/patterns.md +14 -25
  214. package/src/assets/skills/docs-framework/references/violations.md +7 -7
  215. package/src/assets/skills/feature-knowledge/SKILL.md +43 -12
  216. package/src/assets/skills/gap-analysis/SKILL.md +0 -16
  217. package/src/assets/skills/gap-analysis/references/compliance.md +19 -0
  218. package/src/assets/skills/quality-gates/SKILL.md +1 -1
  219. package/dist/skills/git/references/pr/ensure-pr-ready.md +0 -24
  220. package/src/assets/agents/code.md +0 -330
@@ -0,0 +1,190 @@
1
+ /**
2
+ * The CLI's view of the CLAUDE.md import audit — a typed seam onto the package's
3
+ * own `claude-md-audit.cjs`, never a second implementation of it.
4
+ *
5
+ * D-CLAUDE-MD-IMPORT-AUDIT: the logic (import grammar, bounds, thresholds, display
6
+ * text, stamp format) lives once, in src/assets/scripts/claude-md-audit.cjs, because
7
+ * the SessionStart hook reaches only `~/.devflow/scripts/`, which `composeScripts`
8
+ * fills by copying `src/assets/scripts/` verbatim. This module is the typed facade
9
+ * `devflow init` calls, loaded with the evidence-policy seam's `loadScript` against
10
+ * an explicit surface map (D-POLICY-CJS-SEAM). The interfaces below are TRANSCRIBED
11
+ * from the script's JSDoc and are this side's only shape authority; the module is
12
+ * required from `scriptsDir()` (the package's own copy), never from the installed
13
+ * `~/.devflow/scripts`, which may be older than this CLI.
14
+ *
15
+ * Nothing here throws: a load failure, a script that throws and a stamp that cannot
16
+ * be written are `Result` errors, and init prints at most one degraded line for them
17
+ * and keeps its exit code.
18
+ *
19
+ * D-AUDIT-STAMP (the caller's half): the audit writes nothing; this module writes the
20
+ * stamp after the install, only under an existing machine root (a command never
21
+ * creates `~/.devflow` for it), only through a sibling temp file renamed into place
22
+ * ({@link writeFileAtomicExclusive}), and never through a link. Because
23
+ * `writeFileAtomicExclusive` does not refuse a symlink at the target, the write does
24
+ * its own `lstat` regular-file check first (AC-437): a stamp path that is a symbolic
25
+ * link or any other non-regular file is refused and nothing is written.
26
+ */
27
+ import { promises as fs } from 'fs';
28
+ import { join } from 'path';
29
+ import { scriptsDir } from './assets.js';
30
+ import { writeFileAtomicExclusive } from './fs-atomic.js';
31
+ import { loadScript, } from './evidence-policy.js';
32
+ /** The script, relative to src/assets/scripts/ (and ~/.devflow/scripts/). */
33
+ export const CLAUDE_MD_AUDIT_SCRIPT_NAME = 'claude-md-audit.cjs';
34
+ /** The stamp's basename under the machine root (`~/.devflow`). */
35
+ export const CLAUDE_MD_AUDIT_STAMP_FILE = '.claude-md-audit';
36
+ /**
37
+ * The prefix of the stamp's sibling temp file, `<stamp>.tmp.<pid>`: the one name
38
+ * the hook and {@link writeFileAtomicExclusive} both create, so a crash between the
39
+ * write and the rename leaves a name uninstall can sweep.
40
+ */
41
+ export const CLAUDE_MD_AUDIT_STAMP_TMP_PREFIX = `${CLAUDE_MD_AUDIT_STAMP_FILE}.tmp.`;
42
+ /** The stamp is created owner-only, the mode the hook's `umask 077` write gives it. */
43
+ const STAMP_CREATE_MODE = 0o600;
44
+ /** Every key of ClaudeMdAuditModule and the runtime kind the loader requires of it. */
45
+ export const CLAUDE_MD_AUDIT_MODULE_SURFACE = Object.freeze({
46
+ FILE_THRESHOLD_BYTES: 'number',
47
+ CHAIN_THRESHOLD_BYTES: 'number',
48
+ MAX_HOPS: 'number',
49
+ MAX_PATHS_PER_ROOT: 'number',
50
+ SCAN_BYTES: 'number',
51
+ MAX_BYTES_READ: 'number',
52
+ MAX_STAMP_BYTES: 'number',
53
+ MAX_KEYS: 'number',
54
+ SKIP_FILE_BYTES: 'number',
55
+ NOT_SHOWN: 'string',
56
+ HOOK_MAGIC: 'string',
57
+ HOOK_END: 'string',
58
+ extractImports: 'function',
59
+ isDisplayable: 'function',
60
+ isRecordable: 'function',
61
+ formatFinding: 'function',
62
+ findingKey: 'function',
63
+ parseStamp: 'function',
64
+ renderStamp: 'function',
65
+ readStampFile: 'function',
66
+ audit: 'function',
67
+ main: 'function',
68
+ });
69
+ /**
70
+ * Load the audit script from `dir` (default: the package's own scripts directory)
71
+ * and shape-check its surface. Never throws.
72
+ */
73
+ export function loadClaudeMdAuditModule(dir = scriptsDir()) {
74
+ return loadScript(join(dir, CLAUDE_MD_AUDIT_SCRIPT_NAME), CLAUDE_MD_AUDIT_MODULE_SURFACE);
75
+ }
76
+ // ── Roots ──────────────────────────────────────────────────────────────────────
77
+ /**
78
+ * The gated root set, in the order the hook passes it: `CLAUDE.md` in the Claude
79
+ * config directory, then (only for a git project that is not HOME — the caller
80
+ * passes `null` otherwise) the project's `CLAUDE.md`, `.claude/CLAUDE.md` and
81
+ * `CLAUDE.local.md` at its toplevel. Ancestor directories, lazily loaded
82
+ * subdirectory files and `rules/*.md` are not roots.
83
+ */
84
+ export function claudeMdAuditRoots(claudeDir, projectRoot) {
85
+ const roots = [join(claudeDir, 'CLAUDE.md')];
86
+ if (projectRoot !== null) {
87
+ roots.push(join(projectRoot, 'CLAUDE.md'), join(projectRoot, '.claude', 'CLAUDE.md'), join(projectRoot, 'CLAUDE.local.md'));
88
+ }
89
+ return roots;
90
+ }
91
+ /** The stamp's text when it is a regular file of at most MAX_STAMP_BYTES, else null (read as absent). */
92
+ export async function readClaudeMdAuditStamp(devflowDir, maxBytes) {
93
+ const stampPath = join(devflowDir, CLAUDE_MD_AUDIT_STAMP_FILE);
94
+ try {
95
+ const st = await fs.lstat(stampPath);
96
+ if (!st.isFile() || st.size > maxBytes)
97
+ return null;
98
+ return await fs.readFile(stampPath, 'utf-8');
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ }
104
+ /**
105
+ * Run the audit over the gated roots. Reads only; the stamp is written by
106
+ * {@link writeClaudeMdAuditStamp}. Never throws.
107
+ */
108
+ export async function runClaudeMdAudit(opts) {
109
+ const loaded = loadClaudeMdAuditModule(opts.scriptsDir);
110
+ if (!loaded.ok)
111
+ return { ok: false, error: { kind: 'load', detail: loaded.error } };
112
+ const audit = loaded.value;
113
+ try {
114
+ const stampText = await readClaudeMdAuditStamp(opts.devflowDir, audit.MAX_STAMP_BYTES);
115
+ const prior = stampText === null ? [] : audit.parseStamp(stampText).keys;
116
+ const roots = claudeMdAuditRoots(opts.claudeDir, opts.projectRoot);
117
+ if (opts.showAll !== true)
118
+ return { ok: true, value: audit.audit({ roots, home: opts.home, keys: prior }) };
119
+ const result = audit.audit({ roots, home: opts.home, keys: [] });
120
+ const keys = [...prior.filter(key => !result.shown.includes(key)), ...result.shown].slice(-audit.MAX_KEYS);
121
+ return { ok: true, value: { ...result, stamp: audit.renderStamp({ roots, examined: result.examined, keys }) } };
122
+ }
123
+ catch (err) {
124
+ return { ok: false, error: { kind: 'failed', detail: err instanceof Error ? err.message : String(err) } };
125
+ }
126
+ }
127
+ /**
128
+ * Run the audit and record its stamp (D-AUDIT-STAMP): what `devflow init` does after the
129
+ * install. Every finding over a threshold is displayed (`showAll`), as init is a deliberate
130
+ * command. The stamp carries the examined paths and every key the audit displayed, so the
131
+ * first SessionStart afterwards repeats nothing it was just shown. A stamp that cannot be
132
+ * written (no machine root, a link in the way, a full disk) costs a repeated message at
133
+ * most, never the audit's result, so that failure is not reported. Never throws.
134
+ */
135
+ export async function auditAndRecordClaudeMd(opts) {
136
+ const run = await runClaudeMdAudit({ ...opts, showAll: true });
137
+ if (run.ok)
138
+ await writeClaudeMdAuditStamp(opts.devflowDir, run.value.stamp);
139
+ return run;
140
+ }
141
+ /**
142
+ * The display lines of an audit result, as ONE note body, or null when nothing is
143
+ * flagged. The single formatter both init paths (Recommended and Advanced) use; the
144
+ * lines themselves are produced by the script's own formatter, the one the hook
145
+ * shows in its systemMessage.
146
+ */
147
+ export function formatClaudeMdAuditNote(result) {
148
+ return result.lines.length === 0 ? null : result.lines.join('\n');
149
+ }
150
+ /** The one degraded line printed when the audit could not run. */
151
+ export function formatClaudeMdAuditUnavailable(error) {
152
+ if (error.kind === 'load') {
153
+ return error.detail.kind === 'not-found'
154
+ ? 'CLAUDE.md import audit unavailable: audit script not found — reinstall devflow-kit'
155
+ : 'CLAUDE.md import audit unavailable: audit script failed to load — reinstall devflow-kit';
156
+ }
157
+ return 'CLAUDE.md import audit unavailable: the audit failed';
158
+ }
159
+ /**
160
+ * Write the stamp text to `<devflowDir>/.claude-md-audit` (AC-437, D-AUDIT-STAMP).
161
+ *
162
+ * Refuses, writing nothing: a machine root that does not exist; a stamp path that is
163
+ * a symbolic link, a directory, a FIFO or any other non-regular file (an `lstat`
164
+ * check made here, because the atomic writer renames over the target without
165
+ * looking at what it is). Otherwise the text lands in a sibling temp file renamed
166
+ * into place. Never throws.
167
+ */
168
+ export async function writeClaudeMdAuditStamp(devflowDir, text) {
169
+ try {
170
+ const root = await fs.stat(devflowDir).catch(() => null);
171
+ if (root === null || !root.isDirectory())
172
+ return { ok: false, error: { kind: 'no-machine-root' } };
173
+ const stampPath = join(devflowDir, CLAUDE_MD_AUDIT_STAMP_FILE);
174
+ const existing = await fs.lstat(stampPath).catch((err) => {
175
+ if (err.code === 'ENOENT')
176
+ return null;
177
+ throw err;
178
+ });
179
+ if (existing !== null && !existing.isFile()) {
180
+ return { ok: false, error: { kind: 'refused', detail: existing.isSymbolicLink() ? 'the stamp path is a symbolic link' : 'the stamp path is not a regular file' } };
181
+ }
182
+ // Owner-only, as the hook writes it: the stamp names the user's project paths.
183
+ await writeFileAtomicExclusive(stampPath, text, STAMP_CREATE_MODE);
184
+ return { ok: true, value: undefined };
185
+ }
186
+ catch (err) {
187
+ return { ok: false, error: { kind: 'write-failed', detail: err instanceof Error ? err.message : String(err) } };
188
+ }
189
+ }
190
+ //# sourceMappingURL=claude-md-audit.js.map
@@ -172,13 +172,15 @@ function collectMissingFragmentWarnings(fnName, activeFrameworks, fragments, omi
172
172
  *
173
173
  * Every framework reference is installed on every machine
174
174
  * (D-COMPLIANCE-INSTALL-ALWAYS), so file presence no longer says which
175
- * frameworks apply. The caller hands the agent `COMPLIANCE_FRAMEWORKS` — the
176
- * `COMPLIANCE` field of the settings line, which folds this machine's selection
177
- * with the repository's `.devflow/project.json` — and those ids alone are in force.
175
+ * frameworks apply. The command names the ids in the agent's prompt
176
+ * (D-COMPLIANCE-INJECT: the compliance directive) — the `COMPLIANCE` field of the
177
+ * settings line, which folds this machine's selection with the repository's
178
+ * `.devflow/project.json` — and those ids alone are in force. No agent takes a
179
+ * named lens input, so the stamp names the prompt, not a variable.
178
180
  */
179
181
  const RUNTIME_SELECTION_LINES = [
180
182
  'Every `references/{id}.md` is installed, so presence decides nothing: the ids you were given',
181
- "(`COMPLIANCE_FRAMEWORKS`, this machine's plus the repository's) are the frameworks in force.",
183
+ "(named in your prompt; this machine's plus the repository's) are the frameworks in force.",
182
184
  'Load `references/{id}.md` for each given id and no other; `none` means generic controls only.',
183
185
  'Apply generic controls always.',
184
186
  '',
@@ -80,6 +80,16 @@ export function setMachineFeature(rawManifest, feature, enabled, now) {
80
80
  updatedAt: now,
81
81
  };
82
82
  }
83
+ /**
84
+ * Whether a RAW parsed manifest already records `feature` as exactly `enabled`:
85
+ * `features.<feature>` is that boolean. Pure. Stricter than
86
+ * {@link isMachineFeatureOn} on purpose — an absent key, a non-boolean value and
87
+ * a legacy key all read as a state without being recorded as it, so writing the
88
+ * explicit boolean is still a change.
89
+ */
90
+ function isMachineFeatureRecorded(rawManifest, feature, enabled) {
91
+ return isJsonObject(rawManifest) && isJsonObject(rawManifest.features) && rawManifest.features[feature] === enabled;
92
+ }
83
93
  async function readRawManifest(devflowDir) {
84
94
  try {
85
95
  return JSON.parse(await fs.readFile(path.join(devflowDir, 'manifest.json'), 'utf-8'));
@@ -96,14 +106,44 @@ async function readRawManifest(devflowDir) {
96
106
  export async function readMachineFeature(devflowDir, feature) {
97
107
  return isMachineFeatureOn(await readRawManifest(devflowDir), feature);
98
108
  }
109
+ /**
110
+ * Whether a RAW parsed manifest has an opt-in `feature` switched on. Pure.
111
+ *
112
+ * D-BEHAVIOR-OPT-IN: only an explicit boolean `true` is on. A missing key, a
113
+ * non-boolean value, a manifest without a `features` record and anything that is not an
114
+ * object all read as OFF — fail-closed, the mirror of {@link isMachineFeatureOn}. Like
115
+ * that reader it is deliberately not built on readManifest(): a read-only status must
116
+ * not heal-write, and a manifest missing a required field is still a manifest here.
117
+ */
118
+ export function isOptInFeatureOn(rawManifest, feature) {
119
+ if (!isJsonObject(rawManifest) || !isJsonObject(rawManifest.features))
120
+ return false;
121
+ return rawManifest.features[feature] === true;
122
+ }
123
+ /**
124
+ * Read an opt-in machine switch from `<devflowDir>/manifest.json`. Read-only (no heal
125
+ * write); an absent, unreadable or malformed manifest reads as OFF.
126
+ */
127
+ export async function readOptInFeature(devflowDir, feature) {
128
+ return isOptInFeatureOn(await readRawManifest(devflowDir), feature);
129
+ }
99
130
  /**
100
131
  * Write the machine-wide switch to `<devflowDir>/manifest.json` (atomic
101
132
  * temp + rename). Refuses with `not-installed` when there is no manifest to
102
133
  * write into — a manifest is created by `devflow init`, never by a toggle,
103
134
  * because a bare `{features: {...}}` file is not a manifest any reader accepts.
135
+ *
136
+ * D-NOOP-TOGGLE: a toggle to the value the manifest already records writes
137
+ * nothing. The only bytes such a write could change are `updatedAt`, and that
138
+ * field means "the manifest's content last changed", so a no-op must not move
139
+ * it. This is the one write point of `devflow memory|learning|knowledge|behavior
140
+ * --enable/--disable`, so the rule holds for all four at once.
104
141
  */
105
142
  export async function writeMachineFeature(devflowDir, feature, enabled) {
106
- const next = setMachineFeature(await readRawManifest(devflowDir), feature, enabled, new Date().toISOString());
143
+ const raw = await readRawManifest(devflowDir);
144
+ if (isMachineFeatureRecorded(raw, feature, enabled))
145
+ return { ok: true, value: undefined };
146
+ const next = setMachineFeature(raw, feature, enabled, new Date().toISOString());
107
147
  if (next === null)
108
148
  return { ok: false, error: 'not-installed' };
109
149
  await writeFileAtomicExclusive(path.join(devflowDir, 'manifest.json'), JSON.stringify(next, null, 2) + '\n');
@@ -88,6 +88,45 @@ export const FLAG_REGISTRY = [
88
88
  recommended: true,
89
89
  defaultValue: true,
90
90
  },
91
+ {
92
+ // D-SUBAGENT-CACHE-5M: subagents write their prompt cache with the 5-minute TTL
93
+ // while the main session keeps the 1-hour TTL of `prompt-caching-1h` (untouched).
94
+ //
95
+ // Why: the wave-4 token audit (2026-10-10) found 99.998% of subagent cache writes
96
+ // are 1-hour writes, yet fewer than 0.1% of subagent requests idle longer than
97
+ // 5 minutes - a subagent finishes or is resumed long before a 5-minute entry
98
+ // lapses, so the 1-hour premium buys nothing. Modelled effect of 5m: -8.9% on
99
+ // subagent cache-equivalent cost, -7.7% on total.
100
+ //
101
+ // Mechanism (VENDOR BEHAVIOUR, probed 2026-10-11 on Claude Code 2.1.296 in a
102
+ // sandboxed session): with ENABLE_PROMPT_CACHING_1H=true and this setting at
103
+ // "5m", subagent requests wrote only ephemeral_5m cache (36,387 tokens, none at
104
+ // 1h) while the main session wrote 100% 1h (35,209). Precedence of the subagent
105
+ // TTL in the binary, highest first: an env var that forces 5m, env
106
+ // CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL, this setting, a per-agent frontmatter
107
+ // `experimental.cacheTtl`, ENABLE_PROMPT_CACHING_1H. The 1-hour TTL is ignored
108
+ // while a subscription is in overage. A Claude Code that predates the setting
109
+ // ignores the key, so subagents then follow ENABLE_PROMPT_CACHING_1H as before.
110
+ // Re-probe when the precedence matters again; this is an observation, not a spec.
111
+ //
112
+ // Rejected: per-agent `experimental.cacheTtl` - it would spread one policy over
113
+ // every agent file; one devflow-managed flag keeps the policy in one place.
114
+ //
115
+ // Delete guard: only "5m" is deleted on disable or uninstall, so a user's own
116
+ // "1h" stays (D-GUARD-SCALAR). Adoption: init seeds this flag OFF when a value
117
+ // that is not "5m" is already on disk (D-ADOPT-RESPECTS-USER, respectUserOwnedSettings).
118
+ id: 'subagent-cache-5m',
119
+ label: 'Subagent 5-min cache',
120
+ description: 'Write subagent prompt-cache entries with the 5-minute TTL; the main session keeps 1 hour',
121
+ hint: 'Subagents rarely idle past 5 min, so 5-min cache writes cost less',
122
+ blurb: '5-min cache TTL for subagents',
123
+ kind: 'boolean',
124
+ target: { type: 'setting', key: 'subagentPromptCacheTtl' },
125
+ onPayload: '5m',
126
+ settingDeleteGuard: '5m',
127
+ recommended: true,
128
+ defaultValue: true,
129
+ },
91
130
  {
92
131
  id: 'show-turn-duration',
93
132
  label: 'Show turn duration',
@@ -423,6 +462,34 @@ export const FLAG_REGISTRY = [
423
462
  integer: true,
424
463
  upstreamDefault: 600000,
425
464
  },
465
+ {
466
+ // D-AUTO-COMPACT-WINDOW-OPT-IN: opt-in and unset by default (undefined → manifest null →
467
+ // key never written). devflow writes CLAUDE_CODE_AUTO_COMPACT_WINDOW only when the user sets
468
+ // this flag. Init applies a seeded record without asking, which is why the default is neutral
469
+ // and `recommended` stays false: a smaller window compacts sooner, and a compaction
470
+ // mid-command is exactly what the SessionStart resume directive (D-COMPACT-RESUME-DIRECTIVE)
471
+ // recovers from, so the flag is not recommended until a forced mid-/implement /compact has
472
+ // been seen to resume at the phase after the last finished one (plan §9, a follow-up gate).
473
+ // The percent-based auto-compact override is a different variable that devflow deliberately
474
+ // never writes or names; tests/guards/no-autocompact-pct-override.test.ts keeps it out of src/.
475
+ // The env name was confirmed present in Claude Code 2.1.296, where it takes precedence over
476
+ // the in-app setting, and an in-app message names `auto` or 100k–1M (docs/reference/
477
+ // claude-code-flags-probe.md). No upstreamDefault: the default window and the grammar of the
478
+ // variable's own value were not observed, so the description states only devflow's own
479
+ // accepted range, 100000–1000000.
480
+ id: 'auto-compact-window',
481
+ label: 'Auto-compact window',
482
+ description: 'Context window size in tokens at which Claude Code auto-compacts',
483
+ hint: 'Sets the auto-compact window (100000-1000000); unset keeps the default',
484
+ blurb: 'auto-compact window size',
485
+ kind: 'number',
486
+ target: { type: 'env', key: 'CLAUDE_CODE_AUTO_COMPACT_WINDOW' },
487
+ recommended: false,
488
+ defaultValue: undefined,
489
+ min: 100000, // devflow sanity bound
490
+ max: 1000000, // devflow sanity bound
491
+ integer: true,
492
+ },
426
493
  {
427
494
  // Writes as { command: value } per Claude Code spellcheck setting shape.
428
495
  id: 'spellcheck',
@@ -941,6 +1008,78 @@ export function settingHoldsManagedShape(settingsJson, flagId) {
941
1008
  return false;
942
1009
  }
943
1010
  }
1011
+ /**
1012
+ * D-ADOPT-RESPECTS-USER predicate: the mirror of `settingHoldsManagedShape`.
1013
+ *
1014
+ * Returns true when the settings.json string carries a value at the guarded flag's
1015
+ * target key that is NOT the flag's managed shape - a value the user (or another
1016
+ * tool) owns, which enabling the flag would overwrite. Presence is decided with
1017
+ * `hasOwnProperty`, so an explicit `null` counts as a value; the shape decision is
1018
+ * delegated to `settingValueHoldsManagedShape`, never compared here.
1019
+ *
1020
+ * Returns false when:
1021
+ * - settingsJson is malformed
1022
+ * - root is not a plain object
1023
+ * - flagId is not in the registry
1024
+ * - the flag has no settingDeleteGuard, or is not a setting-target boolean
1025
+ * - the key is absent
1026
+ * - the on-disk value holds the managed shape
1027
+ */
1028
+ export function settingHoldsUserValue(settingsJson, flagId) {
1029
+ try {
1030
+ const parsed = JSON.parse(settingsJson);
1031
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
1032
+ return false;
1033
+ const flag = FLAG_REGISTRY_MAP.get(flagId);
1034
+ if (!flag || flag.kind !== 'boolean' || flag.target.type !== 'setting')
1035
+ return false;
1036
+ if (flag.settingDeleteGuard === undefined)
1037
+ return false;
1038
+ const root = parsed;
1039
+ if (!Object.prototype.hasOwnProperty.call(root, flag.target.key))
1040
+ return false;
1041
+ return !settingValueHoldsManagedShape(flag, root[flag.target.key]);
1042
+ }
1043
+ catch {
1044
+ return false;
1045
+ }
1046
+ }
1047
+ /**
1048
+ * D-ADOPT-RESPECTS-USER: seed a guarded setting flag OFF when adopting it ON would
1049
+ * overwrite a value the user already owns.
1050
+ *
1051
+ * A flag the manifest does not record is adopted at its `defaultValue`. For a flag whose
1052
+ * target is a settings.json key with a `settingDeleteGuard`, adopting ON writes its
1053
+ * managed payload over whatever is on disk (the guard protects deletion, not writes).
1054
+ * When the key already holds a value that is not the managed shape - a user's own `"1h"`
1055
+ * under `subagentPromptCacheTtl` - that write would silently replace a deliberate choice.
1056
+ * Such a flag is set `false` instead; with the record then claiming `false`, the converge
1057
+ * pipeline neither writes nor deletes the user's value.
1058
+ *
1059
+ * Scope: only flags ABSENT from `manifestFlags` (or all of them on a fresh install, when
1060
+ * it is null). A flag the manifest records keeps its value in `flags`, so an explicit
1061
+ * enable still wins. `devflow init --reset` hands this an empty settings snapshot
1062
+ * (resolveResetGatedInputs), so nothing is detected and the factory default stands. The rule is generic over guarded
1063
+ * setting flags: no flag id is special-cased, and `settingHoldsUserValue` is the one place
1064
+ * that decides "the disk holds something that is not ours". For a flag whose default is
1065
+ * already OFF (suppress-attribution) it changes nothing.
1066
+ *
1067
+ * Two callers apply the same rule: the init seed, and the flags TUI's starting rows (an
1068
+ * upgrade that has not run init yet would otherwise offer a Save that overwrites the value).
1069
+ *
1070
+ * Pure function - the settings snapshot is read by the caller and passed in.
1071
+ */
1072
+ export function respectUserOwnedSettings(flags, manifestFlags, settingsSnapshot) {
1073
+ const result = { ...flags };
1074
+ for (const flag of FLAG_REGISTRY) {
1075
+ const adoptedAtDefault = manifestFlags === null || !(flag.id in manifestFlags);
1076
+ if (!adoptedAtDefault)
1077
+ continue;
1078
+ if (settingHoldsUserValue(settingsSnapshot, flag.id))
1079
+ result[flag.id] = false;
1080
+ }
1081
+ return result;
1082
+ }
944
1083
  /**
945
1084
  * D-ATTR-GUARD: returns true when it is safe to delete a flag's target key from
946
1085
  * settings. For flags with a settingDeleteGuard the key is only deleted when the
@@ -28,15 +28,20 @@ import { promises as fs } from 'fs';
28
28
  *
29
29
  * @param filePath - Absolute path to the target file.
30
30
  * @param data - UTF-8 encoded content to write.
31
+ * @param createMode - Permission bits the temp file is CREATED with (masked by the
32
+ * umask), so a file that must be owner-only is never readable by others, not even
33
+ * between the write and the rename. Omitted, the umask default applies. Either way
34
+ * an existing target's mode wins (below).
31
35
  */
32
- export async function writeFileAtomicExclusive(filePath, data) {
36
+ export async function writeFileAtomicExclusive(filePath, data, createMode) {
33
37
  // PID-scope the tmp name so concurrent writers from different processes
34
38
  // (e.g., two Claude Code sessions) never collide on the same .tmp path.
35
39
  // mirrors proxy-log.ts rotation at src/core/proxy-log.ts which PID-scopes
36
40
  // for the same reason.
37
41
  const tmp = `${filePath}.tmp.${process.pid}`;
42
+ const options = { encoding: 'utf-8', flag: 'wx', ...(createMode === undefined ? {} : { mode: createMode }) };
38
43
  try {
39
- await fs.writeFile(tmp, data, { encoding: 'utf-8', flag: 'wx' });
44
+ await fs.writeFile(tmp, data, options);
40
45
  }
41
46
  catch (err) {
42
47
  if (err.code !== 'EEXIST')
@@ -48,7 +53,7 @@ export async function writeFileAtomicExclusive(filePath, data) {
48
53
  await fs.unlink(tmp);
49
54
  }
50
55
  catch { /* race — already removed */ }
51
- await fs.writeFile(tmp, data, { encoding: 'utf-8', flag: 'wx' });
56
+ await fs.writeFile(tmp, data, options);
52
57
  }
53
58
  // Preserve the target's permission mode across the atomic replace.
54
59
  // A user who hardened the target (e.g. settings.json → 0600 to protect