@ai-content-space/loopx 0.1.0 → 0.1.2

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 (44) hide show
  1. package/README.md +26 -26
  2. package/package.json +6 -2
  3. package/plugins/loopx/.codex-plugin/plugin.json +6 -6
  4. package/plugins/loopx/scripts/plugin-install.test.mjs +25 -8
  5. package/plugins/loopx/skills/autopilot/SKILL.md +90 -0
  6. package/plugins/loopx/skills/build/SKILL.md +118 -0
  7. package/plugins/loopx/skills/clarify/SKILL.md +219 -0
  8. package/plugins/loopx/skills/plan/SKILL.md +238 -0
  9. package/plugins/loopx/skills/{loopx-review → review}/SKILL.md +9 -4
  10. package/skills/ai-slop-cleaner/SKILL.md +114 -0
  11. package/skills/autopilot/SKILL.md +90 -0
  12. package/skills/autoresearch/SKILL.md +68 -0
  13. package/skills/build/SKILL.md +118 -0
  14. package/skills/clarify/SKILL.md +219 -0
  15. package/skills/deep-interview/SKILL.md +461 -0
  16. package/skills/deepsearch/SKILL.md +38 -0
  17. package/skills/plan/SKILL.md +242 -0
  18. package/skills/ralph/SKILL.md +271 -0
  19. package/skills/ralplan/SKILL.md +49 -0
  20. package/skills/{loopx-review → review}/SKILL.md +9 -4
  21. package/src/autopilot-runtime.mjs +152 -0
  22. package/src/build-runtime.mjs +146 -0
  23. package/src/cli.mjs +49 -12
  24. package/src/codex-exec-runtime.mjs +97 -0
  25. package/src/install-discovery.mjs +7 -7
  26. package/src/next-skill.mjs +33 -0
  27. package/src/plan-runtime.mjs +477 -0
  28. package/src/runtime-maintenance.mjs +36 -8
  29. package/src/workflow.mjs +831 -124
  30. package/templates/architecture.md +3 -3
  31. package/templates/development-plan.md +1 -1
  32. package/templates/execution-record.md +1 -1
  33. package/templates/plan.md +10 -4
  34. package/templates/review-report.md +1 -1
  35. package/templates/spec.md +38 -2
  36. package/templates/test-plan.md +1 -1
  37. package/plugins/loopx/skills/loopx-autopilot/SKILL.md +0 -30
  38. package/plugins/loopx/skills/loopx-build/SKILL.md +0 -25
  39. package/plugins/loopx/skills/loopx-clarify/SKILL.md +0 -25
  40. package/plugins/loopx/skills/loopx-plan/SKILL.md +0 -25
  41. package/skills/loopx-autopilot/SKILL.md +0 -30
  42. package/skills/loopx-build/SKILL.md +0 -25
  43. package/skills/loopx-clarify/SKILL.md +0 -25
  44. package/skills/loopx-plan/SKILL.md +0 -25
package/src/workflow.mjs CHANGED
@@ -1,9 +1,12 @@
1
1
  import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
- import { dirname, join, resolve } from 'node:path';
3
+ import { basename, dirname, join, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
6
- import { ensureLoopXRoot, resolveLoopXRoot } from './runtime-maintenance.mjs';
6
+ import { AUTOPILOT_PHASES, createDefaultAutopilotAdapter } from './autopilot-runtime.mjs';
7
+ import { ensureLoopxRoot, resolveLoopxRoot } from './runtime-maintenance.mjs';
8
+ import { DEFAULT_BUILD_MAX_ITERATIONS, createDefaultBuildAdapter } from './build-runtime.mjs';
9
+ import { DEFAULT_MAX_ITERATIONS, createDefaultPlanAdapter } from './plan-runtime.mjs';
7
10
 
8
11
  const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
9
12
  const WORKSPACE_SCHEMA_VERSION = 1;
@@ -36,6 +39,23 @@ export const TRANSITIONS = {
36
39
  const PLAN_ARTIFACTS = ['plan.md', 'architecture.md', 'development-plan.md', 'test-plan.md'];
37
40
  const V1_ARTIFACTS = ['spec.md', ...PLAN_ARTIFACTS, 'execution-record.md', 'review-report.md'];
38
41
  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
+ const PLAN_REVIEW_DIR = 'plan-reviews';
48
+ const BUILD_SUPPORT_DIR = 'build-support';
49
+ const CLARIFY_PROFILES = {
50
+ standard: {
51
+ threshold: 0.2,
52
+ maxRounds: 15,
53
+ },
54
+ deep: {
55
+ threshold: 0.1,
56
+ maxRounds: 25,
57
+ },
58
+ };
39
59
 
40
60
  function normalizeSlug(raw) {
41
61
  const slug = String(raw || '')
@@ -57,6 +77,14 @@ function nowStamp() {
57
77
  return nowIso().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
58
78
  }
59
79
 
80
+ function normalizeClarifyProfile(raw) {
81
+ const value = String(raw || 'standard').trim().toLowerCase();
82
+ if (!(value in CLARIFY_PROFILES)) {
83
+ throw new Error(`invalid_clarify_profile:${value}`);
84
+ }
85
+ return value;
86
+ }
87
+
60
88
  function parseFrontmatter(text) {
61
89
  if (!text.startsWith('---\n')) {
62
90
  return {};
@@ -113,6 +141,22 @@ function frontmatterBlock(values) {
113
141
  return lines.join('\n');
114
142
  }
115
143
 
144
+ function frontmatterBoolean(value) {
145
+ if (value === true || value === false) {
146
+ return value;
147
+ }
148
+ if (typeof value === 'string') {
149
+ const normalized = value.trim().toLowerCase();
150
+ if (normalized === 'true') {
151
+ return true;
152
+ }
153
+ if (normalized === 'false') {
154
+ return false;
155
+ }
156
+ }
157
+ return Boolean(value);
158
+ }
159
+
116
160
  function statePath(root) {
117
161
  return join(root, 'state.json');
118
162
  }
@@ -153,7 +197,7 @@ async function writeState(root, state) {
153
197
  }
154
198
 
155
199
  export function resolveWorkspaceRoot(cwd) {
156
- return resolveLoopXRoot(cwd);
200
+ return resolveLoopxRoot(cwd);
157
201
  }
158
202
 
159
203
  export function resolveWorkflowRoot(cwd, slug) {
@@ -168,6 +212,41 @@ function resolvePlansRoot(cwd) {
168
212
  return join(resolveWorkspaceRoot(cwd), 'plans');
169
213
  }
170
214
 
215
+ function resolveDocsRoot(cwd, slug) {
216
+ return join(resolve(cwd), 'docs', normalizeSlug(slug));
217
+ }
218
+
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
+ };
227
+ }
228
+
229
+ function resolvePlanReviewPaths(root, iteration) {
230
+ const reviewsRoot = join(root, PLAN_REVIEW_DIR);
231
+ return {
232
+ reviewsRoot,
233
+ planner: join(reviewsRoot, `planner-iteration-${iteration}.md`),
234
+ architect: join(reviewsRoot, `architect-iteration-${iteration}.md`),
235
+ critic: join(reviewsRoot, `critic-iteration-${iteration}.md`),
236
+ };
237
+ }
238
+
239
+ function resolveBuildSupportPaths(root, iteration) {
240
+ const supportRoot = join(root, BUILD_SUPPORT_DIR);
241
+ return {
242
+ supportRoot,
243
+ laneSummary: join(supportRoot, `lanes-iteration-${iteration}.md`),
244
+ architect: join(supportRoot, `architect-iteration-${iteration}.md`),
245
+ deslop: join(supportRoot, `deslop-iteration-${iteration}.md`),
246
+ regression: join(supportRoot, `regression-iteration-${iteration}.md`),
247
+ };
248
+ }
249
+
171
250
  function canonicalClarifySpecPath(cwd, slug, stamp) {
172
251
  return join(resolveSpecsRoot(cwd), `clarify-${normalizeSlug(slug)}-${stamp}.md`);
173
252
  }
@@ -190,9 +269,9 @@ export async function readState(cwd, slug) {
190
269
 
191
270
  function buildWorkspaceReadme() {
192
271
  return [
193
- '# LoopX Workspace',
272
+ '# loopx Workspace',
194
273
  '',
195
- 'This directory is initialized for the LoopX skill-first runtime contract.',
274
+ 'This directory is initialized for the loopx skill-first runtime contract.',
196
275
  '',
197
276
  '## Default Flow',
198
277
  '',
@@ -214,22 +293,66 @@ function buildWorkspaceReadme() {
214
293
  ].join('\n');
215
294
  }
216
295
 
217
- function createInitialState(slug) {
296
+ function createInitialState(slug, profile) {
297
+ const clarifyProfile = CLARIFY_PROFILES[profile];
218
298
  return {
219
299
  schema_version: WORKFLOW_SCHEMA_VERSION,
220
300
  slug,
221
301
  current_stage: STAGES.CLARIFY,
222
302
  stage_status: 'blocked',
303
+ clarify_profile: profile,
304
+ clarify_target_ambiguity_threshold: clarifyProfile.threshold,
305
+ clarify_max_rounds: clarifyProfile.maxRounds,
306
+ clarify_current_round: 0,
307
+ clarify_ambiguity_score: 1,
308
+ clarify_pressure_pass_complete: false,
309
+ clarify_non_goals_resolved: false,
310
+ clarify_decision_boundaries_resolved: false,
223
311
  ambiguity_items: [
224
312
  {
225
313
  id: 'A-1',
226
- question: 'What specific task should LoopX execute in this workflow?',
314
+ question: 'What specific task should loopx execute in this workflow?',
227
315
  status: 'open',
228
316
  resolution: null,
229
317
  },
230
318
  ],
231
319
  unresolved_ambiguity_count: 1,
232
320
  plan_package_status: 'missing',
321
+ plan_current_iteration: 0,
322
+ plan_max_iterations: DEFAULT_MAX_ITERATIONS,
323
+ plan_consensus_mode: false,
324
+ plan_deliberate_mode: false,
325
+ plan_interactive_mode: false,
326
+ plan_principles_resolved: false,
327
+ plan_options_reviewed: false,
328
+ plan_architect_review_status: 'not-started',
329
+ plan_critic_verdict: 'none',
330
+ plan_acceptance_criteria_testable: false,
331
+ plan_verification_steps_resolved: false,
332
+ plan_execution_inputs_resolved: false,
333
+ plan_docs_status: 'missing',
334
+ plan_docs_artifact_paths: null,
335
+ plan_review_artifact_paths: [],
336
+ plan_blockers: [],
337
+ plan_source_spec_path: null,
338
+ build_run_id: null,
339
+ build_current_iteration: 0,
340
+ build_max_iterations: DEFAULT_BUILD_MAX_ITERATIONS,
341
+ build_parallel_mode: false,
342
+ build_lane_statuses: [],
343
+ build_verification_status: 'pending',
344
+ build_architect_verification_status: 'not-started',
345
+ build_deslop_status: 'pending',
346
+ build_regression_status: 'pending',
347
+ build_blockers: [],
348
+ build_progress_artifact_paths: [],
349
+ build_support_evidence_paths: [],
350
+ build_no_deslop: false,
351
+ autopilot_current_phase: 'none',
352
+ autopilot_phase_history: [],
353
+ autopilot_blockers: [],
354
+ autopilot_run_path: null,
355
+ autopilot_completed: false,
233
356
  review_status: 'not-started',
234
357
  recommended_next_action: 'Resolve ambiguity items in spec.md before requesting approval to enter plan.',
235
358
  rollback_target: 'none',
@@ -293,8 +416,10 @@ async function copyArtifact(fromRoot, toPath, name) {
293
416
  }
294
417
 
295
418
  async function writeCanonicalPlanArtifacts(cwd, root, slug) {
296
- const planPath = join(resolvePlansRoot(cwd), `prd-${slug}.md`);
297
- const testSpecPath = join(resolvePlansRoot(cwd), `test-spec-${slug}.md`);
419
+ const plansRoot = resolvePlansRoot(cwd);
420
+ await ensureDir(plansRoot);
421
+ const planPath = join(plansRoot, `prd-${slug}.md`);
422
+ const testSpecPath = join(plansRoot, `test-spec-${slug}.md`);
298
423
  const planText = await readFile(artifactPath(root, 'plan.md'), 'utf8');
299
424
  const architectureText = await readFile(artifactPath(root, 'architecture.md'), 'utf8');
300
425
  const developmentPlanText = await readFile(artifactPath(root, 'development-plan.md'), 'utf8');
@@ -303,7 +428,7 @@ async function writeCanonicalPlanArtifacts(cwd, root, slug) {
303
428
  await writeText(
304
429
  planPath,
305
430
  [
306
- `# LoopX PRD: ${slug}`,
431
+ `# loopx PRD: ${slug}`,
307
432
  '',
308
433
  '## Plan',
309
434
  '',
@@ -322,14 +447,342 @@ async function writeCanonicalPlanArtifacts(cwd, root, slug) {
322
447
  return { planPath, testSpecPath };
323
448
  }
324
449
 
450
+ function deriveSlugFromSpecPath(path, text) {
451
+ const meta = parseFrontmatter(text);
452
+ if (meta.workflow_id) {
453
+ return normalizeSlug(meta.workflow_id);
454
+ }
455
+ const name = basename(path).replace(/\.md$/i, '');
456
+ return normalizeSlug(name.replace(/^deep-interview-/, '').replace(/^clarify-/, ''));
457
+ }
458
+
459
+ function containsChineseText(text) {
460
+ return /[\u3400-\u9fff]/.test(text);
461
+ }
462
+
463
+ async function ensurePlanWorkflowFromDirectSpec(cwd, directSpecPath, explicitSlug, options = {}) {
464
+ const resolvedSpecPath = resolve(cwd, directSpecPath);
465
+ const specText = await readFile(resolvedSpecPath, 'utf8');
466
+ const slug = explicitSlug ? normalizeSlug(explicitSlug) : deriveSlugFromSpecPath(resolvedSpecPath, specText);
467
+ const root = resolveWorkflowRoot(cwd, slug);
468
+ await ensureLoopxRoot(cwd);
469
+ await ensureDir(root);
470
+ await writeText(artifactPath(root, 'spec.md'), specText);
471
+
472
+ const existing = await readState(cwd, slug);
473
+ if (existing) {
474
+ const merged = withRecommendedAction({
475
+ ...existing,
476
+ spec_artifact_path: resolvedSpecPath,
477
+ plan_source_spec_path: resolvedSpecPath,
478
+ plan_consensus_mode: true,
479
+ plan_deliberate_mode: Boolean(options.deliberate),
480
+ plan_interactive_mode: Boolean(options.interactive),
481
+ });
482
+ await writeState(root, merged);
483
+ return { slug, root, state: merged };
484
+ }
485
+
486
+ const state = withRecommendedAction({
487
+ ...createInitialState(slug, 'standard'),
488
+ clarify_current_round: 1,
489
+ clarify_ambiguity_score: 0,
490
+ clarify_pressure_pass_complete: true,
491
+ clarify_non_goals_resolved: true,
492
+ clarify_decision_boundaries_resolved: true,
493
+ unresolved_ambiguity_count: 0,
494
+ spec_artifact_path: resolvedSpecPath,
495
+ plan_source_spec_path: resolvedSpecPath,
496
+ requested_transition: TRANSITIONS.CLARIFY_TO_PLAN,
497
+ stage_status: 'awaiting-approval',
498
+ plan_consensus_mode: true,
499
+ plan_deliberate_mode: Boolean(options.deliberate),
500
+ plan_interactive_mode: Boolean(options.interactive),
501
+ approval: {
502
+ ...createInitialState(slug, 'standard').approval,
503
+ plan: APPROVAL_STATES.APPROVED,
504
+ },
505
+ });
506
+ await writeState(root, state);
507
+ return { slug, root, state };
508
+ }
509
+
510
+ async function writePlanArtifacts(root, cwd, slug, plannerDraft) {
511
+ await writeText(artifactPath(root, 'plan.md'), plannerDraft.planText);
512
+ await writeText(artifactPath(root, 'architecture.md'), plannerDraft.architectureText);
513
+ await writeText(artifactPath(root, 'development-plan.md'), plannerDraft.developmentPlanText);
514
+ 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
+ }
523
+
524
+ async function writePlanReviewArtifacts(root, iteration, plannerDraft, architectReview, criticReview) {
525
+ const paths = resolvePlanReviewPaths(root, iteration);
526
+ await ensureDir(paths.reviewsRoot);
527
+ await writeText(
528
+ paths.planner,
529
+ [
530
+ `# Planner Draft: iteration ${iteration}`,
531
+ '',
532
+ '## Principles',
533
+ '',
534
+ ...plannerDraft.principles.map((item) => `- ${item}`),
535
+ '',
536
+ '## Decision Drivers',
537
+ '',
538
+ ...plannerDraft.decisionDrivers.map((item) => `- ${item}`),
539
+ ].join('\n'),
540
+ );
541
+ await writeText(
542
+ paths.architect,
543
+ [
544
+ `# Architect Review: iteration ${iteration}`,
545
+ '',
546
+ `- status: ${architectReview.status}`,
547
+ `- verdict: ${architectReview.verdict}`,
548
+ '',
549
+ '## Findings',
550
+ '',
551
+ ...architectReview.findings.map((item) => `- ${item}`),
552
+ ].join('\n'),
553
+ );
554
+ await writeText(
555
+ paths.critic,
556
+ [
557
+ `# Critic Review: iteration ${iteration}`,
558
+ '',
559
+ `- verdict: ${criticReview.verdict}`,
560
+ '',
561
+ '## Findings',
562
+ '',
563
+ ...criticReview.findings.map((item) => `- ${item}`),
564
+ ].join('\n'),
565
+ );
566
+ return paths;
567
+ }
568
+
569
+ async function readPlanCompletion(cwd, root, slug, state) {
570
+ 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
+ if (state.plan_architect_review_status !== 'complete') {
578
+ blockers.push('architect_review_incomplete');
579
+ }
580
+ if (state.plan_critic_verdict !== 'approve') {
581
+ blockers.push(`critic_verdict_${state.plan_critic_verdict}`);
582
+ }
583
+ if (state.plan_package_status !== 'complete') {
584
+ blockers.push(`plan_package_${state.plan_package_status}`);
585
+ }
586
+ if (!state.plan_acceptance_criteria_testable) {
587
+ blockers.push('acceptance_criteria_unresolved');
588
+ }
589
+ if (!state.plan_verification_steps_resolved) {
590
+ blockers.push('verification_steps_unresolved');
591
+ }
592
+ if (!state.plan_execution_inputs_resolved) {
593
+ blockers.push('execution_inputs_unresolved');
594
+ }
595
+ if (!state.plan_artifact_path || !existsSync(state.plan_artifact_path)) {
596
+ blockers.push('missing_prd');
597
+ }
598
+ if (!state.test_spec_artifact_path || !existsSync(state.test_spec_artifact_path)) {
599
+ blockers.push('missing_test_spec');
600
+ }
601
+ for (const [key, present] of Object.entries(docsPresent)) {
602
+ if (!present) {
603
+ blockers.push(`missing_doc_${key}`);
604
+ continue;
605
+ }
606
+ const text = await readFile(docPaths[key], 'utf8');
607
+ if (!containsChineseText(text)) {
608
+ blockers.push(`doc_not_chinese_${key}`);
609
+ }
610
+ }
611
+
612
+ const docsComplete = Object.values(docsPresent).every(Boolean)
613
+ && blockers.every((blocker) => !blocker.startsWith('doc_not_chinese_') && !blocker.startsWith('missing_doc_'));
614
+
615
+ return {
616
+ blockers,
617
+ docsStatus: docsComplete ? 'complete' : Object.values(docsPresent).some(Boolean) ? 'partial' : 'missing',
618
+ docPaths,
619
+ };
620
+ }
621
+
622
+ function buildIterationBlockers(iterationData, { noDeslop = false } = {}) {
623
+ const blockers = [];
624
+ for (const lane of iterationData.lanes) {
625
+ if (lane.status !== 'complete') {
626
+ blockers.push(`lane_incomplete_${lane.name}`);
627
+ }
628
+ }
629
+ if (iterationData.verificationStatus !== 'complete') {
630
+ blockers.push(`verification_${iterationData.verificationStatus}`);
631
+ }
632
+ if (iterationData.architectVerdict !== 'approve') {
633
+ blockers.push(`architect_${iterationData.architectVerdict}`);
634
+ }
635
+ if (!noDeslop && iterationData.deslopStatus !== 'complete') {
636
+ blockers.push(`deslop_${iterationData.deslopStatus}`);
637
+ }
638
+ if (!noDeslop && iterationData.regressionStatus !== 'complete') {
639
+ blockers.push(`regression_${iterationData.regressionStatus}`);
640
+ }
641
+ return blockers;
642
+ }
643
+
644
+ function buildExecutionRecordContent({ slug, iterationData, complete }) {
645
+ const placeholder = complete ? null : 'TODO: build iteration is not review-ready yet.';
646
+ return [
647
+ frontmatterBlock({
648
+ schema_version: WORKFLOW_SCHEMA_VERSION,
649
+ workflow_id: slug,
650
+ run_id: iterationData.runId,
651
+ stage: STAGES.BUILD,
652
+ actor_id: iterationData.actorId,
653
+ actor_role: STAGES.BUILD,
654
+ plan_digest: `plan@${slug}`,
655
+ started_at: nowIso(),
656
+ completed_at: nowIso(),
657
+ checkpoint_count: iterationData.lanes.length,
658
+ evidence_manifest: iterationData.lanes.flatMap((lane) => lane.evidence || []),
659
+ }),
660
+ `# loopx Execution Record: ${slug}`,
661
+ '',
662
+ '## Changes',
663
+ '',
664
+ '- Completed the current build iteration lanes and aggregated evidence.',
665
+ '',
666
+ '## Checkpoint Log',
667
+ '',
668
+ ...iterationData.lanes.map((lane) => `- ${lane.name}: ${lane.status}`),
669
+ '',
670
+ '## Execution Evidence',
671
+ '',
672
+ ...iterationData.executionEvidence.map((item) => `- ${item}`),
673
+ '',
674
+ '## Verification Evidence',
675
+ '',
676
+ ...iterationData.verificationEvidence.map((item) => `- ${item}`),
677
+ '',
678
+ '## Limitations',
679
+ '',
680
+ ...(placeholder ? [`- ${placeholder}`] : iterationData.limitations.map((item) => `- ${item}`)),
681
+ ].join('\n');
682
+ }
683
+
684
+ async function writeBuildSupportArtifacts(root, iterationData, noDeslop) {
685
+ const paths = resolveBuildSupportPaths(root, iterationData.iteration);
686
+ await ensureDir(paths.supportRoot);
687
+ await writeText(
688
+ paths.laneSummary,
689
+ [
690
+ `# Build Lanes: iteration ${iterationData.iteration}`,
691
+ '',
692
+ ...iterationData.lanes.map((lane) => `- ${lane.name}: ${lane.status} | ${lane.summary}`),
693
+ ].join('\n'),
694
+ );
695
+ await writeText(
696
+ paths.architect,
697
+ [
698
+ `# Build Architect Gate: iteration ${iterationData.iteration}`,
699
+ '',
700
+ `- verdict: ${iterationData.architectVerdict}`,
701
+ '',
702
+ ...iterationData.architectFindings.map((item) => `- ${item}`),
703
+ ].join('\n'),
704
+ );
705
+ await writeText(
706
+ paths.deslop,
707
+ [
708
+ `# Build Deslop: iteration ${iterationData.iteration}`,
709
+ '',
710
+ `- status: ${noDeslop ? 'skipped' : iterationData.deslopStatus}`,
711
+ ].join('\n'),
712
+ );
713
+ await writeText(
714
+ paths.regression,
715
+ [
716
+ `# Build Regression: iteration ${iterationData.iteration}`,
717
+ '',
718
+ `- status: ${noDeslop ? 'skipped' : iterationData.regressionStatus}`,
719
+ ].join('\n'),
720
+ );
721
+ return paths;
722
+ }
723
+
325
724
  async function readSpecSummary(root) {
326
725
  const text = await readTextIfExists(artifactPath(root, 'spec.md'));
327
726
  if (!text) {
328
- return { unresolvedCount: 1 };
727
+ return {
728
+ unresolvedCount: 1,
729
+ currentRound: 0,
730
+ ambiguityScore: 1,
731
+ pressurePassComplete: false,
732
+ nonGoalsResolved: false,
733
+ decisionBoundariesResolved: false,
734
+ };
329
735
  }
330
736
  const meta = parseFrontmatter(text);
331
737
  const unresolvedCount = Number.parseInt(String(meta.unresolved_ambiguity_count ?? 1), 10);
332
- return { unresolvedCount: Number.isNaN(unresolvedCount) ? 1 : unresolvedCount };
738
+ const currentRound = Number.parseInt(String(meta.current_round ?? meta.clarify_current_round ?? 0), 10);
739
+ const ambiguityScore = Number.parseFloat(String(meta.ambiguity_score ?? meta.clarify_ambiguity_score ?? 1));
740
+ return {
741
+ unresolvedCount: Number.isNaN(unresolvedCount) ? 1 : unresolvedCount,
742
+ currentRound: Number.isNaN(currentRound) ? 0 : currentRound,
743
+ ambiguityScore: Number.isFinite(ambiguityScore) && ambiguityScore >= 0 && ambiguityScore <= 1 ? ambiguityScore : 1,
744
+ pressurePassComplete: frontmatterBoolean(meta.pressure_pass_complete ?? meta.clarify_pressure_pass_complete ?? false),
745
+ nonGoalsResolved: frontmatterBoolean(meta.non_goals_resolved ?? meta.clarify_non_goals_resolved ?? false),
746
+ decisionBoundariesResolved: frontmatterBoolean(meta.decision_boundaries_resolved ?? meta.clarify_decision_boundaries_resolved ?? false),
747
+ };
748
+ }
749
+
750
+ function withClarifySummary(state, spec) {
751
+ return {
752
+ ...state,
753
+ clarify_current_round: spec.currentRound,
754
+ clarify_ambiguity_score: spec.ambiguityScore,
755
+ clarify_pressure_pass_complete: spec.pressurePassComplete,
756
+ clarify_non_goals_resolved: spec.nonGoalsResolved,
757
+ clarify_decision_boundaries_resolved: spec.decisionBoundariesResolved,
758
+ unresolved_ambiguity_count: spec.unresolvedCount,
759
+ };
760
+ }
761
+
762
+ function clarifyReadinessBlockers(state) {
763
+ const blockers = [];
764
+ if (state.unresolved_ambiguity_count > 0) {
765
+ blockers.push('unresolved_ambiguity');
766
+ }
767
+ if (state.clarify_current_round <= 0) {
768
+ blockers.push('clarify_current_round_required');
769
+ }
770
+ if (state.clarify_current_round > state.clarify_max_rounds) {
771
+ blockers.push('clarify_max_rounds_exceeded');
772
+ }
773
+ if (state.clarify_ambiguity_score > state.clarify_target_ambiguity_threshold) {
774
+ blockers.push('clarify_ambiguity_score_above_threshold');
775
+ }
776
+ if (!state.clarify_non_goals_resolved) {
777
+ blockers.push('clarify_non_goals_unresolved');
778
+ }
779
+ if (!state.clarify_decision_boundaries_resolved) {
780
+ blockers.push('clarify_decision_boundaries_unresolved');
781
+ }
782
+ if (!state.clarify_pressure_pass_complete) {
783
+ blockers.push('clarify_pressure_pass_incomplete');
784
+ }
785
+ return blockers;
333
786
  }
334
787
 
335
788
  async function readExecutionRecordSummary(root) {
@@ -363,19 +816,25 @@ async function readExecutionRecordSummary(root) {
363
816
 
364
817
  function recommendedAction(state, legacy = false) {
365
818
  if (legacy) {
366
- return 'Legacy codex-helper workflow detected. Run loopx migrate or create a new LoopX workflow.';
819
+ return 'Legacy codex-helper workflow detected. Run loopx migrate or create a new loopx workflow.';
367
820
  }
368
821
 
369
822
  switch (state.current_stage) {
370
823
  case STAGES.CLARIFY:
371
824
  return state.approval.plan === APPROVAL_STATES.APPROVED
372
825
  ? 'Run loopx plan to consume the approved clarify -> plan transition.'
373
- : 'Resolve ambiguity and approve clarify -> plan.';
826
+ : `Resolve ambiguity in ${state.clarify_profile ?? 'standard'} clarify mode and approve clarify -> plan.`;
374
827
  case STAGES.PLAN:
828
+ 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.';
830
+ }
375
831
  return state.approval.build === APPROVAL_STATES.APPROVED
376
832
  ? 'Run loopx build to consume the approved plan -> build transition.'
377
833
  : 'Approve plan -> build when the plan package is ready.';
378
834
  case STAGES.BUILD:
835
+ if (Array.isArray(state.build_blockers) && state.build_blockers.length > 0) {
836
+ return 'Run loopx build to continue the execution loop until verification, architect, deslop, and regression blockers are cleared.';
837
+ }
379
838
  return state.approval.review === APPROVAL_STATES.APPROVED
380
839
  ? 'Run loopx review to consume the approved build -> review transition.'
381
840
  : 'Approve build -> review when execution-record.md is complete.';
@@ -392,6 +851,9 @@ function recommendedAction(state, legacy = false) {
392
851
  }
393
852
  return 'Run loopx review after build completes.';
394
853
  case STAGES.DONE:
854
+ if (state.autopilot_current_phase && state.autopilot_current_phase !== 'none' && state.autopilot_completed) {
855
+ return 'Autopilot run is complete.';
856
+ }
395
857
  return 'Workflow is complete.';
396
858
  default:
397
859
  return 'Run loopx clarify to start a workflow.';
@@ -466,7 +928,7 @@ function executionRecordTemplate(slug, stage, actorId, runId) {
466
928
  checkpoint_count: 0,
467
929
  evidence_manifest: [],
468
930
  }),
469
- `# LoopX Execution Record: ${slug}`,
931
+ `# loopx Execution Record: ${slug}`,
470
932
  '',
471
933
  '## Changes',
472
934
  '',
@@ -504,7 +966,7 @@ function reviewReportContent({ slug, reviewer, runId, verdict, rollbackTarget, r
504
966
  rollback_target: rollbackTarget,
505
967
  rollback_rationale: rollbackRationale ?? null,
506
968
  }),
507
- `# LoopX Review Report: ${slug}`,
969
+ `# loopx Review Report: ${slug}`,
508
970
  '',
509
971
  '## Verdict',
510
972
  '',
@@ -538,7 +1000,7 @@ async function refreshExecutionStatus(root, state) {
538
1000
 
539
1001
  export async function initWorkspace(cwd, { slug } = {}) {
540
1002
  const workspaceRoot = resolveWorkspaceRoot(cwd);
541
- await ensureLoopXRoot(cwd);
1003
+ await ensureLoopxRoot(cwd);
542
1004
  await ensureDir(join(workspaceRoot, 'context'));
543
1005
  await ensureDir(join(workspaceRoot, 'workflows'));
544
1006
  await ensureDir(join(workspaceRoot, 'specs'));
@@ -547,7 +1009,7 @@ export async function initWorkspace(cwd, { slug } = {}) {
547
1009
 
548
1010
  const config = {
549
1011
  schema_version: WORKSPACE_SCHEMA_VERSION,
550
- tool: 'LoopX',
1012
+ tool: 'loopx',
551
1013
  product_contract: 'skill-first-v1',
552
1014
  default_flow: ['clarify', 'plan', 'build', 'review', 'done'],
553
1015
  preferred_surface: ['clarify', 'plan', 'build', 'review', 'autopilot'],
@@ -567,20 +1029,24 @@ export async function initWorkspace(cwd, { slug } = {}) {
567
1029
  return { workspaceRoot, config, workflow };
568
1030
  }
569
1031
 
570
- export async function clarifyStage(cwd, slug) {
1032
+ export async function clarifyStage(cwd, slug, { profile = 'standard' } = {}) {
571
1033
  const normalized = normalizeSlug(slug);
1034
+ const clarifyProfile = normalizeClarifyProfile(profile);
572
1035
  const root = resolveWorkflowRoot(cwd, normalized);
573
- await ensureLoopXRoot(cwd);
1036
+ await ensureLoopxRoot(cwd);
574
1037
  await ensureDir(root);
575
1038
  const stamp = nowStamp();
576
1039
  await writeTemplateArtifact(root, 'spec.md', {
577
1040
  'task name': normalized,
578
1041
  'workflow id': normalized,
1042
+ profile: clarifyProfile,
1043
+ 'target ambiguity threshold': CLARIFY_PROFILES[clarifyProfile].threshold,
1044
+ 'max rounds': CLARIFY_PROFILES[clarifyProfile].maxRounds,
579
1045
  });
580
1046
  const specArtifactPath = canonicalClarifySpecPath(cwd, normalized, stamp);
581
1047
  await copyArtifact(root, specArtifactPath, 'spec.md');
582
1048
  const state = withRecommendedAction({
583
- ...createInitialState(normalized),
1049
+ ...createInitialState(normalized, clarifyProfile),
584
1050
  spec_artifact_path: specArtifactPath,
585
1051
  });
586
1052
  await writeState(root, state);
@@ -598,9 +1064,17 @@ export async function approveStage(cwd, slug, { from, to }) {
598
1064
 
599
1065
  if (transition === TRANSITIONS.CLARIFY_TO_PLAN) {
600
1066
  const spec = await readSpecSummary(root);
601
- next.unresolved_ambiguity_count = spec.unresolvedCount;
602
- if (spec.unresolvedCount > 0) {
603
- throw new Error('unresolved_ambiguity');
1067
+ next = withClarifySummary(next, spec);
1068
+ const blockers = clarifyReadinessBlockers(next);
1069
+ if (blockers.length > 0) {
1070
+ const blocked = withRecommendedAction({
1071
+ ...next,
1072
+ stage_status: 'blocked',
1073
+ pending_user_decision: TRANSITIONS.CLARIFY_TO_PLAN,
1074
+ requested_transition: TRANSITIONS.NONE,
1075
+ });
1076
+ await writeState(root, blocked);
1077
+ throw new Error(`clarify_readiness_blocked:${blockers.join(',')}`);
604
1078
  }
605
1079
  }
606
1080
 
@@ -609,6 +1083,23 @@ export async function approveStage(cwd, slug, { from, to }) {
609
1083
  throw new Error('plan_package_incomplete');
610
1084
  }
611
1085
  next.plan_package_status = 'complete';
1086
+ const completion = await readPlanCompletion(cwd, root, state.slug, next);
1087
+ next = {
1088
+ ...next,
1089
+ plan_docs_status: completion.docsStatus,
1090
+ plan_docs_artifact_paths: completion.docPaths,
1091
+ plan_blockers: completion.blockers,
1092
+ };
1093
+ if (completion.blockers.length > 0) {
1094
+ const blocked = withRecommendedAction({
1095
+ ...next,
1096
+ stage_status: 'blocked',
1097
+ pending_user_decision: TRANSITIONS.PLAN_TO_BUILD,
1098
+ requested_transition: TRANSITIONS.NONE,
1099
+ });
1100
+ await writeState(root, blocked);
1101
+ throw new Error(`plan_review_gate_blocked:${completion.blockers.join(',')}`);
1102
+ }
612
1103
  }
613
1104
 
614
1105
  if (transition === TRANSITIONS.BUILD_TO_REVIEW) {
@@ -617,6 +1108,16 @@ export async function approveStage(cwd, slug, { from, to }) {
617
1108
  if (next.execution_record_status !== 'complete') {
618
1109
  throw new Error('review_gate_blocked:execution-record.md');
619
1110
  }
1111
+ if (Array.isArray(next.build_blockers) && next.build_blockers.length > 0) {
1112
+ const blocked = withRecommendedAction({
1113
+ ...next,
1114
+ stage_status: 'blocked',
1115
+ pending_user_decision: TRANSITIONS.BUILD_TO_REVIEW,
1116
+ requested_transition: TRANSITIONS.NONE,
1117
+ });
1118
+ await writeState(root, blocked);
1119
+ throw new Error(`build_review_gate_blocked:${next.build_blockers.join(',')}`);
1120
+ }
620
1121
  }
621
1122
 
622
1123
  if (transition === TRANSITIONS.REVIEW_TO_PLAN) {
@@ -643,57 +1144,190 @@ export async function approveStage(cwd, slug, { from, to }) {
643
1144
  return { root, state: next };
644
1145
  }
645
1146
 
646
- export async function planStage(cwd, slug) {
647
- const { root, state, slug: normalized } = await loadWorkflowState(cwd, slug, { allowLegacy: false });
648
- ensureApprovedTransition(state, TRANSITIONS.CLARIFY_TO_PLAN, 'plan');
649
- if (state.spec_artifact_path) {
650
- await copyArtifact(root, state.spec_artifact_path, 'spec.md');
651
- }
652
- for (const name of PLAN_ARTIFACTS) {
653
- await writeTemplateArtifact(root, name, {
654
- 'task name': normalized,
655
- 'workflow id': normalized,
1147
+ export async function planStage(cwd, slug, options = {}) {
1148
+ let normalized = slug ? normalizeSlug(slug) : null;
1149
+ if (options.directSpecPath) {
1150
+ const bootstrapped = await ensurePlanWorkflowFromDirectSpec(cwd, options.directSpecPath, normalized, options);
1151
+ normalized = bootstrapped.slug;
1152
+ }
1153
+
1154
+ const loaded = await loadWorkflowState(cwd, normalized, { allowLegacy: false });
1155
+ const { root } = loaded;
1156
+ let { state } = loaded;
1157
+ if (!options.directSpecPath) {
1158
+ ensureApprovedTransition(state, TRANSITIONS.CLARIFY_TO_PLAN, 'plan');
1159
+ if (state.spec_artifact_path) {
1160
+ await copyArtifact(root, state.spec_artifact_path, 'spec.md');
1161
+ }
1162
+ }
1163
+
1164
+ const sourceSpecPath = options.directSpecPath ? resolve(cwd, options.directSpecPath) : (state.plan_source_spec_path || artifactPath(root, 'spec.md'));
1165
+ const sourceText = await readFile(sourceSpecPath, 'utf8');
1166
+ const adapter = options.adapter || createDefaultPlanAdapter();
1167
+ const maxIterations = DEFAULT_MAX_ITERATIONS;
1168
+ let iteration = 1;
1169
+ let architectReview = null;
1170
+ let criticReview = null;
1171
+ const reviewArtifactPaths = [];
1172
+
1173
+ while (iteration <= maxIterations) {
1174
+ const plannerDraft = await adapter.planner({
1175
+ cwd,
1176
+ root,
1177
+ slug: normalized,
1178
+ sourceText,
1179
+ iteration,
1180
+ deliberateMode: Boolean(options.deliberate),
1181
+ interactiveMode: Boolean(options.interactive),
1182
+ });
1183
+ const docPaths = await writePlanArtifacts(root, cwd, normalized, plannerDraft);
1184
+ const artifactPaths = await writeCanonicalPlanArtifacts(cwd, root, normalized);
1185
+
1186
+ architectReview = await adapter.architect({
1187
+ cwd,
1188
+ root,
1189
+ slug: normalized,
1190
+ sourceText,
1191
+ plannerDraft,
1192
+ iteration,
1193
+ deliberateMode: Boolean(options.deliberate),
656
1194
  });
1195
+ criticReview = await adapter.critic({
1196
+ cwd,
1197
+ root,
1198
+ slug: normalized,
1199
+ sourceText,
1200
+ plannerDraft,
1201
+ architectReview,
1202
+ iteration,
1203
+ deliberateMode: Boolean(options.deliberate),
1204
+ });
1205
+ const reviewPaths = await writePlanReviewArtifacts(root, iteration, plannerDraft, architectReview, criticReview);
1206
+ reviewArtifactPaths.push(reviewPaths);
1207
+
1208
+ state = {
1209
+ ...state,
1210
+ current_stage: STAGES.PLAN,
1211
+ plan_current_iteration: iteration,
1212
+ plan_max_iterations: maxIterations,
1213
+ plan_consensus_mode: true,
1214
+ plan_deliberate_mode: Boolean(options.deliberate),
1215
+ plan_interactive_mode: Boolean(options.interactive),
1216
+ plan_principles_resolved: plannerDraft.principlesResolved,
1217
+ plan_options_reviewed: plannerDraft.optionsReviewed,
1218
+ plan_architect_review_status: architectReview.status,
1219
+ plan_critic_verdict: criticReview.verdict,
1220
+ plan_acceptance_criteria_testable: criticReview.acceptanceCriteriaTestable,
1221
+ plan_verification_steps_resolved: criticReview.verificationStepsResolved,
1222
+ plan_execution_inputs_resolved: criticReview.executionInputsResolved,
1223
+ plan_package_status: 'complete',
1224
+ plan_docs_artifact_paths: docPaths,
1225
+ plan_review_artifact_paths: reviewArtifactPaths,
1226
+ plan_artifact_path: artifactPaths.planPath,
1227
+ test_spec_artifact_path: artifactPaths.testSpecPath,
1228
+ plan_source_spec_path: sourceSpecPath,
1229
+ last_confirmed_transition: TRANSITIONS.CLARIFY_TO_PLAN,
1230
+ approval: {
1231
+ ...state.approval,
1232
+ plan: APPROVAL_STATES.APPROVED,
1233
+ build: APPROVAL_STATES.NOT_REQUESTED,
1234
+ review: APPROVAL_STATES.NOT_REQUESTED,
1235
+ rollback: APPROVAL_STATES.NOT_REQUESTED,
1236
+ complete: APPROVAL_STATES.NOT_REQUESTED,
1237
+ },
1238
+ };
1239
+
1240
+ if (criticReview.verdict === 'approve') {
1241
+ break;
1242
+ }
1243
+ iteration += 1;
657
1244
  }
658
- const { planPath, testSpecPath } = await writeCanonicalPlanArtifacts(cwd, root, normalized);
659
1245
 
1246
+ const completion = await readPlanCompletion(cwd, root, normalized, state);
660
1247
  const next = withRecommendedAction({
661
1248
  ...state,
662
1249
  current_stage: STAGES.PLAN,
663
- stage_status: 'awaiting-approval',
664
- plan_package_status: 'complete',
1250
+ stage_status: completion.blockers.length > 0 ? 'blocked' : 'awaiting-approval',
665
1251
  pending_user_decision: TRANSITIONS.NONE,
666
1252
  requested_transition: TRANSITIONS.NONE,
667
- last_confirmed_transition: TRANSITIONS.CLARIFY_TO_PLAN,
668
- approval: {
669
- ...state.approval,
670
- plan: APPROVAL_STATES.APPROVED,
671
- build: APPROVAL_STATES.NOT_REQUESTED,
672
- review: APPROVAL_STATES.NOT_REQUESTED,
673
- rollback: APPROVAL_STATES.NOT_REQUESTED,
674
- complete: APPROVAL_STATES.NOT_REQUESTED,
675
- },
676
- plan_artifact_path: planPath,
677
- test_spec_artifact_path: testSpecPath,
1253
+ plan_docs_status: completion.docsStatus,
1254
+ plan_docs_artifact_paths: completion.docPaths,
1255
+ plan_blockers: completion.blockers,
678
1256
  });
679
1257
  await writeState(root, next);
680
- return { root, state: next };
1258
+ return { root, state: next, architectReview, criticReview };
681
1259
  }
682
1260
 
683
- export async function buildStage(cwd, slug) {
1261
+ export async function buildStage(cwd, slug, options = {}) {
684
1262
  const { root, state, slug: normalized } = await loadWorkflowState(cwd, slug, { allowLegacy: false });
685
1263
  ensureApprovedTransition(state, TRANSITIONS.PLAN_TO_BUILD, 'build');
686
- const runId = `${normalized}-build-draft`;
687
- await writeText(artifactPath(root, 'execution-record.md'), executionRecordTemplate(normalized, STAGES.BUILD, `${normalized}-builder-1`, runId));
1264
+ if (!PLAN_ARTIFACTS.every((name) => existsSync(artifactPath(root, name)))) {
1265
+ throw new Error('build_requires_workflow_plan_artifacts');
1266
+ }
1267
+ if (!state.plan_artifact_path || !existsSync(state.plan_artifact_path) || !state.test_spec_artifact_path || !existsSync(state.test_spec_artifact_path)) {
1268
+ throw new Error('build_requires_approved_plan_artifacts');
1269
+ }
688
1270
 
1271
+ const adapter = options.adapter || createDefaultBuildAdapter();
1272
+ const maxIterations = adapter.maxIterations || DEFAULT_BUILD_MAX_ITERATIONS;
1273
+ const noDeslop = Boolean(options.noDeslop);
1274
+ const progressArtifacts = [];
1275
+ const supportArtifacts = [];
1276
+ let iteration = 1;
1277
+ let current = null;
1278
+ let blockers = ['build_not_started'];
1279
+
1280
+ while (iteration <= maxIterations) {
1281
+ current = await adapter.executeLanes({
1282
+ cwd,
1283
+ root,
1284
+ slug: normalized,
1285
+ iteration,
1286
+ noDeslop,
1287
+ planArtifactPath: state.plan_artifact_path,
1288
+ testSpecArtifactPath: state.test_spec_artifact_path,
1289
+ });
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);
1294
+ await writeText(
1295
+ artifactPath(root, 'execution-record.md'),
1296
+ buildExecutionRecordContent({
1297
+ slug: normalized,
1298
+ iterationData: current,
1299
+ complete: blockers.length === 0,
1300
+ }),
1301
+ );
1302
+ if (blockers.length === 0) {
1303
+ break;
1304
+ }
1305
+ iteration += 1;
1306
+ }
1307
+
1308
+ const finalBlocked = blockers.length > 0;
1309
+ const refreshed = await refreshExecutionStatus(root, state);
689
1310
  const next = withRecommendedAction({
690
- ...state,
1311
+ ...refreshed.state,
691
1312
  current_stage: STAGES.BUILD,
692
- stage_status: 'blocked',
693
- execution_record_status: 'partial',
694
- review_status: 'pending-input',
695
- active_run_id: runId,
696
- pending_user_decision: TRANSITIONS.NONE,
1313
+ stage_status: finalBlocked ? 'blocked' : 'awaiting-approval',
1314
+ execution_record_status: finalBlocked ? 'partial' : refreshed.state.execution_record_status,
1315
+ review_status: finalBlocked ? 'pending-input' : 'ready-for-review',
1316
+ build_run_id: current?.runId || null,
1317
+ build_current_iteration: current?.iteration || 0,
1318
+ build_max_iterations: maxIterations,
1319
+ build_parallel_mode: true,
1320
+ build_lane_statuses: current?.lanes || [],
1321
+ build_verification_status: current?.verificationStatus || 'pending',
1322
+ build_architect_verification_status: current?.architectVerdict || 'not-started',
1323
+ build_deslop_status: noDeslop ? 'skipped' : (current?.deslopStatus || 'pending'),
1324
+ build_regression_status: noDeslop ? 'skipped' : (current?.regressionStatus || 'pending'),
1325
+ build_blockers: blockers,
1326
+ build_progress_artifact_paths: progressArtifacts,
1327
+ build_support_evidence_paths: supportArtifacts,
1328
+ build_no_deslop: noDeslop,
1329
+ active_run_id: current?.runId || null,
1330
+ pending_user_decision: finalBlocked ? TRANSITIONS.NONE : TRANSITIONS.BUILD_TO_REVIEW,
697
1331
  requested_transition: TRANSITIONS.NONE,
698
1332
  last_confirmed_transition: TRANSITIONS.PLAN_TO_BUILD,
699
1333
  approval: {
@@ -828,98 +1462,171 @@ export async function reviewStage(cwd, slug, { reviewer = 'independent-reviewer'
828
1462
  return { root, state: next, verdict: reviewInput.verdict, rollbackTarget: reviewInput.rollbackTarget };
829
1463
  }
830
1464
 
831
- export async function autopilotStage(cwd, slug, { reviewer = 'autopilot-reviewer' } = {}) {
1465
+ async function writeAutopilotRun(rootPath, payload) {
1466
+ await ensureDir(dirname(rootPath));
1467
+ await writeText(rootPath, JSON.stringify(payload, null, 2));
1468
+ }
1469
+
1470
+ export async function autopilotStage(cwd, slug, { reviewer = 'autopilot-reviewer', phaseAdapter, planOptions = {}, buildOptions = {} } = {}) {
832
1471
  const normalized = normalizeSlug(slug);
833
1472
  const workflowRoot = resolveWorkflowRoot(cwd, normalized);
834
1473
  if (!existsSync(statePath(workflowRoot))) {
835
1474
  await clarifyStage(cwd, normalized);
836
1475
  }
837
1476
 
838
- const { root } = await loadWorkflowState(cwd, normalized, { allowLegacy: false });
839
- const spec = await readSpecSummary(root);
840
- if (spec.unresolvedCount > 0) {
841
- throw new Error('autopilot_requires_resolved_spec');
842
- }
1477
+ const { root, state: initialState } = await loadWorkflowState(cwd, normalized, { allowLegacy: false });
843
1478
 
1479
+ const adapter = phaseAdapter || createDefaultAutopilotAdapter();
1480
+ const autopilotRoot = join(resolveWorkspaceRoot(cwd), 'autopilot', normalized);
1481
+ const runPath = join(autopilotRoot, 'run.json');
844
1482
  const controlEvents = [];
1483
+ const phases = [];
845
1484
  const recordEvent = (transition) => controlEvents.push({
846
1485
  transition,
847
- actor: 'loopx-autopilot',
1486
+ actor: 'autopilot',
848
1487
  recorded_at: nowIso(),
849
1488
  });
1489
+ const blockerKey = (value) => String(value).trim().toLowerCase().replace(/\s+/g, '-');
1490
+ const artifacts = {
1491
+ specPath: initialState.spec_artifact_path || artifactPath(root, 'spec.md'),
1492
+ planPath: null,
1493
+ testSpecPath: null,
1494
+ executionRecordPath: artifactPath(root, 'execution-record.md'),
1495
+ reviewReportPath: artifactPath(root, 'review-report.md'),
1496
+ };
1497
+
1498
+ const updateWorkflowState = async (state, extras) => {
1499
+ const next = withRecommendedAction({
1500
+ ...state,
1501
+ autopilot_current_phase: extras.currentPhase,
1502
+ autopilot_phase_history: phases,
1503
+ autopilot_blockers: extras.blockers || [],
1504
+ autopilot_run_path: runPath,
1505
+ autopilot_completed: Boolean(extras.completed),
1506
+ });
1507
+ await writeState(root, next);
1508
+ return next;
1509
+ };
1510
+
1511
+ const persistRun = async ({ currentPhase, completed, blockers = [], reviewedRunId = null, workflowState = null }) => {
1512
+ await writeAutopilotRun(runPath, {
1513
+ workflowId: normalized,
1514
+ reviewer,
1515
+ currentPhase,
1516
+ phases,
1517
+ controlEvents,
1518
+ reviewedRunId,
1519
+ artifacts,
1520
+ blockers,
1521
+ completed,
1522
+ });
1523
+ if (workflowState) {
1524
+ await updateWorkflowState(workflowState, { currentPhase, blockers, completed });
1525
+ }
1526
+ };
1527
+
1528
+ const expansion = await adapter.expansion({
1529
+ cwd,
1530
+ slug: normalized,
1531
+ root,
1532
+ state: {
1533
+ ...initialState,
1534
+ cwd,
1535
+ root,
1536
+ slug: normalized,
1537
+ unresolved_ambiguity_count: (await readSpecSummary(root)).unresolvedCount,
1538
+ },
1539
+ });
1540
+ phases.push(expansion);
1541
+ if (expansion.status !== 'complete') {
1542
+ await persistRun({
1543
+ currentPhase: 'expansion',
1544
+ completed: false,
1545
+ blockers: [`expansion_${expansion.status}`],
1546
+ workflowState: initialState,
1547
+ });
1548
+ throw new Error(`autopilot_phase_blocked:expansion:${expansion.status}`);
1549
+ }
1550
+ const refreshedSpec = await readSpecSummary(root);
1551
+ if (refreshedSpec.unresolvedCount > 0) {
1552
+ await persistRun({
1553
+ currentPhase: 'expansion',
1554
+ completed: false,
1555
+ blockers: ['expansion_unresolved_ambiguity'],
1556
+ workflowState: initialState,
1557
+ });
1558
+ throw new Error('autopilot_requires_resolved_spec');
1559
+ }
850
1560
 
851
1561
  await approveStage(cwd, normalized, { from: STAGES.CLARIFY, to: STAGES.PLAN });
852
1562
  recordEvent(TRANSITIONS.CLARIFY_TO_PLAN);
853
- await planStage(cwd, normalized);
1563
+ const planned = await planStage(cwd, normalized, planOptions);
1564
+ artifacts.planPath = planned.state.plan_artifact_path;
1565
+ artifacts.testSpecPath = planned.state.test_spec_artifact_path;
1566
+ const planning = await adapter.planning({ cwd, slug: normalized, root, planResult: planned });
1567
+ phases.push(planning);
1568
+ if (planning.status !== 'complete') {
1569
+ await persistRun({
1570
+ currentPhase: 'planning',
1571
+ completed: false,
1572
+ blockers: [`planning_${planning.status}`],
1573
+ workflowState: planned.state,
1574
+ });
1575
+ throw new Error(`autopilot_phase_blocked:planning:${planning.status}`);
1576
+ }
1577
+
854
1578
  await approveStage(cwd, normalized, { from: STAGES.PLAN, to: STAGES.BUILD });
855
1579
  recordEvent(TRANSITIONS.PLAN_TO_BUILD);
856
- const build = await buildStage(cwd, normalized);
857
- await writeText(
858
- artifactPath(build.root, 'execution-record.md'),
859
- [
860
- frontmatterBlock({
861
- schema_version: WORKFLOW_SCHEMA_VERSION,
862
- workflow_id: normalized,
863
- run_id: `${normalized}-autopilot-run-1`,
864
- stage: STAGES.BUILD,
865
- actor_id: 'loopx-autopilot',
866
- actor_role: 'autopilot',
867
- plan_digest: `plan@${normalized}`,
868
- started_at: nowIso(),
869
- completed_at: nowIso(),
870
- checkpoint_count: 3,
871
- evidence_manifest: [
872
- { id: `${normalized}-autopilot`, kind: 'command', summary: 'loopx autopilot executed', ref: `loopx autopilot ${normalized}` },
873
- { id: `${normalized}-review`, kind: 'artifact', summary: 'review report generated', ref: 'review-report.md' },
874
- ],
875
- }),
876
- `# LoopX Execution Record: ${normalized}`,
877
- '',
878
- '## Changes',
879
- '',
880
- '- LoopX autopilot completed one bounded build path.',
881
- '',
882
- '## Checkpoint Log',
883
- '',
884
- '- checkpoint 1: internal approvals recorded',
885
- '- checkpoint 2: plan and build completed',
886
- '- checkpoint 3: review requested',
887
- '',
888
- '## Execution Evidence',
889
- '',
890
- `- \`loopx autopilot ${normalized}\``,
891
- '',
892
- '## Verification Evidence',
893
- '',
894
- '- PASS: bounded autopilot composition completed',
895
- '- PASS: review-ready evidence generated',
896
- '',
897
- '## Limitations',
898
- '',
899
- '- none',
900
- ].join('\n'),
901
- );
1580
+ const build = await buildStage(cwd, normalized, buildOptions);
1581
+ const execution = await adapter.execution({ cwd, slug: normalized, root, buildResult: build });
1582
+ phases.push(execution);
1583
+ if (execution.status !== 'complete') {
1584
+ await persistRun({
1585
+ currentPhase: 'execution',
1586
+ completed: false,
1587
+ blockers: [`execution_${execution.status}`, ...(build.state.build_blockers || [])],
1588
+ workflowState: build.state,
1589
+ });
1590
+ throw new Error(`autopilot_phase_blocked:execution:${execution.status}`);
1591
+ }
1592
+ const qa = await adapter.qa({ cwd, slug: normalized, root, buildResult: build });
1593
+ phases.push(qa);
1594
+ if (qa.status !== 'complete') {
1595
+ await persistRun({
1596
+ currentPhase: 'qa',
1597
+ completed: false,
1598
+ blockers: [`qa_${qa.status}`, ...(build.state.build_blockers || [])],
1599
+ workflowState: build.state,
1600
+ });
1601
+ throw new Error(`autopilot_phase_blocked:qa:${qa.status}`);
1602
+ }
1603
+
902
1604
  await approveStage(cwd, normalized, { from: STAGES.BUILD, to: STAGES.REVIEW });
903
1605
  recordEvent(TRANSITIONS.BUILD_TO_REVIEW);
904
1606
  const review = await reviewStage(cwd, normalized, { reviewer });
905
- if (review.verdict !== 'APPROVE') {
1607
+ const validation = await adapter.validation({ cwd, slug: normalized, root, reviewResult: review });
1608
+ phases.push(validation);
1609
+ if (review.verdict !== 'APPROVE' || validation.status !== 'complete') {
1610
+ await persistRun({
1611
+ currentPhase: 'validation',
1612
+ completed: false,
1613
+ blockers: [`validation_${blockerKey(validation.status)}`, `review_${blockerKey(review.verdict)}`],
1614
+ reviewedRunId: review.state.active_run_id || null,
1615
+ workflowState: review.state,
1616
+ });
906
1617
  throw new Error('autopilot_review_failed');
907
1618
  }
908
1619
  await approveStage(cwd, normalized, { from: STAGES.REVIEW, to: STAGES.DONE });
909
1620
  recordEvent(TRANSITIONS.REVIEW_TO_DONE);
910
1621
  const done = await reviewStage(cwd, normalized, { reviewer });
911
-
912
- const autopilotRoot = join(resolveWorkspaceRoot(cwd), 'autopilot', normalized);
913
- await ensureDir(autopilotRoot);
914
- await writeText(join(autopilotRoot, 'run.json'), JSON.stringify({
915
- workflowId: normalized,
916
- reviewer,
917
- controlEvents,
918
- reviewedRunId: `${normalized}-autopilot-run-1`,
1622
+ await persistRun({
1623
+ currentPhase: 'complete',
919
1624
  completed: true,
920
- }, null, 2));
921
-
922
- return { root: done.root, state: done.state, runPath: join(autopilotRoot, 'run.json') };
1625
+ reviewedRunId: done.state.active_run_id || build.state.build_run_id || null,
1626
+ workflowState: done.state,
1627
+ });
1628
+ const finalState = await readState(cwd, normalized);
1629
+ return { root: done.root, state: finalState ?? done.state, runPath };
923
1630
  }
924
1631
 
925
1632
  async function listWorkflowSummaries(workflowsRoot) {