@ai-content-space/loopx 0.1.1 → 0.1.3

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 (68) hide show
  1. package/README.md +343 -56
  2. package/README.zh-CN.md +392 -0
  3. package/package.json +4 -1
  4. package/plugins/loopx/.codex-plugin/plugin.json +1 -1
  5. package/plugins/loopx/scripts/plugin-install.test.mjs +1 -0
  6. package/plugins/loopx/skills/archive/SKILL.md +39 -0
  7. package/plugins/loopx/skills/build/SKILL.md +111 -9
  8. package/plugins/loopx/skills/clarify/SKILL.md +121 -1
  9. package/plugins/loopx/skills/debug/SKILL.md +296 -0
  10. package/plugins/loopx/skills/debug/condition-based-waiting.md +115 -0
  11. package/plugins/loopx/skills/debug/defense-in-depth.md +122 -0
  12. package/plugins/loopx/skills/debug/find-polluter.sh +63 -0
  13. package/plugins/loopx/skills/debug/root-cause-tracing.md +169 -0
  14. package/plugins/loopx/skills/go-style/SKILL.md +71 -0
  15. package/plugins/loopx/skills/kratos/SKILL.md +74 -0
  16. package/plugins/loopx/skills/kratos/references/advanced-features.md +314 -0
  17. package/plugins/loopx/skills/kratos/references/architecture.md +488 -0
  18. package/plugins/loopx/skills/kratos/references/configuration.md +399 -0
  19. package/plugins/loopx/skills/kratos/references/http-customization.md +512 -0
  20. package/plugins/loopx/skills/kratos/references/middleware-logging.md +400 -0
  21. package/plugins/loopx/skills/kratos/references/proto-api-design.md +432 -0
  22. package/plugins/loopx/skills/kratos/references/security-auth.md +411 -0
  23. package/plugins/loopx/skills/kratos/references/troubleshooting.md +385 -0
  24. package/plugins/loopx/skills/plan/SKILL.md +22 -2
  25. package/plugins/loopx/skills/review/SKILL.md +98 -1
  26. package/plugins/loopx/skills/tdd/SKILL.md +371 -0
  27. package/plugins/loopx/skills/tdd/testing-anti-patterns.md +299 -0
  28. package/plugins/loopx/skills/verify/SKILL.md +139 -0
  29. package/scripts/codex-stop-hook.mjs +71 -0
  30. package/scripts/codex-workflow-hook.mjs +153 -0
  31. package/skills/archive/SKILL.md +39 -0
  32. package/skills/build/SKILL.md +111 -9
  33. package/skills/clarify/SKILL.md +121 -1
  34. package/skills/debug/SKILL.md +296 -0
  35. package/skills/debug/condition-based-waiting.md +115 -0
  36. package/skills/debug/defense-in-depth.md +122 -0
  37. package/skills/debug/find-polluter.sh +63 -0
  38. package/skills/debug/root-cause-tracing.md +169 -0
  39. package/skills/go-style/SKILL.md +71 -0
  40. package/skills/kratos/SKILL.md +74 -0
  41. package/skills/kratos/references/advanced-features.md +314 -0
  42. package/skills/kratos/references/architecture.md +488 -0
  43. package/skills/kratos/references/configuration.md +399 -0
  44. package/skills/kratos/references/http-customization.md +512 -0
  45. package/skills/kratos/references/middleware-logging.md +400 -0
  46. package/skills/kratos/references/proto-api-design.md +432 -0
  47. package/skills/kratos/references/security-auth.md +411 -0
  48. package/skills/kratos/references/troubleshooting.md +385 -0
  49. package/skills/plan/SKILL.md +22 -2
  50. package/skills/review/SKILL.md +98 -1
  51. package/skills/tdd/SKILL.md +371 -0
  52. package/skills/tdd/testing-anti-patterns.md +299 -0
  53. package/skills/verify/SKILL.md +139 -0
  54. package/src/build-runtime.mjs +303 -26
  55. package/src/build-stop-gate.mjs +94 -0
  56. package/src/cli.mjs +51 -8
  57. package/src/codex-exec-runtime.mjs +105 -5
  58. package/src/context-manifest.mjs +172 -0
  59. package/src/install-discovery.mjs +352 -5
  60. package/src/next-skill.mjs +85 -0
  61. package/src/plan-runtime.mjs +100 -122
  62. package/src/review-runtime.mjs +378 -0
  63. package/src/runtime-maintenance.mjs +428 -14
  64. package/src/template-governance.mjs +223 -0
  65. package/src/workflow.mjs +1947 -118
  66. package/src/workspace-context.mjs +166 -0
  67. package/src/workspace-memory.mjs +69 -0
  68. package/templates/plan.md +6 -0
@@ -0,0 +1,378 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { readdir, stat, mkdir, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+
6
+ import { runCodexReviewJson } from './codex-exec-runtime.mjs';
7
+
8
+ const execFileAsync = promisify(execFile);
9
+ const DEFAULT_REVIEW_MODEL = 'gpt-5.4';
10
+ const MAX_DIFF_PROMPT_CHARS = 18000;
11
+
12
+ async function gitOutput(cwd, args) {
13
+ const { stdout } = await execFileAsync('git', args, {
14
+ cwd,
15
+ maxBuffer: 1024 * 1024 * 8,
16
+ });
17
+ return stdout.trim();
18
+ }
19
+
20
+ async function gitOutputAllowExit(cwd, args) {
21
+ try {
22
+ return await gitOutput(cwd, args);
23
+ } catch (error) {
24
+ return `${error?.stdout || ''}${error?.stderr || ''}`.trim();
25
+ }
26
+ }
27
+
28
+ async function isGitWorktree(cwd) {
29
+ try {
30
+ return (await gitOutput(cwd, ['rev-parse', '--is-inside-work-tree'])) === 'true';
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ export function parseChangedFiles(statusText) {
37
+ return statusText
38
+ .split('\n')
39
+ .map((line) => {
40
+ const match = /^(?:[ MADRCU?!]{1,2}\s+|[MADRCU?!]{1,2}\s+)(.+)$/.exec(line);
41
+ return match ? match[1].trim() : line.trim();
42
+ })
43
+ .map((file) => (file.includes(' -> ') ? file.split(' -> ').at(-1).trim() : file))
44
+ .filter((file) => file && !file.startsWith('.loopx/') && !file.startsWith('.codex-helper/') && !file.startsWith('.LoopX/'));
45
+ }
46
+
47
+ export function parseUntrackedFiles(statusText) {
48
+ return statusText
49
+ .split('\n')
50
+ .filter((line) => line.startsWith('?? '))
51
+ .map((line) => line.slice(3).trim())
52
+ .filter((file) => file && !file.startsWith('.loopx/') && !file.startsWith('.codex-helper/') && !file.startsWith('.LoopX/'));
53
+ }
54
+
55
+ async function expandUntrackedPath(cwd, file) {
56
+ const fullPath = join(cwd, file);
57
+ const info = await stat(fullPath);
58
+ if (!info.isDirectory()) {
59
+ return [file];
60
+ }
61
+ const entries = await readdir(fullPath, { withFileTypes: true });
62
+ const nested = await Promise.all(entries
63
+ .sort((left, right) => left.name.localeCompare(right.name))
64
+ .map((entry) => expandUntrackedPath(cwd, join(file, entry.name))));
65
+ return nested.flat();
66
+ }
67
+
68
+ export async function buildReviewDiffEvidence(cwd, statusText) {
69
+ const trackedDiff = await gitOutput(cwd, ['diff', 'HEAD', '--']);
70
+ const untrackedFiles = parseUntrackedFiles(statusText);
71
+ const untrackedDiffs = [];
72
+ for (const file of untrackedFiles) {
73
+ for (const expandedFile of await expandUntrackedPath(cwd, file)) {
74
+ untrackedDiffs.push(await gitOutputAllowExit(cwd, ['diff', '--no-index', '--', '/dev/null', expandedFile]));
75
+ }
76
+ }
77
+ return [trackedDiff, ...untrackedDiffs].filter(Boolean).join('\n\n');
78
+ }
79
+
80
+ function normalizeFinding(item) {
81
+ if (typeof item === 'string') {
82
+ return {
83
+ severity: 'medium',
84
+ file: null,
85
+ line: null,
86
+ message: item,
87
+ };
88
+ }
89
+ return {
90
+ severity: item?.severity || 'medium',
91
+ file: item?.file || null,
92
+ line: item?.line || null,
93
+ message: item?.message || item?.summary || '未提供具体说明。',
94
+ };
95
+ }
96
+
97
+ function normalizeArchitectureReview(raw = {}) {
98
+ const normalizedVerdict = normalizeToken(raw?.verdict || 'pass');
99
+ const verdict = normalizedVerdict === 'block'
100
+ ? 'block'
101
+ : normalizedVerdict === 'warn'
102
+ ? 'warn'
103
+ : 'pass';
104
+ const normalizedRollbackTarget = normalizeToken(raw?.rollbackTarget);
105
+ const rollbackTarget = ['build', 'plan', 'clarify'].includes(normalizedRollbackTarget) ? normalizedRollbackTarget : null;
106
+ return {
107
+ status: raw?.status || 'complete',
108
+ verdict,
109
+ summary: raw?.summary || (verdict === 'pass' ? '架构 smell 扫描通过。' : '架构 smell 扫描发现风险。'),
110
+ rollbackTarget,
111
+ findings: Array.isArray(raw?.findings) ? raw.findings.map(normalizeFinding) : [],
112
+ };
113
+ }
114
+
115
+ function normalizeToken(value) {
116
+ return String(value || '').trim().toLowerCase().replace(/[\s_]+/g, '-');
117
+ }
118
+
119
+ function normalizeCodeReview(raw, changedFiles) {
120
+ const normalizedVerdict = normalizeToken(raw?.verdict);
121
+ const verdict = normalizedVerdict === 'request-changes' ? 'request-changes' : 'approve';
122
+ const findings = Array.isArray(raw?.findings) ? raw.findings.map(normalizeFinding) : [];
123
+ const normalizedRollbackTarget = normalizeToken(raw?.rollbackTarget);
124
+ const rollbackTarget = ['build', 'plan', 'clarify'].includes(normalizedRollbackTarget) ? normalizedRollbackTarget : null;
125
+ return {
126
+ status: raw?.status || 'complete',
127
+ verdict,
128
+ summary: raw?.summary || (verdict === 'approve' ? '代码差异审查未发现阻断问题。' : '代码差异审查发现需要修改的问题。'),
129
+ rollbackTarget,
130
+ changedFiles,
131
+ findings,
132
+ };
133
+ }
134
+
135
+ export function reviewContextPromptLines(context) {
136
+ return [
137
+ `reviewContextManifestStatus: ${context.contextManifestStatus || 'fallback'}`,
138
+ `reviewContextManifestPath: ${context.contextManifestPath || ''}`,
139
+ `reviewContextManifestRows: ${JSON.stringify((context.contextManifestRows || []).map((row) => ({
140
+ kind: row.kind,
141
+ path: row.path,
142
+ reason: row.reason,
143
+ priority: row.priority,
144
+ })))}`,
145
+ ];
146
+ }
147
+
148
+ export function createDefaultReviewAdapter() {
149
+ return createRealReviewAdapter();
150
+ }
151
+
152
+ function truncateForPrompt(text, maxChars = MAX_DIFF_PROMPT_CHARS) {
153
+ const value = String(text || '');
154
+ if (value.length <= maxChars) {
155
+ return value;
156
+ }
157
+ return `${value.slice(0, maxChars)}\n[truncated ${value.length - maxChars} chars]`;
158
+ }
159
+
160
+ export function buildCodeReviewPrompt(context, changedFiles, diffCheck = '') {
161
+ const gitStatusShort = truncateForPrompt(context.gitStatusShort || '');
162
+ const gitDiffStat = truncateForPrompt(context.gitDiffStat || '');
163
+ const gitDiff = truncateForPrompt(context.gitDiff || '');
164
+ const gitDiffEvidencePath = context.gitDiffEvidencePath || '';
165
+ return [
166
+ `你是 loopx workflow "${context.slug}" 的独立 code reviewer。`,
167
+ '请审查当前 git 工作区相对 HEAD 的代码差异,包括 staged、unstaged 和 untracked 文件。',
168
+ gitDiffEvidencePath
169
+ ? '必须以本 prompt 中的当前 git status/diff 预览和完整 git diff evidence 文件为事实来源;不要把既有 review-report.md 或 review-support/code-review.json 当作当前事实来源。'
170
+ : '必须以本 prompt 中的当前 git status/diff 为事实来源;不要把既有 review-report.md 或 review-support/code-review.json 当作当前事实来源。',
171
+ '重点查找真实 bug、回归风险、遗漏测试、接口契约破坏、安全/数据一致性问题。不要因为风格偏好提出阻断意见。',
172
+ '不要修改文件,不要运行 build,不要补代码;只做 code review。',
173
+ '请返回纯 JSON,不要 markdown,结构必须是:',
174
+ '{',
175
+ ' "status": "complete" | "skipped",',
176
+ ' "verdict": "approve" | "request-changes",',
177
+ ' "summary": "中文摘要",',
178
+ ' "rollbackTarget": "build" | "plan" | "clarify" | null,',
179
+ ' "findings": [{"severity": "high" | "medium" | "low", "file": "相对路径", "line": number | null, "message": "中文问题说明"}]',
180
+ '}',
181
+ '',
182
+ '若发现 high 或 medium 级别的真实问题,verdict 必须是 "request-changes"。没有阻断问题时 verdict 为 "approve"。',
183
+ '当问题属于实现 bug、测试缺口或小范围契约修复时,rollbackTarget 用 "build"。',
184
+ '当问题说明计划本身错误、范围不清或架构方向需要调整时,rollbackTarget 用 "plan"。',
185
+ '当问题暴露需求仍不清楚时,rollbackTarget 用 "clarify"。',
186
+ '当 verdict 为 "approve" 时,rollbackTarget 必须为 null。',
187
+ '',
188
+ `executionRecordPath: ${context.executionRecordPath}`,
189
+ `planArtifactPath: ${context.planArtifactPath || ''}`,
190
+ `testSpecArtifactPath: ${context.testSpecArtifactPath || ''}`,
191
+ ...reviewContextPromptLines(context),
192
+ ...(gitDiffEvidencePath ? [
193
+ `完整 git diff evidence 文件: ${gitDiffEvidencePath}`,
194
+ '当前 prompt 中的 git diff 是紧凑预览;必须读取该文件后再给出 code review 结论。',
195
+ ] : []),
196
+ `changedFiles: ${JSON.stringify(changedFiles)}`,
197
+ '',
198
+ `当前 git status --short:\n${gitStatusShort || '(empty)'}`,
199
+ '',
200
+ `当前 git diff --stat:\n${gitDiffStat || '(empty)'}`,
201
+ '',
202
+ `当前 git diff -- HEAD:\n${gitDiff || '(empty)'}`,
203
+ '',
204
+ diffCheck ? `git diff --check output:\n${diffCheck}` : 'git diff --check output: clean',
205
+ ].join('\n');
206
+ }
207
+
208
+ export function buildArchitectureReviewPrompt(context, changedFiles) {
209
+ const gitStatusShort = truncateForPrompt(context.gitStatusShort || '');
210
+ const gitDiffStat = truncateForPrompt(context.gitDiffStat || '');
211
+ const gitDiff = truncateForPrompt(context.gitDiff || '');
212
+ const gitDiffEvidencePath = context.gitDiffEvidencePath || '';
213
+ return [
214
+ `你是 loopx workflow "${context.slug}" 的 architecture smell reviewer。`,
215
+ '这是 `$review` 内部的轻量架构检查 lane,不是新阶段,不要修改文件,不要运行 build。',
216
+ '你的目标是发现会影响长期可维护性、测试 seam、领域边界或 plan 架构假设落地的真实问题。',
217
+ '只在问题足够严重、需要回退 plan/build/clarify 时返回 verdict "block";普通建议用 "warn";没有实质问题用 "pass"。',
218
+ '',
219
+ '重点检查:',
220
+ '- 浅模块:接口复杂度接近或超过实现复杂度。',
221
+ '- 缺少稳定测试 seam:关键行为无法通过公共接口验证。',
222
+ '- 领域概念泄漏:同一领域规则散落在无关模块或跨层穿透。',
223
+ '- 重复规则:同一业务规则被多处复制实现。',
224
+ '- plan 架构假设与实际实现不一致。',
225
+ '',
226
+ '请返回纯 JSON,不要 markdown,结构必须是:',
227
+ '{',
228
+ ' "status": "complete" | "skipped",',
229
+ ' "verdict": "pass" | "warn" | "block",',
230
+ ' "summary": "中文摘要",',
231
+ ' "rollbackTarget": "build" | "plan" | "clarify" | null,',
232
+ ' "findings": [{"severity": "high" | "medium" | "low", "file": "相对路径", "line": number | null, "message": "中文问题说明"}]',
233
+ '}',
234
+ '',
235
+ 'verdict 为 "block" 时 rollbackTarget 必须非 null。',
236
+ '实现边界或测试 seam 可局部修复时 rollbackTarget 用 "build"。',
237
+ '计划模块 seam、架构方向或 slice 拆解错误时 rollbackTarget 用 "plan"。',
238
+ '领域语言或需求边界仍不清楚时 rollbackTarget 用 "clarify"。',
239
+ '',
240
+ `executionRecordPath: ${context.executionRecordPath}`,
241
+ `planArtifactPath: ${context.planArtifactPath || ''}`,
242
+ `testSpecArtifactPath: ${context.testSpecArtifactPath || ''}`,
243
+ `changeArtifactPaths: ${JSON.stringify(context.changeArtifactPaths || {})}`,
244
+ ...reviewContextPromptLines(context),
245
+ ...(gitDiffEvidencePath ? [
246
+ `完整 git diff evidence 文件: ${gitDiffEvidencePath}`,
247
+ '当前 prompt 中的 git diff 是紧凑预览;必须读取该文件后再判断是否存在架构 smell。',
248
+ ] : []),
249
+ `changedFiles: ${JSON.stringify(changedFiles)}`,
250
+ '',
251
+ `当前 git status --short:\n${gitStatusShort || '(empty)'}`,
252
+ '',
253
+ `当前 git diff --stat:\n${gitDiffStat || '(empty)'}`,
254
+ '',
255
+ `当前 git diff -- HEAD:\n${gitDiff || '(empty)'}`,
256
+ ].join('\n');
257
+ }
258
+
259
+ export function createRealReviewAdapter({ model, codexReviewJson = runCodexReviewJson } = {}) {
260
+ return {
261
+ async codeReview(context) {
262
+ if (!(await isGitWorktree(context.cwd))) {
263
+ return {
264
+ status: 'skipped',
265
+ verdict: 'approve',
266
+ summary: '当前目录不是 git 工作区,已跳过代码差异审查。',
267
+ changedFiles: [],
268
+ findings: [],
269
+ };
270
+ }
271
+
272
+ const statusText = await gitOutput(context.cwd, ['status', '--short']);
273
+ const changedFiles = parseChangedFiles(statusText);
274
+ if (changedFiles.length === 0) {
275
+ return {
276
+ status: 'complete',
277
+ verdict: 'approve',
278
+ summary: '未检测到需要审查的代码差异。',
279
+ changedFiles: [],
280
+ findings: [],
281
+ };
282
+ }
283
+
284
+ let diffCheck = '';
285
+ try {
286
+ await gitOutput(context.cwd, ['diff', '--check', 'HEAD', '--']);
287
+ } catch (error) {
288
+ diffCheck = error?.stdout || error?.stderr || error?.message || String(error);
289
+ }
290
+ const gitDiffStat = await gitOutput(context.cwd, ['diff', '--stat', 'HEAD', '--']);
291
+ const gitDiff = await buildReviewDiffEvidence(context.cwd, statusText);
292
+
293
+ await mkdir(join(context.root, 'review-support'), { recursive: true });
294
+ const outputPath = join(context.root, 'review-support', 'code-review.raw.json');
295
+ const gitDiffEvidencePath = join(context.root, 'review-support', 'code-review-diff.patch');
296
+ await writeFile(gitDiffEvidencePath, gitDiff || '');
297
+ const prompt = buildCodeReviewPrompt({
298
+ ...context,
299
+ gitStatusShort: statusText,
300
+ gitDiffStat,
301
+ gitDiff,
302
+ gitDiffEvidencePath,
303
+ }, changedFiles, diffCheck);
304
+
305
+ const raw = await codexReviewJson({
306
+ cwd: context.cwd,
307
+ prompt,
308
+ outputPath,
309
+ model,
310
+ reviewMode: true,
311
+ uncommitted: true,
312
+ });
313
+ return normalizeCodeReview(raw, changedFiles);
314
+ },
315
+ async architectureReview(context) {
316
+ if (!(await isGitWorktree(context.cwd))) {
317
+ return normalizeArchitectureReview({
318
+ status: 'skipped',
319
+ verdict: 'pass',
320
+ summary: '当前目录不是 git 工作区,已跳过架构 smell 扫描。',
321
+ findings: [],
322
+ });
323
+ }
324
+ const statusText = await gitOutput(context.cwd, ['status', '--short']);
325
+ const changedFiles = parseChangedFiles(statusText);
326
+ if (changedFiles.length === 0) {
327
+ return normalizeArchitectureReview({
328
+ status: 'complete',
329
+ verdict: 'pass',
330
+ summary: '未检测到代码差异,架构 smell 扫描通过。',
331
+ findings: [],
332
+ });
333
+ }
334
+ const gitDiffStat = await gitOutput(context.cwd, ['diff', '--stat', 'HEAD', '--']);
335
+ const gitDiff = await buildReviewDiffEvidence(context.cwd, statusText);
336
+ await mkdir(join(context.root, 'review-support'), { recursive: true });
337
+ const outputPath = join(context.root, 'review-support', 'architecture-smell.raw.json');
338
+ const gitDiffEvidencePath = join(context.root, 'review-support', 'code-review-diff.patch');
339
+ const prompt = buildArchitectureReviewPrompt({
340
+ ...context,
341
+ gitStatusShort: statusText,
342
+ gitDiffStat,
343
+ gitDiff,
344
+ gitDiffEvidencePath,
345
+ }, changedFiles);
346
+ const raw = await codexReviewJson({
347
+ cwd: context.cwd,
348
+ prompt,
349
+ outputPath,
350
+ model,
351
+ reviewMode: true,
352
+ uncommitted: true,
353
+ });
354
+ return normalizeArchitectureReview(raw);
355
+ },
356
+ };
357
+ }
358
+
359
+ export function createScriptedReviewAdapter(script = {}) {
360
+ return {
361
+ async codeReview() {
362
+ return normalizeCodeReview(script.codeReview || {
363
+ status: 'complete',
364
+ verdict: 'approve',
365
+ summary: '脚本化 code review 通过。',
366
+ findings: [],
367
+ }, script.changedFiles || []);
368
+ },
369
+ async architectureReview() {
370
+ return normalizeArchitectureReview(script.architectureReview || {
371
+ status: 'complete',
372
+ verdict: 'pass',
373
+ summary: '架构 smell 扫描通过。',
374
+ findings: [],
375
+ });
376
+ },
377
+ };
378
+ }