agent-work-loop 0.0.0 → 0.6.23

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 (54) hide show
  1. package/README.md +272 -12
  2. package/dist/brief-Z3JKXEUP.js +181 -0
  3. package/dist/changelog-R7BNBF2C.js +62 -0
  4. package/dist/chunk-46HZN6UB.js +446 -0
  5. package/dist/chunk-4OCSYHYB.js +274 -0
  6. package/dist/chunk-6E7XEQOH.js +27 -0
  7. package/dist/chunk-7SYRDDTX.js +516 -0
  8. package/dist/chunk-BUWGQVHT.js +1243 -0
  9. package/dist/chunk-C7BR2DCS.js +96 -0
  10. package/dist/chunk-D5OINC3G.js +52 -0
  11. package/dist/chunk-DP4O5ME2.js +307 -0
  12. package/dist/chunk-F5LHXBH7.js +209 -0
  13. package/dist/chunk-G5LAJ5TV.js +453 -0
  14. package/dist/chunk-I77CXOEX.js +693 -0
  15. package/dist/chunk-IMB46O6S.js +286 -0
  16. package/dist/chunk-IXMAFR4Y.js +771 -0
  17. package/dist/chunk-QE2CLNBG.js +347 -0
  18. package/dist/chunk-UOPWVM2H.js +727 -0
  19. package/dist/chunk-YTAHVR4P.js +166 -0
  20. package/dist/chunk-ZE6HXOYG.js +904 -0
  21. package/dist/cli.js +374 -13
  22. package/dist/commit-APXIVOSD.js +411 -0
  23. package/dist/config-TFMW7O4T.js +34 -0
  24. package/dist/doctor-SSKNLPGH.js +29 -0
  25. package/dist/evolve-QPD7TWGO.js +38 -0
  26. package/dist/feedback-KAXNFMUY.js +125 -0
  27. package/dist/gotchas-MCA5Y76R.js +43 -0
  28. package/dist/hold-recheck-WN5EG7HD.js +133 -0
  29. package/dist/init-UDM5AXKI.js +79 -0
  30. package/dist/lane-DAZISODH.js +41 -0
  31. package/dist/loop-summary-XAI6KOGB.js +361 -0
  32. package/dist/metrics-WLRZZRTK.js +25 -0
  33. package/dist/record-UKDIUJ5T.js +68 -0
  34. package/dist/review-ZTHDJ47V.js +118 -0
  35. package/dist/rules-R2UZPIVW.js +33 -0
  36. package/dist/state-XM7NZ2HA.js +37 -0
  37. package/dist/status-L6U5KO6T.js +40 -0
  38. package/dist/uninstall-5DFEOFL5.js +545 -0
  39. package/dist/update-AYTBYAHI.js +61 -0
  40. package/dist/verify-L7ARTK42.js +37 -0
  41. package/dist/version-check-LKGU2DNF.js +14 -0
  42. package/dist/work-KEPTGZ6H.js +50 -0
  43. package/engine/skills/claude/awl-loop/SKILL.md +292 -0
  44. package/engine/skills/claude/awl-loop/reference.md +131 -0
  45. package/engine/skills/claude/awl-pipeline/SKILL.md +89 -0
  46. package/engine/skills/claude/awl-pipeline-exec/SKILL.md +200 -0
  47. package/engine/skills/claude/awl-pipeline-plan/SKILL.md +71 -0
  48. package/engine/skills/claude/awl-pipeline-review/SKILL.md +149 -0
  49. package/engine/skills/codex/AGENTS.awl.md +117 -0
  50. package/engine/templates/block-publish.mjs +9 -0
  51. package/engine/templates/pre-push.sample +7 -0
  52. package/engine/templates/related-cmd-examples.md +37 -0
  53. package/engine/version.json +2 -2
  54. package/package.json +10 -4
@@ -0,0 +1,771 @@
1
+ import {
2
+ gatherVersionInputs
3
+ } from "./chunk-C7BR2DCS.js";
4
+ import {
5
+ checkVersions
6
+ } from "./chunk-YTAHVR4P.js";
7
+ import {
8
+ loadProjectName,
9
+ readRecords
10
+ } from "./chunk-I77CXOEX.js";
11
+ import {
12
+ loadState,
13
+ readStateLock
14
+ } from "./chunk-4OCSYHYB.js";
15
+ import {
16
+ CommandNotFoundError,
17
+ run,
18
+ tokenize
19
+ } from "./chunk-UOPWVM2H.js";
20
+ import {
21
+ caps,
22
+ card,
23
+ clipToWidth,
24
+ findProjectRoot,
25
+ globalRoot,
26
+ gotchasDir,
27
+ installedEngineVersion,
28
+ makeColors,
29
+ makeSymbols,
30
+ makeTokens,
31
+ projectsFile,
32
+ rulesDir,
33
+ signal,
34
+ visibleWidth
35
+ } from "./chunk-7SYRDDTX.js";
36
+
37
+ // src/commands/doctor.ts
38
+ import fs from "fs";
39
+ import path from "path";
40
+ function exists(p) {
41
+ try {
42
+ return fs.existsSync(p);
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+ function isDirectory(p) {
48
+ try {
49
+ return fs.statSync(p).isDirectory();
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+ function readJson(p) {
55
+ try {
56
+ return JSON.parse(fs.readFileSync(p, "utf8"));
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+ function countEntries(dir, ext) {
62
+ try {
63
+ return fs.readdirSync(dir).filter((f) => !f.startsWith(".") && (ext ? f.endsWith(ext) : true)).length;
64
+ } catch {
65
+ return 0;
66
+ }
67
+ }
68
+ async function gitBranch(projectRoot) {
69
+ try {
70
+ const r = await run({
71
+ cmd: "git",
72
+ args: ["symbolic-ref", "--short", "HEAD"],
73
+ cwd: projectRoot,
74
+ timeoutMs: 5e3
75
+ });
76
+ if (r.exitCode !== 0) {
77
+ return null;
78
+ }
79
+ const branch = r.stdout.trim();
80
+ return branch || null;
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+ async function gitDirtyFiles(projectRoot) {
86
+ try {
87
+ const r = await run({
88
+ cmd: "git",
89
+ args: ["status", "--porcelain", "-z"],
90
+ cwd: projectRoot,
91
+ timeoutMs: 5e3
92
+ });
93
+ if (r.exitCode !== 0) {
94
+ return null;
95
+ }
96
+ const tokens = r.stdout.split("\0").filter((t) => t !== "");
97
+ const files = [];
98
+ let skipNext = false;
99
+ for (const token of tokens) {
100
+ if (skipNext) {
101
+ skipNext = false;
102
+ continue;
103
+ }
104
+ const status = token.slice(0, 2);
105
+ files.push(token.slice(3));
106
+ skipNext = status.includes("R") || status.includes("C");
107
+ }
108
+ return files;
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+ async function gitHasCommits(projectRoot) {
114
+ try {
115
+ const r = await run({
116
+ cmd: "git",
117
+ args: ["rev-parse", "--verify", "--quiet", "HEAD"],
118
+ cwd: projectRoot,
119
+ timeoutMs: 5e3
120
+ });
121
+ return r.exitCode === 0;
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+ function detectRecordTrailGap(input) {
127
+ return input.hasCommits && !input.activeWorkitem && input.gateAttemptRecords === 0;
128
+ }
129
+ var MIN_DECISIVE_FILES = 3;
130
+ var MAJORITY_THRESHOLD = 0.8;
131
+ function classifyBasename(nameNoExt) {
132
+ if (nameNoExt.includes("-")) {
133
+ return "kebab-case";
134
+ }
135
+ if (nameNoExt.includes("_")) {
136
+ return "snake_case";
137
+ }
138
+ if (/^[A-Z]/.test(nameNoExt)) {
139
+ return "PascalCase";
140
+ }
141
+ if (/[A-Z]/.test(nameNoExt)) {
142
+ return "camelCase";
143
+ }
144
+ return "ambiguous";
145
+ }
146
+ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
147
+ "ts",
148
+ "tsx",
149
+ "js",
150
+ "jsx",
151
+ "mjs",
152
+ "cjs",
153
+ "py",
154
+ "go",
155
+ "rs",
156
+ "java",
157
+ "rb",
158
+ "php",
159
+ "c",
160
+ "cpp",
161
+ "h",
162
+ "hpp",
163
+ "cs",
164
+ "kt",
165
+ "swift"
166
+ ]);
167
+ var IGNORE_DIRS = /* @__PURE__ */ new Set([
168
+ "node_modules",
169
+ ".git",
170
+ "dist",
171
+ "build",
172
+ ".awl",
173
+ ".awl-worktrees",
174
+ "coverage"
175
+ ]);
176
+ var STATE_SIZE_WARN_BYTES = 1024 * 1024;
177
+ function listSourceFiles(projectRoot) {
178
+ const root = fs.existsSync(path.join(projectRoot, "src")) ? path.join(projectRoot, "src") : projectRoot;
179
+ const out = [];
180
+ const walk = (dir, depth) => {
181
+ if (depth > 8) {
182
+ return;
183
+ }
184
+ let entries;
185
+ try {
186
+ entries = fs.readdirSync(dir, { withFileTypes: true });
187
+ } catch {
188
+ return;
189
+ }
190
+ for (const e of entries) {
191
+ if (e.isDirectory()) {
192
+ if (!IGNORE_DIRS.has(e.name)) {
193
+ walk(path.join(dir, e.name), depth + 1);
194
+ }
195
+ } else if (e.isFile()) {
196
+ const ext = e.name.split(".").pop() ?? "";
197
+ if (CODE_EXTENSIONS.has(ext)) {
198
+ out.push(path.join(dir, e.name));
199
+ }
200
+ }
201
+ }
202
+ };
203
+ walk(root, 0);
204
+ return out;
205
+ }
206
+ function detectNamingConvention(filenames) {
207
+ const counts = {};
208
+ let decisiveTotal = 0;
209
+ for (const raw of filenames) {
210
+ const nameNoExt = raw.replace(/\.[^./]+$/, "");
211
+ const cls = classifyBasename(nameNoExt);
212
+ if (cls === "ambiguous") {
213
+ continue;
214
+ }
215
+ counts[cls] = (counts[cls] ?? 0) + 1;
216
+ decisiveTotal += 1;
217
+ }
218
+ if (decisiveTotal < MIN_DECISIVE_FILES) {
219
+ return { convention: null, reason: "insufficient_data", counts, decisiveTotal };
220
+ }
221
+ const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
222
+ const top = sorted[0];
223
+ if (top && top[1] / decisiveTotal >= MAJORITY_THRESHOLD) {
224
+ return {
225
+ convention: top[0],
226
+ reason: "detected",
227
+ counts,
228
+ decisiveTotal
229
+ };
230
+ }
231
+ return { convention: null, reason: "mixed", counts, decisiveTotal };
232
+ }
233
+ var MIN_FILES_FOR_THRESHOLD = 5;
234
+ var IQR_MULTIPLIER = 1.5;
235
+ function percentile(sorted, p) {
236
+ const pos = p * (sorted.length - 1);
237
+ const lower = Math.floor(pos);
238
+ const upper = Math.ceil(pos);
239
+ const weight = pos - lower;
240
+ const lowerVal = sorted[lower];
241
+ if (upper >= sorted.length) {
242
+ return lowerVal;
243
+ }
244
+ const upperVal = sorted[upper];
245
+ return lowerVal + (upperVal - lowerVal) * weight;
246
+ }
247
+ function computeFileSizeOutliers(files) {
248
+ if (files.length < MIN_FILES_FOR_THRESHOLD) {
249
+ return { threshold: null, outliers: [] };
250
+ }
251
+ const sortedLines = [...files].map((f) => f.lines).sort((a, b) => a - b);
252
+ const q1 = percentile(sortedLines, 0.25);
253
+ const q3 = percentile(sortedLines, 0.75);
254
+ const iqr = q3 - q1;
255
+ const threshold = q3 + IQR_MULTIPLIER * iqr;
256
+ const outliers = files.filter((f) => f.lines > threshold);
257
+ return { threshold, outliers };
258
+ }
259
+ function countFileLines(projectRoot) {
260
+ const out = [];
261
+ for (const abs of listSourceFiles(projectRoot)) {
262
+ try {
263
+ const lines = fs.readFileSync(abs, "utf8").split("\n").length;
264
+ out.push({ path: path.relative(projectRoot, abs), lines });
265
+ } catch {
266
+ }
267
+ }
268
+ return out;
269
+ }
270
+ function isAwlConfig(c) {
271
+ if (typeof c !== "object" || c === null) {
272
+ return false;
273
+ }
274
+ const o = c;
275
+ if (typeof o.engineVersion !== "string") {
276
+ return false;
277
+ }
278
+ if (o.verify !== void 0 && (typeof o.verify !== "object" || o.verify === null)) {
279
+ return false;
280
+ }
281
+ return true;
282
+ }
283
+ var INIT_HINT = "awl init \uC744 \uC2E4\uD589\uD558\uC138\uC694";
284
+ function collectEnv(checks) {
285
+ const major = Number(process.versions.node.split(".")[0]);
286
+ checks.push({
287
+ group: "\uD658\uACBD",
288
+ name: "Node",
289
+ status: major >= 18 ? "ok" : "fail",
290
+ value: `v${process.versions.node}`,
291
+ hint: major >= 18 ? void 0 : "Node 18 \uC774\uC0C1\uC774 \uD544\uC694\uD569\uB2C8\uB2E4"
292
+ });
293
+ checks.push({
294
+ group: "\uD658\uACBD",
295
+ name: "\uD50C\uB7AB\uD3FC",
296
+ status: "ok",
297
+ value: `${process.platform} ${process.arch}`
298
+ });
299
+ const c = caps();
300
+ checks.push({
301
+ group: "\uD658\uACBD",
302
+ name: "\uD130\uBBF8\uB110",
303
+ status: "ok",
304
+ value: `\uC720\uB2C8\uCF54\uB4DC ${c.unicode ? "\uC9C0\uC6D0" : "\uBBF8\uC9C0\uC6D0"}, \uC0C9 ${c.color ? "\uC9C0\uC6D0" : "\uBBF8\uC9C0\uC6D0"}`
305
+ });
306
+ }
307
+ function collectGlobal(checks, versionResult) {
308
+ const root = globalRoot();
309
+ if (!exists(root)) {
310
+ checks.push({
311
+ group: "\uC804\uC5ED \uC124\uCE58",
312
+ name: "~/.awl",
313
+ status: "missing",
314
+ value: "\uC5C6\uC74C",
315
+ hint: INIT_HINT
316
+ });
317
+ return;
318
+ }
319
+ checks.push({ group: "\uC804\uC5ED \uC124\uCE58", name: "~/.awl", status: "ok", value: "\uC788\uC74C" });
320
+ const probe = path.join(root, `.doctor-write-${process.pid}`);
321
+ try {
322
+ fs.writeFileSync(probe, "ok");
323
+ fs.unlinkSync(probe);
324
+ checks.push({ group: "\uC804\uC5ED \uC124\uCE58", name: "\uC4F0\uAE30 \uAD8C\uD55C", status: "ok", value: "\uAC00\uB2A5" });
325
+ } catch {
326
+ checks.push({
327
+ group: "\uC804\uC5ED \uC124\uCE58",
328
+ name: "\uC4F0\uAE30 \uAD8C\uD55C",
329
+ status: "fail",
330
+ value: "\uBD88\uAC00",
331
+ hint: "~/.awl \uC5D0 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uAD8C\uD55C\uC744 \uD655\uC778\uD558\uC138\uC694"
332
+ });
333
+ }
334
+ const engineVer = installedEngineVersion();
335
+ const binaryMismatch = findMismatch(versionResult, "binary-vs-engine");
336
+ checks.push({
337
+ group: "\uC804\uC5ED \uC124\uCE58",
338
+ name: "\uC5D4\uC9C4 \uBC84\uC804",
339
+ status: engineVer ? binaryMismatch ? "warn" : "ok" : "missing",
340
+ value: engineVer ?? "\uC5C6\uC74C",
341
+ hint: binaryMismatch ? binaryMismatch.hint : engineVer ? void 0 : INIT_HINT
342
+ });
343
+ const buildMismatch = findMismatch(versionResult, "build");
344
+ if (buildMismatch) {
345
+ checks.push({
346
+ group: "\uC804\uC5ED \uC124\uCE58",
347
+ name: "\uBE4C\uB4DC \uBB34\uACB0\uC131",
348
+ status: "warn",
349
+ value: `package ${buildMismatch.a} / engine \uC18C\uC2A4 ${buildMismatch.b}`,
350
+ hint: buildMismatch.hint
351
+ });
352
+ }
353
+ checks.push({
354
+ group: "\uC804\uC5ED \uC124\uCE58",
355
+ name: "\uADDC\uCE59",
356
+ status: "info",
357
+ value: `${countEntries(path.join(rulesDir(), "active"))}\uAC1C`
358
+ });
359
+ checks.push({
360
+ group: "\uC804\uC5ED \uC124\uCE58",
361
+ name: "\uAD50\uD6C8",
362
+ status: "info",
363
+ value: `${countEntries(gotchasDir(), ".json")}\uAC1C`
364
+ });
365
+ let projectCount = 0;
366
+ const projects = readJson(projectsFile());
367
+ if (Array.isArray(projects)) {
368
+ projectCount = projects.length;
369
+ } else if (projects && typeof projects === "object") {
370
+ projectCount = Object.keys(projects).length;
371
+ }
372
+ checks.push({ group: "\uC804\uC5ED \uC124\uCE58", name: "\uD504\uB85C\uC81D\uD2B8", status: "info", value: `${projectCount}\uAC1C` });
373
+ }
374
+ async function collectProject(checks, projectRoot, versionResult) {
375
+ if (!projectRoot) {
376
+ checks.push({
377
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
378
+ name: "\uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8",
379
+ status: "info",
380
+ value: "\uC544\uB2D8 (.git/.awl \uC5C6\uC74C)"
381
+ });
382
+ return;
383
+ }
384
+ checks.push({
385
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
386
+ name: "\uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8",
387
+ status: "info",
388
+ value: projectRoot
389
+ });
390
+ const branch = await gitBranch(projectRoot);
391
+ checks.push({
392
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
393
+ name: "\uBE0C\uB79C\uCE58",
394
+ status: "info",
395
+ value: branch ?? "\uC54C \uC218 \uC5C6\uC74C (\uD655\uC778 \uC2E4\uD328)"
396
+ });
397
+ const dirtyFiles = await gitDirtyFiles(projectRoot);
398
+ if (dirtyFiles === null) {
399
+ checks.push({
400
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
401
+ name: "\uC6CC\uD0B9\uD2B8\uB9AC",
402
+ status: "info",
403
+ value: "\uD655\uC778 \uC548 \uB428 (git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2C8\uAC70\uB098 \uD655\uC778 \uC2E4\uD328)"
404
+ });
405
+ } else if (dirtyFiles.length === 0) {
406
+ checks.push({ group: "\uC774 \uD504\uB85C\uC81D\uD2B8", name: "\uC6CC\uD0B9\uD2B8\uB9AC", status: "ok", value: "\uD074\uB9B0" });
407
+ } else {
408
+ checks.push({
409
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
410
+ name: "\uC6CC\uD0B9\uD2B8\uB9AC",
411
+ status: "warn",
412
+ value: `\uBBF8\uCEE4\uBC0B \uBCC0\uACBD ${dirtyFiles.length}\uAC1C`,
413
+ hint: `${dirtyFiles.slice(0, 5).join(", ")}${dirtyFiles.length > 5 ? " \uB4F1" : ""} \u2014 \uC0C8 \uC6CC\uD06C\uC544\uC774\uD15C\uC744 \uC2DC\uC791\uD558\uAE30 \uC804\uC5D0 awl work new --worktree \uB85C \uACA9\uB9AC\uD558\uB294 \uAC78 \uACE0\uB824\uD558\uC138\uC694.`
414
+ });
415
+ }
416
+ const configPath = path.join(projectRoot, ".awl", "config.json");
417
+ if (!exists(configPath)) {
418
+ checks.push({
419
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
420
+ name: "config.json",
421
+ status: "missing",
422
+ value: "\uC5C6\uC74C",
423
+ hint: INIT_HINT
424
+ });
425
+ return;
426
+ }
427
+ const raw = readJson(configPath);
428
+ if (!isAwlConfig(raw)) {
429
+ checks.push({
430
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
431
+ name: "config.json",
432
+ status: "fail",
433
+ value: "\uD615\uC2DD \uC624\uB958",
434
+ hint: "config.json \uD615\uC2DD\uC744 \uD655\uC778\uD558\uC138\uC694"
435
+ });
436
+ return;
437
+ }
438
+ checks.push({ group: "\uC774 \uD504\uB85C\uC81D\uD2B8", name: "config.json", status: "ok", value: "\uC788\uC74C" });
439
+ const recordedNaming = typeof raw.namingConvention === "string" ? raw.namingConvention : void 0;
440
+ if (recordedNaming) {
441
+ checks.push({
442
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
443
+ name: "\uB124\uC774\uBC0D \uCEE8\uBCA4\uC158",
444
+ status: "info",
445
+ value: recordedNaming
446
+ });
447
+ } else {
448
+ const naming = detectNamingConvention(
449
+ listSourceFiles(projectRoot).map((p) => path.basename(p))
450
+ );
451
+ if (naming.reason === "detected" && naming.convention) {
452
+ checks.push({
453
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
454
+ name: "\uB124\uC774\uBC0D \uCEE8\uBCA4\uC158",
455
+ status: "info",
456
+ value: `${naming.convention} (${naming.counts[naming.convention]}/${naming.decisiveTotal} \uD30C\uC77C, \uBBF8\uAE30\uB85D)`,
457
+ hint: `config.json \uC5D0 \uAE30\uB85D\uD558\uB824\uBA74: awl config set namingConvention ${naming.convention}`
458
+ });
459
+ } else if (naming.reason === "mixed") {
460
+ checks.push({ group: "\uC774 \uD504\uB85C\uC81D\uD2B8", name: "\uB124\uC774\uBC0D \uCEE8\uBCA4\uC158", status: "info", value: "\uD63C\uC7AC" });
461
+ } else {
462
+ checks.push({
463
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
464
+ name: "\uB124\uC774\uBC0D \uCEE8\uBCA4\uC158",
465
+ status: "info",
466
+ value: "\uD310\uB2E8 \uBCF4\uB958 (\uD30C\uC77C \uBD80\uC871)"
467
+ });
468
+ }
469
+ }
470
+ const sizeReport = computeFileSizeOutliers(countFileLines(projectRoot));
471
+ if (sizeReport.threshold === null) {
472
+ checks.push({
473
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
474
+ name: "\uD30C\uC77C \uD06C\uAE30",
475
+ status: "info",
476
+ value: "\uD310\uB2E8 \uBCF4\uB958 (\uD30C\uC77C \uBD80\uC871)"
477
+ });
478
+ } else if (sizeReport.outliers.length === 0) {
479
+ checks.push({ group: "\uC774 \uD504\uB85C\uC81D\uD2B8", name: "\uD30C\uC77C \uD06C\uAE30", status: "ok", value: "\uC774\uC0C1\uCE58 \uC5C6\uC74C" });
480
+ } else {
481
+ checks.push({
482
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
483
+ name: "\uD30C\uC77C \uD06C\uAE30",
484
+ status: "warn",
485
+ value: `\uC774\uC0C1\uCE58 ${sizeReport.outliers.length}\uAC1C (\uC784\uACC4\uAC12 ${Math.round(sizeReport.threshold)}\uC904)`,
486
+ hint: `${sizeReport.outliers.slice(0, 5).map((o) => `${o.path}(${o.lines}\uC904)`).join(
487
+ ", "
488
+ )}${sizeReport.outliers.length > 5 ? " \uB4F1" : ""} \u2014 \uB9AC\uD329\uD130 \uD6C4\uBCF4\uB85C \uACE0\uB824\uD574\uBCFC \uB9CC\uD569\uB2C8\uB2E4(\uAC15\uC81C \uC544\uB2D8).`
489
+ });
490
+ }
491
+ const installed = installedEngineVersion();
492
+ if (installed !== null) {
493
+ const projectMismatch = findMismatch(versionResult, "project-vs-engine");
494
+ checks.push({
495
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
496
+ name: "\uC5D4\uC9C4 \uBC84\uC804 \uC77C\uCE58",
497
+ status: projectMismatch ? "warn" : "ok",
498
+ value: projectMismatch ? `config ${raw.engineVersion} / \uC124\uCE58 ${installed}` : raw.engineVersion,
499
+ hint: projectMismatch?.hint
500
+ });
501
+ }
502
+ for (const [vname, spec] of Object.entries(raw.verify ?? {})) {
503
+ if (!spec || typeof spec.cmd !== "string") {
504
+ continue;
505
+ }
506
+ const first = tokenize(spec.cmd)[0] ?? "";
507
+ if (!first) {
508
+ checks.push({
509
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
510
+ name: `\uAC80\uC99D: ${vname}`,
511
+ status: "warn",
512
+ value: "\uBA85\uB839 \uBE44\uC5B4 \uC788\uC74C"
513
+ });
514
+ continue;
515
+ }
516
+ const cwd = spec.cwd ? path.isAbsolute(spec.cwd) ? spec.cwd : path.join(projectRoot, spec.cwd) : void 0;
517
+ if (cwd && !isDirectory(cwd)) {
518
+ checks.push({
519
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
520
+ name: `\uAC80\uC99D: ${vname}`,
521
+ status: "missing",
522
+ value: "cwd \uC5C6\uC74C",
523
+ hint: `cwd \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4: ${spec.cwd}`
524
+ });
525
+ continue;
526
+ }
527
+ try {
528
+ await run({ cmd: first, args: ["--version"], cwd, timeoutMs: 3e3 });
529
+ checks.push({
530
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
531
+ name: `\uAC80\uC99D: ${vname}`,
532
+ status: "ok",
533
+ value: `${first} \uC2E4\uD589 \uAC00\uB2A5`
534
+ });
535
+ } catch (e) {
536
+ if (e instanceof CommandNotFoundError) {
537
+ checks.push({
538
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
539
+ name: `\uAC80\uC99D: ${vname}`,
540
+ status: "missing",
541
+ value: "\uBA85\uB839 \uC5C6\uC74C",
542
+ hint: `\uBA85\uB839\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.command}`
543
+ });
544
+ } else {
545
+ checks.push({
546
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
547
+ name: `\uAC80\uC99D: ${vname}`,
548
+ status: "warn",
549
+ value: "\uD655\uC778 \uC2E4\uD328",
550
+ hint: "\uAC80\uC99D \uBA85\uB839\uC744 \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4"
551
+ });
552
+ }
553
+ }
554
+ }
555
+ const statePath = path.join(projectRoot, ".awl", "state.json");
556
+ if (!exists(statePath)) {
557
+ checks.push({
558
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
559
+ name: "state.json",
560
+ status: "info",
561
+ value: "\uC5C6\uC74C (\uC544\uC9C1 \uC2DC\uC791 \uC804)"
562
+ });
563
+ } else {
564
+ const st = readJson(statePath);
565
+ if (st && typeof st === "object") {
566
+ const o = st;
567
+ const pos = typeof o.phase === "string" && o.phase || typeof o.step === "string" && o.step || typeof o.position === "string" && o.position || "\uC788\uC74C";
568
+ checks.push({ group: "\uC774 \uD504\uB85C\uC81D\uD2B8", name: "state.json", status: "ok", value: String(pos) });
569
+ } else {
570
+ checks.push({ group: "\uC774 \uD504\uB85C\uC81D\uD2B8", name: "state.json", status: "warn", value: "\uD615\uC2DD \uC624\uB958" });
571
+ }
572
+ let stateBytes = null;
573
+ try {
574
+ stateBytes = fs.statSync(statePath).size;
575
+ } catch {
576
+ stateBytes = null;
577
+ }
578
+ if (stateBytes !== null && stateBytes > STATE_SIZE_WARN_BYTES) {
579
+ checks.push({
580
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
581
+ name: "state.json \uD06C\uAE30",
582
+ status: "warn",
583
+ value: `${(stateBytes / (1024 * 1024)).toFixed(1)}MB`,
584
+ hint: "state.json \uC774 \uBE44\uC815\uC0C1\uC801\uC73C\uB85C \uD07D\uB2C8\uB2E4 \u2014 commit --start \uAC00 \uC800\uC7A5\uD558\uB294 untracked \uC2A4\uB0C5\uC0F7 \uB204\uC801\uC774 \uC6D0\uC778\uC77C \uC218 \uC788\uC2B5\uB2C8\uB2E4. .awl-worktrees/ \uB97C .gitignore \uC5D0 \uB123\uACE0(awl init \uC7AC\uC2E4\uD589) \uC644\uB8CC\uB41C \uC6CC\uD06C\uC544\uC774\uD15C\uC744 \uC815\uB9AC\uD558\uC138\uC694."
585
+ });
586
+ }
587
+ }
588
+ const worktreesDir = path.join(projectRoot, ".awl-worktrees");
589
+ let leftoverWorktrees = [];
590
+ try {
591
+ leftoverWorktrees = fs.readdirSync(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
592
+ } catch {
593
+ }
594
+ if (leftoverWorktrees.length > 0) {
595
+ const shown = leftoverWorktrees.slice(0, 3).join(", ");
596
+ checks.push({
597
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
598
+ name: "\uC6CC\uD06C\uD2B8\uB9AC \uC794\uC874",
599
+ status: "warn",
600
+ value: `${leftoverWorktrees.length}\uAC1C`,
601
+ hint: `.awl-worktrees/ \uC5D0 \uC6CC\uD06C\uD2B8\uB9AC\uAC00 \uB0A8\uC544 \uC788\uC2B5\uB2C8\uB2E4(${shown}${leftoverWorktrees.length > 3 ? " \uB4F1" : ""}). \uC644\uB8CC\uB41C \uC6CC\uD06C\uC544\uC774\uD15C\uC774\uBA74 git worktree remove \uB85C \uC815\uB9AC\uD558\uC138\uC694.`
602
+ });
603
+ }
604
+ const trailProjectName = loadProjectName(projectRoot);
605
+ if (trailProjectName) {
606
+ const trailState = loadState(projectRoot);
607
+ const activeWorkitem = typeof trailState.workitem === "string" && trailState.workitem.trim() !== "" ? trailState.workitem : null;
608
+ const gateAttemptRecords = readRecords().filter(
609
+ (r) => r.project === trailProjectName && (r.type === "gate" || r.type === "attempt")
610
+ ).length;
611
+ const hasCommits = await gitHasCommits(projectRoot);
612
+ if (detectRecordTrailGap({ hasCommits, activeWorkitem, gateAttemptRecords })) {
613
+ checks.push({
614
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
615
+ name: "record \uD2B8\uB808\uC77C",
616
+ status: "warn",
617
+ value: "\uACF5\uBC31 (\uD65C\uC131 \uC6CC\uD06C\uC544\uC774\uD15C \uC5C6\uC774 \uCEE4\uBC0B \uC774\uB825)",
618
+ hint: "\uCD5C\uADFC \uCEE4\uBC0B\uC5D0 \uB300\uC751\uD558\uB294 record(gate/attempt)\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 /awl-loop(work new\u2192gate\u2192commit)\uB85C \uD310\uB2E8 \uADFC\uAC70\uB97C \uB0A8\uAE30\uC138\uC694."
619
+ });
620
+ }
621
+ }
622
+ try {
623
+ const recs = readRecords();
624
+ const lastAt = recs.length > 0 ? String(recs[0]?.at ?? "") : "";
625
+ let stateMtime = "";
626
+ try {
627
+ stateMtime = fs.statSync(statePath).mtime.toISOString();
628
+ } catch {
629
+ }
630
+ const lock = readStateLock(projectRoot);
631
+ if (lastAt || stateMtime || lock) {
632
+ const fmt = (iso) => iso ? iso.slice(0, 16).replace("T", " ") : "\uC5C6\uC74C";
633
+ const lockNote = lock ? ` \xB7 \uB2E4\uB978 \uC138\uC158\uC774 state \uC4F0\uB294 \uC911(${lock.token})` : "";
634
+ checks.push({
635
+ group: "\uC774 \uD504\uB85C\uC81D\uD2B8",
636
+ name: "\uCD5C\uADFC \uD65C\uB3D9",
637
+ status: lock ? "warn" : "info",
638
+ value: `\uAE30\uB85D ${fmt(lastAt)} \xB7 state ${fmt(stateMtime)}${lockNote}`,
639
+ hint: lock ? `\uC9C0\uAE08 \uB2E4\uB978 awl \uD504\uB85C\uC138\uC2A4(${lock.token})\uAC00 state \uB97C \uC4F0\uB294 \uC911\uC785\uB2C8\uB2E4(.awl/state.lock). \uBCD1\uB82C \uC138\uC158\uC774\uBA74 AWL_HOME \uC744 \uBD84\uB9AC\uD558\uC138\uC694.` : "\uC5EC\uB7EC \uC138\uC158\uC774 \uAC19\uC740 \uD504\uB85C\uC81D\uD2B8\uC5D0\uC11C awl \uC744 \uB3CC\uB9AC\uBA74 records(\uC804\uC5ED \uACF5\uC720)\xB7state \uAC00 \uB4A4\uC11E\uC77C \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uBCD1\uB82C\uB85C \uC791\uC5C5 \uC911\uC774\uBA74 AWL_HOME \uC744 \uBD84\uB9AC\uD558\uC138\uC694."
640
+ });
641
+ }
642
+ } catch {
643
+ }
644
+ }
645
+ function collectAgents(checks, base, versionResult) {
646
+ const claudeDir = path.join(base, ".claude");
647
+ checks.push({
648
+ group: "\uC5D0\uC774\uC804\uD2B8",
649
+ name: "Claude Code",
650
+ status: "info",
651
+ value: exists(claudeDir) ? "\uAC10\uC9C0\uB428 (.claude/ \uC788\uC74C)" : "\uC5C6\uC74C"
652
+ });
653
+ const agentsMd = path.join(base, "AGENTS.md");
654
+ checks.push({
655
+ group: "\uC5D0\uC774\uC804\uD2B8",
656
+ name: "Codex",
657
+ status: "info",
658
+ value: exists(agentsMd) ? "\uAC10\uC9C0\uB428 (AGENTS.md \uC788\uC74C)" : "\uC5C6\uC74C"
659
+ });
660
+ const skillDir = path.join(base, ".claude", "skills", "awl-loop");
661
+ const claudeInstalled = exists(skillDir);
662
+ const claudeMismatch = findMismatch(versionResult, "claude-skill-vs-engine");
663
+ checks.push({
664
+ group: "\uC5D0\uC774\uC804\uD2B8",
665
+ name: "Claude \uC2A4\uD0AC \uBC84\uC804",
666
+ status: !claudeInstalled ? "warn" : claudeMismatch ? "warn" : "ok",
667
+ value: !claudeInstalled ? "\uC124\uCE58 \uC548 \uB428" : claudeMismatch ? claudeMismatch.a : "\uC5D4\uC9C4\uACFC \uC77C\uCE58",
668
+ hint: !claudeInstalled ? "awl init \uC5D0\uC11C \uC124\uCE58\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4" : claudeMismatch?.hint
669
+ });
670
+ const codexInstalled = exists(agentsMd) && fs.readFileSync(agentsMd, "utf8").includes("awl-loop:start");
671
+ const codexMismatch = findMismatch(versionResult, "codex-skill-vs-engine");
672
+ checks.push({
673
+ group: "\uC5D0\uC774\uC804\uD2B8",
674
+ name: "Codex \uC2A4\uD0AC \uBC84\uC804",
675
+ status: !codexInstalled ? "warn" : codexMismatch ? "warn" : "ok",
676
+ value: !codexInstalled ? "\uC124\uCE58 \uC548 \uB428" : codexMismatch ? codexMismatch.a : "\uC5D4\uC9C4\uACFC \uC77C\uCE58",
677
+ hint: !codexInstalled ? "awl init \uC5D0\uC11C \uC124\uCE58\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4" : codexMismatch?.hint
678
+ });
679
+ }
680
+ function findMismatch(result, kind) {
681
+ return result.mismatches.find((m) => m.kind === kind);
682
+ }
683
+ async function collectChecks() {
684
+ const checks = [];
685
+ let projectRoot = null;
686
+ try {
687
+ projectRoot = findProjectRoot();
688
+ } catch {
689
+ projectRoot = null;
690
+ }
691
+ const versionResult = checkVersions(gatherVersionInputs(projectRoot));
692
+ collectEnv(checks);
693
+ collectGlobal(checks, versionResult);
694
+ await collectProject(checks, projectRoot, versionResult);
695
+ collectAgents(checks, projectRoot ?? process.cwd(), versionResult);
696
+ const problems = checks.filter((c) => c.status === "missing" || c.status === "fail");
697
+ return { ok: problems.length === 0, checks };
698
+ }
699
+ function renderText(report, c) {
700
+ const color = makeColors(c.color);
701
+ const t = makeTokens(c);
702
+ const { checks } = report;
703
+ const s = makeSymbols(c);
704
+ const statusText = (ch) => {
705
+ const hint = clipToWidth(ch.hint ?? "", 52);
706
+ switch (ch.status) {
707
+ case "ok":
708
+ return signal(c, "ok");
709
+ case "missing":
710
+ case "fail":
711
+ return `${signal(c, "error")} ${hint}`;
712
+ case "warn":
713
+ return `${signal(c, "warn")} ${hint}`;
714
+ default:
715
+ return "";
716
+ }
717
+ };
718
+ const lines = [];
719
+ const maxWidth = c.tty ? Math.max(56, Math.min(96, (process.stdout.columns ?? 100) - 4)) : 96;
720
+ const groups = [...new Set(checks.map((ch) => ch.group))];
721
+ for (const group of groups) {
722
+ lines.push(color.bold(group));
723
+ const groupChecks = checks.filter((x) => x.group === group);
724
+ for (let i = 0; i < groupChecks.length; i++) {
725
+ const ch = groupChecks[i];
726
+ const status = statusText(ch);
727
+ const branch = i === groupChecks.length - 1 ? s.lastBranch : s.branch;
728
+ const base = `${branch} ${ch.name}: ${t.emphasis(ch.value ?? "(\uC5C6\uC74C)")}`;
729
+ let line = clipToWidth(
730
+ base,
731
+ status ? Math.max(24, maxWidth - visibleWidth(status) - 2) : maxWidth
732
+ );
733
+ if (status) {
734
+ line += ` ${status}`;
735
+ }
736
+ lines.push(line);
737
+ }
738
+ lines.push("");
739
+ }
740
+ const problems = report.checks.filter((ch) => ch.status === "missing" || ch.status === "fail");
741
+ if (problems.length === 0) {
742
+ lines.push(`${signal(c, "ok")} \uBAA8\uB450 \uC815\uC0C1\uC785\uB2C8\uB2E4.`);
743
+ } else {
744
+ const action = problems.some((p) => (p.hint ?? "").includes("init")) ? "awl init \uC744 \uC2E4\uD589\uD558\uC138\uC694." : "\uC704 \uC548\uB0B4\uB97C \uD655\uC778\uD558\uC138\uC694.";
745
+ lines.push(`${signal(c, "error")} ${color.red(`\uBB38\uC81C ${problems.length}\uAC1C.`)} ${action}`);
746
+ }
747
+ return card("Agent Work Loop \xB7 \uC9C4\uB2E8", lines, c);
748
+ }
749
+ async function runDoctor(opts) {
750
+ const report = await collectChecks();
751
+ if (opts.json) {
752
+ process.stdout.write(`${JSON.stringify(report, null, 2)}
753
+ `);
754
+ } else {
755
+ process.stdout.write(`${renderText(report, caps())}
756
+ `);
757
+ }
758
+ process.exit(report.ok ? 0 : 1);
759
+ }
760
+
761
+ export {
762
+ gitBranch,
763
+ gitDirtyFiles,
764
+ gitHasCommits,
765
+ detectRecordTrailGap,
766
+ detectNamingConvention,
767
+ computeFileSizeOutliers,
768
+ collectChecks,
769
+ renderText,
770
+ runDoctor
771
+ };