@ai-content-space/loopx 0.1.10 → 0.2.1

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 (77) hide show
  1. package/AGENTS.md +49 -0
  2. package/README.md +69 -448
  3. package/README.zh-CN.md +69 -459
  4. package/docs/loopx/design/loopx-skill-suite-v1-design.md +80 -0
  5. package/docs/loopx/plans/loopx-skill-suite-v1-implementation.md +81 -0
  6. package/package.json +7 -3
  7. package/plugins/loopx/.codex-plugin/plugin.json +4 -4
  8. package/plugins/loopx/skills/clarify/SKILL.md +38 -311
  9. package/plugins/loopx/skills/debug/SKILL.md +1 -1
  10. package/plugins/loopx/skills/exec/SKILL.md +71 -0
  11. package/plugins/loopx/skills/finish/SKILL.md +349 -0
  12. package/plugins/loopx/skills/fix-review/SKILL.md +216 -0
  13. package/plugins/loopx/skills/go-style/SKILL.md +2 -2
  14. package/plugins/loopx/skills/kratos/SKILL.md +1 -1
  15. package/plugins/loopx/skills/plan/SKILL.md +138 -271
  16. package/plugins/loopx/skills/refactor-plan/SKILL.md +71 -0
  17. package/plugins/loopx/skills/review/SKILL.md +72 -105
  18. package/plugins/loopx/skills/review/code-reviewer.md +168 -0
  19. package/plugins/loopx/skills/spec/DESIGN_SPEC_TEMPLATE.md +323 -0
  20. package/plugins/loopx/skills/spec/SKILL.md +76 -0
  21. package/plugins/loopx/skills/subagent-exec/SKILL.md +282 -0
  22. package/plugins/loopx/skills/subagent-exec/agents/openai.yaml +3 -0
  23. package/plugins/loopx/skills/subagent-exec/code-quality-reviewer-prompt.md +25 -0
  24. package/plugins/loopx/skills/subagent-exec/codex-subagents.md +37 -0
  25. package/plugins/loopx/skills/subagent-exec/implementer-prompt.md +113 -0
  26. package/plugins/loopx/skills/subagent-exec/spec-reviewer-prompt.md +61 -0
  27. package/plugins/loopx/skills/tdd/SKILL.md +1 -1
  28. package/plugins/loopx/skills/verify/SKILL.md +1 -1
  29. package/scripts/claude-workflow-hook.mjs +109 -0
  30. package/scripts/codex-workflow-hook.mjs +2 -5
  31. package/scripts/install-skills.mjs +3 -3
  32. package/scripts/verify-skills.mjs +34 -2
  33. package/skills/RESOLVER.md +22 -17
  34. package/skills/clarify/SKILL.md +38 -311
  35. package/skills/debug/SKILL.md +1 -1
  36. package/skills/exec/SKILL.md +71 -0
  37. package/skills/finish/SKILL.md +349 -0
  38. package/skills/fix-review/SKILL.md +216 -0
  39. package/skills/go-style/SKILL.md +2 -2
  40. package/skills/kratos/SKILL.md +1 -1
  41. package/skills/plan/SKILL.md +138 -271
  42. package/skills/refactor-plan/SKILL.md +71 -0
  43. package/skills/review/SKILL.md +72 -105
  44. package/skills/review/code-reviewer.md +168 -0
  45. package/skills/spec/DESIGN_SPEC_TEMPLATE.md +323 -0
  46. package/skills/spec/SKILL.md +76 -0
  47. package/skills/subagent-exec/SKILL.md +282 -0
  48. package/skills/subagent-exec/agents/openai.yaml +3 -0
  49. package/skills/subagent-exec/code-quality-reviewer-prompt.md +25 -0
  50. package/skills/subagent-exec/codex-subagents.md +37 -0
  51. package/skills/subagent-exec/implementer-prompt.md +113 -0
  52. package/skills/subagent-exec/spec-reviewer-prompt.md +61 -0
  53. package/skills/tdd/SKILL.md +1 -1
  54. package/skills/verify/SKILL.md +1 -1
  55. package/src/autopilot-runtime.mjs +1 -1
  56. package/src/cli.mjs +78 -7
  57. package/src/context-manifest.mjs +2 -2
  58. package/src/html-views.mjs +129 -195
  59. package/src/install-discovery.mjs +210 -6
  60. package/src/next-skill.mjs +2 -4
  61. package/src/plan-runtime.mjs +219 -93
  62. package/src/runtime-maintenance.mjs +5 -2
  63. package/src/workflow.mjs +749 -71
  64. package/templates/architecture.md +58 -16
  65. package/templates/development-plan.md +42 -12
  66. package/plugins/loopx/scripts/plugin-install.test.mjs +0 -125
  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/ai-slop-cleaner/SKILL.md +0 -114
  71. package/skills/archive/SKILL.md +0 -55
  72. package/skills/autopilot/SKILL.md +0 -93
  73. package/skills/autoresearch/SKILL.md +0 -68
  74. package/skills/build/SKILL.md +0 -228
  75. package/skills/deep-interview/SKILL.md +0 -461
  76. package/skills/ralph/SKILL.md +0 -271
  77. package/skills/ralplan/SKILL.md +0 -49
package/src/cli.mjs CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { readFileSync } from 'node:fs';
4
+ import { createInterface } from 'node:readline/promises';
4
5
 
5
6
  import { archiveStage, autopilotStage, approveStage, buildStage, clarifyStage, initWorkspace, planStage, reviewStage, statusSummary } from './workflow.mjs';
6
7
  import { renderHtmlViews } from './html-views.mjs';
7
- import { installBundledSkills } from './install-discovery.mjs';
8
+ import { installBundledSkills, installSkillsForTargets } from './install-discovery.mjs';
8
9
  import { nextSkillCommand, withNextSkill } from './next-skill.mjs';
9
10
  import { doctorRuntime, migrateLegacyRuntime } from './runtime-maintenance.mjs';
10
11
  import { setupWorkspaceContext } from './workspace-context.mjs';
@@ -15,10 +16,10 @@ function usage() {
15
16
  return [
16
17
  'Usage:',
17
18
  ' loopx --version',
18
- ' loopx init [--slug <slug>]',
19
+ ' loopx init [--slug <slug>] [--enable-agent-delegation] [--auto-agent-delegation] [--agent-delegation-threshold <local|critic-only|parallel-review>]',
19
20
  ' loopx clarify <slug> [--standard|--deep]',
20
21
  ' loopx approve <slug> --from <stage> --to <stage>',
21
- ' loopx plan [slug] [--direct <spec-path>] [--interactive] [--deliberate]',
22
+ ' loopx plan [slug] [--interactive] [--deliberate]',
22
23
  ' loopx build <slug> [--no-deslop]',
23
24
  ' loopx build --from-review <review-report-path> [--no-deslop]',
24
25
  ' loopx review <slug> [--reviewer <name>]',
@@ -27,12 +28,45 @@ function usage() {
27
28
  ' loopx render [slug|--all]',
28
29
  ' loopx status [slug] [--json]',
29
30
  ' loopx setup-context',
31
+ ' loopx install-skills [--target <codex|claude|all>] [--project] [--mode <copy|symlink>] [--dir <path>] [--yes]',
30
32
  ' loopx doctor',
31
33
  ' loopx migrate',
32
34
  ' loopx repair-install',
33
35
  ].join('\n');
34
36
  }
35
37
 
38
+ async function promptInstallOptions() {
39
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
40
+ try {
41
+ const targetAnswer = (await rl.question('Install targets (codex, claude, all) [all]: ')).trim().toLowerCase();
42
+ const projectAnswer = (await rl.question('Install Claude project skills instead of user skills? [y/N]: ')).trim().toLowerCase();
43
+ const modeAnswer = (await rl.question('Install mode (copy, symlink) [copy]: ')).trim().toLowerCase();
44
+ const proceedAnswer = (await rl.question('Proceed? [y/N]: ')).trim().toLowerCase();
45
+ if (proceedAnswer !== 'y' && proceedAnswer !== 'yes') {
46
+ return null;
47
+ }
48
+ const target = targetAnswer || 'all';
49
+ return {
50
+ targets: target === 'all' ? ['codex', 'claude'] : [target],
51
+ project: projectAnswer === 'y' || projectAnswer === 'yes',
52
+ installMethod: modeAnswer === 'symlink' ? 'symlink' : 'copy',
53
+ };
54
+ } finally {
55
+ rl.close();
56
+ }
57
+ }
58
+
59
+ function installOptionsFromArgs(options) {
60
+ const target = String(options.get('--target') || 'all').trim().toLowerCase();
61
+ const targets = target === 'all' ? ['codex', 'claude'] : [target];
62
+ return {
63
+ targets,
64
+ project: Boolean(options.get('--project')),
65
+ installMethod: options.get('--mode') === 'symlink' ? 'symlink' : 'copy',
66
+ dir: options.get('--dir'),
67
+ };
68
+ }
69
+
36
70
  function parseArgs(argv) {
37
71
  const [command, ...rest] = argv;
38
72
  const positionals = [];
@@ -91,6 +125,9 @@ function printHumanStatus(status) {
91
125
  console.log(`plan_critic_verdict: ${status.state.plan_critic_verdict}`);
92
126
  console.log(`plan_artifact_status: ${status.state.plan_docs_status}`);
93
127
  console.log(`plan_delegation_mode: ${status.state.plan_delegation_mode ?? 'unknown'}`);
128
+ console.log(`plan_delegation_recommended_mode: ${status.state.plan_delegation_recommended_mode ?? status.state.plan_delegation_mode ?? 'unknown'}`);
129
+ console.log(`plan_delegation_actual_mode: ${status.state.plan_delegation_actual_mode ?? 'unknown'}`);
130
+ console.log(`plan_delegation_authorization_status: ${status.state.plan_delegation_authorization_status ?? 'unknown'}`);
94
131
  console.log(`plan_delegation_decision_path: ${status.state.plan_delegation_decision_path ?? '(none)'}`);
95
132
  console.log(`source_requirements_status: ${status.state.source_requirements_status ?? 'unknown'}`);
96
133
  console.log(`requirement_traceability_path: ${status.state.requirement_traceability_path ?? '(none)'}`);
@@ -158,7 +195,14 @@ async function main() {
158
195
  try {
159
196
  switch (command) {
160
197
  case 'init': {
161
- const result = await initWorkspace(process.cwd(), { slug: options.get('--slug') || positionals[0] });
198
+ const result = await initWorkspace(process.cwd(), {
199
+ slug: options.get('--slug') || positionals[0],
200
+ agentDelegation: {
201
+ enabled: Boolean(options.get('--enable-agent-delegation') || options.get('--auto-agent-delegation')),
202
+ auto_start: Boolean(options.get('--auto-agent-delegation')),
203
+ threshold: options.get('--agent-delegation-threshold'),
204
+ },
205
+ });
162
206
  console.log(JSON.stringify({ ok: true, command, workspaceRoot: result.workspaceRoot, workflow: result.workflow?.state ?? null }, null, 2));
163
207
  return;
164
208
  }
@@ -167,6 +211,21 @@ async function main() {
167
211
  console.log(JSON.stringify({ ok: true, command, contextSetup }, null, 2));
168
212
  return;
169
213
  }
214
+ case 'install-skills': {
215
+ const installOptions = process.stdin.isTTY && !options.get('--target') && !options.get('--yes')
216
+ ? await promptInstallOptions()
217
+ : installOptionsFromArgs(options);
218
+ if (!installOptions) {
219
+ console.log(JSON.stringify({ ok: false, command, cancelled: true }, null, 2));
220
+ return;
221
+ }
222
+ const result = await installSkillsForTargets({
223
+ ...process.env,
224
+ LOOPX_INSTALL_CWD: process.cwd(),
225
+ }, installOptions);
226
+ console.log(JSON.stringify({ ok: result.ok, command, ...result }, null, 2));
227
+ return;
228
+ }
170
229
  case 'clarify': {
171
230
  const profile = options.get('--deep') ? 'deep' : 'standard';
172
231
  const result = await clarifyStage(process.cwd(), positionals[0], { profile });
@@ -183,7 +242,6 @@ async function main() {
183
242
  }
184
243
  case 'plan': {
185
244
  const result = await planStage(process.cwd(), positionals[0], {
186
- directSpecPath: options.get('--direct'),
187
245
  interactive: Boolean(options.get('--interactive')),
188
246
  deliberate: Boolean(options.get('--deliberate')),
189
247
  });
@@ -245,9 +303,22 @@ async function main() {
245
303
  return;
246
304
  }
247
305
  case 'repair-install': {
248
- const result = await installBundledSkills(process.env);
306
+ const result = await installSkillsForTargets({
307
+ ...process.env,
308
+ LOOPX_INSTALL_CWD: process.cwd(),
309
+ });
249
310
  const ok = result.ok !== false;
250
- console.log(JSON.stringify({ ok, command, ...result }, null, 2));
311
+ const codex = result.results?.codex || {};
312
+ console.log(JSON.stringify({
313
+ ok,
314
+ command,
315
+ ...result,
316
+ installed: codex.installed || [],
317
+ conflicts: codex.conflicts || [],
318
+ skipped: codex.skipped || [],
319
+ templateGovernance: codex.templateGovernance,
320
+ inspection: codex.inspection,
321
+ }, null, 2));
251
322
  if (!ok) {
252
323
  process.exitCode = 1;
253
324
  }
@@ -134,7 +134,7 @@ export async function generateBuildContextManifest({ cwd, root, state, slug }) {
134
134
  row(cwd, { stage: 'build', kind: 'architecture', path: join(root, 'architecture.md'), reason: 'architecture_constraints', priority: 21 }),
135
135
  row(cwd, { stage: 'build', kind: 'development-plan', path: join(root, 'development-plan.md'), reason: 'execution_steps', priority: 22 }),
136
136
  row(cwd, { stage: 'build', kind: 'test-plan', path: join(root, 'test-plan.md'), reason: 'verification_strategy', priority: 23 }),
137
- row(cwd, { stage: 'build', kind: 'prd', path: state.plan_artifact_path || join(cwd, '.loopx', 'plans', `prd-${slug}.md`), reason: 'requirements', priority: 30 }),
137
+ row(cwd, { stage: 'build', kind: 'requirements-snapshot', path: state.plan_artifact_path || join(cwd, '.loopx', 'plans', `requirements-snapshot-${slug}.md`), reason: 'approved_requirements_snapshot', priority: 30 }),
138
138
  row(cwd, { stage: 'build', kind: 'test-spec', path: state.test_spec_artifact_path || join(cwd, '.loopx', 'plans', `test-spec-${slug}.md`), reason: 'test_requirements', priority: 31 }),
139
139
  row(cwd, { stage: 'build', kind: 'vertical-slices', path: state.change_artifact_paths?.slices || join(cwd, '.loopx', 'changes', 'active', state.change_id || `chg-${slug}`, 'slices.json'), reason: 'end_to_end_delivery_slices', priority: 32 }),
140
140
  row(cwd, { stage: 'build', kind: 'review-rework', path: reviewReworkPath, reason: 'review_requested_implementation_fixes', priority: 33, required: requiresReviewRework }),
@@ -155,7 +155,7 @@ export async function generateReviewContextManifest({ cwd, root, state, slug })
155
155
  row(cwd, { stage: 'review', kind: 'source-requirements', path: state.plan_source_spec_path || join(root, 'spec.md'), reason: 'original_requirements_source_of_truth', priority: 11 }),
156
156
  row(cwd, { stage: 'review', kind: 'requirement-traceability', path: state.requirement_traceability_path || join(root, 'requirement-traceability.md'), reason: 'source_requirement_coverage_gate', priority: 12 }),
157
157
  row(cwd, { stage: 'review', kind: 'test-spec', path: state.test_spec_artifact_path || join(cwd, '.loopx', 'plans', `test-spec-${slug}.md`), reason: 'acceptance_tests', priority: 20 }),
158
- row(cwd, { stage: 'review', kind: 'prd', path: state.plan_artifact_path || join(cwd, '.loopx', 'plans', `prd-${slug}.md`), reason: 'requirements', priority: 21 }),
158
+ row(cwd, { stage: 'review', kind: 'requirements-snapshot', path: state.plan_artifact_path || join(cwd, '.loopx', 'plans', `requirements-snapshot-${slug}.md`), reason: 'approved_requirements_snapshot', priority: 21 }),
159
159
  row(cwd, { stage: 'review', kind: 'vertical-slices', path: state.change_artifact_paths?.slices || join(cwd, '.loopx', 'changes', 'active', state.change_id || `chg-${slug}`, 'slices.json'), reason: 'slice_verification_contract', priority: 22 }),
160
160
  row(cwd, { stage: 'review', kind: 'domain-context', path: contextPaths.domainGlossary, reason: 'terminology_and_boundary_review', priority: 23, required: contextSetup.status !== 'missing' }),
161
161
  row(cwd, { stage: 'review', kind: 'changed-files', path: join(root, 'review-support', 'changed-files.json'), reason: 'changed_file_evidence', priority: 25, required: false }),
@@ -1,8 +1,7 @@
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 = [
@@ -24,7 +23,7 @@ const WORKFLOW_ARTIFACTS = [
24
23
 
25
24
  const PAGE_GROUPS = [
26
25
  { file: 'intake.html', title: '需求澄清', artifacts: ['spec'] },
27
- { file: 'plan.html', title: '计划与架构', artifacts: ['plan', 'architecture', 'development-plan', 'test-plan', 'requirement-traceability', 'plan-delegation-decision'] },
26
+ { file: 'plan.html', title: '计划与架构', artifacts: ['plan', 'architecture', 'development-plan', 'test-plan', 'requirement-traceability'] },
28
27
  { file: 'change.html', title: '变更设计方案', artifacts: ['change-proposal', 'change-spec-delta', 'change-design', 'change-tasks', 'change-slices'] },
29
28
  { file: 'build.html', title: '执行与验证', artifacts: ['execution-record'] },
30
29
  { file: 'review.html', title: '评审结论', artifacts: ['review-report'] },
@@ -148,19 +147,15 @@ function resolveArtifactPath(root, state, artifact) {
148
147
  if (artifact.changeKey) {
149
148
  return state.change_artifact_paths?.[artifact.changeKey] || null;
150
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
+ }
151
156
  return join(root, artifact.name);
152
157
  }
153
158
 
154
- function statusBadge(label, status) {
155
- const normalized = String(status ?? '').toLowerCase();
156
- const cls = ['true', 'complete', 'approved', 'written', 'ready', 'exists', 'ok'].includes(normalized)
157
- ? 'ok'
158
- : ['false', 'missing', 'blocked', 'partial', 'failed', 'none', 'needs-review'].includes(normalized)
159
- ? 'bad'
160
- : 'warn';
161
- return `<span class="badge ${cls}">${escapeHtml(label)}: ${escapeHtml(status)}</span>`;
162
- }
163
-
164
159
  function extractHeadings(text) {
165
160
  return String(text || '')
166
161
  .split('\n')
@@ -287,14 +282,6 @@ function parseSlicesJson(text) {
287
282
  }
288
283
  }
289
284
 
290
- function listItems(items) {
291
- const values = Array.isArray(items) ? items.filter(Boolean) : [];
292
- if (values.length === 0) {
293
- return '<span class="muted">无</span>';
294
- }
295
- return values.map((item) => `<span class="badge">${escapeHtml(item)}</span>`).join(' ');
296
- }
297
-
298
285
  function markdownToHtml(markdown) {
299
286
  const lines = String(markdown || '').split('\n');
300
287
  const html = [];
@@ -401,6 +388,59 @@ function markdownToHtml(markdown) {
401
388
  return html.join('\n');
402
389
  }
403
390
 
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, '切片定义');
405
+ }
406
+
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
+
404
444
  function artifactDetailHtml(artifact, text) {
405
445
  const blocks = collectHeadingBlocks(text);
406
446
  const topBlocks = blocks.filter((block) => block.level <= 3).slice(0, 12);
@@ -482,85 +522,41 @@ function artifactDetailHtml(artifact, text) {
482
522
  return sectionCards;
483
523
  }
484
524
 
485
- function artifactLink(viewRoot, artifactPath, label) {
486
- const href = relative(viewRoot, artifactPath).replaceAll('\\', '/');
487
- return `<a href="${escapeHtml(href)}">${escapeHtml(label)}</a>`;
488
- }
489
-
490
- function approvalPanels(state) {
491
- const approval = state.approval || {};
492
- const slug = state.slug || '(slug)';
493
- const transitions = [
494
- ['clarify -> plan', approval.plan || 'not-requested', `loopx approve ${slug} --from clarify --to plan`],
495
- ['plan -> build', approval.build || 'not-requested', `loopx approve ${slug} --from plan --to build`],
496
- ['build -> review', approval.review || 'not-requested', `loopx approve ${slug} --from build --to review`],
497
- ['review -> done', approval.complete || 'not-requested', `loopx approve ${slug} --from review --to done`],
498
- ];
499
- return [
500
- '<section class="panel">',
501
- '<h2>人工确认点</h2>',
502
- '<table><thead><tr><th>阶段流转</th><th>授权状态</th><th>命令</th></tr></thead><tbody>',
503
- ...transitions.map(([label, status, command]) => [
504
- '<tr>',
505
- `<td>${escapeHtml(label)}</td>`,
506
- `<td>${statusBadge('approval', status)}</td>`,
507
- `<td><code>${escapeHtml(command)}</code></td>`,
508
- '</tr>',
509
- ].join('')),
510
- '</tbody></table>',
511
- '</section>',
512
- ].join('\n');
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() || '技术方案';
513
531
  }
514
532
 
515
- function planGateSummary(state) {
516
- const gates = [
517
- ['Planner / Architect / Critic', `${state.plan_architect_review_status || 'not-started'} / ${state.plan_critic_verdict || 'none'}`],
518
- ['中文规划文档', state.plan_docs_status || 'missing'],
519
- ['需求覆盖矩阵', state.source_requirements_status || 'unknown'],
520
- ['变更工件', state.change_artifacts_status || 'missing'],
521
- ['Spec Delta', state.spec_delta_status || 'missing'],
522
- ['Vertical Slices', state.slice_artifacts_status || 'missing'],
523
- ['委派决策', state.plan_delegation_mode || 'unknown'],
524
- ];
525
- return [
526
- '<section class="panel">',
527
- '<h2>Plan 审阅门禁</h2>',
528
- '<table><thead><tr><th>门禁</th><th>状态</th></tr></thead><tbody>',
529
- ...gates.map(([label, value]) => `<tr><td>${escapeHtml(label)}</td><td>${escapeHtml(value)}</td></tr>`),
530
- '</tbody></table>',
531
- '</section>',
532
- ].join('\n');
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 '本页汇总相关方案内容。';
533
550
  }
534
551
 
535
- function statusPanels(status) {
536
- const state = status.state || {};
537
- const readiness = state.readiness || {};
538
- const authorization = state.authorization || {};
539
- const nextSkill = nextSkillCommand(state);
540
- return [
541
- '<section class="grid">',
542
- `<div class="panel"><strong>阶段</strong><br>${escapeHtml(state.current_stage || '(none)')}</div>`,
543
- `<div class="panel"><strong>状态</strong><br>${escapeHtml(state.stage_status || '(unknown)')}</div>`,
544
- `<div class="panel"><strong>需求覆盖</strong><br>${escapeHtml(state.source_requirements_status || 'unknown')}</div>`,
545
- `<div class="panel"><strong>下一步</strong><br><code>${escapeHtml(nextSkill || status.next_action || 'none')}</code></div>`,
546
- `<div class="panel"><strong>归档</strong><br>${escapeHtml(state.archive_status || 'pending')}</div>`,
547
- '</section>',
548
- approvalPanels(state),
549
- state.current_stage === 'plan' || state.plan_package_status ? planGateSummary(state) : '',
550
- '<section class="panel">',
551
- '<h2>readiness / authorization</h2>',
552
- '<table><thead><tr><th>关卡</th><th>ready</th><th>authorized</th><th>blockers</th></tr></thead><tbody>',
553
- ...['plan', 'build', 'review', 'done', 'archive'].map((key) => [
554
- '<tr>',
555
- `<td>${escapeHtml(key)}</td>`,
556
- `<td>${escapeHtml(readiness[key]?.ready ?? false)}</td>`,
557
- `<td>${escapeHtml(authorization[key]?.authorized ?? false)}</td>`,
558
- `<td>${listItems(readiness[key]?.blockers || [])}</td>`,
559
- '</tr>',
560
- ].join('')),
561
- '</tbody></table>',
562
- '</section>',
563
- ].join('\n');
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);
564
560
  }
565
561
 
566
562
  async function renderWorkflowPages(status) {
@@ -579,30 +575,19 @@ async function renderWorkflowPages(status) {
579
575
 
580
576
  const renderArtifactBody = async (artifact) => {
581
577
  const text = await readFile(artifact.path, 'utf8');
582
- const metrics = artifactMetrics(text);
583
- const digest = sectionDigest(text);
578
+ const displayText = displayTextForArtifact(artifact, text);
579
+ const metrics = artifactMetrics(displayText);
580
+ const digest = sectionDigest(displayText);
584
581
  const isJson = artifact.name.endsWith('.json');
585
- const detailHtml = artifactDetailHtml(artifact, text);
586
- const outline = digest.outline.length > 0
587
- ? `<div class="outline">${digest.outline.map((item) => `<span class="badge">${escapeHtml('H'.repeat(item.level))} ${escapeHtml(item.title)}</span>`).join(' ')}</div>`
588
- : '<span class="muted">无可提取标题</span>';
582
+ const detailHtml = artifactDetailHtml(artifact, displayText);
589
583
  return {
590
- text,
584
+ text: displayText,
591
585
  metrics,
592
586
  digest,
593
587
  detailHtml,
594
588
  body: isJson
595
- ? `<pre><code>${escapeHtml(text)}</code></pre>`
596
- : markdownToHtml(text),
597
- meta: [
598
- statusBadge('产物', artifact.name),
599
- statusBadge('中文', metrics.chineseOk ? 'ok' : 'needs-review'),
600
- statusBadge('行数', metrics.lines),
601
- statusBadge('标题', metrics.headings),
602
- statusBadge('表格', metrics.tables),
603
- metrics.todoCount > 0 ? statusBadge('占位符', metrics.todoCount) : '',
604
- ].join(''),
605
- digestHtml: outline,
589
+ ? `<pre><code>${escapeHtml(displayText)}</code></pre>`
590
+ : markdownToHtml(displayText),
606
591
  };
607
592
  };
608
593
 
@@ -620,33 +605,20 @@ async function renderWorkflowPages(status) {
620
605
  const rendered = await renderArtifactBody(artifact);
621
606
  sections.push([
622
607
  `<section class="markdown" id="${escapeHtml(anchorForArtifact(artifact))}">`,
623
- '<div class="hero-grid">',
624
- '<div>',
625
608
  `<h2>${escapeHtml(artifact.label)}</h2>`,
626
- `<p class="muted">${artifactLink(viewRoot, artifact.path, artifact.name)}</p>`,
627
- `<p>${escapeHtml(rendered.digest.summary || '该产物没有单独摘要,直接阅读正文。')}</p>`,
628
- '</div>',
629
- '<div class="stack">',
630
- rendered.meta,
631
- `<div class="panel"><strong>结构预览</strong><div class="outline">${rendered.digestHtml}</div></div>`,
632
- '</div>',
633
- '</div>',
634
- '<div class="artifact-meta">',
635
- rendered.meta,
636
- '</div>',
609
+ `<p>${escapeHtml(rendered.digest.summary || '该部分正文如下。')}</p>`,
637
610
  rendered.detailHtml,
638
611
  rendered.body,
639
612
  '</section>',
640
613
  ].join('\n'));
641
614
  }
642
615
  await writeFile(join(viewRoot, group.file), htmlDoc({
643
- title: `${group.title} - ${status.slug}`,
616
+ title: group.title,
644
617
  body: [
645
618
  '<header>',
646
619
  `<h1>${escapeHtml(group.title)}</h1>`,
647
- `<p class="muted">工作流:${escapeHtml(status.slug)}</p>`,
648
- '<div class="callout">这是人工审阅入口。先看上方视觉摘要,再按导航逐项审阅正文;HTML 负责可视化阅读,原始文件仍可点击查看。</div>',
649
- '<p><a href="index.html">返回工作流首页</a></p>',
620
+ `<div class="callout">${escapeHtml(pageIntro(group))}</div>`,
621
+ '<p><a href="index.html">返回方案总览</a></p>',
650
622
  nav,
651
623
  '</header>',
652
624
  sections.length > 0 ? sections.join('\n') : '<section class="panel muted">暂无对应产物。</section>',
@@ -654,66 +626,38 @@ async function renderWorkflowPages(status) {
654
626
  }));
655
627
  }
656
628
 
657
- const artifactTableRows = await Promise.all(artifactRows.map(async (artifact) => {
658
- if (!artifact.exists) {
659
- return [
660
- '<tr>',
661
- `<td>${escapeHtml(artifact.label)}</td>`,
662
- '<td>缺失</td>',
663
- '<td><span class="muted">无</span></td>',
664
- '<td><span class="muted">无</span></td>',
665
- '<td><span class="muted">无</span></td>',
666
- '<td><span class="muted">无</span></td>',
667
- '</tr>',
668
- ].join('');
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 '';
669
634
  }
670
- const rendered = await renderArtifactBody(artifact);
671
635
  return [
672
636
  '<tr>',
673
- `<td>${escapeHtml(artifact.label)}</td>`,
674
- '<td>存在</td>',
675
- `<td>${artifact.page ? `<a href="${escapeHtml(artifact.page)}">${escapeHtml(basename(artifact.page))}</a>` : '<span class="muted">无</span>'}</td>`,
676
- `<td>${rendered.meta}</td>`,
677
- `<td>${escapeHtml(rendered.metrics.headings)} 标题 / ${escapeHtml(rendered.metrics.tables)} 表格 / ${escapeHtml(rendered.metrics.lines)} 行</td>`,
678
- `<td>${artifactLink(viewRoot, artifact.path, artifact.name)}</td>`,
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>`,
679
640
  '</tr>',
680
641
  ].join('');
681
- }));
682
-
683
- const heroSummary = [
684
- `<div class="hero">`,
685
- '<div class="hero-grid">',
686
- '<div>',
687
- `<h1>工作流 ${escapeHtml(status.slug)}</h1>`,
688
- `<p class="muted">HTML 是派生阅读视图;Markdown 和 JSON 仍是运行时事实源。</p>`,
689
- '<div class="hero-kpi">',
690
- `<div class="kpi"><div class="label">阶段</div><div class="value">${escapeHtml(status.state?.current_stage || '(none)')}</div></div>`,
691
- `<div class="kpi"><div class="label">状态</div><div class="value">${escapeHtml(status.state?.stage_status || '(unknown)')}</div></div>`,
692
- `<div class="kpi"><div class="label">中文产物</div><div class="value">${escapeHtml(status.state?.plan_docs_status || 'unknown')}</div></div>`,
693
- `<div class="kpi"><div class="label">下一步</div><div class="value">${escapeHtml(status.next_action || 'none')}</div></div>`,
694
- '</div>',
695
- '</div>',
696
- '<div class="stack">',
697
- approvalPanels(status.state || {}),
698
- '</div>',
699
- '</div>',
700
- '</div>',
701
- ].join('\n');
642
+ }).filter(Boolean);
702
643
 
703
644
  const indexBody = [
704
- heroSummary,
705
- statusPanels(status),
645
+ '<header>',
646
+ '<h1>技术方案总览</h1>',
647
+ `<p class="muted">${escapeHtml(title)}</p>`,
648
+ '<div class="callout">本页面提供完整 HTML 阅读入口;各页面已内嵌方案正文,可直接阅读和确认。</div>',
649
+ '</header>',
706
650
  '<section class="panel">',
707
- '<h2>关键产物审阅清单</h2>',
708
- '<table><thead><tr><th>产物</th><th>状态</th><th>阅读视图</th><th>中文</th><th>结构</th><th>原始文件</th></tr></thead><tbody>',
709
- ...artifactTableRows,
651
+ '<h2>方案阅读目录</h2>',
652
+ '<table><thead><tr><th>页面</th><th>包含内容</th><th>阅读重点</th></tr></thead><tbody>',
653
+ ...pageRows,
710
654
  '</tbody></table>',
711
655
  '</section>',
712
656
  ].join('\n');
713
657
 
714
658
  const workflowViewPath = join(viewRoot, 'index.html');
715
659
  await writeFile(workflowViewPath, htmlDoc({
716
- title: `loopx 工作流 ${status.slug}`,
660
+ title: '技术方案总览',
717
661
  body: indexBody,
718
662
  }));
719
663
 
@@ -729,27 +673,20 @@ async function renderWorkspaceIndex(workspaceStatus, renderedSlugs = []) {
729
673
  const link = rendered.has(workflow.slug)
730
674
  ? `<a href="${escapeHtml(href)}">${escapeHtml(workflow.slug)}</a>`
731
675
  : escapeHtml(workflow.slug);
732
- return [
733
- '<tr>',
734
- `<td>${link}</td>`,
735
- `<td>${escapeHtml(workflow.current_stage || '(none)')}</td>`,
736
- `<td>${escapeHtml(workflow.contract)}</td>`,
737
- `<td>${escapeHtml(workflow.missing_artifact_count)}</td>`,
738
- '</tr>',
739
- ].join('');
676
+ return `<tr><td>${link}</td><td>技术方案总览</td></tr>`;
740
677
  });
741
678
  const workspaceViewPath = join(viewsRoot, 'index.html');
742
679
  await writeFile(workspaceViewPath, htmlDoc({
743
- title: 'loopx 工作台',
680
+ title: '方案阅读入口',
744
681
  body: [
745
682
  '<header>',
746
- '<h1>loopx 工作台</h1>',
747
- `<p class="muted">工作区:${escapeHtml(workspaceStatus.workspaceRoot)}</p>`,
683
+ '<h1>方案阅读入口</h1>',
684
+ '<p class="muted">选择一个需求方案,进入完整 HTML 阅读页。</p>',
748
685
  '</header>',
749
686
  '<section class="panel">',
750
- '<h2>工作流</h2>',
751
- '<table><thead><tr><th>工作流</th><th>阶段</th><th>契约</th><th>缺失产物数</th></tr></thead><tbody>',
752
- 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>',
753
690
  '</tbody></table>',
754
691
  '</section>',
755
692
  ].join('\n'),
@@ -766,9 +703,6 @@ export async function renderHtmlViews(cwd, { slug = null, all = false } = {}) {
766
703
  const workflowViews = [];
767
704
  if (all || !slug) {
768
705
  for (const workflow of workspaceStatus.workflows) {
769
- if (workflow.legacy) {
770
- continue;
771
- }
772
706
  const workflowStatus = await statusSummary(cwd, workflow.slug);
773
707
  workflowViews.push({
774
708
  slug: workflow.slug,
@@ -777,7 +711,7 @@ export async function renderHtmlViews(cwd, { slug = null, all = false } = {}) {
777
711
  }
778
712
  } else if (slug) {
779
713
  const workflowStatus = await statusSummary(cwd, slug);
780
- if (!workflowStatus.state || workflowStatus.legacy) {
714
+ if (!workflowStatus.state) {
781
715
  throw new Error('render_workflow_not_available');
782
716
  }
783
717
  workflowViews.push({