@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.
- 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 +238 -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 +43 -7
- package/src/codex-exec-runtime.mjs +97 -0
- package/src/install-discovery.mjs +7 -7
- package/src/plan-runtime.mjs +456 -0
- package/src/runtime-maintenance.mjs +36 -8
- package/src/workflow.mjs +825 -123
- package/templates/architecture.md +3 -3
- package/templates/development-plan.md +1 -1
- package/templates/execution-record.md +1 -1
- package/templates/plan.md +4 -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,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
|
|
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
|
|
297
|
-
|
|
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
|
-
`#
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
:
|
|
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
|
-
`#
|
|
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
|
-
`#
|
|
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
|
|
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: '
|
|
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
|
|
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
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
687
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
-
);
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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) {
|