devflow-kit 3.9.0 → 3.11.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 (168) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +1 -1
  3. package/dist/agents/code.md +6 -6
  4. package/dist/agents/fix.md +6 -6
  5. package/dist/agents/harden.md +7 -6
  6. package/dist/agents/refactor.md +6 -6
  7. package/dist/cli/commands/ambient.js +10 -2
  8. package/dist/cli/commands/backup-session.js +131 -0
  9. package/dist/cli/commands/compliance-prompts.js +9 -4
  10. package/dist/cli/commands/compliance.js +184 -24
  11. package/dist/cli/commands/curation.js +23 -10
  12. package/dist/cli/commands/gate-level-prompts.js +39 -25
  13. package/dist/cli/commands/gates.js +5 -7
  14. package/dist/cli/commands/init.js +169 -184
  15. package/dist/cli/commands/install-report.js +129 -0
  16. package/dist/cli/commands/legacy-sweep.js +159 -0
  17. package/dist/cli/commands/proxy.js +6 -1
  18. package/dist/cli/commands/rules.js +75 -21
  19. package/dist/cli/commands/skills.js +42 -9
  20. package/dist/cli/commands/tracker.js +9 -3
  21. package/dist/cli/commands/uninstall.js +339 -75
  22. package/dist/commands/bug-analysis.md +69 -120
  23. package/dist/commands/dynamic-build.md +23 -23
  24. package/dist/commands/implement.md +46 -32
  25. package/dist/commands/release.md +1 -1
  26. package/dist/commands/resolve.md +35 -31
  27. package/dist/commands/self-review.md +8 -7
  28. package/dist/compliance-off/agents/git.md +120 -0
  29. package/dist/compliance-off/agents/triage.md +180 -0
  30. package/dist/compliance-off/commands/bug-analysis.md +398 -0
  31. package/dist/compliance-off/commands/code-review.md +520 -0
  32. package/dist/compliance-off/commands/dynamic-build.md +1251 -0
  33. package/dist/compliance-off/commands/dynamic-tickets.md +676 -0
  34. package/dist/compliance-off/commands/implement.md +736 -0
  35. package/dist/compliance-off/commands/plan.md +671 -0
  36. package/dist/compliance-off/commands/release.md +309 -0
  37. package/dist/compliance-off/commands/resolve.md +742 -0
  38. package/dist/compliance-off/skills/git/references/decision-markers.md +17 -0
  39. package/dist/compliance-off/skills/git/references/learn-conventions.md +80 -0
  40. package/dist/compliance-off/skills/git/references/pr/check-ci-status.md +33 -0
  41. package/dist/compliance-off/skills/git/references/pr/check-merge-readiness.md +49 -0
  42. package/dist/compliance-off/skills/git/references/pr/fetch-review-threads.md +52 -0
  43. package/dist/compliance-off/skills/git/references/pr/validate-branch.md +47 -0
  44. package/dist/compliance-off/skills/git/references/tracker/_contract.md +33 -0
  45. package/dist/compliance-off/skills/git/references/tracker/_mcp.md +153 -0
  46. package/dist/compliance-off/skills/git/references/tracker/github/create-release.md +35 -0
  47. package/dist/compliance-off/skills/git/references/tracker/github/ensure-pr-ready.md +100 -0
  48. package/dist/compliance-off/skills/git/references/tracker/github/ensure-traceable-issue.md +74 -0
  49. package/dist/compliance-off/skills/git/references/tracker/github/fetch-issue.md +69 -0
  50. package/dist/compliance-off/skills/git/references/tracker/github/fetch-issues-batch.md +65 -0
  51. package/dist/compliance-off/skills/git/references/tracker/github/gather-release-evidence.md +48 -0
  52. package/dist/compliance-off/skills/git/references/tracker/github/setup-task.md +77 -0
  53. package/dist/compliance-off/skills/git/references/tracker/jira/create-release.md +35 -0
  54. package/dist/compliance-off/skills/git/references/tracker/jira/ensure-pr-ready.md +100 -0
  55. package/dist/compliance-off/skills/git/references/tracker/jira/ensure-traceable-issue.md +61 -0
  56. package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issue.md +51 -0
  57. package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issues-batch.md +63 -0
  58. package/dist/compliance-off/skills/git/references/tracker/jira/gather-release-evidence.md +47 -0
  59. package/dist/compliance-off/skills/git/references/tracker/jira/setup-task.md +82 -0
  60. package/dist/compliance-off/skills/git/references/tracker/linear/create-release.md +35 -0
  61. package/dist/compliance-off/skills/git/references/tracker/linear/ensure-pr-ready.md +100 -0
  62. package/dist/compliance-off/skills/git/references/tracker/linear/ensure-traceable-issue.md +61 -0
  63. package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issue.md +51 -0
  64. package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issues-batch.md +63 -0
  65. package/dist/compliance-off/skills/git/references/tracker/linear/gather-release-evidence.md +47 -0
  66. package/dist/compliance-off/skills/git/references/tracker/linear/setup-task.md +83 -0
  67. package/dist/compliance-off/skills/git/references/trust-rule.md +7 -0
  68. package/dist/core/agent-models.js +19 -4
  69. package/dist/core/assets.js +126 -17
  70. package/dist/core/compliance-off-files.js +27 -0
  71. package/dist/core/compliance.js +14 -0
  72. package/dist/core/curation-variants.js +460 -112
  73. package/dist/core/flags.js +2 -2
  74. package/dist/core/fs-atomic.js +5 -1
  75. package/dist/core/installed-variants.js +63 -0
  76. package/dist/core/manifest.js +48 -5
  77. package/dist/core/mds-variants.js +700 -3
  78. package/dist/core/orphan-sweep.js +29 -2
  79. package/dist/core/path-set.js +15 -0
  80. package/dist/core/plugins.js +68 -10
  81. package/dist/core/reference-sweep.js +42 -25
  82. package/dist/core/retired-shipped-names.js +278 -0
  83. package/dist/curation-compliance-off/agents/triage.md +176 -0
  84. package/dist/curation-compliance-off/commands/bug-analysis.md +349 -0
  85. package/dist/curation-compliance-off/commands/code-review.md +481 -0
  86. package/dist/curation-compliance-off/commands/dynamic-build.md +1211 -0
  87. package/dist/curation-compliance-off/commands/dynamic-tickets.md +634 -0
  88. package/dist/curation-compliance-off/commands/implement.md +689 -0
  89. package/dist/curation-compliance-off/commands/plan.md +631 -0
  90. package/dist/curation-compliance-off/commands/release.md +279 -0
  91. package/dist/curation-compliance-off/commands/resolve.md +692 -0
  92. package/dist/curation-off/agents/code.md +6 -6
  93. package/dist/curation-off/agents/fix.md +6 -6
  94. package/dist/curation-off/agents/harden.md +7 -6
  95. package/dist/curation-off/agents/refactor.md +6 -6
  96. package/dist/curation-off/commands/bug-analysis.md +69 -119
  97. package/dist/curation-off/commands/dynamic-build.md +23 -23
  98. package/dist/curation-off/commands/implement.md +46 -32
  99. package/dist/curation-off/commands/release.md +1 -1
  100. package/dist/curation-off/commands/resolve.md +35 -31
  101. package/dist/curation-off/commands/self-review.md +8 -7
  102. package/dist/skills/git/references/pr/resolve-review-threads.md +1 -1
  103. package/dist/targets/claude-code/behavior-install.js +23 -9
  104. package/dist/targets/claude-code/charter-render.js +374 -0
  105. package/dist/targets/claude-code/compliance-install.js +62 -17
  106. package/dist/targets/claude-code/compliance-off-files.js +249 -0
  107. package/dist/targets/claude-code/curation-install.js +632 -117
  108. package/dist/targets/claude-code/folder-swap.js +142 -0
  109. package/dist/targets/claude-code/install-backup.js +780 -0
  110. package/dist/targets/claude-code/installer.js +795 -199
  111. package/dist/targets/claude-code/language-stamp.js +6 -0
  112. package/dist/targets/claude-code/legacy-skill-hashes.js +170 -0
  113. package/dist/targets/claude-code/linked-folder.js +176 -0
  114. package/dist/targets/claude-code/reference-converge.js +131 -0
  115. package/dist/targets/claude-code/rules-folder.js +34 -0
  116. package/dist/targets/claude-code/shipped-set.js +160 -0
  117. package/dist/targets/claude-code/tracker-install.js +21 -8
  118. package/dist/targets/claude-code/tracker-servers.js +20 -6
  119. package/dist/targets/claude-code/workflow-folder-install.js +251 -0
  120. package/dist/variant-axes.json +109 -0
  121. package/package.json +1 -1
  122. package/src/assets/agents/_partials/_code.mds +6 -6
  123. package/src/assets/agents/evaluate.md +1 -1
  124. package/src/assets/agents/git.mds +32 -0
  125. package/src/assets/agents/harden.mds +7 -6
  126. package/src/assets/agents/test.md +21 -12
  127. package/src/assets/agents/triage.mds +2 -0
  128. package/src/assets/agents/validate.md +10 -10
  129. package/src/assets/commands/_partials/_compliance.mds +5 -3
  130. package/src/assets/commands/_partials/_engine.mds +9 -8
  131. package/src/assets/commands/_partials/_publication.mds +10 -1
  132. package/src/assets/commands/bug-analysis.mds +80 -126
  133. package/src/assets/commands/code-review.mds +43 -0
  134. package/src/assets/commands/dynamic-build.mds +109 -15
  135. package/src/assets/commands/dynamic-tickets.mds +13 -0
  136. package/src/assets/commands/implement.mds +145 -33
  137. package/src/assets/commands/plan.mds +19 -0
  138. package/src/assets/commands/release.mds +50 -1
  139. package/src/assets/commands/resolve.mds +217 -32
  140. package/src/assets/commands/self-review.mds +8 -7
  141. package/src/assets/mds/git/_ops.mds +23 -0
  142. package/src/assets/mds/git/_ops_steps.mds +17 -0
  143. package/src/assets/mds/git/_pr.mds +1 -1
  144. package/src/assets/mds/git/_references.mds +6 -0
  145. package/src/assets/mds/tracker/_common.mds +4 -0
  146. package/src/assets/mds/tracker/_github.mds +42 -0
  147. package/src/assets/mds/tracker/_jira.mds +43 -0
  148. package/src/assets/mds/tracker/_linear.mds +43 -0
  149. package/src/assets/rules/quality.md +2 -0
  150. package/src/assets/scripts/ci-wait.cjs +204 -126
  151. package/src/assets/scripts/hooks/assets/implement-installed +1 -0
  152. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +13 -11
  153. package/src/assets/scripts/hooks/preamble +18 -8
  154. package/src/assets/scripts/hooks/session-start-orchestrator +4 -2
  155. package/src/assets/scripts/pr-evidence.cjs +41 -2
  156. package/src/assets/scripts/verify-evidence.cjs +408 -56
  157. package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +3 -3
  158. package/src/assets/skills/agent-references/references/commands/plan-levels.md +1 -1
  159. package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +6 -2
  160. package/src/assets/skills/agent-references/references/validate/passed-alone.md +7 -7
  161. package/src/assets/skills/docs-framework/SKILL.md +5 -2
  162. package/src/assets/skills/git/SKILL.md +3 -3
  163. package/src/assets/skills/git/references/github-api.md +2 -139
  164. package/src/assets/skills/git/references/github-posting.md +158 -0
  165. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  166. package/src/assets/skills/review-methodology/references/patterns.md +1 -1
  167. package/src/assets/skills/review-methodology/references/report-template.md +1 -2
  168. package/src/assets/skills/testing/SKILL.md +2 -4
@@ -2,11 +2,14 @@
2
2
  * devflow compliance — Enable, disable, set, and check status of the compliance feature.
3
3
  *
4
4
  * CLI-layer module; pure helpers in src/core/compliance.ts,
5
- * I/O orchestration in src/targets/claude-code/compliance-install.ts.
5
+ * I/O orchestration in src/targets/claude-code/curation-install.ts (the one converge
6
+ * for both axes) and compliance-install.ts (the compliance artifacts it composes).
6
7
  * Compliance is manifest-group (like proxy), not config.json-gated.
7
8
  * Per-artifact failures are warn-not-throw, so one failed artifact never aborts the rest.
8
- * Enable/disable each converge BOTH artifacts unconditionally, so neither is left
9
- * half-converged.
9
+ * Every change of the switch (enable, disable, --set, interactive enable) writes the switch,
10
+ * runs the one converge, and only after a clean converge records which variants are installed.
11
+ * With compliance off the command fails closed (AC-28): it exits non-zero when an installed
12
+ * prompt may still post, and prints no success line.
10
13
  * The evidence-policy lines (--status, and the --enable/--set suggestion) come
11
14
  * from src/core/evidence-policy.ts, the seam onto the package's own resolvers;
12
15
  * the CLI prints the keys to add to .devflow/project.json and never writes it
@@ -18,10 +21,14 @@ import * as path from 'path';
18
21
  import * as p from '@clack/prompts';
19
22
  import color from 'picocolors';
20
23
  import { COMPLIANCE_FRAMEWORKS, normalizeFrameworks, parseFrameworkList, } from '../../core/compliance.js';
24
+ import { openBackupSessionOrFail, runWithBackupReport } from './backup-session.js';
21
25
  import { frameworkChoices, FRAMEWORK_SELECT_MESSAGE } from './compliance-prompts.js';
22
26
  import { COMPLIANCE_SKILL_TOKENS } from '../../core/compliance-compose.js';
23
27
  import { evidencePolicyStatusLine, evidencePolicySuggestion, formatEvidencePolicyUnavailable, loadEvidencePolicyModule, loadSettingsModule, repoComplianceStatusLines, } from '../../core/evidence-policy.js';
28
+ import { postingStatus, readInstalledVariants } from '../../core/installed-variants.js';
29
+ import { withManifestLock } from '../../core/manifest-lock.js';
24
30
  import { readManifest, writeManifest } from '../../core/manifest.js';
31
+ import { applyVariantToggle, describePostingLeft, describeVariantConverge, readRunningVersion, } from '../../targets/claude-code/curation-install.js';
25
32
  import { convergeFromManifest } from '../../targets/claude-code/compliance-install.js';
26
33
  import { validateRuleShadow, validateSkillShadow } from '../../targets/claude-code/installer.js';
27
34
  import { getClaudeDirectory, getDevFlowDirectory, } from '../../targets/claude-code/claude-paths.js';
@@ -67,7 +74,7 @@ export function resolveComplianceCliAction(current, action, setFrameworks) {
67
74
  messages: [
68
75
  {
69
76
  level: 'success',
70
- text: 'Compliance disabled — rule removed, frameworks remembered for re-enable',
77
+ text: 'Compliance disabled — the rule and every posting step removed, frameworks remembered for re-enable',
71
78
  },
72
79
  ],
73
80
  };
@@ -118,10 +125,12 @@ async function skillInstalled(claudeDir) {
118
125
  return false;
119
126
  }
120
127
  }
128
+ /** The installed compliance rule file. */
129
+ const complianceRuleFile = (claudeDir) => path.join(claudeDir, 'rules', 'devflow', 'compliance.md');
121
130
  /** Returns true if the compliance rule file exists at the install target. */
122
131
  async function ruleInstalled(claudeDir) {
123
132
  try {
124
- await fs.access(path.join(claudeDir, 'rules', 'devflow', 'compliance.md'));
133
+ await fs.access(complianceRuleFile(claudeDir));
125
134
  return true;
126
135
  }
127
136
  catch {
@@ -156,10 +165,153 @@ async function skillShadowState(devflowDir) {
156
165
  return 'none';
157
166
  }
158
167
  }
168
+ /** The `Posting:` line of `--status`: the install record read against the machine's compliance switch (AC-31). */
169
+ export function postingStatusLine(record, complianceEnabled) {
170
+ const status = postingStatus(readInstalledVariants(record), complianceEnabled);
171
+ const shown = status === 'installed' ? color.green(status) : status === 'not installed' ? color.dim(status) : color.yellow(status);
172
+ return `Posting: ${shown}`;
173
+ }
174
+ /**
175
+ * Write the switch under the manifest lock, into the manifest read inside the lock, so a field
176
+ * that another devflow run wrote since this command first read the manifest is kept. A lock that
177
+ * cannot be taken, or is lost before the write, writes nothing. Null when the switch is on disk,
178
+ * otherwise the reason it is not. Never throws.
179
+ */
180
+ async function recordComplianceSwitch(devflowDir, nextState) {
181
+ try {
182
+ const locked = await withManifestLock(devflowDir, {}, async (lock) => {
183
+ const current = await readManifest(devflowDir);
184
+ if (current === null)
185
+ return 'no readable install manifest';
186
+ if (!(await lock.stillHeld()))
187
+ return 'the install manifest lock was lost';
188
+ await writeManifest(devflowDir, {
189
+ ...current,
190
+ features: { ...current.features, compliance: nextState },
191
+ updatedAt: new Date().toISOString(),
192
+ });
193
+ return null;
194
+ });
195
+ return locked.ok ? locked.value : 'the install manifest lock could not be taken';
196
+ }
197
+ catch (err) {
198
+ return String(err);
199
+ }
200
+ }
201
+ /**
202
+ * Apply one change of the compliance switch: write the switch, run the one converge for both
203
+ * axes, and only after a clean converge record which variants are installed (the toggle helper
204
+ * does that last step). Every change of the enabled value converges, `--set` and interactive enable
205
+ * included, and a `--set` that only changes the frameworks converges too, so the stamp follows.
206
+ *
207
+ * Fail closed on the off side: with compliance off, a converge that cannot prove every installed
208
+ * compliance-armed prompt holds the compliance-off file is an error that lists the files, a
209
+ * toggle that cannot converge at all (a version skew, or no manifest) says that posting steps remain
210
+ * installed until `devflow init`, and a compliance rule that is still installed is an error that
211
+ * names the file. None of them prints the success text. A skipped prompt converge still converges
212
+ * the compliance skill and rule, which do not depend on the installed version. On the enable side
213
+ * the skip is a warning, and a rule that is wanted and still not installed is a second warning.
214
+ * A reference that the converge could not refresh, a linked folder for example, is an error that
215
+ * names it, with exit 1, and the switch stays recorded as on.
216
+ *
217
+ * Returns a result and never throws; the caller prints the messages and sets the exit code. The
218
+ * record of the backup session prints once, as the change returns (an exit-1 result included),
219
+ * ahead of the messages.
220
+ */
221
+ export function applyComplianceChange(opts) {
222
+ return runWithBackupReport(opts.backups, () => applyChange(opts));
223
+ }
224
+ async function applyChange(opts) {
225
+ const { claudeDir, devflowDir, manifest, action, setFrameworks, runningVersion, packageRoot, warn, backups } = opts;
226
+ const resolved = resolveComplianceCliAction(manifest.features.compliance, action, setFrameworks);
227
+ // normalizeFrameworks deduplicates (e.g. --set gdpr,GDPR → [gdpr]).
228
+ const nextState = {
229
+ enabled: resolved.nextState.enabled,
230
+ frameworks: normalizeFrameworks(resolved.nextState.frameworks),
231
+ };
232
+ // The switch comes first: whatever the converge does next, the machine's choice is on disk.
233
+ const notRecorded = await recordComplianceSwitch(devflowDir, nextState);
234
+ if (notRecorded !== null) {
235
+ return {
236
+ nextState: manifest.features.compliance,
237
+ messages: [{ level: 'error', text: `Could not record the compliance switch, so nothing was changed: ${notRecorded}` }],
238
+ exitCode: 1,
239
+ };
240
+ }
241
+ const outcome = await applyVariantToggle({
242
+ claudeDir,
243
+ devflowDir,
244
+ axis: 'compliance',
245
+ enabled: nextState.enabled,
246
+ runningVersion,
247
+ warn,
248
+ packageRoot,
249
+ backups,
250
+ });
251
+ if (outcome.kind === 'skipped') {
252
+ // The compliance skill and rule do not depend on the installed version, so they converge
253
+ // whatever the prompt converge did. The converge warns per item and never throws.
254
+ await convergeFromManifest({
255
+ claudeDir,
256
+ devflowDir,
257
+ manifest: { features: { compliance: nextState, rules: manifest.features.rules } },
258
+ warn,
259
+ backups,
260
+ });
261
+ if (!nextState.enabled) {
262
+ return { nextState, messages: [{ level: 'error', text: outcome.message }], exitCode: 1 };
263
+ }
264
+ const ruleNote = manifest.features.rules && !(await ruleInstalled(claudeDir))
265
+ ? [{ level: 'warn', text: 'The compliance rule was not installed. Run `devflow init`.' }]
266
+ : [];
267
+ return { nextState, messages: [...resolved.messages, ...ruleNote, { level: 'warn', text: outcome.message }], exitCode: 0 };
268
+ }
269
+ const { posting } = outcome.result;
270
+ if (!nextState.enabled && posting.kind !== 'clear') {
271
+ return {
272
+ nextState,
273
+ messages: [
274
+ { level: 'error', text: 'Compliance is recorded as off, but the install may still post to GitHub or a tracker.' },
275
+ ...describePostingLeft(posting).map((text) => ({ level: 'error', text })),
276
+ ],
277
+ exitCode: 1,
278
+ };
279
+ }
280
+ // A failed rule removal is only a warning inside the converge. The line that says the rule is
281
+ // removed prints only when it is gone.
282
+ if (!nextState.enabled && (await ruleInstalled(claudeDir))) {
283
+ return {
284
+ nextState,
285
+ messages: [{
286
+ level: 'error',
287
+ text: `Compliance is recorded as off, but the compliance rule could not be removed: ${complianceRuleFile(claudeDir)}. ` +
288
+ 'Fix the cause in the warning above, or delete the file. Then run `devflow compliance --disable` again.',
289
+ }],
290
+ exitCode: 1,
291
+ };
292
+ }
293
+ const referenceFailures = outcome.result.references.failures;
294
+ if (nextState.enabled && referenceFailures.length > 0) {
295
+ return {
296
+ nextState,
297
+ messages: [{
298
+ level: 'error',
299
+ text: 'Compliance is recorded as on, but some installed references were not refreshed: ' +
300
+ `${referenceFailures.join('; ')}. Replace any link in the cause with a real folder. Then run the command again.`,
301
+ }],
302
+ exitCode: 1,
303
+ };
304
+ }
305
+ const messages = [...resolved.messages];
306
+ const changed = describeVariantConverge(outcome.result, 'compliance', nextState.enabled);
307
+ if (changed !== null)
308
+ messages.push({ level: 'info', text: `Installed prompts: ${changed}` });
309
+ return { nextState, messages, exitCode: 0 };
310
+ }
159
311
  export const complianceCommand = new Command('compliance')
160
312
  .description('Enable, disable, or configure the compliance feature')
161
- .option('--enable', 'Enable compliance (restores previously selected frameworks)')
162
- .option('--disable', 'Disable compliance (rule removed; frameworks remembered for re-enable)')
313
+ .option('--enable', 'Enable compliance (restores previously selected frameworks and installs the steps that post evidence and summaries to GitHub or a tracker)')
314
+ .option('--disable', 'Disable compliance (rule and every step that posts evidence or summaries to GitHub or a tracker removed; frameworks remembered for re-enable)')
163
315
  .option('--status', 'Show compliance state: manifest, installed artifacts, shadow presence, and the evidence policy for the current repository')
164
316
  .option('--set <list>', 'Set active frameworks (comma-separated IDs); enables compliance. Use --set "" for zero frameworks (generic controls only)')
165
317
  .action(async (options) => {
@@ -169,8 +321,8 @@ export const complianceCommand = new Command('compliance')
169
321
  if (!hasFlag) {
170
322
  p.intro(color.bgCyan(color.white(' Compliance ')));
171
323
  const validIds = COMPLIANCE_FRAMEWORKS.map(fw => `${fw.id} — ${fw.hint}`).join('\n ');
172
- p.note(`${color.cyan('devflow compliance --enable')} Enable compliance (restores prior frameworks)\n` +
173
- `${color.cyan('devflow compliance --disable')} Disable compliance (frameworks remembered)\n` +
324
+ p.note(`${color.cyan('devflow compliance --enable')} Enable compliance (restores prior frameworks; installs the posting steps)\n` +
325
+ `${color.cyan('devflow compliance --disable')} Disable compliance (removes the posting steps; frameworks remembered)\n` +
174
326
  `${color.cyan('devflow compliance --status')} Show current state and installed artifacts\n` +
175
327
  `${color.cyan('devflow compliance --set <ids>')} Set active frameworks and enable\n\n` +
176
328
  `Valid framework IDs:\n ${validIds}`, 'Usage');
@@ -228,6 +380,7 @@ export const complianceCommand = new Command('compliance')
228
380
  ? color.yellow(' (withheld — rules disabled)')
229
381
  : '') +
230
382
  (isRuleShadowed ? color.green(' [shadowed]') : ''),
383
+ postingStatusLine(manifest.installedVariants, current.enabled),
231
384
  '',
232
385
  // The repository in cwd, resolved by the package's own resolver with the
233
386
  // compliance state already read above (D-POLICY-CJS-SEAM). Bounded: at most
@@ -277,23 +430,23 @@ export const complianceCommand = new Command('compliance')
277
430
  else {
278
431
  action = 'disable';
279
432
  }
280
- const resolved = resolveComplianceCliAction(current, action, setFrameworks);
281
- // Converge artifacts (always converge, never short-circuit)
282
- await convergeFromManifest({
433
+ // The session comes before the first change. Without a shipped set the command changes nothing.
434
+ const backups = await openBackupSessionOrFail({ claudeDir, devflowDir });
435
+ if (backups === null)
436
+ return;
437
+ // Write the switch, converge both axes, record the installed variants (applyComplianceChange).
438
+ const applied = await applyComplianceChange({
283
439
  claudeDir,
284
440
  devflowDir,
285
- manifest: { features: { compliance: resolved.nextState, rules: manifest.features.rules } },
441
+ manifest,
442
+ action,
443
+ setFrameworks,
444
+ runningVersion: await readRunningVersion(),
286
445
  warn: (msg) => p.log.warn(msg),
446
+ backups,
287
447
  });
288
- // Persist to manifest — normalizeFrameworks deduplicates (e.g. --set gdpr,GDPR → [gdpr])
289
- manifest.features.compliance = {
290
- enabled: resolved.nextState.enabled,
291
- frameworks: normalizeFrameworks(resolved.nextState.frameworks),
292
- };
293
- manifest.updatedAt = new Date().toISOString();
294
- await writeManifest(devflowDir, manifest);
295
448
  // Emit messages
296
- for (const msg of resolved.messages) {
449
+ for (const msg of applied.messages) {
297
450
  switch (msg.level) {
298
451
  case 'success':
299
452
  p.log.success(msg.text);
@@ -301,18 +454,25 @@ export const complianceCommand = new Command('compliance')
301
454
  case 'warn':
302
455
  p.log.warn(msg.text);
303
456
  break;
457
+ case 'error':
458
+ p.log.error(msg.text);
459
+ break;
304
460
  default:
305
461
  p.log.info(msg.text);
306
462
  break;
307
463
  }
308
464
  }
309
- if (resolved.nextState.enabled && !manifest.features.rules) {
465
+ if (applied.exitCode !== 0) {
466
+ process.exitCode = applied.exitCode;
467
+ return;
468
+ }
469
+ if (applied.nextState.enabled && !manifest.features.rules) {
310
470
  p.log.info(color.dim('Note: compliance rule withheld (rules disabled) — ' +
311
471
  'run `devflow rules --enable` to install the stamped rule'));
312
472
  }
313
473
  // Suggest the team file compliance now implies. Printed, never written:
314
474
  // .devflow/project.json is team-owned (D-POLICY-NO-WRITE).
315
- if (resolved.nextState.enabled) {
475
+ if (applied.nextState.enabled) {
316
476
  const policyModule = loadEvidencePolicyModule();
317
477
  const settingsModule = loadSettingsModule();
318
478
  if (!policyModule.ok) {
@@ -322,7 +482,7 @@ export const complianceCommand = new Command('compliance')
322
482
  p.log.warn(formatEvidencePolicyUnavailable(settingsModule.error));
323
483
  }
324
484
  else {
325
- const suggestion = evidencePolicySuggestion(resolved.nextState, policyModule.value, settingsModule.value);
485
+ const suggestion = evidencePolicySuggestion(applied.nextState, policyModule.value, settingsModule.value);
326
486
  if (suggestion !== null)
327
487
  p.note(suggestion, 'Evidence policy');
328
488
  }
@@ -9,7 +9,8 @@ import { loadShippedAgentDefaults } from '../../core/agent-models.js';
9
9
  import { readMachineFeature, writeMachineFeature } from '../../core/feature-switch.js';
10
10
  import { loadSettingsModule, narrowedSwitchLabel, personalConfigTrackedWarning } from '../../core/evidence-policy.js';
11
11
  import { getClaudeDirectory, getDevFlowDirectory } from '../../targets/claude-code/claude-paths.js';
12
- import { applyCurationToggle, describeCurationConverge, installedScriptsSkew, readRunningVersion, } from '../../targets/claude-code/curation-install.js';
12
+ import { openBackupSessionOrFail, runWithBackupReport } from './backup-session.js';
13
+ import { applyVariantToggle, describeCurationConverge, describePostingLeft, installedScriptsSkew, readRunningVersion, } from '../../targets/claude-code/curation-install.js';
13
14
  import { getLedgerRoot } from '../../core/ledger-root.js';
14
15
  import { AREA_PAUSE_NOTICE } from '../../core/area-pause-notice.js';
15
16
  import { probeTrackedArea } from '../../core/tracked-areas.js';
@@ -643,27 +644,33 @@ async function handleClear() {
643
644
  /**
644
645
  * Make the installed prompts match the switch just written
645
646
  * (D-LEARNING-VARIANT-INSTALL): the curation-on or curation-off commands and
646
- * agents, and the apply-decisions skill. The same convergence `devflow init` runs,
647
- * followed by reapplying the saved agent model mapping so `devflow agents`
648
- * overrides survive.
647
+ * agents, and the apply-decisions skill. The one converge for both axes that
648
+ * `devflow init` and `devflow compliance` run, followed by reapplying the saved
649
+ * agent model mapping so `devflow agents` overrides survive.
649
650
  *
650
651
  * Warn-not-fail: the switch itself is already recorded, and either mismatch is
651
652
  * safe (an on variant is still gated at run time, an off variant loads no
652
- * decisions), so a problem here never changes the exit code.
653
+ * decisions), so a problem here never changes the exit code. A compliance-off
654
+ * machine that still has a prompt that posts is a warning here too: the fail-closed
655
+ * exit belongs to `devflow compliance`, which changes that axis.
653
656
  *
654
657
  * Answers whether the converge ran, which means the installed files are the ones
655
658
  * this CLI writes. The toggle moves the ledger folder only then: under version skew
656
659
  * (or when that could not be told) it moves nothing.
657
660
  */
658
- async function convergeInstalledVariants(enabled) {
661
+ export async function convergeInstalledVariants(enabled, backups) {
659
662
  try {
660
- const outcome = await applyCurationToggle({
663
+ // The record of the session prints when the toggle returns or throws, ahead of the warning
664
+ // below, so a file moved aside before a failure is still named.
665
+ const outcome = await runWithBackupReport(backups, async () => applyVariantToggle({
661
666
  claudeDir: getClaudeDirectory(),
662
667
  devflowDir: getDevFlowDirectory(),
663
- curation: enabled,
668
+ axis: 'curation',
669
+ enabled,
664
670
  runningVersion: await readRunningVersion(),
665
671
  warn: (msg) => p.log.warn(msg),
666
- });
672
+ backups,
673
+ }));
667
674
  if (outcome.kind === 'skipped') {
668
675
  p.log.warn(outcome.message);
669
676
  return 'not-converged';
@@ -671,6 +678,8 @@ async function convergeInstalledVariants(enabled) {
671
678
  const changed = describeCurationConverge(outcome.result, enabled);
672
679
  if (changed !== null)
673
680
  p.log.info(color.dim(`Installed prompts: ${changed}`));
681
+ for (const line of describePostingLeft(outcome.result.posting))
682
+ p.log.warn(line);
674
683
  return 'converged';
675
684
  }
676
685
  catch (err) {
@@ -716,13 +725,17 @@ async function tryMoveForToggle(ledgerRoot) {
716
725
  * tries the shared move with no wait. Only `--disable` drains, skew included.
717
726
  */
718
727
  async function handleToggle(enabled) {
728
+ // The session comes before the first change. Without a shipped set the toggle changes nothing.
729
+ const backups = await openBackupSessionOrFail({ claudeDir: getClaudeDirectory(), devflowDir: getDevFlowDirectory() });
730
+ if (backups === null)
731
+ return;
719
732
  const recorded = await writeMachineFeature(getDevFlowDirectory(), 'curation', enabled);
720
733
  if (!recorded.ok) {
721
734
  p.log.error(`Devflow is not installed on this machine — run ${color.cyan('devflow init')} first`);
722
735
  process.exitCode = 1;
723
736
  return;
724
737
  }
725
- const converged = await convergeInstalledVariants(enabled);
738
+ const converged = await convergeInstalledVariants(enabled, backups);
726
739
  const ledgerRoot = await getLedgerRoot();
727
740
  const shownSkip = converged === 'converged' && ledgerRoot ? await tryMoveForToggle(ledgerRoot) : null;
728
741
  if (enabled) {
@@ -31,23 +31,18 @@ import { DEFAULT_GATE_LEVEL, GATE_LEVELS, MALFORMED_GATE_LEVEL_READS_AS, parseGa
31
31
  /** Message shown on the gate level select prompt. */
32
32
  export const GATE_LEVEL_SELECT_MESSAGE = 'Gate level for this machine';
33
33
  /**
34
- * The one-line effect of each level, as the select hint and the catalogue show it.
35
- * Each hint says what its level does. GATE_LEVEL_EFFECT in gates.ts says which commands
36
- * read the level. Change a hint when what a level does changes. A command that reads the
37
- * level and has no rule that changes a phase leaves the hints true, so it changes
38
- * GATE_LEVEL_EFFECT alone. One test pins the exact text of both tables, so a change to
39
- * either fails it until you check the other table and update the pin; further rows check
40
- * the clauses the two tables share (the lint pack level, the levels that run fewer review
41
- * focuses and the levels that skip Polish). The clauses that lean skips Harden on a
42
- * docs-only diff, that standard and lean run one Evaluate pass and that standard and lean
43
- * apply severity tiers in /resolve have no facet row in `tests/gate-level-prompts.test.ts`:
44
- * the docs guard checks each at every doc site, against its row in the /implement or
45
- * /resolve level reference.
34
+ * One short line for each level, as the select hint and the catalogue show it.
35
+ * clack never wraps a note or a hint, so a hint stays within 60 characters and holds no line
36
+ * break (a guard in `tests/gate-level-prompts.test.ts` holds both). The full effects of each
37
+ * level live in GATE_LEVEL_EFFECT in gates.ts, in `devflow gates` and in the docs.
38
+ * Change a hint when the level it names changes how much it checks; then check
39
+ * GATE_LEVEL_EFFECT and update the pin. The comment on GATE_LEVEL_EFFECT holds the full
40
+ * update rule, the pin and the list of clauses that the two tables share.
46
41
  */
47
42
  export const GATE_LEVEL_HINTS = Object.freeze({
48
- full: 'the most checking: every review focus, the full planning shape and Polish',
49
- standard: 'fewer review focuses and planning agents, no Polish in /implement and /resolve, severity tiers in /resolve, and one Evaluate pass in /implement (the default)',
50
- lean: 'the least checking, and no Polish in /implement and /resolve; /resolve also applies severity tiers; /implement also runs one Evaluate pass, has Validate run the project lint pack, and skips Harden on a docs-only diff',
43
+ full: 'the most checking: every review focus, and Polish',
44
+ standard: 'fewer review focuses, no Polish; the default',
45
+ lean: 'the least checking: no Polish, Validate runs the lint pack',
51
46
  });
52
47
  /** Build clack select options for the gate level list, most to least checking. */
53
48
  export function gateLevelChoices() {
@@ -55,12 +50,24 @@ export function gateLevelChoices() {
55
50
  }
56
51
  /**
57
52
  * Format a padded level catalogue suitable for a clack note body.
58
- * Produces: ` standard fewer review focuses and planning agents, no Polish in /implement and /resolve, severity tiers in /resolve, and one Evaluate pass in /implement (the default)`
53
+ * Produces: ` standard fewer review focuses, no Polish; the default`
59
54
  */
60
55
  export function formatGateLevelCatalogue() {
61
56
  const width = Math.max(...GATE_LEVELS.map(level => level.length));
62
57
  return 'Levels:\n' + GATE_LEVELS.map(level => ` ${level.padEnd(width)} ${GATE_LEVEL_HINTS[level]}`).join('\n');
63
58
  }
59
+ // ── Where the stored level was found ───────────────────────────────────────────
60
+ /**
61
+ * Where init found the stored machine gate level:
62
+ *
63
+ * manifest in the manifest init reads (or nowhere, on a first install or after
64
+ * --reset): the seed holds it
65
+ * rejected in a manifest that readManifest() refuses, read raw: init keeps the
66
+ * level as stored while it replaces that manifest
67
+ * unreadable nowhere: the manifest cannot be read at all, or stores a level that cannot be
68
+ * written back as JSON, so what it stored is unknown
69
+ */
70
+ export const STORED_GATE_LEVEL_ORIGINS = Object.freeze(['manifest', 'rejected', 'unreadable']);
64
71
  /** `features.gateLevel` of a raw manifest, exactly as stored; `undefined` for an absent key. */
65
72
  function rawGateLevelOf(raw) {
66
73
  const features = raw.features;
@@ -228,17 +235,24 @@ export function describeGateLevel(stored) {
228
235
  export function gateLevelPromptSeed(stored, origin = 'manifest') {
229
236
  return appliedLevel(classifyGateLevel(stored, origin));
230
237
  }
231
- /** The "Current setting" lines of the prompt note, from the stored value and where it was found. */
238
+ /** The "Current setting" lines of the prompt note, from the stored value and where it was found. Each line fits the 72 columns the note allows. */
232
239
  function currentSettingLines(stored, origin) {
233
240
  switch (origin) {
234
241
  case 'manifest':
235
242
  return `Current setting: ${describeGateLevel(stored)}`;
236
243
  case 'rejected':
237
- return `Current setting: ${describeGateLevel(stored)}\n` +
238
- 'It comes from a manifest that devflow rejects. Init replaces that manifest and keeps this level unless you choose another.';
244
+ return [
245
+ `Current setting: ${describeGateLevel(stored)}`,
246
+ 'It comes from a manifest that devflow rejects.',
247
+ 'Init replaces that manifest and keeps this level',
248
+ 'unless you choose another.',
249
+ ].join('\n');
239
250
  case 'unreadable':
240
- return 'Current setting: unknown\n' +
241
- `The stored gate level cannot be read, so ${DEFAULT_GATE_LEVEL} applies until you choose a level.`;
251
+ return [
252
+ 'Current setting: unknown',
253
+ `The stored gate level cannot be read, so ${DEFAULT_GATE_LEVEL} applies`,
254
+ 'until you choose a level.',
255
+ ].join('\n');
242
256
  default: {
243
257
  const exhaustive = origin;
244
258
  return exhaustive;
@@ -320,10 +334,10 @@ export function buildClackGateLevelPrompts() {
320
334
  export async function runGateLevelStep(opts) {
321
335
  const { stored, storedOrigin = 'manifest', prompts } = opts;
322
336
  prompts.note(`${currentSettingLines(stored, storedOrigin)}\n\n` +
323
- 'Sets how much checking /code-review, /plan and /implement apply on this\n' +
324
- 'machine. A personal "gateLevel" in a repository\'s .devflow/config.json\n' +
325
- 'overrides it in that repository. Change it later with\n' +
326
- 'devflow gates --set <level>.\n\n' +
337
+ 'Sets how much checking /code-review, /plan, /implement and /resolve\n' +
338
+ 'apply on this machine. A personal "gateLevel" in a repository\'s\n' +
339
+ '.devflow/config.json overrides it in that repository. Change it later\n' +
340
+ 'with devflow gates --set <level>.\n\n' +
327
341
  formatGateLevelCatalogue(), 'Gate level');
328
342
  const outcome = await prompts.selectLevel({
329
343
  message: GATE_LEVEL_SELECT_MESSAGE,
@@ -39,16 +39,14 @@ export const GATE_LEVEL_MEANINGS = Object.freeze({
39
39
  /**
40
40
  * Which commands read the level today; each later command that reads it updates this text.
41
41
  *
42
- * The init prompt says what each level does in GATE_LEVEL_HINTS (gate-level-prompts.ts).
43
- * Change the hints too when what a level does changes. A command that reads the level and
44
- * has no rule that changes a phase leaves the hints true, so it changes this text alone.
42
+ * The init prompt gives each level one short line in GATE_LEVEL_HINTS (gate-level-prompts.ts);
43
+ * this text and the docs hold the full effects. Change the hints too when a level changes
44
+ * how much it checks. A command that reads the level and has no rule that changes a phase
45
+ * leaves the hints true, so it changes this text alone.
45
46
  * One test pins the exact text of both tables, so a change to either fails it until you
46
47
  * check the other table and update the pin; further rows check the clauses the two tables
47
48
  * share (the lint pack level, the levels that run fewer review focuses and the levels that
48
- * skip Polish). The clauses that lean skips Harden on a docs-only diff, that standard
49
- * and lean run one Evaluate pass and that standard and lean apply severity tiers in /resolve
50
- * have no facet row in `tests/gate-level-prompts.test.ts`: the docs guard checks each at
51
- * every doc site, against its row in the /implement or /resolve level reference.
49
+ * skip Polish).
52
50
  */
53
51
  export const GATE_LEVEL_EFFECT = '/code-review and /plan read the level: standard and lean run fewer review focuses and fewer planning agents. At lean, /implement has Validate run the project\'s lint pack. /implement and /resolve also read the level and name it in their report; at standard and lean they skip Polish, and full keeps it. At lean, /implement skips Harden on a docs-only diff. At standard and lean, /implement runs one Evaluate pass and at most one Fix round. At standard and lean, /resolve applies severity tiers: it fixes or escalates every security finding, fixes CRITICAL, HIGH and severe MEDIUM findings in the branch\'s own code, fixes prose-only MEDIUM findings with text edits alone, and sends lower findings and pre-existing non-security findings to one backlog issue.';
54
52
  const MACHINE_MANIFEST_NAME = 'manifest.json';