@lumenflow/cli 2.7.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.
- package/README.md +120 -105
- package/dist/__tests__/agent-spawn-coordination.test.js +451 -0
- package/dist/__tests__/commands/integrate.test.js +165 -0
- package/dist/__tests__/gates-config.test.js +0 -1
- package/dist/__tests__/hooks/enforcement.test.js +279 -0
- package/dist/__tests__/init-greenfield.test.js +247 -0
- package/dist/__tests__/init-quick-ref.test.js +0 -1
- package/dist/__tests__/init-template-portability.test.js +0 -1
- package/dist/__tests__/init.test.js +27 -0
- package/dist/__tests__/initiative-e2e.test.js +442 -0
- package/dist/__tests__/initiative-plan-replacement.test.js +0 -1
- package/dist/__tests__/memory-integration.test.js +333 -0
- package/dist/__tests__/release.test.js +1 -1
- package/dist/__tests__/safe-git.test.js +0 -1
- package/dist/__tests__/state-doctor.test.js +54 -0
- package/dist/__tests__/sync-templates.test.js +255 -0
- package/dist/__tests__/wu-create-required-fields.test.js +121 -0
- package/dist/__tests__/wu-done-auto-cleanup.test.js +135 -0
- package/dist/__tests__/wu-lifecycle-integration.test.js +388 -0
- package/dist/backlog-prune.js +0 -1
- package/dist/cli-entry-point.js +0 -1
- package/dist/commands/integrate.js +229 -0
- package/dist/docs-sync.js +46 -0
- package/dist/doctor.js +0 -2
- package/dist/gates.js +0 -7
- package/dist/hooks/enforcement-checks.js +209 -0
- package/dist/hooks/enforcement-generator.js +365 -0
- package/dist/hooks/enforcement-sync.js +243 -0
- package/dist/hooks/index.js +7 -0
- package/dist/init.js +256 -13
- package/dist/initiative-add-wu.js +0 -2
- package/dist/initiative-create.js +0 -3
- package/dist/initiative-edit.js +0 -5
- package/dist/initiative-plan.js +0 -1
- package/dist/initiative-remove-wu.js +0 -2
- package/dist/lane-health.js +0 -2
- package/dist/lane-suggest.js +0 -1
- package/dist/mem-checkpoint.js +0 -2
- package/dist/mem-cleanup.js +0 -2
- package/dist/mem-context.js +0 -3
- package/dist/mem-create.js +0 -2
- package/dist/mem-delete.js +0 -3
- package/dist/mem-inbox.js +0 -2
- package/dist/mem-index.js +0 -1
- package/dist/mem-init.js +0 -2
- package/dist/mem-profile.js +0 -1
- package/dist/mem-promote.js +0 -1
- package/dist/mem-ready.js +0 -2
- package/dist/mem-signal.js +0 -2
- package/dist/mem-start.js +0 -2
- package/dist/mem-summarize.js +0 -2
- package/dist/metrics-cli.js +1 -1
- package/dist/metrics-snapshot.js +1 -1
- package/dist/onboarding-smoke-test.js +0 -5
- package/dist/orchestrate-init-status.js +0 -1
- package/dist/orchestrate-initiative.js +0 -1
- package/dist/orchestrate-monitor.js +0 -1
- package/dist/plan-create.js +0 -2
- package/dist/plan-edit.js +0 -2
- package/dist/plan-link.js +0 -2
- package/dist/plan-promote.js +0 -2
- package/dist/signal-cleanup.js +0 -4
- package/dist/state-bootstrap.js +0 -1
- package/dist/state-cleanup.js +0 -4
- package/dist/state-doctor-fix.js +5 -8
- package/dist/state-doctor.js +0 -11
- package/dist/sync-templates.js +188 -34
- package/dist/wu-block.js +100 -48
- package/dist/wu-claim.js +1 -22
- package/dist/wu-cleanup.js +0 -1
- package/dist/wu-create.js +0 -2
- package/dist/wu-done-auto-cleanup.js +139 -0
- package/dist/wu-done.js +11 -4
- package/dist/wu-edit.js +0 -12
- package/dist/wu-preflight.js +0 -1
- package/dist/wu-prep.js +0 -1
- package/dist/wu-proto.js +0 -1
- package/dist/wu-spawn.js +0 -3
- package/dist/wu-unblock.js +0 -2
- package/dist/wu-validate.js +0 -1
- package/package.json +8 -7
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 {
|
package/dist/mem-summarize.js
CHANGED
|
@@ -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 {
|
package/dist/metrics-cli.js
CHANGED
|
@@ -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)[
|
|
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,
|
package/dist/metrics-snapshot.js
CHANGED
|
@@ -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)[
|
|
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',
|
package/dist/plan-create.js
CHANGED
package/dist/plan-edit.js
CHANGED
package/dist/plan-link.js
CHANGED
package/dist/plan-promote.js
CHANGED
package/dist/signal-cleanup.js
CHANGED
|
@@ -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)) {
|
package/dist/state-bootstrap.js
CHANGED
|
@@ -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
|
/**
|
package/dist/state-cleanup.js
CHANGED
|
@@ -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)) {
|
package/dist/state-doctor-fix.js
CHANGED
|
@@ -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}`,
|
package/dist/state-doctor.js
CHANGED
|
@@ -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,
|
package/dist/sync-templates.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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;
|