@lumenflow/cli 2.6.0 → 2.8.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 (88) hide show
  1. package/README.md +120 -105
  2. package/dist/__tests__/agent-spawn-coordination.test.js +451 -0
  3. package/dist/__tests__/commands/integrate.test.js +165 -0
  4. package/dist/__tests__/gates-config.test.js +0 -1
  5. package/dist/__tests__/hooks/enforcement.test.js +279 -0
  6. package/dist/__tests__/init-greenfield.test.js +247 -0
  7. package/dist/__tests__/init-quick-ref.test.js +0 -1
  8. package/dist/__tests__/init-template-portability.test.js +0 -1
  9. package/dist/__tests__/init.test.js +27 -0
  10. package/dist/__tests__/initiative-e2e.test.js +442 -0
  11. package/dist/__tests__/initiative-plan-replacement.test.js +0 -1
  12. package/dist/__tests__/memory-integration.test.js +333 -0
  13. package/dist/__tests__/release.test.js +1 -1
  14. package/dist/__tests__/safe-git.test.js +0 -1
  15. package/dist/__tests__/state-doctor.test.js +54 -0
  16. package/dist/__tests__/sync-templates.test.js +255 -0
  17. package/dist/__tests__/wu-create-required-fields.test.js +121 -0
  18. package/dist/__tests__/wu-done-auto-cleanup.test.js +135 -0
  19. package/dist/__tests__/wu-lifecycle-integration.test.js +388 -0
  20. package/dist/__tests__/wu-proto.test.js +97 -0
  21. package/dist/backlog-prune.js +0 -1
  22. package/dist/cli-entry-point.js +0 -1
  23. package/dist/commands/integrate.js +229 -0
  24. package/dist/docs-sync.js +46 -0
  25. package/dist/doctor.js +0 -2
  26. package/dist/gates.js +0 -7
  27. package/dist/hooks/enforcement-checks.js +209 -0
  28. package/dist/hooks/enforcement-generator.js +365 -0
  29. package/dist/hooks/enforcement-sync.js +243 -0
  30. package/dist/hooks/index.js +7 -0
  31. package/dist/init.js +266 -11
  32. package/dist/initiative-add-wu.js +0 -2
  33. package/dist/initiative-create.js +0 -3
  34. package/dist/initiative-edit.js +0 -5
  35. package/dist/initiative-plan.js +0 -1
  36. package/dist/initiative-remove-wu.js +0 -2
  37. package/dist/lane-health.js +0 -2
  38. package/dist/lane-suggest.js +0 -1
  39. package/dist/mem-checkpoint.js +0 -2
  40. package/dist/mem-cleanup.js +0 -2
  41. package/dist/mem-context.js +0 -3
  42. package/dist/mem-create.js +0 -2
  43. package/dist/mem-delete.js +0 -3
  44. package/dist/mem-inbox.js +0 -2
  45. package/dist/mem-index.js +0 -1
  46. package/dist/mem-init.js +0 -2
  47. package/dist/mem-profile.js +0 -1
  48. package/dist/mem-promote.js +0 -1
  49. package/dist/mem-ready.js +0 -2
  50. package/dist/mem-signal.js +0 -2
  51. package/dist/mem-start.js +0 -2
  52. package/dist/mem-summarize.js +0 -2
  53. package/dist/metrics-cli.js +1 -1
  54. package/dist/metrics-snapshot.js +1 -1
  55. package/dist/onboarding-smoke-test.js +0 -5
  56. package/dist/orchestrate-init-status.js +0 -1
  57. package/dist/orchestrate-initiative.js +0 -1
  58. package/dist/orchestrate-monitor.js +0 -1
  59. package/dist/plan-create.js +0 -2
  60. package/dist/plan-edit.js +0 -2
  61. package/dist/plan-link.js +0 -2
  62. package/dist/plan-promote.js +0 -2
  63. package/dist/signal-cleanup.js +0 -4
  64. package/dist/state-bootstrap.js +0 -1
  65. package/dist/state-cleanup.js +0 -4
  66. package/dist/state-doctor-fix.js +5 -8
  67. package/dist/state-doctor.js +0 -11
  68. package/dist/sync-templates.js +188 -34
  69. package/dist/wu-block.js +100 -48
  70. package/dist/wu-claim.js +1 -22
  71. package/dist/wu-cleanup.js +0 -1
  72. package/dist/wu-create.js +0 -2
  73. package/dist/wu-done-auto-cleanup.js +139 -0
  74. package/dist/wu-done.js +11 -4
  75. package/dist/wu-edit.js +0 -12
  76. package/dist/wu-preflight.js +0 -1
  77. package/dist/wu-prep.js +0 -1
  78. package/dist/wu-proto.js +329 -0
  79. package/dist/wu-spawn.js +0 -3
  80. package/dist/wu-unblock.js +0 -2
  81. package/dist/wu-validate.js +0 -1
  82. package/package.json +9 -7
  83. package/templates/core/.husky/pre-commit.template +93 -0
  84. package/templates/core/ai/onboarding/quick-ref-commands.md.template +27 -0
  85. package/templates/core/ai/onboarding/rapid-prototyping.md +143 -0
  86. package/templates/core/ai/onboarding/starting-prompt.md.template +3 -3
  87. package/templates/vendors/claude/.claude/CLAUDE.md.template +25 -0
  88. package/templates/vendors/claude/.claude/hooks/enforce-worktree.sh +135 -0
@@ -51,10 +51,8 @@ async function writeAuditLog(baseDir, entry) {
51
51
  try {
52
52
  const logPath = path.join(baseDir, LUMENFLOW_PATHS.AUDIT_LOG);
53
53
  const logDir = path.dirname(logPath);
54
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool creates known directory
55
54
  await fs.mkdir(logDir, { recursive: true });
56
55
  const line = `${JSON.stringify(entry)}\n`;
57
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes audit log
58
56
  await fs.appendFile(logPath, line, 'utf-8');
59
57
  }
60
58
  catch {
package/dist/mem-start.js CHANGED
@@ -37,11 +37,9 @@ async function writeAuditLog(baseDir, entry) {
37
37
  const logPath = path.join(baseDir, LUMENFLOW_PATHS.AUDIT_LOG);
38
38
  const logDir = path.dirname(logPath);
39
39
  // Ensure telemetry directory exists
40
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool creates known directory
41
40
  await fs.mkdir(logDir, { recursive: true });
42
41
  // Append NDJSON entry
43
42
  const line = `${JSON.stringify(entry)}\n`;
44
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes audit log
45
43
  await fs.appendFile(logPath, line, 'utf-8');
46
44
  }
47
45
  catch {
@@ -72,10 +72,8 @@ async function writeAuditLog(baseDir, entry) {
72
72
  try {
73
73
  const logPath = path.join(baseDir, LUMENFLOW_PATHS.AUDIT_LOG);
74
74
  const logDir = path.dirname(logPath);
75
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool creates known directory
76
75
  await fs.mkdir(logDir, { recursive: true });
77
76
  const line = `${JSON.stringify(entry)}\n`;
78
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes audit log
79
77
  await fs.appendFile(logPath, line, 'utf-8');
80
78
  }
81
79
  catch {
@@ -158,7 +158,7 @@ async function loadGitCommits(weekStart, weekEnd) {
158
158
  const message = entry.message;
159
159
  const wuIdMatch = message.match(/\b(WU-\d+)\b/i);
160
160
  const wuId = wuIdMatch ? wuIdMatch[1].toUpperCase() : undefined;
161
- const typeMatch = message.match(/^(feat|fix|docs|chore|refactor|test|style|perf|ci)[\(:]?/i);
161
+ const typeMatch = message.match(/^(feat|fix|docs|chore|refactor|test|style|perf|ci)[(:]?/i);
162
162
  const type = typeMatch ? typeMatch[1].toLowerCase() : undefined;
163
163
  commits.push({
164
164
  hash: entry.hash,
@@ -145,7 +145,7 @@ async function loadGitCommits(weekStart, weekEnd) {
145
145
  const wuIdMatch = message.match(/\b(WU-\d+)\b/i);
146
146
  const wuId = wuIdMatch ? wuIdMatch[1].toUpperCase() : undefined;
147
147
  // Determine commit type from conventional commit prefix
148
- const typeMatch = message.match(/^(feat|fix|docs|chore|refactor|test|style|perf|ci)[\(:]?/i);
148
+ const typeMatch = message.match(/^(feat|fix|docs|chore|refactor|test|style|perf|ci)[(:]?/i);
149
149
  const type = typeMatch ? typeMatch[1].toLowerCase() : undefined;
150
150
  commits.push({
151
151
  hash: entry.hash,
@@ -174,22 +174,17 @@ export function validateLaneInferenceFormat(options) {
174
174
  * Initialize a git repository in the given directory
175
175
  */
176
176
  function initializeGitRepo(projectDir) {
177
- // eslint-disable-next-line sonarjs/no-os-command-from-path -- Git binary from PATH is acceptable for smoke tests
178
177
  execFileSync(GIT_BINARY, ['init'], { cwd: projectDir, stdio: 'pipe' });
179
- // eslint-disable-next-line sonarjs/no-os-command-from-path -- Git binary from PATH is acceptable for smoke tests
180
178
  execFileSync(GIT_BINARY, ['config', 'user.email', 'test@example.com'], {
181
179
  cwd: projectDir,
182
180
  stdio: 'pipe',
183
181
  });
184
- // eslint-disable-next-line sonarjs/no-os-command-from-path -- Git binary from PATH is acceptable for smoke tests
185
182
  execFileSync(GIT_BINARY, ['config', 'user.name', 'Test User'], {
186
183
  cwd: projectDir,
187
184
  stdio: 'pipe',
188
185
  });
189
186
  // Create initial commit
190
- // eslint-disable-next-line sonarjs/no-os-command-from-path -- Git binary from PATH is acceptable for smoke tests
191
187
  execFileSync(GIT_BINARY, ['add', '-A'], { cwd: projectDir, stdio: 'pipe' });
192
- // eslint-disable-next-line sonarjs/no-os-command-from-path -- Git binary from PATH is acceptable for smoke tests
193
188
  execFileSync(GIT_BINARY, ['commit', '-m', 'Initial commit', '--allow-empty'], {
194
189
  cwd: projectDir,
195
190
  stdio: 'pipe',
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /* eslint-disable no-console -- CLI tool requires console output */
3
2
  /**
4
3
  * Orchestrate Initiative Status CLI
5
4
  *
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /* eslint-disable no-console -- CLI tool requires console output */
3
2
  /**
4
3
  * Orchestrate Initiative CLI
5
4
  *
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /* eslint-disable no-console -- CLI tool requires console output */
3
2
  /**
4
3
  * Orchestrate Monitor CLI (WU-1241)
5
4
  *
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /* eslint-disable no-console -- CLI tool requires console output */
3
- /* eslint-disable security/detect-non-literal-fs-filename */
4
2
  /**
5
3
  * Plan Create Command (WU-1313)
6
4
  *
package/dist/plan-edit.js CHANGED
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /* eslint-disable no-console -- CLI tool requires console output */
3
- /* eslint-disable security/detect-non-literal-fs-filename */
4
2
  /**
5
3
  * Plan Edit Command (WU-1313)
6
4
  *
package/dist/plan-link.js CHANGED
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /* eslint-disable no-console -- CLI tool requires console output */
3
- /* eslint-disable security/detect-non-literal-fs-filename */
4
2
  /**
5
3
  * Plan Link Command (WU-1313)
6
4
  *
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /* eslint-disable no-console -- CLI tool requires console output */
3
- /* eslint-disable security/detect-non-literal-fs-filename */
4
2
  /**
5
3
  * Plan Promote Command (WU-1313)
6
4
  *
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /* eslint-disable no-console -- CLI tool requires console output */
3
2
  /**
4
3
  * Signal Cleanup CLI (WU-1204)
5
4
  *
@@ -94,10 +93,8 @@ async function writeAuditLog(baseDir, entry) {
94
93
  try {
95
94
  const logPath = path.join(baseDir, LUMENFLOW_PATHS.AUDIT_LOG);
96
95
  const logDir = path.dirname(logPath);
97
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool creates known directory
98
96
  await fs.mkdir(logDir, { recursive: true });
99
97
  const line = `${JSON.stringify(entry)}\n`;
100
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes audit log
101
98
  await fs.appendFile(logPath, line, 'utf-8');
102
99
  }
103
100
  catch {
@@ -142,7 +139,6 @@ async function getActiveWuIds(baseDir) {
142
139
  for (const file of wuFiles) {
143
140
  try {
144
141
  const filePath = path.join(wuDir, file);
145
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads known path
146
142
  const content = await fs.readFile(filePath, 'utf-8');
147
143
  const wu = parseYaml(content);
148
144
  if (wu.id && wu.status && ACTIVE_WU_STATUSES.includes(wu.status)) {
@@ -17,7 +17,6 @@ import { parse as parseYaml } from 'yaml';
17
17
  import { readFileSync } from 'node:fs';
18
18
  import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
19
19
  import { CLI_FLAGS, EXIT_CODES, EMOJI, STRING_LITERALS, } from '@lumenflow/core/dist/wu-constants.js';
20
- /* eslint-disable security/detect-non-literal-fs-filename */
21
20
  /** Log prefix for consistent output */
22
21
  const LOG_PREFIX = '[state-bootstrap]';
23
22
  /**
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /* eslint-disable no-console -- CLI tool requires console output */
3
2
  /**
4
3
  * Unified State Cleanup CLI (WU-1208)
5
4
  *
@@ -111,10 +110,8 @@ async function writeAuditLog(baseDir, entry) {
111
110
  try {
112
111
  const logPath = path.join(baseDir, LUMENFLOW_PATHS.AUDIT_LOG);
113
112
  const logDir = path.dirname(logPath);
114
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool creates known directory
115
113
  await fs.mkdir(logDir, { recursive: true });
116
114
  const line = `${JSON.stringify(entry)}\n`;
117
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes audit log
118
115
  await fs.appendFile(logPath, line, 'utf-8');
119
116
  }
120
117
  catch {
@@ -150,7 +147,6 @@ async function getActiveWuIds(baseDir) {
150
147
  for (const file of wuFiles) {
151
148
  try {
152
149
  const filePath = path.join(wuDir, file);
153
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads known path
154
150
  const content = await fs.readFile(filePath, 'utf-8');
155
151
  const wu = parseYaml(content);
156
152
  if (wu.id && wu.status && ACTIVE_WU_STATUSES.includes(wu.status)) {
@@ -7,9 +7,13 @@
7
7
  * 1. No direct file modifications on main branch
8
8
  * 2. Removal of stale WU references from backlog.md and status.md
9
9
  * 3. All changes pushed via merge, not direct file modification
10
+ * 4. WU-1362: Retry logic for push failures (inherited from withMicroWorktree)
11
+ *
12
+ * Retry behavior is configured via .lumenflow.config.yaml git.push_retry section.
13
+ * Default: 3 retries with exponential backoff and jitter.
10
14
  *
11
15
  * @see {@link ./state-doctor.ts} - Main CLI that uses these deps
12
- * @see {@link @lumenflow/core/dist/micro-worktree.js} - Micro-worktree infrastructure
16
+ * @see {@link @lumenflow/core/dist/micro-worktree.js} - Micro-worktree infrastructure with retry logic
13
17
  */
14
18
  import fs from 'node:fs/promises';
15
19
  import path from 'node:path';
@@ -56,7 +60,6 @@ function removeWuReferences(content, wuId) {
56
60
  */
57
61
  async function readFileSafe(filePath) {
58
62
  try {
59
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- Validated path from config
60
63
  return await fs.readFile(filePath, 'utf-8');
61
64
  }
62
65
  catch {
@@ -100,7 +103,6 @@ export function createStateDoctorFixDeps(_baseDir) {
100
103
  return true; // Keep malformed lines
101
104
  }
102
105
  });
103
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- Validated path
104
106
  await fs.writeFile(signalsPath, lines.join('\n') + '\n', 'utf-8');
105
107
  return {
106
108
  commitMessage: `fix(state-doctor): remove dangling signal ${id}`,
@@ -139,7 +141,6 @@ export function createStateDoctorFixDeps(_baseDir) {
139
141
  return true; // Keep malformed lines
140
142
  }
141
143
  });
142
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- Validated path
143
144
  await fs.writeFile(eventsPath, lines.join('\n') + '\n', 'utf-8');
144
145
  modifiedFiles.push(WU_EVENTS_FILE);
145
146
  }
@@ -148,7 +149,6 @@ export function createStateDoctorFixDeps(_baseDir) {
148
149
  const backlogContent = await readFileSafe(backlogPath);
149
150
  if (backlogContent && backlogContent.includes(wuId)) {
150
151
  const updatedBacklog = removeWuReferences(backlogContent, wuId);
151
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- Validated path
152
152
  await fs.writeFile(backlogPath, updatedBacklog, 'utf-8');
153
153
  modifiedFiles.push(BACKLOG_FILE);
154
154
  }
@@ -157,7 +157,6 @@ export function createStateDoctorFixDeps(_baseDir) {
157
157
  const statusContent = await readFileSafe(statusPath);
158
158
  if (statusContent && statusContent.includes(wuId)) {
159
159
  const updatedStatus = removeWuReferences(statusContent, wuId);
160
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- Validated path
161
160
  await fs.writeFile(statusPath, updatedStatus, 'utf-8');
162
161
  modifiedFiles.push(STATUS_FILE);
163
162
  }
@@ -179,12 +178,10 @@ export function createStateDoctorFixDeps(_baseDir) {
179
178
  pushOnly: true,
180
179
  execute: async ({ worktreePath }) => {
181
180
  const stampsDir = path.join(worktreePath, '.lumenflow/stamps');
182
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool creates known directory
183
181
  await fs.mkdir(stampsDir, { recursive: true });
184
182
  // Create stamp file in micro-worktree
185
183
  const stampPath = path.join(stampsDir, `${wuId}.done`);
186
184
  const stampContent = `# ${wuId} Done\n\nTitle: ${title}\nCreated by: state:doctor --fix\nTimestamp: ${new Date().toISOString()}\n`;
187
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- Validated path
188
185
  await fs.writeFile(stampPath, stampContent, 'utf-8');
189
186
  return {
190
187
  commitMessage: `fix(state-doctor): create missing stamp for ${wuId}`,
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /* eslint-disable no-console -- CLI tool requires console output */
3
2
  /**
4
3
  * State Doctor CLI (WU-1209)
5
4
  *
@@ -101,10 +100,8 @@ async function writeAuditLog(baseDir, entry) {
101
100
  try {
102
101
  const logPath = path.join(baseDir, LUMENFLOW_PATHS.AUDIT_LOG);
103
102
  const logDir = path.dirname(logPath);
104
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool creates known directory
105
103
  await fs.mkdir(logDir, { recursive: true });
106
104
  const line = `${JSON.stringify(entry)}\n`;
107
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes audit log
108
105
  await fs.appendFile(logPath, line, 'utf-8');
109
106
  }
110
107
  catch {
@@ -146,7 +143,6 @@ async function createDeps(baseDir) {
146
143
  for (const file of wuFiles) {
147
144
  try {
148
145
  const filePath = path.join(wuDir, file);
149
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads known path
150
146
  const content = await fs.readFile(filePath, 'utf-8');
151
147
  const wu = parseYaml(content);
152
148
  if (wu.id && wu.status) {
@@ -189,7 +185,6 @@ async function createDeps(baseDir) {
189
185
  listSignals: async () => {
190
186
  try {
191
187
  const signalsPath = path.join(baseDir, SIGNALS_FILE);
192
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads known path
193
188
  const content = await fs.readFile(signalsPath, 'utf-8');
194
189
  const signals = [];
195
190
  for (const line of content.split('\n')) {
@@ -223,7 +218,6 @@ async function createDeps(baseDir) {
223
218
  listEvents: async () => {
224
219
  try {
225
220
  const eventsPath = path.join(baseDir, WU_EVENTS_FILE);
226
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads known path
227
221
  const content = await fs.readFile(eventsPath, 'utf-8');
228
222
  const events = [];
229
223
  for (const line of content.split('\n')) {
@@ -255,7 +249,6 @@ async function createDeps(baseDir) {
255
249
  */
256
250
  removeSignal: async (id) => {
257
251
  const signalsPath = path.join(baseDir, SIGNALS_FILE);
258
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads known path
259
252
  const content = await fs.readFile(signalsPath, 'utf-8');
260
253
  const lines = content.split('\n').filter((line) => {
261
254
  if (!line.trim())
@@ -268,7 +261,6 @@ async function createDeps(baseDir) {
268
261
  return true; // Keep malformed lines
269
262
  }
270
263
  });
271
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes known path
272
264
  await fs.writeFile(signalsPath, lines.join('\n') + '\n', 'utf-8');
273
265
  },
274
266
  /**
@@ -276,7 +268,6 @@ async function createDeps(baseDir) {
276
268
  */
277
269
  removeEvent: async (wuId) => {
278
270
  const eventsPath = path.join(baseDir, WU_EVENTS_FILE);
279
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads known path
280
271
  const content = await fs.readFile(eventsPath, 'utf-8');
281
272
  const lines = content.split('\n').filter((line) => {
282
273
  if (!line.trim())
@@ -289,7 +280,6 @@ async function createDeps(baseDir) {
289
280
  return true; // Keep malformed lines
290
281
  }
291
282
  });
292
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes known path
293
283
  await fs.writeFile(eventsPath, lines.join('\n') + '\n', 'utf-8');
294
284
  },
295
285
  /**
@@ -297,7 +287,6 @@ async function createDeps(baseDir) {
297
287
  */
298
288
  createStamp: async (wuId, title) => {
299
289
  const stampsDir = path.join(baseDir, LUMENFLOW_PATHS.STAMPS_DIR);
300
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool creates known directory
301
290
  await fs.mkdir(stampsDir, { recursive: true });
302
291
  await createStamp({
303
292
  id: wuId,
@@ -2,24 +2,29 @@
2
2
  * @file sync-templates.ts
3
3
  * Sync internal docs to CLI templates for release-cycle maintenance (WU-1123)
4
4
  *
5
+ * WU-1368: Fixed two bugs:
6
+ * 1. --check-drift flag now is truly read-only (compares without writing)
7
+ * 2. sync:templates uses micro-worktree isolation for safe atomic commits
8
+ *
5
9
  * This script syncs source docs from the hellmai/os repo to the templates
6
10
  * directory, applying template variable substitutions:
7
11
  * - Onboarding docs -> templates/core/ai/onboarding/
8
12
  * - Claude skills -> templates/vendors/claude/.claude/skills/
9
13
  * - Core docs (LUMENFLOW.md, constraints.md) -> templates/core/
10
14
  */
11
- /* eslint-disable no-console -- CLI tool requires console output */
12
- /* eslint-disable security/detect-non-literal-fs-filename -- CLI tool syncs templates from known paths */
13
- /* eslint-disable security/detect-non-literal-regexp -- Dynamic date pattern for template substitution */
14
15
  import * as fs from 'node:fs';
15
16
  import * as path from 'node:path';
16
- import { createWUParser } from '@lumenflow/core';
17
+ import { createWUParser, withMicroWorktree } from '@lumenflow/core';
17
18
  // Directory name constants to avoid duplicate strings
18
19
  const LUMENFLOW_DIR = '.lumenflow';
19
20
  const CLAUDE_DIR = '.claude';
20
21
  const SKILLS_DIR = 'skills';
21
22
  // Template variable patterns
22
23
  const DATE_PATTERN = /\d{4}-\d{2}-\d{2}/g;
24
+ // Log prefix for console output
25
+ const LOG_PREFIX = '[sync-templates]';
26
+ // Micro-worktree operation name
27
+ const OPERATION_NAME = 'sync-templates';
23
28
  /**
24
29
  * CLI option definitions for sync-templates command
25
30
  */
@@ -212,7 +217,6 @@ function checkFileDrift(sourcePath, templatePath, projectRoot) {
212
217
  * to detect if templates have drifted out of sync. Used by CI to warn
213
218
  * when templates need to be re-synced.
214
219
  */
215
- // eslint-disable-next-line sonarjs/cognitive-complexity -- Multi-category drift check requires nested iteration
216
220
  export async function checkTemplateDrift(projectRoot) {
217
221
  const driftingFiles = [];
218
222
  const checkedFiles = [];
@@ -277,40 +281,145 @@ export async function checkTemplateDrift(projectRoot) {
277
281
  };
278
282
  }
279
283
  /**
280
- * CLI entry point
284
+ * Sync a single file to templates within a worktree path
285
+ *
286
+ * WU-1368: Internal helper for micro-worktree sync operations.
287
+ * Writes to worktreePath instead of projectRoot for isolation.
281
288
  */
282
- // eslint-disable-next-line sonarjs/cognitive-complexity -- CLI main() handles multiple modes and output formatting
283
- export async function main() {
284
- const opts = parseSyncTemplatesOptions();
285
- const projectRoot = process.cwd();
286
- // Check-drift mode: verify templates match source without syncing
287
- if (opts.checkDrift) {
288
- console.log('[sync-templates] Checking for template drift...');
289
- const drift = await checkTemplateDrift(projectRoot);
290
- if (opts.verbose) {
291
- console.log(` Checked ${drift.checkedFiles.length} files`);
289
+ function syncFileToWorktree(sourcePath, targetPath, projectRoot, result) {
290
+ try {
291
+ if (!fs.existsSync(sourcePath)) {
292
+ result.errors.push(`Source not found: ${sourcePath}`);
293
+ return;
292
294
  }
293
- if (drift.hasDrift) {
294
- console.log('\n[sync-templates] WARNING: Template drift detected!');
295
- console.log(' The following templates are out of sync with their source:');
296
- for (const file of drift.driftingFiles) {
297
- console.log(` - ${file}`);
298
- }
299
- console.log('\n Run `pnpm sync:templates` to update templates.');
300
- process.exitCode = 1;
295
+ const content = fs.readFileSync(sourcePath, 'utf-8');
296
+ const templateContent = convertToTemplate(content, projectRoot);
297
+ ensureDir(path.dirname(targetPath));
298
+ fs.writeFileSync(targetPath, templateContent);
299
+ // Store relative path from project root (not worktree path)
300
+ const relPath = targetPath.includes('templates/')
301
+ ? targetPath.substring(targetPath.indexOf('packages/'))
302
+ : path.basename(targetPath);
303
+ result.synced.push(relPath);
304
+ }
305
+ catch (error) {
306
+ result.errors.push(`Error syncing ${sourcePath}: ${error.message}`);
307
+ }
308
+ }
309
+ /**
310
+ * Sync templates using micro-worktree isolation (WU-1368)
311
+ *
312
+ * This function uses the micro-worktree pattern to atomically sync templates:
313
+ * 1. Create temp branch in micro-worktree
314
+ * 2. Sync all templates to micro-worktree
315
+ * 3. Commit and push atomically
316
+ * 4. Cleanup
317
+ *
318
+ * Benefits:
319
+ * - Never modifies main checkout directly
320
+ * - Atomic commit with all template changes
321
+ * - Race-safe with other operations
322
+ *
323
+ * @param {string} projectRoot - Project root directory (source for templates)
324
+ * @returns {Promise<SyncSummary>} Summary of synced files
325
+ */
326
+ export async function syncTemplatesWithWorktree(projectRoot) {
327
+ // Generate unique operation ID using timestamp
328
+ const operationId = `templates-${Date.now()}`;
329
+ console.log(`${LOG_PREFIX} Using micro-worktree isolation for atomic sync...`);
330
+ // Set env var for pre-push hook
331
+ const previousWuTool = process.env.LUMENFLOW_WU_TOOL;
332
+ process.env.LUMENFLOW_WU_TOOL = OPERATION_NAME;
333
+ try {
334
+ let syncResult = {
335
+ onboarding: { synced: [], errors: [] },
336
+ skills: { synced: [], errors: [] },
337
+ core: { synced: [], errors: [] },
338
+ };
339
+ await withMicroWorktree({
340
+ operation: OPERATION_NAME,
341
+ id: operationId,
342
+ logPrefix: LOG_PREFIX,
343
+ execute: async ({ worktreePath }) => {
344
+ const templatesDir = path.join(worktreePath, 'packages', '@lumenflow', 'cli', 'templates');
345
+ // Sync core docs
346
+ const coreResult = { synced: [], errors: [] };
347
+ const lumenflowSource = path.join(projectRoot, 'LUMENFLOW.md');
348
+ const lumenflowTarget = path.join(templatesDir, 'core', 'LUMENFLOW.md.template');
349
+ syncFileToWorktree(lumenflowSource, lumenflowTarget, projectRoot, coreResult);
350
+ const constraintsSource = path.join(projectRoot, LUMENFLOW_DIR, 'constraints.md');
351
+ const constraintsTarget = path.join(templatesDir, 'core', LUMENFLOW_DIR, 'constraints.md.template');
352
+ syncFileToWorktree(constraintsSource, constraintsTarget, projectRoot, coreResult);
353
+ // Sync onboarding docs
354
+ const onboardingResult = { synced: [], errors: [] };
355
+ const onboardingSourceDir = path.join(projectRoot, 'docs', '04-operations', '_frameworks', 'lumenflow', 'agent', 'onboarding');
356
+ const onboardingTargetDir = path.join(templatesDir, 'core', 'ai', 'onboarding');
357
+ if (fs.existsSync(onboardingSourceDir)) {
358
+ const files = fs.readdirSync(onboardingSourceDir).filter((f) => f.endsWith('.md'));
359
+ for (const file of files) {
360
+ const sourcePath = path.join(onboardingSourceDir, file);
361
+ const targetPath = path.join(onboardingTargetDir, `${file}.template`);
362
+ syncFileToWorktree(sourcePath, targetPath, projectRoot, onboardingResult);
363
+ }
364
+ }
365
+ else {
366
+ onboardingResult.errors.push(`Onboarding source directory not found: ${onboardingSourceDir}`);
367
+ }
368
+ // Sync skills
369
+ const skillsResult = { synced: [], errors: [] };
370
+ const skillsSourceDir = path.join(projectRoot, CLAUDE_DIR, SKILLS_DIR);
371
+ const skillsTargetDir = path.join(templatesDir, 'vendors', 'claude', CLAUDE_DIR, SKILLS_DIR);
372
+ if (fs.existsSync(skillsSourceDir)) {
373
+ const skillDirs = fs
374
+ .readdirSync(skillsSourceDir, { withFileTypes: true })
375
+ .filter((d) => d.isDirectory())
376
+ .map((d) => d.name);
377
+ for (const skillName of skillDirs) {
378
+ const skillFile = path.join(skillsSourceDir, skillName, 'SKILL.md');
379
+ if (fs.existsSync(skillFile)) {
380
+ const targetPath = path.join(skillsTargetDir, skillName, 'SKILL.md.template');
381
+ syncFileToWorktree(skillFile, targetPath, projectRoot, skillsResult);
382
+ }
383
+ }
384
+ }
385
+ else {
386
+ skillsResult.errors.push(`Skills source directory not found: ${skillsSourceDir}`);
387
+ }
388
+ syncResult = {
389
+ onboarding: onboardingResult,
390
+ skills: skillsResult,
391
+ core: coreResult,
392
+ };
393
+ // Collect all synced files for commit
394
+ const allSyncedFiles = [
395
+ ...coreResult.synced,
396
+ ...onboardingResult.synced,
397
+ ...skillsResult.synced,
398
+ ];
399
+ const totalSynced = allSyncedFiles.length;
400
+ const commitMessage = `chore(sync:templates): sync ${totalSynced} template files`;
401
+ return {
402
+ commitMessage,
403
+ files: allSyncedFiles,
404
+ };
405
+ },
406
+ });
407
+ return syncResult;
408
+ }
409
+ finally {
410
+ // Restore env var
411
+ if (previousWuTool === undefined) {
412
+ delete process.env.LUMENFLOW_WU_TOOL;
301
413
  }
302
414
  else {
303
- console.log('[sync-templates] All templates are in sync.');
415
+ process.env.LUMENFLOW_WU_TOOL = previousWuTool;
304
416
  }
305
- return;
306
- }
307
- // Sync mode: update templates from source
308
- console.log('[sync-templates] Syncing internal docs to CLI templates...');
309
- if (opts.dryRun) {
310
- console.log(' (dry-run mode - no files will be written)');
311
417
  }
312
- const result = await syncTemplates(projectRoot, opts.dryRun);
313
- // Print results
418
+ }
419
+ /**
420
+ * Print sync results summary
421
+ */
422
+ function printSyncResults(result) {
314
423
  const sections = [
315
424
  { name: 'Onboarding docs', data: result.onboarding },
316
425
  { name: 'Claude skills', data: result.skills },
@@ -331,7 +440,52 @@ export async function main() {
331
440
  }
332
441
  }
333
442
  }
334
- console.log(`\n[sync-templates] Done! Synced ${totalSynced} files.`);
443
+ return { totalSynced, totalErrors };
444
+ }
445
+ /**
446
+ * CLI entry point
447
+ */
448
+ export async function main() {
449
+ const opts = parseSyncTemplatesOptions();
450
+ const projectRoot = process.cwd();
451
+ // Check-drift mode: verify templates match source without syncing (read-only)
452
+ if (opts.checkDrift) {
453
+ console.log(`${LOG_PREFIX} Checking for template drift...`);
454
+ const drift = await checkTemplateDrift(projectRoot);
455
+ if (opts.verbose) {
456
+ console.log(` Checked ${drift.checkedFiles.length} files`);
457
+ }
458
+ if (drift.hasDrift) {
459
+ console.log(`\n${LOG_PREFIX} WARNING: Template drift detected!`);
460
+ console.log(' The following templates are out of sync with their source:');
461
+ for (const file of drift.driftingFiles) {
462
+ console.log(` - ${file}`);
463
+ }
464
+ console.log('\n Run `pnpm sync:templates` to update templates.');
465
+ process.exitCode = 1;
466
+ }
467
+ else {
468
+ console.log(`${LOG_PREFIX} All templates are in sync.`);
469
+ }
470
+ return;
471
+ }
472
+ // Dry-run mode: show what would be synced without writing
473
+ if (opts.dryRun) {
474
+ console.log(`${LOG_PREFIX} Dry-run mode - showing what would be synced...`);
475
+ const result = await syncTemplates(projectRoot, true);
476
+ const { totalSynced, totalErrors } = printSyncResults(result);
477
+ console.log(`\n${LOG_PREFIX} Dry run complete. Would sync ${totalSynced} files.`);
478
+ if (totalErrors > 0) {
479
+ console.log(` ${totalErrors} error(s) would occur.`);
480
+ process.exitCode = 1;
481
+ }
482
+ return;
483
+ }
484
+ // Sync mode: update templates using micro-worktree isolation (WU-1368)
485
+ console.log(`${LOG_PREFIX} Syncing internal docs to CLI templates...`);
486
+ const result = await syncTemplatesWithWorktree(projectRoot);
487
+ const { totalSynced, totalErrors } = printSyncResults(result);
488
+ console.log(`\n${LOG_PREFIX} Done! Synced ${totalSynced} files.`);
335
489
  if (totalErrors > 0) {
336
490
  console.log(` ${totalErrors} error(s) occurred.`);
337
491
  process.exitCode = 1;