@ai-content-space/loopx 0.1.0 → 0.1.1

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 (43) 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 +238 -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 +43 -7
  24. package/src/codex-exec-runtime.mjs +97 -0
  25. package/src/install-discovery.mjs +7 -7
  26. package/src/plan-runtime.mjs +456 -0
  27. package/src/runtime-maintenance.mjs +36 -8
  28. package/src/workflow.mjs +825 -123
  29. package/templates/architecture.md +3 -3
  30. package/templates/development-plan.md +1 -1
  31. package/templates/execution-record.md +1 -1
  32. package/templates/plan.md +4 -4
  33. package/templates/review-report.md +1 -1
  34. package/templates/spec.md +38 -2
  35. package/templates/test-plan.md +1 -1
  36. package/plugins/loopx/skills/loopx-autopilot/SKILL.md +0 -30
  37. package/plugins/loopx/skills/loopx-build/SKILL.md +0 -25
  38. package/plugins/loopx/skills/loopx-clarify/SKILL.md +0 -25
  39. package/plugins/loopx/skills/loopx-plan/SKILL.md +0 -25
  40. package/skills/loopx-autopilot/SKILL.md +0 -30
  41. package/skills/loopx-build/SKILL.md +0 -25
  42. package/skills/loopx-clarify/SKILL.md +0 -25
  43. 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,65 @@ 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_docs_status: 'missing',
333
+ plan_docs_artifact_paths: null,
334
+ plan_review_artifact_paths: [],
335
+ plan_blockers: [],
336
+ plan_source_spec_path: null,
337
+ build_run_id: null,
338
+ build_current_iteration: 0,
339
+ build_max_iterations: DEFAULT_BUILD_MAX_ITERATIONS,
340
+ build_parallel_mode: false,
341
+ build_lane_statuses: [],
342
+ build_verification_status: 'pending',
343
+ build_architect_verification_status: 'not-started',
344
+ build_deslop_status: 'pending',
345
+ build_regression_status: 'pending',
346
+ build_blockers: [],
347
+ build_progress_artifact_paths: [],
348
+ build_support_evidence_paths: [],
349
+ build_no_deslop: false,
350
+ autopilot_current_phase: 'none',
351
+ autopilot_phase_history: [],
352
+ autopilot_blockers: [],
353
+ autopilot_run_path: null,
354
+ autopilot_completed: false,
233
355
  review_status: 'not-started',
234
356
  recommended_next_action: 'Resolve ambiguity items in spec.md before requesting approval to enter plan.',
235
357
  rollback_target: 'none',
@@ -293,8 +415,10 @@ async function copyArtifact(fromRoot, toPath, name) {
293
415
  }
294
416
 
295
417
  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`);
418
+ const plansRoot = resolvePlansRoot(cwd);
419
+ await ensureDir(plansRoot);
420
+ const planPath = join(plansRoot, `prd-${slug}.md`);
421
+ const testSpecPath = join(plansRoot, `test-spec-${slug}.md`);
298
422
  const planText = await readFile(artifactPath(root, 'plan.md'), 'utf8');
299
423
  const architectureText = await readFile(artifactPath(root, 'architecture.md'), 'utf8');
300
424
  const developmentPlanText = await readFile(artifactPath(root, 'development-plan.md'), 'utf8');
@@ -303,7 +427,7 @@ async function writeCanonicalPlanArtifacts(cwd, root, slug) {
303
427
  await writeText(
304
428
  planPath,
305
429
  [
306
- `# LoopX PRD: ${slug}`,
430
+ `# loopx PRD: ${slug}`,
307
431
  '',
308
432
  '## Plan',
309
433
  '',
@@ -322,14 +446,339 @@ async function writeCanonicalPlanArtifacts(cwd, root, slug) {
322
446
  return { planPath, testSpecPath };
323
447
  }
324
448
 
449
+ function deriveSlugFromSpecPath(path, text) {
450
+ const meta = parseFrontmatter(text);
451
+ if (meta.workflow_id) {
452
+ return normalizeSlug(meta.workflow_id);
453
+ }
454
+ const name = basename(path).replace(/\.md$/i, '');
455
+ return normalizeSlug(name.replace(/^deep-interview-/, '').replace(/^clarify-/, ''));
456
+ }
457
+
458
+ function containsChineseText(text) {
459
+ return /[\u3400-\u9fff]/.test(text);
460
+ }
461
+
462
+ async function ensurePlanWorkflowFromDirectSpec(cwd, directSpecPath, explicitSlug, options = {}) {
463
+ const resolvedSpecPath = resolve(cwd, directSpecPath);
464
+ const specText = await readFile(resolvedSpecPath, 'utf8');
465
+ const slug = explicitSlug ? normalizeSlug(explicitSlug) : deriveSlugFromSpecPath(resolvedSpecPath, specText);
466
+ const root = resolveWorkflowRoot(cwd, slug);
467
+ await ensureLoopxRoot(cwd);
468
+ await ensureDir(root);
469
+ await writeText(artifactPath(root, 'spec.md'), specText);
470
+
471
+ const existing = await readState(cwd, slug);
472
+ if (existing) {
473
+ const merged = withRecommendedAction({
474
+ ...existing,
475
+ spec_artifact_path: resolvedSpecPath,
476
+ plan_source_spec_path: resolvedSpecPath,
477
+ plan_consensus_mode: true,
478
+ plan_deliberate_mode: Boolean(options.deliberate),
479
+ plan_interactive_mode: Boolean(options.interactive),
480
+ });
481
+ await writeState(root, merged);
482
+ return { slug, root, state: merged };
483
+ }
484
+
485
+ const state = withRecommendedAction({
486
+ ...createInitialState(slug, 'standard'),
487
+ clarify_current_round: 1,
488
+ clarify_ambiguity_score: 0,
489
+ clarify_pressure_pass_complete: true,
490
+ clarify_non_goals_resolved: true,
491
+ clarify_decision_boundaries_resolved: true,
492
+ unresolved_ambiguity_count: 0,
493
+ spec_artifact_path: resolvedSpecPath,
494
+ plan_source_spec_path: resolvedSpecPath,
495
+ requested_transition: TRANSITIONS.CLARIFY_TO_PLAN,
496
+ stage_status: 'awaiting-approval',
497
+ plan_consensus_mode: true,
498
+ plan_deliberate_mode: Boolean(options.deliberate),
499
+ plan_interactive_mode: Boolean(options.interactive),
500
+ approval: {
501
+ ...createInitialState(slug, 'standard').approval,
502
+ plan: APPROVAL_STATES.APPROVED,
503
+ },
504
+ });
505
+ await writeState(root, state);
506
+ return { slug, root, state };
507
+ }
508
+
509
+ async function writePlanArtifacts(root, cwd, slug, plannerDraft) {
510
+ await writeText(artifactPath(root, 'plan.md'), plannerDraft.planText);
511
+ await writeText(artifactPath(root, 'architecture.md'), plannerDraft.architectureText);
512
+ await writeText(artifactPath(root, 'development-plan.md'), plannerDraft.developmentPlanText);
513
+ await writeText(artifactPath(root, 'test-plan.md'), plannerDraft.testPlanText);
514
+
515
+ const docPaths = resolvePlanDocPaths(cwd, slug);
516
+ await ensureDir(docPaths.docsRoot);
517
+ await writeText(docPaths.architecture, plannerDraft.docs.architecture);
518
+ await writeText(docPaths.design, plannerDraft.docs.design);
519
+ await writeText(docPaths.testPlan, plannerDraft.docs.testPlan);
520
+ return docPaths;
521
+ }
522
+
523
+ async function writePlanReviewArtifacts(root, iteration, plannerDraft, architectReview, criticReview) {
524
+ const paths = resolvePlanReviewPaths(root, iteration);
525
+ await ensureDir(paths.reviewsRoot);
526
+ await writeText(
527
+ paths.planner,
528
+ [
529
+ `# Planner Draft: iteration ${iteration}`,
530
+ '',
531
+ '## Principles',
532
+ '',
533
+ ...plannerDraft.principles.map((item) => `- ${item}`),
534
+ '',
535
+ '## Decision Drivers',
536
+ '',
537
+ ...plannerDraft.decisionDrivers.map((item) => `- ${item}`),
538
+ ].join('\n'),
539
+ );
540
+ await writeText(
541
+ paths.architect,
542
+ [
543
+ `# Architect Review: iteration ${iteration}`,
544
+ '',
545
+ `- status: ${architectReview.status}`,
546
+ `- verdict: ${architectReview.verdict}`,
547
+ '',
548
+ '## Findings',
549
+ '',
550
+ ...architectReview.findings.map((item) => `- ${item}`),
551
+ ].join('\n'),
552
+ );
553
+ await writeText(
554
+ paths.critic,
555
+ [
556
+ `# Critic Review: iteration ${iteration}`,
557
+ '',
558
+ `- verdict: ${criticReview.verdict}`,
559
+ '',
560
+ '## Findings',
561
+ '',
562
+ ...criticReview.findings.map((item) => `- ${item}`),
563
+ ].join('\n'),
564
+ );
565
+ return paths;
566
+ }
567
+
568
+ async function readPlanCompletion(cwd, root, slug, state) {
569
+ const blockers = [];
570
+ const docPaths = resolvePlanDocPaths(cwd, slug);
571
+ const docsPresent = {
572
+ architecture: existsSync(docPaths.architecture),
573
+ design: existsSync(docPaths.design),
574
+ testPlan: existsSync(docPaths.testPlan),
575
+ };
576
+ if (state.plan_architect_review_status !== 'complete') {
577
+ blockers.push('architect_review_incomplete');
578
+ }
579
+ if (state.plan_critic_verdict !== 'approve') {
580
+ blockers.push(`critic_verdict_${state.plan_critic_verdict}`);
581
+ }
582
+ if (state.plan_package_status !== 'complete') {
583
+ blockers.push(`plan_package_${state.plan_package_status}`);
584
+ }
585
+ if (!state.plan_acceptance_criteria_testable) {
586
+ blockers.push('acceptance_criteria_unresolved');
587
+ }
588
+ if (!state.plan_verification_steps_resolved) {
589
+ blockers.push('verification_steps_unresolved');
590
+ }
591
+ if (!state.plan_artifact_path || !existsSync(state.plan_artifact_path)) {
592
+ blockers.push('missing_prd');
593
+ }
594
+ if (!state.test_spec_artifact_path || !existsSync(state.test_spec_artifact_path)) {
595
+ blockers.push('missing_test_spec');
596
+ }
597
+ for (const [key, present] of Object.entries(docsPresent)) {
598
+ if (!present) {
599
+ blockers.push(`missing_doc_${key}`);
600
+ continue;
601
+ }
602
+ const text = await readFile(docPaths[key], 'utf8');
603
+ if (!containsChineseText(text)) {
604
+ blockers.push(`doc_not_chinese_${key}`);
605
+ }
606
+ }
607
+
608
+ const docsComplete = Object.values(docsPresent).every(Boolean)
609
+ && blockers.every((blocker) => !blocker.startsWith('doc_not_chinese_') && !blocker.startsWith('missing_doc_'));
610
+
611
+ return {
612
+ blockers,
613
+ docsStatus: docsComplete ? 'complete' : Object.values(docsPresent).some(Boolean) ? 'partial' : 'missing',
614
+ docPaths,
615
+ };
616
+ }
617
+
618
+ function buildIterationBlockers(iterationData, { noDeslop = false } = {}) {
619
+ const blockers = [];
620
+ for (const lane of iterationData.lanes) {
621
+ if (lane.status !== 'complete') {
622
+ blockers.push(`lane_incomplete_${lane.name}`);
623
+ }
624
+ }
625
+ if (iterationData.verificationStatus !== 'complete') {
626
+ blockers.push(`verification_${iterationData.verificationStatus}`);
627
+ }
628
+ if (iterationData.architectVerdict !== 'approve') {
629
+ blockers.push(`architect_${iterationData.architectVerdict}`);
630
+ }
631
+ if (!noDeslop && iterationData.deslopStatus !== 'complete') {
632
+ blockers.push(`deslop_${iterationData.deslopStatus}`);
633
+ }
634
+ if (!noDeslop && iterationData.regressionStatus !== 'complete') {
635
+ blockers.push(`regression_${iterationData.regressionStatus}`);
636
+ }
637
+ return blockers;
638
+ }
639
+
640
+ function buildExecutionRecordContent({ slug, iterationData, complete }) {
641
+ const placeholder = complete ? null : 'TODO: build iteration is not review-ready yet.';
642
+ return [
643
+ frontmatterBlock({
644
+ schema_version: WORKFLOW_SCHEMA_VERSION,
645
+ workflow_id: slug,
646
+ run_id: iterationData.runId,
647
+ stage: STAGES.BUILD,
648
+ actor_id: iterationData.actorId,
649
+ actor_role: STAGES.BUILD,
650
+ plan_digest: `plan@${slug}`,
651
+ started_at: nowIso(),
652
+ completed_at: nowIso(),
653
+ checkpoint_count: iterationData.lanes.length,
654
+ evidence_manifest: iterationData.lanes.flatMap((lane) => lane.evidence || []),
655
+ }),
656
+ `# loopx Execution Record: ${slug}`,
657
+ '',
658
+ '## Changes',
659
+ '',
660
+ '- Completed the current build iteration lanes and aggregated evidence.',
661
+ '',
662
+ '## Checkpoint Log',
663
+ '',
664
+ ...iterationData.lanes.map((lane) => `- ${lane.name}: ${lane.status}`),
665
+ '',
666
+ '## Execution Evidence',
667
+ '',
668
+ ...iterationData.executionEvidence.map((item) => `- ${item}`),
669
+ '',
670
+ '## Verification Evidence',
671
+ '',
672
+ ...iterationData.verificationEvidence.map((item) => `- ${item}`),
673
+ '',
674
+ '## Limitations',
675
+ '',
676
+ ...(placeholder ? [`- ${placeholder}`] : iterationData.limitations.map((item) => `- ${item}`)),
677
+ ].join('\n');
678
+ }
679
+
680
+ async function writeBuildSupportArtifacts(root, iterationData, noDeslop) {
681
+ const paths = resolveBuildSupportPaths(root, iterationData.iteration);
682
+ await ensureDir(paths.supportRoot);
683
+ await writeText(
684
+ paths.laneSummary,
685
+ [
686
+ `# Build Lanes: iteration ${iterationData.iteration}`,
687
+ '',
688
+ ...iterationData.lanes.map((lane) => `- ${lane.name}: ${lane.status} | ${lane.summary}`),
689
+ ].join('\n'),
690
+ );
691
+ await writeText(
692
+ paths.architect,
693
+ [
694
+ `# Build Architect Gate: iteration ${iterationData.iteration}`,
695
+ '',
696
+ `- verdict: ${iterationData.architectVerdict}`,
697
+ '',
698
+ ...iterationData.architectFindings.map((item) => `- ${item}`),
699
+ ].join('\n'),
700
+ );
701
+ await writeText(
702
+ paths.deslop,
703
+ [
704
+ `# Build Deslop: iteration ${iterationData.iteration}`,
705
+ '',
706
+ `- status: ${noDeslop ? 'skipped' : iterationData.deslopStatus}`,
707
+ ].join('\n'),
708
+ );
709
+ await writeText(
710
+ paths.regression,
711
+ [
712
+ `# Build Regression: iteration ${iterationData.iteration}`,
713
+ '',
714
+ `- status: ${noDeslop ? 'skipped' : iterationData.regressionStatus}`,
715
+ ].join('\n'),
716
+ );
717
+ return paths;
718
+ }
719
+
325
720
  async function readSpecSummary(root) {
326
721
  const text = await readTextIfExists(artifactPath(root, 'spec.md'));
327
722
  if (!text) {
328
- return { unresolvedCount: 1 };
723
+ return {
724
+ unresolvedCount: 1,
725
+ currentRound: 0,
726
+ ambiguityScore: 1,
727
+ pressurePassComplete: false,
728
+ nonGoalsResolved: false,
729
+ decisionBoundariesResolved: false,
730
+ };
329
731
  }
330
732
  const meta = parseFrontmatter(text);
331
733
  const unresolvedCount = Number.parseInt(String(meta.unresolved_ambiguity_count ?? 1), 10);
332
- return { unresolvedCount: Number.isNaN(unresolvedCount) ? 1 : unresolvedCount };
734
+ const currentRound = Number.parseInt(String(meta.current_round ?? meta.clarify_current_round ?? 0), 10);
735
+ const ambiguityScore = Number.parseFloat(String(meta.ambiguity_score ?? meta.clarify_ambiguity_score ?? 1));
736
+ return {
737
+ unresolvedCount: Number.isNaN(unresolvedCount) ? 1 : unresolvedCount,
738
+ currentRound: Number.isNaN(currentRound) ? 0 : currentRound,
739
+ ambiguityScore: Number.isFinite(ambiguityScore) && ambiguityScore >= 0 && ambiguityScore <= 1 ? ambiguityScore : 1,
740
+ pressurePassComplete: frontmatterBoolean(meta.pressure_pass_complete ?? meta.clarify_pressure_pass_complete ?? false),
741
+ nonGoalsResolved: frontmatterBoolean(meta.non_goals_resolved ?? meta.clarify_non_goals_resolved ?? false),
742
+ decisionBoundariesResolved: frontmatterBoolean(meta.decision_boundaries_resolved ?? meta.clarify_decision_boundaries_resolved ?? false),
743
+ };
744
+ }
745
+
746
+ function withClarifySummary(state, spec) {
747
+ return {
748
+ ...state,
749
+ clarify_current_round: spec.currentRound,
750
+ clarify_ambiguity_score: spec.ambiguityScore,
751
+ clarify_pressure_pass_complete: spec.pressurePassComplete,
752
+ clarify_non_goals_resolved: spec.nonGoalsResolved,
753
+ clarify_decision_boundaries_resolved: spec.decisionBoundariesResolved,
754
+ unresolved_ambiguity_count: spec.unresolvedCount,
755
+ };
756
+ }
757
+
758
+ function clarifyReadinessBlockers(state) {
759
+ const blockers = [];
760
+ if (state.unresolved_ambiguity_count > 0) {
761
+ blockers.push('unresolved_ambiguity');
762
+ }
763
+ if (state.clarify_current_round <= 0) {
764
+ blockers.push('clarify_current_round_required');
765
+ }
766
+ if (state.clarify_current_round > state.clarify_max_rounds) {
767
+ blockers.push('clarify_max_rounds_exceeded');
768
+ }
769
+ if (state.clarify_ambiguity_score > state.clarify_target_ambiguity_threshold) {
770
+ blockers.push('clarify_ambiguity_score_above_threshold');
771
+ }
772
+ if (!state.clarify_non_goals_resolved) {
773
+ blockers.push('clarify_non_goals_unresolved');
774
+ }
775
+ if (!state.clarify_decision_boundaries_resolved) {
776
+ blockers.push('clarify_decision_boundaries_unresolved');
777
+ }
778
+ if (!state.clarify_pressure_pass_complete) {
779
+ blockers.push('clarify_pressure_pass_incomplete');
780
+ }
781
+ return blockers;
333
782
  }
334
783
 
335
784
  async function readExecutionRecordSummary(root) {
@@ -363,19 +812,25 @@ async function readExecutionRecordSummary(root) {
363
812
 
364
813
  function recommendedAction(state, legacy = false) {
365
814
  if (legacy) {
366
- return 'Legacy codex-helper workflow detected. Run loopx migrate or create a new LoopX workflow.';
815
+ return 'Legacy codex-helper workflow detected. Run loopx migrate or create a new loopx workflow.';
367
816
  }
368
817
 
369
818
  switch (state.current_stage) {
370
819
  case STAGES.CLARIFY:
371
820
  return state.approval.plan === APPROVAL_STATES.APPROVED
372
821
  ? 'Run loopx plan to consume the approved clarify -> plan transition.'
373
- : 'Resolve ambiguity and approve clarify -> plan.';
822
+ : `Resolve ambiguity in ${state.clarify_profile ?? 'standard'} clarify mode and approve clarify -> plan.`;
374
823
  case STAGES.PLAN:
824
+ if (Array.isArray(state.plan_blockers) && state.plan_blockers.length > 0) {
825
+ return 'Run loopx plan to continue the planning review loop until architect, critic, and docs blockers are cleared.';
826
+ }
375
827
  return state.approval.build === APPROVAL_STATES.APPROVED
376
828
  ? 'Run loopx build to consume the approved plan -> build transition.'
377
829
  : 'Approve plan -> build when the plan package is ready.';
378
830
  case STAGES.BUILD:
831
+ if (Array.isArray(state.build_blockers) && state.build_blockers.length > 0) {
832
+ return 'Run loopx build to continue the execution loop until verification, architect, deslop, and regression blockers are cleared.';
833
+ }
379
834
  return state.approval.review === APPROVAL_STATES.APPROVED
380
835
  ? 'Run loopx review to consume the approved build -> review transition.'
381
836
  : 'Approve build -> review when execution-record.md is complete.';
@@ -392,6 +847,9 @@ function recommendedAction(state, legacy = false) {
392
847
  }
393
848
  return 'Run loopx review after build completes.';
394
849
  case STAGES.DONE:
850
+ if (state.autopilot_current_phase && state.autopilot_current_phase !== 'none' && state.autopilot_completed) {
851
+ return 'Autopilot run is complete.';
852
+ }
395
853
  return 'Workflow is complete.';
396
854
  default:
397
855
  return 'Run loopx clarify to start a workflow.';
@@ -466,7 +924,7 @@ function executionRecordTemplate(slug, stage, actorId, runId) {
466
924
  checkpoint_count: 0,
467
925
  evidence_manifest: [],
468
926
  }),
469
- `# LoopX Execution Record: ${slug}`,
927
+ `# loopx Execution Record: ${slug}`,
470
928
  '',
471
929
  '## Changes',
472
930
  '',
@@ -504,7 +962,7 @@ function reviewReportContent({ slug, reviewer, runId, verdict, rollbackTarget, r
504
962
  rollback_target: rollbackTarget,
505
963
  rollback_rationale: rollbackRationale ?? null,
506
964
  }),
507
- `# LoopX Review Report: ${slug}`,
965
+ `# loopx Review Report: ${slug}`,
508
966
  '',
509
967
  '## Verdict',
510
968
  '',
@@ -538,7 +996,7 @@ async function refreshExecutionStatus(root, state) {
538
996
 
539
997
  export async function initWorkspace(cwd, { slug } = {}) {
540
998
  const workspaceRoot = resolveWorkspaceRoot(cwd);
541
- await ensureLoopXRoot(cwd);
999
+ await ensureLoopxRoot(cwd);
542
1000
  await ensureDir(join(workspaceRoot, 'context'));
543
1001
  await ensureDir(join(workspaceRoot, 'workflows'));
544
1002
  await ensureDir(join(workspaceRoot, 'specs'));
@@ -547,7 +1005,7 @@ export async function initWorkspace(cwd, { slug } = {}) {
547
1005
 
548
1006
  const config = {
549
1007
  schema_version: WORKSPACE_SCHEMA_VERSION,
550
- tool: 'LoopX',
1008
+ tool: 'loopx',
551
1009
  product_contract: 'skill-first-v1',
552
1010
  default_flow: ['clarify', 'plan', 'build', 'review', 'done'],
553
1011
  preferred_surface: ['clarify', 'plan', 'build', 'review', 'autopilot'],
@@ -567,20 +1025,24 @@ export async function initWorkspace(cwd, { slug } = {}) {
567
1025
  return { workspaceRoot, config, workflow };
568
1026
  }
569
1027
 
570
- export async function clarifyStage(cwd, slug) {
1028
+ export async function clarifyStage(cwd, slug, { profile = 'standard' } = {}) {
571
1029
  const normalized = normalizeSlug(slug);
1030
+ const clarifyProfile = normalizeClarifyProfile(profile);
572
1031
  const root = resolveWorkflowRoot(cwd, normalized);
573
- await ensureLoopXRoot(cwd);
1032
+ await ensureLoopxRoot(cwd);
574
1033
  await ensureDir(root);
575
1034
  const stamp = nowStamp();
576
1035
  await writeTemplateArtifact(root, 'spec.md', {
577
1036
  'task name': normalized,
578
1037
  'workflow id': normalized,
1038
+ profile: clarifyProfile,
1039
+ 'target ambiguity threshold': CLARIFY_PROFILES[clarifyProfile].threshold,
1040
+ 'max rounds': CLARIFY_PROFILES[clarifyProfile].maxRounds,
579
1041
  });
580
1042
  const specArtifactPath = canonicalClarifySpecPath(cwd, normalized, stamp);
581
1043
  await copyArtifact(root, specArtifactPath, 'spec.md');
582
1044
  const state = withRecommendedAction({
583
- ...createInitialState(normalized),
1045
+ ...createInitialState(normalized, clarifyProfile),
584
1046
  spec_artifact_path: specArtifactPath,
585
1047
  });
586
1048
  await writeState(root, state);
@@ -598,9 +1060,17 @@ export async function approveStage(cwd, slug, { from, to }) {
598
1060
 
599
1061
  if (transition === TRANSITIONS.CLARIFY_TO_PLAN) {
600
1062
  const spec = await readSpecSummary(root);
601
- next.unresolved_ambiguity_count = spec.unresolvedCount;
602
- if (spec.unresolvedCount > 0) {
603
- throw new Error('unresolved_ambiguity');
1063
+ next = withClarifySummary(next, spec);
1064
+ const blockers = clarifyReadinessBlockers(next);
1065
+ if (blockers.length > 0) {
1066
+ const blocked = withRecommendedAction({
1067
+ ...next,
1068
+ stage_status: 'blocked',
1069
+ pending_user_decision: TRANSITIONS.CLARIFY_TO_PLAN,
1070
+ requested_transition: TRANSITIONS.NONE,
1071
+ });
1072
+ await writeState(root, blocked);
1073
+ throw new Error(`clarify_readiness_blocked:${blockers.join(',')}`);
604
1074
  }
605
1075
  }
606
1076
 
@@ -609,6 +1079,23 @@ export async function approveStage(cwd, slug, { from, to }) {
609
1079
  throw new Error('plan_package_incomplete');
610
1080
  }
611
1081
  next.plan_package_status = 'complete';
1082
+ const completion = await readPlanCompletion(cwd, root, state.slug, next);
1083
+ next = {
1084
+ ...next,
1085
+ plan_docs_status: completion.docsStatus,
1086
+ plan_docs_artifact_paths: completion.docPaths,
1087
+ plan_blockers: completion.blockers,
1088
+ };
1089
+ if (completion.blockers.length > 0) {
1090
+ const blocked = withRecommendedAction({
1091
+ ...next,
1092
+ stage_status: 'blocked',
1093
+ pending_user_decision: TRANSITIONS.PLAN_TO_BUILD,
1094
+ requested_transition: TRANSITIONS.NONE,
1095
+ });
1096
+ await writeState(root, blocked);
1097
+ throw new Error(`plan_review_gate_blocked:${completion.blockers.join(',')}`);
1098
+ }
612
1099
  }
613
1100
 
614
1101
  if (transition === TRANSITIONS.BUILD_TO_REVIEW) {
@@ -617,6 +1104,16 @@ export async function approveStage(cwd, slug, { from, to }) {
617
1104
  if (next.execution_record_status !== 'complete') {
618
1105
  throw new Error('review_gate_blocked:execution-record.md');
619
1106
  }
1107
+ if (Array.isArray(next.build_blockers) && next.build_blockers.length > 0) {
1108
+ const blocked = withRecommendedAction({
1109
+ ...next,
1110
+ stage_status: 'blocked',
1111
+ pending_user_decision: TRANSITIONS.BUILD_TO_REVIEW,
1112
+ requested_transition: TRANSITIONS.NONE,
1113
+ });
1114
+ await writeState(root, blocked);
1115
+ throw new Error(`build_review_gate_blocked:${next.build_blockers.join(',')}`);
1116
+ }
620
1117
  }
621
1118
 
622
1119
  if (transition === TRANSITIONS.REVIEW_TO_PLAN) {
@@ -643,56 +1140,188 @@ export async function approveStage(cwd, slug, { from, to }) {
643
1140
  return { root, state: next };
644
1141
  }
645
1142
 
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,
1143
+ export async function planStage(cwd, slug, options = {}) {
1144
+ let normalized = slug ? normalizeSlug(slug) : null;
1145
+ if (options.directSpecPath) {
1146
+ const bootstrapped = await ensurePlanWorkflowFromDirectSpec(cwd, options.directSpecPath, normalized, options);
1147
+ normalized = bootstrapped.slug;
1148
+ }
1149
+
1150
+ const loaded = await loadWorkflowState(cwd, normalized, { allowLegacy: false });
1151
+ const { root } = loaded;
1152
+ let { state } = loaded;
1153
+ if (!options.directSpecPath) {
1154
+ ensureApprovedTransition(state, TRANSITIONS.CLARIFY_TO_PLAN, 'plan');
1155
+ if (state.spec_artifact_path) {
1156
+ await copyArtifact(root, state.spec_artifact_path, 'spec.md');
1157
+ }
1158
+ }
1159
+
1160
+ const sourceSpecPath = options.directSpecPath ? resolve(cwd, options.directSpecPath) : (state.plan_source_spec_path || artifactPath(root, 'spec.md'));
1161
+ const sourceText = await readFile(sourceSpecPath, 'utf8');
1162
+ const adapter = options.adapter || createDefaultPlanAdapter();
1163
+ const maxIterations = DEFAULT_MAX_ITERATIONS;
1164
+ let iteration = 1;
1165
+ let architectReview = null;
1166
+ let criticReview = null;
1167
+ const reviewArtifactPaths = [];
1168
+
1169
+ while (iteration <= maxIterations) {
1170
+ const plannerDraft = await adapter.planner({
1171
+ cwd,
1172
+ root,
1173
+ slug: normalized,
1174
+ sourceText,
1175
+ iteration,
1176
+ deliberateMode: Boolean(options.deliberate),
1177
+ interactiveMode: Boolean(options.interactive),
1178
+ });
1179
+ const docPaths = await writePlanArtifacts(root, cwd, normalized, plannerDraft);
1180
+ const artifactPaths = await writeCanonicalPlanArtifacts(cwd, root, normalized);
1181
+
1182
+ architectReview = await adapter.architect({
1183
+ cwd,
1184
+ root,
1185
+ slug: normalized,
1186
+ sourceText,
1187
+ plannerDraft,
1188
+ iteration,
1189
+ deliberateMode: Boolean(options.deliberate),
656
1190
  });
1191
+ criticReview = await adapter.critic({
1192
+ cwd,
1193
+ root,
1194
+ slug: normalized,
1195
+ sourceText,
1196
+ plannerDraft,
1197
+ architectReview,
1198
+ iteration,
1199
+ deliberateMode: Boolean(options.deliberate),
1200
+ });
1201
+ const reviewPaths = await writePlanReviewArtifacts(root, iteration, plannerDraft, architectReview, criticReview);
1202
+ reviewArtifactPaths.push(reviewPaths);
1203
+
1204
+ state = {
1205
+ ...state,
1206
+ current_stage: STAGES.PLAN,
1207
+ plan_current_iteration: iteration,
1208
+ plan_max_iterations: maxIterations,
1209
+ plan_consensus_mode: true,
1210
+ plan_deliberate_mode: Boolean(options.deliberate),
1211
+ plan_interactive_mode: Boolean(options.interactive),
1212
+ plan_principles_resolved: plannerDraft.principlesResolved,
1213
+ plan_options_reviewed: plannerDraft.optionsReviewed,
1214
+ plan_architect_review_status: architectReview.status,
1215
+ plan_critic_verdict: criticReview.verdict,
1216
+ plan_acceptance_criteria_testable: criticReview.acceptanceCriteriaTestable,
1217
+ plan_verification_steps_resolved: criticReview.verificationStepsResolved,
1218
+ plan_package_status: 'complete',
1219
+ plan_docs_artifact_paths: docPaths,
1220
+ plan_review_artifact_paths: reviewArtifactPaths,
1221
+ plan_artifact_path: artifactPaths.planPath,
1222
+ test_spec_artifact_path: artifactPaths.testSpecPath,
1223
+ plan_source_spec_path: sourceSpecPath,
1224
+ last_confirmed_transition: TRANSITIONS.CLARIFY_TO_PLAN,
1225
+ approval: {
1226
+ ...state.approval,
1227
+ plan: APPROVAL_STATES.APPROVED,
1228
+ build: APPROVAL_STATES.NOT_REQUESTED,
1229
+ review: APPROVAL_STATES.NOT_REQUESTED,
1230
+ rollback: APPROVAL_STATES.NOT_REQUESTED,
1231
+ complete: APPROVAL_STATES.NOT_REQUESTED,
1232
+ },
1233
+ };
1234
+
1235
+ if (criticReview.verdict === 'approve') {
1236
+ break;
1237
+ }
1238
+ iteration += 1;
657
1239
  }
658
- const { planPath, testSpecPath } = await writeCanonicalPlanArtifacts(cwd, root, normalized);
659
1240
 
1241
+ const completion = await readPlanCompletion(cwd, root, normalized, state);
660
1242
  const next = withRecommendedAction({
661
1243
  ...state,
662
1244
  current_stage: STAGES.PLAN,
663
- stage_status: 'awaiting-approval',
664
- plan_package_status: 'complete',
1245
+ stage_status: completion.blockers.length > 0 ? 'blocked' : 'awaiting-approval',
665
1246
  pending_user_decision: TRANSITIONS.NONE,
666
1247
  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,
1248
+ plan_docs_status: completion.docsStatus,
1249
+ plan_docs_artifact_paths: completion.docPaths,
1250
+ plan_blockers: completion.blockers,
678
1251
  });
679
1252
  await writeState(root, next);
680
- return { root, state: next };
1253
+ return { root, state: next, architectReview, criticReview };
681
1254
  }
682
1255
 
683
- export async function buildStage(cwd, slug) {
1256
+ export async function buildStage(cwd, slug, options = {}) {
684
1257
  const { root, state, slug: normalized } = await loadWorkflowState(cwd, slug, { allowLegacy: false });
685
1258
  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));
1259
+ if (!PLAN_ARTIFACTS.every((name) => existsSync(artifactPath(root, name)))) {
1260
+ throw new Error('build_requires_workflow_plan_artifacts');
1261
+ }
1262
+ if (!state.plan_artifact_path || !existsSync(state.plan_artifact_path) || !state.test_spec_artifact_path || !existsSync(state.test_spec_artifact_path)) {
1263
+ throw new Error('build_requires_approved_plan_artifacts');
1264
+ }
688
1265
 
1266
+ const adapter = options.adapter || createDefaultBuildAdapter();
1267
+ const maxIterations = adapter.maxIterations || DEFAULT_BUILD_MAX_ITERATIONS;
1268
+ const noDeslop = Boolean(options.noDeslop);
1269
+ const progressArtifacts = [];
1270
+ const supportArtifacts = [];
1271
+ let iteration = 1;
1272
+ let current = null;
1273
+ let blockers = ['build_not_started'];
1274
+
1275
+ while (iteration <= maxIterations) {
1276
+ current = await adapter.executeLanes({
1277
+ cwd,
1278
+ root,
1279
+ slug: normalized,
1280
+ iteration,
1281
+ noDeslop,
1282
+ planArtifactPath: state.plan_artifact_path,
1283
+ testSpecArtifactPath: state.test_spec_artifact_path,
1284
+ });
1285
+ blockers = buildIterationBlockers(current, { noDeslop });
1286
+ const supportPaths = await writeBuildSupportArtifacts(root, current, noDeslop);
1287
+ progressArtifacts.push(supportPaths.laneSummary);
1288
+ supportArtifacts.push(supportPaths.architect, supportPaths.deslop, supportPaths.regression);
1289
+ await writeText(
1290
+ artifactPath(root, 'execution-record.md'),
1291
+ buildExecutionRecordContent({
1292
+ slug: normalized,
1293
+ iterationData: current,
1294
+ complete: blockers.length === 0,
1295
+ }),
1296
+ );
1297
+ if (blockers.length === 0) {
1298
+ break;
1299
+ }
1300
+ iteration += 1;
1301
+ }
1302
+
1303
+ const finalBlocked = blockers.length > 0;
1304
+ const refreshed = await refreshExecutionStatus(root, state);
689
1305
  const next = withRecommendedAction({
690
- ...state,
1306
+ ...refreshed.state,
691
1307
  current_stage: STAGES.BUILD,
692
- stage_status: 'blocked',
693
- execution_record_status: 'partial',
694
- review_status: 'pending-input',
695
- active_run_id: runId,
1308
+ stage_status: finalBlocked ? 'blocked' : 'awaiting-approval',
1309
+ execution_record_status: finalBlocked ? 'partial' : refreshed.state.execution_record_status,
1310
+ review_status: finalBlocked ? 'pending-input' : 'ready-for-review',
1311
+ build_run_id: current?.runId || null,
1312
+ build_current_iteration: current?.iteration || 0,
1313
+ build_max_iterations: maxIterations,
1314
+ build_parallel_mode: true,
1315
+ build_lane_statuses: current?.lanes || [],
1316
+ build_verification_status: current?.verificationStatus || 'pending',
1317
+ build_architect_verification_status: current?.architectVerdict || 'not-started',
1318
+ build_deslop_status: noDeslop ? 'skipped' : (current?.deslopStatus || 'pending'),
1319
+ build_regression_status: noDeslop ? 'skipped' : (current?.regressionStatus || 'pending'),
1320
+ build_blockers: blockers,
1321
+ build_progress_artifact_paths: progressArtifacts,
1322
+ build_support_evidence_paths: supportArtifacts,
1323
+ build_no_deslop: noDeslop,
1324
+ active_run_id: current?.runId || null,
696
1325
  pending_user_decision: TRANSITIONS.NONE,
697
1326
  requested_transition: TRANSITIONS.NONE,
698
1327
  last_confirmed_transition: TRANSITIONS.PLAN_TO_BUILD,
@@ -828,98 +1457,171 @@ export async function reviewStage(cwd, slug, { reviewer = 'independent-reviewer'
828
1457
  return { root, state: next, verdict: reviewInput.verdict, rollbackTarget: reviewInput.rollbackTarget };
829
1458
  }
830
1459
 
831
- export async function autopilotStage(cwd, slug, { reviewer = 'autopilot-reviewer' } = {}) {
1460
+ async function writeAutopilotRun(rootPath, payload) {
1461
+ await ensureDir(dirname(rootPath));
1462
+ await writeText(rootPath, JSON.stringify(payload, null, 2));
1463
+ }
1464
+
1465
+ export async function autopilotStage(cwd, slug, { reviewer = 'autopilot-reviewer', phaseAdapter, planOptions = {}, buildOptions = {} } = {}) {
832
1466
  const normalized = normalizeSlug(slug);
833
1467
  const workflowRoot = resolveWorkflowRoot(cwd, normalized);
834
1468
  if (!existsSync(statePath(workflowRoot))) {
835
1469
  await clarifyStage(cwd, normalized);
836
1470
  }
837
1471
 
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
- }
1472
+ const { root, state: initialState } = await loadWorkflowState(cwd, normalized, { allowLegacy: false });
843
1473
 
1474
+ const adapter = phaseAdapter || createDefaultAutopilotAdapter();
1475
+ const autopilotRoot = join(resolveWorkspaceRoot(cwd), 'autopilot', normalized);
1476
+ const runPath = join(autopilotRoot, 'run.json');
844
1477
  const controlEvents = [];
1478
+ const phases = [];
845
1479
  const recordEvent = (transition) => controlEvents.push({
846
1480
  transition,
847
- actor: 'loopx-autopilot',
1481
+ actor: 'autopilot',
848
1482
  recorded_at: nowIso(),
849
1483
  });
1484
+ const blockerKey = (value) => String(value).trim().toLowerCase().replace(/\s+/g, '-');
1485
+ const artifacts = {
1486
+ specPath: initialState.spec_artifact_path || artifactPath(root, 'spec.md'),
1487
+ planPath: null,
1488
+ testSpecPath: null,
1489
+ executionRecordPath: artifactPath(root, 'execution-record.md'),
1490
+ reviewReportPath: artifactPath(root, 'review-report.md'),
1491
+ };
1492
+
1493
+ const updateWorkflowState = async (state, extras) => {
1494
+ const next = withRecommendedAction({
1495
+ ...state,
1496
+ autopilot_current_phase: extras.currentPhase,
1497
+ autopilot_phase_history: phases,
1498
+ autopilot_blockers: extras.blockers || [],
1499
+ autopilot_run_path: runPath,
1500
+ autopilot_completed: Boolean(extras.completed),
1501
+ });
1502
+ await writeState(root, next);
1503
+ return next;
1504
+ };
1505
+
1506
+ const persistRun = async ({ currentPhase, completed, blockers = [], reviewedRunId = null, workflowState = null }) => {
1507
+ await writeAutopilotRun(runPath, {
1508
+ workflowId: normalized,
1509
+ reviewer,
1510
+ currentPhase,
1511
+ phases,
1512
+ controlEvents,
1513
+ reviewedRunId,
1514
+ artifacts,
1515
+ blockers,
1516
+ completed,
1517
+ });
1518
+ if (workflowState) {
1519
+ await updateWorkflowState(workflowState, { currentPhase, blockers, completed });
1520
+ }
1521
+ };
1522
+
1523
+ const expansion = await adapter.expansion({
1524
+ cwd,
1525
+ slug: normalized,
1526
+ root,
1527
+ state: {
1528
+ ...initialState,
1529
+ cwd,
1530
+ root,
1531
+ slug: normalized,
1532
+ unresolved_ambiguity_count: (await readSpecSummary(root)).unresolvedCount,
1533
+ },
1534
+ });
1535
+ phases.push(expansion);
1536
+ if (expansion.status !== 'complete') {
1537
+ await persistRun({
1538
+ currentPhase: 'expansion',
1539
+ completed: false,
1540
+ blockers: [`expansion_${expansion.status}`],
1541
+ workflowState: initialState,
1542
+ });
1543
+ throw new Error(`autopilot_phase_blocked:expansion:${expansion.status}`);
1544
+ }
1545
+ const refreshedSpec = await readSpecSummary(root);
1546
+ if (refreshedSpec.unresolvedCount > 0) {
1547
+ await persistRun({
1548
+ currentPhase: 'expansion',
1549
+ completed: false,
1550
+ blockers: ['expansion_unresolved_ambiguity'],
1551
+ workflowState: initialState,
1552
+ });
1553
+ throw new Error('autopilot_requires_resolved_spec');
1554
+ }
850
1555
 
851
1556
  await approveStage(cwd, normalized, { from: STAGES.CLARIFY, to: STAGES.PLAN });
852
1557
  recordEvent(TRANSITIONS.CLARIFY_TO_PLAN);
853
- await planStage(cwd, normalized);
1558
+ const planned = await planStage(cwd, normalized, planOptions);
1559
+ artifacts.planPath = planned.state.plan_artifact_path;
1560
+ artifacts.testSpecPath = planned.state.test_spec_artifact_path;
1561
+ const planning = await adapter.planning({ cwd, slug: normalized, root, planResult: planned });
1562
+ phases.push(planning);
1563
+ if (planning.status !== 'complete') {
1564
+ await persistRun({
1565
+ currentPhase: 'planning',
1566
+ completed: false,
1567
+ blockers: [`planning_${planning.status}`],
1568
+ workflowState: planned.state,
1569
+ });
1570
+ throw new Error(`autopilot_phase_blocked:planning:${planning.status}`);
1571
+ }
1572
+
854
1573
  await approveStage(cwd, normalized, { from: STAGES.PLAN, to: STAGES.BUILD });
855
1574
  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
- );
1575
+ const build = await buildStage(cwd, normalized, buildOptions);
1576
+ const execution = await adapter.execution({ cwd, slug: normalized, root, buildResult: build });
1577
+ phases.push(execution);
1578
+ if (execution.status !== 'complete') {
1579
+ await persistRun({
1580
+ currentPhase: 'execution',
1581
+ completed: false,
1582
+ blockers: [`execution_${execution.status}`, ...(build.state.build_blockers || [])],
1583
+ workflowState: build.state,
1584
+ });
1585
+ throw new Error(`autopilot_phase_blocked:execution:${execution.status}`);
1586
+ }
1587
+ const qa = await adapter.qa({ cwd, slug: normalized, root, buildResult: build });
1588
+ phases.push(qa);
1589
+ if (qa.status !== 'complete') {
1590
+ await persistRun({
1591
+ currentPhase: 'qa',
1592
+ completed: false,
1593
+ blockers: [`qa_${qa.status}`, ...(build.state.build_blockers || [])],
1594
+ workflowState: build.state,
1595
+ });
1596
+ throw new Error(`autopilot_phase_blocked:qa:${qa.status}`);
1597
+ }
1598
+
902
1599
  await approveStage(cwd, normalized, { from: STAGES.BUILD, to: STAGES.REVIEW });
903
1600
  recordEvent(TRANSITIONS.BUILD_TO_REVIEW);
904
1601
  const review = await reviewStage(cwd, normalized, { reviewer });
905
- if (review.verdict !== 'APPROVE') {
1602
+ const validation = await adapter.validation({ cwd, slug: normalized, root, reviewResult: review });
1603
+ phases.push(validation);
1604
+ if (review.verdict !== 'APPROVE' || validation.status !== 'complete') {
1605
+ await persistRun({
1606
+ currentPhase: 'validation',
1607
+ completed: false,
1608
+ blockers: [`validation_${blockerKey(validation.status)}`, `review_${blockerKey(review.verdict)}`],
1609
+ reviewedRunId: review.state.active_run_id || null,
1610
+ workflowState: review.state,
1611
+ });
906
1612
  throw new Error('autopilot_review_failed');
907
1613
  }
908
1614
  await approveStage(cwd, normalized, { from: STAGES.REVIEW, to: STAGES.DONE });
909
1615
  recordEvent(TRANSITIONS.REVIEW_TO_DONE);
910
1616
  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`,
1617
+ await persistRun({
1618
+ currentPhase: 'complete',
919
1619
  completed: true,
920
- }, null, 2));
921
-
922
- return { root: done.root, state: done.state, runPath: join(autopilotRoot, 'run.json') };
1620
+ reviewedRunId: done.state.active_run_id || build.state.build_run_id || null,
1621
+ workflowState: done.state,
1622
+ });
1623
+ const finalState = await readState(cwd, normalized);
1624
+ return { root: done.root, state: finalState ?? done.state, runPath };
923
1625
  }
924
1626
 
925
1627
  async function listWorkflowSummaries(workflowsRoot) {