@ai-content-space/loopx 0.1.8 → 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
 
@@ -2248,7 +2363,7 @@ function buildCurrentEvidenceChain(state, readiness = buildReadiness(state), aut
2248
2363
  evidence.push(evidenceEntry(
2249
2364
  'review_approved',
2250
2365
  'Review verdict is approve.',
2251
- authorization.done.authorized ? 'The approved review -> done transition can be consumed.' : 'Completion still requires explicit review -> done authorization.',
2366
+ authorization.done.authorized ? 'Archive can consume the approved review -> done transition before syncing specs.' : 'Completion still requires explicit review -> done authorization.',
2252
2367
  ));
2253
2368
  }
2254
2369
  if (state.archive_status === 'archived' && state.spec_sync_status === 'synced') {
@@ -2371,9 +2486,7 @@ function recommendedAction(state, legacy = false) {
2371
2486
  : 'Approve build -> review when execution-record.md is complete.';
2372
2487
  case STAGES.REVIEW:
2373
2488
  if (state.review_verdict === 'approve') {
2374
- return state.approval.complete === APPROVAL_STATES.APPROVED
2375
- ? 'Run loopx review again to consume the approved review -> done transition.'
2376
- : 'Approve review -> done to complete the workflow.';
2489
+ return 'Run loopx archive; archive consumes the pending review -> done completion transition before syncing specs.';
2377
2490
  }
2378
2491
  if (state.review_verdict === 'request-changes') {
2379
2492
  if (state.requested_transition === TRANSITIONS.REVIEW_TO_BUILD && state.approval.build === APPROVAL_STATES.APPROVED) {
@@ -2574,7 +2687,11 @@ function nextCommandForRollbackTarget(slug, target) {
2574
2687
  if (target === 'none') {
2575
2688
  return [
2576
2689
  'Next:',
2690
+ `$archive ${slug}`,
2691
+ '',
2692
+ 'CLI-only equivalent:',
2577
2693
  `loopx approve ${slug} --from review --to done`,
2694
+ `loopx archive ${slug}`,
2578
2695
  ].join('\n');
2579
2696
  }
2580
2697
  return [
@@ -2587,7 +2704,7 @@ function nextCommandForRollbackTarget(slug, target) {
2587
2704
  function reviewUserMessageZh({ slug, verdict, rollbackTarget, findings }) {
2588
2705
  const label = reviewVerdictLabel(verdict);
2589
2706
  const next = verdict === 'APPROVE'
2590
- ? `下一步:批准 review -> done 后完成工作流。\n${nextCommandForRollbackTarget(slug, 'none')}`
2707
+ ? `下一步:直接归档;archive 会先消费 pending 的 review -> done 完成态。\n${nextCommandForRollbackTarget(slug, 'none')}`
2591
2708
  : `下一步:按审查发现处理,并${rollbackTargetLabel(rollbackTarget)}。\n${nextCommandForRollbackTarget(slug, rollbackTarget)}`;
2592
2709
  const findingText = Array.isArray(findings) && findings.length > 0 ? findings.join(';') : '无额外发现。';
2593
2710
  return `Review 结果:${slug} ${label}。审查发现:${findingText} ${next}`;
@@ -2833,7 +2950,9 @@ export async function clarifyStage(cwd, slug, { profile = 'standard' } = {}) {
2833
2950
  },
2834
2951
  });
2835
2952
  await writeState(root, state);
2836
- return { root, state };
2953
+ const rendered = await renderPlanReadingViews(cwd, root, state, normalized);
2954
+ await writeState(root, rendered);
2955
+ return { root, state: rendered };
2837
2956
  }
2838
2957
 
2839
2958
  export async function approveStage(cwd, slug, { from, to }) {
@@ -3199,6 +3318,11 @@ export async function planStage(cwd, slug, options = {}) {
3199
3318
  plannerDraft,
3200
3319
  changeArtifactPaths,
3201
3320
  });
3321
+ const delegationDecision = await writePlanDelegationDecisionArtifact({
3322
+ root,
3323
+ sourceText,
3324
+ plannerDraft,
3325
+ });
3202
3326
 
3203
3327
  architectReview = await adapter.architect({
3204
3328
  cwd,
@@ -3247,6 +3371,11 @@ export async function planStage(cwd, slug, options = {}) {
3247
3371
  requirement_traceability_path: traceability.path,
3248
3372
  source_requirements_status: traceability.status,
3249
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,
3250
3379
  change_id: normalizeSlug(changeId),
3251
3380
  change_artifacts_status: changeArtifactStatus.status,
3252
3381
  change_artifact_paths: changeArtifactPaths,
@@ -3929,7 +4058,7 @@ export async function reviewStage(cwd, slug, { reviewer = 'independent-reviewer'
3929
4058
  verdict: reviewInput.verdict,
3930
4059
  reviewMessageZh: reviewMessage,
3931
4060
  evidenceManifest: reviewInput.evidenceManifest,
3932
- followUps: ['等待 review -> done 审批。'],
4061
+ followUps: ['执行 $archive;archive 会消费 pending 的 review -> done 完成态。'],
3933
4062
  });
3934
4063
  } catch (error) {
3935
4064
  journalWarning = error instanceof Error ? error.message : String(error);