@ai-content-space/loopx 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/AGENTS.md +50 -0
  2. package/README.md +59 -446
  3. package/README.zh-CN.md +59 -457
  4. package/docs/loopx/design/loopx-skill-suite-v1-design.md +73 -0
  5. package/docs/loopx/plans/loopx-skill-suite-v1-implementation.md +77 -0
  6. package/package.json +5 -2
  7. package/plugins/loopx/.codex-plugin/plugin.json +4 -4
  8. package/plugins/loopx/scripts/plugin-install.test.mjs +20 -20
  9. package/plugins/loopx/skills/clarify/SKILL.md +38 -311
  10. package/plugins/loopx/skills/debug/SKILL.md +1 -1
  11. package/plugins/loopx/skills/exec/SKILL.md +71 -0
  12. package/plugins/loopx/skills/finish/SKILL.md +254 -0
  13. package/plugins/loopx/skills/fix-review/SKILL.md +216 -0
  14. package/plugins/loopx/skills/go-style/SKILL.md +1 -1
  15. package/plugins/loopx/skills/kratos/SKILL.md +1 -1
  16. package/plugins/loopx/skills/plan/SKILL.md +136 -258
  17. package/plugins/loopx/skills/refactor-plan/SKILL.md +71 -0
  18. package/plugins/loopx/skills/review/SKILL.md +72 -105
  19. package/plugins/loopx/skills/review/code-reviewer.md +168 -0
  20. package/plugins/loopx/skills/spec/DESIGN_SPEC_TEMPLATE.md +323 -0
  21. package/plugins/loopx/skills/spec/SKILL.md +76 -0
  22. package/plugins/loopx/skills/subagent-exec/SKILL.md +282 -0
  23. package/plugins/loopx/skills/subagent-exec/agents/openai.yaml +3 -0
  24. package/plugins/loopx/skills/subagent-exec/code-quality-reviewer-prompt.md +25 -0
  25. package/plugins/loopx/skills/subagent-exec/codex-subagents.md +37 -0
  26. package/plugins/loopx/skills/subagent-exec/implementer-prompt.md +113 -0
  27. package/plugins/loopx/skills/subagent-exec/spec-reviewer-prompt.md +61 -0
  28. package/plugins/loopx/skills/tdd/SKILL.md +1 -1
  29. package/plugins/loopx/skills/verify/SKILL.md +1 -1
  30. package/scripts/claude-workflow-hook.mjs +109 -0
  31. package/scripts/codex-workflow-hook.mjs +2 -5
  32. package/scripts/install-skills.mjs +3 -3
  33. package/scripts/verify-skills.mjs +32 -1
  34. package/skills/RESOLVER.md +26 -17
  35. package/skills/clarify/SKILL.md +38 -311
  36. package/skills/debug/SKILL.md +1 -1
  37. package/skills/exec/SKILL.md +71 -0
  38. package/skills/finish/SKILL.md +254 -0
  39. package/skills/fix-review/SKILL.md +216 -0
  40. package/skills/go-style/SKILL.md +1 -1
  41. package/skills/kratos/SKILL.md +1 -1
  42. package/skills/plan/SKILL.md +136 -258
  43. package/skills/refactor-plan/SKILL.md +71 -0
  44. package/skills/review/SKILL.md +72 -105
  45. package/skills/review/code-reviewer.md +168 -0
  46. package/skills/spec/DESIGN_SPEC_TEMPLATE.md +323 -0
  47. package/skills/spec/SKILL.md +76 -0
  48. package/skills/subagent-exec/SKILL.md +282 -0
  49. package/skills/subagent-exec/agents/openai.yaml +3 -0
  50. package/skills/subagent-exec/code-quality-reviewer-prompt.md +25 -0
  51. package/skills/subagent-exec/codex-subagents.md +37 -0
  52. package/skills/subagent-exec/implementer-prompt.md +113 -0
  53. package/skills/subagent-exec/spec-reviewer-prompt.md +61 -0
  54. package/skills/tdd/SKILL.md +1 -1
  55. package/skills/verify/SKILL.md +1 -1
  56. package/src/autopilot-runtime.mjs +1 -1
  57. package/src/cli.mjs +79 -5
  58. package/src/context-manifest.mjs +2 -2
  59. package/src/html-views.mjs +457 -95
  60. package/src/install-discovery.mjs +210 -6
  61. package/src/next-skill.mjs +2 -4
  62. package/src/plan-runtime.mjs +572 -93
  63. package/src/runtime-maintenance.mjs +60 -16
  64. package/src/workflow.mjs +989 -65
  65. package/templates/architecture.md +58 -16
  66. package/templates/development-plan.md +42 -12
  67. package/plugins/loopx/skills/archive/SKILL.md +0 -55
  68. package/plugins/loopx/skills/autopilot/SKILL.md +0 -93
  69. package/plugins/loopx/skills/build/SKILL.md +0 -228
  70. package/skills/archive/SKILL.md +0 -55
  71. package/skills/autopilot/SKILL.md +0 -93
  72. package/skills/build/SKILL.md +0 -228
@@ -1,26 +1,32 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
- import { basename, join, relative } from 'node:path';
3
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
4
4
 
5
- import { nextSkillCommand } from './next-skill.mjs';
6
5
  import { statusSummary } from './workflow.mjs';
7
6
 
8
7
  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: 'requirement-traceability.md', label: '需求覆盖矩阵', page: 'plan.html' },
15
- { name: 'execution-record.md', label: '执行记录', page: 'build.html' },
16
- { name: 'review-report.md', label: '评审报告', page: 'review.html' },
8
+ { id: 'spec', name: 'spec.md', label: '需求工作副本', page: 'intake.html' },
9
+ { id: 'plan', name: 'plan.md', label: '计划', page: 'plan.html' },
10
+ { id: 'architecture', name: 'architecture.md', label: '架构', page: 'plan.html' },
11
+ { id: 'development-plan', name: 'development-plan.md', label: '开发计划', page: 'plan.html' },
12
+ { id: 'test-plan', name: 'test-plan.md', label: '测试计划', page: 'plan.html' },
13
+ { id: 'requirement-traceability', name: 'requirement-traceability.md', label: '需求覆盖矩阵', page: 'plan.html' },
14
+ { id: 'plan-delegation-decision', name: 'plan-delegation-decision.md', label: '委派决策', page: 'plan.html' },
15
+ { id: 'change-proposal', name: 'proposal.md', label: '变更提案', page: 'change.html', changeKey: 'proposal' },
16
+ { id: 'change-spec-delta', name: 'spec-delta.md', label: '规格增量', page: 'change.html', changeKey: 'specDelta' },
17
+ { id: 'change-design', name: 'design.md', label: '设计方案', page: 'change.html', changeKey: 'design' },
18
+ { id: 'change-tasks', name: 'tasks.md', label: '任务拆解', page: 'change.html', changeKey: 'tasks' },
19
+ { id: 'change-slices', name: 'slices.json', label: '垂直切片', page: 'change.html', changeKey: 'slices' },
20
+ { id: 'execution-record', name: 'execution-record.md', label: '执行记录', page: 'build.html' },
21
+ { id: 'review-report', name: 'review-report.md', label: '评审报告', page: 'review.html' },
17
22
  ];
18
23
 
19
24
  const PAGE_GROUPS = [
20
- { file: 'intake.html', title: '需求澄清', artifacts: ['spec.md'] },
21
- { file: 'plan.html', title: '计划与架构', artifacts: ['plan.md', 'architecture.md', 'development-plan.md', 'test-plan.md', 'requirement-traceability.md'] },
22
- { file: 'build.html', title: '执行与验证', artifacts: ['execution-record.md'] },
23
- { file: 'review.html', title: '评审结论', artifacts: ['review-report.md'] },
25
+ { file: 'intake.html', title: '需求澄清', artifacts: ['spec'] },
26
+ { file: 'plan.html', title: '计划与架构', artifacts: ['plan', 'architecture', 'development-plan', 'test-plan', 'requirement-traceability'] },
27
+ { file: 'change.html', title: '变更设计方案', artifacts: ['change-proposal', 'change-spec-delta', 'change-design', 'change-tasks', 'change-slices'] },
28
+ { file: 'build.html', title: '执行与验证', artifacts: ['execution-record'] },
29
+ { file: 'review.html', title: '评审结论', artifacts: ['review-report'] },
24
30
  ];
25
31
 
26
32
  function escapeHtml(value) {
@@ -41,26 +47,56 @@ function htmlDoc({ title, body }) {
41
47
  ' <meta name="viewport" content="width=device-width, initial-scale=1">',
42
48
  ` <title>${escapeHtml(title)}</title>`,
43
49
  ' <style>',
44
- ' :root { color-scheme: light; --text: #17202a; --muted: #5f6f7f; --line: #d8e0e8; --bg: #f6f8fa; --panel: #ffffff; --accent: #1769aa; }',
50
+ ' :root { color-scheme: light; --text: #17202a; --muted: #5f6f7f; --line: #d8e0e8; --bg: #f6f8fa; --panel: #ffffff; --accent: #1769aa; --ok: #1b7f4d; --warn: #a15c00; --bad: #b42318; --soft: #edf4fb; }',
45
51
  ' * { box-sizing: border-box; }',
46
52
  ' body { margin: 0; font: 15px/1.65 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--text); background: var(--bg); }',
47
- ' main { max-width: 1080px; margin: 0 auto; padding: 28px 20px 48px; }',
53
+ ' main { max-width: 1180px; margin: 0 auto; padding: 28px 20px 48px; }',
48
54
  ' header { margin-bottom: 20px; }',
49
55
  ' h1 { margin: 0 0 8px; font-size: 28px; line-height: 1.25; }',
50
56
  ' h2 { margin: 24px 0 10px; font-size: 18px; }',
51
57
  ' h3 { margin: 18px 0 8px; font-size: 16px; }',
58
+ ' h4 { margin: 14px 0 6px; font-size: 14px; }',
52
59
  ' a { color: var(--accent); text-decoration: none; }',
53
60
  ' a:hover { text-decoration: underline; }',
54
61
  ' .muted { color: var(--muted); }',
55
62
  ' .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }',
56
63
  ' .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 14px 16px; }',
64
+ ' .panel h2:first-child { margin-top: 0; }',
57
65
  ' .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; }',
66
+ ' .badge.ok { color: var(--ok); border-color: #b8dfc9; background: #f0fbf4; }',
67
+ ' .badge.warn { color: var(--warn); border-color: #f1d29b; background: #fff8eb; }',
68
+ ' .badge.bad { color: var(--bad); border-color: #f0b7b1; background: #fff1f0; }',
69
+ ' .review-nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 18px; }',
70
+ ' .review-nav a { border: 1px solid var(--line); border-radius: 8px; padding: 6px 10px; background: #fff; }',
71
+ ' .callout { border-left: 4px solid var(--accent); background: var(--soft); padding: 10px 12px; margin: 12px 0; }',
72
+ ' .hero { background: linear-gradient(135deg, #fff, #f7fbff); border: 1px solid var(--line); border-radius: 12px; padding: 18px 20px; margin-bottom: 16px; }',
73
+ ' .hero-grid { display: grid; grid-template-columns: 1.4fr 1fr; gap: 14px; }',
74
+ ' .hero-kpi { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-top: 12px; }',
75
+ ' .kpi { border: 1px solid var(--line); background: #fff; border-radius: 10px; padding: 10px 12px; }',
76
+ ' .kpi .label { color: var(--muted); font-size: 12px; }',
77
+ ' .kpi .value { font-size: 18px; font-weight: 700; margin-top: 2px; }',
78
+ ' .layout { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 16px; align-items: start; }',
79
+ ' .sticky { position: sticky; top: 14px; }',
80
+ ' .stack { display: grid; gap: 12px; }',
81
+ ' .visual-map { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; }',
82
+ ' .visual-card { border: 1px solid var(--line); border-radius: 10px; padding: 12px; background: #fff; }',
83
+ ' .visual-card .title { font-weight: 700; margin-bottom: 4px; }',
84
+ ' .visual-card .meta { color: var(--muted); font-size: 12px; }',
85
+ ' .outline { border-left: 2px solid var(--line); padding-left: 12px; }',
86
+ ' .outline a { display: block; color: var(--text); padding: 3px 0; }',
58
87
  ' table { width: 100%; border-collapse: collapse; background: var(--panel); border: 1px solid var(--line); }',
59
88
  ' th, td { padding: 8px 10px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }',
60
89
  ' th { color: var(--muted); font-weight: 600; }',
61
90
  ' pre { overflow: auto; padding: 12px; background: #0f1720; color: #edf4fb; border-radius: 8px; }',
62
91
  ' code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }',
63
- ' .markdown { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 16px; }',
92
+ ' .markdown { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 16px; margin-bottom: 16px; }',
93
+ ' .markdown pre { margin: 10px 0 0; }',
94
+ ' .markdown table { margin-top: 10px; }',
95
+ ' .markdown ul, .markdown ol { margin-top: 10px; padding-left: 22px; }',
96
+ ' .markdown p { margin: 10px 0; }',
97
+ ' .markdown h2:first-child, .markdown h3:first-child, .markdown h4:first-child { margin-top: 0; }',
98
+ ' .artifact-meta { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 10px; }',
99
+ ' @media (max-width: 860px) { .hero-grid, .layout { grid-template-columns: 1fr; } .sticky { position: static; } }',
64
100
  ' </style>',
65
101
  '</head>',
66
102
  '<body>',
@@ -72,12 +108,178 @@ function htmlDoc({ title, body }) {
72
108
  ].join('\n');
73
109
  }
74
110
 
75
- function listItems(items) {
76
- const values = Array.isArray(items) ? items.filter(Boolean) : [];
77
- if (values.length === 0) {
78
- return '<span class="muted">无</span>';
111
+ function languageMetric(text) {
112
+ const chineseChars = String(text || '').match(/[\u3400-\u9fff]/g) || [];
113
+ const latinChars = String(text || '').match(/[A-Za-z]/g) || [];
114
+ const signalChars = chineseChars.length + latinChars.length;
115
+ const chineseRatio = signalChars === 0 ? 0 : chineseChars.length / signalChars;
116
+ return {
117
+ chineseChars: chineseChars.length,
118
+ latinChars: latinChars.length,
119
+ chineseRatio,
120
+ chineseOk: signalChars > 0 && (chineseChars.length >= 40 || (chineseChars.length >= 8 && chineseRatio >= 0.2)),
121
+ };
122
+ }
123
+
124
+ function artifactMetrics(text) {
125
+ const lines = String(text || '').split('\n');
126
+ const headings = lines.filter((line) => /^#{1,4}\s+/.test(line)).length;
127
+ const tables = lines.filter((line, index) => line.trim().startsWith('|') && lines[index + 1] && /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(lines[index + 1])).length;
128
+ const todoCount = lines.filter((line) => /\b(TBD|TODO|REPLACE_ME)\b/i.test(line)).length;
129
+ return {
130
+ lines: lines.filter((line) => line.trim()).length,
131
+ headings,
132
+ tables,
133
+ todoCount,
134
+ ...languageMetric(text),
135
+ };
136
+ }
137
+
138
+ function artifactId(artifact) {
139
+ return artifact.id || artifact.name;
140
+ }
141
+
142
+ function anchorForArtifact(artifact) {
143
+ return artifactId(artifact).replace(/[^a-z0-9-]/gi, '-');
144
+ }
145
+
146
+ function resolveArtifactPath(root, state, artifact) {
147
+ if (artifact.changeKey) {
148
+ return state.change_artifact_paths?.[artifact.changeKey] || null;
149
+ }
150
+ if (artifact.id === 'spec') {
151
+ const candidate = state.spec_artifact_path || state.clarify_spec_path || null;
152
+ if (candidate) {
153
+ return isAbsolute(candidate) ? candidate : resolve(dirname(dirname(root)), candidate);
154
+ }
155
+ }
156
+ return join(root, artifact.name);
157
+ }
158
+
159
+ function extractHeadings(text) {
160
+ return String(text || '')
161
+ .split('\n')
162
+ .map((line) => line.match(/^(#{1,4})\s+(.+?)\s*$/))
163
+ .filter(Boolean)
164
+ .map((match) => ({
165
+ level: match[1].length,
166
+ title: match[2].replace(/`/g, '').trim(),
167
+ }));
168
+ }
169
+
170
+ function firstParagraph(text) {
171
+ return String(text || '')
172
+ .split('\n')
173
+ .map((line) => line.trim())
174
+ .filter(Boolean)
175
+ .filter((line) => !line.startsWith('#'))
176
+ .filter((line) => !line.startsWith('|'))
177
+ .filter((line) => !/^[-*]\s+/.test(line))
178
+ .filter((line) => !/^\d+[.)]\s+/.test(line))
179
+ .find((line) => line.length > 20) || '';
180
+ }
181
+
182
+ function sectionDigest(text) {
183
+ const headings = extractHeadings(text);
184
+ return {
185
+ title: headings[0]?.title || firstParagraph(text) || '无标题产物',
186
+ outline: headings.filter((item) => item.level <= 3).slice(0, 8),
187
+ summary: firstParagraph(text),
188
+ };
189
+ }
190
+
191
+ function collectHeadingBlocks(text) {
192
+ const lines = String(text || '').split('\n');
193
+ const blocks = [];
194
+ let current = null;
195
+ for (const line of lines) {
196
+ const heading = line.match(/^(#{1,4})\s+(.+?)\s*$/);
197
+ if (heading) {
198
+ if (current) {
199
+ blocks.push(current);
200
+ }
201
+ current = {
202
+ level: heading[1].length,
203
+ title: heading[2].replace(/`/g, '').trim(),
204
+ lines: [],
205
+ };
206
+ continue;
207
+ }
208
+ if (current) {
209
+ current.lines.push(line);
210
+ }
211
+ }
212
+ if (current) {
213
+ blocks.push(current);
214
+ }
215
+ return blocks;
216
+ }
217
+
218
+ function blockPreview(block) {
219
+ return block.lines
220
+ .map((line) => line.trim())
221
+ .filter(Boolean)
222
+ .find((line) => !line.startsWith('#') && !line.startsWith('|') && !/^[-*]\s+/.test(line) && !/^\d+[.)]\s+/.test(line)) || '';
223
+ }
224
+
225
+ function parseFrontmatterBlock(text) {
226
+ if (!String(text || '').startsWith('---\n')) {
227
+ return {};
228
+ }
229
+ const end = String(text).indexOf('\n---\n', 4);
230
+ if (end === -1) {
231
+ return {};
232
+ }
233
+ return Object.fromEntries(
234
+ String(text)
235
+ .slice(4, end)
236
+ .split('\n')
237
+ .map((line) => line.trim())
238
+ .filter(Boolean)
239
+ .map((line) => {
240
+ const separator = line.indexOf(':');
241
+ const key = line.slice(0, separator).trim();
242
+ const rawValue = line.slice(separator + 1).trim();
243
+ if (rawValue.startsWith('[') || rawValue.startsWith('{')) {
244
+ try {
245
+ return [key, JSON.parse(rawValue)];
246
+ } catch {
247
+ return [key, rawValue];
248
+ }
249
+ }
250
+ return [key, rawValue];
251
+ }),
252
+ );
253
+ }
254
+
255
+ function renderListItems(lines) {
256
+ const items = lines
257
+ .map((line) => line.trim())
258
+ .filter((line) => /^[-*]\s+/.test(line) || /^\d+[.)]\s+/.test(line))
259
+ .map((line) => line.replace(/^[-*]\s+/, '').replace(/^\d+[.)]\s+/, '').trim())
260
+ .filter(Boolean);
261
+ return items.length > 0 ? `<ul>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>` : '<span class="muted">无</span>';
262
+ }
263
+
264
+ function renderKeyValueTable(entries) {
265
+ return `<table><tbody>${entries.map(([key, value]) => `<tr><th>${escapeHtml(key)}</th><td>${value}</td></tr>`).join('')}</tbody></table>`;
266
+ }
267
+
268
+ function summarizeRequirementBlocks(text) {
269
+ const matches = [...String(text || '').matchAll(/^###\s+Requirement:\s*(.+?)\s*$/gim)];
270
+ return matches.map((match) => ({
271
+ name: match[1].trim(),
272
+ preview: '',
273
+ }));
274
+ }
275
+
276
+ function parseSlicesJson(text) {
277
+ try {
278
+ const payload = JSON.parse(text);
279
+ return Array.isArray(payload.slices) ? payload.slices : [];
280
+ } catch {
281
+ return [];
79
282
  }
80
- return values.map((item) => `<span class="badge">${escapeHtml(item)}</span>`).join(' ');
81
283
  }
82
284
 
83
285
  function markdownToHtml(markdown) {
@@ -186,38 +388,175 @@ function markdownToHtml(markdown) {
186
388
  return html.join('\n');
187
389
  }
188
390
 
189
- function artifactLink(viewRoot, artifactPath, label) {
190
- const href = relative(viewRoot, artifactPath).replaceAll('\\', '/');
191
- return `<a href="${escapeHtml(href)}">${escapeHtml(label)}</a>`;
391
+ function replaceDocumentReferences(text) {
392
+ return String(text || '')
393
+ .replace(/^#\s+Clarify Spec:\s*/gim, '# ')
394
+ .replace(/^#+\s+loopx\s+/gim, (match) => match.replace(/loopx\s+/i, ''))
395
+ .replace(/`?architecture\.md`?/g, '架构方案')
396
+ .replace(/`?development-plan\.md`?/g, '开发计划')
397
+ .replace(/`?design\.md`?/g, '详细设计')
398
+ .replace(/`?plan\.md`?/g, '计划')
399
+ .replace(/`?test-plan\.md`?/g, '测试计划')
400
+ .replace(/`?requirement-traceability\.md`?/g, '需求覆盖矩阵')
401
+ .replace(/`?plan-delegation-decision\.md`?/g, '委派决策')
402
+ .replace(/`?spec-delta\.md`?/g, '规格增量')
403
+ .replace(/`?tasks\.md`?/g, '任务拆解')
404
+ .replace(/`?slices\.json`?/g, '切片定义');
192
405
  }
193
406
 
194
- function statusPanels(status) {
195
- const state = status.state || {};
196
- const readiness = state.readiness || {};
197
- const authorization = state.authorization || {};
198
- const nextSkill = nextSkillCommand(state);
199
- return [
200
- '<section class="grid">',
201
- `<div class="panel"><strong>阶段</strong><br>${escapeHtml(state.current_stage || '(none)')}</div>`,
202
- `<div class="panel"><strong>状态</strong><br>${escapeHtml(state.stage_status || '(unknown)')}</div>`,
203
- `<div class="panel"><strong>需求覆盖</strong><br>${escapeHtml(state.source_requirements_status || 'unknown')}</div>`,
204
- `<div class="panel"><strong>下一步</strong><br><code>${escapeHtml(nextSkill || status.next_action || 'none')}</code></div>`,
205
- `<div class="panel"><strong>归档</strong><br>${escapeHtml(state.archive_status || 'pending')}</div>`,
206
- '</section>',
207
- '<section class="panel">',
208
- '<h2>readiness / authorization</h2>',
209
- '<table><thead><tr><th>关卡</th><th>ready</th><th>authorized</th><th>blockers</th></tr></thead><tbody>',
210
- ...['plan', 'build', 'review', 'done', 'archive'].map((key) => [
211
- '<tr>',
212
- `<td>${escapeHtml(key)}</td>`,
213
- `<td>${escapeHtml(readiness[key]?.ready ?? false)}</td>`,
214
- `<td>${escapeHtml(authorization[key]?.authorized ?? false)}</td>`,
215
- `<td>${listItems(readiness[key]?.blockers || [])}</td>`,
216
- '</tr>',
217
- ].join('')),
218
- '</tbody></table>',
219
- '</section>',
220
- ].join('\n');
407
+ function displayTextForArtifact(artifact, text) {
408
+ if (artifact.name.endsWith('.json')) {
409
+ return text;
410
+ }
411
+ const removeSectionTitles = artifact.id === 'plan'
412
+ ? [/^状态$/, /^推荐执行入口$/, /^Build 前审阅清单$/i]
413
+ : [];
414
+ const lines = String(text || '').split('\n');
415
+ const kept = [];
416
+ let skippingLevel = null;
417
+ for (const line of lines) {
418
+ const heading = line.match(/^(#{1,4})\s+(.+?)\s*$/);
419
+ if (heading) {
420
+ const level = heading[1].length;
421
+ const title = heading[2].replace(/`/g, '').trim();
422
+ if (skippingLevel !== null && level <= skippingLevel) {
423
+ skippingLevel = null;
424
+ }
425
+ if (skippingLevel === null && removeSectionTitles.some((pattern) => pattern.test(title))) {
426
+ skippingLevel = level;
427
+ continue;
428
+ }
429
+ }
430
+ if (skippingLevel !== null) {
431
+ continue;
432
+ }
433
+ if (/\$build\s+/.test(line) || /\.loopx\//.test(line)) {
434
+ continue;
435
+ }
436
+ if (/^\s*-\s*(iteration|Architect review|Critic verdict|plan package|execution approved)\s*:/i.test(line)) {
437
+ continue;
438
+ }
439
+ kept.push(line);
440
+ }
441
+ return replaceDocumentReferences(kept.join('\n'));
442
+ }
443
+
444
+ function artifactDetailHtml(artifact, text) {
445
+ const blocks = collectHeadingBlocks(text);
446
+ const topBlocks = blocks.filter((block) => block.level <= 3).slice(0, 12);
447
+ const sectionCards = topBlocks.length > 0
448
+ ? [
449
+ '<section class="panel">',
450
+ '<h3>章节速览</h3>',
451
+ '<div class="visual-map">',
452
+ ...topBlocks.map((block) => [
453
+ '<div class="visual-card">',
454
+ `<div class="title">${escapeHtml(block.title)}</div>`,
455
+ `<div class="meta">${escapeHtml(blockPreview(block) || '该章节主要由列表、表格或代码块组成。')}</div>`,
456
+ '</div>',
457
+ ].join('')),
458
+ '</div>',
459
+ '</section>',
460
+ ].join('\n')
461
+ : '';
462
+
463
+ if (artifact.name === 'spec-delta.md') {
464
+ const frontmatter = parseFrontmatterBlock(text);
465
+ const requirements = summarizeRequirementBlocks(text);
466
+ return [
467
+ '<section class="panel">',
468
+ '<h3>规格增量详解</h3>',
469
+ renderKeyValueTable([
470
+ ['change_id', escapeHtml(frontmatter.change_id || '(未声明)')],
471
+ ['slug', escapeHtml(frontmatter.slug || '(未声明)')],
472
+ ['target_domains', escapeHtml(Array.isArray(frontmatter.target_domains) ? frontmatter.target_domains.join(', ') : (frontmatter.target_domains || '(见正文)'))],
473
+ ['requirement_blocks', escapeHtml(requirements.length)],
474
+ ]),
475
+ requirements.length > 0 ? [
476
+ '<h4>Requirement Blocks</h4>',
477
+ '<div class="visual-map">',
478
+ ...requirements.map((item) => `<div class="visual-card"><div class="title">${escapeHtml(item.name)}</div><div class="meta">包含 SHALL/MUST 与 Scenario 的归档候选需求块。</div></div>`),
479
+ '</div>',
480
+ ].join('\n') : '',
481
+ '</section>',
482
+ sectionCards,
483
+ ].join('\n');
484
+ }
485
+
486
+ if (artifact.name === 'slices.json') {
487
+ const slices = parseSlicesJson(text);
488
+ return [
489
+ '<section class="panel">',
490
+ '<h3>垂直切片详解</h3>',
491
+ '<div class="visual-map">',
492
+ ...(slices.length > 0 ? slices.map((slice) => [
493
+ '<div class="visual-card">',
494
+ `<div class="title">${escapeHtml(slice.id || '(no id)')} ${escapeHtml(slice.title || slice.name || '')}</div>`,
495
+ `<div class="meta">type: ${escapeHtml(slice.type || 'unknown')}</div>`,
496
+ `<p>${escapeHtml(slice.behavior || slice.description || '')}</p>`,
497
+ '<h4>验收</h4>',
498
+ renderListItems(Array.isArray(slice.acceptance_criteria) ? slice.acceptance_criteria.map((item) => `- ${item}`) : (Array.isArray(slice.acceptance) ? slice.acceptance.map((item) => `- ${item}`) : [])),
499
+ `<p class="muted">verification: ${escapeHtml(slice.verification_signal || '(missing)')}</p>`,
500
+ '</div>',
501
+ ].join('')) : ['<span class="muted">未解析到 slices。</span>']),
502
+ '</div>',
503
+ '</section>',
504
+ ].join('\n');
505
+ }
506
+
507
+ if (artifact.name === 'tasks.md') {
508
+ const checklist = String(text || '').split('\n').filter((line) => /^\s*-\s+\[[ xX]\]\s+/.test(line));
509
+ return [
510
+ '<section class="panel">',
511
+ '<h3>任务拆解详解</h3>',
512
+ renderKeyValueTable([
513
+ ['checklist_items', escapeHtml(checklist.length)],
514
+ ['open_items', escapeHtml(checklist.filter((line) => /\[\s\]/.test(line)).length)],
515
+ ['completed_items', escapeHtml(checklist.filter((line) => /\[[xX]\]/.test(line)).length)],
516
+ ]),
517
+ '</section>',
518
+ sectionCards,
519
+ ].join('\n');
520
+ }
521
+
522
+ return sectionCards;
523
+ }
524
+
525
+ function cleanDocumentTitle(title) {
526
+ return String(title || '')
527
+ .replace(/^loopx\s+/i, '')
528
+ .replace(/^工作流\s+/i, '')
529
+ .replace(/^Clarify Spec:\s*/i, '')
530
+ .trim() || '技术方案';
531
+ }
532
+
533
+ function pageIntro(group) {
534
+ if (group.file === 'plan.html') {
535
+ return '本页汇总需求方案、架构方案、开发计划、测试计划和需求覆盖,供直接阅读和人工确认。';
536
+ }
537
+ if (group.file === 'change.html') {
538
+ return '本页汇总变更提案、规格增量、详细设计和任务拆解,供确认具体实现边界。';
539
+ }
540
+ if (group.file === 'intake.html') {
541
+ return '本页汇总需求澄清结果,供确认范围、非目标、约束和验收口径。';
542
+ }
543
+ if (group.file === 'build.html') {
544
+ return '本页汇总执行结果和验证证据。';
545
+ }
546
+ if (group.file === 'review.html') {
547
+ return '本页汇总评审结论、问题和后续处理建议。';
548
+ }
549
+ return '本页汇总相关方案内容。';
550
+ }
551
+
552
+ async function solutionTitle(artifactRows) {
553
+ const preferred = artifactRows.find((artifact) => ['plan', 'spec', 'change-proposal'].includes(artifactId(artifact)) && artifact.exists)
554
+ || artifactRows.find((artifact) => artifact.exists);
555
+ if (!preferred) {
556
+ return '技术方案';
557
+ }
558
+ const text = await readFile(preferred.path, 'utf8');
559
+ return cleanDocumentTitle(sectionDigest(text).title);
221
560
  }
222
561
 
223
562
  async function renderWorkflowPages(status) {
@@ -226,66 +565,99 @@ async function renderWorkflowPages(status) {
226
565
  await mkdir(viewRoot, { recursive: true });
227
566
 
228
567
  const artifactRows = WORKFLOW_ARTIFACTS.map((artifact) => {
229
- const artifactPath = join(root, artifact.name);
568
+ const artifactPath = resolveArtifactPath(root, status.state || {}, artifact);
230
569
  return {
231
570
  ...artifact,
232
571
  path: artifactPath,
233
- exists: existsSync(artifactPath),
572
+ exists: Boolean(artifactPath) && existsSync(artifactPath),
234
573
  };
235
574
  });
236
575
 
576
+ const renderArtifactBody = async (artifact) => {
577
+ const text = await readFile(artifact.path, 'utf8');
578
+ const displayText = displayTextForArtifact(artifact, text);
579
+ const metrics = artifactMetrics(displayText);
580
+ const digest = sectionDigest(displayText);
581
+ const isJson = artifact.name.endsWith('.json');
582
+ const detailHtml = artifactDetailHtml(artifact, displayText);
583
+ return {
584
+ text: displayText,
585
+ metrics,
586
+ digest,
587
+ detailHtml,
588
+ body: isJson
589
+ ? `<pre><code>${escapeHtml(displayText)}</code></pre>`
590
+ : markdownToHtml(displayText),
591
+ };
592
+ };
593
+
237
594
  for (const group of PAGE_GROUPS) {
238
595
  const sections = [];
239
- for (const artifactName of group.artifacts) {
240
- const artifact = artifactRows.find((item) => item.name === artifactName);
596
+ const availableArtifacts = artifactRows.filter((artifact) => group.artifacts.includes(artifactId(artifact)) && artifact.exists);
597
+ const nav = availableArtifacts.length > 0
598
+ ? `<nav class="review-nav">${availableArtifacts.map((artifact) => `<a href="#${escapeHtml(anchorForArtifact(artifact))}">${escapeHtml(artifact.label)}</a>`).join('')}</nav>`
599
+ : '';
600
+ for (const artifactIdValue of group.artifacts) {
601
+ const artifact = artifactRows.find((item) => artifactId(item) === artifactIdValue);
241
602
  if (!artifact?.exists) {
242
603
  continue;
243
604
  }
244
- const text = await readFile(artifact.path, 'utf8');
605
+ const rendered = await renderArtifactBody(artifact);
245
606
  sections.push([
246
- `<section class="markdown">`,
247
- `<p class="muted">${artifactLink(viewRoot, artifact.path, artifact.name)}</p>`,
248
- markdownToHtml(text),
607
+ `<section class="markdown" id="${escapeHtml(anchorForArtifact(artifact))}">`,
608
+ `<h2>${escapeHtml(artifact.label)}</h2>`,
609
+ `<p>${escapeHtml(rendered.digest.summary || '该部分正文如下。')}</p>`,
610
+ rendered.detailHtml,
611
+ rendered.body,
249
612
  '</section>',
250
613
  ].join('\n'));
251
614
  }
252
615
  await writeFile(join(viewRoot, group.file), htmlDoc({
253
- title: `${group.title} - ${status.slug}`,
616
+ title: group.title,
254
617
  body: [
255
618
  '<header>',
256
619
  `<h1>${escapeHtml(group.title)}</h1>`,
257
- `<p class="muted">工作流:${escapeHtml(status.slug)}</p>`,
258
- '<p><a href="index.html">返回工作流首页</a></p>',
620
+ `<div class="callout">${escapeHtml(pageIntro(group))}</div>`,
621
+ '<p><a href="index.html">返回方案总览</a></p>',
622
+ nav,
259
623
  '</header>',
260
624
  sections.length > 0 ? sections.join('\n') : '<section class="panel muted">暂无对应产物。</section>',
261
625
  ].join('\n'),
262
626
  }));
263
627
  }
264
628
 
629
+ const title = await solutionTitle(artifactRows);
630
+ const pageRows = PAGE_GROUPS.map((group) => {
631
+ const availableArtifacts = artifactRows.filter((artifact) => group.artifacts.includes(artifactId(artifact)) && artifact.exists);
632
+ if (availableArtifacts.length === 0) {
633
+ return '';
634
+ }
635
+ return [
636
+ '<tr>',
637
+ `<td><a href="${escapeHtml(group.file)}">${escapeHtml(group.title)}</a></td>`,
638
+ `<td>${escapeHtml(availableArtifacts.map((artifact) => artifact.label).join('、'))}</td>`,
639
+ `<td>${escapeHtml(pageIntro(group))}</td>`,
640
+ '</tr>',
641
+ ].join('');
642
+ }).filter(Boolean);
643
+
265
644
  const indexBody = [
266
645
  '<header>',
267
- `<h1>工作流 ${escapeHtml(status.slug)}</h1>`,
268
- `<p class="muted">HTML 是派生阅读视图;Markdown 和 JSON 仍是运行时事实源。</p>`,
646
+ '<h1>技术方案总览</h1>',
647
+ `<p class="muted">${escapeHtml(title)}</p>`,
648
+ '<div class="callout">本页面提供完整 HTML 阅读入口;各页面已内嵌方案正文,可直接阅读和确认。</div>',
269
649
  '</header>',
270
- statusPanels(status),
271
650
  '<section class="panel">',
272
- '<h2>关键产物</h2>',
273
- '<table><thead><tr><th>产物</th><th>状态</th><th>阅读视图</th><th>原始文件</th></tr></thead><tbody>',
274
- ...artifactRows.map((artifact) => [
275
- '<tr>',
276
- `<td>${escapeHtml(artifact.label)}</td>`,
277
- `<td>${artifact.exists ? '存在' : '缺失'}</td>`,
278
- `<td>${artifact.exists ? `<a href="${escapeHtml(artifact.page)}">${escapeHtml(basename(artifact.page))}</a>` : '<span class="muted">无</span>'}</td>`,
279
- `<td>${artifact.exists ? artifactLink(viewRoot, artifact.path, artifact.name) : '<span class="muted">无</span>'}</td>`,
280
- '</tr>',
281
- ].join('')),
651
+ '<h2>方案阅读目录</h2>',
652
+ '<table><thead><tr><th>页面</th><th>包含内容</th><th>阅读重点</th></tr></thead><tbody>',
653
+ ...pageRows,
282
654
  '</tbody></table>',
283
655
  '</section>',
284
656
  ].join('\n');
285
657
 
286
658
  const workflowViewPath = join(viewRoot, 'index.html');
287
659
  await writeFile(workflowViewPath, htmlDoc({
288
- title: `loopx 工作流 ${status.slug}`,
660
+ title: '技术方案总览',
289
661
  body: indexBody,
290
662
  }));
291
663
 
@@ -301,27 +673,20 @@ async function renderWorkspaceIndex(workspaceStatus, renderedSlugs = []) {
301
673
  const link = rendered.has(workflow.slug)
302
674
  ? `<a href="${escapeHtml(href)}">${escapeHtml(workflow.slug)}</a>`
303
675
  : escapeHtml(workflow.slug);
304
- return [
305
- '<tr>',
306
- `<td>${link}</td>`,
307
- `<td>${escapeHtml(workflow.current_stage || '(none)')}</td>`,
308
- `<td>${escapeHtml(workflow.contract)}</td>`,
309
- `<td>${escapeHtml(workflow.missing_artifact_count)}</td>`,
310
- '</tr>',
311
- ].join('');
676
+ return `<tr><td>${link}</td><td>技术方案总览</td></tr>`;
312
677
  });
313
678
  const workspaceViewPath = join(viewsRoot, 'index.html');
314
679
  await writeFile(workspaceViewPath, htmlDoc({
315
- title: 'loopx 工作台',
680
+ title: '方案阅读入口',
316
681
  body: [
317
682
  '<header>',
318
- '<h1>loopx 工作台</h1>',
319
- `<p class="muted">工作区:${escapeHtml(workspaceStatus.workspaceRoot)}</p>`,
683
+ '<h1>方案阅读入口</h1>',
684
+ '<p class="muted">选择一个需求方案,进入完整 HTML 阅读页。</p>',
320
685
  '</header>',
321
686
  '<section class="panel">',
322
- '<h2>工作流</h2>',
323
- '<table><thead><tr><th>工作流</th><th>阶段</th><th>契约</th><th>缺失产物数</th></tr></thead><tbody>',
324
- rows.join('\n') || '<tr><td colspan="4" class="muted">暂无工作流。</td></tr>',
687
+ '<h2>方案列表</h2>',
688
+ '<table><thead><tr><th>方案</th><th>阅读入口</th></tr></thead><tbody>',
689
+ rows.join('\n') || '<tr><td colspan="2" class="muted">暂无方案。</td></tr>',
325
690
  '</tbody></table>',
326
691
  '</section>',
327
692
  ].join('\n'),
@@ -338,9 +703,6 @@ export async function renderHtmlViews(cwd, { slug = null, all = false } = {}) {
338
703
  const workflowViews = [];
339
704
  if (all || !slug) {
340
705
  for (const workflow of workspaceStatus.workflows) {
341
- if (workflow.legacy) {
342
- continue;
343
- }
344
706
  const workflowStatus = await statusSummary(cwd, workflow.slug);
345
707
  workflowViews.push({
346
708
  slug: workflow.slug,
@@ -349,7 +711,7 @@ export async function renderHtmlViews(cwd, { slug = null, all = false } = {}) {
349
711
  }
350
712
  } else if (slug) {
351
713
  const workflowStatus = await statusSummary(cwd, slug);
352
- if (!workflowStatus.state || workflowStatus.legacy) {
714
+ if (!workflowStatus.state) {
353
715
  throw new Error('render_workflow_not_available');
354
716
  }
355
717
  workflowViews.push({