@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.
- package/AGENTS.md +49 -0
- package/README.md +69 -448
- package/README.zh-CN.md +69 -459
- package/docs/loopx/design/loopx-skill-suite-v1-design.md +80 -0
- package/docs/loopx/plans/loopx-skill-suite-v1-implementation.md +81 -0
- package/package.json +7 -3
- package/plugins/loopx/.codex-plugin/plugin.json +4 -4
- package/plugins/loopx/skills/clarify/SKILL.md +38 -311
- package/plugins/loopx/skills/debug/SKILL.md +1 -1
- package/plugins/loopx/skills/exec/SKILL.md +71 -0
- package/plugins/loopx/skills/finish/SKILL.md +349 -0
- package/plugins/loopx/skills/fix-review/SKILL.md +216 -0
- package/plugins/loopx/skills/go-style/SKILL.md +2 -2
- package/plugins/loopx/skills/kratos/SKILL.md +1 -1
- package/plugins/loopx/skills/plan/SKILL.md +138 -271
- package/plugins/loopx/skills/refactor-plan/SKILL.md +71 -0
- package/plugins/loopx/skills/review/SKILL.md +72 -105
- package/plugins/loopx/skills/review/code-reviewer.md +168 -0
- package/plugins/loopx/skills/spec/DESIGN_SPEC_TEMPLATE.md +323 -0
- package/plugins/loopx/skills/spec/SKILL.md +76 -0
- package/plugins/loopx/skills/subagent-exec/SKILL.md +282 -0
- package/plugins/loopx/skills/subagent-exec/agents/openai.yaml +3 -0
- package/plugins/loopx/skills/subagent-exec/code-quality-reviewer-prompt.md +25 -0
- package/plugins/loopx/skills/subagent-exec/codex-subagents.md +37 -0
- package/plugins/loopx/skills/subagent-exec/implementer-prompt.md +113 -0
- package/plugins/loopx/skills/subagent-exec/spec-reviewer-prompt.md +61 -0
- package/plugins/loopx/skills/tdd/SKILL.md +1 -1
- package/plugins/loopx/skills/verify/SKILL.md +1 -1
- package/scripts/claude-workflow-hook.mjs +109 -0
- package/scripts/codex-workflow-hook.mjs +2 -5
- package/scripts/install-skills.mjs +3 -3
- package/scripts/verify-skills.mjs +34 -2
- package/skills/RESOLVER.md +22 -17
- package/skills/clarify/SKILL.md +38 -311
- package/skills/debug/SKILL.md +1 -1
- package/skills/exec/SKILL.md +71 -0
- package/skills/finish/SKILL.md +349 -0
- package/skills/fix-review/SKILL.md +216 -0
- package/skills/go-style/SKILL.md +2 -2
- package/skills/kratos/SKILL.md +1 -1
- package/skills/plan/SKILL.md +138 -271
- package/skills/refactor-plan/SKILL.md +71 -0
- package/skills/review/SKILL.md +72 -105
- package/skills/review/code-reviewer.md +168 -0
- package/skills/spec/DESIGN_SPEC_TEMPLATE.md +323 -0
- package/skills/spec/SKILL.md +76 -0
- package/skills/subagent-exec/SKILL.md +282 -0
- package/skills/subagent-exec/agents/openai.yaml +3 -0
- package/skills/subagent-exec/code-quality-reviewer-prompt.md +25 -0
- package/skills/subagent-exec/codex-subagents.md +37 -0
- package/skills/subagent-exec/implementer-prompt.md +113 -0
- package/skills/subagent-exec/spec-reviewer-prompt.md +61 -0
- package/skills/tdd/SKILL.md +1 -1
- package/skills/verify/SKILL.md +1 -1
- package/src/autopilot-runtime.mjs +1 -1
- package/src/cli.mjs +78 -7
- package/src/context-manifest.mjs +2 -2
- package/src/html-views.mjs +129 -195
- package/src/install-discovery.mjs +210 -6
- package/src/next-skill.mjs +2 -4
- package/src/plan-runtime.mjs +219 -93
- package/src/runtime-maintenance.mjs +5 -2
- package/src/workflow.mjs +749 -71
- package/templates/architecture.md +58 -16
- package/templates/development-plan.md +42 -12
- package/plugins/loopx/scripts/plugin-install.test.mjs +0 -125
- package/plugins/loopx/skills/archive/SKILL.md +0 -55
- package/plugins/loopx/skills/autopilot/SKILL.md +0 -93
- package/plugins/loopx/skills/build/SKILL.md +0 -228
- package/skills/ai-slop-cleaner/SKILL.md +0 -114
- package/skills/archive/SKILL.md +0 -55
- package/skills/autopilot/SKILL.md +0 -93
- package/skills/autoresearch/SKILL.md +0 -68
- package/skills/build/SKILL.md +0 -228
- package/skills/deep-interview/SKILL.md +0 -461
- package/skills/ralph/SKILL.md +0 -271
- 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] [--
|
|
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(), {
|
|
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
|
|
306
|
+
const result = await installSkillsForTargets({
|
|
307
|
+
...process.env,
|
|
308
|
+
LOOPX_INSTALL_CWD: process.cwd(),
|
|
309
|
+
});
|
|
249
310
|
const ok = result.ok !== false;
|
|
250
|
-
|
|
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
|
}
|
package/src/context-manifest.mjs
CHANGED
|
@@ -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: '
|
|
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: '
|
|
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 }),
|
package/src/html-views.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
-
import {
|
|
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'
|
|
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
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
|
583
|
-
const
|
|
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,
|
|
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(
|
|
596
|
-
: markdownToHtml(
|
|
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
|
|
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:
|
|
616
|
+
title: group.title,
|
|
644
617
|
body: [
|
|
645
618
|
'<header>',
|
|
646
619
|
`<h1>${escapeHtml(group.title)}</h1>`,
|
|
647
|
-
`<
|
|
648
|
-
'<
|
|
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
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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(
|
|
674
|
-
'
|
|
675
|
-
`<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
|
-
|
|
705
|
-
|
|
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
|
|
708
|
-
'<table><thead><tr><th
|
|
709
|
-
...
|
|
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:
|
|
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: '
|
|
680
|
+
title: '方案阅读入口',
|
|
744
681
|
body: [
|
|
745
682
|
'<header>',
|
|
746
|
-
'<h1
|
|
747
|
-
|
|
683
|
+
'<h1>方案阅读入口</h1>',
|
|
684
|
+
'<p class="muted">选择一个需求方案,进入完整 HTML 阅读页。</p>',
|
|
748
685
|
'</header>',
|
|
749
686
|
'<section class="panel">',
|
|
750
|
-
'<h2
|
|
751
|
-
'<table><thead><tr><th
|
|
752
|
-
rows.join('\n') || '<tr><td colspan="
|
|
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
|
|
714
|
+
if (!workflowStatus.state) {
|
|
781
715
|
throw new Error('render_workflow_not_available');
|
|
782
716
|
}
|
|
783
717
|
workflowViews.push({
|