@ai-content-space/loopx 0.1.9 → 0.1.10

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.
@@ -39,6 +39,8 @@ const CHANGE_ARTIFACT_FILE_MAP = {
39
39
  graph: 'artifact-graph.json',
40
40
  };
41
41
 
42
+ const PLAN_ARTIFACTS = ['plan.md', 'architecture.md', 'development-plan.md', 'test-plan.md'];
43
+
42
44
  function normalizeSlug(raw) {
43
45
  return String(raw || '')
44
46
  .trim()
@@ -47,6 +49,44 @@ function normalizeSlug(raw) {
47
49
  .replace(/^-+|-+$/g, '');
48
50
  }
49
51
 
52
+ function containsChineseText(text) {
53
+ const chineseChars = text.match(/[\u3400-\u9fff]/g) || [];
54
+ const latinChars = text.match(/[A-Za-z]/g) || [];
55
+ const signalChars = chineseChars.length + latinChars.length;
56
+ if (signalChars === 0) {
57
+ return false;
58
+ }
59
+ return chineseChars.length >= 40 || (chineseChars.length >= 8 && chineseChars.length / signalChars >= 0.2);
60
+ }
61
+
62
+ async function legacyPlanArtifactBlockers(workflowRoot) {
63
+ const blockers = [];
64
+ for (const name of PLAN_ARTIFACTS) {
65
+ const path = join(workflowRoot, name);
66
+ const key = name
67
+ .replace(/\.md$/, '')
68
+ .replace(/-([a-z])/g, (_, char) => char.toUpperCase());
69
+ if (!existsSync(path)) {
70
+ blockers.push(`missing_plan_artifact_${key}`);
71
+ continue;
72
+ }
73
+ const text = await readFile(path, 'utf8');
74
+ if (!containsChineseText(text)) {
75
+ blockers.push(`plan_artifact_not_chinese_${key}`);
76
+ }
77
+ }
78
+ if (!existsSync(join(workflowRoot, 'requirement-traceability.md'))) {
79
+ blockers.push('missing_requirement_traceability');
80
+ }
81
+ if (!existsSync(join(workflowRoot, 'plan-delegation-decision.md'))) {
82
+ blockers.push('missing_plan_delegation_decision');
83
+ }
84
+ if (!existsSync(join(workflowRoot, 'plan-reviews'))) {
85
+ blockers.push('missing_plan_review_artifacts');
86
+ }
87
+ return blockers;
88
+ }
89
+
50
90
  export function resolveLoopxRoot(cwd) {
51
91
  return join(resolve(cwd), '.loopx');
52
92
  }
@@ -332,24 +372,25 @@ async function migrateLegacyWorkflowState(cwd, slug, workflowRoot, legacyState)
332
372
  const canonicalPlanPath = join(resolveLoopxRoot(cwd), 'plans', `prd-${slug}.md`);
333
373
  const canonicalTestSpecPath = join(resolveLoopxRoot(cwd), 'plans', `test-spec-${slug}.md`);
334
374
  const baseState = createMigratedWorkflowBaseState(slug, legacyState, change);
335
- const planDocsComplete = ['plan.md', 'architecture.md', 'development-plan.md', 'test-plan.md']
336
- .every((name) => existsSync(join(workflowRoot, name)));
375
+ const planBlockers = await legacyPlanArtifactBlockers(workflowRoot);
376
+ const planDocsComplete = planBlockers.length === 0;
337
377
  const executionRecordStatus = await inferExecutionStatus(workflowRoot);
338
- const planState = planDocsComplete ? {
378
+ const planState = PLAN_ARTIFACTS.some((name) => existsSync(join(workflowRoot, name))) ? {
339
379
  current_stage: STAGES.PLAN,
340
- stage_status: 'awaiting-approval',
341
- plan_package_status: 'complete',
380
+ stage_status: planDocsComplete ? 'awaiting-approval' : 'blocked',
381
+ plan_package_status: planDocsComplete ? 'complete' : 'partial',
342
382
  plan_current_iteration: 1,
343
- plan_principles_resolved: true,
344
- plan_options_reviewed: true,
345
- plan_architect_review_status: 'complete',
346
- plan_critic_verdict: 'approve',
347
- plan_acceptance_criteria_testable: true,
348
- plan_verification_steps_resolved: true,
349
- plan_execution_inputs_resolved: true,
350
- plan_docs_status: 'complete',
383
+ plan_principles_resolved: planDocsComplete,
384
+ plan_options_reviewed: planDocsComplete,
385
+ plan_architect_review_status: planDocsComplete ? 'complete' : 'not-started',
386
+ plan_critic_verdict: planDocsComplete ? 'approve' : 'none',
387
+ plan_acceptance_criteria_testable: planDocsComplete,
388
+ plan_verification_steps_resolved: planDocsComplete,
389
+ plan_execution_inputs_resolved: planDocsComplete,
390
+ plan_docs_status: planDocsComplete ? 'complete' : 'partial',
391
+ plan_blockers: planBlockers,
351
392
  approval: {
352
- plan: APPROVAL_STATES.APPROVED,
393
+ plan: planDocsComplete ? APPROVAL_STATES.APPROVED : APPROVAL_STATES.NOT_REQUESTED,
353
394
  build: APPROVAL_STATES.NOT_REQUESTED,
354
395
  review: APPROVAL_STATES.NOT_REQUESTED,
355
396
  rollback: APPROVAL_STATES.NOT_REQUESTED,
package/src/workflow.mjs CHANGED
@@ -416,6 +416,11 @@ function createInitialState(slug, profile) {
416
416
  plan_execution_inputs_resolved: false,
417
417
  plan_docs_status: 'missing',
418
418
  plan_docs_artifact_paths: null,
419
+ plan_delegation_decision_path: null,
420
+ plan_delegation_mode: 'local',
421
+ plan_delegation_score: 0,
422
+ plan_delegation_triggers: [],
423
+ plan_delegation_reason: null,
419
424
  plan_review_artifact_paths: [],
420
425
  plan_review_history: [],
421
426
  plan_blockers: [],
@@ -749,6 +754,93 @@ async function writeRequirementTraceabilityArtifact({ root, sourceSpecPath, sour
749
754
  };
750
755
  }
751
756
 
757
+ function delegationDecisionForPlan(sourceText, plannerDraft) {
758
+ const source = String(sourceText || '');
759
+ const draft = [
760
+ plannerDraft.planText,
761
+ plannerDraft.architectureText,
762
+ plannerDraft.developmentPlanText,
763
+ plannerDraft.testPlanText,
764
+ ].join('\n');
765
+ const combined = `${source}\n${draft}`;
766
+ const requirementCount = sourceRequirementItems(source).length;
767
+ const lineCount = source.split('\n').filter((line) => line.trim()).length;
768
+ const triggers = [];
769
+ let score = 0;
770
+
771
+ const addTrigger = (trigger, weight) => {
772
+ if (!triggers.includes(trigger)) {
773
+ triggers.push(trigger);
774
+ score += weight;
775
+ }
776
+ };
777
+
778
+ if (requirementCount >= 12 || lineCount >= 180) {
779
+ addTrigger('large_requirement_surface', 3);
780
+ } else if (requirementCount >= 6 || lineCount >= 90) {
781
+ addTrigger('medium_requirement_surface', 2);
782
+ }
783
+ if (/资金|资产|清算|结算|交易|订单|风控|权限|安全|合规|审计|corporate action|settlement|trading|order|asset|security|auth|permission|compliance|audit|financial/i.test(combined)) {
784
+ addTrigger('high_risk_domain', 3);
785
+ }
786
+ if (/api|接口|service|biz|data|database|schema|migration|数据库|迁移|worker|cron|frontend|后台|部署|deploy/i.test(combined)) {
787
+ addTrigger('cross_module_scope', 2);
788
+ }
789
+ if (/状态机|幂等|补偿|差异|回滚|并发|重试|eventual|idempot|retry|rollback|concurrency|state machine/i.test(combined)) {
790
+ addTrigger('state_or_integrity_complexity', 2);
791
+ }
792
+ if (/e2e|集成测试|integration|regression|回归|验收|acceptance|fixture|mock|真实数据|external/i.test(combined)) {
793
+ addTrigger('verification_complexity', 1);
794
+ }
795
+ if (/多个方案|备选|取舍|tradeoff|alternative|ADR|architecture/i.test(combined)) {
796
+ addTrigger('architectural_tradeoff', 1);
797
+ }
798
+
799
+ const mode = score >= 7 ? 'parallel-review' : (score >= 4 ? 'critic-only' : 'local');
800
+ const reason = mode === 'parallel-review'
801
+ ? '高风险或跨模块规划,建议独立 Planner/Architect/Critic 视角并行审查。'
802
+ : mode === 'critic-only'
803
+ ? '存在中等复杂度或验证风险,建议至少引入独立 critic 复核 PRD 覆盖和风险。'
804
+ : '范围较小或风险较低,本地顺序 Planner/Architect/Critic 审阅足够。';
805
+
806
+ return {
807
+ mode,
808
+ score,
809
+ triggers,
810
+ reason,
811
+ current_runtime_execution: 'local-sequential',
812
+ execution_note: '当前 runtime 记录委派决策依据;是否实际启动 native subagents 仍受执行环境和用户授权约束。',
813
+ };
814
+ }
815
+
816
+ async function writePlanDelegationDecisionArtifact({ root, sourceText, plannerDraft }) {
817
+ const decision = delegationDecisionForPlan(sourceText, plannerDraft);
818
+ const path = artifactPath(root, 'plan-delegation-decision.md');
819
+ await writeText(path, [
820
+ '# Plan Delegation Decision',
821
+ '',
822
+ `- mode: ${decision.mode}`,
823
+ `- score: ${decision.score}`,
824
+ `- current_runtime_execution: ${decision.current_runtime_execution}`,
825
+ `- reason: ${decision.reason}`,
826
+ '',
827
+ '## Triggers',
828
+ '',
829
+ ...(decision.triggers.length > 0 ? decision.triggers.map((item) => `- ${item}`) : ['- none']),
830
+ '',
831
+ '## Guidance',
832
+ '',
833
+ '- local: 低风险、小范围、单模块任务,本地顺序 Planner/Architect/Critic 即可。',
834
+ '- critic-only: 中等风险或覆盖面较宽,至少需要独立 critic 复核 PRD 覆盖、验证和遗漏风险。',
835
+ '- parallel-review: 高风险、多模块、状态/资产/安全相关任务,建议独立 Planner/Architect/Critic 视角并行审查。',
836
+ '',
837
+ '## Runtime Note',
838
+ '',
839
+ `- ${decision.execution_note}`,
840
+ ].join('\n'));
841
+ return { path, ...decision };
842
+ }
843
+
752
844
  function frontmatterList(text, key) {
753
845
  if (!text.startsWith('---\n')) {
754
846
  return [];
@@ -1522,6 +1614,31 @@ function containsChineseText(text) {
1522
1614
  return chineseChars.length >= 40 || (chineseChars.length >= 8 && chineseChars.length / signalChars >= 0.2);
1523
1615
  }
1524
1616
 
1617
+ async function planLanguageBlockers(pathsByKey) {
1618
+ const blockers = [];
1619
+ for (const [key, path] of Object.entries(pathsByKey)) {
1620
+ if (!existsSync(path)) {
1621
+ blockers.push(`missing_plan_artifact_${key}`);
1622
+ continue;
1623
+ }
1624
+ const text = await readFile(path, 'utf8');
1625
+ if (!containsChineseText(text)) {
1626
+ blockers.push(`plan_artifact_not_chinese_${key}`);
1627
+ }
1628
+ }
1629
+ return blockers;
1630
+ }
1631
+
1632
+ function planReviewArtifactBlockers(state) {
1633
+ if (!Array.isArray(state.plan_review_artifact_paths) || state.plan_review_artifact_paths.length === 0) {
1634
+ return ['missing_plan_review_artifacts'];
1635
+ }
1636
+ const latest = state.plan_review_artifact_paths[state.plan_review_artifact_paths.length - 1] || {};
1637
+ return ['planner', 'architect', 'critic']
1638
+ .filter((key) => !latest[key] || !existsSync(latest[key]))
1639
+ .map((key) => `missing_plan_review_artifact_${key}`);
1640
+ }
1641
+
1525
1642
  async function ensurePlanWorkflowFromDirectSpec(cwd, directSpecPath, explicitSlug, options = {}) {
1526
1643
  const resolvedSpecPath = resolve(cwd, directSpecPath);
1527
1644
  const specText = await readFile(resolvedSpecPath, 'utf8');
@@ -1681,6 +1798,10 @@ async function readPlanCompletion(cwd, root, slug, state) {
1681
1798
  if (!state.requirement_traceability_path || !existsSync(state.requirement_traceability_path)) {
1682
1799
  blockers.push('missing_requirement_traceability');
1683
1800
  }
1801
+ if (!state.plan_delegation_decision_path || !existsSync(state.plan_delegation_decision_path)) {
1802
+ blockers.push('missing_plan_delegation_decision');
1803
+ }
1804
+ blockers.push(...planReviewArtifactBlockers(state));
1684
1805
  if (state.source_requirements_status && state.source_requirements_status !== 'complete') {
1685
1806
  if (state.requirement_traceability_path && existsSync(state.requirement_traceability_path)) {
1686
1807
  const traceabilityText = await readFile(state.requirement_traceability_path, 'utf8');
@@ -1695,22 +1816,16 @@ async function readPlanCompletion(cwd, root, slug, state) {
1695
1816
  blockers.push(`source_requirements_${state.source_requirements_status}`);
1696
1817
  }
1697
1818
  }
1698
- const workflowDocs = {
1819
+ blockers.push(...await planLanguageBlockers({
1699
1820
  plan: artifactPath(root, 'plan.md'),
1700
1821
  architecture: artifactPath(root, 'architecture.md'),
1701
1822
  developmentPlan: artifactPath(root, 'development-plan.md'),
1702
1823
  testPlan: artifactPath(root, 'test-plan.md'),
1703
- };
1704
- for (const [key, path] of Object.entries(workflowDocs)) {
1705
- if (!existsSync(path)) {
1706
- blockers.push(`missing_plan_artifact_${key}`);
1707
- continue;
1708
- }
1709
- const text = await readFile(path, 'utf8');
1710
- if (!containsChineseText(text)) {
1711
- blockers.push(`plan_artifact_not_chinese_${key}`);
1712
- }
1713
- }
1824
+ prd: state.plan_artifact_path || join(resolvePlansRoot(cwd), `prd-${slug}.md`),
1825
+ testSpec: state.test_spec_artifact_path || join(resolvePlansRoot(cwd), `test-spec-${slug}.md`),
1826
+ traceability: state.requirement_traceability_path || artifactPath(root, 'requirement-traceability.md'),
1827
+ delegationDecision: state.plan_delegation_decision_path || artifactPath(root, 'plan-delegation-decision.md'),
1828
+ }));
1714
1829
  const changeStatus = await readChangeArtifactStatus(state.change_artifact_paths);
1715
1830
  blockers.push(...changeStatus.blockers);
1716
1831
 
@@ -2835,7 +2950,9 @@ export async function clarifyStage(cwd, slug, { profile = 'standard' } = {}) {
2835
2950
  },
2836
2951
  });
2837
2952
  await writeState(root, state);
2838
- return { root, state };
2953
+ const rendered = await renderPlanReadingViews(cwd, root, state, normalized);
2954
+ await writeState(root, rendered);
2955
+ return { root, state: rendered };
2839
2956
  }
2840
2957
 
2841
2958
  export async function approveStage(cwd, slug, { from, to }) {
@@ -3201,6 +3318,11 @@ export async function planStage(cwd, slug, options = {}) {
3201
3318
  plannerDraft,
3202
3319
  changeArtifactPaths,
3203
3320
  });
3321
+ const delegationDecision = await writePlanDelegationDecisionArtifact({
3322
+ root,
3323
+ sourceText,
3324
+ plannerDraft,
3325
+ });
3204
3326
 
3205
3327
  architectReview = await adapter.architect({
3206
3328
  cwd,
@@ -3249,6 +3371,11 @@ export async function planStage(cwd, slug, options = {}) {
3249
3371
  requirement_traceability_path: traceability.path,
3250
3372
  source_requirements_status: traceability.status,
3251
3373
  source_requirements_item_count: traceability.itemCount,
3374
+ plan_delegation_decision_path: delegationDecision.path,
3375
+ plan_delegation_mode: delegationDecision.mode,
3376
+ plan_delegation_score: delegationDecision.score,
3377
+ plan_delegation_triggers: delegationDecision.triggers,
3378
+ plan_delegation_reason: delegationDecision.reason,
3252
3379
  change_id: normalizeSlug(changeId),
3253
3380
  change_artifacts_status: changeArtifactStatus.status,
3254
3381
  change_artifact_paths: changeArtifactPaths,