devflow-kit 3.8.0 → 3.9.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 (94) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/README.md +5 -5
  3. package/dist/agents/code.md +4 -4
  4. package/dist/agents/fix.md +1 -1
  5. package/dist/agents/git.md +2 -2
  6. package/dist/agents/skim.md +1 -1
  7. package/dist/agents/triage.md +7 -0
  8. package/dist/cli/commands/curation.js +67 -16
  9. package/dist/cli/commands/gate-level-prompts.js +8 -6
  10. package/dist/cli/commands/gates.js +5 -4
  11. package/dist/cli/commands/init.js +8 -7
  12. package/dist/cli/commands/memory.js +59 -12
  13. package/dist/cli/commands/uninstall.js +77 -9
  14. package/dist/commands/bug-analysis.md +2 -2
  15. package/dist/commands/code-review.md +11 -11
  16. package/dist/commands/debug.md +2 -2
  17. package/dist/commands/dynamic-build.md +2 -2
  18. package/dist/commands/dynamic-plan.md +2 -2
  19. package/dist/commands/dynamic-profile.md +2 -2
  20. package/dist/commands/dynamic-tickets.md +2 -2
  21. package/dist/commands/explore.md +2 -2
  22. package/dist/commands/implement.md +2 -2
  23. package/dist/commands/plan.md +2 -2
  24. package/dist/commands/release.md +2 -2
  25. package/dist/commands/research.md +2 -2
  26. package/dist/commands/resolve.md +16 -16
  27. package/dist/commands/self-review.md +2 -2
  28. package/dist/core/area-pause-notice.js +44 -0
  29. package/dist/core/curation-move-skip.js +4 -5
  30. package/dist/core/curation-queue-cleanup.js +6 -5
  31. package/dist/core/curation-store.js +1 -0
  32. package/dist/core/evidence-policy.js +10 -8
  33. package/dist/core/plugins.js +4 -4
  34. package/dist/core/tracked-areas.js +138 -14
  35. package/dist/curation-off/agents/code.md +4 -4
  36. package/dist/curation-off/agents/fix.md +1 -1
  37. package/dist/curation-off/agents/triage.md +7 -0
  38. package/dist/curation-off/commands/code-review.md +11 -11
  39. package/dist/curation-off/commands/debug.md +2 -2
  40. package/dist/curation-off/commands/dynamic-build.md +2 -2
  41. package/dist/curation-off/commands/explore.md +2 -2
  42. package/dist/curation-off/commands/implement.md +2 -2
  43. package/dist/curation-off/commands/plan.md +2 -2
  44. package/dist/curation-off/commands/resolve.md +16 -16
  45. package/dist/curation-off/commands/self-review.md +2 -2
  46. package/dist/hud/components/curation-counts.js +27 -0
  47. package/dist/skills/git/references/pr/post-resolution-summary.md +12 -11
  48. package/dist/skills/git/references/pr/post-review-summary.md +12 -11
  49. package/dist/skills/git/references/pr/update-pr-evidence.md +2 -2
  50. package/dist/skills/git/references/publication-gate.md +6 -7
  51. package/dist/skills/git/references/tracker/github/manage-debt.md +6 -2
  52. package/dist/skills/git/references/tracker/jira/manage-debt.md +6 -2
  53. package/dist/skills/git/references/tracker/linear/manage-debt.md +6 -2
  54. package/package.json +1 -1
  55. package/src/assets/agents/code.mds +4 -4
  56. package/src/assets/agents/evaluate.md +6 -12
  57. package/src/assets/agents/fix.mds +1 -1
  58. package/src/assets/agents/git.mds +2 -2
  59. package/src/assets/agents/skim.mds +1 -1
  60. package/src/assets/agents/triage.mds +7 -0
  61. package/src/assets/agents/validate.md +20 -27
  62. package/src/assets/commands/_partials/_decisions.mds +10 -8
  63. package/src/assets/commands/_partials/_settings.mds +2 -2
  64. package/src/assets/commands/code-review.mds +9 -9
  65. package/src/assets/commands/resolve.mds +14 -14
  66. package/src/assets/mds/git/_ops.mds +6 -1
  67. package/src/assets/mds/git/_ops_pr.mds +20 -10
  68. package/src/assets/mds/git/_ops_steps.mds +10 -0
  69. package/src/assets/mds/git/_pr.mds +14 -14
  70. package/src/assets/mds/git/_references.mds +6 -7
  71. package/src/assets/mds/tracker/_github.mds +1 -4
  72. package/src/assets/mds/tracker/_jira.mds +1 -4
  73. package/src/assets/mds/tracker/_linear.mds +1 -4
  74. package/src/assets/scripts/ci-wait.cjs +389 -92
  75. package/src/assets/scripts/hooks/background-memory-update +79 -1
  76. package/src/assets/scripts/hooks/json-helper.cjs +44 -2
  77. package/src/assets/scripts/hooks/lib/curation-store.cjs +82 -20
  78. package/src/assets/scripts/hooks/lib/render-decisions.cjs +9 -1
  79. package/src/assets/scripts/hooks/lib/tracked-areas.cjs +226 -33
  80. package/src/assets/scripts/hooks/memory-permission +10 -2
  81. package/src/assets/scripts/hooks/memory-worker +11 -0
  82. package/src/assets/scripts/hooks/pre-compact-memory +11 -0
  83. package/src/assets/scripts/hooks/queue-append +218 -1
  84. package/src/assets/scripts/hooks/session-start-context +89 -37
  85. package/src/assets/scripts/hooks/session-start-memory +39 -0
  86. package/src/assets/scripts/pr-evidence.cjs +5 -2
  87. package/src/assets/scripts/resolve-settings.cjs +244 -30
  88. package/src/assets/scripts/verify-evidence.cjs +307 -75
  89. package/src/assets/skills/agent-references/SKILL.md +7 -1
  90. package/src/assets/skills/agent-references/references/commands/implement-levels.md +6 -4
  91. package/src/assets/skills/agent-references/references/commands/resolve-levels.md +3 -2
  92. package/src/assets/skills/agent-references/references/synthesize/review.md +4 -4
  93. package/src/assets/skills/agent-references/references/triage/severity-tiers.md +43 -0
  94. package/src/assets/skills/agent-references/references/validate/passed-alone.md +14 -0
@@ -8,8 +8,11 @@ import { writeSettingsFileAtomic } from '../../core/fs-atomic.js';
8
8
  import { discoverProjectGitRoots } from '../../targets/claude-code/post-install.js';
9
9
  import { getGitRoot } from '../../core/git.js';
10
10
  import { firstSymbolicLink } from '../../core/linked-path.js';
11
+ import { pooled } from '../../core/pool.js';
11
12
  import { getMemoryDir, getPendingTurnsPath, getPendingTurnsProcessingPath, } from '../../core/project-paths.js';
12
13
  import { drainQueueFiles, formatRefusedDrain } from '../../core/queue-drain.js';
14
+ import { AREA_PAUSE_NOTICE } from '../../core/area-pause-notice.js';
15
+ import { probeTrackedArea } from '../../core/tracked-areas.js';
13
16
  import { HOOKS_DIR_SUFFIX, devflowHookOwner, endsWithAny, ensureHook, hasHook, removeHooks, runHookCommand, runHookSuffix, } from '../../targets/claude-code/hooks.js';
14
17
  import { readMachineFeature, writeMachineFeature } from '../../core/feature-switch.js';
15
18
  import { loadSettingsModule, narrowedSwitchLabel, personalConfigTrackedWarning } from '../../core/evidence-policy.js';
@@ -159,21 +162,42 @@ export function convergeMemoryHooks(settingsJson, enabled, devflowDir) {
159
162
  const cleaned = removeMemoryHooks(settingsJson);
160
163
  return enabled ? addMemoryHooks(cleaned, devflowDir) : cleaned;
161
164
  }
165
+ /**
166
+ * Why the memory area at `projectRoot` is paused, or null when git tracks nothing in the
167
+ * memory folder there. The area is paused when git tracks files in the memory folder, or
168
+ * a `.git` entry stands and git did not answer (`unknown`); a root with no `.git` entry
169
+ * makes no git call. The root is the checkout's toplevel, where the memory hooks work.
170
+ */
171
+ async function memoryPauseReason(projectRoot) {
172
+ const { answer } = await probeTrackedArea(projectRoot, 'memory');
173
+ return answer === 'untracked' ? null : answer;
174
+ }
162
175
  /**
163
176
  * Drain a project's pending memory queue (and a claimed batch) so stale turns
164
177
  * are not processed when memory is next switched on. Shared by `devflow init
165
- * --no-memory` and `devflow memory --disable`. Refused, deleting nothing, when
166
- * `.devflow` or `.devflow/memory` under `projectRoot` is a symbolic link
167
- * (D-CLI-NO-SYMLINK). ENOENT-tolerant; any other error propagates to the command
168
- * boundary, like drainCurationQueue.
178
+ * --no-memory` and `devflow memory --disable`. Deletes nothing, and answers
179
+ * `skipped`, when git tracks files in the memory folder or a `.git` entry stands and
180
+ * git does not answer: a folder that git tracks is the repository's content. Refused,
181
+ * deleting nothing, when `.devflow` or `.devflow/memory` under `projectRoot` is a
182
+ * symbolic link (D-CLI-NO-SYMLINK). ENOENT-tolerant; any other error propagates to the
183
+ * command boundary, like drainCurationQueue.
169
184
  */
170
185
  export async function drainMemoryQueue(projectRoot) {
186
+ const skipped = await memoryPauseReason(projectRoot);
187
+ if (skipped !== null)
188
+ return { drained: false, skipped };
171
189
  const memoryDir = getMemoryDir(projectRoot);
172
190
  return drainQueueFiles([path.dirname(memoryDir), memoryDir], [
173
191
  getPendingTurnsPath(projectRoot),
174
192
  getPendingTurnsProcessingPath(projectRoot),
175
193
  ]);
176
194
  }
195
+ /** The line a caller prints for a memory drain that deleted nothing: the fixed notice of a paused area, or the refusal at a link. */
196
+ export function formatMemoryNotDrained(drain) {
197
+ if (!('skipped' in drain))
198
+ return formatRefusedDrain('memory', drain.linkedFolder);
199
+ return `The memory queue was not drained. ${AREA_PAUSE_NOTICE.memory[drain.skipped]}`;
200
+ }
177
201
  /**
178
202
  * Returns true if the given project root contains a `.devflow/memory/` directory.
179
203
  * Treats unexpected errors (e.g. EACCES) as absent to avoid false positives.
@@ -200,15 +224,23 @@ export async function filterProjectsWithMemory(gitRoots) {
200
224
  const checks = await Promise.all(gitRoots.map(async (root) => ({ root, has: await hasMemoryDir(root) })));
201
225
  return checks.filter((c) => c.has).map((c) => c.root);
202
226
  }
227
+ /** At most this many projects are checked and cleaned at once, so `memory --clear` starts few git processes. */
228
+ export const CLEAN_QUEUE_POOL_LIMIT = 8;
203
229
  /**
204
230
  * Clean up memory queue files from the given project paths.
205
231
  * Skips projects where the background updater lock is held to avoid data loss,
206
- * and refuses, deleting nothing there, a project whose `.devflow` or
207
- * `.devflow/memory` is a symbolic link (D-CLI-NO-SYMLINK); `refused` names each
208
- * such link. Returns the count of projects from which at least one file was removed.
232
+ * leaves a project whose memory area is paused alone (git tracks files in the memory
233
+ * folder, or a `.git` entry stands and git did not answer; `paused` names each such
234
+ * project with the reason), and refuses, deleting nothing there, a project whose
235
+ * `.devflow` or `.devflow/memory` is a symbolic link (D-CLI-NO-SYMLINK); `refused` names
236
+ * each such link. At most {@link CLEAN_QUEUE_POOL_LIMIT} projects are handled at once.
237
+ * Returns the count of projects from which at least one file was removed.
209
238
  */
210
239
  export async function cleanQueueFiles(projectPaths) {
211
- const results = await Promise.all(projectPaths.map(async (project) => {
240
+ const settled = await pooled(projectPaths, CLEAN_QUEUE_POOL_LIMIT, async (project) => {
241
+ const reason = await memoryPauseReason(project);
242
+ if (reason !== null)
243
+ return { paused: reason, project };
212
244
  const memDir = getMemoryDir(project);
213
245
  let linkedFolder;
214
246
  try {
@@ -234,10 +266,16 @@ export async function cleanQueueFiles(projectPaths) {
234
266
  fs.unlink(getPendingTurnsProcessingPath(project)).then(() => true).catch(() => false),
235
267
  ]);
236
268
  return (q || pr) ? { cleaned: project } : null;
237
- }));
269
+ });
270
+ const results = settled.map((entry) => {
271
+ if (entry.status === 'rejected')
272
+ throw entry.reason;
273
+ return entry.value;
274
+ });
238
275
  const cleanedProjects = results.flatMap((r) => (r !== null && 'cleaned' in r ? [r.cleaned] : []));
239
276
  const refused = results.flatMap((r) => (r !== null && 'refused' in r ? [r.refused] : []));
240
- return { cleaned: cleanedProjects.length, projects: cleanedProjects, refused };
277
+ const paused = results.flatMap((r) => (r !== null && 'paused' in r ? [{ project: r.project, reason: r.paused }] : []));
278
+ return { cleaned: cleanedProjects.length, projects: cleanedProjects, refused, paused };
241
279
  }
242
280
  export const memoryCommand = new Command('memory')
243
281
  .description('Enable, disable, or clean up working memory (session context preservation)')
@@ -297,10 +335,13 @@ export const memoryCommand = new Command('memory')
297
335
  }
298
336
  targets = scope === 'local' && currentProject ? [currentProject] : allProjects;
299
337
  }
300
- const { cleaned, projects: cleanedProjects, refused } = await cleanQueueFiles(targets);
338
+ const { cleaned, projects: cleanedProjects, refused, paused } = await cleanQueueFiles(targets);
301
339
  for (const project of cleanedProjects) {
302
340
  p.log.info(color.dim(`Cleaned: ${project}`));
303
341
  }
342
+ for (const { project, reason } of paused) {
343
+ p.log.warn(`The memory queue of ${project} was not cleaned. ${AREA_PAUSE_NOTICE.memory[reason]}`);
344
+ }
304
345
  for (const linkedFolder of refused) {
305
346
  p.log.warn(formatRefusedDrain('memory', linkedFolder));
306
347
  }
@@ -340,6 +381,12 @@ export const memoryCommand = new Command('memory')
340
381
  const narrowed = enabled ? narrowedSwitchLabel(settingsModule, { dir: process.cwd() }, 'memory') : null;
341
382
  if (narrowed !== null)
342
383
  p.log.info(`Effective here: ${color.yellow(narrowed)}`);
384
+ // A paused memory area (git tracks files in the memory folder, or did not answer):
385
+ // the fixed notice, from the checkout's toplevel, where the memory hooks work. The
386
+ // current directory stands in outside a git work tree, as it does for the settings line.
387
+ const paused = await memoryPauseReason((await getGitRoot()) ?? process.cwd());
388
+ if (paused !== null)
389
+ p.log.info(AREA_PAUSE_NOTICE.memory[paused]);
343
390
  const trackedWarning = personalConfigTrackedWarning(settingsModule, { dir: process.cwd() });
344
391
  if (trackedWarning !== null)
345
392
  p.log.warn(trackedWarning);
@@ -380,7 +427,7 @@ export const memoryCommand = new Command('memory')
380
427
  if (gitRoot) {
381
428
  const drain = await drainMemoryQueue(gitRoot);
382
429
  if (!drain.drained)
383
- p.log.warn(formatRefusedDrain('memory', drain.linkedFolder));
430
+ p.log.warn(formatMemoryNotDrained(drain));
384
431
  }
385
432
  p.log.success('Working memory disabled in every project');
386
433
  });
@@ -32,7 +32,8 @@ import { stripDevflowTeammateModeFromJson } from '../../core/teammate-mode-clean
32
32
  import { getPackageRoot, isContainedIn } from '../../core/paths.js';
33
33
  import { firstSymbolicLink } from '../../core/linked-path.js';
34
34
  import { MANIFEST_LOCK_NAME, MANIFEST_LOCK_STALE_MARK } from '../../core/manifest-lock.js';
35
- import { CURATION_TUNING_FILE_NAME, LEGACY_NAMES, MOVE_JOURNAL_SUFFIX, MOVE_LOCK_PREFIX, MOVE_LOCK_SUFFIX } from '../../core/project-paths.js';
35
+ import { CURATION_DIR_NAME, CURATION_TUNING_FILE_NAME, LEGACY_NAMES, MOVE_JOURNAL_SUFFIX, MOVE_LOCK_PREFIX, MOVE_LOCK_SUFFIX, getMemoryDir } from '../../core/project-paths.js';
36
+ import { probeTrackedArea } from '../../core/tracked-areas.js';
36
37
  import { restampInstalledCommands } from '../../targets/claude-code/language-stamp.js';
37
38
  import { installCommandsAndAgents } from '../../targets/claude-code/installer.js';
38
39
  /**
@@ -285,20 +286,66 @@ const TRACKED_ENTRIES = DEVFLOW_TRACKED_PATHS.map((tracked) => ({
285
286
  name: tracked.replace(/\/$/, ''),
286
287
  isDir: tracked.endsWith('/'),
287
288
  }));
289
+ /** The memory folder's name under `.devflow/`, by the name the path helper gives it. */
290
+ const MEMORY_FOLDER_NAME = path.basename(getMemoryDir(path.sep));
291
+ /**
292
+ * The data area an entry under `.devflow/` belongs to, or null. The curation area holds the
293
+ * curation folder, the old ledger folder and the backup folders; the memory area holds the
294
+ * memory folder. Decided by name alone, whatever the kind of entry, and without regard to
295
+ * ASCII letter case, as the tracked-areas check matches them.
296
+ */
297
+ function projectDataAreaOf(name) {
298
+ const lower = name.toLowerCase();
299
+ if (lower === CURATION_DIR_NAME || lower === LEGACY_NAMES.folder || lower.startsWith(LEGACY_NAMES.backupPrefix))
300
+ return 'curation';
301
+ if (lower === MEMORY_FOLDER_NAME)
302
+ return 'memory';
303
+ return null;
304
+ }
288
305
  /**
289
306
  * Split the entries under a repository's `.devflow/` into what a confirmed cleanup
290
307
  * removes and what it keeps (D-UNINSTALL-CARVE-OUT). An entry is kept only when both
291
308
  * its name and its kind match a tracked path, so a stray FILE named `features` is
292
- * not mistaken for the tracked directory. PURE — both lists sorted by name.
309
+ * not mistaken for the tracked directory. An entry of a paused data area (`pausedAreas`:
310
+ * git tracks files in the area's folders, or did not answer) is kept too, in
311
+ * `keepPaused`. PURE — all three lists sorted by name.
293
312
  */
294
- export function partitionProjectData(entries) {
313
+ export function partitionProjectData(entries, pausedAreas = new Set()) {
295
314
  const isTracked = (entry) => TRACKED_ENTRIES.some((t) => t.name === entry.name && t.isDir === entry.isDir);
315
+ const isPaused = (entry) => {
316
+ const area = projectDataAreaOf(entry.name);
317
+ return area !== null && pausedAreas.has(area);
318
+ };
296
319
  const byName = (a, b) => a.name.localeCompare(b.name);
297
320
  return {
298
- remove: entries.filter((e) => !isTracked(e)).sort(byName),
321
+ remove: entries.filter((e) => !isTracked(e) && !isPaused(e)).sort(byName),
299
322
  keep: entries.filter(isTracked).sort(byName),
323
+ keepPaused: entries.filter((e) => !isTracked(e) && isPaused(e)).sort(byName),
300
324
  };
301
325
  }
326
+ /** The check's own answer, from the package's own copy of it. */
327
+ const probeProjectDataArea = async (root, area) => (await probeTrackedArea(root, area)).answer;
328
+ /**
329
+ * The data areas that hold an entry in `entries` and are paused at `gitRoot`: git tracks
330
+ * files in the area's folders, or a `.git` entry stands and git did not answer. An area
331
+ * with no entry is not asked, and a root with no `.git` entry makes no git call. A probe
332
+ * that fails to answer counts as `unknown`, so the area is kept.
333
+ */
334
+ async function pausedProjectDataAreas(gitRoot, entries, probe) {
335
+ const present = new Set(entries.flatMap((entry) => {
336
+ const area = projectDataAreaOf(entry.name);
337
+ return area === null ? [] : [area];
338
+ }));
339
+ const answers = await Promise.all([...present].map(async (area) => {
340
+ try {
341
+ return [area, await probe(gitRoot, area)];
342
+ }
343
+ catch {
344
+ return [area, 'unknown'];
345
+ }
346
+ }));
347
+ return new Set(answers.flatMap(([area, answer]) => (answer === 'untracked' ? [] : [area])));
348
+ }
302
349
  /**
303
350
  * Resolve the project-data step's target: `<gitRoot>/.devflow`, partitioned.
304
351
  *
@@ -312,6 +359,12 @@ export function partitionProjectData(entries) {
312
359
  * A `.devflow` that is a symbolic link to anywhere else is skipped too, and never
313
360
  * followed: its target is a directory devflow cannot prove it wrote, so a
314
361
  * confirmed cleanup must not empty it — nor unlink a link the user made.
362
+ *
363
+ * A folder that git tracks is the repository's content, not devflow's data. The
364
+ * curation area (the curation folder, the old ledger folder and the backup folders)
365
+ * and the memory area (the memory folder) are each asked of the tracked-areas check at
366
+ * the git root, when an entry of the area stands. An area that git tracks files in, or
367
+ * that git did not answer for, moves its entries from the removal list to `keepPaused`.
315
368
  */
316
369
  export async function resolveProjectDataPlan(opts) {
317
370
  if (opts.gitRoot === null)
@@ -335,8 +388,10 @@ export async function resolveProjectDataPlan(opts) {
335
388
  catch {
336
389
  return { kind: 'skip', reason: 'absent' };
337
390
  }
338
- const { remove, keep } = partitionProjectData(dirents.map((d) => ({ name: d.name, isDir: d.isDirectory() })));
339
- return { kind: 'plan', plan: { dir, remove, keep } };
391
+ const entries = dirents.map((d) => ({ name: d.name, isDir: d.isDirectory() }));
392
+ const paused = await pausedProjectDataAreas(opts.gitRoot, entries, opts.probe ?? probeProjectDataArea);
393
+ const { remove, keep, keepPaused } = partitionProjectData(entries, paused);
394
+ return { kind: 'plan', plan: { dir, remove, keep, keepPaused } };
340
395
  }
341
396
  /**
342
397
  * The line that tells the user why the project-data step was skipped, or null
@@ -366,15 +421,21 @@ function entryLabel(entry) {
366
421
  function entryList(entries) {
367
422
  return entries.length === 0 ? '(none)' : entries.map(entryLabel).join(', ');
368
423
  }
424
+ /** Why the entries of a data area stay, said beside a list of them. */
425
+ const KEEP_PAUSED_REASON = 'git tracks them, or git did not answer';
426
+ /** Why one entry of a data area stays, said beside it. */
427
+ const KEEP_PAUSED_REASON_ONE = 'git tracks it, or git did not answer';
369
428
  /**
370
429
  * The lines shown before the project-data confirm: what is removed and what is
371
- * kept. PURE.
430
+ * kept, and, only where there are such entries, what is kept because git tracks them
431
+ * or did not answer. PURE.
372
432
  */
373
433
  export function formatProjectDataPlan(plan) {
374
434
  return [
375
435
  `Project data in ${plan.dir}/:`,
376
436
  ` Remove: ${entryList(plan.remove)}`,
377
437
  ` Keep (shared via git): ${entryList(plan.keep)}`,
438
+ ...(plan.keepPaused.length === 0 ? [] : [` Keep (${KEEP_PAUSED_REASON}): ${entryList(plan.keepPaused)}`]),
378
439
  ];
379
440
  }
380
441
  /**
@@ -946,6 +1007,9 @@ export async function runDryRunPhase(opts) {
946
1007
  for (const entry of projectData.plan.remove) {
947
1008
  extras.push(`${path.join(projectData.plan.dir, entryLabel(entry))} (if confirmed)`);
948
1009
  }
1010
+ for (const entry of projectData.plan.keepPaused) {
1011
+ extras.push(`${path.join(projectData.plan.dir, entryLabel(entry))} (kept: ${KEEP_PAUSED_REASON_ONE})`);
1012
+ }
949
1013
  }
950
1014
  else {
951
1015
  const skipped = formatProjectDataSkip(projectData.reason, projectRoot);
@@ -1251,7 +1315,9 @@ export async function runFullPhaseForScope(opts) {
1251
1315
  async function runProjectDataStep(plan, gates) {
1252
1316
  const shown = `${plan.dir}/`;
1253
1317
  if (plan.remove.length === 0) {
1254
- p.log.info(`${shown} preserved (holds only files shared via git)`);
1318
+ p.log.info(plan.keepPaused.length === 0
1319
+ ? `${shown} preserved (holds only files shared via git)`
1320
+ : `${shown} preserved (holds only files shared via git, and ${entryList(plan.keepPaused)}, which stay: ${KEEP_PAUSED_REASON})`);
1255
1321
  return;
1256
1322
  }
1257
1323
  if (gates.keepDocs) {
@@ -1287,7 +1353,7 @@ async function runProjectDataStep(plan, gates) {
1287
1353
  failed.push(`${entryLabel(entry)} (${err.code ?? err.message})`);
1288
1354
  }
1289
1355
  }
1290
- if (plan.keep.length === 0 && failed.length === 0) {
1356
+ if (plan.keep.length === 0 && plan.keepPaused.length === 0 && failed.length === 0) {
1291
1357
  // Nothing tracked was there: drop the now-empty directory. rmdir refuses a
1292
1358
  // directory something wrote into meanwhile, which is then left in place.
1293
1359
  await fs.rmdir(plan.dir).catch(() => undefined);
@@ -1298,6 +1364,8 @@ async function runProjectDataStep(plan, gates) {
1298
1364
  p.log.warn(`Could not remove from ${shown}: ${failed.join(', ')}`);
1299
1365
  if (plan.keep.length > 0)
1300
1366
  p.log.info(`Kept: ${entryList(plan.keep)}`);
1367
+ if (plan.keepPaused.length > 0)
1368
+ p.log.info(`Kept: ${entryList(plan.keepPaused)} (${KEEP_PAUSED_REASON})`);
1301
1369
  }
1302
1370
  /**
1303
1371
  * CLEANUP PHASE: post-loop extras run only on full uninstall.
@@ -180,13 +180,13 @@ If no tool produced findings: set `STATIC_FINDINGS` to `(none)`.
180
180
 
181
181
  #### Decisions Index
182
182
 
183
- **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root when the run has one worktree:
183
+ **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root in a one-worktree run:
184
184
 
185
185
  ```bash
186
186
  node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
187
187
  ```
188
188
 
189
- Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on CURATION=on KNOWLEDGE=off GATE_LEVEL=full` instead.
189
+ Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=off CURATION=off KNOWLEDGE=off GATE_LEVEL=full` instead.
190
190
 
191
191
  The accepted line is the only source of these values.
192
192
 
@@ -35,13 +35,13 @@ Run a comprehensive code review of the current branch by spawning parallel revie
35
35
 
36
36
  **Produces:** COMPLIANCE_ACTIVE, COMPLIANCE_FRAMEWORKS, GATE_LEVEL
37
37
 
38
- **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root when the run has one worktree:
38
+ **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root in a one-worktree run:
39
39
 
40
40
  ```bash
41
41
  node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
42
42
  ```
43
43
 
44
- Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on CURATION=on KNOWLEDGE=off GATE_LEVEL=full` instead.
44
+ Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=off CURATION=off KNOWLEDGE=off GATE_LEVEL=full` instead.
45
45
 
46
46
  The accepted line is the only source of these values.
47
47
 
@@ -136,11 +136,11 @@ For each worktree:
136
136
  **Produces:** PRIOR_RESOLUTIONS, CYCLE_NUMBER
137
137
  **Requires:** BRANCH_INFO
138
138
 
139
- For each worktree, perform a single pass over timestamped directories:
140
- 1. List timestamped directories in `{worktree}/.devflow/docs/reviews/{branch-slug}/` sorted descending: `ls -1d {worktree}/.devflow/docs/reviews/{branch-slug}/20* 2>/dev/null | sort -r`
141
- 2. Iterate once: accumulate CYCLE_NUMBER count for each directory containing `resolution-summary.md`; capture the first (most-recent) such directory as PRIOR_DIR.
139
+ For each worktree:
140
+ 1. List the timestamped directories: `ls -1d {worktree}/.devflow/docs/reviews/{branch-slug}/20* 2>/dev/null | sort -r`
141
+ 2. Iterate once: count each directory containing `resolution-summary.md` as CYCLE_NUMBER; capture the first (newest) as PRIOR_DIR.
142
142
  3. If CYCLE_NUMBER = 0: set PRIOR_RESOLUTIONS=(none), CYCLE_NUMBER=1, proceed.
143
- 4. Otherwise: set CYCLE_NUMBER = count + 1. Read `{PRIOR_DIR}/resolution-summary.md` as PRIOR_RESOLUTIONS.
143
+ 4. Otherwise: set CYCLE_NUMBER = count + 1. Read `{PRIOR_DIR}/resolution-summary.md` as PRIOR_RESOLUTIONS minus each section from a heading line (fenced or not) with the word `Private` (any case) not followed by a letter or digit, to the next unfenced heading with as many `#` as its start or fewer, else at end of file. Close any fence it leaves open. When unsure, cut more; if the cut fails, PRIOR_RESOLUTIONS=(none).
144
144
  5. If `--full`: still load PRIOR_RESOLUTIONS.
145
145
 
146
146
  #### Step 0e-ii: Convergence Assessment
@@ -158,7 +158,7 @@ MAX_REVIEW_CYCLES = 10
158
158
  - `deferred_count` = the Deferred row value (= FIX_SEPARATE + TECH_DEBT); By Design and Escalated are excluded from the fp_ratio denominator
159
159
  - fp_ratio = fp_count / (fp_count + fixed_count + deferred_count)
160
160
  - If denominator = 0: fp_ratio = 0, skip warning
161
- - If parsing fails: fp_ratio = 0, skip warning; note in output: "Warning: Could not parse Statistics table from prior resolution. FP ratio unavailable — convergence tracking degraded."
161
+ - If parsing fails: fp_ratio = 0, skip warning; note in output: "Warning: Could not parse Statistics table. Convergence tracking degraded."
162
162
  3. If fp_ratio > 0.7 AND CYCLE_NUMBER >= 3:
163
163
  Warn in output: "⚠️ Convergence: {ratio}% false positives in cycle {N-1}. Consider merging or manual inspection."
164
164
  Continue with review.
@@ -453,7 +453,7 @@ Follow devflow:apply-feature-knowledge for FEATURE_KNOWLEDGE — feature-specifi
453
453
  IMPORTANT: Write report to {worktree_path}/.devflow/docs/reviews/{branch-slug}/{timestamp}/{focus}.md using Write tool"
454
454
  ```
455
455
 
456
- In multi-worktree mode, process worktrees **sequentially**: Phases 1-4 for one worktree, then the next.
456
+ In multi-worktree mode, process worktrees **sequentially**: Phases 1-4 for each.
457
457
 
458
458
  ### Phase 3: Synthesis → Review Comment (Sequential)
459
459
 
@@ -488,7 +488,7 @@ CYCLE_NUMBER: {cycle_number}
488
488
  REVIEW_TIMESTAMP: {timestamp}
489
489
  REVIEW_PUBLICATION: {REVIEW_PUBLICATION}
490
490
  WORKTREE_PATH: {worktree_path} (omit if cwd)
491
- Posts the consolidated review-summary comment (marker-deduped by the operation on the cycle+timestamp pair)."
491
+ Posts the consolidated review-summary comment (marker-deduped on the cycle+timestamp pair)."
492
492
  ```
493
493
 
494
494
  ### Phase 4: Write Review Head Marker & Report
@@ -536,7 +536,7 @@ Per worktree, after successful completion:
536
536
  | Case | Handling |
537
537
  |------|----------|
538
538
  | `--full` in multi-worktree mode | Applies to all worktrees (global modifier). It sets the diff range, never the gate level |
539
- | Review comment already posted | Git agent matches its own marker on the `{cycle, timestamp}` pair → skip silently (D7); a re-review in the same cycle (different timestamp) posts its own comment. The marker's format belongs to the operation |
539
+ | Review comment already posted | Git agent matches its own marker on the `{cycle, timestamp}` pair → skip silently (D7) |
540
540
  | Docs-only diff | Its Step 1.1 set, narrowed by level |
541
541
  | Tests-only diff | Its Step 1.1 set, narrowed by level |
542
542
  | Lockfile-only diff | Its Step 1.1 set, narrowed by level |
@@ -545,7 +545,7 @@ Per worktree, after successful completion:
545
545
  | A file moved across classes | `--no-renames` lists both paths, so a code path makes the diff `code` |
546
546
  | Path listing above about 40 lines | Read in ranges (Step 1.1) |
547
547
  | Language files changed, plugin not installed | The focus is absent from the stamped line: not spawned |
548
- | `reviewPublication: off` | Git agent skips posting the review comment (OFF) — except that, only when `EVIDENCE_POLICY` is `required`, Step 0f passes `stub` and a counts-only STUB comment posts |
548
+ | `reviewPublication: off` | Git agent skips posting the review comment (OFF) — except that, only when `EVIDENCE_POLICY` is `required`, Step 0f passes `stub` and a counts-only STUB comment posts unless no public text remains |
549
549
 
550
550
  ## Backwards Compatibility
551
551
 
@@ -32,13 +32,13 @@ $ARGUMENTS
32
32
 
33
33
  **Produces:** DECISIONS_CONTEXT
34
34
 
35
- **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root when the run has one worktree:
35
+ **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root in a one-worktree run:
36
36
 
37
37
  ```bash
38
38
  node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
39
39
  ```
40
40
 
41
- Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on CURATION=on KNOWLEDGE=off GATE_LEVEL=full` instead.
41
+ Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=off CURATION=off KNOWLEDGE=off GATE_LEVEL=full` instead.
42
42
 
43
43
  The accepted line is the only source of these values.
44
44
 
@@ -77,13 +77,13 @@ When a ticket requires multiple sequential Code agent phases, each Code agent ap
77
77
 
78
78
  ### Settings line
79
79
 
80
- **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root when the run has one worktree:
80
+ **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root in a one-worktree run:
81
81
 
82
82
  ```bash
83
83
  node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
84
84
  ```
85
85
 
86
- Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on CURATION=on KNOWLEDGE=off GATE_LEVEL=full` instead.
86
+ Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=off CURATION=off KNOWLEDGE=off GATE_LEVEL=full` instead.
87
87
 
88
88
  The accepted line is the only source of these values.
89
89
 
@@ -77,13 +77,13 @@ When a ticket requires multiple sequential Code agent phases, each Code agent ap
77
77
 
78
78
  ### Settings line
79
79
 
80
- **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root when the run has one worktree:
80
+ **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root in a one-worktree run:
81
81
 
82
82
  ```bash
83
83
  node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
84
84
  ```
85
85
 
86
- Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on CURATION=on KNOWLEDGE=off GATE_LEVEL=full` instead.
86
+ Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=off CURATION=off KNOWLEDGE=off GATE_LEVEL=full` instead.
87
87
 
88
88
  The accepted line is the only source of these values.
89
89
 
@@ -77,13 +77,13 @@ When a ticket requires multiple sequential Code agent phases, each Code agent ap
77
77
 
78
78
  ### Settings line
79
79
 
80
- **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root when the run has one worktree:
80
+ **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root in a one-worktree run:
81
81
 
82
82
  ```bash
83
83
  node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
84
84
  ```
85
85
 
86
- Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on CURATION=on KNOWLEDGE=off GATE_LEVEL=full` instead.
86
+ Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=off CURATION=off KNOWLEDGE=off GATE_LEVEL=full` instead.
87
87
 
88
88
  The accepted line is the only source of these values.
89
89
 
@@ -77,13 +77,13 @@ When a ticket requires multiple sequential Code agent phases, each Code agent ap
77
77
 
78
78
  ### Settings line
79
79
 
80
- **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root when the run has one worktree:
80
+ **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root in a one-worktree run:
81
81
 
82
82
  ```bash
83
83
  node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
84
84
  ```
85
85
 
86
- Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on CURATION=on KNOWLEDGE=off GATE_LEVEL=full` instead.
86
+ Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=off CURATION=off KNOWLEDGE=off GATE_LEVEL=full` instead.
87
87
 
88
88
  The accepted line is the only source of these values.
89
89
 
@@ -32,13 +32,13 @@ $ARGUMENTS
32
32
 
33
33
  **Produces:** DECISIONS_CONTEXT
34
34
 
35
- **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root when the run has one worktree:
35
+ **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root in a one-worktree run:
36
36
 
37
37
  ```bash
38
38
  node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
39
39
  ```
40
40
 
41
- Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on CURATION=on KNOWLEDGE=off GATE_LEVEL=full` instead.
41
+ Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=off CURATION=off KNOWLEDGE=off GATE_LEVEL=full` instead.
42
42
 
43
43
  The accepted line is the only source of these values.
44
44
 
@@ -144,13 +144,13 @@ node "$HOME/.devflow/scripts/verify-evidence.cjs" render --plan "{worktree}/.dev
144
144
 
145
145
  - `EVIDENCE_FILE` — `{worktree}/.devflow/docs/evidence-{branch_slug}.md`, its `## Evidence Exceptions` section now a byte copy of `PR_EXCEPTIONS` (absent when that is `(none)`).
146
146
 
147
- **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root when the run has one worktree:
147
+ **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root in a one-worktree run:
148
148
 
149
149
  ```bash
150
150
  node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
151
151
  ```
152
152
 
153
- Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on CURATION=on KNOWLEDGE=off GATE_LEVEL=full` instead.
153
+ Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=off CURATION=off KNOWLEDGE=off GATE_LEVEL=full` instead.
154
154
 
155
155
  The accepted line is the only source of these values.
156
156
 
@@ -108,13 +108,13 @@ If the user says "skip" or "just proceed" — skip remaining questions, present
108
108
  **Produces:** SKIM_CONTEXT, DECISIONS_CONTEXT, FEATURE_KNOWLEDGE, FEATURE_KNOWLEDGE_RULES, GATE_LEVEL
109
109
  **Requires:** CONFIRMED_SCOPE
110
110
 
111
- **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root when the run has one worktree:
111
+ **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root in a one-worktree run:
112
112
 
113
113
  ```bash
114
114
  node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
115
115
  ```
116
116
 
117
- Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on CURATION=on KNOWLEDGE=off GATE_LEVEL=full` instead.
117
+ Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=off CURATION=off KNOWLEDGE=off GATE_LEVEL=full` instead.
118
118
 
119
119
  The accepted line is the only source of these values.
120
120
 
@@ -52,13 +52,13 @@ Read `.release/RELEASE-FLOW.md`:
52
52
 
53
53
  **Produces:** DECISIONS_CONTEXT, FEATURE_KNOWLEDGE, FEATURE_KNOWLEDGE_RULES
54
54
 
55
- **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root when the run has one worktree:
55
+ **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root in a one-worktree run:
56
56
 
57
57
  ```bash
58
58
  node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
59
59
  ```
60
60
 
61
- Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on CURATION=on KNOWLEDGE=off GATE_LEVEL=full` instead.
61
+ Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=off CURATION=off KNOWLEDGE=off GATE_LEVEL=full` instead.
62
62
 
63
63
  The accepted line is the only source of these values.
64
64
 
@@ -41,13 +41,13 @@ git -C "{start}" rev-parse --show-toplevel
41
41
 
42
42
  and using its one-line output. If the command fails (outside a git repository), `{worktree}` is the start directory itself. Every docs path below is written `{worktree}/.devflow/docs/…`; a repo-relative docs path handed to an agent always travels with a `WORKTREE_PATH` naming the checkout it is relative to.
43
43
 
44
- **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root when the run has one worktree:
44
+ **Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root in a one-worktree run:
45
45
 
46
46
  ```bash
47
47
  node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
48
48
  ```
49
49
 
50
- Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on CURATION=on KNOWLEDGE=off GATE_LEVEL=full` instead.
50
+ Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> CURATION=<on|off> KNOWLEDGE=<on|off> GATE_LEVEL=<full|standard|lean>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=off CURATION=off KNOWLEDGE=off GATE_LEVEL=full` instead.
51
51
 
52
52
  The accepted line is the only source of these values.
53
53