@lumenflow/cli 3.12.4 → 3.12.6
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/dist/chunk-2D2VOCA4.js +37 -0
- package/dist/chunk-2D5KFYGX.js +284 -0
- package/dist/chunk-2GXVIN57.js +14072 -0
- package/dist/chunk-2MQ7HZWZ.js +26 -0
- package/dist/chunk-2UFQ3A3C.js +643 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-4N74J3UT.js +15 -0
- package/dist/chunk-5GTOXFYR.js +392 -0
- package/dist/chunk-5VY6MQMC.js +240 -0
- package/dist/chunk-67XVPMRY.js +1297 -0
- package/dist/chunk-6HO4GWJE.js +164 -0
- package/dist/chunk-6W5XHWYV.js +1890 -0
- package/dist/chunk-6X4EMYJQ.js +64 -0
- package/dist/chunk-6XYXI2NQ.js +772 -0
- package/dist/chunk-7ANSOV6Q.js +285 -0
- package/dist/chunk-A624LFLB.js +1380 -0
- package/dist/chunk-ADN5NHG4.js +126 -0
- package/dist/chunk-B7YJYJKG.js +33 -0
- package/dist/chunk-CCLHCPKG.js +210 -0
- package/dist/chunk-CK36VROC.js +1584 -0
- package/dist/chunk-D3UOFRSB.js +81 -0
- package/dist/chunk-DFR4DJBM.js +230 -0
- package/dist/chunk-DSYBDHYH.js +79 -0
- package/dist/chunk-DWMLTXKQ.js +1176 -0
- package/dist/chunk-E3REJTAJ.js +28 -0
- package/dist/chunk-EA3IVO64.js +633 -0
- package/dist/chunk-EK2AKZKD.js +55 -0
- package/dist/chunk-ELD7JTTT.js +343 -0
- package/dist/chunk-EX6TT2XI.js +195 -0
- package/dist/chunk-EXINSFZE.js +82 -0
- package/dist/chunk-EZ6ZBYBM.js +510 -0
- package/dist/chunk-FBKAPTJ2.js +16 -0
- package/dist/chunk-FVLV5RYH.js +1118 -0
- package/dist/chunk-GDNSBQVK.js +2485 -0
- package/dist/chunk-GPQHMBNN.js +278 -0
- package/dist/chunk-GTFJB67L.js +68 -0
- package/dist/chunk-HANJXVKW.js +1127 -0
- package/dist/chunk-HEVS5YLD.js +269 -0
- package/dist/chunk-HMEVZKPQ.js +9 -0
- package/dist/chunk-HRGSYNLM.js +3511 -0
- package/dist/chunk-ISZR5N4K.js +60 -0
- package/dist/chunk-J6SUPR2C.js +226 -0
- package/dist/chunk-JERYVEIZ.js +244 -0
- package/dist/chunk-JHHWGL2N.js +87 -0
- package/dist/chunk-JONWQUB5.js +775 -0
- package/dist/chunk-K2DIWWDM.js +1766 -0
- package/dist/chunk-KY4PGL5V.js +969 -0
- package/dist/chunk-L737LQ4C.js +1285 -0
- package/dist/chunk-LFTWYIB2.js +497 -0
- package/dist/chunk-LV47RFNJ.js +41 -0
- package/dist/chunk-MKSAITI7.js +15 -0
- package/dist/chunk-MZ7RKIX4.js +212 -0
- package/dist/chunk-NAP6CFSO.js +84 -0
- package/dist/chunk-ND6MY37M.js +16 -0
- package/dist/chunk-NMG736UR.js +683 -0
- package/dist/chunk-NRAXROED.js +32 -0
- package/dist/chunk-NRIZR3A7.js +690 -0
- package/dist/chunk-NX43BG3M.js +233 -0
- package/dist/chunk-O645XLSI.js +297 -0
- package/dist/chunk-OMJD6A3S.js +235 -0
- package/dist/chunk-QB6SJD4T.js +430 -0
- package/dist/chunk-QFSTL4J3.js +276 -0
- package/dist/chunk-QLGDFMFX.js +212 -0
- package/dist/chunk-RIAAGL2E.js +13 -0
- package/dist/chunk-RWO5XMZ6.js +86 -0
- package/dist/chunk-RXRKBBSM.js +149 -0
- package/dist/chunk-RZOZMML6.js +363 -0
- package/dist/chunk-U7I7FS7T.js +113 -0
- package/dist/chunk-UI42RODY.js +717 -0
- package/dist/chunk-UTVMVSCO.js +519 -0
- package/dist/chunk-V6OJGLBA.js +1746 -0
- package/dist/chunk-W2JHVH7D.js +152 -0
- package/dist/chunk-WD3Y7VQN.js +280 -0
- package/dist/chunk-WOCTQ5MS.js +303 -0
- package/dist/chunk-WZR3ZUNN.js +696 -0
- package/dist/chunk-XGI665H7.js +150 -0
- package/dist/chunk-XKY65P2T.js +304 -0
- package/dist/chunk-Y4CQZY65.js +57 -0
- package/dist/chunk-YFEXKLVE.js +194 -0
- package/dist/chunk-YHO3HS5X.js +287 -0
- package/dist/chunk-YLS7AZSX.js +738 -0
- package/dist/chunk-ZE473AO6.js +49 -0
- package/dist/chunk-ZF747T3O.js +644 -0
- package/dist/chunk-ZHCZHZH3.js +43 -0
- package/dist/chunk-ZZNZX2XY.js +87 -0
- package/dist/constants-7QAP3VQ4.js +23 -0
- package/dist/dist-IY3UUMWK.js +33 -0
- package/dist/init-templates.js +9 -9
- package/dist/invariants-runner-W5RGHCSU.js +27 -0
- package/dist/lane-lock-6J36HD5O.js +35 -0
- package/dist/mem-checkpoint-core-EANG2GVN.js +14 -0
- package/dist/mem-signal-core-2LZ2WYHW.js +19 -0
- package/dist/memory-store-OLB5FO7K.js +18 -0
- package/dist/service-6BYCOCO5.js +13 -0
- package/dist/spawn-policy-resolver-NTSZYQ6R.js +17 -0
- package/dist/spawn-task-builder-R4E2BHSW.js +22 -0
- package/dist/wu-claim.js +2 -2
- package/dist/wu-claim.js.map +1 -1
- package/dist/wu-done-already-merged.js +12 -5
- package/dist/wu-done-already-merged.js.map +1 -1
- package/dist/wu-done-gates.js +25 -4
- package/dist/wu-done-gates.js.map +1 -1
- package/dist/wu-done-ownership.js +6 -1
- package/dist/wu-done-ownership.js.map +1 -1
- package/dist/wu-done-pr-WLFFFEPJ.js +25 -0
- package/dist/wu-done-validation-3J5E36FE.js +30 -0
- package/dist/wu-done.js +6 -6
- package/dist/wu-done.js.map +1 -1
- package/dist/wu-duplicate-id-detector-5S7JHELK.js +232 -0
- package/dist/wu-edit-operations.js +58 -17
- package/dist/wu-edit-operations.js.map +1 -1
- package/dist/wu-edit-validators.js +104 -28
- package/dist/wu-edit-validators.js.map +1 -1
- package/dist/wu-edit.js +1 -1
- package/dist/wu-edit.js.map +1 -1
- package/dist/wu-spawn-prompt-builders.js +8 -7
- package/dist/wu-spawn-prompt-builders.js.map +1 -1
- package/package.json +8 -8
- package/packs/sidekick/.turbo/turbo-build.log +1 -1
- package/packs/sidekick/package.json +1 -1
- package/packs/software-delivery/.turbo/turbo-build.log +1 -1
- package/packs/software-delivery/package.json +1 -1
- package/templates/core/LUMENFLOW.md.template +1 -1
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +3 -3
- package/templates/core/ai/onboarding/starting-prompt.md.template +11 -11
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MS_PER_DAY
|
|
3
|
+
} from "./chunk-V6OJGLBA.js";
|
|
4
|
+
|
|
5
|
+
// ../memory/dist/decay/scoring.js
|
|
6
|
+
var DEFAULT_HALF_LIFE_MS = 30 * MS_PER_DAY;
|
|
7
|
+
var IMPORTANCE_BY_PRIORITY = {
|
|
8
|
+
P0: 2,
|
|
9
|
+
P1: 1.5,
|
|
10
|
+
P2: 1,
|
|
11
|
+
P3: 0.5
|
|
12
|
+
};
|
|
13
|
+
var DEFAULT_IMPORTANCE = 1;
|
|
14
|
+
function getNodeTimestamp(node) {
|
|
15
|
+
const createdAt = new Date(node.created_at).getTime();
|
|
16
|
+
if (!node.updated_at) {
|
|
17
|
+
return createdAt;
|
|
18
|
+
}
|
|
19
|
+
const updatedAt = new Date(node.updated_at).getTime();
|
|
20
|
+
return Math.max(createdAt, updatedAt);
|
|
21
|
+
}
|
|
22
|
+
function computeRecencyScore(node, halfLifeMs = DEFAULT_HALF_LIFE_MS, now = Date.now()) {
|
|
23
|
+
const nodeTimestamp = getNodeTimestamp(node);
|
|
24
|
+
const age = now - nodeTimestamp;
|
|
25
|
+
return Math.exp(-age / halfLifeMs);
|
|
26
|
+
}
|
|
27
|
+
function computeAccessScore(node) {
|
|
28
|
+
const accessCount = node.metadata?.access?.count ?? 0;
|
|
29
|
+
return Math.log1p(accessCount) / 10;
|
|
30
|
+
}
|
|
31
|
+
function computeImportanceScore(node) {
|
|
32
|
+
const priority = node.metadata?.priority;
|
|
33
|
+
if (!priority) {
|
|
34
|
+
return DEFAULT_IMPORTANCE;
|
|
35
|
+
}
|
|
36
|
+
return IMPORTANCE_BY_PRIORITY[priority] ?? DEFAULT_IMPORTANCE;
|
|
37
|
+
}
|
|
38
|
+
function computeDecayScore(node, options = {}) {
|
|
39
|
+
const { now = Date.now(), halfLifeMs = DEFAULT_HALF_LIFE_MS } = options;
|
|
40
|
+
const recencyScore = computeRecencyScore(node, halfLifeMs, now);
|
|
41
|
+
const accessScore = computeAccessScore(node);
|
|
42
|
+
const importanceScore = computeImportanceScore(node);
|
|
43
|
+
return recencyScore * (1 + accessScore) * importanceScore;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
DEFAULT_HALF_LIFE_MS,
|
|
48
|
+
computeDecayScore
|
|
49
|
+
};
|
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
import {
|
|
2
|
+
detectLaneOverlaps,
|
|
3
|
+
loadLaneDefinitions
|
|
4
|
+
} from "./chunk-XKY65P2T.js";
|
|
5
|
+
import {
|
|
6
|
+
createWUParser,
|
|
7
|
+
detectMissingTrackedWorktrees,
|
|
8
|
+
detectOrphanWorktrees,
|
|
9
|
+
runCLI
|
|
10
|
+
} from "./chunk-2GXVIN57.js";
|
|
11
|
+
import {
|
|
12
|
+
createWuPaths
|
|
13
|
+
} from "./chunk-6HO4GWJE.js";
|
|
14
|
+
import {
|
|
15
|
+
CONFIG_FILES
|
|
16
|
+
} from "./chunk-DWMLTXKQ.js";
|
|
17
|
+
import {
|
|
18
|
+
WORKSPACE_CONFIG_FILE_NAME,
|
|
19
|
+
getConfig,
|
|
20
|
+
getConfigFilePresence,
|
|
21
|
+
getResolvedPaths
|
|
22
|
+
} from "./chunk-V6OJGLBA.js";
|
|
23
|
+
|
|
24
|
+
// src/doctor.ts
|
|
25
|
+
import * as fs from "fs";
|
|
26
|
+
import * as path from "path";
|
|
27
|
+
import { execFileSync } from "child_process";
|
|
28
|
+
var MANAGED_FILE_PATTERNS = [
|
|
29
|
+
WORKSPACE_CONFIG_FILE_NAME,
|
|
30
|
+
CONFIG_FILES.LANE_INFERENCE,
|
|
31
|
+
"AGENTS.md",
|
|
32
|
+
"CLAUDE.md"
|
|
33
|
+
];
|
|
34
|
+
var DOCTOR_OPTIONS = {
|
|
35
|
+
verbose: {
|
|
36
|
+
name: "verbose",
|
|
37
|
+
flags: "-v, --verbose",
|
|
38
|
+
description: "Show detailed output including all checks"
|
|
39
|
+
},
|
|
40
|
+
json: {
|
|
41
|
+
name: "json",
|
|
42
|
+
flags: "--json",
|
|
43
|
+
description: "Output results as JSON"
|
|
44
|
+
},
|
|
45
|
+
deep: {
|
|
46
|
+
name: "deep",
|
|
47
|
+
flags: "--deep",
|
|
48
|
+
description: "Run heavier checks including WU validation (slower)"
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
function parseDoctorOptions() {
|
|
52
|
+
const opts = createWUParser({
|
|
53
|
+
name: "lumenflow-doctor",
|
|
54
|
+
description: "Check LumenFlow safety components and configuration",
|
|
55
|
+
options: Object.values(DOCTOR_OPTIONS)
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
verbose: opts.verbose ?? false,
|
|
59
|
+
json: opts.json ?? false,
|
|
60
|
+
deep: opts.deep ?? false
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function checkHusky(projectDir) {
|
|
64
|
+
const huskyDir = path.join(projectDir, ".husky");
|
|
65
|
+
const preCommit = path.join(huskyDir, "pre-commit");
|
|
66
|
+
if (!fs.existsSync(huskyDir)) {
|
|
67
|
+
return {
|
|
68
|
+
passed: false,
|
|
69
|
+
message: "Husky hooks not installed",
|
|
70
|
+
details: "Run: pnpm install && pnpm prepare"
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (!fs.existsSync(preCommit)) {
|
|
74
|
+
return {
|
|
75
|
+
passed: false,
|
|
76
|
+
message: "Husky pre-commit hook missing",
|
|
77
|
+
details: "Run: pnpm prepare"
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
passed: true,
|
|
82
|
+
message: "Husky hooks installed (.husky/pre-commit)"
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function checkSafeGit(projectDir) {
|
|
86
|
+
let safeGitPath;
|
|
87
|
+
try {
|
|
88
|
+
const resolved = getResolvedPaths({ projectRoot: projectDir, strictWorkspace: true });
|
|
89
|
+
safeGitPath = resolved.safeGitPath;
|
|
90
|
+
} catch {
|
|
91
|
+
safeGitPath = path.join(projectDir, "scripts", "safe-git");
|
|
92
|
+
}
|
|
93
|
+
const relativePath = path.relative(projectDir, safeGitPath);
|
|
94
|
+
if (!fs.existsSync(safeGitPath)) {
|
|
95
|
+
return {
|
|
96
|
+
passed: false,
|
|
97
|
+
message: "Safe-git wrapper missing",
|
|
98
|
+
details: `The ${relativePath} file should exist to block destructive git commands`
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
passed: true,
|
|
103
|
+
message: `Safe-git wrapper present (${relativePath})`
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function checkAgentsMd(projectDir) {
|
|
107
|
+
const agentsMdPath = path.join(projectDir, "AGENTS.md");
|
|
108
|
+
if (!fs.existsSync(agentsMdPath)) {
|
|
109
|
+
return {
|
|
110
|
+
passed: false,
|
|
111
|
+
message: "AGENTS.md missing",
|
|
112
|
+
details: "Run: lumenflow init to create universal agent instructions"
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
passed: true,
|
|
117
|
+
message: "AGENTS.md exists"
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function checkLumenflowConfig(projectDir) {
|
|
121
|
+
const { workspaceConfigExists } = getConfigFilePresence(projectDir);
|
|
122
|
+
if (!workspaceConfigExists) {
|
|
123
|
+
return {
|
|
124
|
+
passed: false,
|
|
125
|
+
message: `${WORKSPACE_CONFIG_FILE_NAME} missing`,
|
|
126
|
+
details: `Run: pnpm workspace-init --yes`
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
passed: true,
|
|
131
|
+
message: `Workspace config present (${WORKSPACE_CONFIG_FILE_NAME})`
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function checkVendorConfigs(projectDir) {
|
|
135
|
+
const claudeMdPath = path.join(projectDir, "CLAUDE.md");
|
|
136
|
+
const cursorRulesPath = path.join(projectDir, ".cursor", "rules", "lumenflow.md");
|
|
137
|
+
const windsurfRulesPath = path.join(projectDir, ".windsurf", "rules", "lumenflow.md");
|
|
138
|
+
const clineRulesPath = path.join(projectDir, ".clinerules");
|
|
139
|
+
const agentsMdPath = path.join(projectDir, "AGENTS.md");
|
|
140
|
+
return {
|
|
141
|
+
claude: {
|
|
142
|
+
present: fs.existsSync(claudeMdPath),
|
|
143
|
+
path: "CLAUDE.md"
|
|
144
|
+
},
|
|
145
|
+
cursor: {
|
|
146
|
+
present: fs.existsSync(cursorRulesPath),
|
|
147
|
+
path: ".cursor/rules/lumenflow.md"
|
|
148
|
+
},
|
|
149
|
+
windsurf: {
|
|
150
|
+
present: fs.existsSync(windsurfRulesPath),
|
|
151
|
+
path: ".windsurf/rules/lumenflow.md"
|
|
152
|
+
},
|
|
153
|
+
cline: {
|
|
154
|
+
present: fs.existsSync(clineRulesPath),
|
|
155
|
+
path: ".clinerules"
|
|
156
|
+
},
|
|
157
|
+
codex: {
|
|
158
|
+
// Codex reads AGENTS.md directly
|
|
159
|
+
present: fs.existsSync(agentsMdPath),
|
|
160
|
+
path: "AGENTS.md"
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function getCommandVersion(command, args) {
|
|
165
|
+
try {
|
|
166
|
+
const output = execFileSync(command, args, {
|
|
167
|
+
encoding: "utf-8",
|
|
168
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
169
|
+
}).trim();
|
|
170
|
+
return output;
|
|
171
|
+
} catch {
|
|
172
|
+
return "not found";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function parseVersion(versionStr) {
|
|
176
|
+
const match = versionStr.match(/(\d+)\.(\d+)\.?(\d+)?/);
|
|
177
|
+
if (!match) {
|
|
178
|
+
return [0, 0, 0];
|
|
179
|
+
}
|
|
180
|
+
return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3] || "0", 10)];
|
|
181
|
+
}
|
|
182
|
+
function compareVersions(actual, required) {
|
|
183
|
+
const actualParts = parseVersion(actual);
|
|
184
|
+
const requiredParts = parseVersion(required);
|
|
185
|
+
for (let i = 0; i < 3; i++) {
|
|
186
|
+
if (actualParts[i] > requiredParts[i]) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
if (actualParts[i] < requiredParts[i]) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
function checkLaneHealth(projectDir) {
|
|
196
|
+
const lanes = loadLaneDefinitions(projectDir);
|
|
197
|
+
if (lanes.length === 0) {
|
|
198
|
+
return {
|
|
199
|
+
passed: true,
|
|
200
|
+
message: "No lane definitions found - skipping lane health check"
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const overlapResult = detectLaneOverlaps(lanes);
|
|
204
|
+
if (overlapResult.hasOverlaps) {
|
|
205
|
+
const overlapCount = overlapResult.overlaps.length;
|
|
206
|
+
const firstOverlap = overlapResult.overlaps[0];
|
|
207
|
+
const laneNames = firstOverlap.lanes.join(" <-> ");
|
|
208
|
+
return {
|
|
209
|
+
passed: false,
|
|
210
|
+
message: `Lane overlap detected: ${overlapCount} overlap(s) found`,
|
|
211
|
+
details: `First overlap: ${laneNames}. Run 'pnpm lane:health' for full report.`
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
passed: true,
|
|
216
|
+
message: `Lane configuration healthy (${lanes.length} lanes, no overlaps)`
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function checkPrerequisites() {
|
|
220
|
+
const nodeVersion = getCommandVersion("node", ["--version"]);
|
|
221
|
+
const pnpmVersion = getCommandVersion("pnpm", ["--version"]);
|
|
222
|
+
const gitVersion = getCommandVersion("git", ["--version"]);
|
|
223
|
+
const requiredNode = "22.0.0";
|
|
224
|
+
const requiredPnpm = "9.0.0";
|
|
225
|
+
const requiredGit = "2.0.0";
|
|
226
|
+
const nodeOk = nodeVersion !== "not found" && compareVersions(nodeVersion, requiredNode);
|
|
227
|
+
const pnpmOk = pnpmVersion !== "not found" && compareVersions(pnpmVersion, requiredPnpm);
|
|
228
|
+
const gitOk = gitVersion !== "not found" && compareVersions(gitVersion, requiredGit);
|
|
229
|
+
return {
|
|
230
|
+
node: {
|
|
231
|
+
passed: nodeOk,
|
|
232
|
+
version: nodeVersion,
|
|
233
|
+
required: `>=${requiredNode}`,
|
|
234
|
+
message: nodeOk ? void 0 : `Node.js ${requiredNode}+ required`
|
|
235
|
+
},
|
|
236
|
+
pnpm: {
|
|
237
|
+
passed: pnpmOk,
|
|
238
|
+
version: pnpmVersion,
|
|
239
|
+
required: `>=${requiredPnpm}`,
|
|
240
|
+
message: pnpmOk ? void 0 : `pnpm ${requiredPnpm}+ required`
|
|
241
|
+
},
|
|
242
|
+
git: {
|
|
243
|
+
passed: gitOk,
|
|
244
|
+
version: gitVersion,
|
|
245
|
+
required: `>=${requiredGit}`,
|
|
246
|
+
message: gitOk ? void 0 : `Git ${requiredGit}+ required`
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function getGitRepoRoot(projectDir) {
|
|
251
|
+
try {
|
|
252
|
+
const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
253
|
+
cwd: projectDir,
|
|
254
|
+
encoding: "utf-8",
|
|
255
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
256
|
+
}).trim();
|
|
257
|
+
return repoRoot;
|
|
258
|
+
} catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async function checkManagedFilesDirty(projectDir) {
|
|
263
|
+
try {
|
|
264
|
+
const repoRoot = getGitRepoRoot(projectDir);
|
|
265
|
+
if (!repoRoot) {
|
|
266
|
+
return {
|
|
267
|
+
passed: true,
|
|
268
|
+
files: [],
|
|
269
|
+
message: "Git status check skipped (not a git repository)"
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const statusOutput = execFileSync("git", ["status", "--porcelain"], {
|
|
273
|
+
cwd: repoRoot,
|
|
274
|
+
encoding: "utf-8",
|
|
275
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
276
|
+
});
|
|
277
|
+
const config = getConfig({ projectRoot: repoRoot });
|
|
278
|
+
const managedDirPrefix = `${config.directories.wuDir.replace(/\\/g, "/").replace(/\/wu\/?$/, "/")}`;
|
|
279
|
+
const dirtyFiles = [];
|
|
280
|
+
const lines = statusOutput.split("\n").filter((line) => line.trim());
|
|
281
|
+
for (const line of lines) {
|
|
282
|
+
const filePath = line.slice(3).trim();
|
|
283
|
+
if (!filePath) continue;
|
|
284
|
+
const isManaged = MANAGED_FILE_PATTERNS.some((pattern) => filePath === pattern) || filePath.startsWith(managedDirPrefix);
|
|
285
|
+
if (isManaged) {
|
|
286
|
+
dirtyFiles.push(filePath);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (dirtyFiles.length > 0) {
|
|
290
|
+
return {
|
|
291
|
+
passed: false,
|
|
292
|
+
files: dirtyFiles,
|
|
293
|
+
message: `${dirtyFiles.length} managed file(s) have uncommitted changes`
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
passed: true,
|
|
298
|
+
files: [],
|
|
299
|
+
message: "No uncommitted changes to managed files"
|
|
300
|
+
};
|
|
301
|
+
} catch {
|
|
302
|
+
return {
|
|
303
|
+
passed: true,
|
|
304
|
+
files: [],
|
|
305
|
+
message: "Git status check skipped (not a git repository)"
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async function checkWorktreeSanity(projectDir) {
|
|
310
|
+
try {
|
|
311
|
+
execFileSync("git", ["rev-parse", "--git-dir"], {
|
|
312
|
+
cwd: projectDir,
|
|
313
|
+
encoding: "utf-8",
|
|
314
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
315
|
+
});
|
|
316
|
+
const orphanResult = await detectOrphanWorktrees(projectDir);
|
|
317
|
+
const missingTracked = await detectMissingTrackedWorktrees(projectDir);
|
|
318
|
+
const orphanCount = orphanResult.orphans.length;
|
|
319
|
+
const missingCount = missingTracked.length;
|
|
320
|
+
const errorCount = orphanResult.errors.length;
|
|
321
|
+
const totalIssues = orphanCount + missingCount + errorCount;
|
|
322
|
+
const passed = totalIssues === 0;
|
|
323
|
+
let message = "All worktrees are valid";
|
|
324
|
+
if (!passed) {
|
|
325
|
+
const parts = [];
|
|
326
|
+
if (orphanCount > 0) parts.push(`${orphanCount} orphan(s)`);
|
|
327
|
+
if (missingCount > 0) parts.push(`${missingCount} missing`);
|
|
328
|
+
if (errorCount > 0) parts.push(`${errorCount} error(s)`);
|
|
329
|
+
message = `Worktree issues: ${parts.join(", ")}`;
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
passed,
|
|
333
|
+
orphans: orphanCount,
|
|
334
|
+
stale: missingCount,
|
|
335
|
+
// Combined non-orphan issues for API compat
|
|
336
|
+
message
|
|
337
|
+
};
|
|
338
|
+
} catch {
|
|
339
|
+
return {
|
|
340
|
+
passed: true,
|
|
341
|
+
orphans: 0,
|
|
342
|
+
stale: 0,
|
|
343
|
+
message: "Worktree check skipped (not a git repository)"
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async function checkWUValidity(projectDir) {
|
|
348
|
+
const wuDir = path.join(projectDir, createWuPaths({ projectRoot: projectDir }).WU_DIR());
|
|
349
|
+
if (!fs.existsSync(wuDir)) {
|
|
350
|
+
return {
|
|
351
|
+
passed: true,
|
|
352
|
+
total: 0,
|
|
353
|
+
valid: 0,
|
|
354
|
+
invalid: 0,
|
|
355
|
+
warnings: 0,
|
|
356
|
+
message: "No WU directory found"
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
let validateOutput;
|
|
361
|
+
let cliError = false;
|
|
362
|
+
let cliErrorMessage = "";
|
|
363
|
+
try {
|
|
364
|
+
validateOutput = execFileSync("pnpm", ["wu:validate", "--all", "--no-strict"], {
|
|
365
|
+
cwd: projectDir,
|
|
366
|
+
encoding: "utf-8",
|
|
367
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
368
|
+
timeout: 6e4
|
|
369
|
+
// 60 second timeout for full validation
|
|
370
|
+
});
|
|
371
|
+
} catch (e) {
|
|
372
|
+
const execError = e;
|
|
373
|
+
validateOutput = (execError.stdout || "") + (execError.stderr || "");
|
|
374
|
+
const errorMsg = execError.message || "";
|
|
375
|
+
if (errorMsg.includes("ERR_PNPM") || errorMsg.includes("ENOENT") || errorMsg.includes("Missing script") || errorMsg.includes("command not found") || execError.code === "ENOENT" || // If no recognizable wu:validate output, assume CLI failed
|
|
376
|
+
!validateOutput.includes("[wu:validate]") && !validateOutput.includes("Valid:")) {
|
|
377
|
+
cliError = true;
|
|
378
|
+
cliErrorMessage = errorMsg.includes("Missing script") ? "wu:validate script not found" : errorMsg.includes("ENOENT") ? "pnpm command not available" : "wu:validate could not run";
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (cliError) {
|
|
382
|
+
return {
|
|
383
|
+
passed: false,
|
|
384
|
+
total: 0,
|
|
385
|
+
valid: 0,
|
|
386
|
+
invalid: 0,
|
|
387
|
+
warnings: 0,
|
|
388
|
+
message: `WU validation failed: ${cliErrorMessage}`
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const validMatch = validateOutput.match(/Valid:\s*(\d+)/);
|
|
392
|
+
const invalidMatch = validateOutput.match(/Invalid:\s*(\d+)/);
|
|
393
|
+
const warningMatch = validateOutput.match(/Warnings:\s*(\d+)/);
|
|
394
|
+
const validCount = validMatch ? parseInt(validMatch[1], 10) : 0;
|
|
395
|
+
const invalidCount = invalidMatch ? parseInt(invalidMatch[1], 10) : 0;
|
|
396
|
+
const warningCount = warningMatch ? parseInt(warningMatch[1], 10) : 0;
|
|
397
|
+
const total = validCount + invalidCount;
|
|
398
|
+
const passed = invalidCount === 0;
|
|
399
|
+
return {
|
|
400
|
+
passed,
|
|
401
|
+
total,
|
|
402
|
+
valid: validCount,
|
|
403
|
+
invalid: invalidCount,
|
|
404
|
+
warnings: warningCount,
|
|
405
|
+
message: passed ? total > 0 ? `All ${total} WU(s) valid${warningCount > 0 ? ` (${warningCount} warning(s))` : ""}` : "No WUs to validate" : `${invalidCount}/${total} WU(s) have issues`
|
|
406
|
+
};
|
|
407
|
+
} catch (e) {
|
|
408
|
+
const error = e;
|
|
409
|
+
return {
|
|
410
|
+
passed: false,
|
|
411
|
+
total: 0,
|
|
412
|
+
valid: 0,
|
|
413
|
+
invalid: 0,
|
|
414
|
+
warnings: 0,
|
|
415
|
+
message: `WU validation failed: ${error.message || "unknown error"}`
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async function runDoctor(projectDir, options = {}) {
|
|
420
|
+
const checks = {
|
|
421
|
+
husky: checkHusky(projectDir),
|
|
422
|
+
safeGit: checkSafeGit(projectDir),
|
|
423
|
+
agentsMd: checkAgentsMd(projectDir),
|
|
424
|
+
lumenflowConfig: checkLumenflowConfig(projectDir),
|
|
425
|
+
// WU-1191: Lane health check
|
|
426
|
+
laneHealth: checkLaneHealth(projectDir)
|
|
427
|
+
};
|
|
428
|
+
const vendorConfigs = checkVendorConfigs(projectDir);
|
|
429
|
+
const prerequisites = checkPrerequisites();
|
|
430
|
+
const managedFilesDirty = await checkManagedFilesDirty(projectDir);
|
|
431
|
+
const worktreeSanity = await checkWorktreeSanity(projectDir);
|
|
432
|
+
const workflowHealth = {
|
|
433
|
+
managedFilesDirty,
|
|
434
|
+
worktreeSanity
|
|
435
|
+
};
|
|
436
|
+
if (options.deep) {
|
|
437
|
+
workflowHealth.wuValidity = await checkWUValidity(projectDir);
|
|
438
|
+
}
|
|
439
|
+
const criticalChecks = [checks.husky, checks.safeGit, checks.agentsMd, checks.lumenflowConfig];
|
|
440
|
+
const allCriticalPassed = criticalChecks.every((check) => check.passed);
|
|
441
|
+
let exitCode = 0;
|
|
442
|
+
if (!allCriticalPassed) {
|
|
443
|
+
exitCode = 2;
|
|
444
|
+
} else if (!managedFilesDirty.passed || !worktreeSanity.passed || workflowHealth.wuValidity && !workflowHealth.wuValidity.passed) {
|
|
445
|
+
exitCode = 1;
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
status: allCriticalPassed ? "ACTIVE" : "INCOMPLETE",
|
|
449
|
+
exitCode,
|
|
450
|
+
checks,
|
|
451
|
+
vendorConfigs,
|
|
452
|
+
prerequisites,
|
|
453
|
+
workflowHealth
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
async function runDoctorForInit(projectDir) {
|
|
457
|
+
const result = await runDoctor(projectDir, { deep: false });
|
|
458
|
+
let warnings = 0;
|
|
459
|
+
let errors = 0;
|
|
460
|
+
const criticalCheckKeys = ["husky", "safeGit", "agentsMd", "lumenflowConfig"];
|
|
461
|
+
for (const [key, check] of Object.entries(result.checks)) {
|
|
462
|
+
if (!check.passed) {
|
|
463
|
+
if (criticalCheckKeys.includes(key)) {
|
|
464
|
+
errors++;
|
|
465
|
+
} else {
|
|
466
|
+
warnings++;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
for (const [, prereq] of Object.entries(result.prerequisites)) {
|
|
471
|
+
if (!prereq.passed) {
|
|
472
|
+
warnings++;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (result.workflowHealth) {
|
|
476
|
+
if (!result.workflowHealth.managedFilesDirty.passed) warnings++;
|
|
477
|
+
if (!result.workflowHealth.worktreeSanity.passed) warnings++;
|
|
478
|
+
}
|
|
479
|
+
const lines = [];
|
|
480
|
+
lines.push("[lumenflow doctor] Quick health check...");
|
|
481
|
+
if (result.exitCode === 0 && warnings === 0) {
|
|
482
|
+
lines.push(" All checks passed");
|
|
483
|
+
} else {
|
|
484
|
+
if (!result.checks.husky.passed) {
|
|
485
|
+
lines.push(" Error: Husky hooks not installed");
|
|
486
|
+
}
|
|
487
|
+
if (!result.checks.safeGit.passed) {
|
|
488
|
+
lines.push(" Error: safe-git script not found");
|
|
489
|
+
}
|
|
490
|
+
if (!result.checks.agentsMd.passed) {
|
|
491
|
+
lines.push(" Error: AGENTS.md not found");
|
|
492
|
+
}
|
|
493
|
+
if (!result.checks.lumenflowConfig.passed) {
|
|
494
|
+
lines.push(` Error: ${result.checks.lumenflowConfig.message}`);
|
|
495
|
+
if (result.checks.lumenflowConfig.details) {
|
|
496
|
+
lines.push(` ${result.checks.lumenflowConfig.details}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (!result.checks.laneHealth.passed) {
|
|
500
|
+
lines.push(` Warning: Lane overlap detected - ${result.checks.laneHealth.message}`);
|
|
501
|
+
}
|
|
502
|
+
if (!result.prerequisites.node.passed) {
|
|
503
|
+
lines.push(
|
|
504
|
+
` Warning: Node.js version ${result.prerequisites.node.version} (required: ${result.prerequisites.node.required})`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
if (!result.prerequisites.pnpm.passed) {
|
|
508
|
+
lines.push(
|
|
509
|
+
` Warning: pnpm version ${result.prerequisites.pnpm.version} (required: ${result.prerequisites.pnpm.required})`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
if (!result.prerequisites.git.passed) {
|
|
513
|
+
lines.push(
|
|
514
|
+
` Warning: Git version ${result.prerequisites.git.version} (required: ${result.prerequisites.git.required})`
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
if (result.workflowHealth?.managedFilesDirty.files.length) {
|
|
518
|
+
lines.push(
|
|
519
|
+
` Warning: ${result.workflowHealth.managedFilesDirty.files.length} managed file(s) have uncommitted changes`
|
|
520
|
+
);
|
|
521
|
+
for (const file of result.workflowHealth.managedFilesDirty.files.slice(0, 3)) {
|
|
522
|
+
lines.push(` -> ${file}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (result.workflowHealth?.worktreeSanity.orphans) {
|
|
526
|
+
lines.push(
|
|
527
|
+
` Warning: ${result.workflowHealth.worktreeSanity.orphans} orphan worktree(s) found`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
if (result.workflowHealth && !result.workflowHealth.worktreeSanity.passed) {
|
|
531
|
+
const wsSanity = result.workflowHealth.worktreeSanity;
|
|
532
|
+
if (wsSanity.stale > 0 && wsSanity.orphans === 0) {
|
|
533
|
+
lines.push(` Warning: ${wsSanity.message}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
blocked: false,
|
|
539
|
+
// Never blocks init
|
|
540
|
+
warnings,
|
|
541
|
+
errors,
|
|
542
|
+
output: lines.join("\n")
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function formatDoctorOutput(result) {
|
|
546
|
+
const lines = [];
|
|
547
|
+
lines.push("");
|
|
548
|
+
lines.push("LumenFlow Health Check");
|
|
549
|
+
lines.push("\u2500".repeat(45));
|
|
550
|
+
lines.push("");
|
|
551
|
+
lines.push("Core Safety Components:");
|
|
552
|
+
const formatCheck = (check) => {
|
|
553
|
+
const symbol = check.passed ? "\u2713" : "\u2717";
|
|
554
|
+
return ` ${symbol} ${check.message}`;
|
|
555
|
+
};
|
|
556
|
+
lines.push(formatCheck(result.checks.husky));
|
|
557
|
+
lines.push(formatCheck(result.checks.safeGit));
|
|
558
|
+
lines.push(formatCheck(result.checks.agentsMd));
|
|
559
|
+
lines.push(formatCheck(result.checks.lumenflowConfig));
|
|
560
|
+
lines.push("");
|
|
561
|
+
lines.push("Lane Health:");
|
|
562
|
+
lines.push(formatCheck(result.checks.laneHealth));
|
|
563
|
+
if (result.workflowHealth) {
|
|
564
|
+
lines.push("");
|
|
565
|
+
lines.push("Workflow Health:");
|
|
566
|
+
const mfd = result.workflowHealth.managedFilesDirty;
|
|
567
|
+
const mfdSymbol = mfd.passed ? "\u2713" : "\u26A0";
|
|
568
|
+
lines.push(` ${mfdSymbol} ${mfd.message}`);
|
|
569
|
+
if (!mfd.passed && mfd.files.length > 0) {
|
|
570
|
+
for (const file of mfd.files.slice(0, 5)) {
|
|
571
|
+
lines.push(` \u2192 ${file}`);
|
|
572
|
+
}
|
|
573
|
+
if (mfd.files.length > 5) {
|
|
574
|
+
lines.push(` \u2192 ... and ${mfd.files.length - 5} more`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const ws = result.workflowHealth.worktreeSanity;
|
|
578
|
+
const wsSymbol = ws.passed ? "\u2713" : "\u26A0";
|
|
579
|
+
lines.push(` ${wsSymbol} ${ws.message}`);
|
|
580
|
+
if (result.workflowHealth.wuValidity) {
|
|
581
|
+
const wv = result.workflowHealth.wuValidity;
|
|
582
|
+
const wvSymbol = wv.passed ? "\u2713" : "\u26A0";
|
|
583
|
+
lines.push(` ${wvSymbol} ${wv.message}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
lines.push("");
|
|
587
|
+
lines.push("Vendor Configs:");
|
|
588
|
+
const vendors = ["claude", "cursor", "windsurf", "cline", "codex"];
|
|
589
|
+
for (const vendor of vendors) {
|
|
590
|
+
const config = result.vendorConfigs[vendor];
|
|
591
|
+
const symbol = config.present ? "\u2713" : "\u25CB";
|
|
592
|
+
const status = config.present ? "present" : "not configured";
|
|
593
|
+
lines.push(` ${symbol} ${vendor}: ${status} (${config.path})`);
|
|
594
|
+
}
|
|
595
|
+
lines.push("");
|
|
596
|
+
lines.push("Prerequisites:");
|
|
597
|
+
const prereqs = ["node", "pnpm", "git"];
|
|
598
|
+
for (const prereq of prereqs) {
|
|
599
|
+
const check = result.prerequisites[prereq];
|
|
600
|
+
const symbol = check.passed ? "\u2713" : "\u2717";
|
|
601
|
+
const versionDisplay = check.version === "not found" ? "not found" : check.version;
|
|
602
|
+
lines.push(` ${symbol} ${prereq}: ${versionDisplay} (required: ${check.required})`);
|
|
603
|
+
}
|
|
604
|
+
lines.push("");
|
|
605
|
+
lines.push("\u2500".repeat(45));
|
|
606
|
+
lines.push(`LumenFlow safety: ${result.status}`);
|
|
607
|
+
if (result.exitCode === 1) {
|
|
608
|
+
lines.push("");
|
|
609
|
+
lines.push("Warnings detected (exit code 1). Use --deep for full WU validation.");
|
|
610
|
+
} else if (result.exitCode === 2) {
|
|
611
|
+
lines.push("");
|
|
612
|
+
lines.push("To fix missing components:");
|
|
613
|
+
lines.push(" pnpm install && pnpm prepare # Install Husky hooks");
|
|
614
|
+
lines.push(" pnpm workspace-init --yes # Create canonical workspace.yaml");
|
|
615
|
+
}
|
|
616
|
+
lines.push("");
|
|
617
|
+
return lines.join("\n");
|
|
618
|
+
}
|
|
619
|
+
function formatDoctorJson(result) {
|
|
620
|
+
return JSON.stringify(result, null, 2);
|
|
621
|
+
}
|
|
622
|
+
async function main() {
|
|
623
|
+
const opts = parseDoctorOptions();
|
|
624
|
+
const projectDir = process.cwd();
|
|
625
|
+
const result = await runDoctor(projectDir, { deep: opts.deep });
|
|
626
|
+
if (opts.json) {
|
|
627
|
+
console.log(formatDoctorJson(result));
|
|
628
|
+
} else {
|
|
629
|
+
console.log(formatDoctorOutput(result));
|
|
630
|
+
}
|
|
631
|
+
process.exit(result.exitCode);
|
|
632
|
+
}
|
|
633
|
+
if (import.meta.main) {
|
|
634
|
+
void runCLI(main);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export {
|
|
638
|
+
parseDoctorOptions,
|
|
639
|
+
runDoctor,
|
|
640
|
+
runDoctorForInit,
|
|
641
|
+
formatDoctorOutput,
|
|
642
|
+
formatDoctorJson,
|
|
643
|
+
main
|
|
644
|
+
};
|