@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.
- package/README.md +26 -26
- package/package.json +6 -2
- package/plugins/loopx/.codex-plugin/plugin.json +6 -6
- package/plugins/loopx/scripts/plugin-install.test.mjs +25 -8
- package/plugins/loopx/skills/autopilot/SKILL.md +90 -0
- package/plugins/loopx/skills/build/SKILL.md +118 -0
- package/plugins/loopx/skills/clarify/SKILL.md +219 -0
- package/plugins/loopx/skills/plan/SKILL.md +238 -0
- package/plugins/loopx/skills/{loopx-review → review}/SKILL.md +9 -4
- package/skills/ai-slop-cleaner/SKILL.md +114 -0
- package/skills/autopilot/SKILL.md +90 -0
- package/skills/autoresearch/SKILL.md +68 -0
- package/skills/build/SKILL.md +118 -0
- package/skills/clarify/SKILL.md +219 -0
- package/skills/deep-interview/SKILL.md +461 -0
- package/skills/deepsearch/SKILL.md +38 -0
- package/skills/plan/SKILL.md +242 -0
- package/skills/ralph/SKILL.md +271 -0
- package/skills/ralplan/SKILL.md +49 -0
- package/skills/{loopx-review → review}/SKILL.md +9 -4
- package/src/autopilot-runtime.mjs +152 -0
- package/src/build-runtime.mjs +146 -0
- package/src/cli.mjs +49 -12
- package/src/codex-exec-runtime.mjs +97 -0
- package/src/install-discovery.mjs +7 -7
- package/src/next-skill.mjs +33 -0
- package/src/plan-runtime.mjs +477 -0
- package/src/runtime-maintenance.mjs +36 -8
- package/src/workflow.mjs +831 -124
- package/templates/architecture.md +3 -3
- package/templates/development-plan.md +1 -1
- package/templates/execution-record.md +1 -1
- package/templates/plan.md +10 -4
- package/templates/review-report.md +1 -1
- package/templates/spec.md +38 -2
- package/templates/test-plan.md +1 -1
- package/plugins/loopx/skills/loopx-autopilot/SKILL.md +0 -30
- package/plugins/loopx/skills/loopx-build/SKILL.md +0 -25
- package/plugins/loopx/skills/loopx-clarify/SKILL.md +0 -25
- package/plugins/loopx/skills/loopx-plan/SKILL.md +0 -25
- package/skills/loopx-autopilot/SKILL.md +0 -30
- package/skills/loopx-build/SKILL.md +0 -25
- package/skills/loopx-clarify/SKILL.md +0 -25
- 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 {
|
|
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
|
|
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
|
-
'#
|
|
272
|
+
'# loopx Workspace',
|
|
194
273
|
'',
|
|
195
|
-
'This directory is initialized for the
|
|
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
|
|
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
|
|
297
|
-
|
|
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
|
-
`#
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
:
|
|
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
|
-
`#
|
|
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
|
-
`#
|
|
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
|
|
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: '
|
|
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
|
|
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
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
|
|
687
|
-
|
|
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
|
-
|
|
696
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
857
|
-
await
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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) {
|