@ai-content-space/loopx 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +343 -56
  2. package/README.zh-CN.md +392 -0
  3. package/package.json +4 -1
  4. package/plugins/loopx/.codex-plugin/plugin.json +1 -1
  5. package/plugins/loopx/scripts/plugin-install.test.mjs +1 -0
  6. package/plugins/loopx/skills/archive/SKILL.md +39 -0
  7. package/plugins/loopx/skills/build/SKILL.md +111 -9
  8. package/plugins/loopx/skills/clarify/SKILL.md +121 -1
  9. package/plugins/loopx/skills/debug/SKILL.md +296 -0
  10. package/plugins/loopx/skills/debug/condition-based-waiting.md +115 -0
  11. package/plugins/loopx/skills/debug/defense-in-depth.md +122 -0
  12. package/plugins/loopx/skills/debug/find-polluter.sh +63 -0
  13. package/plugins/loopx/skills/debug/root-cause-tracing.md +169 -0
  14. package/plugins/loopx/skills/go-style/SKILL.md +71 -0
  15. package/plugins/loopx/skills/kratos/SKILL.md +74 -0
  16. package/plugins/loopx/skills/kratos/references/advanced-features.md +314 -0
  17. package/plugins/loopx/skills/kratos/references/architecture.md +488 -0
  18. package/plugins/loopx/skills/kratos/references/configuration.md +399 -0
  19. package/plugins/loopx/skills/kratos/references/http-customization.md +512 -0
  20. package/plugins/loopx/skills/kratos/references/middleware-logging.md +400 -0
  21. package/plugins/loopx/skills/kratos/references/proto-api-design.md +432 -0
  22. package/plugins/loopx/skills/kratos/references/security-auth.md +411 -0
  23. package/plugins/loopx/skills/kratos/references/troubleshooting.md +385 -0
  24. package/plugins/loopx/skills/plan/SKILL.md +22 -2
  25. package/plugins/loopx/skills/review/SKILL.md +98 -1
  26. package/plugins/loopx/skills/tdd/SKILL.md +371 -0
  27. package/plugins/loopx/skills/tdd/testing-anti-patterns.md +299 -0
  28. package/plugins/loopx/skills/verify/SKILL.md +139 -0
  29. package/scripts/codex-stop-hook.mjs +71 -0
  30. package/scripts/codex-workflow-hook.mjs +153 -0
  31. package/skills/archive/SKILL.md +39 -0
  32. package/skills/build/SKILL.md +111 -9
  33. package/skills/clarify/SKILL.md +121 -1
  34. package/skills/debug/SKILL.md +296 -0
  35. package/skills/debug/condition-based-waiting.md +115 -0
  36. package/skills/debug/defense-in-depth.md +122 -0
  37. package/skills/debug/find-polluter.sh +63 -0
  38. package/skills/debug/root-cause-tracing.md +169 -0
  39. package/skills/go-style/SKILL.md +71 -0
  40. package/skills/kratos/SKILL.md +74 -0
  41. package/skills/kratos/references/advanced-features.md +314 -0
  42. package/skills/kratos/references/architecture.md +488 -0
  43. package/skills/kratos/references/configuration.md +399 -0
  44. package/skills/kratos/references/http-customization.md +512 -0
  45. package/skills/kratos/references/middleware-logging.md +400 -0
  46. package/skills/kratos/references/proto-api-design.md +432 -0
  47. package/skills/kratos/references/security-auth.md +411 -0
  48. package/skills/kratos/references/troubleshooting.md +385 -0
  49. package/skills/plan/SKILL.md +18 -2
  50. package/skills/review/SKILL.md +98 -1
  51. package/skills/tdd/SKILL.md +371 -0
  52. package/skills/tdd/testing-anti-patterns.md +299 -0
  53. package/skills/verify/SKILL.md +139 -0
  54. package/src/build-runtime.mjs +303 -26
  55. package/src/build-stop-gate.mjs +94 -0
  56. package/src/cli.mjs +47 -5
  57. package/src/codex-exec-runtime.mjs +105 -5
  58. package/src/context-manifest.mjs +172 -0
  59. package/src/install-discovery.mjs +352 -5
  60. package/src/next-skill.mjs +57 -5
  61. package/src/plan-runtime.mjs +79 -122
  62. package/src/review-runtime.mjs +378 -0
  63. package/src/runtime-maintenance.mjs +428 -14
  64. package/src/template-governance.mjs +223 -0
  65. package/src/workflow.mjs +1941 -117
  66. package/src/workspace-context.mjs +166 -0
  67. package/src/workspace-memory.mjs +69 -0
package/src/workflow.mjs CHANGED
@@ -1,12 +1,24 @@
1
- import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
1
+ import { cp, mkdir, readFile, readdir, rename, writeFile } from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
- import { basename, dirname, join, resolve } from 'node:path';
3
+ import { basename, dirname, 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';
7
- import { ensureLoopxRoot, resolveLoopxRoot } from './runtime-maintenance.mjs';
7
+ import { writeBuildActiveState } from './build-stop-gate.mjs';
8
+ import {
9
+ buildContextManifestPath,
10
+ generateBuildContextManifest,
11
+ generateReviewContextManifest,
12
+ manifestRowsToInputManifest,
13
+ readContextManifest,
14
+ reviewContextManifestPath,
15
+ } from './context-manifest.mjs';
16
+ import { doctorRuntime, ensureLoopxRoot, resolveLoopxRoot } from './runtime-maintenance.mjs';
8
17
  import { DEFAULT_BUILD_MAX_ITERATIONS, createDefaultBuildAdapter } from './build-runtime.mjs';
9
18
  import { DEFAULT_MAX_ITERATIONS, createDefaultPlanAdapter } from './plan-runtime.mjs';
19
+ import { createDefaultReviewAdapter } from './review-runtime.mjs';
20
+ import { appendWorkspaceJournal } from './workspace-memory.mjs';
21
+ import { inspectWorkspaceContext, setupWorkspaceContext } from './workspace-context.mjs';
10
22
 
11
23
  const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
12
24
  const WORKSPACE_SCHEMA_VERSION = 1;
@@ -32,20 +44,18 @@ export const TRANSITIONS = {
32
44
  CLARIFY_TO_PLAN: 'clarify->plan',
33
45
  PLAN_TO_BUILD: 'plan->build',
34
46
  BUILD_TO_REVIEW: 'build->review',
47
+ REVIEW_TO_BUILD: 'review->build',
35
48
  REVIEW_TO_PLAN: 'review->plan',
49
+ REVIEW_TO_CLARIFY: 'review->clarify',
36
50
  REVIEW_TO_DONE: 'review->done',
37
51
  };
38
52
 
39
53
  const PLAN_ARTIFACTS = ['plan.md', 'architecture.md', 'development-plan.md', 'test-plan.md'];
40
54
  const V1_ARTIFACTS = ['spec.md', ...PLAN_ARTIFACTS, 'execution-record.md', 'review-report.md'];
41
55
  const LEGACY_ARTIFACTS = ['brief.md', 'plan.md', 'detailed-design.md', 'architecture.md', 'test-plan.md', 'build-result.md', 'review-report.md'];
42
- const PLAN_DOC_FILENAMES = {
43
- architecture: '架构文档.md',
44
- design: '设计文档.md',
45
- testPlan: '测试计划.md',
46
- };
47
56
  const PLAN_REVIEW_DIR = 'plan-reviews';
48
57
  const BUILD_SUPPORT_DIR = 'build-support';
58
+ const CHANGE_ARTIFACTS = ['proposal.md', 'spec-delta.md', 'design.md', 'tasks.md', 'slices.json', 'artifact-graph.json'];
49
59
  const CLARIFY_PROFILES = {
50
60
  standard: {
51
61
  threshold: 0.2,
@@ -69,6 +79,39 @@ function normalizeSlug(raw) {
69
79
  return slug;
70
80
  }
71
81
 
82
+ function slugFromBuildInput(raw) {
83
+ const value = String(raw || '');
84
+ const name = basename(value);
85
+ const match = /^prd-(.+)\.md$/.exec(name);
86
+ return match ? normalizeSlug(match[1]) : normalizeSlug(value);
87
+ }
88
+
89
+ function isReviewReworkArtifactInput(raw) {
90
+ const name = basename(String(raw || ''));
91
+ return name === 'review-report.md' || name === 'review.md';
92
+ }
93
+
94
+ function slugFromReviewReworkInput(raw) {
95
+ if (!isReviewReworkArtifactInput(raw)) {
96
+ throw new Error('build_from_review_artifact_required');
97
+ }
98
+ return normalizeSlug(basename(dirname(resolve(String(raw)))));
99
+ }
100
+
101
+ function displayPath(cwd, path) {
102
+ const resolved = resolve(cwd, path);
103
+ const rel = relative(cwd, resolved);
104
+ return rel && !rel.startsWith('..') ? rel : resolved;
105
+ }
106
+
107
+ function reviewReportArtifactPath(slug) {
108
+ return `.loopx/workflows/${normalizeSlug(slug)}/review-report.md`;
109
+ }
110
+
111
+ function reviewReworkBuildCommand(slug) {
112
+ return `$build --from-review ${reviewReportArtifactPath(slug)}`;
113
+ }
114
+
72
115
  function nowIso() {
73
116
  return new Date().toISOString();
74
117
  }
@@ -193,7 +236,7 @@ async function writeText(path, text) {
193
236
  }
194
237
 
195
238
  async function writeState(root, state) {
196
- await writeText(statePath(root), JSON.stringify(state, null, 2));
239
+ await writeText(statePath(root), JSON.stringify(enrichRuntimeJudgment(state), null, 2));
197
240
  }
198
241
 
199
242
  export function resolveWorkspaceRoot(cwd) {
@@ -208,22 +251,28 @@ function resolveSpecsRoot(cwd) {
208
251
  return join(resolveWorkspaceRoot(cwd), 'specs');
209
252
  }
210
253
 
211
- function resolvePlansRoot(cwd) {
212
- return join(resolveWorkspaceRoot(cwd), 'plans');
254
+ function resolveChangesRoot(cwd) {
255
+ return join(resolveWorkspaceRoot(cwd), 'changes');
213
256
  }
214
257
 
215
- function resolveDocsRoot(cwd, slug) {
216
- return join(resolve(cwd), 'docs', normalizeSlug(slug));
258
+ function changeIdForWorkflowSlug(slug) {
259
+ return `chg-${normalizeSlug(slug)}`;
217
260
  }
218
261
 
219
- function resolvePlanDocPaths(cwd, slug) {
220
- const docsRoot = resolveDocsRoot(cwd, slug);
221
- return {
222
- docsRoot,
223
- architecture: join(docsRoot, PLAN_DOC_FILENAMES.architecture),
224
- design: join(docsRoot, PLAN_DOC_FILENAMES.design),
225
- testPlan: join(docsRoot, PLAN_DOC_FILENAMES.testPlan),
226
- };
262
+ function resolveChangeRoot(cwd, changeId) {
263
+ return join(resolveChangesRoot(cwd), 'active', normalizeSlug(changeId));
264
+ }
265
+
266
+ function resolveArchivedChangeRoot(cwd, changeId) {
267
+ return join(resolveChangesRoot(cwd), 'archive', normalizeSlug(changeId));
268
+ }
269
+
270
+ function resolvePlansRoot(cwd) {
271
+ return join(resolveWorkspaceRoot(cwd), 'plans');
272
+ }
273
+
274
+ function resolveContextRoot(cwd) {
275
+ return join(resolveWorkspaceRoot(cwd), 'context');
227
276
  }
228
277
 
229
278
  function resolvePlanReviewPaths(root, iteration) {
@@ -244,6 +293,8 @@ function resolveBuildSupportPaths(root, iteration) {
244
293
  architect: join(supportRoot, `architect-iteration-${iteration}.md`),
245
294
  deslop: join(supportRoot, `deslop-iteration-${iteration}.md`),
246
295
  regression: join(supportRoot, `regression-iteration-${iteration}.md`),
296
+ delegationLedger: join(supportRoot, 'delegation-ledger.json'),
297
+ completionAudit: join(supportRoot, 'completion-audit.json'),
247
298
  };
248
299
  }
249
300
 
@@ -335,6 +386,16 @@ function createInitialState(slug, profile) {
335
386
  plan_review_artifact_paths: [],
336
387
  plan_blockers: [],
337
388
  plan_source_spec_path: null,
389
+ change_id: changeIdForWorkflowSlug(slug),
390
+ change_artifacts_status: 'missing',
391
+ change_artifact_paths: null,
392
+ slice_artifacts_status: 'missing',
393
+ spec_delta_status: 'missing',
394
+ spec_sync_status: 'pending',
395
+ archive_status: 'pending',
396
+ archived_change_path: null,
397
+ archived_spec_paths: [],
398
+ adr_candidate_path: null,
338
399
  build_run_id: null,
339
400
  build_current_iteration: 0,
340
401
  build_max_iterations: DEFAULT_BUILD_MAX_ITERATIONS,
@@ -348,6 +409,14 @@ function createInitialState(slug, profile) {
348
409
  build_progress_artifact_paths: [],
349
410
  build_support_evidence_paths: [],
350
411
  build_no_deslop: false,
412
+ build_owner_id: null,
413
+ build_owner_session_id: null,
414
+ build_owner_status: 'not-started',
415
+ build_delegation_status: 'not-started',
416
+ build_delegation_ledger_path: null,
417
+ build_active_delegation_count: 0,
418
+ build_completion_audit_status: 'not-started',
419
+ build_completion_audit_path: null,
351
420
  autopilot_current_phase: 'none',
352
421
  autopilot_phase_history: [],
353
422
  autopilot_blockers: [],
@@ -415,6 +484,10 @@ async function copyArtifact(fromRoot, toPath, name) {
415
484
  await writeText(toPath, content);
416
485
  }
417
486
 
487
+ async function writeJson(path, value) {
488
+ await writeText(path, JSON.stringify(value, null, 2));
489
+ }
490
+
418
491
  async function writeCanonicalPlanArtifacts(cwd, root, slug) {
419
492
  const plansRoot = resolvePlansRoot(cwd);
420
493
  await ensureDir(plansRoot);
@@ -447,6 +520,477 @@ async function writeCanonicalPlanArtifacts(cwd, root, slug) {
447
520
  return { planPath, testSpecPath };
448
521
  }
449
522
 
523
+ function dedupeStrings(items) {
524
+ return [...new Set((items || []).map((item) => String(item || '').trim()).filter(Boolean))];
525
+ }
526
+
527
+ function bulletsFromSectionText(text, heading) {
528
+ const pattern = new RegExp(`#{2,3} ${heading}\\n\\n([\\s\\S]*?)(?=\\n#{2,3} |$)`, 'i');
529
+ const match = text.match(pattern);
530
+ if (!match) {
531
+ return [];
532
+ }
533
+ return match[1]
534
+ .split('\n')
535
+ .map((line) => line.trim())
536
+ .filter((line) => line.startsWith('- '))
537
+ .map((line) => line.slice(2).trim())
538
+ .filter(Boolean);
539
+ }
540
+
541
+ function frontmatterList(text, key) {
542
+ if (!text.startsWith('---\n')) {
543
+ return [];
544
+ }
545
+ const end = text.indexOf('\n---\n', 4);
546
+ if (end === -1) {
547
+ return [];
548
+ }
549
+ const lines = text.slice(4, end).split('\n');
550
+ const values = [];
551
+ for (let index = 0; index < lines.length; index += 1) {
552
+ const line = lines[index];
553
+ if (line.trim() === `${key}:`) {
554
+ for (let child = index + 1; child < lines.length; child += 1) {
555
+ const childLine = lines[child];
556
+ if (!/^\s+-\s+/.test(childLine)) {
557
+ break;
558
+ }
559
+ values.push(childLine.replace(/^\s+-\s+/, '').trim());
560
+ }
561
+ break;
562
+ }
563
+ }
564
+ return values.filter(Boolean);
565
+ }
566
+
567
+ function targetDomainsForChange(slug, sourceText) {
568
+ const explicit = bulletsFromSectionText(sourceText, 'Target Spec Domains');
569
+ if (explicit.length > 0) {
570
+ return dedupeStrings(explicit.map((item) => item.replace(/`/g, '')));
571
+ }
572
+ const frontmatterDomains = frontmatterList(sourceText, 'target_domains');
573
+ if (frontmatterDomains.length > 0) {
574
+ return dedupeStrings(frontmatterDomains.map((item) => item.replace(/`/g, '')));
575
+ }
576
+ return ['general'];
577
+ }
578
+
579
+ function sectionTextForHeading(text, heading, level = 2) {
580
+ const hashes = '#'.repeat(level);
581
+ const pattern = new RegExp(`${hashes} ${heading}\\n\\n([\\s\\S]*?)(?=\\n${hashes} |$)`, 'i');
582
+ return text.match(pattern)?.[1] || '';
583
+ }
584
+
585
+ function parseLegacyDomainDeltas(text) {
586
+ const domains = targetDomainsForChange('general', text);
587
+ const entries = new Map();
588
+ for (const domain of domains) {
589
+ const domainText = sectionTextForHeading(text, domain, 2);
590
+ if (!domainText.trim()) {
591
+ continue;
592
+ }
593
+ entries.set(domain, {
594
+ added: bulletsFromSectionText(domainText, 'Added Requirements').filter((item) => item !== 'none'),
595
+ modified: bulletsFromSectionText(domainText, 'Modified Requirements').filter((item) => item !== 'none'),
596
+ removed: bulletsFromSectionText(domainText, 'Removed Requirements').filter((item) => item !== 'none'),
597
+ scenarios: bulletsFromSectionText(domainText, 'Scenarios'),
598
+ });
599
+ }
600
+ return entries;
601
+ }
602
+
603
+ function requirementsForDelta(slug, plannerDraft) {
604
+ const requirements = String(plannerDraft.planText || '')
605
+ .split('\n')
606
+ .map((line) => line.trim())
607
+ .filter((line) => /^\d+\.\s+/.test(line))
608
+ .map((line) => line.replace(/^\d+\.\s+/, '').trim());
609
+ return dedupeStrings(requirements.length > 0 ? requirements : [
610
+ `Workflow ${slug} SHALL implement the approved loopx plan package.`,
611
+ ]);
612
+ }
613
+
614
+ function verticalSlicesForChange(slug, plannerDraft) {
615
+ const requirements = requirementsForDelta(slug, plannerDraft);
616
+ const slices = requirements.slice(0, 8).map((requirement, index) => ({
617
+ id: `VS-${index + 1}`,
618
+ title: requirement.length > 90 ? `${requirement.slice(0, 87)}...` : requirement,
619
+ type: 'AFK',
620
+ blocked_by: index === 0 ? [] : [`VS-${index}`],
621
+ behavior: requirement,
622
+ acceptance_criteria: [
623
+ `完成端到端行为:${requirement}`,
624
+ '执行记录包含对应验证证据。',
625
+ ],
626
+ verification_signal: 'execution-record.md verification evidence',
627
+ }));
628
+ return {
629
+ schema_version: 1,
630
+ philosophy: 'tracer-bullet-vertical-slices',
631
+ workflow: slug,
632
+ slices: slices.length > 0 ? slices : [{
633
+ id: 'VS-1',
634
+ title: `Implement approved workflow ${slug}`,
635
+ type: 'AFK',
636
+ blocked_by: [],
637
+ behavior: `Workflow ${slug} delivers the approved plan end-to-end.`,
638
+ acceptance_criteria: ['Execution record verifies the approved behavior.'],
639
+ verification_signal: 'execution-record.md verification evidence',
640
+ }],
641
+ };
642
+ }
643
+
644
+ function changeArtifactGraph({ changeId, slug, artifacts }) {
645
+ const graph = {
646
+ schema_version: 1,
647
+ change: changeId,
648
+ workflow: slug,
649
+ philosophy: 'artifact-dependency-graph',
650
+ artifacts: {
651
+ proposal: {
652
+ path: artifacts.proposal,
653
+ status: existsSync(artifacts.proposal) ? 'done' : 'missing',
654
+ dependsOn: [],
655
+ },
656
+ specDelta: {
657
+ path: artifacts.specDelta,
658
+ status: existsSync(artifacts.specDelta) ? 'done' : 'missing',
659
+ dependsOn: ['proposal'],
660
+ },
661
+ design: {
662
+ path: artifacts.design,
663
+ status: existsSync(artifacts.design) ? 'done' : 'missing',
664
+ dependsOn: ['proposal', 'specDelta'],
665
+ },
666
+ tasks: {
667
+ path: artifacts.tasks,
668
+ status: existsSync(artifacts.tasks) ? 'done' : 'missing',
669
+ dependsOn: ['proposal', 'specDelta', 'design'],
670
+ },
671
+ slices: {
672
+ path: artifacts.slices,
673
+ status: existsSync(artifacts.slices) ? 'done' : 'missing',
674
+ dependsOn: ['proposal', 'specDelta', 'design'],
675
+ },
676
+ },
677
+ };
678
+ graph.nextReady = Object.entries(graph.artifacts)
679
+ .filter(([, node]) => node.status !== 'done')
680
+ .filter(([, node]) => node.dependsOn.every((dependency) => graph.artifacts[dependency]?.status === 'done'))
681
+ .map(([name]) => name);
682
+ return graph;
683
+ }
684
+
685
+ async function writeChangeArtifacts(cwd, root, slug, sourceText, plannerDraft, changeId = changeIdForWorkflowSlug(slug)) {
686
+ const normalizedChangeId = normalizeSlug(changeId);
687
+ const changeRoot = resolveChangeRoot(cwd, normalizedChangeId);
688
+ const specsRoot = join(changeRoot, 'specs');
689
+ await ensureDir(specsRoot);
690
+ const paths = {
691
+ root: changeRoot,
692
+ proposal: join(changeRoot, 'proposal.md'),
693
+ specDelta: join(changeRoot, 'spec-delta.md'),
694
+ design: join(changeRoot, 'design.md'),
695
+ tasks: join(changeRoot, 'tasks.md'),
696
+ slices: join(changeRoot, 'slices.json'),
697
+ graph: join(changeRoot, 'artifact-graph.json'),
698
+ };
699
+ const domains = targetDomainsForChange(slug, sourceText);
700
+ const requirements = requirementsForDelta(slug, plannerDraft);
701
+ const slices = verticalSlicesForChange(slug, plannerDraft);
702
+
703
+ await writeText(paths.proposal, [
704
+ `# loopx Change Proposal: ${normalizedChangeId}`,
705
+ '',
706
+ '## Why',
707
+ '',
708
+ '- Preserve the approved workflow intent as a durable change proposal.',
709
+ '',
710
+ '## What Changes',
711
+ '',
712
+ ...requirements.map((item) => `- ${item}`),
713
+ '',
714
+ '## Target Spec Domains',
715
+ '',
716
+ ...domains.map((domain) => `- ${domain}`),
717
+ '',
718
+ '## Source',
719
+ '',
720
+ `- change id: ${normalizedChangeId}`,
721
+ `- workflow slug: ${slug}`,
722
+ `- workflow: ${artifactPath(root, 'state.json')}`,
723
+ `- source spec: ${artifactPath(root, 'spec.md')}`,
724
+ ].join('\n'));
725
+
726
+ await writeText(paths.specDelta, [
727
+ `# loopx Spec Delta: ${normalizedChangeId}`,
728
+ '',
729
+ '## Target Spec Domains',
730
+ '',
731
+ ...domains.map((domain) => `- ${domain}`),
732
+ '',
733
+ '## Added Requirements',
734
+ '',
735
+ ...requirements.map((item) => `- ${item}`),
736
+ '',
737
+ '## Modified Requirements',
738
+ '',
739
+ '- none',
740
+ '',
741
+ '## Removed Requirements',
742
+ '',
743
+ '- none',
744
+ '',
745
+ '## Scenarios',
746
+ '',
747
+ `- GIVEN workflow ${slug} has an approved plan`,
748
+ '- WHEN build and review complete successfully',
749
+ '- THEN the accepted behavior is merged into long-lived loopx specs during archive',
750
+ ].join('\n'));
751
+
752
+ await writeText(paths.design, [
753
+ `# loopx Change Design: ${normalizedChangeId}`,
754
+ '',
755
+ '## Technical Approach',
756
+ '',
757
+ plannerDraft.architectureText || '- See workflow architecture artifact.',
758
+ '',
759
+ '## Task Plan',
760
+ '',
761
+ plannerDraft.developmentPlanText || '- See workflow development plan artifact.',
762
+ ].join('\n'));
763
+
764
+ await writeText(paths.tasks, [
765
+ `# loopx Change Tasks: ${normalizedChangeId}`,
766
+ '',
767
+ '## Vertical Slices',
768
+ '',
769
+ ...slices.slices.map((slice) => `- [ ] ${slice.id} ${slice.title} (${slice.type}) - verification: ${slice.verification_signal}`),
770
+ '',
771
+ '## Tasks',
772
+ '',
773
+ ...requirements.map((item, index) => `- [ ] ${index + 1}. ${item}`),
774
+ '',
775
+ '## Verification',
776
+ '',
777
+ plannerDraft.testPlanText || '- See workflow test plan artifact.',
778
+ ].join('\n'));
779
+
780
+ await writeJson(paths.slices, slices);
781
+ await writeJson(paths.graph, changeArtifactGraph({ changeId: normalizedChangeId, slug, artifacts: paths }));
782
+ for (const domain of domains) {
783
+ const specDeltaPath = join(specsRoot, ...domain.split('/'), 'spec.md');
784
+ await ensureDir(dirname(specDeltaPath));
785
+ await copyArtifact(changeRoot, specDeltaPath, 'spec-delta.md');
786
+ }
787
+ return paths;
788
+ }
789
+
790
+ async function readChangeArtifactStatus(paths) {
791
+ if (!paths || typeof paths !== 'object') {
792
+ return {
793
+ status: 'missing',
794
+ specDeltaStatus: 'missing',
795
+ blockers: ['missing_change_artifacts'],
796
+ };
797
+ }
798
+ const blockers = [];
799
+ for (const name of ['proposal', 'specDelta', 'design', 'tasks', 'slices', 'graph']) {
800
+ const path = paths[name];
801
+ if (!path || !existsSync(path)) {
802
+ blockers.push(`missing_change_artifact_${name}`);
803
+ }
804
+ }
805
+ let specDeltaStatus = 'missing';
806
+ if (paths.specDelta && existsSync(paths.specDelta)) {
807
+ const text = await readFile(paths.specDelta, 'utf8');
808
+ const parsedDelta = parseSpecDelta(text);
809
+ const domainDeltas = Array.from(parsedDelta.domainDeltas?.values() || []);
810
+ const hasDomains = parsedDelta.domains.length > 0;
811
+ const hasRequirements = parsedDelta.added.length > 0
812
+ || parsedDelta.modified.length > 0
813
+ || domainDeltas.some((delta) => delta.added.length > 0 || delta.modified.length > 0);
814
+ if (!text.trim()) {
815
+ specDeltaStatus = 'partial';
816
+ blockers.push('spec_delta_empty');
817
+ } else if (!hasDomains || !hasRequirements) {
818
+ specDeltaStatus = 'partial';
819
+ if (!hasDomains) {
820
+ blockers.push('spec_delta_missing_domains');
821
+ }
822
+ if (!hasRequirements) {
823
+ blockers.push('spec_delta_missing_requirements');
824
+ }
825
+ } else {
826
+ specDeltaStatus = 'complete';
827
+ }
828
+ }
829
+ if (paths.slices && existsSync(paths.slices)) {
830
+ try {
831
+ const payload = JSON.parse(await readFile(paths.slices, 'utf8'));
832
+ const slices = Array.isArray(payload.slices) ? payload.slices : [];
833
+ const valid = slices.length > 0 && slices.every((slice) => (
834
+ slice
835
+ && typeof slice.id === 'string'
836
+ && slice.id
837
+ && ['AFK', 'HITL'].includes(slice.type)
838
+ && typeof slice.behavior === 'string'
839
+ && slice.behavior
840
+ && Array.isArray(slice.acceptance_criteria)
841
+ && slice.acceptance_criteria.length > 0
842
+ && typeof slice.verification_signal === 'string'
843
+ && slice.verification_signal
844
+ ));
845
+ if (!valid) {
846
+ blockers.push('vertical_slices_missing');
847
+ }
848
+ } catch {
849
+ blockers.push('vertical_slices_invalid');
850
+ }
851
+ }
852
+ return {
853
+ status: blockers.length > 0 ? 'partial' : 'complete',
854
+ specDeltaStatus,
855
+ sliceArtifactsStatus: blockers.some((blocker) => blocker.startsWith('missing_change_artifact_slices') || blocker.startsWith('vertical_slices_')) ? 'partial' : 'complete',
856
+ blockers,
857
+ };
858
+ }
859
+
860
+ async function ensureArchiveSlicesArtifact(cwd, root, slug, state) {
861
+ if (state.change_artifact_paths?.slices && existsSync(state.change_artifact_paths.slices)) {
862
+ return state.change_artifact_paths;
863
+ }
864
+ if (!state.change_artifact_paths?.root || !existsSync(state.change_artifact_paths.root)) {
865
+ return state.change_artifact_paths;
866
+ }
867
+ const slicesPath = join(state.change_artifact_paths.root, 'slices.json');
868
+ const draft = {
869
+ planText: existsSync(state.change_artifact_paths.tasks)
870
+ ? await readFile(state.change_artifact_paths.tasks, 'utf8')
871
+ : `1. Archive approved workflow ${slug}`,
872
+ };
873
+ await writeJson(slicesPath, verticalSlicesForChange(slug, draft));
874
+ const nextPaths = {
875
+ ...state.change_artifact_paths,
876
+ slices: slicesPath,
877
+ };
878
+ if (nextPaths.graph && existsSync(nextPaths.graph)) {
879
+ await writeJson(nextPaths.graph, changeArtifactGraph({
880
+ changeId: state.change_id || changeIdForWorkflowSlug(slug),
881
+ slug,
882
+ artifacts: nextPaths,
883
+ }));
884
+ }
885
+ await writeState(root, withRecommendedAction({
886
+ ...state,
887
+ change_artifact_paths: nextPaths,
888
+ slice_artifacts_status: 'complete',
889
+ }));
890
+ return nextPaths;
891
+ }
892
+
893
+ function parseSpecDelta(text) {
894
+ const domainDeltas = parseLegacyDomainDeltas(text);
895
+ return {
896
+ domains: targetDomainsForChange('general', text),
897
+ added: bulletsFromSectionText(text, 'Added Requirements').filter((item) => item !== 'none'),
898
+ modified: bulletsFromSectionText(text, 'Modified Requirements').filter((item) => item !== 'none'),
899
+ removed: bulletsFromSectionText(text, 'Removed Requirements').filter((item) => item !== 'none'),
900
+ scenarios: bulletsFromSectionText(text, 'Scenarios'),
901
+ domainDeltas,
902
+ };
903
+ }
904
+
905
+ function specDomainPath(cwd, domain) {
906
+ return join(resolveSpecsRoot(cwd), ...String(domain).split('/').map((part) => normalizeSlug(part)), 'spec.md');
907
+ }
908
+
909
+ async function writeAdrCandidate(cwd, changeId, state, archivedSpecPaths) {
910
+ const path = join(resolveWorkspaceRoot(cwd), 'decisions', 'adr-candidates', `${normalizeSlug(changeId)}.md`);
911
+ await ensureDir(dirname(path));
912
+ await writeText(path, [
913
+ `# ADR Candidate: ${normalizeSlug(changeId)}`,
914
+ '',
915
+ '## Decision',
916
+ '',
917
+ `- Archive accepted workflow ${state.slug} into long-lived loopx specs.`,
918
+ '',
919
+ '## Drivers',
920
+ '',
921
+ '- The reviewed change delta has reached done.',
922
+ '- The change may affect future planning, build, and review context.',
923
+ '',
924
+ '## Alternatives Considered',
925
+ '',
926
+ '- Keep the decision only in workflow artifacts.',
927
+ '- Promote the accepted behavior into long-lived specs and keep this ADR candidate as advisory memory.',
928
+ '',
929
+ '## Why Candidate Only',
930
+ '',
931
+ '- loopx should not make irreversible architectural decisions without human confirmation.',
932
+ '- This file records the candidate so a future human can promote it to docs/adr if useful.',
933
+ '',
934
+ '## Consequences',
935
+ '',
936
+ ...archivedSpecPaths.map((item) => `- Updated spec: ${item}`),
937
+ '',
938
+ '## Follow-ups',
939
+ '',
940
+ '- Promote to a real ADR only if the decision is hard to reverse, surprising, and trade-off-heavy.',
941
+ ].join('\n'));
942
+ return path;
943
+ }
944
+
945
+ function replaceChangeBlock(existing, slug, nextBlock) {
946
+ if (!existing) {
947
+ return nextBlock;
948
+ }
949
+ const marker = `### Change: ${slug}`;
950
+ const start = existing.indexOf(marker);
951
+ if (start === -1) {
952
+ return [existing.replace(/\s+$/, ''), '', nextBlock].join('\n');
953
+ }
954
+ const before = existing.slice(0, start).replace(/\s+$/, '');
955
+ const rest = existing.slice(start);
956
+ const nextStart = rest.slice(marker.length).search(/\n### Change: /);
957
+ const after = nextStart === -1 ? '' : rest.slice(marker.length + nextStart).replace(/^\s+/, '\n');
958
+ return [before, nextBlock, after.trimEnd()].filter(Boolean).join('\n\n');
959
+ }
960
+
961
+ async function mergeSpecDeltaIntoLongLivedSpecs(cwd, slug, specDeltaPath) {
962
+ const deltaText = await readFile(specDeltaPath, 'utf8');
963
+ const delta = parseSpecDelta(deltaText);
964
+ const updated = [];
965
+ for (const domain of delta.domains) {
966
+ const domainDelta = delta.domainDeltas?.get(domain) || delta;
967
+ const path = specDomainPath(cwd, domain);
968
+ await ensureDir(dirname(path));
969
+ const existing = await readTextIfExists(path);
970
+ const base = existing || [
971
+ `# loopx Spec Domain: ${domain}`,
972
+ '',
973
+ '## Purpose',
974
+ '',
975
+ `Long-lived accepted behavior for ${domain}.`,
976
+ '',
977
+ '## Requirements',
978
+ ].join('\n');
979
+ const changeBlock = [
980
+ `### Change: ${slug}`,
981
+ '',
982
+ ...(domainDelta.added.length > 0 ? ['#### Added Requirements', '', ...domainDelta.added.map((item) => `- ${item}`), ''] : []),
983
+ ...(domainDelta.modified.length > 0 ? ['#### Modified Requirements', '', ...domainDelta.modified.map((item) => `- ${item}`), ''] : []),
984
+ ...(domainDelta.removed.length > 0 ? ['#### Removed Requirements', '', ...domainDelta.removed.map((item) => `- ${item}`), ''] : []),
985
+ ...(domainDelta.scenarios.length > 0 ? ['#### Scenarios', '', ...domainDelta.scenarios.map((item) => `- ${item}`)] : []),
986
+ ].join('\n');
987
+ const next = replaceChangeBlock(base, slug, changeBlock);
988
+ await writeText(path, next);
989
+ updated.push(path);
990
+ }
991
+ return updated;
992
+ }
993
+
450
994
  function deriveSlugFromSpecPath(path, text) {
451
995
  const meta = parseFrontmatter(text);
452
996
  if (meta.workflow_id) {
@@ -457,7 +1001,13 @@ function deriveSlugFromSpecPath(path, text) {
457
1001
  }
458
1002
 
459
1003
  function containsChineseText(text) {
460
- return /[\u3400-\u9fff]/.test(text);
1004
+ const chineseChars = text.match(/[\u3400-\u9fff]/g) || [];
1005
+ const latinChars = text.match(/[A-Za-z]/g) || [];
1006
+ const signalChars = chineseChars.length + latinChars.length;
1007
+ if (signalChars === 0) {
1008
+ return false;
1009
+ }
1010
+ return chineseChars.length >= 40 || (chineseChars.length >= 8 && chineseChars.length / signalChars >= 0.2);
461
1011
  }
462
1012
 
463
1013
  async function ensurePlanWorkflowFromDirectSpec(cwd, directSpecPath, explicitSlug, options = {}) {
@@ -512,13 +1062,6 @@ async function writePlanArtifacts(root, cwd, slug, plannerDraft) {
512
1062
  await writeText(artifactPath(root, 'architecture.md'), plannerDraft.architectureText);
513
1063
  await writeText(artifactPath(root, 'development-plan.md'), plannerDraft.developmentPlanText);
514
1064
  await writeText(artifactPath(root, 'test-plan.md'), plannerDraft.testPlanText);
515
-
516
- const docPaths = resolvePlanDocPaths(cwd, slug);
517
- await ensureDir(docPaths.docsRoot);
518
- await writeText(docPaths.architecture, plannerDraft.docs.architecture);
519
- await writeText(docPaths.design, plannerDraft.docs.design);
520
- await writeText(docPaths.testPlan, plannerDraft.docs.testPlan);
521
- return docPaths;
522
1065
  }
523
1066
 
524
1067
  async function writePlanReviewArtifacts(root, iteration, plannerDraft, architectReview, criticReview) {
@@ -568,12 +1111,6 @@ async function writePlanReviewArtifacts(root, iteration, plannerDraft, architect
568
1111
 
569
1112
  async function readPlanCompletion(cwd, root, slug, state) {
570
1113
  const blockers = [];
571
- const docPaths = resolvePlanDocPaths(cwd, slug);
572
- const docsPresent = {
573
- architecture: existsSync(docPaths.architecture),
574
- design: existsSync(docPaths.design),
575
- testPlan: existsSync(docPaths.testPlan),
576
- };
577
1114
  if (state.plan_architect_review_status !== 'complete') {
578
1115
  blockers.push('architect_review_incomplete');
579
1116
  }
@@ -598,24 +1135,31 @@ async function readPlanCompletion(cwd, root, slug, state) {
598
1135
  if (!state.test_spec_artifact_path || !existsSync(state.test_spec_artifact_path)) {
599
1136
  blockers.push('missing_test_spec');
600
1137
  }
601
- for (const [key, present] of Object.entries(docsPresent)) {
602
- if (!present) {
603
- blockers.push(`missing_doc_${key}`);
1138
+ const workflowDocs = {
1139
+ plan: artifactPath(root, 'plan.md'),
1140
+ architecture: artifactPath(root, 'architecture.md'),
1141
+ developmentPlan: artifactPath(root, 'development-plan.md'),
1142
+ testPlan: artifactPath(root, 'test-plan.md'),
1143
+ };
1144
+ for (const [key, path] of Object.entries(workflowDocs)) {
1145
+ if (!existsSync(path)) {
1146
+ blockers.push(`missing_plan_artifact_${key}`);
604
1147
  continue;
605
1148
  }
606
- const text = await readFile(docPaths[key], 'utf8');
1149
+ const text = await readFile(path, 'utf8');
607
1150
  if (!containsChineseText(text)) {
608
- blockers.push(`doc_not_chinese_${key}`);
1151
+ blockers.push(`plan_artifact_not_chinese_${key}`);
609
1152
  }
610
1153
  }
611
-
612
- const docsComplete = Object.values(docsPresent).every(Boolean)
613
- && blockers.every((blocker) => !blocker.startsWith('doc_not_chinese_') && !blocker.startsWith('missing_doc_'));
1154
+ const changeStatus = await readChangeArtifactStatus(state.change_artifact_paths);
1155
+ blockers.push(...changeStatus.blockers);
614
1156
 
615
1157
  return {
616
1158
  blockers,
617
- docsStatus: docsComplete ? 'complete' : Object.values(docsPresent).some(Boolean) ? 'partial' : 'missing',
618
- docPaths,
1159
+ docsStatus: blockers.some((blocker) => blocker.startsWith('missing_plan_artifact_') || blocker.startsWith('plan_artifact_not_chinese_')) ? 'partial' : 'complete',
1160
+ changeArtifactsStatus: changeStatus.status,
1161
+ specDeltaStatus: changeStatus.specDeltaStatus,
1162
+ sliceArtifactsStatus: changeStatus.sliceArtifactsStatus,
619
1163
  };
620
1164
  }
621
1165
 
@@ -641,6 +1185,168 @@ function buildIterationBlockers(iterationData, { noDeslop = false } = {}) {
641
1185
  return blockers;
642
1186
  }
643
1187
 
1188
+ function buildOwnerId(slug) {
1189
+ return `loopx-build-owner:${normalizeSlug(slug)}`;
1190
+ }
1191
+
1192
+ function buildOwnerSessionId(slug, runId) {
1193
+ return `${buildOwnerId(slug)}:${runId || 'pending'}`;
1194
+ }
1195
+
1196
+ function normalizeBuildDelegations(iterationData = {}) {
1197
+ return Array.isArray(iterationData.delegations)
1198
+ ? iterationData.delegations.map((item, index) => ({
1199
+ id: item?.id || `delegation-${index + 1}`,
1200
+ role: item?.role || 'implementation',
1201
+ status: ['active', 'complete', 'failed', 'blocked', 'pending', 'skipped'].includes(String(item?.status || '').trim().toLowerCase())
1202
+ ? String(item.status).trim().toLowerCase()
1203
+ : 'pending',
1204
+ blocking: item?.blocking !== false,
1205
+ scope: Array.isArray(item?.scope) ? item.scope.map(String) : [],
1206
+ evidence_path: item?.evidence_path || item?.evidencePath || null,
1207
+ summary: item?.summary || 'Build delegation entry',
1208
+ }))
1209
+ : [];
1210
+ }
1211
+
1212
+ function isBlockingDelegationOpen(item) {
1213
+ return item?.blocking && !['complete', 'skipped'].includes(String(item.status));
1214
+ }
1215
+
1216
+ function buildDelegationLedger({ slug, ownerId, ownerSessionId, iterationData, previousLedger = null }) {
1217
+ const delegationsById = new Map();
1218
+ for (const item of previousLedger?.delegations || []) {
1219
+ if (isBlockingDelegationOpen(item)) {
1220
+ delegationsById.set(item.id, item);
1221
+ }
1222
+ }
1223
+ for (const item of normalizeBuildDelegations(iterationData)) {
1224
+ if (['complete', 'skipped'].includes(String(item.status))) {
1225
+ delegationsById.delete(item.id);
1226
+ } else {
1227
+ delegationsById.set(item.id, item);
1228
+ }
1229
+ }
1230
+ const delegations = [...delegationsById.values()];
1231
+ const activeBlocking = delegations.filter((item) => item.blocking && !['complete', 'skipped'].includes(String(item.status)));
1232
+ return {
1233
+ schema_version: WORKFLOW_SCHEMA_VERSION,
1234
+ slug,
1235
+ owner_id: ownerId,
1236
+ owner_session_id: ownerSessionId,
1237
+ updated_at: nowIso(),
1238
+ active_blocking_count: activeBlocking.length,
1239
+ status: activeBlocking.length > 0 ? 'active' : 'drained',
1240
+ delegations,
1241
+ };
1242
+ }
1243
+
1244
+ function buildDelegationBlockers(ledger) {
1245
+ return (ledger.delegations || [])
1246
+ .filter((item) => item.blocking && !['complete', 'skipped'].includes(String(item.status)))
1247
+ .map((item) => `delegation_active_${item.id}`);
1248
+ }
1249
+
1250
+ async function readJsonIfExists(path) {
1251
+ if (!path || !existsSync(path)) {
1252
+ return null;
1253
+ }
1254
+ try {
1255
+ return JSON.parse(await readFile(path, 'utf8'));
1256
+ } catch {
1257
+ return null;
1258
+ }
1259
+ }
1260
+
1261
+ async function buildCompletionAudit({ cwd, root, slug, state, reviewReworkArtifactPath = null, iterationData, ledger, baseBlockers }) {
1262
+ const checklist = [];
1263
+ const addChecklistItem = (item) => {
1264
+ checklist.push({
1265
+ status: 'covered',
1266
+ evidence: [],
1267
+ ...item,
1268
+ });
1269
+ };
1270
+
1271
+ addChecklistItem({
1272
+ id: 'approved-prd',
1273
+ source: 'approved-plan',
1274
+ requirement: state.plan_artifact_path || join(cwd, '.loopx', 'plans', `prd-${slug}.md`),
1275
+ evidence: [state.plan_artifact_path || 'approved plan artifact'],
1276
+ });
1277
+ addChecklistItem({
1278
+ id: 'test-spec',
1279
+ source: 'test-spec',
1280
+ requirement: state.test_spec_artifact_path || join(cwd, '.loopx', 'plans', `test-spec-${slug}.md`),
1281
+ evidence: iterationData.verificationEvidence || [],
1282
+ });
1283
+ const effectiveReviewReworkPath = reviewReworkArtifactPath || state.review_rework_artifact_path;
1284
+ if (effectiveReviewReworkPath) {
1285
+ addChecklistItem({
1286
+ id: 'review-rework',
1287
+ source: 'review-rework',
1288
+ requirement: effectiveReviewReworkPath,
1289
+ evidence: [
1290
+ effectiveReviewReworkPath,
1291
+ ...(iterationData.executionEvidence || []),
1292
+ ...(iterationData.verificationEvidence || []),
1293
+ ].filter(Boolean),
1294
+ });
1295
+ }
1296
+
1297
+ const slicesPayload = await readJsonIfExists(state.change_artifact_paths?.slices);
1298
+ const slices = Array.isArray(slicesPayload?.slices) ? slicesPayload.slices : [];
1299
+ for (const slice of slices) {
1300
+ addChecklistItem({
1301
+ id: slice.id || `slice-${checklist.length + 1}`,
1302
+ source: 'vertical-slice',
1303
+ requirement: slice.behavior || slice.verification_signal || 'vertical slice',
1304
+ evidence: [
1305
+ slice.verification_signal,
1306
+ ...(iterationData.executionEvidence || []),
1307
+ ...(iterationData.verificationEvidence || []),
1308
+ ].filter(Boolean),
1309
+ });
1310
+ }
1311
+
1312
+ const verificationEvidence = [
1313
+ ...(iterationData.verificationEvidence || []),
1314
+ ...(iterationData.lanes || [])
1315
+ .flatMap((lane) => Array.isArray(lane.evidence) ? lane.evidence : [])
1316
+ .map((item) => `${item.kind}:${item.summary}:${item.ref}`),
1317
+ ].filter(Boolean);
1318
+ const blockers = dedupeStrings([
1319
+ ...baseBlockers,
1320
+ ...buildDelegationBlockers(ledger),
1321
+ ]);
1322
+ const missingEvidence = checklist.filter((item) => !Array.isArray(item.evidence) || item.evidence.length === 0);
1323
+ if (checklist.length === 0 || missingEvidence.length > 0 || verificationEvidence.length === 0) {
1324
+ blockers.push('completion_audit_missing_evidence');
1325
+ }
1326
+ const passed = blockers.length === 0;
1327
+ return {
1328
+ schema_version: WORKFLOW_SCHEMA_VERSION,
1329
+ slug,
1330
+ owner_id: ledger.owner_id,
1331
+ owner_session_id: ledger.owner_session_id,
1332
+ status: passed ? 'passed' : 'blocked',
1333
+ passed,
1334
+ updated_at: nowIso(),
1335
+ blockers: dedupeStrings(blockers),
1336
+ checklist,
1337
+ verification_evidence: verificationEvidence,
1338
+ lane_statuses: (iterationData.lanes || []).map((lane) => ({ name: lane.name, status: lane.status })),
1339
+ };
1340
+ }
1341
+
1342
+ function buildHasInfrastructureFailure(iterationData) {
1343
+ const limitationText = [
1344
+ ...(Array.isArray(iterationData.limitations) ? iterationData.limitations : []),
1345
+ ...(Array.isArray(iterationData.lanes) ? iterationData.lanes.flatMap((lane) => [lane.summary, ...(Array.isArray(lane.limitations) ? lane.limitations : [])]) : []),
1346
+ ].join('\n');
1347
+ return /codex_exec_failed:|codex_exec_invalid_json:|timeout/i.test(limitationText);
1348
+ }
1349
+
644
1350
  function buildExecutionRecordContent({ slug, iterationData, complete }) {
645
1351
  const placeholder = complete ? null : 'TODO: build iteration is not review-ready yet.';
646
1352
  return [
@@ -681,7 +1387,7 @@ function buildExecutionRecordContent({ slug, iterationData, complete }) {
681
1387
  ].join('\n');
682
1388
  }
683
1389
 
684
- async function writeBuildSupportArtifacts(root, iterationData, noDeslop) {
1390
+ async function writeBuildSupportArtifacts(root, iterationData, noDeslop, { delegationLedger = null, completionAudit = null } = {}) {
685
1391
  const paths = resolveBuildSupportPaths(root, iterationData.iteration);
686
1392
  await ensureDir(paths.supportRoot);
687
1393
  await writeText(
@@ -718,6 +1424,22 @@ async function writeBuildSupportArtifacts(root, iterationData, noDeslop) {
718
1424
  `- status: ${noDeslop ? 'skipped' : iterationData.regressionStatus}`,
719
1425
  ].join('\n'),
720
1426
  );
1427
+ await writeJson(paths.delegationLedger, delegationLedger || {
1428
+ schema_version: WORKFLOW_SCHEMA_VERSION,
1429
+ slug: iterationData.slug,
1430
+ status: 'drained',
1431
+ active_blocking_count: 0,
1432
+ delegations: [],
1433
+ });
1434
+ await writeJson(paths.completionAudit, completionAudit || {
1435
+ schema_version: WORKFLOW_SCHEMA_VERSION,
1436
+ slug: iterationData.slug,
1437
+ status: 'blocked',
1438
+ passed: false,
1439
+ blockers: ['completion_audit_not_run'],
1440
+ checklist: [],
1441
+ verification_evidence: [],
1442
+ });
721
1443
  return paths;
722
1444
  }
723
1445
 
@@ -785,6 +1507,207 @@ function clarifyReadinessBlockers(state) {
785
1507
  return blockers;
786
1508
  }
787
1509
 
1510
+ function planReadinessBlockersSync(state) {
1511
+ const blockers = [];
1512
+ if (state.plan_architect_review_status !== 'complete') {
1513
+ blockers.push('architect_review_incomplete');
1514
+ }
1515
+ if (state.plan_critic_verdict !== 'approve') {
1516
+ blockers.push(`critic_verdict_${state.plan_critic_verdict}`);
1517
+ }
1518
+ if (state.plan_package_status !== 'complete') {
1519
+ blockers.push(`plan_package_${state.plan_package_status}`);
1520
+ }
1521
+ if (!state.plan_acceptance_criteria_testable) {
1522
+ blockers.push('acceptance_criteria_unresolved');
1523
+ }
1524
+ if (!state.plan_verification_steps_resolved) {
1525
+ blockers.push('verification_steps_unresolved');
1526
+ }
1527
+ if (!state.plan_execution_inputs_resolved) {
1528
+ blockers.push('execution_inputs_unresolved');
1529
+ }
1530
+ if (!state.plan_artifact_path) {
1531
+ blockers.push('missing_prd');
1532
+ }
1533
+ if (!state.test_spec_artifact_path) {
1534
+ blockers.push('missing_test_spec');
1535
+ }
1536
+ if (state.change_artifacts_status !== 'complete' && state.change_artifacts_status !== 'archived') {
1537
+ blockers.push(`change_artifacts_${state.change_artifacts_status || 'missing'}`);
1538
+ }
1539
+ if (state.spec_delta_status !== 'complete') {
1540
+ blockers.push(`spec_delta_${state.spec_delta_status || 'missing'}`);
1541
+ }
1542
+ if (Array.isArray(state.plan_blockers)) {
1543
+ blockers.push(...state.plan_blockers);
1544
+ }
1545
+ return dedupeStrings(blockers);
1546
+ }
1547
+
1548
+ function buildReadinessBlockersSync(state) {
1549
+ const blockers = [];
1550
+ if (state.execution_record_status !== 'complete') {
1551
+ blockers.push(`execution_record_${state.execution_record_status || 'missing'}`);
1552
+ }
1553
+ if (Array.isArray(state.build_blockers)) {
1554
+ blockers.push(...state.build_blockers);
1555
+ }
1556
+ return dedupeStrings(blockers);
1557
+ }
1558
+
1559
+ function reviewReadinessBlockersSync(state) {
1560
+ const blockers = [];
1561
+ if (state.review_status !== 'ready-for-review' && state.review_status !== 'in-review') {
1562
+ blockers.push(`review_status_${state.review_status || 'not-started'}`);
1563
+ }
1564
+ if (state.execution_record_status !== 'complete') {
1565
+ blockers.push(`execution_record_${state.execution_record_status || 'missing'}`);
1566
+ }
1567
+ if (Array.isArray(state.build_blockers)) {
1568
+ blockers.push(...state.build_blockers);
1569
+ }
1570
+ return dedupeStrings(blockers);
1571
+ }
1572
+
1573
+ function doneReadinessBlockersSync(state) {
1574
+ const blockers = [];
1575
+ if (state.review_verdict !== 'approve') {
1576
+ blockers.push(`review_verdict_${state.review_verdict || 'none'}`);
1577
+ }
1578
+ return blockers;
1579
+ }
1580
+
1581
+ function archiveReadinessBlockersSync(state) {
1582
+ const blockers = [];
1583
+ if (state.current_stage !== STAGES.DONE || state.completion_confirmed !== true) {
1584
+ blockers.push('workflow_not_done');
1585
+ }
1586
+ if (state.spec_delta_status !== 'complete') {
1587
+ blockers.push(`spec_delta_${state.spec_delta_status || 'missing'}`);
1588
+ }
1589
+ if (!state.change_artifact_paths?.specDelta) {
1590
+ blockers.push('missing_spec_delta_path');
1591
+ }
1592
+ return dedupeStrings(blockers);
1593
+ }
1594
+
1595
+ function readinessEntry(blockers) {
1596
+ const unique = dedupeStrings(blockers);
1597
+ return {
1598
+ ready: unique.length === 0,
1599
+ blockers: unique,
1600
+ };
1601
+ }
1602
+
1603
+ function authorizationEntry(state, key, transition) {
1604
+ return {
1605
+ authorized: state.approval?.[key] === APPROVAL_STATES.APPROVED,
1606
+ approval_status: state.approval?.[key] || APPROVAL_STATES.NOT_REQUESTED,
1607
+ transition,
1608
+ };
1609
+ }
1610
+
1611
+ function buildReadiness(state) {
1612
+ return {
1613
+ plan: readinessEntry(clarifyReadinessBlockers(state)),
1614
+ build: readinessEntry(planReadinessBlockersSync(state)),
1615
+ review: readinessEntry(buildReadinessBlockersSync(state)),
1616
+ done: readinessEntry(doneReadinessBlockersSync(state)),
1617
+ archive: readinessEntry(archiveReadinessBlockersSync(state)),
1618
+ };
1619
+ }
1620
+
1621
+ function buildAuthorization(state) {
1622
+ return {
1623
+ plan: authorizationEntry(state, 'plan', TRANSITIONS.CLARIFY_TO_PLAN),
1624
+ build: authorizationEntry(state, 'build', TRANSITIONS.PLAN_TO_BUILD),
1625
+ review: authorizationEntry(state, 'review', TRANSITIONS.BUILD_TO_REVIEW),
1626
+ done: authorizationEntry(state, 'complete', TRANSITIONS.REVIEW_TO_DONE),
1627
+ rollback: authorizationEntry(state, 'rollback', state.requested_transition || TRANSITIONS.NONE),
1628
+ };
1629
+ }
1630
+
1631
+ function evidenceEntry(claim, basis, implication) {
1632
+ return { claim, basis, implication };
1633
+ }
1634
+
1635
+ function buildCurrentEvidenceChain(state, readiness = buildReadiness(state), authorization = buildAuthorization(state)) {
1636
+ const evidence = [];
1637
+ if (readiness.plan.ready) {
1638
+ evidence.push(evidenceEntry(
1639
+ 'clarify_ready_for_plan',
1640
+ 'Clarify ambiguity score, non-goals, decision boundaries, pressure pass, and unresolved ambiguity gates are satisfied.',
1641
+ authorization.plan.authorized ? 'The approved clarify -> plan transition can be consumed by plan.' : 'Plan readiness exists, but user authorization is still separate.',
1642
+ ));
1643
+ }
1644
+ if (authorization.plan.authorized) {
1645
+ evidence.push(evidenceEntry(
1646
+ 'plan_authorized',
1647
+ 'approval.plan is approved for clarify -> plan.',
1648
+ 'Planning may proceed without treating readiness alone as authorization.',
1649
+ ));
1650
+ }
1651
+ if (readiness.build.ready) {
1652
+ evidence.push(evidenceEntry(
1653
+ 'plan_ready_for_build',
1654
+ 'Planner, architect, critic, plan artifacts, execution inputs, and change delta gates are satisfied.',
1655
+ authorization.build.authorized ? 'The approved plan -> build transition can be consumed by build.' : 'Build readiness exists, but user authorization is still separate.',
1656
+ ));
1657
+ }
1658
+ if (authorization.build.authorized) {
1659
+ evidence.push(evidenceEntry(
1660
+ 'build_authorized',
1661
+ 'approval.build is approved for plan -> build or review-requested build rework.',
1662
+ 'Build may consume the approved transition while preserving gate evidence.',
1663
+ ));
1664
+ }
1665
+ if (readiness.review.ready) {
1666
+ evidence.push(evidenceEntry(
1667
+ 'build_ready_for_review',
1668
+ 'Execution record is complete and no build blockers remain.',
1669
+ authorization.review.authorized ? 'The approved build -> review transition can be consumed by review.' : 'Review readiness exists, but user authorization is still separate.',
1670
+ ));
1671
+ }
1672
+ if (authorization.review.authorized) {
1673
+ evidence.push(evidenceEntry(
1674
+ 'review_authorized',
1675
+ 'approval.review is approved for build -> review.',
1676
+ 'Review may proceed as an independent acceptance gate.',
1677
+ ));
1678
+ }
1679
+ if (state.review_verdict === 'approve') {
1680
+ evidence.push(evidenceEntry(
1681
+ 'review_approved',
1682
+ 'Review verdict is approve.',
1683
+ authorization.done.authorized ? 'The approved review -> done transition can be consumed.' : 'Completion still requires explicit review -> done authorization.',
1684
+ ));
1685
+ }
1686
+ if (state.archive_status === 'archived' && state.spec_sync_status === 'synced') {
1687
+ evidence.push(evidenceEntry(
1688
+ 'change_delta_archived',
1689
+ 'Archive synced the accepted spec delta into long-lived specs.',
1690
+ 'The workflow has durable spec memory and can remain closed.',
1691
+ ));
1692
+ }
1693
+ return evidence;
1694
+ }
1695
+
1696
+ function enrichRuntimeJudgment(state, legacy = false) {
1697
+ if (!state || legacy) {
1698
+ return state;
1699
+ }
1700
+ const readiness = buildReadiness(state);
1701
+ const authorization = buildAuthorization(state);
1702
+ return {
1703
+ ...state,
1704
+ readiness,
1705
+ authorization,
1706
+ current_evidence_chain: buildCurrentEvidenceChain(state, readiness, authorization),
1707
+ recommended_next_action: recommendedAction(state, legacy),
1708
+ };
1709
+ }
1710
+
788
1711
  async function readExecutionRecordSummary(root) {
789
1712
  const text = await readTextIfExists(artifactPath(root, 'execution-record.md'));
790
1713
  if (!text) {
@@ -814,6 +1737,46 @@ async function readExecutionRecordSummary(root) {
814
1737
  };
815
1738
  }
816
1739
 
1740
+ function normalizeScopeList(value) {
1741
+ if (Array.isArray(value)) {
1742
+ return value.map((item) => String(item).trim()).filter(Boolean);
1743
+ }
1744
+ if (value === null || value === undefined || value === '') {
1745
+ return [];
1746
+ }
1747
+ return String(value)
1748
+ .split(/[,;\n]/)
1749
+ .map((item) => item.trim())
1750
+ .filter(Boolean);
1751
+ }
1752
+
1753
+ function executionScopeGate(meta = {}) {
1754
+ const plannedScope = String(meta.planned_scope || '').trim();
1755
+ const implementedScope = String(meta.implemented_scope || '').trim();
1756
+ const completionClaim = String(meta.completion_claim || '').trim().toLowerCase();
1757
+ const remainingScope = normalizeScopeList(meta.remaining_scope);
1758
+ const blockers = [];
1759
+
1760
+ if (remainingScope.length > 0) {
1761
+ blockers.push('partial_scope_remaining');
1762
+ }
1763
+ if (completionClaim && !['full', 'complete', 'workflow', 'all'].includes(completionClaim)) {
1764
+ blockers.push(`completion_claim_${completionClaim}`);
1765
+ }
1766
+ if (plannedScope && implementedScope && plannedScope !== implementedScope && completionClaim !== 'full') {
1767
+ blockers.push('implemented_scope_mismatch');
1768
+ }
1769
+
1770
+ return {
1771
+ ok: blockers.length === 0,
1772
+ blockers: dedupeStrings(blockers),
1773
+ plannedScope,
1774
+ implementedScope,
1775
+ completionClaim,
1776
+ remainingScope,
1777
+ };
1778
+ }
1779
+
817
1780
  function recommendedAction(state, legacy = false) {
818
1781
  if (legacy) {
819
1782
  return 'Legacy codex-helper workflow detected. Run loopx migrate or create a new loopx workflow.';
@@ -826,7 +1789,7 @@ function recommendedAction(state, legacy = false) {
826
1789
  : `Resolve ambiguity in ${state.clarify_profile ?? 'standard'} clarify mode and approve clarify -> plan.`;
827
1790
  case STAGES.PLAN:
828
1791
  if (Array.isArray(state.plan_blockers) && state.plan_blockers.length > 0) {
829
- return 'Run loopx plan to continue the planning review loop until architect, critic, and docs blockers are cleared.';
1792
+ return 'Run loopx plan to continue the planning review loop until architect, critic, and planning artifact blockers are cleared.';
830
1793
  }
831
1794
  return state.approval.build === APPROVAL_STATES.APPROVED
832
1795
  ? 'Run loopx build to consume the approved plan -> build transition.'
@@ -845,15 +1808,31 @@ function recommendedAction(state, legacy = false) {
845
1808
  : 'Approve review -> done to complete the workflow.';
846
1809
  }
847
1810
  if (state.review_verdict === 'request-changes') {
848
- return state.approval.rollback === APPROVAL_STATES.APPROVED
849
- ? 'Run loopx review again to consume the approved review -> plan transition.'
850
- : 'Approve review -> plan to roll back for another planning pass.';
1811
+ if (state.requested_transition === TRANSITIONS.REVIEW_TO_BUILD && state.approval.build === APPROVAL_STATES.APPROVED) {
1812
+ return 'Run loopx build to consume the approved review -> build transition.';
1813
+ }
1814
+ if (state.requested_transition === TRANSITIONS.REVIEW_TO_PLAN && state.approval.rollback === APPROVAL_STATES.APPROVED) {
1815
+ return 'Run loopx plan to consume the approved review -> plan transition.';
1816
+ }
1817
+ if (state.requested_transition === TRANSITIONS.REVIEW_TO_CLARIFY && state.approval.rollback === APPROVAL_STATES.APPROVED) {
1818
+ return 'Run loopx clarify to consume the approved review -> clarify transition.';
1819
+ }
1820
+ if (state.rollback_target === STAGES.BUILD) {
1821
+ return 'Approve review -> build to fix implementation issues.';
1822
+ }
1823
+ if (state.rollback_target === STAGES.CLARIFY) {
1824
+ return 'Approve review -> clarify to resolve requirement ambiguity.';
1825
+ }
1826
+ return 'Approve review -> plan to revise the plan package.';
851
1827
  }
852
1828
  return 'Run loopx review after build completes.';
853
1829
  case STAGES.DONE:
854
1830
  if (state.autopilot_current_phase && state.autopilot_current_phase !== 'none' && state.autopilot_completed) {
855
1831
  return 'Autopilot run is complete.';
856
1832
  }
1833
+ if (state.archive_status !== 'archived') {
1834
+ return 'Run loopx archive to sync the approved change delta into long-lived specs.';
1835
+ }
857
1836
  return 'Workflow is complete.';
858
1837
  default:
859
1838
  return 'Run loopx clarify to start a workflow.';
@@ -861,10 +1840,7 @@ function recommendedAction(state, legacy = false) {
861
1840
  }
862
1841
 
863
1842
  function withRecommendedAction(state, legacy = false) {
864
- return {
865
- ...state,
866
- recommended_next_action: recommendedAction(state, legacy),
867
- };
1843
+ return enrichRuntimeJudgment(state, legacy);
868
1844
  }
869
1845
 
870
1846
  async function loadWorkflowState(cwd, slug, { allowLegacy = true } = {}) {
@@ -897,7 +1873,10 @@ function approvalKeyForTransition(transition) {
897
1873
  return 'build';
898
1874
  case TRANSITIONS.BUILD_TO_REVIEW:
899
1875
  return 'review';
1876
+ case TRANSITIONS.REVIEW_TO_BUILD:
1877
+ return 'build';
900
1878
  case TRANSITIONS.REVIEW_TO_PLAN:
1879
+ case TRANSITIONS.REVIEW_TO_CLARIFY:
901
1880
  return 'rollback';
902
1881
  case TRANSITIONS.REVIEW_TO_DONE:
903
1882
  return 'complete';
@@ -912,6 +1891,34 @@ function ensureApprovedTransition(state, expectedTransition, key) {
912
1891
  }
913
1892
  }
914
1893
 
1894
+ function ensureValidContextManifest(manifest, stage) {
1895
+ if (manifest?.status === 'invalid') {
1896
+ throw new Error(`context_manifest_invalid:${stage}:${manifest.error || 'unknown'}`);
1897
+ }
1898
+ }
1899
+
1900
+ async function writeReviewJournal({ cwd, slug, verdict, reviewMessageZh, evidenceManifest = [], findings = [], followUps = [] }) {
1901
+ return appendWorkspaceJournal({
1902
+ cwd,
1903
+ workspaceRoot: resolveWorkspaceRoot(cwd),
1904
+ slug,
1905
+ stage: STAGES.REVIEW,
1906
+ verdict,
1907
+ reviewMessageZh,
1908
+ verificationEvidence: evidenceManifest.map((item) => item.summary || item.ref || JSON.stringify(item)),
1909
+ decisions: ['review 已执行 code review 与证据完整性检查。'],
1910
+ risks: verdict === 'APPROVE' ? ['暂无阻断风险。'] : findings,
1911
+ followUps,
1912
+ });
1913
+ }
1914
+
1915
+ async function writeReviewChangedFiles(root, changedFiles = []) {
1916
+ await ensureDir(join(root, 'review-support'));
1917
+ const path = join(root, 'review-support', 'changed-files.json');
1918
+ await writeText(path, `${JSON.stringify(Array.isArray(changedFiles) ? changedFiles : [], null, 2)}\n`);
1919
+ return path;
1920
+ }
1921
+
915
1922
  function executionRecordTemplate(slug, stage, actorId, runId) {
916
1923
  const timestamp = nowIso();
917
1924
  return [
@@ -952,7 +1959,116 @@ function executionRecordTemplate(slug, stage, actorId, runId) {
952
1959
  ].join('\n');
953
1960
  }
954
1961
 
955
- function reviewReportContent({ slug, reviewer, runId, verdict, rollbackTarget, rollbackRationale, inputManifest, evidenceManifest, findings }) {
1962
+ function reviewVerdictLabel(verdict) {
1963
+ return verdict === 'APPROVE' ? '通过' : '要求修改';
1964
+ }
1965
+
1966
+ function rollbackTargetLabel(rollbackTarget) {
1967
+ if (rollbackTarget === 'none') {
1968
+ return '无需回滚';
1969
+ }
1970
+ if (rollbackTarget === 'build') {
1971
+ return '回到 build 阶段修复实现问题';
1972
+ }
1973
+ if (rollbackTarget === 'plan') {
1974
+ return '回退到 plan 阶段';
1975
+ }
1976
+ if (rollbackTarget === 'clarify') {
1977
+ return '回到 clarify 阶段澄清需求';
1978
+ }
1979
+ return rollbackTarget;
1980
+ }
1981
+
1982
+ function transitionForRollbackTarget(target) {
1983
+ if (target === STAGES.BUILD) {
1984
+ return TRANSITIONS.REVIEW_TO_BUILD;
1985
+ }
1986
+ if (target === STAGES.CLARIFY) {
1987
+ return TRANSITIONS.REVIEW_TO_CLARIFY;
1988
+ }
1989
+ return TRANSITIONS.REVIEW_TO_PLAN;
1990
+ }
1991
+
1992
+ function nextCommandForRollbackTarget(slug, target) {
1993
+ if (target === STAGES.BUILD) {
1994
+ return [
1995
+ 'Next:',
1996
+ reviewReworkBuildCommand(slug),
1997
+ ].join('\n');
1998
+ }
1999
+ if (target === STAGES.CLARIFY) {
2000
+ return [
2001
+ 'Next:',
2002
+ `loopx approve ${slug} --from review --to clarify`,
2003
+ `$clarify ${slug}`,
2004
+ ].join('\n');
2005
+ }
2006
+ if (target === 'none') {
2007
+ return [
2008
+ 'Next:',
2009
+ `loopx approve ${slug} --from review --to done`,
2010
+ ].join('\n');
2011
+ }
2012
+ return [
2013
+ 'Next:',
2014
+ `loopx approve ${slug} --from review --to plan`,
2015
+ `$plan ${slug}`,
2016
+ ].join('\n');
2017
+ }
2018
+
2019
+ function reviewUserMessageZh({ slug, verdict, rollbackTarget, findings }) {
2020
+ const label = reviewVerdictLabel(verdict);
2021
+ const next = verdict === 'APPROVE'
2022
+ ? `下一步:批准 review -> done 后完成工作流。\n${nextCommandForRollbackTarget(slug, 'none')}`
2023
+ : `下一步:按审查发现处理,并${rollbackTargetLabel(rollbackTarget)}。\n${nextCommandForRollbackTarget(slug, rollbackTarget)}`;
2024
+ const findingText = Array.isArray(findings) && findings.length > 0 ? findings.join(';') : '无额外发现。';
2025
+ return `Review 结果:${slug} ${label}。审查发现:${findingText} ${next}`;
2026
+ }
2027
+
2028
+ function codeReviewFindingText(finding) {
2029
+ const location = finding.file ? `${finding.file}${finding.line ? `:${finding.line}` : ''}` : '未定位文件';
2030
+ return `[${finding.severity || 'medium'}] ${location}:${finding.message}`;
2031
+ }
2032
+
2033
+ function codeReviewFailureResult(error) {
2034
+ const message = error instanceof Error ? error.message : String(error);
2035
+ return {
2036
+ status: 'failed',
2037
+ verdict: 'request-changes',
2038
+ summary: `code-review 子流程失败,review 不能接受本次运行:${message}`,
2039
+ rollbackTarget: STAGES.BUILD,
2040
+ changedFiles: [],
2041
+ findings: [{
2042
+ severity: 'high',
2043
+ file: 'review-support/code-review.raw.json',
2044
+ line: null,
2045
+ message: `code-review 子流程未返回有效结构化 JSON:${message}`,
2046
+ }],
2047
+ };
2048
+ }
2049
+
2050
+ function architectureReviewFailureResult(error) {
2051
+ const message = error instanceof Error ? error.message : String(error);
2052
+ return {
2053
+ status: 'failed',
2054
+ verdict: 'block',
2055
+ summary: `architecture-smell 子流程失败,review 不能接受本次运行:${message}`,
2056
+ rollbackTarget: STAGES.BUILD,
2057
+ findings: [{
2058
+ severity: 'high',
2059
+ file: 'review-support/architecture-smell.raw.json',
2060
+ line: null,
2061
+ message: `architecture-smell 子流程未返回有效结构化 JSON:${message}`,
2062
+ }],
2063
+ };
2064
+ }
2065
+
2066
+ function architectureReviewFindingText(finding) {
2067
+ const location = finding.file ? `${finding.file}${finding.line ? `:${finding.line}` : ''}` : '未定位文件';
2068
+ return `[${finding.severity || 'medium'}] ${location}:${finding.message}`;
2069
+ }
2070
+
2071
+ function reviewReportContent({ slug, reviewer, runId, verdict, rollbackTarget, rollbackRationale, inputManifest, evidenceManifest, findings, codeReview, architectureReview }) {
956
2072
  return [
957
2073
  frontmatterBlock({
958
2074
  schema_version: WORKFLOW_SCHEMA_VERSION,
@@ -962,28 +2078,52 @@ function reviewReportContent({ slug, reviewer, runId, verdict, rollbackTarget, r
962
2078
  reviewed_run_id: runId,
963
2079
  input_manifest: inputManifest,
964
2080
  evidence_manifest: evidenceManifest,
2081
+ code_review: codeReview ? {
2082
+ status: codeReview.status,
2083
+ verdict: codeReview.verdict,
2084
+ changed_files: codeReview.changedFiles,
2085
+ } : null,
2086
+ architecture_smell: architectureReview ? {
2087
+ status: architectureReview.status,
2088
+ verdict: architectureReview.verdict,
2089
+ } : null,
965
2090
  verdict: verdict.toLowerCase().replace('request changes', 'request-changes'),
966
2091
  rollback_target: rollbackTarget,
967
2092
  rollback_rationale: rollbackRationale ?? null,
968
2093
  }),
969
- `# loopx Review Report: ${slug}`,
2094
+ `# loopx Review 结果:${slug}`,
970
2095
  '',
971
- '## Verdict',
2096
+ '## 结论',
972
2097
  '',
973
- `- ${verdict}`,
2098
+ `- ${reviewVerdictLabel(verdict)}(${verdict})`,
974
2099
  '',
975
- '## Evidence Reviewed',
2100
+ '## 已审查证据',
976
2101
  '',
977
2102
  ...inputManifest.map((item) => `- ${item}`),
978
2103
  '',
979
- '## Findings',
2104
+ '## 审查发现',
980
2105
  '',
981
2106
  ...findings.map((item) => `- ${item}`),
982
2107
  '',
983
- '## Rollback Recommendation',
2108
+ '## 代码审查',
2109
+ '',
2110
+ codeReview ? `- 状态:${codeReview.status}` : '- 状态:未执行',
2111
+ codeReview ? `- 结论:${codeReview.verdict}` : '- 结论:未知',
2112
+ codeReview ? `- 摘要:${codeReview.summary}` : '- 摘要:无',
2113
+ codeReview && codeReview.changedFiles.length > 0 ? `- 变更文件:${codeReview.changedFiles.join(', ')}` : '- 变更文件:无',
2114
+ ...(codeReview && codeReview.findings.length > 0 ? codeReview.findings.map((item) => `- ${codeReviewFindingText(item)}`) : ['- 未发现阻断性代码问题。']),
2115
+ '',
2116
+ '## Architecture Smell Scan',
2117
+ '',
2118
+ architectureReview ? `- 状态:${architectureReview.status}` : '- 状态:未执行',
2119
+ architectureReview ? `- 结论:${architectureReview.verdict}` : '- 结论:未知',
2120
+ architectureReview ? `- 摘要:${architectureReview.summary}` : '- 摘要:无',
2121
+ ...(architectureReview && architectureReview.findings.length > 0 ? architectureReview.findings.map((item) => `- ${architectureReviewFindingText(item)}`) : ['- 架构 smell 扫描通过。']),
2122
+ '',
2123
+ '## 回退建议',
984
2124
  '',
985
- `- ${rollbackTarget}`,
986
- rollbackRationale ? `- ${rollbackRationale}` : '- none',
2125
+ `- ${rollbackTargetLabel(rollbackTarget)}`,
2126
+ rollbackRationale ? `- ${rollbackRationale}` : '- ',
987
2127
  ].join('\n');
988
2128
  }
989
2129
 
@@ -1004,15 +2144,19 @@ export async function initWorkspace(cwd, { slug } = {}) {
1004
2144
  await ensureDir(join(workspaceRoot, 'context'));
1005
2145
  await ensureDir(join(workspaceRoot, 'workflows'));
1006
2146
  await ensureDir(join(workspaceRoot, 'specs'));
2147
+ await ensureDir(join(workspaceRoot, 'changes'));
2148
+ await ensureDir(join(workspaceRoot, 'changes', 'active'));
2149
+ await ensureDir(join(workspaceRoot, 'changes', 'archive'));
1007
2150
  await ensureDir(join(workspaceRoot, 'plans'));
1008
2151
  await ensureDir(join(workspaceRoot, 'autopilot'));
2152
+ await setupWorkspaceContext(cwd);
1009
2153
 
1010
2154
  const config = {
1011
2155
  schema_version: WORKSPACE_SCHEMA_VERSION,
1012
2156
  tool: 'loopx',
1013
2157
  product_contract: 'skill-first-v1',
1014
- default_flow: ['clarify', 'plan', 'build', 'review', 'done'],
1015
- preferred_surface: ['clarify', 'plan', 'build', 'review', 'autopilot'],
2158
+ default_flow: ['clarify', 'plan', 'build', 'review', 'done', 'archive'],
2159
+ preferred_surface: ['clarify', 'plan', 'build', 'review', 'archive', 'autopilot'],
1016
2160
  };
1017
2161
 
1018
2162
  if (!existsSync(workspaceConfigPath(workspaceRoot))) {
@@ -1033,21 +2177,62 @@ export async function clarifyStage(cwd, slug, { profile = 'standard' } = {}) {
1033
2177
  const normalized = normalizeSlug(slug);
1034
2178
  const clarifyProfile = normalizeClarifyProfile(profile);
1035
2179
  const root = resolveWorkflowRoot(cwd, normalized);
2180
+ const existing = await readState(cwd, normalized);
2181
+ const consumesReviewClarify = existing?.current_stage === STAGES.REVIEW
2182
+ && existing?.requested_transition === TRANSITIONS.REVIEW_TO_CLARIFY
2183
+ && existing?.approval?.rollback === APPROVAL_STATES.APPROVED
2184
+ && existing?.review_verdict === 'request-changes';
2185
+ const resumesConsumedReviewClarify = existing?.current_stage === STAGES.CLARIFY
2186
+ && existing?.last_confirmed_transition === TRANSITIONS.REVIEW_TO_CLARIFY
2187
+ && existing?.approval?.rollback === APPROVAL_STATES.APPROVED;
2188
+ const preservesExistingClarifySpec = consumesReviewClarify || resumesConsumedReviewClarify;
1036
2189
  await ensureLoopxRoot(cwd);
1037
2190
  await ensureDir(root);
1038
2191
  const stamp = nowStamp();
1039
- await writeTemplateArtifact(root, 'spec.md', {
1040
- 'task name': normalized,
1041
- 'workflow id': normalized,
1042
- profile: clarifyProfile,
1043
- 'target ambiguity threshold': CLARIFY_PROFILES[clarifyProfile].threshold,
1044
- 'max rounds': CLARIFY_PROFILES[clarifyProfile].maxRounds,
1045
- });
2192
+ if (!preservesExistingClarifySpec) {
2193
+ await writeTemplateArtifact(root, 'spec.md', {
2194
+ 'task name': normalized,
2195
+ 'workflow id': normalized,
2196
+ profile: clarifyProfile,
2197
+ 'target ambiguity threshold': CLARIFY_PROFILES[clarifyProfile].threshold,
2198
+ 'max rounds': CLARIFY_PROFILES[clarifyProfile].maxRounds,
2199
+ });
2200
+ }
1046
2201
  const specArtifactPath = canonicalClarifySpecPath(cwd, normalized, stamp);
1047
2202
  await copyArtifact(root, specArtifactPath, 'spec.md');
1048
- const state = withRecommendedAction({
1049
- ...createInitialState(normalized, clarifyProfile),
2203
+ const state = withRecommendedAction({
2204
+ ...(preservesExistingClarifySpec ? existing : createInitialState(normalized, clarifyProfile)),
2205
+ current_stage: STAGES.CLARIFY,
2206
+ stage_status: 'blocked',
2207
+ clarify_profile: clarifyProfile,
2208
+ clarify_target_ambiguity_threshold: CLARIFY_PROFILES[clarifyProfile].threshold,
2209
+ clarify_max_rounds: CLARIFY_PROFILES[clarifyProfile].maxRounds,
2210
+ clarify_current_round: preservesExistingClarifySpec ? existing.clarify_current_round : 0,
2211
+ clarify_ambiguity_score: 1,
2212
+ clarify_pressure_pass_complete: false,
2213
+ clarify_non_goals_resolved: false,
2214
+ clarify_decision_boundaries_resolved: false,
2215
+ ambiguity_items: preservesExistingClarifySpec ? existing.ambiguity_items : [
2216
+ {
2217
+ id: 'A-1',
2218
+ question: 'What specific task should loopx execute in this workflow?',
2219
+ status: 'open',
2220
+ resolution: null,
2221
+ },
2222
+ ],
2223
+ unresolved_ambiguity_count: preservesExistingClarifySpec ? Math.max(1, Number(existing.unresolved_ambiguity_count || 0)) : 1,
1050
2224
  spec_artifact_path: specArtifactPath,
2225
+ pending_user_decision: TRANSITIONS.NONE,
2226
+ requested_transition: TRANSITIONS.NONE,
2227
+ last_confirmed_transition: preservesExistingClarifySpec ? TRANSITIONS.REVIEW_TO_CLARIFY : TRANSITIONS.NONE,
2228
+ approval: {
2229
+ ...(preservesExistingClarifySpec ? existing.approval : createInitialState(normalized, clarifyProfile).approval),
2230
+ plan: APPROVAL_STATES.NOT_REQUESTED,
2231
+ build: APPROVAL_STATES.NOT_REQUESTED,
2232
+ review: APPROVAL_STATES.NOT_REQUESTED,
2233
+ rollback: preservesExistingClarifySpec ? APPROVAL_STATES.APPROVED : APPROVAL_STATES.NOT_REQUESTED,
2234
+ complete: APPROVAL_STATES.NOT_REQUESTED,
2235
+ },
1051
2236
  });
1052
2237
  await writeState(root, state);
1053
2238
  return { root, state };
@@ -1087,7 +2272,10 @@ export async function approveStage(cwd, slug, { from, to }) {
1087
2272
  next = {
1088
2273
  ...next,
1089
2274
  plan_docs_status: completion.docsStatus,
1090
- plan_docs_artifact_paths: completion.docPaths,
2275
+ plan_docs_artifact_paths: null,
2276
+ change_artifacts_status: completion.changeArtifactsStatus,
2277
+ spec_delta_status: completion.specDeltaStatus,
2278
+ slice_artifacts_status: completion.sliceArtifactsStatus,
1091
2279
  plan_blockers: completion.blockers,
1092
2280
  };
1093
2281
  if (completion.blockers.length > 0) {
@@ -1121,6 +2309,22 @@ export async function approveStage(cwd, slug, { from, to }) {
1121
2309
  }
1122
2310
 
1123
2311
  if (transition === TRANSITIONS.REVIEW_TO_PLAN) {
2312
+ if (next.review_verdict !== 'request-changes' || next.rollback_target !== STAGES.PLAN) {
2313
+ throw new Error('review_plan_fix_not_requested');
2314
+ }
2315
+ if (!next.rollback_rationale) {
2316
+ throw new Error('rollback_rationale_required');
2317
+ }
2318
+ }
2319
+ if (transition === TRANSITIONS.REVIEW_TO_BUILD) {
2320
+ if (next.review_verdict !== 'request-changes' || next.rollback_target !== STAGES.BUILD) {
2321
+ throw new Error('review_build_fix_not_requested');
2322
+ }
2323
+ }
2324
+ if (transition === TRANSITIONS.REVIEW_TO_CLARIFY) {
2325
+ if (next.review_verdict !== 'request-changes' || next.rollback_target !== STAGES.CLARIFY) {
2326
+ throw new Error('review_clarify_fix_not_requested');
2327
+ }
1124
2328
  if (!next.rollback_rationale) {
1125
2329
  throw new Error('rollback_rationale_required');
1126
2330
  }
@@ -1130,6 +2334,65 @@ export async function approveStage(cwd, slug, { from, to }) {
1130
2334
  throw new Error('review_not_approved');
1131
2335
  }
1132
2336
 
2337
+ if (transition === TRANSITIONS.REVIEW_TO_DONE) {
2338
+ const executionSummary = await readExecutionRecordSummary(root);
2339
+ const scopeGate = executionScopeGate(executionSummary.meta);
2340
+ if (!scopeGate.ok) {
2341
+ const blocked = withRecommendedAction({
2342
+ ...next,
2343
+ stage_status: 'blocked',
2344
+ pending_user_decision: TRANSITIONS.REVIEW_TO_BUILD,
2345
+ requested_transition: TRANSITIONS.REVIEW_TO_BUILD,
2346
+ review_verdict: 'request-changes',
2347
+ rollback_target: STAGES.BUILD,
2348
+ rollback_rationale: 'execution-record.md scope gate blocked review -> done because remaining workflow scope is declared.',
2349
+ plan_blockers: dedupeStrings([...(next.plan_blockers || []), ...scopeGate.blockers]),
2350
+ approval: {
2351
+ ...next.approval,
2352
+ build: APPROVAL_STATES.REQUESTED,
2353
+ complete: APPROVAL_STATES.NOT_REQUESTED,
2354
+ },
2355
+ });
2356
+ await writeState(root, blocked);
2357
+ throw new Error(`review_done_scope_blocked:${scopeGate.blockers.join(',')}`);
2358
+ }
2359
+ let doneJournal = null;
2360
+ let doneJournalWarning = null;
2361
+ if (next.workspace_journal_status !== 'written' || !next.workspace_journal_path) {
2362
+ try {
2363
+ doneJournal = await writeReviewJournal({
2364
+ cwd,
2365
+ slug: state.slug,
2366
+ verdict: 'APPROVE',
2367
+ reviewMessageZh: `Review 结果:${state.slug} 已批准完成,工作流进入 done。`,
2368
+ evidenceManifest: [],
2369
+ findings: [],
2370
+ followUps: ['工作流已完成。'],
2371
+ });
2372
+ } catch (error) {
2373
+ doneJournalWarning = error instanceof Error ? error.message : String(error);
2374
+ }
2375
+ }
2376
+ next = withRecommendedAction({
2377
+ ...next,
2378
+ current_stage: STAGES.DONE,
2379
+ stage_status: 'completed',
2380
+ pending_user_decision: TRANSITIONS.NONE,
2381
+ requested_transition: TRANSITIONS.NONE,
2382
+ last_confirmed_transition: TRANSITIONS.REVIEW_TO_DONE,
2383
+ completion_confirmed: true,
2384
+ workspace_journal_status: doneJournal ? 'written' : (next.workspace_journal_status || 'failed'),
2385
+ workspace_journal_path: doneJournal?.journalPath || next.workspace_journal_path || null,
2386
+ workspace_journal_error: doneJournalWarning || next.workspace_journal_error || null,
2387
+ approval: {
2388
+ ...next.approval,
2389
+ [approvalKey]: APPROVAL_STATES.APPROVED,
2390
+ },
2391
+ });
2392
+ await writeState(root, next);
2393
+ return { root, state: next };
2394
+ }
2395
+
1133
2396
  next = withRecommendedAction({
1134
2397
  ...next,
1135
2398
  stage_status: 'awaiting-approval',
@@ -1144,6 +2407,79 @@ export async function approveStage(cwd, slug, { from, to }) {
1144
2407
  return { root, state: next };
1145
2408
  }
1146
2409
 
2410
+ export async function archiveStage(cwd, slug) {
2411
+ const { root, state, slug: normalized } = await loadWorkflowState(cwd, slug, { allowLegacy: false });
2412
+ if (state.current_stage !== STAGES.DONE || !state.completion_confirmed) {
2413
+ throw new Error('archive_requires_done_workflow');
2414
+ }
2415
+ const executionSummary = await readExecutionRecordSummary(root);
2416
+ const scopeGate = executionScopeGate(executionSummary.meta);
2417
+ if (!scopeGate.ok) {
2418
+ const blocked = withRecommendedAction({
2419
+ ...state,
2420
+ archive_status: 'blocked',
2421
+ plan_blockers: dedupeStrings([...(state.plan_blockers || []), ...scopeGate.blockers]),
2422
+ });
2423
+ await writeState(root, blocked);
2424
+ throw new Error(`archive_scope_blocked:${scopeGate.blockers.join(',')}`);
2425
+ }
2426
+ const effectiveChangeArtifactPaths = await ensureArchiveSlicesArtifact(cwd, root, normalized, state);
2427
+ const effectiveState = {
2428
+ ...state,
2429
+ change_artifact_paths: effectiveChangeArtifactPaths,
2430
+ slice_artifacts_status: effectiveChangeArtifactPaths?.slices && existsSync(effectiveChangeArtifactPaths.slices) ? 'complete' : state.slice_artifacts_status,
2431
+ };
2432
+ const changeStatus = await readChangeArtifactStatus(effectiveState.change_artifact_paths);
2433
+ if (changeStatus.blockers.length > 0) {
2434
+ const blocked = withRecommendedAction({
2435
+ ...effectiveState,
2436
+ archive_status: 'blocked',
2437
+ spec_sync_status: changeStatus.specDeltaStatus,
2438
+ plan_blockers: [...(effectiveState.plan_blockers || []), ...changeStatus.blockers],
2439
+ });
2440
+ await writeState(root, blocked);
2441
+ throw new Error(`archive_blocked:${changeStatus.blockers.join(',')}`);
2442
+ }
2443
+
2444
+ const changeId = normalizeSlug(effectiveState.change_id || changeIdForWorkflowSlug(normalized));
2445
+ const archivedSpecPaths = await mergeSpecDeltaIntoLongLivedSpecs(cwd, changeId, effectiveState.change_artifact_paths.specDelta);
2446
+ const adrCandidatePath = await writeAdrCandidate(cwd, changeId, effectiveState, archivedSpecPaths);
2447
+ const archiveRoot = resolveArchivedChangeRoot(cwd, changeId);
2448
+ await ensureDir(dirname(archiveRoot));
2449
+ if (effectiveState.change_artifact_paths.root === archiveRoot) {
2450
+ // Already archived; keep paths stable and use merge as an idempotent re-sync.
2451
+ } else if (existsSync(archiveRoot)) {
2452
+ await cp(effectiveState.change_artifact_paths.root, archiveRoot, { recursive: true, force: true });
2453
+ } else {
2454
+ await rename(effectiveState.change_artifact_paths.root, archiveRoot);
2455
+ }
2456
+ const archivedPaths = {
2457
+ ...effectiveState.change_artifact_paths,
2458
+ root: archiveRoot,
2459
+ proposal: join(archiveRoot, 'proposal.md'),
2460
+ specDelta: join(archiveRoot, 'spec-delta.md'),
2461
+ design: join(archiveRoot, 'design.md'),
2462
+ tasks: join(archiveRoot, 'tasks.md'),
2463
+ slices: join(archiveRoot, 'slices.json'),
2464
+ graph: join(archiveRoot, 'artifact-graph.json'),
2465
+ };
2466
+ const next = withRecommendedAction({
2467
+ ...effectiveState,
2468
+ archive_status: 'archived',
2469
+ spec_sync_status: 'synced',
2470
+ spec_delta_status: 'complete',
2471
+ slice_artifacts_status: 'complete',
2472
+ change_id: changeId,
2473
+ change_artifacts_status: 'archived',
2474
+ archived_change_path: archiveRoot,
2475
+ archived_spec_paths: archivedSpecPaths,
2476
+ adr_candidate_path: adrCandidatePath,
2477
+ change_artifact_paths: archivedPaths,
2478
+ });
2479
+ await writeState(root, next);
2480
+ return { root, state: next };
2481
+ }
2482
+
1147
2483
  export async function planStage(cwd, slug, options = {}) {
1148
2484
  let normalized = slug ? normalizeSlug(slug) : null;
1149
2485
  if (options.directSpecPath) {
@@ -1154,9 +2490,20 @@ export async function planStage(cwd, slug, options = {}) {
1154
2490
  const loaded = await loadWorkflowState(cwd, normalized, { allowLegacy: false });
1155
2491
  const { root } = loaded;
1156
2492
  let { state } = loaded;
2493
+ const consumesReviewPlan = state.current_stage === STAGES.REVIEW
2494
+ && state.requested_transition === TRANSITIONS.REVIEW_TO_PLAN
2495
+ && state.approval.rollback === APPROVAL_STATES.APPROVED
2496
+ && state.review_verdict === 'request-changes';
2497
+ const resumesConsumedReviewPlan = state.current_stage === STAGES.PLAN
2498
+ && state.last_confirmed_transition === TRANSITIONS.REVIEW_TO_PLAN
2499
+ && state.approval.rollback === APPROVAL_STATES.APPROVED;
1157
2500
  if (!options.directSpecPath) {
1158
- ensureApprovedTransition(state, TRANSITIONS.CLARIFY_TO_PLAN, 'plan');
1159
- if (state.spec_artifact_path) {
2501
+ if (consumesReviewPlan || resumesConsumedReviewPlan) {
2502
+ // A no-go review may route back to plan; the printed Next command is $plan.
2503
+ } else {
2504
+ ensureApprovedTransition(state, TRANSITIONS.CLARIFY_TO_PLAN, 'plan');
2505
+ }
2506
+ if (!consumesReviewPlan && !resumesConsumedReviewPlan && state.spec_artifact_path) {
1160
2507
  await copyArtifact(root, state.spec_artifact_path, 'spec.md');
1161
2508
  }
1162
2509
  }
@@ -1180,8 +2527,11 @@ export async function planStage(cwd, slug, options = {}) {
1180
2527
  deliberateMode: Boolean(options.deliberate),
1181
2528
  interactiveMode: Boolean(options.interactive),
1182
2529
  });
1183
- const docPaths = await writePlanArtifacts(root, cwd, normalized, plannerDraft);
2530
+ await writePlanArtifacts(root, cwd, normalized, plannerDraft);
1184
2531
  const artifactPaths = await writeCanonicalPlanArtifacts(cwd, root, normalized);
2532
+ const changeId = state.change_id || changeIdForWorkflowSlug(normalized);
2533
+ const changeArtifactPaths = await writeChangeArtifacts(cwd, root, normalized, sourceText, plannerDraft, changeId);
2534
+ const changeArtifactStatus = await readChangeArtifactStatus(changeArtifactPaths);
1185
2535
 
1186
2536
  architectReview = await adapter.architect({
1187
2537
  cwd,
@@ -1221,18 +2571,23 @@ export async function planStage(cwd, slug, options = {}) {
1221
2571
  plan_verification_steps_resolved: criticReview.verificationStepsResolved,
1222
2572
  plan_execution_inputs_resolved: criticReview.executionInputsResolved,
1223
2573
  plan_package_status: 'complete',
1224
- plan_docs_artifact_paths: docPaths,
2574
+ plan_docs_artifact_paths: null,
1225
2575
  plan_review_artifact_paths: reviewArtifactPaths,
1226
2576
  plan_artifact_path: artifactPaths.planPath,
1227
2577
  test_spec_artifact_path: artifactPaths.testSpecPath,
2578
+ change_id: normalizeSlug(changeId),
2579
+ change_artifacts_status: changeArtifactStatus.status,
2580
+ change_artifact_paths: changeArtifactPaths,
2581
+ spec_delta_status: changeArtifactStatus.specDeltaStatus,
2582
+ slice_artifacts_status: changeArtifactStatus.sliceArtifactsStatus,
1228
2583
  plan_source_spec_path: sourceSpecPath,
1229
- last_confirmed_transition: TRANSITIONS.CLARIFY_TO_PLAN,
2584
+ last_confirmed_transition: consumesReviewPlan || resumesConsumedReviewPlan ? TRANSITIONS.REVIEW_TO_PLAN : TRANSITIONS.CLARIFY_TO_PLAN,
1230
2585
  approval: {
1231
2586
  ...state.approval,
1232
2587
  plan: APPROVAL_STATES.APPROVED,
1233
2588
  build: APPROVAL_STATES.NOT_REQUESTED,
1234
2589
  review: APPROVAL_STATES.NOT_REQUESTED,
1235
- rollback: APPROVAL_STATES.NOT_REQUESTED,
2590
+ rollback: consumesReviewPlan || resumesConsumedReviewPlan ? APPROVAL_STATES.APPROVED : APPROVAL_STATES.NOT_REQUESTED,
1236
2591
  complete: APPROVAL_STATES.NOT_REQUESTED,
1237
2592
  },
1238
2593
  };
@@ -1244,23 +2599,54 @@ export async function planStage(cwd, slug, options = {}) {
1244
2599
  }
1245
2600
 
1246
2601
  const completion = await readPlanCompletion(cwd, root, normalized, state);
2602
+ const buildManifest = completion.blockers.length > 0
2603
+ ? null
2604
+ : await generateBuildContextManifest({ cwd, root, state, slug: normalized });
1247
2605
  const next = withRecommendedAction({
1248
2606
  ...state,
1249
2607
  current_stage: STAGES.PLAN,
1250
2608
  stage_status: completion.blockers.length > 0 ? 'blocked' : 'awaiting-approval',
1251
- pending_user_decision: TRANSITIONS.NONE,
2609
+ pending_user_decision: completion.blockers.length > 0 ? TRANSITIONS.NONE : TRANSITIONS.PLAN_TO_BUILD,
1252
2610
  requested_transition: TRANSITIONS.NONE,
1253
2611
  plan_docs_status: completion.docsStatus,
1254
- plan_docs_artifact_paths: completion.docPaths,
2612
+ plan_docs_artifact_paths: null,
2613
+ change_artifacts_status: completion.changeArtifactsStatus,
2614
+ spec_delta_status: completion.specDeltaStatus,
2615
+ slice_artifacts_status: completion.sliceArtifactsStatus,
1255
2616
  plan_blockers: completion.blockers,
2617
+ context_manifest_status: buildManifest ? 'hit' : 'fallback',
2618
+ build_context_manifest_path: buildManifest?.path || buildContextManifestPath(root),
1256
2619
  });
1257
2620
  await writeState(root, next);
1258
2621
  return { root, state: next, architectReview, criticReview };
1259
2622
  }
1260
2623
 
1261
2624
  export async function buildStage(cwd, slug, options = {}) {
1262
- const { root, state, slug: normalized } = await loadWorkflowState(cwd, slug, { allowLegacy: false });
1263
- ensureApprovedTransition(state, TRANSITIONS.PLAN_TO_BUILD, 'build');
2625
+ const explicitReviewReworkPath = options.fromReviewPath || (isReviewReworkArtifactInput(slug) ? slug : null);
2626
+ const buildSlug = explicitReviewReworkPath ? slugFromReviewReworkInput(explicitReviewReworkPath) : slugFromBuildInput(slug);
2627
+ const { root, state, slug: normalized } = await loadWorkflowState(cwd, buildSlug, { allowLegacy: false });
2628
+ const reviewReworkArtifactDisplayPath = explicitReviewReworkPath ? displayPath(cwd, explicitReviewReworkPath) : null;
2629
+ const reviewReworkArtifactResolvedPath = explicitReviewReworkPath ? resolve(cwd, explicitReviewReworkPath) : null;
2630
+ const effectiveReviewReworkArtifactPath = reviewReworkArtifactDisplayPath || state.review_rework_artifact_path || null;
2631
+ if (explicitReviewReworkPath && !existsSync(reviewReworkArtifactResolvedPath)) {
2632
+ throw new Error('build_from_review_artifact_missing');
2633
+ }
2634
+ const consumesReviewBuild = state.current_stage === STAGES.REVIEW
2635
+ && state.review_verdict === 'request-changes'
2636
+ && state.rollback_target === STAGES.BUILD
2637
+ && (
2638
+ state.pending_user_decision === TRANSITIONS.REVIEW_TO_BUILD
2639
+ || state.requested_transition === TRANSITIONS.REVIEW_TO_BUILD
2640
+ || state.approval.build === APPROVAL_STATES.REQUESTED
2641
+ || state.approval.build === APPROVAL_STATES.APPROVED
2642
+ )
2643
+ && Boolean(explicitReviewReworkPath);
2644
+ const resumesConsumedReviewBuild = state.current_stage === STAGES.BUILD
2645
+ && state.last_confirmed_transition === TRANSITIONS.REVIEW_TO_BUILD
2646
+ && state.approval.build === APPROVAL_STATES.APPROVED;
2647
+ if (!consumesReviewBuild && !resumesConsumedReviewBuild) {
2648
+ ensureApprovedTransition(state, TRANSITIONS.PLAN_TO_BUILD, 'build');
2649
+ }
1264
2650
  if (!PLAN_ARTIFACTS.every((name) => existsSync(artifactPath(root, name)))) {
1265
2651
  throw new Error('build_requires_workflow_plan_artifacts');
1266
2652
  }
@@ -1273,11 +2659,70 @@ export async function buildStage(cwd, slug, options = {}) {
1273
2659
  const noDeslop = Boolean(options.noDeslop);
1274
2660
  const progressArtifacts = [];
1275
2661
  const supportArtifacts = [];
2662
+ const ownerId = buildOwnerId(normalized);
1276
2663
  let iteration = 1;
1277
2664
  let current = null;
1278
2665
  let blockers = ['build_not_started'];
2666
+ let delegationLedger = null;
2667
+ let completionAudit = null;
2668
+ let delegationLedgerPath = resolveBuildSupportPaths(root, 1).delegationLedger;
2669
+ let completionAuditPath = resolveBuildSupportPaths(root, 1).completionAudit;
2670
+ if (consumesReviewBuild || resumesConsumedReviewBuild) {
2671
+ await generateBuildContextManifest({
2672
+ cwd,
2673
+ root,
2674
+ state: {
2675
+ ...state,
2676
+ current_stage: STAGES.BUILD,
2677
+ last_confirmed_transition: TRANSITIONS.REVIEW_TO_BUILD,
2678
+ review_rework_artifact_path: reviewReworkArtifactResolvedPath || state.review_rework_artifact_path || artifactPath(root, 'review-report.md'),
2679
+ },
2680
+ slug: normalized,
2681
+ });
2682
+ }
2683
+ const buildManifest = await readContextManifest(buildContextManifestPath(root), { cwd });
2684
+ ensureValidContextManifest(buildManifest, STAGES.BUILD);
2685
+ const contextManifestStatus = buildManifest.status;
2686
+
2687
+ await writeBuildActiveState(cwd, {
2688
+ active: true,
2689
+ slug: normalized,
2690
+ phase: 'starting',
2691
+ iteration: 0,
2692
+ max_iterations: maxIterations,
2693
+ review_handoff_ready: false,
2694
+ blockers,
2695
+ build_owner_id: ownerId,
2696
+ build_owner_session_id: buildOwnerSessionId(normalized, null),
2697
+ delegation_ledger_path: displayPath(cwd, delegationLedgerPath),
2698
+ active_delegation_count: 0,
2699
+ completion_audit_path: displayPath(cwd, completionAuditPath),
2700
+ completion_audit_status: 'pending',
2701
+ next_action: 'Run build execution lanes and write execution-record.md.',
2702
+ completion_signal: 'Build may stop only after execution-record.md is complete and build -> review handoff readiness is reached, or after a real blocker is recorded.',
2703
+ workflow_root: root,
2704
+ execution_record_path: artifactPath(root, 'execution-record.md'),
2705
+ started_at: nowIso(),
2706
+ });
1279
2707
 
1280
2708
  while (iteration <= maxIterations) {
2709
+ await writeBuildActiveState(cwd, {
2710
+ active: true,
2711
+ slug: normalized,
2712
+ phase: 'executing',
2713
+ iteration,
2714
+ max_iterations: maxIterations,
2715
+ review_handoff_ready: false,
2716
+ blockers,
2717
+ build_owner_id: ownerId,
2718
+ build_owner_session_id: buildOwnerSessionId(normalized, null),
2719
+ delegation_ledger_path: displayPath(cwd, delegationLedgerPath),
2720
+ active_delegation_count: delegationLedger?.active_blocking_count || 0,
2721
+ completion_audit_path: displayPath(cwd, completionAuditPath),
2722
+ completion_audit_status: completionAudit?.status || 'pending',
2723
+ next_action: 'Continue $build execution and gather fresh implementation evidence.',
2724
+ completion_signal: 'Build may stop only after execution-record.md is complete and build -> review handoff readiness is reached, or after a real blocker is recorded.',
2725
+ });
1281
2726
  current = await adapter.executeLanes({
1282
2727
  cwd,
1283
2728
  root,
@@ -1286,11 +2731,70 @@ export async function buildStage(cwd, slug, options = {}) {
1286
2731
  noDeslop,
1287
2732
  planArtifactPath: state.plan_artifact_path,
1288
2733
  testSpecArtifactPath: state.test_spec_artifact_path,
2734
+ reviewReworkArtifactPath: reviewReworkArtifactDisplayPath || state.review_rework_artifact_path || null,
2735
+ contextManifestPath: buildContextManifestPath(root),
2736
+ contextManifestRows: buildManifest.rows,
2737
+ contextManifestStatus,
2738
+ });
2739
+ const supportPaths = resolveBuildSupportPaths(root, current.iteration);
2740
+ delegationLedgerPath = supportPaths.delegationLedger;
2741
+ completionAuditPath = supportPaths.completionAudit;
2742
+ delegationLedger = buildDelegationLedger({
2743
+ slug: normalized,
2744
+ ownerId,
2745
+ ownerSessionId: buildOwnerSessionId(normalized, current?.runId || null),
2746
+ iterationData: current,
2747
+ previousLedger: delegationLedger,
2748
+ });
2749
+ const baseBlockers = buildIterationBlockers(current, { noDeslop });
2750
+ completionAudit = await buildCompletionAudit({
2751
+ cwd,
2752
+ root,
2753
+ slug: normalized,
2754
+ state,
2755
+ reviewReworkArtifactPath: effectiveReviewReworkArtifactPath,
2756
+ iterationData: current,
2757
+ ledger: delegationLedger,
2758
+ baseBlockers,
2759
+ });
2760
+ const auditBlocksHandoff = !completionAudit.passed
2761
+ && baseBlockers.length === 0;
2762
+ blockers = dedupeStrings([
2763
+ ...baseBlockers,
2764
+ ...buildDelegationBlockers(delegationLedger),
2765
+ ...(auditBlocksHandoff ? ['completion_audit_blocked'] : []),
2766
+ ]);
2767
+ await writeBuildActiveState(cwd, {
2768
+ active: true,
2769
+ slug: normalized,
2770
+ phase: blockers.length === 0 ? 'verifying' : 'fixing',
2771
+ iteration,
2772
+ max_iterations: maxIterations,
2773
+ review_handoff_ready: false,
2774
+ blockers,
2775
+ build_owner_id: ownerId,
2776
+ build_owner_session_id: buildOwnerSessionId(normalized, current?.runId || null),
2777
+ delegation_ledger_path: displayPath(cwd, delegationLedgerPath),
2778
+ active_delegation_count: delegationLedger.active_blocking_count,
2779
+ completion_audit_path: displayPath(cwd, completionAuditPath),
2780
+ completion_audit_status: completionAudit.status,
2781
+ next_action: blockers.length === 0
2782
+ ? 'Verify execution evidence and prepare build -> review handoff.'
2783
+ : 'Continue $build to resolve blockers before review handoff.',
2784
+ completion_signal: 'Build may stop only after execution-record.md is complete and build -> review handoff readiness is reached, or after a real blocker is recorded.',
1289
2785
  });
1290
- blockers = buildIterationBlockers(current, { noDeslop });
1291
- const supportPaths = await writeBuildSupportArtifacts(root, current, noDeslop);
1292
- progressArtifacts.push(supportPaths.laneSummary);
1293
- supportArtifacts.push(supportPaths.architect, supportPaths.deslop, supportPaths.regression);
2786
+ const writtenSupportPaths = await writeBuildSupportArtifacts(root, current, noDeslop, {
2787
+ delegationLedger,
2788
+ completionAudit,
2789
+ });
2790
+ progressArtifacts.push(writtenSupportPaths.laneSummary);
2791
+ supportArtifacts.push(
2792
+ writtenSupportPaths.architect,
2793
+ writtenSupportPaths.deslop,
2794
+ writtenSupportPaths.regression,
2795
+ writtenSupportPaths.delegationLedger,
2796
+ writtenSupportPaths.completionAudit,
2797
+ );
1294
2798
  await writeText(
1295
2799
  artifactPath(root, 'execution-record.md'),
1296
2800
  buildExecutionRecordContent({
@@ -1302,10 +2806,16 @@ export async function buildStage(cwd, slug, options = {}) {
1302
2806
  if (blockers.length === 0) {
1303
2807
  break;
1304
2808
  }
2809
+ if (buildHasInfrastructureFailure(current)) {
2810
+ break;
2811
+ }
1305
2812
  iteration += 1;
1306
2813
  }
1307
2814
 
1308
2815
  const finalBlocked = blockers.length > 0;
2816
+ const reviewManifest = finalBlocked
2817
+ ? null
2818
+ : await generateReviewContextManifest({ cwd, root, state, slug: normalized });
1309
2819
  const refreshed = await refreshExecutionStatus(root, state);
1310
2820
  const next = withRecommendedAction({
1311
2821
  ...refreshed.state,
@@ -1313,6 +2823,7 @@ export async function buildStage(cwd, slug, options = {}) {
1313
2823
  stage_status: finalBlocked ? 'blocked' : 'awaiting-approval',
1314
2824
  execution_record_status: finalBlocked ? 'partial' : refreshed.state.execution_record_status,
1315
2825
  review_status: finalBlocked ? 'pending-input' : 'ready-for-review',
2826
+ review_handoff_ready: !finalBlocked,
1316
2827
  build_run_id: current?.runId || null,
1317
2828
  build_current_iteration: current?.iteration || 0,
1318
2829
  build_max_iterations: maxIterations,
@@ -1326,10 +2837,28 @@ export async function buildStage(cwd, slug, options = {}) {
1326
2837
  build_progress_artifact_paths: progressArtifacts,
1327
2838
  build_support_evidence_paths: supportArtifacts,
1328
2839
  build_no_deslop: noDeslop,
2840
+ build_owner_id: ownerId,
2841
+ build_owner_session_id: buildOwnerSessionId(normalized, current?.runId || null),
2842
+ build_owner_status: finalBlocked ? 'blocked' : 'review-ready',
2843
+ build_delegation_status: delegationLedger?.status || 'drained',
2844
+ build_delegation_ledger_path: delegationLedgerPath,
2845
+ build_active_delegation_count: delegationLedger?.active_blocking_count || 0,
2846
+ build_completion_audit_status: completionAudit?.status || (finalBlocked ? 'blocked' : 'passed'),
2847
+ build_completion_audit_path: completionAuditPath,
2848
+ review_rework_artifact_path: reviewReworkArtifactDisplayPath || state.review_rework_artifact_path || null,
2849
+ context_manifest_status: contextManifestStatus,
2850
+ build_context_manifest_path: buildContextManifestPath(root),
2851
+ review_context_manifest_path: reviewManifest?.path || reviewContextManifestPath(root),
1329
2852
  active_run_id: current?.runId || null,
1330
2853
  pending_user_decision: finalBlocked ? TRANSITIONS.NONE : TRANSITIONS.BUILD_TO_REVIEW,
1331
2854
  requested_transition: TRANSITIONS.NONE,
1332
- last_confirmed_transition: TRANSITIONS.PLAN_TO_BUILD,
2855
+ last_confirmed_transition: consumesReviewBuild || resumesConsumedReviewBuild ? TRANSITIONS.REVIEW_TO_BUILD : TRANSITIONS.PLAN_TO_BUILD,
2856
+ review_verdict: 'none',
2857
+ rollback_target: null,
2858
+ rollback_rationale: null,
2859
+ workspace_journal_path: null,
2860
+ workspace_journal_status: 'skipped',
2861
+ workspace_journal_error: null,
1333
2862
  approval: {
1334
2863
  ...state.approval,
1335
2864
  build: APPROVAL_STATES.APPROVED,
@@ -1339,39 +2868,114 @@ export async function buildStage(cwd, slug, options = {}) {
1339
2868
  },
1340
2869
  });
1341
2870
  await writeState(root, next);
2871
+ await writeBuildActiveState(cwd, {
2872
+ active: false,
2873
+ slug: normalized,
2874
+ phase: finalBlocked ? 'blocked' : 'review-ready',
2875
+ iteration: current?.iteration || 0,
2876
+ max_iterations: maxIterations,
2877
+ review_handoff_ready: !finalBlocked,
2878
+ blockers,
2879
+ build_owner_id: ownerId,
2880
+ build_owner_session_id: buildOwnerSessionId(normalized, current?.runId || null),
2881
+ delegation_ledger_path: displayPath(cwd, delegationLedgerPath),
2882
+ active_delegation_count: delegationLedger?.active_blocking_count || 0,
2883
+ completion_audit_path: displayPath(cwd, completionAuditPath),
2884
+ completion_audit_status: completionAudit?.status || (finalBlocked ? 'blocked' : 'passed'),
2885
+ next_action: finalBlocked ? 'Run $build again after resolving recorded blockers.' : 'Approve build -> review and run $review.',
2886
+ completion_signal: finalBlocked ? 'Build is stopped because real blockers remain recorded.' : 'execution-record.md is complete and build -> review handoff is ready.',
2887
+ execution_record_status: next.execution_record_status,
2888
+ execution_record_path: artifactPath(root, 'execution-record.md'),
2889
+ completed_at: nowIso(),
2890
+ });
1342
2891
  return { root, state: next };
1343
2892
  }
1344
2893
 
1345
- function reviewFindings({ executionMeta, executionStatus, reviewer }) {
1346
- const inputManifest = ['spec.md', ...PLAN_ARTIFACTS, 'execution-record.md'];
2894
+ function reviewFindings({ executionMeta, executionStatus, reviewer, codeReview, architectureReview }) {
2895
+ const inputManifest = ['spec.md', ...PLAN_ARTIFACTS, 'execution-record.md', 'review-support/code-review.json', 'review-support/architecture-smell.json'];
1347
2896
  const evidenceManifest = Array.isArray(executionMeta.evidence_manifest) ? [...executionMeta.evidence_manifest] : [];
2897
+ const scopeGate = executionScopeGate(executionMeta);
1348
2898
  const findings = [];
1349
2899
  let verdict = 'APPROVE';
1350
2900
  let rollbackTarget = 'none';
1351
2901
  let rollbackRationale = null;
1352
2902
 
1353
2903
  if (executionStatus !== 'complete') {
1354
- findings.push('execution-record.md is missing required execution or verification evidence.');
2904
+ findings.push('execution-record.md 缺少必要的执行或验证证据。');
1355
2905
  verdict = 'REQUEST CHANGES';
1356
2906
  rollbackTarget = 'plan';
1357
- rollbackRationale = 'Execution evidence is incomplete, so the workflow must return to planning before another run.';
2907
+ rollbackRationale = '执行证据不完整,工作流需要回退到计划阶段后再重新执行。';
1358
2908
  }
1359
2909
  if (!Array.isArray(executionMeta.evidence_manifest) || executionMeta.evidence_manifest.length === 0) {
1360
- findings.push('execution-record.md is missing the required evidence_manifest schema.');
2910
+ findings.push('execution-record.md 缺少必需的 evidence_manifest 结构。');
1361
2911
  verdict = 'REQUEST CHANGES';
1362
2912
  rollbackTarget = 'plan';
1363
- rollbackRationale = 'Execution evidence schema is incomplete, so review cannot accept the run.';
2913
+ rollbackRationale = '执行证据结构不完整,review 不能接受本次运行。';
1364
2914
  }
1365
2915
  if (executionMeta.actor_id === reviewer) {
1366
- findings.push('Reviewer provenance matches the execution actor and is not independent.');
2916
+ findings.push('Reviewer 来源与执行者一致,不满足独立审查要求。');
1367
2917
  verdict = 'REQUEST CHANGES';
1368
2918
  rollbackTarget = 'plan';
1369
- rollbackRationale = 'Review independence failed because reviewer provenance matches the execution actor.';
2919
+ rollbackRationale = 'review 独立性校验失败,因为 reviewer 与执行者来源一致。';
2920
+ }
2921
+ if (!scopeGate.ok) {
2922
+ findings.push(`execution-record.md 声明只完成了部分 scope,不能批准完整工作流完成:${scopeGate.blockers.join(', ')}`);
2923
+ if (scopeGate.plannedScope) {
2924
+ findings.push(`planned_scope=${scopeGate.plannedScope}`);
2925
+ }
2926
+ if (scopeGate.implementedScope) {
2927
+ findings.push(`implemented_scope=${scopeGate.implementedScope}`);
2928
+ }
2929
+ if (scopeGate.remainingScope.length > 0) {
2930
+ findings.push(`remaining_scope=${scopeGate.remainingScope.join(', ')}`);
2931
+ }
2932
+ verdict = 'REQUEST CHANGES';
2933
+ if (rollbackTarget === 'none') {
2934
+ rollbackTarget = STAGES.BUILD;
2935
+ rollbackRationale = '执行记录显示当前 build 只完成了部分 scope,需要回到 build 继续执行剩余工作,或回到 plan 重新拆分独立 slice。';
2936
+ }
2937
+ }
2938
+ if (codeReview?.status === 'skipped') {
2939
+ findings.push(`代码审查已跳过:${codeReview.summary}`);
2940
+ }
2941
+ if (codeReview?.verdict === 'request-changes') {
2942
+ findings.push(`代码审查发现阻断问题:${codeReview.summary}`);
2943
+ for (const finding of codeReview.findings || []) {
2944
+ findings.push(codeReviewFindingText(finding));
2945
+ }
2946
+ verdict = 'REQUEST CHANGES';
2947
+ if (rollbackTarget === 'none' || rollbackTarget === STAGES.BUILD) {
2948
+ rollbackTarget = codeReview.rollbackTarget || STAGES.BUILD;
2949
+ }
2950
+ rollbackRationale = rollbackTarget === STAGES.BUILD
2951
+ ? '代码审查发现实现问题,需要回到 build 阶段修复后重新 review。'
2952
+ : rollbackTarget === STAGES.CLARIFY
2953
+ ? '代码审查暴露需求歧义,需要回到 clarify 阶段重新澄清。'
2954
+ : '代码审查发现计划或架构问题,需要回到 plan 阶段修订后重新执行。';
2955
+ }
2956
+ if (architectureReview?.verdict === 'warn') {
2957
+ findings.push(`架构 smell 扫描提示风险:${architectureReview.summary}`);
2958
+ for (const finding of architectureReview.findings || []) {
2959
+ findings.push(architectureReviewFindingText(finding));
2960
+ }
2961
+ }
2962
+ if (architectureReview?.verdict === 'block') {
2963
+ findings.push(`架构 smell 扫描发现阻断问题:${architectureReview.summary}`);
2964
+ for (const finding of architectureReview.findings || []) {
2965
+ findings.push(architectureReviewFindingText(finding));
2966
+ }
2967
+ verdict = 'REQUEST CHANGES';
2968
+ rollbackTarget = architectureReview.rollbackTarget || STAGES.PLAN;
2969
+ rollbackRationale = rollbackTarget === STAGES.BUILD
2970
+ ? '架构 smell 扫描发现实现边界问题,需要回到 build 阶段修复后重新 review。'
2971
+ : rollbackTarget === STAGES.CLARIFY
2972
+ ? '架构 smell 扫描暴露需求或领域语言歧义,需要回到 clarify 阶段重新澄清。'
2973
+ : '架构 smell 扫描发现计划或模块 seam 问题,需要回到 plan 阶段修订。';
1370
2974
  }
1371
2975
 
1372
2976
  return {
1373
2977
  verdict,
1374
- findings: findings.length > 0 ? findings : ['Structured evidence and provenance checks passed.'],
2978
+ findings: findings.length > 0 ? findings : ['结构化证据与来源独立性检查均已通过。'],
1375
2979
  inputManifest,
1376
2980
  evidenceManifest,
1377
2981
  rollbackTarget,
@@ -1379,8 +2983,15 @@ function reviewFindings({ executionMeta, executionStatus, reviewer }) {
1379
2983
  };
1380
2984
  }
1381
2985
 
1382
- export async function reviewStage(cwd, slug, { reviewer = 'independent-reviewer' } = {}) {
1383
- const { root, state, slug: normalized } = await loadWorkflowState(cwd, slug, { allowLegacy: false });
2986
+ export async function reviewStage(cwd, slug, { reviewer = 'independent-reviewer', adapter } = {}) {
2987
+ const reviewSlug = String(slug || '').endsWith('execution-record.md')
2988
+ ? basename(dirname(resolve(cwd, slug)))
2989
+ : slug;
2990
+ const { root, state, slug: normalized } = await loadWorkflowState(cwd, reviewSlug, { allowLegacy: false });
2991
+ const rerunsAwaitingCompletionReview = state.current_stage === STAGES.REVIEW
2992
+ && state.review_verdict === 'approve'
2993
+ && state.pending_user_decision === TRANSITIONS.REVIEW_TO_DONE
2994
+ && state.requested_transition === TRANSITIONS.NONE;
1384
2995
 
1385
2996
  if (state.current_stage === STAGES.REVIEW && state.approval.complete === APPROVAL_STATES.APPROVED && state.review_verdict === 'approve') {
1386
2997
  const next = withRecommendedAction({
@@ -1393,36 +3004,194 @@ export async function reviewStage(cwd, slug, { reviewer = 'independent-reviewer'
1393
3004
  completion_confirmed: true,
1394
3005
  });
1395
3006
  await writeState(root, next);
1396
- return { root, state: next, verdict: 'APPROVE', rollbackTarget: 'none' };
3007
+ return {
3008
+ root,
3009
+ state: next,
3010
+ verdict: 'APPROVE',
3011
+ rollbackTarget: 'none',
3012
+ reviewMessageZh: `Review 结果:${normalized} 已完成,工作流已进入 done。`,
3013
+ };
3014
+ }
3015
+
3016
+ if (state.current_stage === STAGES.REVIEW && state.approval.build === APPROVAL_STATES.APPROVED && state.requested_transition === TRANSITIONS.REVIEW_TO_BUILD && state.review_verdict === 'request-changes') {
3017
+ const next = withRecommendedAction({
3018
+ ...state,
3019
+ current_stage: STAGES.BUILD,
3020
+ stage_status: 'pending-rework',
3021
+ review_status: 'pending-fix',
3022
+ pending_user_decision: TRANSITIONS.NONE,
3023
+ requested_transition: TRANSITIONS.NONE,
3024
+ last_confirmed_transition: TRANSITIONS.REVIEW_TO_BUILD,
3025
+ execution_record_status: 'pending-rework',
3026
+ build_verification_status: 'pending',
3027
+ build_architect_verification_status: 'pending',
3028
+ build_deslop_status: state.build_no_deslop ? 'skipped' : 'pending',
3029
+ build_regression_status: state.build_no_deslop ? 'skipped' : 'pending',
3030
+ build_blockers: ['review_rework_required'],
3031
+ approval: {
3032
+ ...state.approval,
3033
+ build: APPROVAL_STATES.APPROVED,
3034
+ review: APPROVAL_STATES.NOT_REQUESTED,
3035
+ rollback: APPROVAL_STATES.NOT_REQUESTED,
3036
+ complete: APPROVAL_STATES.NOT_REQUESTED,
3037
+ },
3038
+ });
3039
+ await writeState(root, next);
3040
+ return {
3041
+ root,
3042
+ state: next,
3043
+ verdict: 'REQUEST CHANGES',
3044
+ rollbackTarget: 'build',
3045
+ reviewMessageZh: `Review 结果:${normalized} 要求修改,已回到 build 阶段。\nNext:\n${reviewReworkBuildCommand(normalized)}`,
3046
+ };
1397
3047
  }
1398
3048
 
1399
- if (state.current_stage === STAGES.REVIEW && state.approval.rollback === APPROVAL_STATES.APPROVED && state.review_verdict === 'request-changes') {
3049
+ if (state.current_stage === STAGES.REVIEW && state.approval.rollback === APPROVAL_STATES.APPROVED && state.requested_transition === TRANSITIONS.REVIEW_TO_PLAN && state.review_verdict === 'request-changes') {
1400
3050
  const next = withRecommendedAction({
1401
3051
  ...state,
1402
3052
  current_stage: STAGES.PLAN,
1403
- stage_status: 'awaiting-approval',
3053
+ stage_status: 'pending-rework',
3054
+ pending_user_decision: TRANSITIONS.NONE,
3055
+ requested_transition: TRANSITIONS.NONE,
3056
+ last_confirmed_transition: TRANSITIONS.REVIEW_TO_PLAN,
3057
+ plan_package_status: 'pending-rework',
3058
+ plan_principles_resolved: false,
3059
+ plan_options_reviewed: false,
3060
+ plan_architect_review_status: 'pending',
3061
+ plan_critic_verdict: 'pending',
3062
+ plan_acceptance_criteria_testable: false,
3063
+ plan_verification_steps_resolved: false,
3064
+ plan_execution_inputs_resolved: false,
3065
+ plan_docs_status: 'pending-rework',
3066
+ plan_blockers: ['review_rework_required'],
3067
+ plan_current_iteration: 0,
3068
+ build_blockers: ['plan_rework_required'],
3069
+ review_status: 'pending-fix',
3070
+ approval: {
3071
+ ...state.approval,
3072
+ plan: APPROVAL_STATES.NOT_REQUESTED,
3073
+ build: APPROVAL_STATES.NOT_REQUESTED,
3074
+ review: APPROVAL_STATES.NOT_REQUESTED,
3075
+ rollback: APPROVAL_STATES.APPROVED,
3076
+ },
3077
+ });
3078
+ await writeState(root, next);
3079
+ return {
3080
+ root,
3081
+ state: next,
3082
+ verdict: 'REQUEST CHANGES',
3083
+ rollbackTarget: 'plan',
3084
+ reviewMessageZh: `Review 结果:${normalized} 要求修改,已回退到 plan 阶段。`,
3085
+ };
3086
+ }
3087
+
3088
+ if (state.current_stage === STAGES.REVIEW && state.approval.rollback === APPROVAL_STATES.APPROVED && state.requested_transition === TRANSITIONS.REVIEW_TO_CLARIFY && state.review_verdict === 'request-changes') {
3089
+ const next = withRecommendedAction({
3090
+ ...state,
3091
+ current_stage: STAGES.CLARIFY,
3092
+ stage_status: 'pending-rework',
3093
+ clarify_ambiguity_score: 1,
3094
+ clarify_pressure_pass_complete: false,
3095
+ clarify_non_goals_resolved: false,
3096
+ clarify_decision_boundaries_resolved: false,
3097
+ unresolved_ambiguity_count: Math.max(1, Number(state.unresolved_ambiguity_count || 0)),
3098
+ plan_package_status: 'pending-rework',
3099
+ plan_principles_resolved: false,
3100
+ plan_options_reviewed: false,
3101
+ plan_architect_review_status: 'pending',
3102
+ plan_critic_verdict: 'pending',
3103
+ plan_acceptance_criteria_testable: false,
3104
+ plan_verification_steps_resolved: false,
3105
+ plan_execution_inputs_resolved: false,
3106
+ plan_docs_status: 'pending-rework',
3107
+ plan_blockers: ['clarify_rework_required'],
3108
+ build_blockers: ['clarify_rework_required'],
3109
+ review_status: 'pending-fix',
1404
3110
  pending_user_decision: TRANSITIONS.NONE,
1405
3111
  requested_transition: TRANSITIONS.NONE,
1406
- last_confirmed_transition: TRANSITIONS.REVIEW_TO_PLAN,
1407
- plan_package_status: 'complete',
3112
+ last_confirmed_transition: TRANSITIONS.REVIEW_TO_CLARIFY,
1408
3113
  approval: {
1409
3114
  ...state.approval,
3115
+ plan: APPROVAL_STATES.NOT_REQUESTED,
1410
3116
  build: APPROVAL_STATES.NOT_REQUESTED,
1411
3117
  review: APPROVAL_STATES.NOT_REQUESTED,
1412
3118
  rollback: APPROVAL_STATES.APPROVED,
3119
+ complete: APPROVAL_STATES.NOT_REQUESTED,
1413
3120
  },
1414
3121
  });
1415
3122
  await writeState(root, next);
1416
- return { root, state: next, verdict: 'REQUEST CHANGES', rollbackTarget: 'plan' };
3123
+ return {
3124
+ root,
3125
+ state: next,
3126
+ verdict: 'REQUEST CHANGES',
3127
+ rollbackTarget: 'clarify',
3128
+ reviewMessageZh: `Review 结果:${normalized} 要求修改,已回到 clarify 阶段。\nNext:\n$clarify ${normalized}`,
3129
+ };
1417
3130
  }
1418
3131
 
1419
- ensureApprovedTransition(state, TRANSITIONS.BUILD_TO_REVIEW, 'review');
3132
+ if (!rerunsAwaitingCompletionReview) {
3133
+ ensureApprovedTransition(state, TRANSITIONS.BUILD_TO_REVIEW, 'review');
3134
+ }
1420
3135
  const { state: refreshed, executionSummary } = await refreshExecutionStatus(root, state);
3136
+ const reviewManifest = await readContextManifest(reviewContextManifestPath(root), { cwd });
3137
+ ensureValidContextManifest(reviewManifest, STAGES.REVIEW);
3138
+ const reviewAdapter = adapter || createDefaultReviewAdapter();
3139
+ let codeReview = null;
3140
+ try {
3141
+ codeReview = await reviewAdapter.codeReview({
3142
+ cwd,
3143
+ root,
3144
+ slug: normalized,
3145
+ reviewer,
3146
+ executionRecordPath: artifactPath(root, 'execution-record.md'),
3147
+ planArtifactPath: refreshed.plan_artifact_path,
3148
+ testSpecArtifactPath: refreshed.test_spec_artifact_path,
3149
+ contextManifestStatus: reviewManifest.status,
3150
+ contextManifestPath: reviewContextManifestPath(root),
3151
+ contextManifestRows: reviewManifest.rows,
3152
+ });
3153
+ } catch (error) {
3154
+ codeReview = codeReviewFailureResult(error);
3155
+ }
3156
+ await ensureDir(join(root, 'review-support'));
3157
+ await writeText(join(root, 'review-support', 'code-review.json'), JSON.stringify(codeReview, null, 2));
3158
+ await writeReviewChangedFiles(root, codeReview?.changedFiles || []);
3159
+ let architectureReview = null;
3160
+ if (reviewAdapter.architectureReview) {
3161
+ try {
3162
+ architectureReview = await reviewAdapter.architectureReview({
3163
+ cwd,
3164
+ root,
3165
+ slug: normalized,
3166
+ reviewer,
3167
+ executionRecordPath: artifactPath(root, 'execution-record.md'),
3168
+ planArtifactPath: refreshed.plan_artifact_path,
3169
+ testSpecArtifactPath: refreshed.test_spec_artifact_path,
3170
+ changeArtifactPaths: refreshed.change_artifact_paths,
3171
+ contextManifestStatus: reviewManifest.status,
3172
+ contextManifestPath: reviewContextManifestPath(root),
3173
+ contextManifestRows: reviewManifest.rows,
3174
+ });
3175
+ } catch (error) {
3176
+ architectureReview = architectureReviewFailureResult(error);
3177
+ }
3178
+ } else {
3179
+ architectureReview = {
3180
+ status: 'complete',
3181
+ verdict: 'pass',
3182
+ summary: '架构 smell 扫描通过。',
3183
+ findings: [],
3184
+ };
3185
+ }
3186
+ await writeText(join(root, 'review-support', 'architecture-smell.json'), JSON.stringify(architectureReview, null, 2));
1421
3187
  const reviewInput = reviewFindings({
1422
3188
  executionMeta: executionSummary.meta,
1423
3189
  executionStatus: refreshed.execution_record_status,
1424
3190
  reviewer,
3191
+ codeReview,
3192
+ architectureReview,
1425
3193
  });
3194
+ reviewInput.inputManifest = manifestRowsToInputManifest(reviewManifest.rows, reviewInput.inputManifest);
1426
3195
  const runId = executionSummary.meta.run_id || refreshed.active_run_id || `${normalized}-unknown-run`;
1427
3196
 
1428
3197
  await writeText(
@@ -1437,29 +3206,74 @@ export async function reviewStage(cwd, slug, { reviewer = 'independent-reviewer'
1437
3206
  inputManifest: reviewInput.inputManifest,
1438
3207
  evidenceManifest: reviewInput.evidenceManifest,
1439
3208
  findings: reviewInput.findings,
3209
+ codeReview,
3210
+ architectureReview,
1440
3211
  }),
1441
3212
  );
1442
3213
 
3214
+ const reviewMessage = reviewUserMessageZh({
3215
+ slug: normalized,
3216
+ verdict: reviewInput.verdict,
3217
+ rollbackTarget: reviewInput.rollbackTarget,
3218
+ findings: reviewInput.findings,
3219
+ });
3220
+ let journal = null;
3221
+ let journalWarning = null;
3222
+ const shouldReuseReviewJournal = reviewInput.verdict === 'APPROVE'
3223
+ && rerunsAwaitingCompletionReview
3224
+ && refreshed.workspace_journal_status === 'written'
3225
+ && refreshed.workspace_journal_path;
3226
+ const shouldWriteReviewJournal = reviewInput.verdict === 'APPROVE' && !shouldReuseReviewJournal;
3227
+ if (shouldReuseReviewJournal) {
3228
+ journal = { journalPath: refreshed.workspace_journal_path };
3229
+ }
3230
+ if (shouldWriteReviewJournal) {
3231
+ try {
3232
+ journal = await writeReviewJournal({
3233
+ cwd,
3234
+ slug: normalized,
3235
+ verdict: reviewInput.verdict,
3236
+ reviewMessageZh: reviewMessage,
3237
+ evidenceManifest: reviewInput.evidenceManifest,
3238
+ followUps: ['等待 review -> done 审批。'],
3239
+ });
3240
+ } catch (error) {
3241
+ journalWarning = error instanceof Error ? error.message : String(error);
3242
+ }
3243
+ }
3244
+
1443
3245
  const next = withRecommendedAction({
1444
3246
  ...refreshed,
1445
3247
  current_stage: STAGES.REVIEW,
1446
3248
  stage_status: 'awaiting-approval',
1447
3249
  review_status: 'in-review',
1448
- pending_user_decision: reviewInput.verdict === 'APPROVE' ? TRANSITIONS.REVIEW_TO_DONE : TRANSITIONS.REVIEW_TO_PLAN,
3250
+ pending_user_decision: reviewInput.verdict === 'APPROVE' ? TRANSITIONS.REVIEW_TO_DONE : transitionForRollbackTarget(reviewInput.rollbackTarget),
1449
3251
  requested_transition: TRANSITIONS.NONE,
1450
3252
  last_confirmed_transition: TRANSITIONS.BUILD_TO_REVIEW,
1451
3253
  review_verdict: reviewInput.verdict === 'APPROVE' ? 'approve' : 'request-changes',
1452
3254
  rollback_target: reviewInput.rollbackTarget,
1453
3255
  rollback_rationale: reviewInput.rollbackRationale,
3256
+ context_manifest_status: reviewManifest.status,
3257
+ review_context_manifest_path: reviewContextManifestPath(root),
3258
+ workspace_journal_status: reviewInput.verdict === 'APPROVE' ? (journal ? 'written' : 'failed') : 'skipped',
3259
+ workspace_journal_path: journal?.journalPath || null,
3260
+ workspace_journal_error: journalWarning,
1454
3261
  approval: {
1455
3262
  ...refreshed.approval,
1456
3263
  review: APPROVAL_STATES.APPROVED,
1457
- rollback: reviewInput.verdict === 'APPROVE' ? APPROVAL_STATES.NOT_REQUESTED : APPROVAL_STATES.REQUESTED,
3264
+ build: reviewInput.verdict === 'REQUEST CHANGES' && reviewInput.rollbackTarget === STAGES.BUILD ? APPROVAL_STATES.REQUESTED : refreshed.approval.build,
3265
+ rollback: reviewInput.verdict === 'APPROVE' || reviewInput.rollbackTarget === STAGES.BUILD ? APPROVAL_STATES.NOT_REQUESTED : APPROVAL_STATES.REQUESTED,
1458
3266
  complete: reviewInput.verdict === 'APPROVE' ? APPROVAL_STATES.REQUESTED : APPROVAL_STATES.NOT_REQUESTED,
1459
3267
  },
1460
3268
  });
1461
3269
  await writeState(root, next);
1462
- return { root, state: next, verdict: reviewInput.verdict, rollbackTarget: reviewInput.rollbackTarget };
3270
+ return {
3271
+ root,
3272
+ state: next,
3273
+ verdict: reviewInput.verdict,
3274
+ rollbackTarget: reviewInput.rollbackTarget,
3275
+ reviewMessageZh: `${reviewMessage} 代码审查:${codeReview.summary} 架构扫描:${architectureReview.summary}${journalWarning ? ` journal 写入失败:${journalWarning}` : ''}`,
3276
+ };
1463
3277
  }
1464
3278
 
1465
3279
  async function writeAutopilotRun(rootPath, payload) {
@@ -1616,9 +3430,8 @@ export async function autopilotStage(cwd, slug, { reviewer = 'autopilot-reviewer
1616
3430
  });
1617
3431
  throw new Error('autopilot_review_failed');
1618
3432
  }
1619
- await approveStage(cwd, normalized, { from: STAGES.REVIEW, to: STAGES.DONE });
3433
+ const done = await approveStage(cwd, normalized, { from: STAGES.REVIEW, to: STAGES.DONE });
1620
3434
  recordEvent(TRANSITIONS.REVIEW_TO_DONE);
1621
- const done = await reviewStage(cwd, normalized, { reviewer });
1622
3435
  await persistRun({
1623
3436
  currentPhase: 'complete',
1624
3437
  completed: true,
@@ -1647,6 +3460,7 @@ async function listWorkflowSummaries(workflowsRoot) {
1647
3460
  workflows.push({
1648
3461
  slug,
1649
3462
  current_stage: state?.current_stage ?? null,
3463
+ archive_status: state?.archive_status ?? null,
1650
3464
  contract: legacy ? 'legacy-codex-helper' : 'loopx-v1',
1651
3465
  legacy,
1652
3466
  schema_version: state?.schema_version ?? 0,
@@ -1678,6 +3492,8 @@ export async function statusSummary(cwd, slug) {
1678
3492
  const initialized = existsSync(workspaceRoot);
1679
3493
  const config = await readWorkspaceConfig(cwd);
1680
3494
  const workflowsRoot = join(workspaceRoot, 'workflows');
3495
+ const { hook } = await doctorRuntime(cwd);
3496
+ const contextSetup = await inspectWorkspaceContext(cwd);
1681
3497
 
1682
3498
  if (!slug) {
1683
3499
  const workflows = await listWorkflowSummaries(workflowsRoot);
@@ -1688,6 +3504,8 @@ export async function statusSummary(cwd, slug) {
1688
3504
  workflows,
1689
3505
  workflow_count: workflows.length,
1690
3506
  summary: summarizeWorkspace(workflows),
3507
+ hook,
3508
+ contextSetup,
1691
3509
  next_action: initialized ? 'Run loopx clarify <slug> to start a workflow, or inspect one with loopx status <slug>.' : 'Run loopx init to prepare the workspace.',
1692
3510
  };
1693
3511
  }
@@ -1695,7 +3513,11 @@ export async function statusSummary(cwd, slug) {
1695
3513
  const normalized = normalizeSlug(slug);
1696
3514
  const root = resolveWorkflowRoot(cwd, normalized);
1697
3515
  const state = await readState(cwd, normalized);
1698
- const legacy = detectLegacyContract(root, state);
3516
+ let effectiveState = state;
3517
+ if (state?.current_stage === STAGES.CLARIFY) {
3518
+ effectiveState = withClarifySummary(state, await readSpecSummary(root));
3519
+ }
3520
+ const legacy = detectLegacyContract(root, effectiveState);
1699
3521
  const artifacts = collectArtifactPresence(root, legacy ? LEGACY_ARTIFACTS : V1_ARTIFACTS);
1700
3522
  const missing = Object.entries(artifacts).filter(([, present]) => !present).map(([name]) => name);
1701
3523
  return {
@@ -1704,12 +3526,14 @@ export async function statusSummary(cwd, slug) {
1704
3526
  config,
1705
3527
  slug: normalized,
1706
3528
  root,
1707
- state: state ? withRecommendedAction(state, legacy) : null,
3529
+ state: effectiveState ? withRecommendedAction(effectiveState, legacy) : null,
1708
3530
  legacy,
1709
3531
  contract: legacy ? 'legacy-codex-helper' : 'loopx-v1',
1710
- schema_version: state?.schema_version ?? 0,
3532
+ schema_version: effectiveState?.schema_version ?? 0,
1711
3533
  artifacts,
1712
3534
  missing_artifacts: missing,
1713
- next_action: state ? recommendedAction(state, legacy) : 'Run loopx clarify to start a workflow.',
3535
+ hook,
3536
+ contextSetup,
3537
+ next_action: effectiveState ? recommendedAction(effectiveState, legacy) : 'Run loopx clarify to start a workflow.',
1714
3538
  };
1715
3539
  }