@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/workflow.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { cp, mkdir, readFile, readdir, rename, writeFile } from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
- import { basename, dirname, join, relative, resolve } from 'node:path';
3
+ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
6
6
  import { AUTOPILOT_PHASES, createDefaultAutopilotAdapter } from './autopilot-runtime.mjs';
@@ -67,6 +67,14 @@ const CLARIFY_PROFILES = {
67
67
  maxRounds: 25,
68
68
  },
69
69
  };
70
+ const DELEGATION_MODES = ['local', 'critic-only', 'parallel-review'];
71
+ const DEFAULT_AGENT_DELEGATION_CONFIG = {
72
+ enabled: false,
73
+ auto_start: false,
74
+ threshold: 'critic-only',
75
+ plan_parallelism: 'review-only',
76
+ build_parallelism: 'disjoint-only',
77
+ };
70
78
 
71
79
  function normalizeSlug(raw) {
72
80
  const slug = String(raw || '')
@@ -83,7 +91,7 @@ function normalizeSlug(raw) {
83
91
  function slugFromBuildInput(raw) {
84
92
  const value = String(raw || '');
85
93
  const name = basename(value);
86
- const match = /^prd-(.+)\.md$/.exec(name);
94
+ const match = /^(?:requirements-snapshot|prd)-(.+)\.md$/.exec(name);
87
95
  return match ? normalizeSlug(match[1]) : normalizeSlug(value);
88
96
  }
89
97
 
@@ -129,6 +137,33 @@ function normalizeClarifyProfile(raw) {
129
137
  return value;
130
138
  }
131
139
 
140
+ function normalizeDelegationThreshold(raw) {
141
+ const value = String(raw || DEFAULT_AGENT_DELEGATION_CONFIG.threshold).trim().toLowerCase();
142
+ if (!DELEGATION_MODES.includes(value)) {
143
+ throw new Error(`invalid_agent_delegation_threshold:${value}`);
144
+ }
145
+ return value;
146
+ }
147
+
148
+ function delegationModeRank(mode) {
149
+ return DELEGATION_MODES.indexOf(String(mode || 'local'));
150
+ }
151
+
152
+ function delegationMeetsThreshold(mode, threshold) {
153
+ return delegationModeRank(mode) >= delegationModeRank(normalizeDelegationThreshold(threshold));
154
+ }
155
+
156
+ function normalizeAgentDelegationConfig(raw = {}) {
157
+ const candidate = raw && typeof raw === 'object' ? raw : {};
158
+ return {
159
+ enabled: candidate.enabled === true,
160
+ auto_start: candidate.auto_start === true,
161
+ threshold: normalizeDelegationThreshold(candidate.threshold),
162
+ plan_parallelism: String(candidate.plan_parallelism || DEFAULT_AGENT_DELEGATION_CONFIG.plan_parallelism),
163
+ build_parallelism: String(candidate.build_parallelism || DEFAULT_AGENT_DELEGATION_CONFIG.build_parallelism),
164
+ };
165
+ }
166
+
132
167
  function parseFrontmatter(text) {
133
168
  if (!text.startsWith('---\n')) {
134
169
  return {};
@@ -170,6 +205,158 @@ function parseFrontmatter(text) {
170
205
  return result;
171
206
  }
172
207
 
208
+ const PLAN_SOURCE_DOCUMENT_KEYS = [
209
+ 'source_product_doc',
210
+ 'source_prototype_doc',
211
+ 'source_prototype_html',
212
+ 'product_doc',
213
+ 'prototype_doc',
214
+ 'prototype_html',
215
+ ];
216
+ const MAX_PLAN_SOURCE_DOCUMENT_CHARS = 30000;
217
+ const MAX_PLAN_SOURCE_HTML_CHARS = 8000;
218
+ const MAX_PLAN_SOURCE_BUNDLE_CHARS = 70000;
219
+
220
+ function sourceDocumentPathsFromSpecAndState(sourceSpecPath, sourceText, state = {}) {
221
+ const meta = parseFrontmatter(sourceText);
222
+ const candidates = [];
223
+ const pushValue = (value) => {
224
+ if (Array.isArray(value)) {
225
+ for (const item of value) {
226
+ pushValue(item);
227
+ }
228
+ return;
229
+ }
230
+ if (typeof value === 'string' && value.trim()) {
231
+ candidates.push(value.trim());
232
+ }
233
+ };
234
+
235
+ for (const key of PLAN_SOURCE_DOCUMENT_KEYS) {
236
+ pushValue(meta[key]);
237
+ pushValue(state?.source_context?.[key]);
238
+ }
239
+
240
+ return dedupeStrings(candidates).map((candidate) => {
241
+ if (isAbsolute(candidate)) {
242
+ return candidate;
243
+ }
244
+ return resolve(dirname(sourceSpecPath), candidate);
245
+ });
246
+ }
247
+
248
+ function htmlToPlanningText(text) {
249
+ return String(text || '')
250
+ .replace(/<script[\s\S]*?<\/script>/gi, '\n')
251
+ .replace(/<style[\s\S]*?<\/style>/gi, '\n')
252
+ .replace(/<h1[^>]*>/gi, '\n# ')
253
+ .replace(/<h2[^>]*>/gi, '\n## ')
254
+ .replace(/<h3[^>]*>/gi, '\n### ')
255
+ .replace(/<h4[^>]*>/gi, '\n#### ')
256
+ .replace(/<h[5-6][^>]*>/gi, '\n#### ')
257
+ .replace(/<\/h[1-6]>/gi, '\n')
258
+ .replace(/<tr[^>]*>/gi, '\n')
259
+ .replace(/<\/tr>/gi, ' |\n')
260
+ .replace(/<t[dh][^>]*>/gi, '| ')
261
+ .replace(/<\/t[dh]>/gi, ' ')
262
+ .replace(/<\/(p|li|table|section|article|div)>/gi, '\n')
263
+ .replace(/<[^>]+>/g, ' ')
264
+ .replace(/&nbsp;/g, ' ')
265
+ .replace(/&amp;/g, '&')
266
+ .replace(/&lt;/g, '<')
267
+ .replace(/&gt;/g, '>')
268
+ .replace(/\n{3,}/g, '\n\n')
269
+ .trim();
270
+ }
271
+
272
+ function trimPlanSourceDocument(text) {
273
+ const value = String(text || '').trim();
274
+ if (value.length <= MAX_PLAN_SOURCE_DOCUMENT_CHARS) {
275
+ return value;
276
+ }
277
+ return [
278
+ value.slice(0, MAX_PLAN_SOURCE_DOCUMENT_CHARS),
279
+ '',
280
+ `...已截断,原始来源文档超过 ${MAX_PLAN_SOURCE_DOCUMENT_CHARS} 字符;plan 仍必须优先覆盖前文已提取的需求表、字段和流程。`,
281
+ ].join('\n');
282
+ }
283
+
284
+ function compactPlanningText(text, { html = false } = {}) {
285
+ const source = html ? htmlToPlanningText(text) : String(text || '');
286
+ const maxChars = html ? MAX_PLAN_SOURCE_HTML_CHARS : MAX_PLAN_SOURCE_DOCUMENT_CHARS;
287
+ const kept = [];
288
+ let total = 0;
289
+ let previousWasBlank = false;
290
+ const keepLine = (line) => {
291
+ const trimmed = line.trim();
292
+ if (!trimmed) {
293
+ return false;
294
+ }
295
+ return /^#{1,6}\s+/.test(trimmed)
296
+ || trimmed.startsWith('|')
297
+ || /^[-*]\s+/.test(trimmed)
298
+ || /^\d+[.)]\s+/.test(trimmed)
299
+ || /MUST|SHALL|必须|不得|不能|不自动|人工|确认|复核|执行|下发|任务|字段|状态|流程|规则|范围|来源|验收|示例|异常|差异|mock|API|接口|持久化|页面|明细|日志|权限/i.test(trimmed);
300
+ };
301
+
302
+ for (const line of source.split('\n')) {
303
+ const trimmed = line.trim();
304
+ if (!keepLine(line)) {
305
+ continue;
306
+ }
307
+ if (!previousWasBlank && kept.length > 0 && /^#{1,6}\s+/.test(trimmed)) {
308
+ kept.push('');
309
+ total += 1;
310
+ previousWasBlank = true;
311
+ }
312
+ if (total + trimmed.length + 1 > maxChars) {
313
+ kept.push(`...已压缩截断,来源文档高信号内容超过 ${maxChars} 字符。`);
314
+ break;
315
+ }
316
+ kept.push(trimmed);
317
+ total += trimmed.length + 1;
318
+ previousWasBlank = false;
319
+ }
320
+
321
+ const compacted = kept.join('\n').trim();
322
+ return compacted || trimPlanSourceDocument(source);
323
+ }
324
+
325
+ async function readPlanSourceText(cwd, state, sourceSpecPath) {
326
+ const sourceText = await readFile(sourceSpecPath, 'utf8');
327
+ const sourceDocumentPaths = sourceDocumentPathsFromSpecAndState(sourceSpecPath, sourceText, state);
328
+ if (sourceDocumentPaths.length === 0) {
329
+ return { sourceText, sourceDocumentPaths: [] };
330
+ }
331
+
332
+ const parts = [sourceText.trimEnd()];
333
+ const loaded = [];
334
+ for (const path of sourceDocumentPaths) {
335
+ if (!existsSync(path)) {
336
+ continue;
337
+ }
338
+ const raw = await readFile(path, 'utf8');
339
+ const body = compactPlanningText(raw, { html: /\.html?$/i.test(path) });
340
+ loaded.push(path);
341
+ parts.push([
342
+ '',
343
+ `# 引用源文档:${relative(cwd, path) || path}`,
344
+ '',
345
+ trimPlanSourceDocument(body),
346
+ ].join('\n'));
347
+ const currentLength = parts.join('\n\n').length;
348
+ if (currentLength >= MAX_PLAN_SOURCE_BUNDLE_CHARS) {
349
+ parts.push(`\n# 引用源文档截断说明\n\n源文档合并内容超过 ${MAX_PLAN_SOURCE_BUNDLE_CHARS} 字符,后续文档未继续注入 planner prompt。`);
350
+ break;
351
+ }
352
+ }
353
+
354
+ return {
355
+ sourceText: parts.join('\n\n').slice(0, MAX_PLAN_SOURCE_BUNDLE_CHARS),
356
+ sourceDocumentPaths: loaded,
357
+ };
358
+ }
359
+
173
360
  function frontmatterBlock(values) {
174
361
  const lines = ['---'];
175
362
  for (const [key, value] of Object.entries(values)) {
@@ -315,6 +502,11 @@ export async function readWorkspaceConfig(cwd) {
315
502
  return JSON.parse(await readFile(path, 'utf8'));
316
503
  }
317
504
 
505
+ async function readAgentDelegationConfig(cwd) {
506
+ const config = await readWorkspaceConfig(cwd);
507
+ return normalizeAgentDelegationConfig(config?.agent_delegation);
508
+ }
509
+
318
510
  export async function readState(cwd, slug) {
319
511
  const path = statePath(resolveWorkflowRoot(cwd, slug));
320
512
  if (!existsSync(path)) {
@@ -418,6 +610,12 @@ function createInitialState(slug, profile) {
418
610
  plan_docs_artifact_paths: null,
419
611
  plan_delegation_decision_path: null,
420
612
  plan_delegation_mode: 'local',
613
+ plan_delegation_recommended_mode: 'local',
614
+ plan_delegation_actual_mode: 'local',
615
+ plan_delegation_runtime_execution: 'local-sequential',
616
+ plan_delegation_authorization_status: 'disabled',
617
+ plan_delegation_authorization_source: '.loopx/config.json:agent_delegation.enabled=false',
618
+ plan_delegation_threshold: DEFAULT_AGENT_DELEGATION_CONFIG.threshold,
421
619
  plan_delegation_score: 0,
422
620
  plan_delegation_triggers: [],
423
621
  plan_delegation_reason: null,
@@ -530,7 +728,7 @@ async function writeJson(path, value) {
530
728
  async function writeCanonicalPlanArtifacts(cwd, root, slug) {
531
729
  const plansRoot = resolvePlansRoot(cwd);
532
730
  await ensureDir(plansRoot);
533
- const planPath = join(plansRoot, `prd-${slug}.md`);
731
+ const planPath = join(plansRoot, `requirements-snapshot-${slug}.md`);
534
732
  const testSpecPath = join(plansRoot, `test-spec-${slug}.md`);
535
733
  const planText = await readFile(artifactPath(root, 'plan.md'), 'utf8');
536
734
  const architectureText = await readFile(artifactPath(root, 'architecture.md'), 'utf8');
@@ -540,7 +738,9 @@ async function writeCanonicalPlanArtifacts(cwd, root, slug) {
540
738
  await writeText(
541
739
  planPath,
542
740
  [
543
- `# loopx PRD: ${slug}`,
741
+ `# loopx Requirements Snapshot: ${slug}`,
742
+ '',
743
+ '本文件是用户原始需求和已批准计划包的执行快照,不是由 loopx 生成的 PRD。原始需求来源仍以 `spec.md` / `plan_source_spec_path` 指向的用户材料为准。',
544
744
  '',
545
745
  '## Plan',
546
746
  '',
@@ -587,10 +787,11 @@ function bulletsFromSectionText(text, heading) {
587
787
  .filter(Boolean);
588
788
  }
589
789
 
590
- function sectionBodyForHeadings(text, headingPatterns) {
790
+ function sectionBodiesForHeadings(text, headingPatterns) {
591
791
  const body = stripFrontmatter(text);
592
792
  const headingPattern = /^#{2,4}\s+(.+?)\s*$/gm;
593
793
  const headings = [...body.matchAll(headingPattern)];
794
+ const bodies = [];
594
795
  for (let index = 0; index < headings.length; index += 1) {
595
796
  const title = headings[index][1].trim();
596
797
  if (!headingPatterns.some((pattern) => pattern.test(title))) {
@@ -598,25 +799,33 @@ function sectionBodyForHeadings(text, headingPatterns) {
598
799
  }
599
800
  const start = headings[index].index + headings[index][0].length;
600
801
  const end = index + 1 < headings.length ? headings[index + 1].index : body.length;
601
- return body.slice(start, end).trim();
802
+ bodies.push(body.slice(start, end).trim());
602
803
  }
603
- return '';
804
+ return bodies;
604
805
  }
605
806
 
606
807
  function explicitCoverageItems(sourceText) {
607
- const body = sectionBodyForHeadings(sourceText, [
808
+ const bodies = sectionBodiesForHeadings(sourceText, [
809
+ /^in\s+scope$/i,
810
+ /^testable\s+acceptance\s+criteria$/i,
811
+ /^functional\s+requirements?$/i,
608
812
  /required\s+coverage/i,
609
813
  /requirement\s+coverage/i,
814
+ /requirements?/i,
610
815
  /coverage\s+matrix/i,
816
+ /功能需求/,
817
+ /交付范围/,
818
+ /验收/,
819
+ /成功标准/,
611
820
  /需求.*覆盖/,
612
821
  /需求.*完整/,
613
822
  /需求.*卡点/,
614
823
  ]);
615
- if (!body) {
824
+ if (bodies.length === 0) {
616
825
  return [];
617
826
  }
618
- return body
619
- .split('\n')
827
+ return bodies
828
+ .flatMap((body) => body.split('\n'))
620
829
  .map((line) => line.trim())
621
830
  .filter((line) => /^[-*]\s+/.test(line) || /^\d+[.)]\s+/.test(line))
622
831
  .map((line) => line.replace(/^[-*]\s+/, '').replace(/^\d+[.)]\s+/, '').trim())
@@ -645,8 +854,8 @@ function markdownTableCoverageItems(sourceText) {
645
854
  continue;
646
855
  }
647
856
  if (
648
- /事件|字段|处理模式|标准化|范围|任务|确认|下发|source|event|field|coverage|requirement/i.test(currentHeading)
649
- || cells.some((cell) => /SHALL|MUST|Reuters|OCC|manual_|raw_snapshot|event_|处理|确认|下发|不自动/i.test(cell))
857
+ /事件|字段|处理模式|标准化|范围|任务|确认|复核|审批|审计|权限|资源|下发|source|event|field|coverage|requirement/i.test(currentHeading)
858
+ || cells.some((cell) => /SHALL|MUST|manual_|raw_snapshot|event_|处理|确认|复核|审批|补偿|回滚|下发|不自动/i.test(cell))
650
859
  ) {
651
860
  items.push(first);
652
861
  }
@@ -660,14 +869,356 @@ function requirementHeadingCoverageItems(sourceText) {
660
869
  .filter(Boolean);
661
870
  }
662
871
 
872
+ function relevantHeadingCoverageItems(sourceText) {
873
+ return [...String(sourceText || '').matchAll(/^#{2,4}\s+(.+?)\s*$/gm)]
874
+ .map((match) => match[1].replace(/`/g, '').trim())
875
+ .filter((title) => /需求|范围|验收|页面|任务|事件|流程|规则|字段|接口|架构|设计|计划|处理|异常|差异|复核|审批|审计|权限|资源|mock/i.test(title))
876
+ .filter((title) => !/^(审阅说明|门禁|source|context|inference)$/i.test(title));
877
+ }
878
+
663
879
  function sourceRequirementItems(sourceText) {
664
880
  return dedupeStrings([
665
881
  ...explicitCoverageItems(sourceText),
882
+ ...relevantHeadingCoverageItems(sourceText),
666
883
  ...markdownTableCoverageItems(sourceText),
667
884
  ...requirementHeadingCoverageItems(sourceText),
668
885
  ]).slice(0, 80);
669
886
  }
670
887
 
888
+ function markdownTableCell(value) {
889
+ return String(value ?? '')
890
+ .replace(/\n/g, ' ')
891
+ .replace(/\|/g, '\\|')
892
+ .trim();
893
+ }
894
+
895
+ function numberedPlanItems(text) {
896
+ return String(text || '')
897
+ .split('\n')
898
+ .map((line) => line.trim())
899
+ .filter((line) => /^\d+[.)]\s+/.test(line))
900
+ .map((line) => line.replace(/^\d+[.)]\s+/, '').trim())
901
+ .filter(Boolean);
902
+ }
903
+
904
+ function hasMarkdownHeading(text, heading) {
905
+ const escaped = String(heading || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
906
+ return new RegExp(`^#{2,4}\\s+${escaped}\\s*$`, 'm').test(String(text || ''));
907
+ }
908
+
909
+ function appendMarkdownSectionIfMissing(text, heading, body) {
910
+ if (hasMarkdownHeading(text, heading)) {
911
+ return text;
912
+ }
913
+ return [
914
+ String(text || '').trimEnd(),
915
+ '',
916
+ `## ${heading}`,
917
+ '',
918
+ String(body || '').trim(),
919
+ ].join('\n');
920
+ }
921
+
922
+ function sourceItemsForPlanEnrichment(sourceText, plannerDraft) {
923
+ const sourceItems = sourceRequirementItems(sourceText);
924
+ if (sourceItems.length > 0) {
925
+ return sourceItems;
926
+ }
927
+ return dedupeStrings([
928
+ ...numberedPlanItems(plannerDraft?.planText),
929
+ ...numberedPlanItems(plannerDraft?.developmentPlanText),
930
+ ]).slice(0, 24);
931
+ }
932
+
933
+ function requirementMappingTable(items, columns) {
934
+ const header = `| ${columns.map(([label]) => markdownTableCell(label)).join(' | ')} |`;
935
+ const divider = `| ${columns.map(() => '---').join(' | ')} |`;
936
+ const rows = items.map((item, index) => `| ${columns.map(([, render]) => markdownTableCell(render(item, index))).join(' | ')} |`);
937
+ return [header, divider, ...rows].join('\n');
938
+ }
939
+
940
+ function sourceRequirementRows(items, columns) {
941
+ return requirementMappingTable(items, [
942
+ ['#', (_, index) => index + 1],
943
+ ['原始需求项', (item) => item],
944
+ ...columns,
945
+ ]);
946
+ }
947
+
948
+ function enrichPlanTextForReview(text, items) {
949
+ let next = text;
950
+ next = appendMarkdownSectionIfMissing(next, '原始需求清单', [
951
+ '以下条目来自本次 plan 的源需求,是 build 前必须保留的审阅面。后续实现不得把这些条目隐含在泛化任务里。',
952
+ '',
953
+ ...items.map((item, index) => `${index + 1}. ${item}`),
954
+ ].join('\n'));
955
+ next = appendMarkdownSectionIfMissing(next, '原始需求映射', [
956
+ requirementMappingTable(items, [
957
+ ['#', (_, index) => index + 1],
958
+ ['原始需求项', (item) => item],
959
+ ['计划落点', () => '进入交付范围、变更规格、开发切片和测试计划'],
960
+ ['Build 证据要求', () => 'execution-record.md 中必须记录对应实现、验证命令或人工验收证据'],
961
+ ]),
962
+ ].join('\n'));
963
+ next = appendMarkdownSectionIfMissing(next, 'Build 前审阅清单', [
964
+ '- `requirement-traceability.md` 中所有原始需求项必须为 covered。',
965
+ '- `spec-delta.md` 中每个新增/修改需求必须有 SHALL/MUST 和 Scenario。',
966
+ '- `slices.json` 中每个切片必须有 AFK/HITL 类型、验收标准和验证信号。',
967
+ '- 执行阶段只能从用户显式批准的 plan package 启动,不允许 plan 自动进入 build。',
968
+ ].join('\n'));
969
+ return next;
970
+ }
971
+
972
+ function enrichArchitectureTextForReview(text, items) {
973
+ let next = text;
974
+ next = appendMarkdownSectionIfMissing(next, '文档定位', [
975
+ '架构文档回答“系统应如何分层、如何集成、哪些边界不能越过”。它不是任务清单,也不是字段级详细设计;build 阶段必须用它约束模块归属、数据流、状态边界和风险控制。',
976
+ '',
977
+ '| 文档 | 负责回答 | 不负责回答 |',
978
+ '| --- | --- | --- |',
979
+ '| `architecture.md` | 系统边界、模块职责、数据/状态流、接口边界、架构决策和质量属性 | 逐文件编码步骤、字段默认值、函数签名细节 |',
980
+ '| `development-plan.md` | 交付顺序、切片、依赖、验证和完成定义 | 重新选择架构方向 |',
981
+ '| `design.md` | 数据结构、接口/函数/组件契约、流程细节和边界条件 | 跨系统架构取舍或排期 |',
982
+ ].join('\n'));
983
+ next = appendMarkdownSectionIfMissing(next, '架构目标与非目标', [
984
+ '- 目标:把每个原始需求映射到稳定的系统边界、模块职责、状态/数据模型和集成方式。',
985
+ '- 目标:暴露关键风险、不可越过的副作用边界,以及后续真实接入需要重新规划的位置。',
986
+ '- 非目标:不在架构文档里安排开发顺序,不写字段级实现细节,不替代详细设计。',
987
+ ].join('\n'));
988
+ next = appendMarkdownSectionIfMissing(next, '上下文与系统边界', [
989
+ sourceRequirementRows(items, [
990
+ ['系统入口/用户', () => '在 build 前由 Planner 明确入口、操作者、上游来源和下游消费者'],
991
+ ['边界约束', () => '列明本次可修改模块、不可修改模块、外部系统是否 mock、权限/审计约束'],
992
+ ]),
993
+ ].join('\n'));
994
+ next = appendMarkdownSectionIfMissing(next, '组件与职责', [
995
+ sourceRequirementRows(items, [
996
+ ['承载组件', () => '明确后端 domain/usecase/repository/API、前端页面/组件、provider 或 adapter 的归属'],
997
+ ['职责边界', () => '说明该组件负责什么、不负责什么,以及和相邻模块的调用方向'],
998
+ ]),
999
+ ].join('\n'));
1000
+ next = appendMarkdownSectionIfMissing(next, '数据与状态模型', [
1001
+ sourceRequirementRows(items, [
1002
+ ['核心数据', () => '列出必须结构化保存或传递的实体、字段组、状态值和关联键'],
1003
+ ['状态/一致性', () => '说明状态推进、幂等、去重、审计、补偿或异常处理边界'],
1004
+ ]),
1005
+ ].join('\n'));
1006
+ next = appendMarkdownSectionIfMissing(next, '接口与集成契约', [
1007
+ sourceRequirementRows(items, [
1008
+ ['入口契约', () => '列出 API、CLI、任务、页面路由、事件或 provider 方法的输入输出边界'],
1009
+ ['集成约束', () => '说明真实依赖、mock 依赖、权限、错误传播和副作用控制'],
1010
+ ]),
1011
+ ].join('\n'));
1012
+ next = appendMarkdownSectionIfMissing(next, '关键流程', [
1013
+ sourceRequirementRows(items, [
1014
+ ['主流程', () => '用步骤描述从入口到状态/数据落点再到响应或回写的路径'],
1015
+ ['异常流程', () => '列出失败、重试、人工介入、回滚或 no-op 的处理方式'],
1016
+ ]),
1017
+ ].join('\n'));
1018
+ next = appendMarkdownSectionIfMissing(next, '需求到架构映射', [
1019
+ requirementMappingTable(items, [
1020
+ ['#', (_, index) => index + 1],
1021
+ ['原始需求项', (item) => item],
1022
+ ['架构落点', () => '在模块边界、数据结构、接口入口或状态流转中显式承接'],
1023
+ ['风险控制', () => '通过状态校验、mock/adapter 隔离、权限/日志或回归测试约束副作用'],
1024
+ ]),
1025
+ ].join('\n'));
1026
+ next = appendMarkdownSectionIfMissing(next, '架构审阅重点', [
1027
+ '- 模块边界必须能解释每个原始需求项由谁负责,不得只写“新增模块”。',
1028
+ '- 数据持久化、外部依赖、前端入口和状态推进必须分别列出约束。',
1029
+ '- 高风险领域必须写出不做什么,以及为什么不会触达真实副作用。',
1030
+ '- 后续接真实系统时,必须通过 adapter 或新 plan 增量承接,不在本次 build 中暗接。',
1031
+ ].join('\n'));
1032
+ next = appendMarkdownSectionIfMissing(next, '质量属性与风险', [
1033
+ '- 可测试性:每个模块边界都必须能被单测、集成测试或人工验收独立证明。',
1034
+ '- 可观测性:关键状态推进、人工动作、外部依赖和异常处理必须有日志或执行记录证据。',
1035
+ '- 可维护性:共享抽象只能承载真实共性;事件/场景差异必须在受控扩展点中表达。',
1036
+ '- 安全与副作用:涉及资金、资产、交易、权限、通知或外部系统时必须显式说明 mock/真实边界。',
1037
+ ].join('\n'));
1038
+ next = appendMarkdownSectionIfMissing(next, '架构决策记录', [
1039
+ '| 决策 | 选项 | 取舍 | 后续影响 |',
1040
+ '| --- | --- | --- | --- |',
1041
+ '| 当前架构方向 | 采用计划中已批准的模块边界和 adapter/provider 隔离方式 | 优先降低误实现和真实副作用风险 | build 阶段如发现边界不成立,必须回到 plan 修订 |',
1042
+ ].join('\n'));
1043
+ return next;
1044
+ }
1045
+
1046
+ function enrichDevelopmentPlanTextForReview(text, items) {
1047
+ let next = text;
1048
+ next = appendMarkdownSectionIfMissing(next, '文档定位', [
1049
+ '开发计划回答“按什么顺序交付、每个切片完成到什么程度、如何验证和交接”。它不重新做架构取舍,也不写字段级详细设计;build 阶段用它排定执行顺序和完成定义。',
1050
+ ].join('\n'));
1051
+ next = appendMarkdownSectionIfMissing(next, '交付切片', [
1052
+ sourceRequirementRows(items, [
1053
+ ['切片目标', (_, index) => `Slice ${index + 1}: 交付该需求的最小端到端可验证行为`],
1054
+ ['验收标准', () => '代码、数据/接口、测试、执行记录和必要人工验收证据齐全'],
1055
+ ['模式', () => '按风险标记 AFK 或 HITL;涉及人工审批、外部副作用或产品判断时必须 HITL'],
1056
+ ]),
1057
+ ].join('\n'));
1058
+ next = appendMarkdownSectionIfMissing(next, '实施顺序与依赖', [
1059
+ '| 顺序 | 工作 | 依赖 | 退出条件 |',
1060
+ '| --- | --- | --- | --- |',
1061
+ '| 1 | 建立领域/数据/状态底座 | 已批准架构和详细设计 | 状态、数据结构和基础测试可运行 |',
1062
+ '| 2 | 接入入口和业务编排 | 底座可测 | API/页面/任务入口能驱动核心流程 |',
1063
+ '| 3 | 完成异常、权限、日志和验收样例 | 主流程可运行 | 风险边界和异常路径有证据 |',
1064
+ '| 4 | 收敛回归和人工验收 | 自动化验证通过 | execution-record.md 覆盖全部切片 |',
1065
+ ].join('\n'));
1066
+ next = appendMarkdownSectionIfMissing(next, '需求到开发切片', [
1067
+ requirementMappingTable(items, [
1068
+ ['#', (_, index) => index + 1],
1069
+ ['原始需求项', (item) => item],
1070
+ ['建议切片', (_, index) => `Slice ${index + 1}`],
1071
+ ['交付物', () => '代码变更、测试、执行记录和必要的人工验收截图/说明'],
1072
+ ['完成判定', () => '对应验证信号通过且 completion audit 标记 covered'],
1073
+ ]),
1074
+ ].join('\n'));
1075
+ next = appendMarkdownSectionIfMissing(next, '文件级变更清单', [
1076
+ sourceRequirementRows(items, [
1077
+ ['预计文件/目录', () => '列出应新增或修改的后端、前端、schema、测试、配置或文档路径'],
1078
+ ['变更类型', () => '新增/修改/生成/迁移/测试;生成代码必须说明来源命令'],
1079
+ ]),
1080
+ ].join('\n'));
1081
+ next = appendMarkdownSectionIfMissing(next, '验证计划', [
1082
+ sourceRequirementRows(items, [
1083
+ ['自动化验证', () => '列出最小命令、仓库级回归命令和失败时回退路径'],
1084
+ ['人工验证', () => '列出页面、审批、外部副作用或数据核对等必须人工确认的点'],
1085
+ ]),
1086
+ ].join('\n'));
1087
+ next = appendMarkdownSectionIfMissing(next, '人工确认点', [
1088
+ '- Plan 完成后只能等待用户批准 `plan -> build`。',
1089
+ '- 每个 HITL 切片在 build 阶段必须记录人工确认或人工验收缺口。',
1090
+ '- 如果实现时发现源需求与代码事实冲突,必须停止对应分支并回到 plan/clarify,而不是自行改范围。',
1091
+ ].join('\n'));
1092
+ next = appendMarkdownSectionIfMissing(next, '回滚/降级策略', [
1093
+ '- 如果某个切片无法完成,`execution-record.md` 必须把它放入 `remaining_scope`,不得声明 full completion。',
1094
+ '- 如果验证失败来自计划边界错误,回到 plan;如果来自需求歧义,回到 clarify;如果来自实现缺陷,留在 build 修复。',
1095
+ '- 不允许为了通过 build 删除源需求或把未完成项改成隐含非目标。',
1096
+ ].join('\n'));
1097
+ next = appendMarkdownSectionIfMissing(next, '完成定义', [
1098
+ '- 所有源需求在 `requirement-traceability.md` 中保持 covered。',
1099
+ '- 每个 vertical slice 有实现证据、验证证据和必要人工验收记录。',
1100
+ '- `execution-record.md` 的 `completion_claim` 与实际完成范围一致。',
1101
+ '- deslop 后回归重新通过,且 review handoff blocker 为空。',
1102
+ ].join('\n'));
1103
+ return next;
1104
+ }
1105
+
1106
+ function detailedDesignTextForChange({ changeId, slug, items, plannerDraft }) {
1107
+ return [
1108
+ `# loopx Detailed Design: ${changeId}`,
1109
+ '',
1110
+ '## 文档定位',
1111
+ '',
1112
+ '详细设计回答“具体怎么实现到字段、接口、函数、组件、状态流转和边界条件”。它承接 `architecture.md` 的边界和 `development-plan.md` 的切片,但比二者更接近 build 可执行输入;build 阶段不得只凭概要描述自行发明字段、接口或状态。',
1113
+ '',
1114
+ '## 需求到设计映射',
1115
+ '',
1116
+ sourceRequirementRows(items, [
1117
+ ['设计落点', () => '数据结构、接口/函数/组件契约、状态机、错误处理和测试设计均需有对应条目'],
1118
+ ['实现证据', () => 'build 阶段在 execution-record.md 记录代码路径、验证命令或人工验收证据'],
1119
+ ]),
1120
+ '',
1121
+ '## 数据结构与字段',
1122
+ '',
1123
+ sourceRequirementRows(items, [
1124
+ ['实体/结构', () => '列出需要新增或修改的实体、DTO、schema、payload 或前端 state'],
1125
+ ['关键字段', () => '字段名、类型、来源、是否必填、默认值、唯一性/索引、审计要求'],
1126
+ ['迁移/兼容', () => '是否需要 migration、生成代码、旧数据兼容或回填策略'],
1127
+ ]),
1128
+ '',
1129
+ '## 接口、函数与组件契约',
1130
+ '',
1131
+ sourceRequirementRows(items, [
1132
+ ['契约对象', () => 'API 路径、provider 方法、usecase 函数、repository 方法、前端组件 props/events'],
1133
+ ['输入输出', () => '参数、响应、错误码、权限、幂等键、分页/筛选/排序或事件格式'],
1134
+ ['调用方/被调方', () => '明确调用方向和禁止调用的真实外部依赖'],
1135
+ ]),
1136
+ '',
1137
+ '## 状态机与流程细节',
1138
+ '',
1139
+ sourceRequirementRows(items, [
1140
+ ['状态/步骤', () => '列出允许状态、动作、前置条件、后置条件和审计日志'],
1141
+ ['非法路径', () => '列出必须拒绝的动作、重复请求、越权、缺数据和异常回写'],
1142
+ ]),
1143
+ '',
1144
+ '## 错误处理与边界条件',
1145
+ '',
1146
+ sourceRequirementRows(items, [
1147
+ ['错误场景', () => '输入缺失、依赖失败、并发冲突、数据不一致、mock/真实边界误用'],
1148
+ ['处理方式', () => '返回错误、保持原状态、写日志、创建异常、人工处理或回滚'],
1149
+ ]),
1150
+ '',
1151
+ '## 测试设计',
1152
+ '',
1153
+ sourceRequirementRows(items, [
1154
+ ['测试类型', () => '单测、集成、API、前端构建、浏览器人工验收或回归命令'],
1155
+ ['断言重点', () => '状态、字段、权限、副作用隔离、日志、错误路径和源需求覆盖'],
1156
+ ]),
1157
+ '',
1158
+ '## 实现注意事项',
1159
+ '',
1160
+ '- build 阶段必须优先遵循本详细设计;发现字段、接口或状态缺失时,不得自行扩大范围,必须记录 blocker 或回到 plan。',
1161
+ '- 任何真实外部系统、资金资产、交易订单、通知或权限相关副作用,都必须在本文件中有明确允许才可实现。',
1162
+ '- 生成代码、迁移和前端构建产物必须记录来源命令,避免把运行时临时产物当作设计输入。',
1163
+ '',
1164
+ '## 上游架构摘要',
1165
+ '',
1166
+ plannerDraft.architectureText || '- 见 workflow-local `architecture.md`。',
1167
+ '',
1168
+ '## 上游开发切片摘要',
1169
+ '',
1170
+ plannerDraft.developmentPlanText || '- 见 workflow-local `development-plan.md`。',
1171
+ '',
1172
+ '## Source',
1173
+ '',
1174
+ `- workflow slug: ${slug}`,
1175
+ `- change id: ${changeId}`,
1176
+ ].join('\n');
1177
+ }
1178
+
1179
+ function enrichTestPlanTextForReview(text, items) {
1180
+ let next = text;
1181
+ next = appendMarkdownSectionIfMissing(next, '需求到测试矩阵', [
1182
+ requirementMappingTable(items, [
1183
+ ['#', (_, index) => index + 1],
1184
+ ['原始需求项', (item) => item],
1185
+ ['自动化验证', () => '优先使用仓库原生命令覆盖状态、接口、数据或构建行为'],
1186
+ ['人工验收', () => '对无法自动证明的页面、审批、风险边界做人工确认'],
1187
+ ['证据', () => '命令输出、截图路径、日志片段或执行记录条目'],
1188
+ ]),
1189
+ ].join('\n'));
1190
+ next = appendMarkdownSectionIfMissing(next, '回归门禁', [
1191
+ '- build 阶段必须先跑计划列出的最小验证,再跑仓库级回归。',
1192
+ '- deslop 后必须重新验证,不能复用旧输出。',
1193
+ '- 如果某个源需求没有验证信号,execution-record.md 必须把它列入 blocker 或 remaining_scope。',
1194
+ ].join('\n'));
1195
+ return next;
1196
+ }
1197
+
1198
+ function enrichPlannerDraftForReview({ sourceText, plannerDraft }) {
1199
+ const draft = {
1200
+ ...plannerDraft,
1201
+ principles: Array.isArray(plannerDraft.principles) ? plannerDraft.principles : [],
1202
+ decisionDrivers: Array.isArray(plannerDraft.decisionDrivers) ? plannerDraft.decisionDrivers : [],
1203
+ options: Array.isArray(plannerDraft.options) ? plannerDraft.options : [],
1204
+ planText: String(plannerDraft.planText || ''),
1205
+ architectureText: String(plannerDraft.architectureText || ''),
1206
+ developmentPlanText: String(plannerDraft.developmentPlanText || ''),
1207
+ testPlanText: String(plannerDraft.testPlanText || ''),
1208
+ };
1209
+ const items = sourceItemsForPlanEnrichment(sourceText, draft);
1210
+ if (items.length === 0) {
1211
+ return draft;
1212
+ }
1213
+ return {
1214
+ ...draft,
1215
+ planText: canEnrichChineseReviewText(draft.planText) ? enrichPlanTextForReview(draft.planText, items) : draft.planText,
1216
+ architectureText: canEnrichChineseReviewText(draft.architectureText) ? enrichArchitectureTextForReview(draft.architectureText, items) : draft.architectureText,
1217
+ developmentPlanText: canEnrichChineseReviewText(draft.developmentPlanText) ? enrichDevelopmentPlanTextForReview(draft.developmentPlanText, items) : draft.developmentPlanText,
1218
+ testPlanText: canEnrichChineseReviewText(draft.testPlanText) ? enrichTestPlanTextForReview(draft.testPlanText, items) : draft.testPlanText,
1219
+ };
1220
+ }
1221
+
671
1222
  function normalizedCoverageText(...parts) {
672
1223
  return parts.join('\n')
673
1224
  .toLowerCase()
@@ -695,7 +1246,8 @@ function sourceRequirementCovered(item, haystack) {
695
1246
  if (tokens.length === 0) {
696
1247
  return false;
697
1248
  }
698
- return tokens.every((token) => haystack.includes(token));
1249
+ const matched = tokens.filter((token) => haystack.includes(token)).length;
1250
+ return matched / tokens.length >= 0.65;
699
1251
  }
700
1252
 
701
1253
  async function writeRequirementTraceabilityArtifact({ root, sourceSpecPath, sourceText, plannerDraft, changeArtifactPaths }) {
@@ -728,22 +1280,34 @@ async function writeRequirementTraceabilityArtifact({ root, sourceSpecPath, sour
728
1280
  '# 原始需求覆盖矩阵',
729
1281
  '',
730
1282
  `- 来源:${sourceSpecPath}`,
731
- `- 状态:${status}`,
1283
+ `- 覆盖状态:${status === 'complete' ? '完整' : '部分缺失'} (${status})`,
732
1284
  `- 提取项数量:${rows.length}`,
733
1285
  '',
1286
+ '## 审阅说明',
1287
+ '',
1288
+ '- 本文件用于人工确认源需求是否被计划、架构、开发切片、规格增量和测试计划承接。',
1289
+ '- “原始需求项”保留源文档原文;如果源文档是英文,表格会保留英文原句,但覆盖状态和审阅说明必须使用中文。',
1290
+ '- 未覆盖项会阻断 `plan -> build`,直到 Planner 重新展开计划或明确把该项列为非目标并说明理由。',
1291
+ '',
734
1292
  '## 覆盖矩阵',
735
1293
  '',
736
- '| 原始需求项 | 覆盖状态 |',
737
- '| --- | --- |',
1294
+ '| 原始需求项 | 覆盖状态 | 审阅说明 |',
1295
+ '| --- | --- | --- |',
738
1296
  ...(rows.length > 0
739
- ? rows.map((row) => `| ${row.item.replace(/\|/g, '\\|')} | ${row.status} |`)
740
- : ['| 未检测到显式需求覆盖项 | covered |']),
1297
+ ? rows.map((row) => `| ${row.item.replace(/\|/g, '\\|')} | ${row.status === 'covered' ? '已覆盖' : '未覆盖'} | ${row.status === 'covered' ? '已在计划包或变更工件中找到对应表述。' : '计划包没有找到可追溯表述,需要回到 plan 修订。'} |`)
1298
+ : ['| 未检测到显式需求覆盖项 | 已覆盖 | 没有从源文档中提取到独立覆盖项。 |']),
741
1299
  '',
742
1300
  '## 门禁',
743
1301
  '',
744
1302
  ...(blockers.length > 0
745
- ? blockers.map((blocker) => `- ${blocker}`)
746
- : ['- complete']),
1303
+ ? [
1304
+ '- 结果:存在原始需求未被计划包充分承接,不能进入 build handoff。',
1305
+ ...rows
1306
+ .filter((row) => row.status !== 'covered')
1307
+ .map((row) => `- 未覆盖需求:${row.item}`),
1308
+ ...blockers.map((blocker) => `- ${blocker}`),
1309
+ ]
1310
+ : ['- 结果:全部原始需求已覆盖,可以进入后续 plan gate 检查。']),
747
1311
  ].join('\n'));
748
1312
 
749
1313
  return {
@@ -796,32 +1360,83 @@ function delegationDecisionForPlan(sourceText, plannerDraft) {
796
1360
  addTrigger('architectural_tradeoff', 1);
797
1361
  }
798
1362
 
799
- const mode = score >= 7 ? 'parallel-review' : (score >= 4 ? 'critic-only' : 'local');
800
- const reason = mode === 'parallel-review'
1363
+ const recommendedMode = score >= 7 ? 'parallel-review' : (score >= 4 ? 'critic-only' : 'local');
1364
+ const reason = recommendedMode === 'parallel-review'
801
1365
  ? '高风险或跨模块规划,建议独立 Planner/Architect/Critic 视角并行审查。'
802
- : mode === 'critic-only'
803
- ? '存在中等复杂度或验证风险,建议至少引入独立 critic 复核 PRD 覆盖和风险。'
1366
+ : recommendedMode === 'critic-only'
1367
+ ? '存在中等复杂度或验证风险,建议至少引入独立 critic 复核需求覆盖和风险。'
804
1368
  : '范围较小或风险较低,本地顺序 Planner/Architect/Critic 审阅足够。';
805
1369
 
806
1370
  return {
807
- mode,
1371
+ mode: recommendedMode,
1372
+ recommended_mode: recommendedMode,
808
1373
  score,
809
1374
  triggers,
810
1375
  reason,
811
- current_runtime_execution: 'local-sequential',
812
- execution_note: '当前 runtime 记录委派决策依据;是否实际启动 native subagents 仍受执行环境和用户授权约束。',
813
1376
  };
814
1377
  }
815
1378
 
816
- async function writePlanDelegationDecisionArtifact({ root, sourceText, plannerDraft }) {
1379
+ function resolvePlanDelegationExecution(recommendedMode, config) {
1380
+ const normalized = normalizeAgentDelegationConfig(config);
1381
+ const thresholdMet = delegationMeetsThreshold(recommendedMode, normalized.threshold);
1382
+ if (!normalized.enabled) {
1383
+ return {
1384
+ actual_mode: 'local',
1385
+ runtime_execution: 'local-sequential',
1386
+ authorization_status: 'disabled',
1387
+ authorization_source: '.loopx/config.json:agent_delegation.enabled=false',
1388
+ threshold: normalized.threshold,
1389
+ config: normalized,
1390
+ note: '已记录推荐委派模式;未授权自动启动 subagents,因此本次实际执行保持本地顺序审阅。',
1391
+ };
1392
+ }
1393
+ if (!thresholdMet) {
1394
+ return {
1395
+ actual_mode: 'local',
1396
+ runtime_execution: 'local-sequential',
1397
+ authorization_status: 'below-threshold',
1398
+ authorization_source: '.loopx/config.json:agent_delegation.threshold',
1399
+ threshold: normalized.threshold,
1400
+ config: normalized,
1401
+ note: `推荐模式 ${recommendedMode} 低于自动委派阈值 ${normalized.threshold},实际执行保持本地顺序审阅。`,
1402
+ };
1403
+ }
1404
+ if (!normalized.auto_start) {
1405
+ return {
1406
+ actual_mode: 'local',
1407
+ runtime_execution: 'manual-subagent-review',
1408
+ authorization_status: 'manual-required',
1409
+ authorization_source: '.loopx/config.json:agent_delegation.auto_start=false',
1410
+ threshold: normalized.threshold,
1411
+ config: normalized,
1412
+ note: '配置允许记录委派建议,但未授权自动启动;需要用户或外部执行器手动开启推荐的 subagent review。',
1413
+ };
1414
+ }
1415
+ return {
1416
+ actual_mode: recommendedMode,
1417
+ runtime_execution: 'auto-subagent-review',
1418
+ authorization_status: 'auto-authorized',
1419
+ authorization_source: '.loopx/config.json:agent_delegation.auto_start=true',
1420
+ threshold: normalized.threshold,
1421
+ config: normalized,
1422
+ note: '配置已授权达到阈值时自动使用推荐的 subagent review 模式;具体启动由当前 agent runtime 执行。',
1423
+ };
1424
+ }
1425
+
1426
+ async function writePlanDelegationDecisionArtifact({ root, sourceText, plannerDraft, agentDelegationConfig }) {
817
1427
  const decision = delegationDecisionForPlan(sourceText, plannerDraft);
1428
+ const execution = resolvePlanDelegationExecution(decision.recommended_mode, agentDelegationConfig);
818
1429
  const path = artifactPath(root, 'plan-delegation-decision.md');
819
1430
  await writeText(path, [
820
1431
  '# Plan Delegation Decision',
821
1432
  '',
822
- `- mode: ${decision.mode}`,
1433
+ `- recommended_mode: ${decision.recommended_mode}`,
1434
+ `- actual_mode: ${execution.actual_mode}`,
1435
+ `- runtime_execution: ${execution.runtime_execution}`,
1436
+ `- authorization_status: ${execution.authorization_status}`,
1437
+ `- authorization_source: ${execution.authorization_source}`,
1438
+ `- threshold: ${execution.threshold}`,
823
1439
  `- score: ${decision.score}`,
824
- `- current_runtime_execution: ${decision.current_runtime_execution}`,
825
1440
  `- reason: ${decision.reason}`,
826
1441
  '',
827
1442
  '## Triggers',
@@ -831,14 +1446,20 @@ async function writePlanDelegationDecisionArtifact({ root, sourceText, plannerDr
831
1446
  '## Guidance',
832
1447
  '',
833
1448
  '- local: 低风险、小范围、单模块任务,本地顺序 Planner/Architect/Critic 即可。',
834
- '- critic-only: 中等风险或覆盖面较宽,至少需要独立 critic 复核 PRD 覆盖、验证和遗漏风险。',
1449
+ '- critic-only: 中等风险或覆盖面较宽,至少需要独立 critic 复核需求覆盖、验证和遗漏风险。',
835
1450
  '- parallel-review: 高风险、多模块、状态/资产/安全相关任务,建议独立 Planner/Architect/Critic 视角并行审查。',
836
1451
  '',
1452
+ '## Authorization',
1453
+ '',
1454
+ '- `recommended_mode` 是基于需求/plan 风险面的规划建议。',
1455
+ '- `actual_mode` 是结合 `.loopx/config.json` 授权边界后的本次实际执行模式。',
1456
+ '- 只有 `agent_delegation.enabled=true`、`auto_start=true` 且推荐模式达到 `threshold` 时,loopx 才会把实际模式提升到推荐的 subagent review。',
1457
+ '',
837
1458
  '## Runtime Note',
838
1459
  '',
839
- `- ${decision.execution_note}`,
1460
+ `- ${execution.note}`,
840
1461
  ].join('\n'));
841
- return { path, ...decision };
1462
+ return { path, ...decision, ...execution };
842
1463
  }
843
1464
 
844
1465
  function frontmatterList(text, key) {
@@ -1092,19 +1713,18 @@ function validateRequirementDelta(text) {
1092
1713
  return { delta, blockers: dedupeStrings(blockers) };
1093
1714
  }
1094
1715
 
1095
- function requirementsForDelta(slug, plannerDraft) {
1096
- const requirements = String(plannerDraft.planText || '')
1097
- .split('\n')
1098
- .map((line) => line.trim())
1099
- .filter((line) => /^\d+\.\s+/.test(line))
1100
- .map((line) => line.replace(/^\d+\.\s+/, '').trim());
1716
+ function requirementsForDelta(slug, plannerDraft, sourceText = '') {
1717
+ const sourceRequirements = sourceRequirementItems(sourceText);
1718
+ const requirements = sourceRequirements.length > 0
1719
+ ? sourceRequirements
1720
+ : numberedPlanItems(plannerDraft.planText);
1101
1721
  return dedupeStrings(requirements.length > 0 ? requirements : [
1102
1722
  `Workflow ${slug} SHALL implement the approved loopx plan package.`,
1103
1723
  ]);
1104
1724
  }
1105
1725
 
1106
- function verticalSlicesForChange(slug, plannerDraft) {
1107
- const requirements = requirementsForDelta(slug, plannerDraft);
1726
+ function verticalSlicesForChange(slug, plannerDraft, sourceText = '') {
1727
+ const requirements = requirementsForDelta(slug, plannerDraft, sourceText);
1108
1728
  const slices = requirements.slice(0, 8).map((requirement, index) => ({
1109
1729
  id: `VS-${index + 1}`,
1110
1730
  title: requirement.length > 90 ? `${requirement.slice(0, 87)}...` : requirement,
@@ -1189,8 +1809,8 @@ async function writeChangeArtifacts(cwd, root, slug, sourceText, plannerDraft, c
1189
1809
  graph: join(changeRoot, 'artifact-graph.json'),
1190
1810
  };
1191
1811
  const domains = targetDomainsForChange(slug, sourceText);
1192
- const requirements = requirementsForDelta(slug, plannerDraft);
1193
- const slices = verticalSlicesForChange(slug, plannerDraft);
1812
+ const requirements = requirementsForDelta(slug, plannerDraft, sourceText);
1813
+ const slices = verticalSlicesForChange(slug, plannerDraft, sourceText);
1194
1814
 
1195
1815
  await writeText(paths.proposal, [
1196
1816
  `# loopx Change Proposal: ${normalizedChangeId}`,
@@ -1230,17 +1850,12 @@ async function writeChangeArtifacts(cwd, root, slug, sourceText, plannerDraft, c
1230
1850
  ...requirements.flatMap((item, index) => [requirementBlockFromText({ slug, text: item, index }), '']),
1231
1851
  ].join('\n'));
1232
1852
 
1233
- await writeText(paths.design, [
1234
- `# loopx Change Design: ${normalizedChangeId}`,
1235
- '',
1236
- '## Technical Approach',
1237
- '',
1238
- plannerDraft.architectureText || '- See workflow architecture artifact.',
1239
- '',
1240
- '## Task Plan',
1241
- '',
1242
- plannerDraft.developmentPlanText || '- See workflow development plan artifact.',
1243
- ].join('\n'));
1853
+ await writeText(paths.design, detailedDesignTextForChange({
1854
+ changeId: normalizedChangeId,
1855
+ slug,
1856
+ items: requirements,
1857
+ plannerDraft,
1858
+ }));
1244
1859
 
1245
1860
  await writeText(paths.tasks, [
1246
1861
  `# loopx Change Tasks: ${normalizedChangeId}`,
@@ -1601,7 +2216,7 @@ function deriveSlugFromSpecPath(path, text) {
1601
2216
  return normalizeSlug(meta.workflow_id);
1602
2217
  }
1603
2218
  const name = basename(path).replace(/\.md$/i, '');
1604
- return normalizeSlug(name.replace(/^deep-interview-/, '').replace(/^clarify-/, ''));
2219
+ return normalizeSlug(name.replace(/^clarify-/, ''));
1605
2220
  }
1606
2221
 
1607
2222
  function containsChineseText(text) {
@@ -1614,7 +2229,44 @@ function containsChineseText(text) {
1614
2229
  return chineseChars.length >= 40 || (chineseChars.length >= 8 && chineseChars.length / signalChars >= 0.2);
1615
2230
  }
1616
2231
 
1617
- async function planLanguageBlockers(pathsByKey) {
2232
+ function canEnrichChineseReviewText(text) {
2233
+ const chineseChars = String(text || '').match(/[\u3400-\u9fff]/g) || [];
2234
+ return chineseChars.length >= 12;
2235
+ }
2236
+
2237
+ const REVIEW_DOCUMENT_CONTRACTS = {
2238
+ architecture: ['文档定位', '架构目标与非目标', '上下文与系统边界', '组件与职责', '数据与状态模型', '接口与集成契约', '关键流程', '架构决策记录'],
2239
+ developmentPlan: ['文档定位', '交付切片', '实施顺序与依赖', '文件级变更清单', '验证计划', '完成定义'],
2240
+ design: ['文档定位', '需求到设计映射', '数据结构与字段', '接口、函数与组件契约', '状态机与流程细节', '错误处理与边界条件', '测试设计'],
2241
+ };
2242
+
2243
+ function planReviewabilityBlockers(key, text, sourceItemCount) {
2244
+ const reviewerDocs = new Set(['plan', 'architecture', 'developmentPlan', 'testPlan', 'requirementsSnapshot', 'testSpec', 'design']);
2245
+ if (!reviewerDocs.has(key)) {
2246
+ return [];
2247
+ }
2248
+ const blockers = [];
2249
+ const nonEmptyLineCount = String(text || '').split('\n').filter((line) => line.trim()).length;
2250
+ const headingCount = (String(text || '').match(/^#{2,4}\s+/gm) || []).length;
2251
+ const needsSourceMapping = sourceItemCount >= 2;
2252
+ const minLines = needsSourceMapping ? Math.min(22, 8 + sourceItemCount) : 3;
2253
+ const minHeadings = needsSourceMapping ? 3 : 1;
2254
+ if (nonEmptyLineCount < minLines || headingCount < minHeadings) {
2255
+ blockers.push(`plan_artifact_too_thin_${key}`);
2256
+ }
2257
+ if (needsSourceMapping && !/(原始需求|需求.*映射|需求.*覆盖|覆盖矩阵|需求到|测试矩阵|交付切片)/.test(text)) {
2258
+ blockers.push(`plan_artifact_missing_source_mapping_${key}`);
2259
+ }
2260
+ const requiredHeadings = REVIEW_DOCUMENT_CONTRACTS[key] || [];
2261
+ for (const heading of requiredHeadings) {
2262
+ if (!hasMarkdownHeading(text, heading)) {
2263
+ blockers.push(`plan_artifact_missing_section_${key}_${slugKey(heading)}`);
2264
+ }
2265
+ }
2266
+ return blockers;
2267
+ }
2268
+
2269
+ async function planLanguageBlockers(pathsByKey, { sourceItemCount = 0 } = {}) {
1618
2270
  const blockers = [];
1619
2271
  for (const [key, path] of Object.entries(pathsByKey)) {
1620
2272
  if (!existsSync(path)) {
@@ -1625,6 +2277,7 @@ async function planLanguageBlockers(pathsByKey) {
1625
2277
  if (!containsChineseText(text)) {
1626
2278
  blockers.push(`plan_artifact_not_chinese_${key}`);
1627
2279
  }
2280
+ blockers.push(...planReviewabilityBlockers(key, text, sourceItemCount));
1628
2281
  }
1629
2282
  return blockers;
1630
2283
  }
@@ -1651,12 +2304,23 @@ async function ensurePlanWorkflowFromDirectSpec(cwd, directSpecPath, explicitSlu
1651
2304
  const existing = await readState(cwd, slug);
1652
2305
  if (existing) {
1653
2306
  const merged = withRecommendedAction({
2307
+ ...createInitialState(slug, existing.clarify_profile || existing.profile || 'standard'),
1654
2308
  ...existing,
2309
+ schema_version: WORKFLOW_SCHEMA_VERSION,
2310
+ slug,
2311
+ current_stage: existing.current_stage || STAGES.CLARIFY,
2312
+ stage_status: existing.stage_status || 'awaiting-approval',
1655
2313
  spec_artifact_path: resolvedSpecPath,
1656
2314
  plan_source_spec_path: resolvedSpecPath,
2315
+ requested_transition: existing.requested_transition || TRANSITIONS.CLARIFY_TO_PLAN,
1657
2316
  plan_consensus_mode: true,
1658
2317
  plan_deliberate_mode: Boolean(options.deliberate),
1659
2318
  plan_interactive_mode: Boolean(options.interactive),
2319
+ approval: {
2320
+ ...createInitialState(slug, existing.clarify_profile || existing.profile || 'standard').approval,
2321
+ ...(existing.approval || {}),
2322
+ plan: APPROVAL_STATES.APPROVED,
2323
+ },
1660
2324
  });
1661
2325
  await writeState(root, merged);
1662
2326
  return { slug, root, state: merged };
@@ -1787,7 +2451,7 @@ async function readPlanCompletion(cwd, root, slug, state) {
1787
2451
  blockers.push('execution_inputs_unresolved');
1788
2452
  }
1789
2453
  if (!state.plan_artifact_path || !existsSync(state.plan_artifact_path)) {
1790
- blockers.push('missing_prd');
2454
+ blockers.push('missing_requirements_snapshot');
1791
2455
  }
1792
2456
  if (!state.test_spec_artifact_path || !existsSync(state.test_spec_artifact_path)) {
1793
2457
  blockers.push('missing_test_spec');
@@ -1816,16 +2480,21 @@ async function readPlanCompletion(cwd, root, slug, state) {
1816
2480
  blockers.push(`source_requirements_${state.source_requirements_status}`);
1817
2481
  }
1818
2482
  }
2483
+ let sourceItemCount = 0;
2484
+ if (state.plan_source_spec_path && existsSync(state.plan_source_spec_path)) {
2485
+ sourceItemCount = sourceRequirementItems(await readFile(state.plan_source_spec_path, 'utf8')).length;
2486
+ }
1819
2487
  blockers.push(...await planLanguageBlockers({
1820
2488
  plan: artifactPath(root, 'plan.md'),
1821
2489
  architecture: artifactPath(root, 'architecture.md'),
1822
2490
  developmentPlan: artifactPath(root, 'development-plan.md'),
1823
2491
  testPlan: artifactPath(root, 'test-plan.md'),
1824
- prd: state.plan_artifact_path || join(resolvePlansRoot(cwd), `prd-${slug}.md`),
2492
+ requirementsSnapshot: state.plan_artifact_path || join(resolvePlansRoot(cwd), `requirements-snapshot-${slug}.md`),
1825
2493
  testSpec: state.test_spec_artifact_path || join(resolvePlansRoot(cwd), `test-spec-${slug}.md`),
1826
2494
  traceability: state.requirement_traceability_path || artifactPath(root, 'requirement-traceability.md'),
1827
2495
  delegationDecision: state.plan_delegation_decision_path || artifactPath(root, 'plan-delegation-decision.md'),
1828
- }));
2496
+ design: state.change_artifact_paths?.design || join(resolveChangeRoot(cwd, state.change_id || changeIdForWorkflowSlug(slug)), 'design.md'),
2497
+ }, { sourceItemCount }));
1829
2498
  const changeStatus = await readChangeArtifactStatus(state.change_artifact_paths);
1830
2499
  blockers.push(...changeStatus.blockers);
1831
2500
 
@@ -1949,10 +2618,10 @@ async function buildCompletionAudit({ cwd, root, slug, state, reviewReworkArtifa
1949
2618
  };
1950
2619
 
1951
2620
  addChecklistItem({
1952
- id: 'approved-prd',
2621
+ id: 'requirements-snapshot',
1953
2622
  source: 'approved-plan',
1954
- requirement: state.plan_artifact_path || join(cwd, '.loopx', 'plans', `prd-${slug}.md`),
1955
- evidence: [state.plan_artifact_path || 'approved plan artifact'],
2623
+ requirement: state.plan_artifact_path || join(cwd, '.loopx', 'plans', `requirements-snapshot-${slug}.md`),
2624
+ evidence: [state.plan_artifact_path || 'requirements snapshot artifact'],
1956
2625
  });
1957
2626
  addChecklistItem({
1958
2627
  id: 'test-spec',
@@ -2175,9 +2844,6 @@ function clarifyReadinessBlockers(state) {
2175
2844
  if (state.clarify_current_round > state.clarify_max_rounds) {
2176
2845
  blockers.push('clarify_max_rounds_exceeded');
2177
2846
  }
2178
- if (state.clarify_ambiguity_score > state.clarify_target_ambiguity_threshold) {
2179
- blockers.push('clarify_ambiguity_score_above_threshold');
2180
- }
2181
2847
  if (!state.clarify_non_goals_resolved) {
2182
2848
  blockers.push('clarify_non_goals_unresolved');
2183
2849
  }
@@ -2211,7 +2877,7 @@ function planReadinessBlockersSync(state) {
2211
2877
  blockers.push('execution_inputs_unresolved');
2212
2878
  }
2213
2879
  if (!state.plan_artifact_path) {
2214
- blockers.push('missing_prd');
2880
+ blockers.push('missing_requirements_snapshot');
2215
2881
  }
2216
2882
  if (!state.test_spec_artifact_path) {
2217
2883
  blockers.push('missing_test_spec');
@@ -2320,7 +2986,7 @@ function buildCurrentEvidenceChain(state, readiness = buildReadiness(state), aut
2320
2986
  if (readiness.plan.ready) {
2321
2987
  evidence.push(evidenceEntry(
2322
2988
  'clarify_ready_for_plan',
2323
- 'Clarify ambiguity score, non-goals, decision boundaries, pressure pass, and unresolved ambiguity gates are satisfied.',
2989
+ 'Clarify has zero unresolved ambiguity and non-goals, decision boundaries, and pressure pass gates are satisfied.',
2324
2990
  authorization.plan.authorized ? 'The approved clarify -> plan transition can be consumed by plan.' : 'Plan readiness exists, but user authorization is still separate.',
2325
2991
  ));
2326
2992
  }
@@ -2845,7 +3511,7 @@ async function renderPlanReadingViews(cwd, root, state, slug) {
2845
3511
  }
2846
3512
  }
2847
3513
 
2848
- export async function initWorkspace(cwd, { slug } = {}) {
3514
+ export async function initWorkspace(cwd, { slug, agentDelegation = {} } = {}) {
2849
3515
  const workspaceRoot = resolveWorkspaceRoot(cwd);
2850
3516
  const projectConventions = await inspectProjectConventions(cwd);
2851
3517
  await ensureLoopxRoot(cwd);
@@ -2872,6 +3538,7 @@ export async function initWorkspace(cwd, { slug } = {}) {
2872
3538
  existing_spec_sources: projectConventions.existing_spec_sources,
2873
3539
  },
2874
3540
  verification_commands: projectConventions.verification_commands,
3541
+ agent_delegation: normalizeAgentDelegationConfig(agentDelegation),
2875
3542
  };
2876
3543
 
2877
3544
  if (!existsSync(workspaceConfigPath(workspaceRoot))) {
@@ -3286,7 +3953,9 @@ export async function planStage(cwd, slug, options = {}) {
3286
3953
  }
3287
3954
 
3288
3955
  const sourceSpecPath = options.directSpecPath ? resolve(cwd, options.directSpecPath) : (state.plan_source_spec_path || artifactPath(root, 'spec.md'));
3289
- const sourceText = await readFile(sourceSpecPath, 'utf8');
3956
+ const sourceBundle = await readPlanSourceText(cwd, state, sourceSpecPath);
3957
+ const sourceText = sourceBundle.sourceText;
3958
+ const agentDelegationConfig = await readAgentDelegationConfig(cwd);
3290
3959
  const adapter = options.adapter || createDefaultPlanAdapter();
3291
3960
  const maxIterations = DEFAULT_MAX_ITERATIONS;
3292
3961
  let iteration = 1;
@@ -3296,7 +3965,7 @@ export async function planStage(cwd, slug, options = {}) {
3296
3965
  const reviewHistory = initialPlanReviewHistory(state);
3297
3966
 
3298
3967
  while (iteration <= maxIterations) {
3299
- const plannerDraft = await adapter.planner({
3968
+ const rawPlannerDraft = await adapter.planner({
3300
3969
  cwd,
3301
3970
  root,
3302
3971
  slug: normalized,
@@ -3306,6 +3975,7 @@ export async function planStage(cwd, slug, options = {}) {
3306
3975
  deliberateMode: Boolean(options.deliberate),
3307
3976
  interactiveMode: Boolean(options.interactive),
3308
3977
  });
3978
+ const plannerDraft = enrichPlannerDraftForReview({ sourceText, plannerDraft: rawPlannerDraft });
3309
3979
  await writePlanArtifacts(root, cwd, normalized, plannerDraft);
3310
3980
  const artifactPaths = await writeCanonicalPlanArtifacts(cwd, root, normalized);
3311
3981
  const changeId = state.change_id || changeIdForWorkflowSlug(normalized);
@@ -3322,6 +3992,7 @@ export async function planStage(cwd, slug, options = {}) {
3322
3992
  root,
3323
3993
  sourceText,
3324
3994
  plannerDraft,
3995
+ agentDelegationConfig,
3325
3996
  });
3326
3997
 
3327
3998
  architectReview = await adapter.architect({
@@ -3373,6 +4044,12 @@ export async function planStage(cwd, slug, options = {}) {
3373
4044
  source_requirements_item_count: traceability.itemCount,
3374
4045
  plan_delegation_decision_path: delegationDecision.path,
3375
4046
  plan_delegation_mode: delegationDecision.mode,
4047
+ plan_delegation_recommended_mode: delegationDecision.recommended_mode,
4048
+ plan_delegation_actual_mode: delegationDecision.actual_mode,
4049
+ plan_delegation_runtime_execution: delegationDecision.runtime_execution,
4050
+ plan_delegation_authorization_status: delegationDecision.authorization_status,
4051
+ plan_delegation_authorization_source: delegationDecision.authorization_source,
4052
+ plan_delegation_threshold: delegationDecision.threshold,
3376
4053
  plan_delegation_score: delegationDecision.score,
3377
4054
  plan_delegation_triggers: delegationDecision.triggers,
3378
4055
  plan_delegation_reason: delegationDecision.reason,
@@ -3382,6 +4059,7 @@ export async function planStage(cwd, slug, options = {}) {
3382
4059
  spec_delta_status: changeArtifactStatus.specDeltaStatus,
3383
4060
  slice_artifacts_status: changeArtifactStatus.sliceArtifactsStatus,
3384
4061
  plan_source_spec_path: sourceSpecPath,
4062
+ plan_source_document_paths: sourceBundle.sourceDocumentPaths,
3385
4063
  last_confirmed_transition: consumesReviewPlan || resumesConsumedReviewPlan ? TRANSITIONS.REVIEW_TO_PLAN : TRANSITIONS.CLARIFY_TO_PLAN,
3386
4064
  approval: {
3387
4065
  ...state.approval,