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