@ai-content-space/loopx 0.1.10 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/AGENTS.md +50 -0
  2. package/README.md +59 -450
  3. package/README.zh-CN.md +59 -461
  4. package/docs/loopx/design/loopx-skill-suite-v1-design.md +73 -0
  5. package/docs/loopx/plans/loopx-skill-suite-v1-implementation.md +77 -0
  6. package/package.json +5 -2
  7. package/plugins/loopx/.codex-plugin/plugin.json +4 -4
  8. package/plugins/loopx/scripts/plugin-install.test.mjs +20 -20
  9. package/plugins/loopx/skills/clarify/SKILL.md +38 -311
  10. package/plugins/loopx/skills/debug/SKILL.md +1 -1
  11. package/plugins/loopx/skills/exec/SKILL.md +71 -0
  12. package/plugins/loopx/skills/finish/SKILL.md +254 -0
  13. package/plugins/loopx/skills/fix-review/SKILL.md +216 -0
  14. package/plugins/loopx/skills/go-style/SKILL.md +1 -1
  15. package/plugins/loopx/skills/kratos/SKILL.md +1 -1
  16. package/plugins/loopx/skills/plan/SKILL.md +138 -271
  17. package/plugins/loopx/skills/refactor-plan/SKILL.md +71 -0
  18. package/plugins/loopx/skills/review/SKILL.md +72 -105
  19. package/plugins/loopx/skills/review/code-reviewer.md +168 -0
  20. package/plugins/loopx/skills/spec/DESIGN_SPEC_TEMPLATE.md +323 -0
  21. package/plugins/loopx/skills/spec/SKILL.md +76 -0
  22. package/plugins/loopx/skills/subagent-exec/SKILL.md +282 -0
  23. package/plugins/loopx/skills/subagent-exec/agents/openai.yaml +3 -0
  24. package/plugins/loopx/skills/subagent-exec/code-quality-reviewer-prompt.md +25 -0
  25. package/plugins/loopx/skills/subagent-exec/codex-subagents.md +37 -0
  26. package/plugins/loopx/skills/subagent-exec/implementer-prompt.md +113 -0
  27. package/plugins/loopx/skills/subagent-exec/spec-reviewer-prompt.md +61 -0
  28. package/plugins/loopx/skills/tdd/SKILL.md +1 -1
  29. package/plugins/loopx/skills/verify/SKILL.md +1 -1
  30. package/scripts/claude-workflow-hook.mjs +109 -0
  31. package/scripts/codex-workflow-hook.mjs +2 -5
  32. package/scripts/install-skills.mjs +3 -3
  33. package/scripts/verify-skills.mjs +32 -1
  34. package/skills/RESOLVER.md +26 -17
  35. package/skills/clarify/SKILL.md +38 -311
  36. package/skills/debug/SKILL.md +1 -1
  37. package/skills/exec/SKILL.md +71 -0
  38. package/skills/finish/SKILL.md +254 -0
  39. package/skills/fix-review/SKILL.md +216 -0
  40. package/skills/go-style/SKILL.md +1 -1
  41. package/skills/kratos/SKILL.md +1 -1
  42. package/skills/plan/SKILL.md +138 -271
  43. package/skills/refactor-plan/SKILL.md +71 -0
  44. package/skills/review/SKILL.md +72 -105
  45. package/skills/review/code-reviewer.md +168 -0
  46. package/skills/spec/DESIGN_SPEC_TEMPLATE.md +323 -0
  47. package/skills/spec/SKILL.md +76 -0
  48. package/skills/subagent-exec/SKILL.md +282 -0
  49. package/skills/subagent-exec/agents/openai.yaml +3 -0
  50. package/skills/subagent-exec/code-quality-reviewer-prompt.md +25 -0
  51. package/skills/subagent-exec/codex-subagents.md +37 -0
  52. package/skills/subagent-exec/implementer-prompt.md +113 -0
  53. package/skills/subagent-exec/spec-reviewer-prompt.md +61 -0
  54. package/skills/tdd/SKILL.md +1 -1
  55. package/skills/verify/SKILL.md +1 -1
  56. package/src/autopilot-runtime.mjs +1 -1
  57. package/src/cli.mjs +77 -5
  58. package/src/context-manifest.mjs +2 -2
  59. package/src/html-views.mjs +129 -195
  60. package/src/install-discovery.mjs +210 -6
  61. package/src/next-skill.mjs +2 -4
  62. package/src/plan-runtime.mjs +571 -93
  63. package/src/runtime-maintenance.mjs +5 -2
  64. package/src/workflow.mjs +865 -68
  65. package/templates/architecture.md +58 -16
  66. package/templates/development-plan.md +42 -12
  67. package/plugins/loopx/skills/archive/SKILL.md +0 -55
  68. package/plugins/loopx/skills/autopilot/SKILL.md +0 -93
  69. package/plugins/loopx/skills/build/SKILL.md +0 -228
  70. package/skills/archive/SKILL.md +0 -55
  71. package/skills/autopilot/SKILL.md +0 -93
  72. package/skills/build/SKILL.md +0 -228
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|必须|不得|不能|不自动|人工|确认|复核|执行|下发|任务|字段|状态|流程|规则|范围|来源|验收|示例|异常|差异|OCC|security_id|corporate_action_event_id|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())
@@ -660,14 +869,475 @@ 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) => /需求|范围|验收|页面|任务|事件|流程|规则|字段|接口|架构|设计|计划|处理|异常|差异|OCC|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 isCorporateActionDesignDraft(items, plannerDraft) {
1107
+ return /公司行动|corporate-actions|OCC|分红派息|拆股|合股|退市|摘牌/.test([
1108
+ ...items,
1109
+ plannerDraft?.planText,
1110
+ plannerDraft?.architectureText,
1111
+ plannerDraft?.developmentPlanText,
1112
+ ].join('\n'));
1113
+ }
1114
+
1115
+ function corporateActionDetailedDesignTextForChange({ changeId, slug }) {
1116
+ const stateRows = [
1117
+ ['生成明细', '待生成', '待复核', '事件已入库且未生成有效明细版本', '写 CustomerDetail/OptionAdjustment/Discrepancy/OperationLog'],
1118
+ ['重算', '待复核、待确认、差异待处理', '待复核', '任务未执行且存在修正输入', '废弃旧 detail_version,生成新版本'],
1119
+ ['复核', '待复核', '待确认', '明细完整且无阻断异常', '仅人工触发,写 OperationLog'],
1120
+ ['确认', '待确认', '待执行', '税费、数量、客户范围、OCC 字段均已复核', '冻结当前有效明细版本'],
1121
+ ['执行', '待执行', '执行中', '现金/持仓类动作且无阻断异常', '调用 mock ExecutionProvider,写 DownstreamStatus'],
1122
+ ['下发', '待执行', '执行中', '展示/订单/期权链/柜台类动作且无阻断异常', '调用 mock ExecutionProvider,写 DownstreamStatus'],
1123
+ ['mock 回写成功', '执行中', '已完成', '全部目标系统 success', '系统回写日志,不允许从非执行中推进'],
1124
+ ['mock 回写失败', '执行中', '差异待处理', '任一目标系统 failed/timeout', '创建 Discrepancy,保留 failure_reason'],
1125
+ ['重试', '差异待处理、执行中', '执行中', '失败原因已处理或允许补偿查询', 'retry_count + 1 并写日志'],
1126
+ ['标记无需处理', '非终态', '无需处理', '人工确认无客户/平台动作', '必须填写原因,终态禁止再执行'],
1127
+ ['标记人工完成', '待执行、差异待处理', '已完成', '外部已人工处理且有备注/凭证', '不触发 mock 执行,只写日志'],
1128
+ ];
1129
+ return [
1130
+ `# 详细设计:${changeId}`,
1131
+ '',
1132
+ '## 文档定位',
1133
+ '',
1134
+ '本文档给 build 阶段提供字段、接口、函数、组件、状态机和边界条件级别的实现输入。架构文档决定边界,开发计划决定顺序,本文决定具体怎么落地。',
1135
+ '',
1136
+ '## 需求到设计映射',
1137
+ '',
1138
+ requirementMappingTable(['总览', '分红派息', '拆股', '合股', '退市/摘牌', '期权退市 OCC', '代码/名称变更', '事件异常处理'], [
1139
+ ['需求', (item) => item],
1140
+ ['后端落点', () => 'CorporateActionUsecase + Ent repository + mock provider'],
1141
+ ['前端落点', () => 'web/admin 共享任务 shell,按 event_type 配置列表列、详情字段、动作按钮'],
1142
+ ['验证证据', () => 'usecase/API 测试 + 浏览器人工验收'],
1143
+ ]),
1144
+ '',
1145
+ '## 数据结构与字段',
1146
+ '',
1147
+ '| 实体 | 字段 | 索引/关系 | 说明 |',
1148
+ '| --- | --- | --- | --- |',
1149
+ '| CorporateActionEvent | corporate_action_event_id, source, source_event_id, event_type, market, asset_type, security_id, underlying_security_id, symbol, key_date, version, event_status, raw_snapshot_id, event_payload | unique(corporate_action_event_id, version) | 事件快照和修订追溯;同版本重复不建任务 |',
1150
+ '| CorporateActionTask | task_id, event_id, event_type, task_status, asset_scope, security_id, symbol, key_date, affected_customer_count, affected_order_count, risk_flags, current_detail_version, detail_payload | unique(task_id), index(event_id,status,type) | 运营主任务和状态机载体 |',
1151
+ '| CorporateActionCustomerDetail | task_id, detail_version, user_id, trade_account_id, security_id, asset_type, position_qty, cash_amount, tax_amount, net_amount, order_action, margin_status, process_status, detail_payload | index(task_id,detail_version,user_id) | 客户级明细;支持多版本重算 |',
1152
+ '| CorporateActionOptionAdjustment | task_id, memo_no, underlying_security_id, option_security_id, original_contract, adjusted_contract, old_strike, new_strike, old_multiplier, new_multiplier, deliverable, review_status, adjustment_payload | index(task_id,memo_no,underlying_security_id) | OCC 和期权调整明细 |',
1153
+ '| CorporateActionDiscrepancy | task_id, detail_id, option_adjustment_id, downstream_status_id, discrepancy_type, field_name, estimated_value, confirmed_value, status, resolution_note | index(task_id,status,type) | 金额、数量、客户范围、合约映射、下游回写差异 |',
1154
+ '| CorporateActionDownstreamStatus | task_id, target_system, action_type, status, request_payload, response_payload, failure_reason, retry_count | index(task_id,target_system,status) | mock 执行/下发状态;禁止真实 client 写入 |',
1155
+ '| CorporateActionOperationLog | task_id, operator_id, operator_name, action_type, before_status, after_status, remark, created_at | index(task_id,created_at) | 所有人工动作和系统回写留痕 |',
1156
+ '',
1157
+ '## 接口、函数与组件契约',
1158
+ '',
1159
+ '| 契约 | 输入 | 输出 | 错误/权限 |',
1160
+ '| --- | --- | --- | --- |',
1161
+ '| GET /admin/v1/corporate-actions/overview | status/date range | 统计卡片、风险提示、异常/超时 | admin.corporate_action.view |',
1162
+ '| POST /admin/v1/corporate-actions/mock/seed | seed profile | 8 类 mock 任务数量 | admin.corporate_action.seed;重复 seed 按 event id 去重 |',
1163
+ '| GET /admin/v1/corporate-actions/tasks | event_type/status/symbol/task_id/key_date/page | 任务列表和分页 | admin.corporate_action.view |',
1164
+ '| GET /admin/v1/corporate-actions/tasks/{task_id} | task_id/detail_version | event/task/details/options/discrepancies/downstream/logs/available_actions | admin.corporate_action.view |',
1165
+ '| POST /tasks/{task_id}/details/generate | task_id, force_recalc, remark | 新 detail_version 和状态 | admin.corporate_action.operate;非法状态拒绝 |',
1166
+ '| POST /tasks/{task_id}/actions/{action} | action, remark, optional resolution payload | 新状态、日志、下游状态 | admin.corporate_action.operate;状态机统一校验 |',
1167
+ '| GET /discrepancies | status/type/task_id/symbol/page | 异常列表 | admin.corporate_action.view |',
1168
+ '| POST /discrepancies/{id}/resolve | confirmed_value/resolution_note/next_status | 异常处理结果和任务状态 | admin.corporate_action.operate |',
1169
+ '',
1170
+ 'Provider 接口固定为:`EventSourceProvider.FetchMockEvents`、`ImpactProvider.BuildDetails`、`ExecutionProvider.Execute`、`ExceptionProvider.Resolve`。首期只提供 local mock 实现;真实 adapter 不在本次范围。',
1171
+ '',
1172
+ '## 状态机与流程细节',
1173
+ '',
1174
+ requirementMappingTable(stateRows, [
1175
+ ['动作', (row) => row[0]],
1176
+ ['起始状态', (row) => row[1]],
1177
+ ['成功状态', (row) => row[2]],
1178
+ ['前置条件', (row) => row[3]],
1179
+ ['副作用', (row) => row[4]],
1180
+ ]),
1181
+ '',
1182
+ '任务级状态优先级为:差异待处理 > 执行中 > 待执行 > 待确认 > 待复核 > 待生成。明细级阻断异常优先于任务级普通状态;只要存在未处理阻断异常,确认、执行、下发必须拒绝。',
1183
+ '',
1184
+ '## 错误处理与边界条件',
1185
+ '',
1186
+ '- 重复事件:同 `corporate_action_event_id + version` 返回已有事件,不重复建任务。',
1187
+ '- 事件修订:新 version 建立新事件快照;旧任务未完成时进入差异待处理或无需处理,保留日志。',
1188
+ '- 缺证券映射:创建事件异常,任务停在待复核,不允许执行。',
1189
+ '- OCC 字段缺失或合约匹配失败:写 OptionAdjustment + Discrepancy,任务差异待处理。',
1190
+ '- mock 下游失败/超时:写 DownstreamStatus failure_reason,任务差异待处理。',
1191
+ '- 真实副作用防线:公司行动模块不得注入真实资产、交易、清算、通知 client;测试需要断言执行/下发只写 mock 表。',
1192
+ '',
1193
+ '## 前端设计',
1194
+ '',
1195
+ '- `web/admin` 提供总览、任务页、异常页;总览首屏直接展示公司行动任务数据,不做营销页。',
1196
+ '- 任务页使用共享 shell:左侧筛选/列表,中间客户明细和期权明细,右侧任务详情、可用动作、操作日志。',
1197
+ '- `event_type` 配置列表列、详情字段、明细表列和动作按钮;按钮只使用后端 `available_actions`。',
1198
+ '- API base 固定同源 `/admin/v1/corporate-actions`,不要出现 `/account/admin/v1`。',
1199
+ '',
1200
+ '## 测试设计',
1201
+ '',
1202
+ '- 状态机:覆盖每个动作的合法/非法状态,尤其未确认执行、执行中重算、终态继续动作、存在阻断异常时确认/执行。',
1203
+ '- 数据:事件去重、版本修订、任务详情聚合、服务重启后查询。',
1204
+ '- 业务:8 类 mock 工作流各至少一条闭环样例。',
1205
+ '- API:overview/list/detail/action/discrepancy、权限标识、非法参数。',
1206
+ '- 前端:`npm run build`、浏览器检查总览/任务/异常、最长字段不重叠。',
1207
+ '- 副作用:执行/下发不调用真实 client,不发送通知。',
1208
+ '',
1209
+ '## 实现注意事项',
1210
+ '',
1211
+ '- 先实现状态机单测,再写 handler 和页面动作。',
1212
+ '- Ent schema 为 additive;生产回滚优先关闭菜单/路由,不直接删表。',
1213
+ '- build 阶段如果发现源文档和原型冲突,停止对应切片并回 plan 修订。',
1214
+ '',
1215
+ '## Source',
1216
+ '',
1217
+ `- workflow slug: ${slug}`,
1218
+ `- change id: ${changeId}`,
1219
+ ].join('\n');
1220
+ }
1221
+
1222
+ function detailedDesignTextForChange({ changeId, slug, items, plannerDraft }) {
1223
+ if (isCorporateActionDesignDraft(items, plannerDraft)) {
1224
+ return corporateActionDetailedDesignTextForChange({ changeId, slug });
1225
+ }
1226
+ return [
1227
+ `# loopx Detailed Design: ${changeId}`,
1228
+ '',
1229
+ '## 文档定位',
1230
+ '',
1231
+ '详细设计回答“具体怎么实现到字段、接口、函数、组件、状态流转和边界条件”。它承接 `architecture.md` 的边界和 `development-plan.md` 的切片,但比二者更接近 build 可执行输入;build 阶段不得只凭概要描述自行发明字段、接口或状态。',
1232
+ '',
1233
+ '## 需求到设计映射',
1234
+ '',
1235
+ sourceRequirementRows(items, [
1236
+ ['设计落点', () => '数据结构、接口/函数/组件契约、状态机、错误处理和测试设计均需有对应条目'],
1237
+ ['实现证据', () => 'build 阶段在 execution-record.md 记录代码路径、验证命令或人工验收证据'],
1238
+ ]),
1239
+ '',
1240
+ '## 数据结构与字段',
1241
+ '',
1242
+ sourceRequirementRows(items, [
1243
+ ['实体/结构', () => '列出需要新增或修改的实体、DTO、schema、payload 或前端 state'],
1244
+ ['关键字段', () => '字段名、类型、来源、是否必填、默认值、唯一性/索引、审计要求'],
1245
+ ['迁移/兼容', () => '是否需要 migration、生成代码、旧数据兼容或回填策略'],
1246
+ ]),
1247
+ '',
1248
+ '## 接口、函数与组件契约',
1249
+ '',
1250
+ sourceRequirementRows(items, [
1251
+ ['契约对象', () => 'API 路径、provider 方法、usecase 函数、repository 方法、前端组件 props/events'],
1252
+ ['输入输出', () => '参数、响应、错误码、权限、幂等键、分页/筛选/排序或事件格式'],
1253
+ ['调用方/被调方', () => '明确调用方向和禁止调用的真实外部依赖'],
1254
+ ]),
1255
+ '',
1256
+ '## 状态机与流程细节',
1257
+ '',
1258
+ sourceRequirementRows(items, [
1259
+ ['状态/步骤', () => '列出允许状态、动作、前置条件、后置条件和审计日志'],
1260
+ ['非法路径', () => '列出必须拒绝的动作、重复请求、越权、缺数据和异常回写'],
1261
+ ]),
1262
+ '',
1263
+ '## 错误处理与边界条件',
1264
+ '',
1265
+ sourceRequirementRows(items, [
1266
+ ['错误场景', () => '输入缺失、依赖失败、并发冲突、数据不一致、mock/真实边界误用'],
1267
+ ['处理方式', () => '返回错误、保持原状态、写日志、创建异常、人工处理或回滚'],
1268
+ ]),
1269
+ '',
1270
+ '## 测试设计',
1271
+ '',
1272
+ sourceRequirementRows(items, [
1273
+ ['测试类型', () => '单测、集成、API、前端构建、浏览器人工验收或回归命令'],
1274
+ ['断言重点', () => '状态、字段、权限、副作用隔离、日志、错误路径和源需求覆盖'],
1275
+ ]),
1276
+ '',
1277
+ '## 实现注意事项',
1278
+ '',
1279
+ '- build 阶段必须优先遵循本详细设计;发现字段、接口或状态缺失时,不得自行扩大范围,必须记录 blocker 或回到 plan。',
1280
+ '- 任何真实外部系统、资金资产、交易订单、通知或权限相关副作用,都必须在本文件中有明确允许才可实现。',
1281
+ '- 生成代码、迁移和前端构建产物必须记录来源命令,避免把运行时临时产物当作设计输入。',
1282
+ '',
1283
+ '## 上游架构摘要',
1284
+ '',
1285
+ plannerDraft.architectureText || '- 见 workflow-local `architecture.md`。',
1286
+ '',
1287
+ '## 上游开发切片摘要',
1288
+ '',
1289
+ plannerDraft.developmentPlanText || '- 见 workflow-local `development-plan.md`。',
1290
+ '',
1291
+ '## Source',
1292
+ '',
1293
+ `- workflow slug: ${slug}`,
1294
+ `- change id: ${changeId}`,
1295
+ ].join('\n');
1296
+ }
1297
+
1298
+ function enrichTestPlanTextForReview(text, items) {
1299
+ let next = text;
1300
+ next = appendMarkdownSectionIfMissing(next, '需求到测试矩阵', [
1301
+ requirementMappingTable(items, [
1302
+ ['#', (_, index) => index + 1],
1303
+ ['原始需求项', (item) => item],
1304
+ ['自动化验证', () => '优先使用仓库原生命令覆盖状态、接口、数据或构建行为'],
1305
+ ['人工验收', () => '对无法自动证明的页面、审批、风险边界做人工确认'],
1306
+ ['证据', () => '命令输出、截图路径、日志片段或执行记录条目'],
1307
+ ]),
1308
+ ].join('\n'));
1309
+ next = appendMarkdownSectionIfMissing(next, '回归门禁', [
1310
+ '- build 阶段必须先跑计划列出的最小验证,再跑仓库级回归。',
1311
+ '- deslop 后必须重新验证,不能复用旧输出。',
1312
+ '- 如果某个源需求没有验证信号,execution-record.md 必须把它列入 blocker 或 remaining_scope。',
1313
+ ].join('\n'));
1314
+ return next;
1315
+ }
1316
+
1317
+ function enrichPlannerDraftForReview({ sourceText, plannerDraft }) {
1318
+ const draft = {
1319
+ ...plannerDraft,
1320
+ principles: Array.isArray(plannerDraft.principles) ? plannerDraft.principles : [],
1321
+ decisionDrivers: Array.isArray(plannerDraft.decisionDrivers) ? plannerDraft.decisionDrivers : [],
1322
+ options: Array.isArray(plannerDraft.options) ? plannerDraft.options : [],
1323
+ planText: String(plannerDraft.planText || ''),
1324
+ architectureText: String(plannerDraft.architectureText || ''),
1325
+ developmentPlanText: String(plannerDraft.developmentPlanText || ''),
1326
+ testPlanText: String(plannerDraft.testPlanText || ''),
1327
+ };
1328
+ const items = sourceItemsForPlanEnrichment(sourceText, draft);
1329
+ if (items.length === 0) {
1330
+ return draft;
1331
+ }
1332
+ return {
1333
+ ...draft,
1334
+ planText: canEnrichChineseReviewText(draft.planText) ? enrichPlanTextForReview(draft.planText, items) : draft.planText,
1335
+ architectureText: canEnrichChineseReviewText(draft.architectureText) ? enrichArchitectureTextForReview(draft.architectureText, items) : draft.architectureText,
1336
+ developmentPlanText: canEnrichChineseReviewText(draft.developmentPlanText) ? enrichDevelopmentPlanTextForReview(draft.developmentPlanText, items) : draft.developmentPlanText,
1337
+ testPlanText: canEnrichChineseReviewText(draft.testPlanText) ? enrichTestPlanTextForReview(draft.testPlanText, items) : draft.testPlanText,
1338
+ };
1339
+ }
1340
+
671
1341
  function normalizedCoverageText(...parts) {
672
1342
  return parts.join('\n')
673
1343
  .toLowerCase()
@@ -695,7 +1365,8 @@ function sourceRequirementCovered(item, haystack) {
695
1365
  if (tokens.length === 0) {
696
1366
  return false;
697
1367
  }
698
- return tokens.every((token) => haystack.includes(token));
1368
+ const matched = tokens.filter((token) => haystack.includes(token)).length;
1369
+ return matched / tokens.length >= 0.65;
699
1370
  }
700
1371
 
701
1372
  async function writeRequirementTraceabilityArtifact({ root, sourceSpecPath, sourceText, plannerDraft, changeArtifactPaths }) {
@@ -728,22 +1399,34 @@ async function writeRequirementTraceabilityArtifact({ root, sourceSpecPath, sour
728
1399
  '# 原始需求覆盖矩阵',
729
1400
  '',
730
1401
  `- 来源:${sourceSpecPath}`,
731
- `- 状态:${status}`,
1402
+ `- 覆盖状态:${status === 'complete' ? '完整' : '部分缺失'} (${status})`,
732
1403
  `- 提取项数量:${rows.length}`,
733
1404
  '',
1405
+ '## 审阅说明',
1406
+ '',
1407
+ '- 本文件用于人工确认源需求是否被计划、架构、开发切片、规格增量和测试计划承接。',
1408
+ '- “原始需求项”保留源文档原文;如果源文档是英文,表格会保留英文原句,但覆盖状态和审阅说明必须使用中文。',
1409
+ '- 未覆盖项会阻断 `plan -> build`,直到 Planner 重新展开计划或明确把该项列为非目标并说明理由。',
1410
+ '',
734
1411
  '## 覆盖矩阵',
735
1412
  '',
736
- '| 原始需求项 | 覆盖状态 |',
737
- '| --- | --- |',
1413
+ '| 原始需求项 | 覆盖状态 | 审阅说明 |',
1414
+ '| --- | --- | --- |',
738
1415
  ...(rows.length > 0
739
- ? rows.map((row) => `| ${row.item.replace(/\|/g, '\\|')} | ${row.status} |`)
740
- : ['| 未检测到显式需求覆盖项 | covered |']),
1416
+ ? rows.map((row) => `| ${row.item.replace(/\|/g, '\\|')} | ${row.status === 'covered' ? '已覆盖' : '未覆盖'} | ${row.status === 'covered' ? '已在计划包或变更工件中找到对应表述。' : '计划包没有找到可追溯表述,需要回到 plan 修订。'} |`)
1417
+ : ['| 未检测到显式需求覆盖项 | 已覆盖 | 没有从源文档中提取到独立覆盖项。 |']),
741
1418
  '',
742
1419
  '## 门禁',
743
1420
  '',
744
1421
  ...(blockers.length > 0
745
- ? blockers.map((blocker) => `- ${blocker}`)
746
- : ['- complete']),
1422
+ ? [
1423
+ '- 结果:存在原始需求未被计划包充分承接,不能进入 build handoff。',
1424
+ ...rows
1425
+ .filter((row) => row.status !== 'covered')
1426
+ .map((row) => `- 未覆盖需求:${row.item}`),
1427
+ ...blockers.map((blocker) => `- ${blocker}`),
1428
+ ]
1429
+ : ['- 结果:全部原始需求已覆盖,可以进入后续 plan gate 检查。']),
747
1430
  ].join('\n'));
748
1431
 
749
1432
  return {
@@ -796,32 +1479,83 @@ function delegationDecisionForPlan(sourceText, plannerDraft) {
796
1479
  addTrigger('architectural_tradeoff', 1);
797
1480
  }
798
1481
 
799
- const mode = score >= 7 ? 'parallel-review' : (score >= 4 ? 'critic-only' : 'local');
800
- const reason = mode === 'parallel-review'
1482
+ const recommendedMode = score >= 7 ? 'parallel-review' : (score >= 4 ? 'critic-only' : 'local');
1483
+ const reason = recommendedMode === 'parallel-review'
801
1484
  ? '高风险或跨模块规划,建议独立 Planner/Architect/Critic 视角并行审查。'
802
- : mode === 'critic-only'
803
- ? '存在中等复杂度或验证风险,建议至少引入独立 critic 复核 PRD 覆盖和风险。'
1485
+ : recommendedMode === 'critic-only'
1486
+ ? '存在中等复杂度或验证风险,建议至少引入独立 critic 复核需求覆盖和风险。'
804
1487
  : '范围较小或风险较低,本地顺序 Planner/Architect/Critic 审阅足够。';
805
1488
 
806
1489
  return {
807
- mode,
1490
+ mode: recommendedMode,
1491
+ recommended_mode: recommendedMode,
808
1492
  score,
809
1493
  triggers,
810
1494
  reason,
811
- current_runtime_execution: 'local-sequential',
812
- execution_note: '当前 runtime 记录委派决策依据;是否实际启动 native subagents 仍受执行环境和用户授权约束。',
813
1495
  };
814
1496
  }
815
1497
 
816
- async function writePlanDelegationDecisionArtifact({ root, sourceText, plannerDraft }) {
1498
+ function resolvePlanDelegationExecution(recommendedMode, config) {
1499
+ const normalized = normalizeAgentDelegationConfig(config);
1500
+ const thresholdMet = delegationMeetsThreshold(recommendedMode, normalized.threshold);
1501
+ if (!normalized.enabled) {
1502
+ return {
1503
+ actual_mode: 'local',
1504
+ runtime_execution: 'local-sequential',
1505
+ authorization_status: 'disabled',
1506
+ authorization_source: '.loopx/config.json:agent_delegation.enabled=false',
1507
+ threshold: normalized.threshold,
1508
+ config: normalized,
1509
+ note: '已记录推荐委派模式;未授权自动启动 subagents,因此本次实际执行保持本地顺序审阅。',
1510
+ };
1511
+ }
1512
+ if (!thresholdMet) {
1513
+ return {
1514
+ actual_mode: 'local',
1515
+ runtime_execution: 'local-sequential',
1516
+ authorization_status: 'below-threshold',
1517
+ authorization_source: '.loopx/config.json:agent_delegation.threshold',
1518
+ threshold: normalized.threshold,
1519
+ config: normalized,
1520
+ note: `推荐模式 ${recommendedMode} 低于自动委派阈值 ${normalized.threshold},实际执行保持本地顺序审阅。`,
1521
+ };
1522
+ }
1523
+ if (!normalized.auto_start) {
1524
+ return {
1525
+ actual_mode: 'local',
1526
+ runtime_execution: 'manual-subagent-review',
1527
+ authorization_status: 'manual-required',
1528
+ authorization_source: '.loopx/config.json:agent_delegation.auto_start=false',
1529
+ threshold: normalized.threshold,
1530
+ config: normalized,
1531
+ note: '配置允许记录委派建议,但未授权自动启动;需要用户或外部执行器手动开启推荐的 subagent review。',
1532
+ };
1533
+ }
1534
+ return {
1535
+ actual_mode: recommendedMode,
1536
+ runtime_execution: 'auto-subagent-review',
1537
+ authorization_status: 'auto-authorized',
1538
+ authorization_source: '.loopx/config.json:agent_delegation.auto_start=true',
1539
+ threshold: normalized.threshold,
1540
+ config: normalized,
1541
+ note: '配置已授权达到阈值时自动使用推荐的 subagent review 模式;具体启动由当前 agent runtime 执行。',
1542
+ };
1543
+ }
1544
+
1545
+ async function writePlanDelegationDecisionArtifact({ root, sourceText, plannerDraft, agentDelegationConfig }) {
817
1546
  const decision = delegationDecisionForPlan(sourceText, plannerDraft);
1547
+ const execution = resolvePlanDelegationExecution(decision.recommended_mode, agentDelegationConfig);
818
1548
  const path = artifactPath(root, 'plan-delegation-decision.md');
819
1549
  await writeText(path, [
820
1550
  '# Plan Delegation Decision',
821
1551
  '',
822
- `- mode: ${decision.mode}`,
1552
+ `- recommended_mode: ${decision.recommended_mode}`,
1553
+ `- actual_mode: ${execution.actual_mode}`,
1554
+ `- runtime_execution: ${execution.runtime_execution}`,
1555
+ `- authorization_status: ${execution.authorization_status}`,
1556
+ `- authorization_source: ${execution.authorization_source}`,
1557
+ `- threshold: ${execution.threshold}`,
823
1558
  `- score: ${decision.score}`,
824
- `- current_runtime_execution: ${decision.current_runtime_execution}`,
825
1559
  `- reason: ${decision.reason}`,
826
1560
  '',
827
1561
  '## Triggers',
@@ -831,14 +1565,20 @@ async function writePlanDelegationDecisionArtifact({ root, sourceText, plannerDr
831
1565
  '## Guidance',
832
1566
  '',
833
1567
  '- local: 低风险、小范围、单模块任务,本地顺序 Planner/Architect/Critic 即可。',
834
- '- critic-only: 中等风险或覆盖面较宽,至少需要独立 critic 复核 PRD 覆盖、验证和遗漏风险。',
1568
+ '- critic-only: 中等风险或覆盖面较宽,至少需要独立 critic 复核需求覆盖、验证和遗漏风险。',
835
1569
  '- parallel-review: 高风险、多模块、状态/资产/安全相关任务,建议独立 Planner/Architect/Critic 视角并行审查。',
836
1570
  '',
1571
+ '## Authorization',
1572
+ '',
1573
+ '- `recommended_mode` 是基于需求/plan 风险面的规划建议。',
1574
+ '- `actual_mode` 是结合 `.loopx/config.json` 授权边界后的本次实际执行模式。',
1575
+ '- 只有 `agent_delegation.enabled=true`、`auto_start=true` 且推荐模式达到 `threshold` 时,loopx 才会把实际模式提升到推荐的 subagent review。',
1576
+ '',
837
1577
  '## Runtime Note',
838
1578
  '',
839
- `- ${decision.execution_note}`,
1579
+ `- ${execution.note}`,
840
1580
  ].join('\n'));
841
- return { path, ...decision };
1581
+ return { path, ...decision, ...execution };
842
1582
  }
843
1583
 
844
1584
  function frontmatterList(text, key) {
@@ -1092,19 +1832,18 @@ function validateRequirementDelta(text) {
1092
1832
  return { delta, blockers: dedupeStrings(blockers) };
1093
1833
  }
1094
1834
 
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());
1835
+ function requirementsForDelta(slug, plannerDraft, sourceText = '') {
1836
+ const sourceRequirements = sourceRequirementItems(sourceText);
1837
+ const requirements = sourceRequirements.length > 0
1838
+ ? sourceRequirements
1839
+ : numberedPlanItems(plannerDraft.planText);
1101
1840
  return dedupeStrings(requirements.length > 0 ? requirements : [
1102
1841
  `Workflow ${slug} SHALL implement the approved loopx plan package.`,
1103
1842
  ]);
1104
1843
  }
1105
1844
 
1106
- function verticalSlicesForChange(slug, plannerDraft) {
1107
- const requirements = requirementsForDelta(slug, plannerDraft);
1845
+ function verticalSlicesForChange(slug, plannerDraft, sourceText = '') {
1846
+ const requirements = requirementsForDelta(slug, plannerDraft, sourceText);
1108
1847
  const slices = requirements.slice(0, 8).map((requirement, index) => ({
1109
1848
  id: `VS-${index + 1}`,
1110
1849
  title: requirement.length > 90 ? `${requirement.slice(0, 87)}...` : requirement,
@@ -1189,8 +1928,8 @@ async function writeChangeArtifacts(cwd, root, slug, sourceText, plannerDraft, c
1189
1928
  graph: join(changeRoot, 'artifact-graph.json'),
1190
1929
  };
1191
1930
  const domains = targetDomainsForChange(slug, sourceText);
1192
- const requirements = requirementsForDelta(slug, plannerDraft);
1193
- const slices = verticalSlicesForChange(slug, plannerDraft);
1931
+ const requirements = requirementsForDelta(slug, plannerDraft, sourceText);
1932
+ const slices = verticalSlicesForChange(slug, plannerDraft, sourceText);
1194
1933
 
1195
1934
  await writeText(paths.proposal, [
1196
1935
  `# loopx Change Proposal: ${normalizedChangeId}`,
@@ -1230,17 +1969,12 @@ async function writeChangeArtifacts(cwd, root, slug, sourceText, plannerDraft, c
1230
1969
  ...requirements.flatMap((item, index) => [requirementBlockFromText({ slug, text: item, index }), '']),
1231
1970
  ].join('\n'));
1232
1971
 
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'));
1972
+ await writeText(paths.design, detailedDesignTextForChange({
1973
+ changeId: normalizedChangeId,
1974
+ slug,
1975
+ items: requirements,
1976
+ plannerDraft,
1977
+ }));
1244
1978
 
1245
1979
  await writeText(paths.tasks, [
1246
1980
  `# loopx Change Tasks: ${normalizedChangeId}`,
@@ -1614,7 +2348,44 @@ function containsChineseText(text) {
1614
2348
  return chineseChars.length >= 40 || (chineseChars.length >= 8 && chineseChars.length / signalChars >= 0.2);
1615
2349
  }
1616
2350
 
1617
- async function planLanguageBlockers(pathsByKey) {
2351
+ function canEnrichChineseReviewText(text) {
2352
+ const chineseChars = String(text || '').match(/[\u3400-\u9fff]/g) || [];
2353
+ return chineseChars.length >= 12;
2354
+ }
2355
+
2356
+ const REVIEW_DOCUMENT_CONTRACTS = {
2357
+ architecture: ['文档定位', '架构目标与非目标', '上下文与系统边界', '组件与职责', '数据与状态模型', '接口与集成契约', '关键流程', '架构决策记录'],
2358
+ developmentPlan: ['文档定位', '交付切片', '实施顺序与依赖', '文件级变更清单', '验证计划', '完成定义'],
2359
+ design: ['文档定位', '需求到设计映射', '数据结构与字段', '接口、函数与组件契约', '状态机与流程细节', '错误处理与边界条件', '测试设计'],
2360
+ };
2361
+
2362
+ function planReviewabilityBlockers(key, text, sourceItemCount) {
2363
+ const reviewerDocs = new Set(['plan', 'architecture', 'developmentPlan', 'testPlan', 'requirementsSnapshot', 'testSpec', 'design']);
2364
+ if (!reviewerDocs.has(key)) {
2365
+ return [];
2366
+ }
2367
+ const blockers = [];
2368
+ const nonEmptyLineCount = String(text || '').split('\n').filter((line) => line.trim()).length;
2369
+ const headingCount = (String(text || '').match(/^#{2,4}\s+/gm) || []).length;
2370
+ const needsSourceMapping = sourceItemCount >= 2;
2371
+ const minLines = needsSourceMapping ? Math.min(22, 8 + sourceItemCount) : 3;
2372
+ const minHeadings = needsSourceMapping ? 3 : 1;
2373
+ if (nonEmptyLineCount < minLines || headingCount < minHeadings) {
2374
+ blockers.push(`plan_artifact_too_thin_${key}`);
2375
+ }
2376
+ if (needsSourceMapping && !/(原始需求|需求.*映射|需求.*覆盖|覆盖矩阵|需求到|测试矩阵|交付切片)/.test(text)) {
2377
+ blockers.push(`plan_artifact_missing_source_mapping_${key}`);
2378
+ }
2379
+ const requiredHeadings = REVIEW_DOCUMENT_CONTRACTS[key] || [];
2380
+ for (const heading of requiredHeadings) {
2381
+ if (!hasMarkdownHeading(text, heading)) {
2382
+ blockers.push(`plan_artifact_missing_section_${key}_${slugKey(heading)}`);
2383
+ }
2384
+ }
2385
+ return blockers;
2386
+ }
2387
+
2388
+ async function planLanguageBlockers(pathsByKey, { sourceItemCount = 0 } = {}) {
1618
2389
  const blockers = [];
1619
2390
  for (const [key, path] of Object.entries(pathsByKey)) {
1620
2391
  if (!existsSync(path)) {
@@ -1625,6 +2396,7 @@ async function planLanguageBlockers(pathsByKey) {
1625
2396
  if (!containsChineseText(text)) {
1626
2397
  blockers.push(`plan_artifact_not_chinese_${key}`);
1627
2398
  }
2399
+ blockers.push(...planReviewabilityBlockers(key, text, sourceItemCount));
1628
2400
  }
1629
2401
  return blockers;
1630
2402
  }
@@ -1651,12 +2423,23 @@ async function ensurePlanWorkflowFromDirectSpec(cwd, directSpecPath, explicitSlu
1651
2423
  const existing = await readState(cwd, slug);
1652
2424
  if (existing) {
1653
2425
  const merged = withRecommendedAction({
2426
+ ...createInitialState(slug, existing.clarify_profile || existing.profile || 'standard'),
1654
2427
  ...existing,
2428
+ schema_version: WORKFLOW_SCHEMA_VERSION,
2429
+ slug,
2430
+ current_stage: existing.current_stage || STAGES.CLARIFY,
2431
+ stage_status: existing.stage_status || 'awaiting-approval',
1655
2432
  spec_artifact_path: resolvedSpecPath,
1656
2433
  plan_source_spec_path: resolvedSpecPath,
2434
+ requested_transition: existing.requested_transition || TRANSITIONS.CLARIFY_TO_PLAN,
1657
2435
  plan_consensus_mode: true,
1658
2436
  plan_deliberate_mode: Boolean(options.deliberate),
1659
2437
  plan_interactive_mode: Boolean(options.interactive),
2438
+ approval: {
2439
+ ...createInitialState(slug, existing.clarify_profile || existing.profile || 'standard').approval,
2440
+ ...(existing.approval || {}),
2441
+ plan: APPROVAL_STATES.APPROVED,
2442
+ },
1660
2443
  });
1661
2444
  await writeState(root, merged);
1662
2445
  return { slug, root, state: merged };
@@ -1787,7 +2570,7 @@ async function readPlanCompletion(cwd, root, slug, state) {
1787
2570
  blockers.push('execution_inputs_unresolved');
1788
2571
  }
1789
2572
  if (!state.plan_artifact_path || !existsSync(state.plan_artifact_path)) {
1790
- blockers.push('missing_prd');
2573
+ blockers.push('missing_requirements_snapshot');
1791
2574
  }
1792
2575
  if (!state.test_spec_artifact_path || !existsSync(state.test_spec_artifact_path)) {
1793
2576
  blockers.push('missing_test_spec');
@@ -1816,16 +2599,21 @@ async function readPlanCompletion(cwd, root, slug, state) {
1816
2599
  blockers.push(`source_requirements_${state.source_requirements_status}`);
1817
2600
  }
1818
2601
  }
2602
+ let sourceItemCount = 0;
2603
+ if (state.plan_source_spec_path && existsSync(state.plan_source_spec_path)) {
2604
+ sourceItemCount = sourceRequirementItems(await readFile(state.plan_source_spec_path, 'utf8')).length;
2605
+ }
1819
2606
  blockers.push(...await planLanguageBlockers({
1820
2607
  plan: artifactPath(root, 'plan.md'),
1821
2608
  architecture: artifactPath(root, 'architecture.md'),
1822
2609
  developmentPlan: artifactPath(root, 'development-plan.md'),
1823
2610
  testPlan: artifactPath(root, 'test-plan.md'),
1824
- prd: state.plan_artifact_path || join(resolvePlansRoot(cwd), `prd-${slug}.md`),
2611
+ requirementsSnapshot: state.plan_artifact_path || join(resolvePlansRoot(cwd), `requirements-snapshot-${slug}.md`),
1825
2612
  testSpec: state.test_spec_artifact_path || join(resolvePlansRoot(cwd), `test-spec-${slug}.md`),
1826
2613
  traceability: state.requirement_traceability_path || artifactPath(root, 'requirement-traceability.md'),
1827
2614
  delegationDecision: state.plan_delegation_decision_path || artifactPath(root, 'plan-delegation-decision.md'),
1828
- }));
2615
+ design: state.change_artifact_paths?.design || join(resolveChangeRoot(cwd, state.change_id || changeIdForWorkflowSlug(slug)), 'design.md'),
2616
+ }, { sourceItemCount }));
1829
2617
  const changeStatus = await readChangeArtifactStatus(state.change_artifact_paths);
1830
2618
  blockers.push(...changeStatus.blockers);
1831
2619
 
@@ -1949,10 +2737,10 @@ async function buildCompletionAudit({ cwd, root, slug, state, reviewReworkArtifa
1949
2737
  };
1950
2738
 
1951
2739
  addChecklistItem({
1952
- id: 'approved-prd',
2740
+ id: 'requirements-snapshot',
1953
2741
  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'],
2742
+ requirement: state.plan_artifact_path || join(cwd, '.loopx', 'plans', `requirements-snapshot-${slug}.md`),
2743
+ evidence: [state.plan_artifact_path || 'requirements snapshot artifact'],
1956
2744
  });
1957
2745
  addChecklistItem({
1958
2746
  id: 'test-spec',
@@ -2175,9 +2963,6 @@ function clarifyReadinessBlockers(state) {
2175
2963
  if (state.clarify_current_round > state.clarify_max_rounds) {
2176
2964
  blockers.push('clarify_max_rounds_exceeded');
2177
2965
  }
2178
- if (state.clarify_ambiguity_score > state.clarify_target_ambiguity_threshold) {
2179
- blockers.push('clarify_ambiguity_score_above_threshold');
2180
- }
2181
2966
  if (!state.clarify_non_goals_resolved) {
2182
2967
  blockers.push('clarify_non_goals_unresolved');
2183
2968
  }
@@ -2211,7 +2996,7 @@ function planReadinessBlockersSync(state) {
2211
2996
  blockers.push('execution_inputs_unresolved');
2212
2997
  }
2213
2998
  if (!state.plan_artifact_path) {
2214
- blockers.push('missing_prd');
2999
+ blockers.push('missing_requirements_snapshot');
2215
3000
  }
2216
3001
  if (!state.test_spec_artifact_path) {
2217
3002
  blockers.push('missing_test_spec');
@@ -2320,7 +3105,7 @@ function buildCurrentEvidenceChain(state, readiness = buildReadiness(state), aut
2320
3105
  if (readiness.plan.ready) {
2321
3106
  evidence.push(evidenceEntry(
2322
3107
  'clarify_ready_for_plan',
2323
- 'Clarify ambiguity score, non-goals, decision boundaries, pressure pass, and unresolved ambiguity gates are satisfied.',
3108
+ 'Clarify has zero unresolved ambiguity and non-goals, decision boundaries, and pressure pass gates are satisfied.',
2324
3109
  authorization.plan.authorized ? 'The approved clarify -> plan transition can be consumed by plan.' : 'Plan readiness exists, but user authorization is still separate.',
2325
3110
  ));
2326
3111
  }
@@ -2845,7 +3630,7 @@ async function renderPlanReadingViews(cwd, root, state, slug) {
2845
3630
  }
2846
3631
  }
2847
3632
 
2848
- export async function initWorkspace(cwd, { slug } = {}) {
3633
+ export async function initWorkspace(cwd, { slug, agentDelegation = {} } = {}) {
2849
3634
  const workspaceRoot = resolveWorkspaceRoot(cwd);
2850
3635
  const projectConventions = await inspectProjectConventions(cwd);
2851
3636
  await ensureLoopxRoot(cwd);
@@ -2872,6 +3657,7 @@ export async function initWorkspace(cwd, { slug } = {}) {
2872
3657
  existing_spec_sources: projectConventions.existing_spec_sources,
2873
3658
  },
2874
3659
  verification_commands: projectConventions.verification_commands,
3660
+ agent_delegation: normalizeAgentDelegationConfig(agentDelegation),
2875
3661
  };
2876
3662
 
2877
3663
  if (!existsSync(workspaceConfigPath(workspaceRoot))) {
@@ -3286,7 +4072,9 @@ export async function planStage(cwd, slug, options = {}) {
3286
4072
  }
3287
4073
 
3288
4074
  const sourceSpecPath = options.directSpecPath ? resolve(cwd, options.directSpecPath) : (state.plan_source_spec_path || artifactPath(root, 'spec.md'));
3289
- const sourceText = await readFile(sourceSpecPath, 'utf8');
4075
+ const sourceBundle = await readPlanSourceText(cwd, state, sourceSpecPath);
4076
+ const sourceText = sourceBundle.sourceText;
4077
+ const agentDelegationConfig = await readAgentDelegationConfig(cwd);
3290
4078
  const adapter = options.adapter || createDefaultPlanAdapter();
3291
4079
  const maxIterations = DEFAULT_MAX_ITERATIONS;
3292
4080
  let iteration = 1;
@@ -3296,7 +4084,7 @@ export async function planStage(cwd, slug, options = {}) {
3296
4084
  const reviewHistory = initialPlanReviewHistory(state);
3297
4085
 
3298
4086
  while (iteration <= maxIterations) {
3299
- const plannerDraft = await adapter.planner({
4087
+ const rawPlannerDraft = await adapter.planner({
3300
4088
  cwd,
3301
4089
  root,
3302
4090
  slug: normalized,
@@ -3306,6 +4094,7 @@ export async function planStage(cwd, slug, options = {}) {
3306
4094
  deliberateMode: Boolean(options.deliberate),
3307
4095
  interactiveMode: Boolean(options.interactive),
3308
4096
  });
4097
+ const plannerDraft = enrichPlannerDraftForReview({ sourceText, plannerDraft: rawPlannerDraft });
3309
4098
  await writePlanArtifacts(root, cwd, normalized, plannerDraft);
3310
4099
  const artifactPaths = await writeCanonicalPlanArtifacts(cwd, root, normalized);
3311
4100
  const changeId = state.change_id || changeIdForWorkflowSlug(normalized);
@@ -3322,6 +4111,7 @@ export async function planStage(cwd, slug, options = {}) {
3322
4111
  root,
3323
4112
  sourceText,
3324
4113
  plannerDraft,
4114
+ agentDelegationConfig,
3325
4115
  });
3326
4116
 
3327
4117
  architectReview = await adapter.architect({
@@ -3373,6 +4163,12 @@ export async function planStage(cwd, slug, options = {}) {
3373
4163
  source_requirements_item_count: traceability.itemCount,
3374
4164
  plan_delegation_decision_path: delegationDecision.path,
3375
4165
  plan_delegation_mode: delegationDecision.mode,
4166
+ plan_delegation_recommended_mode: delegationDecision.recommended_mode,
4167
+ plan_delegation_actual_mode: delegationDecision.actual_mode,
4168
+ plan_delegation_runtime_execution: delegationDecision.runtime_execution,
4169
+ plan_delegation_authorization_status: delegationDecision.authorization_status,
4170
+ plan_delegation_authorization_source: delegationDecision.authorization_source,
4171
+ plan_delegation_threshold: delegationDecision.threshold,
3376
4172
  plan_delegation_score: delegationDecision.score,
3377
4173
  plan_delegation_triggers: delegationDecision.triggers,
3378
4174
  plan_delegation_reason: delegationDecision.reason,
@@ -3382,6 +4178,7 @@ export async function planStage(cwd, slug, options = {}) {
3382
4178
  spec_delta_status: changeArtifactStatus.specDeltaStatus,
3383
4179
  slice_artifacts_status: changeArtifactStatus.sliceArtifactsStatus,
3384
4180
  plan_source_spec_path: sourceSpecPath,
4181
+ plan_source_document_paths: sourceBundle.sourceDocumentPaths,
3385
4182
  last_confirmed_transition: consumesReviewPlan || resumesConsumedReviewPlan ? TRANSITIONS.REVIEW_TO_PLAN : TRANSITIONS.CLARIFY_TO_PLAN,
3386
4183
  approval: {
3387
4184
  ...state.approval,