@ai-content-space/loopx 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +50 -0
- package/README.md +59 -446
- package/README.zh-CN.md +59 -457
- package/docs/loopx/design/loopx-skill-suite-v1-design.md +73 -0
- package/docs/loopx/plans/loopx-skill-suite-v1-implementation.md +77 -0
- package/package.json +5 -2
- package/plugins/loopx/.codex-plugin/plugin.json +4 -4
- package/plugins/loopx/scripts/plugin-install.test.mjs +20 -20
- package/plugins/loopx/skills/clarify/SKILL.md +38 -311
- package/plugins/loopx/skills/debug/SKILL.md +1 -1
- package/plugins/loopx/skills/exec/SKILL.md +71 -0
- package/plugins/loopx/skills/finish/SKILL.md +254 -0
- package/plugins/loopx/skills/fix-review/SKILL.md +216 -0
- package/plugins/loopx/skills/go-style/SKILL.md +1 -1
- package/plugins/loopx/skills/kratos/SKILL.md +1 -1
- package/plugins/loopx/skills/plan/SKILL.md +136 -258
- package/plugins/loopx/skills/refactor-plan/SKILL.md +71 -0
- package/plugins/loopx/skills/review/SKILL.md +72 -105
- package/plugins/loopx/skills/review/code-reviewer.md +168 -0
- package/plugins/loopx/skills/spec/DESIGN_SPEC_TEMPLATE.md +323 -0
- package/plugins/loopx/skills/spec/SKILL.md +76 -0
- package/plugins/loopx/skills/subagent-exec/SKILL.md +282 -0
- package/plugins/loopx/skills/subagent-exec/agents/openai.yaml +3 -0
- package/plugins/loopx/skills/subagent-exec/code-quality-reviewer-prompt.md +25 -0
- package/plugins/loopx/skills/subagent-exec/codex-subagents.md +37 -0
- package/plugins/loopx/skills/subagent-exec/implementer-prompt.md +113 -0
- package/plugins/loopx/skills/subagent-exec/spec-reviewer-prompt.md +61 -0
- package/plugins/loopx/skills/tdd/SKILL.md +1 -1
- package/plugins/loopx/skills/verify/SKILL.md +1 -1
- package/scripts/claude-workflow-hook.mjs +109 -0
- package/scripts/codex-workflow-hook.mjs +2 -5
- package/scripts/install-skills.mjs +3 -3
- package/scripts/verify-skills.mjs +32 -1
- package/skills/RESOLVER.md +26 -17
- package/skills/clarify/SKILL.md +38 -311
- package/skills/debug/SKILL.md +1 -1
- package/skills/exec/SKILL.md +71 -0
- package/skills/finish/SKILL.md +254 -0
- package/skills/fix-review/SKILL.md +216 -0
- package/skills/go-style/SKILL.md +1 -1
- package/skills/kratos/SKILL.md +1 -1
- package/skills/plan/SKILL.md +136 -258
- package/skills/refactor-plan/SKILL.md +71 -0
- package/skills/review/SKILL.md +72 -105
- package/skills/review/code-reviewer.md +168 -0
- package/skills/spec/DESIGN_SPEC_TEMPLATE.md +323 -0
- package/skills/spec/SKILL.md +76 -0
- package/skills/subagent-exec/SKILL.md +282 -0
- package/skills/subagent-exec/agents/openai.yaml +3 -0
- package/skills/subagent-exec/code-quality-reviewer-prompt.md +25 -0
- package/skills/subagent-exec/codex-subagents.md +37 -0
- package/skills/subagent-exec/implementer-prompt.md +113 -0
- package/skills/subagent-exec/spec-reviewer-prompt.md +61 -0
- package/skills/tdd/SKILL.md +1 -1
- package/skills/verify/SKILL.md +1 -1
- package/src/autopilot-runtime.mjs +1 -1
- package/src/cli.mjs +79 -5
- package/src/context-manifest.mjs +2 -2
- package/src/html-views.mjs +457 -95
- package/src/install-discovery.mjs +210 -6
- package/src/next-skill.mjs +2 -4
- package/src/plan-runtime.mjs +572 -93
- package/src/runtime-maintenance.mjs +60 -16
- package/src/workflow.mjs +989 -65
- package/templates/architecture.md +58 -16
- package/templates/development-plan.md +42 -12
- package/plugins/loopx/skills/archive/SKILL.md +0 -55
- package/plugins/loopx/skills/autopilot/SKILL.md +0 -93
- package/plugins/loopx/skills/build/SKILL.md +0 -228
- package/skills/archive/SKILL.md +0 -55
- package/skills/autopilot/SKILL.md +0 -93
- 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(/ /g, ' ')
|
|
265
|
+
.replace(/&/g, '&')
|
|
266
|
+
.replace(/</g, '<')
|
|
267
|
+
.replace(/>/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)) {
|
|
@@ -416,6 +608,17 @@ function createInitialState(slug, profile) {
|
|
|
416
608
|
plan_execution_inputs_resolved: false,
|
|
417
609
|
plan_docs_status: 'missing',
|
|
418
610
|
plan_docs_artifact_paths: null,
|
|
611
|
+
plan_delegation_decision_path: null,
|
|
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,
|
|
619
|
+
plan_delegation_score: 0,
|
|
620
|
+
plan_delegation_triggers: [],
|
|
621
|
+
plan_delegation_reason: null,
|
|
419
622
|
plan_review_artifact_paths: [],
|
|
420
623
|
plan_review_history: [],
|
|
421
624
|
plan_blockers: [],
|
|
@@ -525,7 +728,7 @@ async function writeJson(path, value) {
|
|
|
525
728
|
async function writeCanonicalPlanArtifacts(cwd, root, slug) {
|
|
526
729
|
const plansRoot = resolvePlansRoot(cwd);
|
|
527
730
|
await ensureDir(plansRoot);
|
|
528
|
-
const planPath = join(plansRoot, `
|
|
731
|
+
const planPath = join(plansRoot, `requirements-snapshot-${slug}.md`);
|
|
529
732
|
const testSpecPath = join(plansRoot, `test-spec-${slug}.md`);
|
|
530
733
|
const planText = await readFile(artifactPath(root, 'plan.md'), 'utf8');
|
|
531
734
|
const architectureText = await readFile(artifactPath(root, 'architecture.md'), 'utf8');
|
|
@@ -535,7 +738,9 @@ async function writeCanonicalPlanArtifacts(cwd, root, slug) {
|
|
|
535
738
|
await writeText(
|
|
536
739
|
planPath,
|
|
537
740
|
[
|
|
538
|
-
`# loopx
|
|
741
|
+
`# loopx Requirements Snapshot: ${slug}`,
|
|
742
|
+
'',
|
|
743
|
+
'本文件是用户原始需求和已批准计划包的执行快照,不是由 loopx 生成的 PRD。原始需求来源仍以 `spec.md` / `plan_source_spec_path` 指向的用户材料为准。',
|
|
539
744
|
'',
|
|
540
745
|
'## Plan',
|
|
541
746
|
'',
|
|
@@ -582,10 +787,11 @@ function bulletsFromSectionText(text, heading) {
|
|
|
582
787
|
.filter(Boolean);
|
|
583
788
|
}
|
|
584
789
|
|
|
585
|
-
function
|
|
790
|
+
function sectionBodiesForHeadings(text, headingPatterns) {
|
|
586
791
|
const body = stripFrontmatter(text);
|
|
587
792
|
const headingPattern = /^#{2,4}\s+(.+?)\s*$/gm;
|
|
588
793
|
const headings = [...body.matchAll(headingPattern)];
|
|
794
|
+
const bodies = [];
|
|
589
795
|
for (let index = 0; index < headings.length; index += 1) {
|
|
590
796
|
const title = headings[index][1].trim();
|
|
591
797
|
if (!headingPatterns.some((pattern) => pattern.test(title))) {
|
|
@@ -593,25 +799,33 @@ function sectionBodyForHeadings(text, headingPatterns) {
|
|
|
593
799
|
}
|
|
594
800
|
const start = headings[index].index + headings[index][0].length;
|
|
595
801
|
const end = index + 1 < headings.length ? headings[index + 1].index : body.length;
|
|
596
|
-
|
|
802
|
+
bodies.push(body.slice(start, end).trim());
|
|
597
803
|
}
|
|
598
|
-
return
|
|
804
|
+
return bodies;
|
|
599
805
|
}
|
|
600
806
|
|
|
601
807
|
function explicitCoverageItems(sourceText) {
|
|
602
|
-
const
|
|
808
|
+
const bodies = sectionBodiesForHeadings(sourceText, [
|
|
809
|
+
/^in\s+scope$/i,
|
|
810
|
+
/^testable\s+acceptance\s+criteria$/i,
|
|
811
|
+
/^functional\s+requirements?$/i,
|
|
603
812
|
/required\s+coverage/i,
|
|
604
813
|
/requirement\s+coverage/i,
|
|
814
|
+
/requirements?/i,
|
|
605
815
|
/coverage\s+matrix/i,
|
|
816
|
+
/功能需求/,
|
|
817
|
+
/交付范围/,
|
|
818
|
+
/验收/,
|
|
819
|
+
/成功标准/,
|
|
606
820
|
/需求.*覆盖/,
|
|
607
821
|
/需求.*完整/,
|
|
608
822
|
/需求.*卡点/,
|
|
609
823
|
]);
|
|
610
|
-
if (
|
|
824
|
+
if (bodies.length === 0) {
|
|
611
825
|
return [];
|
|
612
826
|
}
|
|
613
|
-
return
|
|
614
|
-
.split('\n')
|
|
827
|
+
return bodies
|
|
828
|
+
.flatMap((body) => body.split('\n'))
|
|
615
829
|
.map((line) => line.trim())
|
|
616
830
|
.filter((line) => /^[-*]\s+/.test(line) || /^\d+[.)]\s+/.test(line))
|
|
617
831
|
.map((line) => line.replace(/^[-*]\s+/, '').replace(/^\d+[.)]\s+/, '').trim())
|
|
@@ -655,14 +869,475 @@ function requirementHeadingCoverageItems(sourceText) {
|
|
|
655
869
|
.filter(Boolean);
|
|
656
870
|
}
|
|
657
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
|
+
|
|
658
879
|
function sourceRequirementItems(sourceText) {
|
|
659
880
|
return dedupeStrings([
|
|
660
881
|
...explicitCoverageItems(sourceText),
|
|
882
|
+
...relevantHeadingCoverageItems(sourceText),
|
|
661
883
|
...markdownTableCoverageItems(sourceText),
|
|
662
884
|
...requirementHeadingCoverageItems(sourceText),
|
|
663
885
|
]).slice(0, 80);
|
|
664
886
|
}
|
|
665
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
|
+
|
|
666
1341
|
function normalizedCoverageText(...parts) {
|
|
667
1342
|
return parts.join('\n')
|
|
668
1343
|
.toLowerCase()
|
|
@@ -690,7 +1365,8 @@ function sourceRequirementCovered(item, haystack) {
|
|
|
690
1365
|
if (tokens.length === 0) {
|
|
691
1366
|
return false;
|
|
692
1367
|
}
|
|
693
|
-
|
|
1368
|
+
const matched = tokens.filter((token) => haystack.includes(token)).length;
|
|
1369
|
+
return matched / tokens.length >= 0.65;
|
|
694
1370
|
}
|
|
695
1371
|
|
|
696
1372
|
async function writeRequirementTraceabilityArtifact({ root, sourceSpecPath, sourceText, plannerDraft, changeArtifactPaths }) {
|
|
@@ -723,22 +1399,34 @@ async function writeRequirementTraceabilityArtifact({ root, sourceSpecPath, sour
|
|
|
723
1399
|
'# 原始需求覆盖矩阵',
|
|
724
1400
|
'',
|
|
725
1401
|
`- 来源:${sourceSpecPath}`,
|
|
726
|
-
`-
|
|
1402
|
+
`- 覆盖状态:${status === 'complete' ? '完整' : '部分缺失'} (${status})`,
|
|
727
1403
|
`- 提取项数量:${rows.length}`,
|
|
728
1404
|
'',
|
|
1405
|
+
'## 审阅说明',
|
|
1406
|
+
'',
|
|
1407
|
+
'- 本文件用于人工确认源需求是否被计划、架构、开发切片、规格增量和测试计划承接。',
|
|
1408
|
+
'- “原始需求项”保留源文档原文;如果源文档是英文,表格会保留英文原句,但覆盖状态和审阅说明必须使用中文。',
|
|
1409
|
+
'- 未覆盖项会阻断 `plan -> build`,直到 Planner 重新展开计划或明确把该项列为非目标并说明理由。',
|
|
1410
|
+
'',
|
|
729
1411
|
'## 覆盖矩阵',
|
|
730
1412
|
'',
|
|
731
|
-
'| 原始需求项 | 覆盖状态 |',
|
|
732
|
-
'| --- | --- |',
|
|
1413
|
+
'| 原始需求项 | 覆盖状态 | 审阅说明 |',
|
|
1414
|
+
'| --- | --- | --- |',
|
|
733
1415
|
...(rows.length > 0
|
|
734
|
-
? rows.map((row) => `| ${row.item.replace(/\|/g, '\\|')} | ${row.status} |`)
|
|
735
|
-
: ['| 未检测到显式需求覆盖项 |
|
|
1416
|
+
? rows.map((row) => `| ${row.item.replace(/\|/g, '\\|')} | ${row.status === 'covered' ? '已覆盖' : '未覆盖'} | ${row.status === 'covered' ? '已在计划包或变更工件中找到对应表述。' : '计划包没有找到可追溯表述,需要回到 plan 修订。'} |`)
|
|
1417
|
+
: ['| 未检测到显式需求覆盖项 | 已覆盖 | 没有从源文档中提取到独立覆盖项。 |']),
|
|
736
1418
|
'',
|
|
737
1419
|
'## 门禁',
|
|
738
1420
|
'',
|
|
739
1421
|
...(blockers.length > 0
|
|
740
|
-
?
|
|
741
|
-
|
|
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 检查。']),
|
|
742
1430
|
].join('\n'));
|
|
743
1431
|
|
|
744
1432
|
return {
|
|
@@ -749,6 +1437,150 @@ async function writeRequirementTraceabilityArtifact({ root, sourceSpecPath, sour
|
|
|
749
1437
|
};
|
|
750
1438
|
}
|
|
751
1439
|
|
|
1440
|
+
function delegationDecisionForPlan(sourceText, plannerDraft) {
|
|
1441
|
+
const source = String(sourceText || '');
|
|
1442
|
+
const draft = [
|
|
1443
|
+
plannerDraft.planText,
|
|
1444
|
+
plannerDraft.architectureText,
|
|
1445
|
+
plannerDraft.developmentPlanText,
|
|
1446
|
+
plannerDraft.testPlanText,
|
|
1447
|
+
].join('\n');
|
|
1448
|
+
const combined = `${source}\n${draft}`;
|
|
1449
|
+
const requirementCount = sourceRequirementItems(source).length;
|
|
1450
|
+
const lineCount = source.split('\n').filter((line) => line.trim()).length;
|
|
1451
|
+
const triggers = [];
|
|
1452
|
+
let score = 0;
|
|
1453
|
+
|
|
1454
|
+
const addTrigger = (trigger, weight) => {
|
|
1455
|
+
if (!triggers.includes(trigger)) {
|
|
1456
|
+
triggers.push(trigger);
|
|
1457
|
+
score += weight;
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
|
|
1461
|
+
if (requirementCount >= 12 || lineCount >= 180) {
|
|
1462
|
+
addTrigger('large_requirement_surface', 3);
|
|
1463
|
+
} else if (requirementCount >= 6 || lineCount >= 90) {
|
|
1464
|
+
addTrigger('medium_requirement_surface', 2);
|
|
1465
|
+
}
|
|
1466
|
+
if (/资金|资产|清算|结算|交易|订单|风控|权限|安全|合规|审计|corporate action|settlement|trading|order|asset|security|auth|permission|compliance|audit|financial/i.test(combined)) {
|
|
1467
|
+
addTrigger('high_risk_domain', 3);
|
|
1468
|
+
}
|
|
1469
|
+
if (/api|接口|service|biz|data|database|schema|migration|数据库|迁移|worker|cron|frontend|后台|部署|deploy/i.test(combined)) {
|
|
1470
|
+
addTrigger('cross_module_scope', 2);
|
|
1471
|
+
}
|
|
1472
|
+
if (/状态机|幂等|补偿|差异|回滚|并发|重试|eventual|idempot|retry|rollback|concurrency|state machine/i.test(combined)) {
|
|
1473
|
+
addTrigger('state_or_integrity_complexity', 2);
|
|
1474
|
+
}
|
|
1475
|
+
if (/e2e|集成测试|integration|regression|回归|验收|acceptance|fixture|mock|真实数据|external/i.test(combined)) {
|
|
1476
|
+
addTrigger('verification_complexity', 1);
|
|
1477
|
+
}
|
|
1478
|
+
if (/多个方案|备选|取舍|tradeoff|alternative|ADR|architecture/i.test(combined)) {
|
|
1479
|
+
addTrigger('architectural_tradeoff', 1);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
const recommendedMode = score >= 7 ? 'parallel-review' : (score >= 4 ? 'critic-only' : 'local');
|
|
1483
|
+
const reason = recommendedMode === 'parallel-review'
|
|
1484
|
+
? '高风险或跨模块规划,建议独立 Planner/Architect/Critic 视角并行审查。'
|
|
1485
|
+
: recommendedMode === 'critic-only'
|
|
1486
|
+
? '存在中等复杂度或验证风险,建议至少引入独立 critic 复核需求覆盖和风险。'
|
|
1487
|
+
: '范围较小或风险较低,本地顺序 Planner/Architect/Critic 审阅足够。';
|
|
1488
|
+
|
|
1489
|
+
return {
|
|
1490
|
+
mode: recommendedMode,
|
|
1491
|
+
recommended_mode: recommendedMode,
|
|
1492
|
+
score,
|
|
1493
|
+
triggers,
|
|
1494
|
+
reason,
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
|
|
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 }) {
|
|
1546
|
+
const decision = delegationDecisionForPlan(sourceText, plannerDraft);
|
|
1547
|
+
const execution = resolvePlanDelegationExecution(decision.recommended_mode, agentDelegationConfig);
|
|
1548
|
+
const path = artifactPath(root, 'plan-delegation-decision.md');
|
|
1549
|
+
await writeText(path, [
|
|
1550
|
+
'# Plan Delegation Decision',
|
|
1551
|
+
'',
|
|
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}`,
|
|
1558
|
+
`- score: ${decision.score}`,
|
|
1559
|
+
`- reason: ${decision.reason}`,
|
|
1560
|
+
'',
|
|
1561
|
+
'## Triggers',
|
|
1562
|
+
'',
|
|
1563
|
+
...(decision.triggers.length > 0 ? decision.triggers.map((item) => `- ${item}`) : ['- none']),
|
|
1564
|
+
'',
|
|
1565
|
+
'## Guidance',
|
|
1566
|
+
'',
|
|
1567
|
+
'- local: 低风险、小范围、单模块任务,本地顺序 Planner/Architect/Critic 即可。',
|
|
1568
|
+
'- critic-only: 中等风险或覆盖面较宽,至少需要独立 critic 复核需求覆盖、验证和遗漏风险。',
|
|
1569
|
+
'- parallel-review: 高风险、多模块、状态/资产/安全相关任务,建议独立 Planner/Architect/Critic 视角并行审查。',
|
|
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
|
+
'',
|
|
1577
|
+
'## Runtime Note',
|
|
1578
|
+
'',
|
|
1579
|
+
`- ${execution.note}`,
|
|
1580
|
+
].join('\n'));
|
|
1581
|
+
return { path, ...decision, ...execution };
|
|
1582
|
+
}
|
|
1583
|
+
|
|
752
1584
|
function frontmatterList(text, key) {
|
|
753
1585
|
if (!text.startsWith('---\n')) {
|
|
754
1586
|
return [];
|
|
@@ -1000,19 +1832,18 @@ function validateRequirementDelta(text) {
|
|
|
1000
1832
|
return { delta, blockers: dedupeStrings(blockers) };
|
|
1001
1833
|
}
|
|
1002
1834
|
|
|
1003
|
-
function requirementsForDelta(slug, plannerDraft) {
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
.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);
|
|
1009
1840
|
return dedupeStrings(requirements.length > 0 ? requirements : [
|
|
1010
1841
|
`Workflow ${slug} SHALL implement the approved loopx plan package.`,
|
|
1011
1842
|
]);
|
|
1012
1843
|
}
|
|
1013
1844
|
|
|
1014
|
-
function verticalSlicesForChange(slug, plannerDraft) {
|
|
1015
|
-
const requirements = requirementsForDelta(slug, plannerDraft);
|
|
1845
|
+
function verticalSlicesForChange(slug, plannerDraft, sourceText = '') {
|
|
1846
|
+
const requirements = requirementsForDelta(slug, plannerDraft, sourceText);
|
|
1016
1847
|
const slices = requirements.slice(0, 8).map((requirement, index) => ({
|
|
1017
1848
|
id: `VS-${index + 1}`,
|
|
1018
1849
|
title: requirement.length > 90 ? `${requirement.slice(0, 87)}...` : requirement,
|
|
@@ -1097,8 +1928,8 @@ async function writeChangeArtifacts(cwd, root, slug, sourceText, plannerDraft, c
|
|
|
1097
1928
|
graph: join(changeRoot, 'artifact-graph.json'),
|
|
1098
1929
|
};
|
|
1099
1930
|
const domains = targetDomainsForChange(slug, sourceText);
|
|
1100
|
-
const requirements = requirementsForDelta(slug, plannerDraft);
|
|
1101
|
-
const slices = verticalSlicesForChange(slug, plannerDraft);
|
|
1931
|
+
const requirements = requirementsForDelta(slug, plannerDraft, sourceText);
|
|
1932
|
+
const slices = verticalSlicesForChange(slug, plannerDraft, sourceText);
|
|
1102
1933
|
|
|
1103
1934
|
await writeText(paths.proposal, [
|
|
1104
1935
|
`# loopx Change Proposal: ${normalizedChangeId}`,
|
|
@@ -1138,17 +1969,12 @@ async function writeChangeArtifacts(cwd, root, slug, sourceText, plannerDraft, c
|
|
|
1138
1969
|
...requirements.flatMap((item, index) => [requirementBlockFromText({ slug, text: item, index }), '']),
|
|
1139
1970
|
].join('\n'));
|
|
1140
1971
|
|
|
1141
|
-
await writeText(paths.design,
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
'',
|
|
1148
|
-
'## Task Plan',
|
|
1149
|
-
'',
|
|
1150
|
-
plannerDraft.developmentPlanText || '- See workflow development plan artifact.',
|
|
1151
|
-
].join('\n'));
|
|
1972
|
+
await writeText(paths.design, detailedDesignTextForChange({
|
|
1973
|
+
changeId: normalizedChangeId,
|
|
1974
|
+
slug,
|
|
1975
|
+
items: requirements,
|
|
1976
|
+
plannerDraft,
|
|
1977
|
+
}));
|
|
1152
1978
|
|
|
1153
1979
|
await writeText(paths.tasks, [
|
|
1154
1980
|
`# loopx Change Tasks: ${normalizedChangeId}`,
|
|
@@ -1522,6 +2348,69 @@ function containsChineseText(text) {
|
|
|
1522
2348
|
return chineseChars.length >= 40 || (chineseChars.length >= 8 && chineseChars.length / signalChars >= 0.2);
|
|
1523
2349
|
}
|
|
1524
2350
|
|
|
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 } = {}) {
|
|
2389
|
+
const blockers = [];
|
|
2390
|
+
for (const [key, path] of Object.entries(pathsByKey)) {
|
|
2391
|
+
if (!existsSync(path)) {
|
|
2392
|
+
blockers.push(`missing_plan_artifact_${key}`);
|
|
2393
|
+
continue;
|
|
2394
|
+
}
|
|
2395
|
+
const text = await readFile(path, 'utf8');
|
|
2396
|
+
if (!containsChineseText(text)) {
|
|
2397
|
+
blockers.push(`plan_artifact_not_chinese_${key}`);
|
|
2398
|
+
}
|
|
2399
|
+
blockers.push(...planReviewabilityBlockers(key, text, sourceItemCount));
|
|
2400
|
+
}
|
|
2401
|
+
return blockers;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
function planReviewArtifactBlockers(state) {
|
|
2405
|
+
if (!Array.isArray(state.plan_review_artifact_paths) || state.plan_review_artifact_paths.length === 0) {
|
|
2406
|
+
return ['missing_plan_review_artifacts'];
|
|
2407
|
+
}
|
|
2408
|
+
const latest = state.plan_review_artifact_paths[state.plan_review_artifact_paths.length - 1] || {};
|
|
2409
|
+
return ['planner', 'architect', 'critic']
|
|
2410
|
+
.filter((key) => !latest[key] || !existsSync(latest[key]))
|
|
2411
|
+
.map((key) => `missing_plan_review_artifact_${key}`);
|
|
2412
|
+
}
|
|
2413
|
+
|
|
1525
2414
|
async function ensurePlanWorkflowFromDirectSpec(cwd, directSpecPath, explicitSlug, options = {}) {
|
|
1526
2415
|
const resolvedSpecPath = resolve(cwd, directSpecPath);
|
|
1527
2416
|
const specText = await readFile(resolvedSpecPath, 'utf8');
|
|
@@ -1534,12 +2423,23 @@ async function ensurePlanWorkflowFromDirectSpec(cwd, directSpecPath, explicitSlu
|
|
|
1534
2423
|
const existing = await readState(cwd, slug);
|
|
1535
2424
|
if (existing) {
|
|
1536
2425
|
const merged = withRecommendedAction({
|
|
2426
|
+
...createInitialState(slug, existing.clarify_profile || existing.profile || 'standard'),
|
|
1537
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',
|
|
1538
2432
|
spec_artifact_path: resolvedSpecPath,
|
|
1539
2433
|
plan_source_spec_path: resolvedSpecPath,
|
|
2434
|
+
requested_transition: existing.requested_transition || TRANSITIONS.CLARIFY_TO_PLAN,
|
|
1540
2435
|
plan_consensus_mode: true,
|
|
1541
2436
|
plan_deliberate_mode: Boolean(options.deliberate),
|
|
1542
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
|
+
},
|
|
1543
2443
|
});
|
|
1544
2444
|
await writeState(root, merged);
|
|
1545
2445
|
return { slug, root, state: merged };
|
|
@@ -1670,7 +2570,7 @@ async function readPlanCompletion(cwd, root, slug, state) {
|
|
|
1670
2570
|
blockers.push('execution_inputs_unresolved');
|
|
1671
2571
|
}
|
|
1672
2572
|
if (!state.plan_artifact_path || !existsSync(state.plan_artifact_path)) {
|
|
1673
|
-
blockers.push('
|
|
2573
|
+
blockers.push('missing_requirements_snapshot');
|
|
1674
2574
|
}
|
|
1675
2575
|
if (!state.test_spec_artifact_path || !existsSync(state.test_spec_artifact_path)) {
|
|
1676
2576
|
blockers.push('missing_test_spec');
|
|
@@ -1681,6 +2581,10 @@ async function readPlanCompletion(cwd, root, slug, state) {
|
|
|
1681
2581
|
if (!state.requirement_traceability_path || !existsSync(state.requirement_traceability_path)) {
|
|
1682
2582
|
blockers.push('missing_requirement_traceability');
|
|
1683
2583
|
}
|
|
2584
|
+
if (!state.plan_delegation_decision_path || !existsSync(state.plan_delegation_decision_path)) {
|
|
2585
|
+
blockers.push('missing_plan_delegation_decision');
|
|
2586
|
+
}
|
|
2587
|
+
blockers.push(...planReviewArtifactBlockers(state));
|
|
1684
2588
|
if (state.source_requirements_status && state.source_requirements_status !== 'complete') {
|
|
1685
2589
|
if (state.requirement_traceability_path && existsSync(state.requirement_traceability_path)) {
|
|
1686
2590
|
const traceabilityText = await readFile(state.requirement_traceability_path, 'utf8');
|
|
@@ -1695,22 +2599,21 @@ async function readPlanCompletion(cwd, root, slug, state) {
|
|
|
1695
2599
|
blockers.push(`source_requirements_${state.source_requirements_status}`);
|
|
1696
2600
|
}
|
|
1697
2601
|
}
|
|
1698
|
-
|
|
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
|
+
}
|
|
2606
|
+
blockers.push(...await planLanguageBlockers({
|
|
1699
2607
|
plan: artifactPath(root, 'plan.md'),
|
|
1700
2608
|
architecture: artifactPath(root, 'architecture.md'),
|
|
1701
2609
|
developmentPlan: artifactPath(root, 'development-plan.md'),
|
|
1702
2610
|
testPlan: artifactPath(root, 'test-plan.md'),
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
const text = await readFile(path, 'utf8');
|
|
1710
|
-
if (!containsChineseText(text)) {
|
|
1711
|
-
blockers.push(`plan_artifact_not_chinese_${key}`);
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
2611
|
+
requirementsSnapshot: state.plan_artifact_path || join(resolvePlansRoot(cwd), `requirements-snapshot-${slug}.md`),
|
|
2612
|
+
testSpec: state.test_spec_artifact_path || join(resolvePlansRoot(cwd), `test-spec-${slug}.md`),
|
|
2613
|
+
traceability: state.requirement_traceability_path || artifactPath(root, 'requirement-traceability.md'),
|
|
2614
|
+
delegationDecision: state.plan_delegation_decision_path || artifactPath(root, 'plan-delegation-decision.md'),
|
|
2615
|
+
design: state.change_artifact_paths?.design || join(resolveChangeRoot(cwd, state.change_id || changeIdForWorkflowSlug(slug)), 'design.md'),
|
|
2616
|
+
}, { sourceItemCount }));
|
|
1714
2617
|
const changeStatus = await readChangeArtifactStatus(state.change_artifact_paths);
|
|
1715
2618
|
blockers.push(...changeStatus.blockers);
|
|
1716
2619
|
|
|
@@ -1834,10 +2737,10 @@ async function buildCompletionAudit({ cwd, root, slug, state, reviewReworkArtifa
|
|
|
1834
2737
|
};
|
|
1835
2738
|
|
|
1836
2739
|
addChecklistItem({
|
|
1837
|
-
id: '
|
|
2740
|
+
id: 'requirements-snapshot',
|
|
1838
2741
|
source: 'approved-plan',
|
|
1839
|
-
requirement: state.plan_artifact_path || join(cwd, '.loopx', 'plans', `
|
|
1840
|
-
evidence: [state.plan_artifact_path || '
|
|
2742
|
+
requirement: state.plan_artifact_path || join(cwd, '.loopx', 'plans', `requirements-snapshot-${slug}.md`),
|
|
2743
|
+
evidence: [state.plan_artifact_path || 'requirements snapshot artifact'],
|
|
1841
2744
|
});
|
|
1842
2745
|
addChecklistItem({
|
|
1843
2746
|
id: 'test-spec',
|
|
@@ -2060,9 +2963,6 @@ function clarifyReadinessBlockers(state) {
|
|
|
2060
2963
|
if (state.clarify_current_round > state.clarify_max_rounds) {
|
|
2061
2964
|
blockers.push('clarify_max_rounds_exceeded');
|
|
2062
2965
|
}
|
|
2063
|
-
if (state.clarify_ambiguity_score > state.clarify_target_ambiguity_threshold) {
|
|
2064
|
-
blockers.push('clarify_ambiguity_score_above_threshold');
|
|
2065
|
-
}
|
|
2066
2966
|
if (!state.clarify_non_goals_resolved) {
|
|
2067
2967
|
blockers.push('clarify_non_goals_unresolved');
|
|
2068
2968
|
}
|
|
@@ -2096,7 +2996,7 @@ function planReadinessBlockersSync(state) {
|
|
|
2096
2996
|
blockers.push('execution_inputs_unresolved');
|
|
2097
2997
|
}
|
|
2098
2998
|
if (!state.plan_artifact_path) {
|
|
2099
|
-
blockers.push('
|
|
2999
|
+
blockers.push('missing_requirements_snapshot');
|
|
2100
3000
|
}
|
|
2101
3001
|
if (!state.test_spec_artifact_path) {
|
|
2102
3002
|
blockers.push('missing_test_spec');
|
|
@@ -2205,7 +3105,7 @@ function buildCurrentEvidenceChain(state, readiness = buildReadiness(state), aut
|
|
|
2205
3105
|
if (readiness.plan.ready) {
|
|
2206
3106
|
evidence.push(evidenceEntry(
|
|
2207
3107
|
'clarify_ready_for_plan',
|
|
2208
|
-
'Clarify ambiguity
|
|
3108
|
+
'Clarify has zero unresolved ambiguity and non-goals, decision boundaries, and pressure pass gates are satisfied.',
|
|
2209
3109
|
authorization.plan.authorized ? 'The approved clarify -> plan transition can be consumed by plan.' : 'Plan readiness exists, but user authorization is still separate.',
|
|
2210
3110
|
));
|
|
2211
3111
|
}
|
|
@@ -2730,7 +3630,7 @@ async function renderPlanReadingViews(cwd, root, state, slug) {
|
|
|
2730
3630
|
}
|
|
2731
3631
|
}
|
|
2732
3632
|
|
|
2733
|
-
export async function initWorkspace(cwd, { slug } = {}) {
|
|
3633
|
+
export async function initWorkspace(cwd, { slug, agentDelegation = {} } = {}) {
|
|
2734
3634
|
const workspaceRoot = resolveWorkspaceRoot(cwd);
|
|
2735
3635
|
const projectConventions = await inspectProjectConventions(cwd);
|
|
2736
3636
|
await ensureLoopxRoot(cwd);
|
|
@@ -2757,6 +3657,7 @@ export async function initWorkspace(cwd, { slug } = {}) {
|
|
|
2757
3657
|
existing_spec_sources: projectConventions.existing_spec_sources,
|
|
2758
3658
|
},
|
|
2759
3659
|
verification_commands: projectConventions.verification_commands,
|
|
3660
|
+
agent_delegation: normalizeAgentDelegationConfig(agentDelegation),
|
|
2760
3661
|
};
|
|
2761
3662
|
|
|
2762
3663
|
if (!existsSync(workspaceConfigPath(workspaceRoot))) {
|
|
@@ -2835,7 +3736,9 @@ export async function clarifyStage(cwd, slug, { profile = 'standard' } = {}) {
|
|
|
2835
3736
|
},
|
|
2836
3737
|
});
|
|
2837
3738
|
await writeState(root, state);
|
|
2838
|
-
|
|
3739
|
+
const rendered = await renderPlanReadingViews(cwd, root, state, normalized);
|
|
3740
|
+
await writeState(root, rendered);
|
|
3741
|
+
return { root, state: rendered };
|
|
2839
3742
|
}
|
|
2840
3743
|
|
|
2841
3744
|
export async function approveStage(cwd, slug, { from, to }) {
|
|
@@ -3169,7 +4072,9 @@ export async function planStage(cwd, slug, options = {}) {
|
|
|
3169
4072
|
}
|
|
3170
4073
|
|
|
3171
4074
|
const sourceSpecPath = options.directSpecPath ? resolve(cwd, options.directSpecPath) : (state.plan_source_spec_path || artifactPath(root, 'spec.md'));
|
|
3172
|
-
const
|
|
4075
|
+
const sourceBundle = await readPlanSourceText(cwd, state, sourceSpecPath);
|
|
4076
|
+
const sourceText = sourceBundle.sourceText;
|
|
4077
|
+
const agentDelegationConfig = await readAgentDelegationConfig(cwd);
|
|
3173
4078
|
const adapter = options.adapter || createDefaultPlanAdapter();
|
|
3174
4079
|
const maxIterations = DEFAULT_MAX_ITERATIONS;
|
|
3175
4080
|
let iteration = 1;
|
|
@@ -3179,7 +4084,7 @@ export async function planStage(cwd, slug, options = {}) {
|
|
|
3179
4084
|
const reviewHistory = initialPlanReviewHistory(state);
|
|
3180
4085
|
|
|
3181
4086
|
while (iteration <= maxIterations) {
|
|
3182
|
-
const
|
|
4087
|
+
const rawPlannerDraft = await adapter.planner({
|
|
3183
4088
|
cwd,
|
|
3184
4089
|
root,
|
|
3185
4090
|
slug: normalized,
|
|
@@ -3189,6 +4094,7 @@ export async function planStage(cwd, slug, options = {}) {
|
|
|
3189
4094
|
deliberateMode: Boolean(options.deliberate),
|
|
3190
4095
|
interactiveMode: Boolean(options.interactive),
|
|
3191
4096
|
});
|
|
4097
|
+
const plannerDraft = enrichPlannerDraftForReview({ sourceText, plannerDraft: rawPlannerDraft });
|
|
3192
4098
|
await writePlanArtifacts(root, cwd, normalized, plannerDraft);
|
|
3193
4099
|
const artifactPaths = await writeCanonicalPlanArtifacts(cwd, root, normalized);
|
|
3194
4100
|
const changeId = state.change_id || changeIdForWorkflowSlug(normalized);
|
|
@@ -3201,6 +4107,12 @@ export async function planStage(cwd, slug, options = {}) {
|
|
|
3201
4107
|
plannerDraft,
|
|
3202
4108
|
changeArtifactPaths,
|
|
3203
4109
|
});
|
|
4110
|
+
const delegationDecision = await writePlanDelegationDecisionArtifact({
|
|
4111
|
+
root,
|
|
4112
|
+
sourceText,
|
|
4113
|
+
plannerDraft,
|
|
4114
|
+
agentDelegationConfig,
|
|
4115
|
+
});
|
|
3204
4116
|
|
|
3205
4117
|
architectReview = await adapter.architect({
|
|
3206
4118
|
cwd,
|
|
@@ -3249,12 +4161,24 @@ export async function planStage(cwd, slug, options = {}) {
|
|
|
3249
4161
|
requirement_traceability_path: traceability.path,
|
|
3250
4162
|
source_requirements_status: traceability.status,
|
|
3251
4163
|
source_requirements_item_count: traceability.itemCount,
|
|
4164
|
+
plan_delegation_decision_path: delegationDecision.path,
|
|
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,
|
|
4172
|
+
plan_delegation_score: delegationDecision.score,
|
|
4173
|
+
plan_delegation_triggers: delegationDecision.triggers,
|
|
4174
|
+
plan_delegation_reason: delegationDecision.reason,
|
|
3252
4175
|
change_id: normalizeSlug(changeId),
|
|
3253
4176
|
change_artifacts_status: changeArtifactStatus.status,
|
|
3254
4177
|
change_artifact_paths: changeArtifactPaths,
|
|
3255
4178
|
spec_delta_status: changeArtifactStatus.specDeltaStatus,
|
|
3256
4179
|
slice_artifacts_status: changeArtifactStatus.sliceArtifactsStatus,
|
|
3257
4180
|
plan_source_spec_path: sourceSpecPath,
|
|
4181
|
+
plan_source_document_paths: sourceBundle.sourceDocumentPaths,
|
|
3258
4182
|
last_confirmed_transition: consumesReviewPlan || resumesConsumedReviewPlan ? TRANSITIONS.REVIEW_TO_PLAN : TRANSITIONS.CLARIFY_TO_PLAN,
|
|
3259
4183
|
approval: {
|
|
3260
4184
|
...state.approval,
|