@ai-content-space/loopx 0.1.2 → 0.1.4
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 +422 -57
- package/README.zh-CN.md +485 -0
- package/assets/logo.svg +89 -0
- package/package.json +5 -1
- package/plugins/loopx/.codex-plugin/plugin.json +1 -1
- package/plugins/loopx/scripts/plugin-install.test.mjs +14 -0
- package/plugins/loopx/skills/archive/SKILL.md +49 -0
- package/plugins/loopx/skills/build/SKILL.md +111 -9
- package/plugins/loopx/skills/clarify/SKILL.md +129 -8
- package/plugins/loopx/skills/debug/SKILL.md +296 -0
- package/plugins/loopx/skills/debug/condition-based-waiting.md +115 -0
- package/plugins/loopx/skills/debug/defense-in-depth.md +122 -0
- package/plugins/loopx/skills/debug/find-polluter.sh +63 -0
- package/plugins/loopx/skills/debug/root-cause-tracing.md +169 -0
- package/plugins/loopx/skills/go-style/SKILL.md +71 -0
- package/plugins/loopx/skills/kratos/SKILL.md +74 -0
- package/plugins/loopx/skills/kratos/references/advanced-features.md +314 -0
- package/plugins/loopx/skills/kratos/references/architecture.md +488 -0
- package/plugins/loopx/skills/kratos/references/configuration.md +399 -0
- package/plugins/loopx/skills/kratos/references/http-customization.md +512 -0
- package/plugins/loopx/skills/kratos/references/middleware-logging.md +400 -0
- package/plugins/loopx/skills/kratos/references/proto-api-design.md +432 -0
- package/plugins/loopx/skills/kratos/references/security-auth.md +411 -0
- package/plugins/loopx/skills/kratos/references/troubleshooting.md +385 -0
- package/plugins/loopx/skills/plan/SKILL.md +24 -3
- package/plugins/loopx/skills/review/SKILL.md +98 -1
- package/plugins/loopx/skills/tdd/SKILL.md +371 -0
- package/plugins/loopx/skills/tdd/testing-anti-patterns.md +299 -0
- package/plugins/loopx/skills/verify/SKILL.md +139 -0
- package/scripts/codex-stop-hook.mjs +71 -0
- package/scripts/codex-workflow-hook.mjs +248 -0
- package/skills/archive/SKILL.md +49 -0
- package/skills/build/SKILL.md +111 -9
- package/skills/clarify/SKILL.md +129 -8
- package/skills/debug/SKILL.md +296 -0
- package/skills/debug/condition-based-waiting.md +115 -0
- package/skills/debug/defense-in-depth.md +122 -0
- package/skills/debug/find-polluter.sh +63 -0
- package/skills/debug/root-cause-tracing.md +169 -0
- package/skills/go-style/SKILL.md +71 -0
- package/skills/kratos/SKILL.md +74 -0
- package/skills/kratos/references/advanced-features.md +314 -0
- package/skills/kratos/references/architecture.md +488 -0
- package/skills/kratos/references/configuration.md +399 -0
- package/skills/kratos/references/http-customization.md +512 -0
- package/skills/kratos/references/middleware-logging.md +400 -0
- package/skills/kratos/references/proto-api-design.md +432 -0
- package/skills/kratos/references/security-auth.md +411 -0
- package/skills/kratos/references/troubleshooting.md +385 -0
- package/skills/plan/SKILL.md +20 -3
- package/skills/review/SKILL.md +98 -1
- package/skills/tdd/SKILL.md +371 -0
- package/skills/tdd/testing-anti-patterns.md +299 -0
- package/skills/verify/SKILL.md +139 -0
- package/src/build-runtime.mjs +311 -26
- package/src/build-stop-gate.mjs +94 -0
- package/src/cli.mjs +57 -5
- package/src/codex-exec-runtime.mjs +105 -5
- package/src/context-manifest.mjs +172 -0
- package/src/html-views.mjs +316 -0
- package/src/install-discovery.mjs +352 -5
- package/src/next-skill.mjs +57 -5
- package/src/plan-runtime.mjs +102 -122
- package/src/review-runtime.mjs +558 -0
- package/src/runtime-maintenance.mjs +429 -14
- package/src/template-governance.mjs +223 -0
- package/src/workflow.mjs +2341 -120
- package/src/workspace-context.mjs +166 -0
- package/src/workspace-memory.mjs +69 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, join, relative } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { nextSkillCommand } from './next-skill.mjs';
|
|
6
|
+
import { statusSummary } from './workflow.mjs';
|
|
7
|
+
|
|
8
|
+
const WORKFLOW_ARTIFACTS = [
|
|
9
|
+
{ name: 'spec.md', label: '需求工作副本', page: 'intake.html' },
|
|
10
|
+
{ name: 'plan.md', label: '计划', page: 'plan.html' },
|
|
11
|
+
{ name: 'architecture.md', label: '架构', page: 'plan.html' },
|
|
12
|
+
{ name: 'development-plan.md', label: '开发计划', page: 'plan.html' },
|
|
13
|
+
{ name: 'test-plan.md', label: '测试计划', page: 'plan.html' },
|
|
14
|
+
{ name: 'execution-record.md', label: '执行记录', page: 'build.html' },
|
|
15
|
+
{ name: 'review-report.md', label: '评审报告', page: 'review.html' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const PAGE_GROUPS = [
|
|
19
|
+
{ file: 'intake.html', title: '需求澄清', artifacts: ['spec.md'] },
|
|
20
|
+
{ file: 'plan.html', title: '计划与架构', artifacts: ['plan.md', 'architecture.md', 'development-plan.md', 'test-plan.md'] },
|
|
21
|
+
{ file: 'build.html', title: '执行与验证', artifacts: ['execution-record.md'] },
|
|
22
|
+
{ file: 'review.html', title: '评审结论', artifacts: ['review-report.md'] },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function escapeHtml(value) {
|
|
26
|
+
return String(value ?? '')
|
|
27
|
+
.replaceAll('&', '&')
|
|
28
|
+
.replaceAll('<', '<')
|
|
29
|
+
.replaceAll('>', '>')
|
|
30
|
+
.replaceAll('"', '"')
|
|
31
|
+
.replaceAll("'", ''');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function htmlDoc({ title, body }) {
|
|
35
|
+
return [
|
|
36
|
+
'<!doctype html>',
|
|
37
|
+
'<html lang="zh-CN">',
|
|
38
|
+
'<head>',
|
|
39
|
+
' <meta charset="utf-8">',
|
|
40
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
41
|
+
` <title>${escapeHtml(title)}</title>`,
|
|
42
|
+
' <style>',
|
|
43
|
+
' :root { color-scheme: light; --text: #17202a; --muted: #5f6f7f; --line: #d8e0e8; --bg: #f6f8fa; --panel: #ffffff; --accent: #1769aa; }',
|
|
44
|
+
' * { box-sizing: border-box; }',
|
|
45
|
+
' body { margin: 0; font: 15px/1.65 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--text); background: var(--bg); }',
|
|
46
|
+
' main { max-width: 1080px; margin: 0 auto; padding: 28px 20px 48px; }',
|
|
47
|
+
' header { margin-bottom: 20px; }',
|
|
48
|
+
' h1 { margin: 0 0 8px; font-size: 28px; line-height: 1.25; }',
|
|
49
|
+
' h2 { margin: 24px 0 10px; font-size: 18px; }',
|
|
50
|
+
' h3 { margin: 18px 0 8px; font-size: 16px; }',
|
|
51
|
+
' a { color: var(--accent); text-decoration: none; }',
|
|
52
|
+
' a:hover { text-decoration: underline; }',
|
|
53
|
+
' .muted { color: var(--muted); }',
|
|
54
|
+
' .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }',
|
|
55
|
+
' .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 14px 16px; }',
|
|
56
|
+
' .badge { display: inline-block; border: 1px solid var(--line); border-radius: 999px; padding: 2px 9px; margin: 2px 4px 2px 0; color: var(--muted); background: #fff; font-size: 12px; }',
|
|
57
|
+
' table { width: 100%; border-collapse: collapse; background: var(--panel); border: 1px solid var(--line); }',
|
|
58
|
+
' th, td { padding: 8px 10px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }',
|
|
59
|
+
' th { color: var(--muted); font-weight: 600; }',
|
|
60
|
+
' pre { overflow: auto; padding: 12px; background: #0f1720; color: #edf4fb; border-radius: 8px; }',
|
|
61
|
+
' code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }',
|
|
62
|
+
' .markdown { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 16px; }',
|
|
63
|
+
' </style>',
|
|
64
|
+
'</head>',
|
|
65
|
+
'<body>',
|
|
66
|
+
'<main>',
|
|
67
|
+
body,
|
|
68
|
+
'</main>',
|
|
69
|
+
'</body>',
|
|
70
|
+
'</html>',
|
|
71
|
+
].join('\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function listItems(items) {
|
|
75
|
+
const values = Array.isArray(items) ? items.filter(Boolean) : [];
|
|
76
|
+
if (values.length === 0) {
|
|
77
|
+
return '<span class="muted">无</span>';
|
|
78
|
+
}
|
|
79
|
+
return values.map((item) => `<span class="badge">${escapeHtml(item)}</span>`).join(' ');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function markdownToHtml(markdown) {
|
|
83
|
+
const lines = String(markdown || '').split('\n');
|
|
84
|
+
const html = [];
|
|
85
|
+
let inCode = false;
|
|
86
|
+
let listOpen = false;
|
|
87
|
+
|
|
88
|
+
const closeList = () => {
|
|
89
|
+
if (listOpen) {
|
|
90
|
+
html.push('</ul>');
|
|
91
|
+
listOpen = false;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
if (line.startsWith('```')) {
|
|
97
|
+
if (inCode) {
|
|
98
|
+
html.push('</code></pre>');
|
|
99
|
+
inCode = false;
|
|
100
|
+
} else {
|
|
101
|
+
closeList();
|
|
102
|
+
html.push('<pre><code>');
|
|
103
|
+
inCode = true;
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (inCode) {
|
|
108
|
+
html.push(escapeHtml(line));
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (/^#{1,4}\s+/.test(line)) {
|
|
112
|
+
closeList();
|
|
113
|
+
const level = Math.min(4, line.match(/^#+/)?.[0].length || 2);
|
|
114
|
+
html.push(`<h${level}>${escapeHtml(line.replace(/^#{1,4}\s+/, ''))}</h${level}>`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (line.startsWith('- ')) {
|
|
118
|
+
if (!listOpen) {
|
|
119
|
+
html.push('<ul>');
|
|
120
|
+
listOpen = true;
|
|
121
|
+
}
|
|
122
|
+
html.push(`<li>${escapeHtml(line.slice(2))}</li>`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!line.trim()) {
|
|
126
|
+
closeList();
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
closeList();
|
|
130
|
+
html.push(`<p>${escapeHtml(line)}</p>`);
|
|
131
|
+
}
|
|
132
|
+
closeList();
|
|
133
|
+
if (inCode) {
|
|
134
|
+
html.push('</code></pre>');
|
|
135
|
+
}
|
|
136
|
+
return html.join('\n');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function artifactLink(viewRoot, artifactPath, label) {
|
|
140
|
+
const href = relative(viewRoot, artifactPath).replaceAll('\\', '/');
|
|
141
|
+
return `<a href="${escapeHtml(href)}">${escapeHtml(label)}</a>`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function statusPanels(status) {
|
|
145
|
+
const state = status.state || {};
|
|
146
|
+
const readiness = state.readiness || {};
|
|
147
|
+
const authorization = state.authorization || {};
|
|
148
|
+
const nextSkill = nextSkillCommand(state);
|
|
149
|
+
return [
|
|
150
|
+
'<section class="grid">',
|
|
151
|
+
`<div class="panel"><strong>阶段</strong><br>${escapeHtml(state.current_stage || '(none)')}</div>`,
|
|
152
|
+
`<div class="panel"><strong>状态</strong><br>${escapeHtml(state.stage_status || '(unknown)')}</div>`,
|
|
153
|
+
`<div class="panel"><strong>下一步</strong><br><code>${escapeHtml(nextSkill || status.next_action || 'none')}</code></div>`,
|
|
154
|
+
`<div class="panel"><strong>归档</strong><br>${escapeHtml(state.archive_status || 'pending')}</div>`,
|
|
155
|
+
'</section>',
|
|
156
|
+
'<section class="panel">',
|
|
157
|
+
'<h2>readiness / authorization</h2>',
|
|
158
|
+
'<table><thead><tr><th>关卡</th><th>ready</th><th>authorized</th><th>blockers</th></tr></thead><tbody>',
|
|
159
|
+
...['plan', 'build', 'review', 'done', 'archive'].map((key) => [
|
|
160
|
+
'<tr>',
|
|
161
|
+
`<td>${escapeHtml(key)}</td>`,
|
|
162
|
+
`<td>${escapeHtml(readiness[key]?.ready ?? false)}</td>`,
|
|
163
|
+
`<td>${escapeHtml(authorization[key]?.authorized ?? false)}</td>`,
|
|
164
|
+
`<td>${listItems(readiness[key]?.blockers || [])}</td>`,
|
|
165
|
+
'</tr>',
|
|
166
|
+
].join('')),
|
|
167
|
+
'</tbody></table>',
|
|
168
|
+
'</section>',
|
|
169
|
+
].join('\n');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function renderWorkflowPages(status) {
|
|
173
|
+
const root = status.root;
|
|
174
|
+
const viewRoot = join(root, 'view');
|
|
175
|
+
await mkdir(viewRoot, { recursive: true });
|
|
176
|
+
|
|
177
|
+
const artifactRows = WORKFLOW_ARTIFACTS.map((artifact) => {
|
|
178
|
+
const artifactPath = join(root, artifact.name);
|
|
179
|
+
return {
|
|
180
|
+
...artifact,
|
|
181
|
+
path: artifactPath,
|
|
182
|
+
exists: existsSync(artifactPath),
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
for (const group of PAGE_GROUPS) {
|
|
187
|
+
const sections = [];
|
|
188
|
+
for (const artifactName of group.artifacts) {
|
|
189
|
+
const artifact = artifactRows.find((item) => item.name === artifactName);
|
|
190
|
+
if (!artifact?.exists) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const text = await readFile(artifact.path, 'utf8');
|
|
194
|
+
sections.push([
|
|
195
|
+
`<section class="markdown">`,
|
|
196
|
+
`<p class="muted">${artifactLink(viewRoot, artifact.path, artifact.name)}</p>`,
|
|
197
|
+
markdownToHtml(text),
|
|
198
|
+
'</section>',
|
|
199
|
+
].join('\n'));
|
|
200
|
+
}
|
|
201
|
+
await writeFile(join(viewRoot, group.file), htmlDoc({
|
|
202
|
+
title: `${group.title} - ${status.slug}`,
|
|
203
|
+
body: [
|
|
204
|
+
'<header>',
|
|
205
|
+
`<h1>${escapeHtml(group.title)}</h1>`,
|
|
206
|
+
`<p class="muted">工作流:${escapeHtml(status.slug)}</p>`,
|
|
207
|
+
'<p><a href="index.html">返回工作流首页</a></p>',
|
|
208
|
+
'</header>',
|
|
209
|
+
sections.length > 0 ? sections.join('\n') : '<section class="panel muted">暂无对应产物。</section>',
|
|
210
|
+
].join('\n'),
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const indexBody = [
|
|
215
|
+
'<header>',
|
|
216
|
+
`<h1>工作流 ${escapeHtml(status.slug)}</h1>`,
|
|
217
|
+
`<p class="muted">HTML 是派生阅读视图;Markdown 和 JSON 仍是运行时事实源。</p>`,
|
|
218
|
+
'</header>',
|
|
219
|
+
statusPanels(status),
|
|
220
|
+
'<section class="panel">',
|
|
221
|
+
'<h2>关键产物</h2>',
|
|
222
|
+
'<table><thead><tr><th>产物</th><th>状态</th><th>阅读视图</th><th>原始文件</th></tr></thead><tbody>',
|
|
223
|
+
...artifactRows.map((artifact) => [
|
|
224
|
+
'<tr>',
|
|
225
|
+
`<td>${escapeHtml(artifact.label)}</td>`,
|
|
226
|
+
`<td>${artifact.exists ? '存在' : '缺失'}</td>`,
|
|
227
|
+
`<td>${artifact.exists ? `<a href="${escapeHtml(artifact.page)}">${escapeHtml(basename(artifact.page))}</a>` : '<span class="muted">无</span>'}</td>`,
|
|
228
|
+
`<td>${artifact.exists ? artifactLink(viewRoot, artifact.path, artifact.name) : '<span class="muted">无</span>'}</td>`,
|
|
229
|
+
'</tr>',
|
|
230
|
+
].join('')),
|
|
231
|
+
'</tbody></table>',
|
|
232
|
+
'</section>',
|
|
233
|
+
].join('\n');
|
|
234
|
+
|
|
235
|
+
const workflowViewPath = join(viewRoot, 'index.html');
|
|
236
|
+
await writeFile(workflowViewPath, htmlDoc({
|
|
237
|
+
title: `loopx 工作流 ${status.slug}`,
|
|
238
|
+
body: indexBody,
|
|
239
|
+
}));
|
|
240
|
+
|
|
241
|
+
return workflowViewPath;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function renderWorkspaceIndex(workspaceStatus, renderedSlugs = []) {
|
|
245
|
+
const viewsRoot = join(workspaceStatus.workspaceRoot, 'views');
|
|
246
|
+
await mkdir(viewsRoot, { recursive: true });
|
|
247
|
+
const rendered = new Set(renderedSlugs);
|
|
248
|
+
const rows = (workspaceStatus.workflows || []).map((workflow) => {
|
|
249
|
+
const href = `../workflows/${workflow.slug}/view/index.html`;
|
|
250
|
+
const link = rendered.has(workflow.slug)
|
|
251
|
+
? `<a href="${escapeHtml(href)}">${escapeHtml(workflow.slug)}</a>`
|
|
252
|
+
: escapeHtml(workflow.slug);
|
|
253
|
+
return [
|
|
254
|
+
'<tr>',
|
|
255
|
+
`<td>${link}</td>`,
|
|
256
|
+
`<td>${escapeHtml(workflow.current_stage || '(none)')}</td>`,
|
|
257
|
+
`<td>${escapeHtml(workflow.contract)}</td>`,
|
|
258
|
+
`<td>${escapeHtml(workflow.missing_artifact_count)}</td>`,
|
|
259
|
+
'</tr>',
|
|
260
|
+
].join('');
|
|
261
|
+
});
|
|
262
|
+
const workspaceViewPath = join(viewsRoot, 'index.html');
|
|
263
|
+
await writeFile(workspaceViewPath, htmlDoc({
|
|
264
|
+
title: 'loopx 工作台',
|
|
265
|
+
body: [
|
|
266
|
+
'<header>',
|
|
267
|
+
'<h1>loopx 工作台</h1>',
|
|
268
|
+
`<p class="muted">工作区:${escapeHtml(workspaceStatus.workspaceRoot)}</p>`,
|
|
269
|
+
'</header>',
|
|
270
|
+
'<section class="panel">',
|
|
271
|
+
'<h2>工作流</h2>',
|
|
272
|
+
'<table><thead><tr><th>工作流</th><th>阶段</th><th>契约</th><th>缺失产物数</th></tr></thead><tbody>',
|
|
273
|
+
rows.join('\n') || '<tr><td colspan="4" class="muted">暂无工作流。</td></tr>',
|
|
274
|
+
'</tbody></table>',
|
|
275
|
+
'</section>',
|
|
276
|
+
].join('\n'),
|
|
277
|
+
}));
|
|
278
|
+
return workspaceViewPath;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function renderHtmlViews(cwd, { slug = null, all = false } = {}) {
|
|
282
|
+
const workspaceStatus = await statusSummary(cwd);
|
|
283
|
+
if (!workspaceStatus.initialized) {
|
|
284
|
+
throw new Error('loopx_workspace_not_initialized');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const workflowViews = [];
|
|
288
|
+
if (all || !slug) {
|
|
289
|
+
for (const workflow of workspaceStatus.workflows) {
|
|
290
|
+
if (workflow.legacy) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const workflowStatus = await statusSummary(cwd, workflow.slug);
|
|
294
|
+
workflowViews.push({
|
|
295
|
+
slug: workflow.slug,
|
|
296
|
+
path: await renderWorkflowPages(workflowStatus),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
} else if (slug) {
|
|
300
|
+
const workflowStatus = await statusSummary(cwd, slug);
|
|
301
|
+
if (!workflowStatus.state || workflowStatus.legacy) {
|
|
302
|
+
throw new Error('render_workflow_not_available');
|
|
303
|
+
}
|
|
304
|
+
workflowViews.push({
|
|
305
|
+
slug: workflowStatus.slug,
|
|
306
|
+
path: await renderWorkflowPages(workflowStatus),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const workspaceViewPath = await renderWorkspaceIndex(workspaceStatus, workflowViews.map((item) => item.slug));
|
|
311
|
+
return {
|
|
312
|
+
workflowViews,
|
|
313
|
+
workflowViewPath: workflowViews[0]?.path || null,
|
|
314
|
+
workspaceViewPath,
|
|
315
|
+
};
|
|
316
|
+
}
|