@ai-content-space/loopx 0.1.2 → 0.1.4
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 +422 -57
- package/README.zh-CN.md +485 -0
- package/assets/logo.svg +89 -0
- package/package.json +5 -1
- package/plugins/loopx/.codex-plugin/plugin.json +1 -1
- package/plugins/loopx/scripts/plugin-install.test.mjs +14 -0
- package/plugins/loopx/skills/archive/SKILL.md +49 -0
- package/plugins/loopx/skills/build/SKILL.md +111 -9
- package/plugins/loopx/skills/clarify/SKILL.md +129 -8
- 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 +24 -3
- 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 +248 -0
- package/skills/archive/SKILL.md +49 -0
- package/skills/build/SKILL.md +111 -9
- package/skills/clarify/SKILL.md +129 -8
- 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 +20 -3
- 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 +311 -26
- package/src/build-stop-gate.mjs +94 -0
- package/src/cli.mjs +57 -5
- package/src/codex-exec-runtime.mjs +105 -5
- package/src/context-manifest.mjs +172 -0
- package/src/html-views.mjs +316 -0
- package/src/install-discovery.mjs +352 -5
- package/src/next-skill.mjs +57 -5
- package/src/plan-runtime.mjs +102 -122
- package/src/review-runtime.mjs +558 -0
- package/src/runtime-maintenance.mjs +429 -14
- package/src/template-governance.mjs +223 -0
- package/src/workflow.mjs +2341 -120
- package/src/workspace-context.mjs +166 -0
- package/src/workspace-memory.mjs +69 -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,32 @@ function resolveSpecsRoot(cwd) {
|
|
|
208
251
|
return join(resolveWorkspaceRoot(cwd), 'specs');
|
|
209
252
|
}
|
|
210
253
|
|
|
211
|
-
function
|
|
212
|
-
return join(resolveWorkspaceRoot(cwd), '
|
|
254
|
+
function resolveIntakeRoot(cwd) {
|
|
255
|
+
return join(resolveWorkspaceRoot(cwd), 'intake');
|
|
213
256
|
}
|
|
214
257
|
|
|
215
|
-
function
|
|
216
|
-
return join(
|
|
258
|
+
function resolveChangesRoot(cwd) {
|
|
259
|
+
return join(resolveWorkspaceRoot(cwd), 'changes');
|
|
217
260
|
}
|
|
218
261
|
|
|
219
|
-
function
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
262
|
+
function changeIdForWorkflowSlug(slug) {
|
|
263
|
+
return `chg-${normalizeSlug(slug)}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function resolveChangeRoot(cwd, changeId) {
|
|
267
|
+
return join(resolveChangesRoot(cwd), 'active', normalizeSlug(changeId));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function resolveArchivedChangeRoot(cwd, changeId) {
|
|
271
|
+
return join(resolveChangesRoot(cwd), 'archive', normalizeSlug(changeId));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function resolvePlansRoot(cwd) {
|
|
275
|
+
return join(resolveWorkspaceRoot(cwd), 'plans');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function resolveContextRoot(cwd) {
|
|
279
|
+
return join(resolveWorkspaceRoot(cwd), 'context');
|
|
227
280
|
}
|
|
228
281
|
|
|
229
282
|
function resolvePlanReviewPaths(root, iteration) {
|
|
@@ -244,11 +297,13 @@ function resolveBuildSupportPaths(root, iteration) {
|
|
|
244
297
|
architect: join(supportRoot, `architect-iteration-${iteration}.md`),
|
|
245
298
|
deslop: join(supportRoot, `deslop-iteration-${iteration}.md`),
|
|
246
299
|
regression: join(supportRoot, `regression-iteration-${iteration}.md`),
|
|
300
|
+
delegationLedger: join(supportRoot, 'delegation-ledger.json'),
|
|
301
|
+
completionAudit: join(supportRoot, 'completion-audit.json'),
|
|
247
302
|
};
|
|
248
303
|
}
|
|
249
304
|
|
|
250
305
|
function canonicalClarifySpecPath(cwd, slug, stamp) {
|
|
251
|
-
return join(
|
|
306
|
+
return join(resolveIntakeRoot(cwd), `clarify-${normalizeSlug(slug)}-${stamp}.md`);
|
|
252
307
|
}
|
|
253
308
|
|
|
254
309
|
export async function readWorkspaceConfig(cwd) {
|
|
@@ -285,11 +340,39 @@ function buildWorkspaceReadme() {
|
|
|
285
340
|
'- `loopx plan <slug>`',
|
|
286
341
|
'- `loopx build <slug>`',
|
|
287
342
|
'- `loopx review <slug> [--reviewer <name>]`',
|
|
343
|
+
'- `loopx archive <slug>`',
|
|
288
344
|
'- `loopx autopilot <slug> [--reviewer <name>]`',
|
|
345
|
+
'- `loopx render [slug|--all]`',
|
|
289
346
|
'- `loopx status [slug] [--json]`',
|
|
347
|
+
'- `loopx setup-context`',
|
|
290
348
|
'- `loopx doctor`',
|
|
291
349
|
'- `loopx migrate`',
|
|
292
350
|
'- `loopx repair-install`',
|
|
351
|
+
'',
|
|
352
|
+
'## Document Boundaries',
|
|
353
|
+
'',
|
|
354
|
+
'User-facing documents to watch:',
|
|
355
|
+
'',
|
|
356
|
+
'- `workflows/<slug>/spec.md`',
|
|
357
|
+
'- `workflows/<slug>/plan.md`, `architecture.md`, `development-plan.md`, and `test-plan.md`',
|
|
358
|
+
'- `workflows/<slug>/execution-record.md` and `review-report.md`',
|
|
359
|
+
'- `views/index.html` and `workflows/<slug>/view/index.html` after `loopx render`',
|
|
360
|
+
'',
|
|
361
|
+
'Documents users may read and edit as workflow fact sources:',
|
|
362
|
+
'',
|
|
363
|
+
'- `workflows/<slug>/*.md` for the active workflow working copy',
|
|
364
|
+
'- `context/domain.md` and `agents/*.md` for project context and collaboration guidance',
|
|
365
|
+
'- `changes/active/<change-id>/*.md` for proposal, design, tasks, and spec delta',
|
|
366
|
+
'- `specs/<domain>/spec.md` for archived long-lived behavior specs',
|
|
367
|
+
'',
|
|
368
|
+
'Tool-owned or derived files:',
|
|
369
|
+
'',
|
|
370
|
+
'- `workflows/<slug>/state.json`, `build-context.jsonl`, and `review-context.jsonl`',
|
|
371
|
+
'- `workflows/<slug>/plan-reviews/`, `build-support/`, and `review-support/`',
|
|
372
|
+
'- `intake/clarify-*.md` clarify snapshots',
|
|
373
|
+
'- `changes/active/<change-id>/slices.json` and `artifact-graph.json`',
|
|
374
|
+
'- `autopilot/<slug>/run.json` and `build-active.json`',
|
|
375
|
+
'- `views/` and `workflows/<slug>/view/` generated HTML views',
|
|
293
376
|
].join('\n');
|
|
294
377
|
}
|
|
295
378
|
|
|
@@ -333,8 +416,19 @@ function createInitialState(slug, profile) {
|
|
|
333
416
|
plan_docs_status: 'missing',
|
|
334
417
|
plan_docs_artifact_paths: null,
|
|
335
418
|
plan_review_artifact_paths: [],
|
|
419
|
+
plan_review_history: [],
|
|
336
420
|
plan_blockers: [],
|
|
337
421
|
plan_source_spec_path: null,
|
|
422
|
+
change_id: changeIdForWorkflowSlug(slug),
|
|
423
|
+
change_artifacts_status: 'missing',
|
|
424
|
+
change_artifact_paths: null,
|
|
425
|
+
slice_artifacts_status: 'missing',
|
|
426
|
+
spec_delta_status: 'missing',
|
|
427
|
+
spec_sync_status: 'pending',
|
|
428
|
+
archive_status: 'pending',
|
|
429
|
+
archived_change_path: null,
|
|
430
|
+
archived_spec_paths: [],
|
|
431
|
+
adr_candidate_path: null,
|
|
338
432
|
build_run_id: null,
|
|
339
433
|
build_current_iteration: 0,
|
|
340
434
|
build_max_iterations: DEFAULT_BUILD_MAX_ITERATIONS,
|
|
@@ -348,6 +442,14 @@ function createInitialState(slug, profile) {
|
|
|
348
442
|
build_progress_artifact_paths: [],
|
|
349
443
|
build_support_evidence_paths: [],
|
|
350
444
|
build_no_deslop: false,
|
|
445
|
+
build_owner_id: null,
|
|
446
|
+
build_owner_session_id: null,
|
|
447
|
+
build_owner_status: 'not-started',
|
|
448
|
+
build_delegation_status: 'not-started',
|
|
449
|
+
build_delegation_ledger_path: null,
|
|
450
|
+
build_active_delegation_count: 0,
|
|
451
|
+
build_completion_audit_status: 'not-started',
|
|
452
|
+
build_completion_audit_path: null,
|
|
351
453
|
autopilot_current_phase: 'none',
|
|
352
454
|
autopilot_phase_history: [],
|
|
353
455
|
autopilot_blockers: [],
|
|
@@ -415,6 +517,10 @@ async function copyArtifact(fromRoot, toPath, name) {
|
|
|
415
517
|
await writeText(toPath, content);
|
|
416
518
|
}
|
|
417
519
|
|
|
520
|
+
async function writeJson(path, value) {
|
|
521
|
+
await writeText(path, JSON.stringify(value, null, 2));
|
|
522
|
+
}
|
|
523
|
+
|
|
418
524
|
async function writeCanonicalPlanArtifacts(cwd, root, slug) {
|
|
419
525
|
const plansRoot = resolvePlansRoot(cwd);
|
|
420
526
|
await ensureDir(plansRoot);
|
|
@@ -447,6 +553,778 @@ async function writeCanonicalPlanArtifacts(cwd, root, slug) {
|
|
|
447
553
|
return { planPath, testSpecPath };
|
|
448
554
|
}
|
|
449
555
|
|
|
556
|
+
function dedupeStrings(items) {
|
|
557
|
+
return [...new Set((items || []).map((item) => String(item || '').trim()).filter(Boolean))];
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function bulletsFromSectionText(text, heading) {
|
|
561
|
+
const pattern = new RegExp(`#{2,3} ${heading}\\n\\n([\\s\\S]*?)(?=\\n#{2,3} |$)`, 'i');
|
|
562
|
+
const match = text.match(pattern);
|
|
563
|
+
if (!match) {
|
|
564
|
+
return [];
|
|
565
|
+
}
|
|
566
|
+
return match[1]
|
|
567
|
+
.split('\n')
|
|
568
|
+
.map((line) => line.trim())
|
|
569
|
+
.filter((line) => line.startsWith('- '))
|
|
570
|
+
.map((line) => line.slice(2).trim())
|
|
571
|
+
.filter(Boolean);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function frontmatterList(text, key) {
|
|
575
|
+
if (!text.startsWith('---\n')) {
|
|
576
|
+
return [];
|
|
577
|
+
}
|
|
578
|
+
const end = text.indexOf('\n---\n', 4);
|
|
579
|
+
if (end === -1) {
|
|
580
|
+
return [];
|
|
581
|
+
}
|
|
582
|
+
const lines = text.slice(4, end).split('\n');
|
|
583
|
+
const values = [];
|
|
584
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
585
|
+
const line = lines[index];
|
|
586
|
+
if (line.trim() === `${key}:`) {
|
|
587
|
+
for (let child = index + 1; child < lines.length; child += 1) {
|
|
588
|
+
const childLine = lines[child];
|
|
589
|
+
if (!/^\s+-\s+/.test(childLine)) {
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
values.push(childLine.replace(/^\s+-\s+/, '').trim());
|
|
593
|
+
}
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return values.filter(Boolean);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function targetDomainsForChange(slug, sourceText) {
|
|
601
|
+
const explicit = bulletsFromSectionText(sourceText, 'Target Spec Domains');
|
|
602
|
+
if (explicit.length > 0) {
|
|
603
|
+
return dedupeStrings(explicit.map((item) => item.replace(/`/g, '')));
|
|
604
|
+
}
|
|
605
|
+
const frontmatterDomains = frontmatterList(sourceText, 'target_domains');
|
|
606
|
+
if (frontmatterDomains.length > 0) {
|
|
607
|
+
return dedupeStrings(frontmatterDomains.map((item) => item.replace(/`/g, '')));
|
|
608
|
+
}
|
|
609
|
+
return ['general'];
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function declaredTargetDomainsForDelta(sourceText) {
|
|
613
|
+
const explicit = bulletsFromSectionText(sourceText, 'Target Spec Domains');
|
|
614
|
+
if (explicit.length > 0) {
|
|
615
|
+
return dedupeStrings(explicit.map((item) => item.replace(/`/g, '')));
|
|
616
|
+
}
|
|
617
|
+
const frontmatterDomains = frontmatterList(sourceText, 'target_domains');
|
|
618
|
+
if (frontmatterDomains.length > 0) {
|
|
619
|
+
return dedupeStrings(frontmatterDomains.map((item) => item.replace(/`/g, '')));
|
|
620
|
+
}
|
|
621
|
+
return [];
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function stripFrontmatter(text) {
|
|
625
|
+
if (!text.startsWith('---\n')) {
|
|
626
|
+
return text;
|
|
627
|
+
}
|
|
628
|
+
const end = text.indexOf('\n---\n', 4);
|
|
629
|
+
return end === -1 ? text : text.slice(end + 5);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function normalizeRequirementName(raw) {
|
|
633
|
+
return String(raw || '').trim().replace(/\s+/g, ' ').toLowerCase();
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function requirementDisplayName(raw) {
|
|
637
|
+
return String(raw || '').trim().replace(/\s+/g, ' ');
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function sentenceToRequirementName(text, fallback) {
|
|
641
|
+
const cleaned = String(text || '')
|
|
642
|
+
.replace(/[`*_#]/g, '')
|
|
643
|
+
.replace(/\s+/g, ' ')
|
|
644
|
+
.trim()
|
|
645
|
+
.replace(/[.。::]+$/, '');
|
|
646
|
+
if (!cleaned) {
|
|
647
|
+
return fallback;
|
|
648
|
+
}
|
|
649
|
+
const withoutModal = cleaned
|
|
650
|
+
.replace(/\bSHALL\b.*$/i, '')
|
|
651
|
+
.replace(/\bMUST\b.*$/i, '')
|
|
652
|
+
.trim();
|
|
653
|
+
const value = withoutModal || cleaned;
|
|
654
|
+
return value.length > 80 ? value.slice(0, 77).trim() : value;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function normativeRequirementText(text, slug, index) {
|
|
658
|
+
const cleaned = String(text || '').replace(/\s+/g, ' ').trim().replace(/[.。]+$/, '');
|
|
659
|
+
if (/\b(SHALL|MUST)\b/i.test(cleaned)) {
|
|
660
|
+
return `${cleaned}.`;
|
|
661
|
+
}
|
|
662
|
+
return `Workflow ${slug} SHALL satisfy: ${cleaned || `approved requirement ${index + 1}`}.`;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function scenarioNameForRequirement(name) {
|
|
666
|
+
const cleaned = requirementDisplayName(name).replace(/[.。]+$/, '');
|
|
667
|
+
return cleaned.length > 70 ? cleaned.slice(0, 67).trim() : cleaned;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function requirementBlockFromText({ slug, text, index }) {
|
|
671
|
+
const name = sentenceToRequirementName(text, `Approved requirement ${index + 1}`);
|
|
672
|
+
return [
|
|
673
|
+
`### Requirement: ${name}`,
|
|
674
|
+
normativeRequirementText(text, slug, index),
|
|
675
|
+
'',
|
|
676
|
+
`#### Scenario: ${scenarioNameForRequirement(name)}`,
|
|
677
|
+
`- GIVEN workflow ${slug} has an approved plan`,
|
|
678
|
+
`- WHEN the accepted implementation is archived`,
|
|
679
|
+
`- THEN the system satisfies: ${String(text || '').replace(/\s+/g, ' ').trim() || name}`,
|
|
680
|
+
].join('\n');
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function splitDeltaSections(text) {
|
|
684
|
+
const body = stripFrontmatter(text);
|
|
685
|
+
const pattern = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\s*$/gim;
|
|
686
|
+
const matches = [...body.matchAll(pattern)];
|
|
687
|
+
const sections = new Map();
|
|
688
|
+
for (let index = 0; index < matches.length; index += 1) {
|
|
689
|
+
const match = matches[index];
|
|
690
|
+
const kind = match[1].toUpperCase();
|
|
691
|
+
const start = match.index + match[0].length;
|
|
692
|
+
const end = index + 1 < matches.length ? matches[index + 1].index : body.length;
|
|
693
|
+
sections.set(kind, body.slice(start, end).trim());
|
|
694
|
+
}
|
|
695
|
+
return sections;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function parseRequirementBlocks(sectionText) {
|
|
699
|
+
const pattern = /^###\s+Requirement:\s*(.+?)\s*$/gm;
|
|
700
|
+
const matches = [...String(sectionText || '').matchAll(pattern)];
|
|
701
|
+
return matches.map((match, index) => {
|
|
702
|
+
const start = match.index;
|
|
703
|
+
const end = index + 1 < matches.length ? matches[index + 1].index : sectionText.length;
|
|
704
|
+
return {
|
|
705
|
+
name: requirementDisplayName(match[1]),
|
|
706
|
+
raw: sectionText.slice(start, end).trim(),
|
|
707
|
+
};
|
|
708
|
+
}).filter((block) => block.name && block.raw);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function parseRenamedRequirement(block) {
|
|
712
|
+
const inline = block.name.match(/^(.*?)\s*(?:->|=>)\s*(.*?)$/);
|
|
713
|
+
if (inline) {
|
|
714
|
+
return {
|
|
715
|
+
from: requirementDisplayName(inline[1]),
|
|
716
|
+
to: requirementDisplayName(inline[2]),
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
const from = block.raw.match(/^FROM:\s*(.+?)\s*$/im)?.[1];
|
|
720
|
+
const to = block.raw.match(/^TO:\s*(.+?)\s*$/im)?.[1];
|
|
721
|
+
return {
|
|
722
|
+
from: requirementDisplayName(from || block.name),
|
|
723
|
+
to: requirementDisplayName(to || ''),
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function countRequirementScenarios(raw) {
|
|
728
|
+
return (String(raw || '').match(/^####\s+Scenario:\s*.+$/gim) || []).length;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function requirementTextBeforeScenarios(raw) {
|
|
732
|
+
const lines = String(raw || '').split('\n').slice(1);
|
|
733
|
+
const scenarioIndex = lines.findIndex((line) => /^####\s+Scenario:/i.test(line.trim()));
|
|
734
|
+
const requirementLines = scenarioIndex === -1 ? lines : lines.slice(0, scenarioIndex);
|
|
735
|
+
return requirementLines.map((line) => line.trim()).filter(Boolean).join(' ');
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function parseRequirementDelta(text) {
|
|
739
|
+
const sections = splitDeltaSections(text);
|
|
740
|
+
const added = parseRequirementBlocks(sections.get('ADDED') || '');
|
|
741
|
+
const modified = parseRequirementBlocks(sections.get('MODIFIED') || '');
|
|
742
|
+
const removed = parseRequirementBlocks(sections.get('REMOVED') || '').map((block) => block.name);
|
|
743
|
+
const renamed = parseRequirementBlocks(sections.get('RENAMED') || '').map(parseRenamedRequirement);
|
|
744
|
+
return { added, modified, removed, renamed };
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function deltaOperationCount(delta) {
|
|
748
|
+
return delta.added.length + delta.modified.length + delta.removed.length + delta.renamed.length;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function validateRequirementDelta(text) {
|
|
752
|
+
const delta = parseRequirementDelta(text);
|
|
753
|
+
const blockers = [];
|
|
754
|
+
if (deltaOperationCount(delta) === 0) {
|
|
755
|
+
blockers.push('spec_delta_missing_requirement_operations');
|
|
756
|
+
return { delta, blockers };
|
|
757
|
+
}
|
|
758
|
+
const seenBySection = {
|
|
759
|
+
added: new Set(),
|
|
760
|
+
modified: new Set(),
|
|
761
|
+
removed: new Set(),
|
|
762
|
+
renamedFrom: new Set(),
|
|
763
|
+
renamedTo: new Set(),
|
|
764
|
+
};
|
|
765
|
+
for (const [section, blocks] of [['added', delta.added], ['modified', delta.modified]]) {
|
|
766
|
+
for (const block of blocks) {
|
|
767
|
+
const key = normalizeRequirementName(block.name);
|
|
768
|
+
if (seenBySection[section].has(key)) {
|
|
769
|
+
blockers.push(`spec_delta_duplicate_${section}_${key}`);
|
|
770
|
+
}
|
|
771
|
+
seenBySection[section].add(key);
|
|
772
|
+
const requirementText = requirementTextBeforeScenarios(block.raw);
|
|
773
|
+
if (!requirementText) {
|
|
774
|
+
blockers.push(`spec_delta_${section}_${key}_missing_text`);
|
|
775
|
+
}
|
|
776
|
+
if (!/\b(SHALL|MUST)\b/i.test(requirementText)) {
|
|
777
|
+
blockers.push(`spec_delta_${section}_${key}_missing_shall_must`);
|
|
778
|
+
}
|
|
779
|
+
if (countRequirementScenarios(block.raw) === 0) {
|
|
780
|
+
blockers.push(`spec_delta_${section}_${key}_missing_scenario`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
for (const name of delta.removed) {
|
|
785
|
+
const key = normalizeRequirementName(name);
|
|
786
|
+
if (seenBySection.removed.has(key)) {
|
|
787
|
+
blockers.push(`spec_delta_duplicate_removed_${key}`);
|
|
788
|
+
}
|
|
789
|
+
seenBySection.removed.add(key);
|
|
790
|
+
}
|
|
791
|
+
for (const item of delta.renamed) {
|
|
792
|
+
const from = normalizeRequirementName(item.from);
|
|
793
|
+
const to = normalizeRequirementName(item.to);
|
|
794
|
+
if (!from || !to) {
|
|
795
|
+
blockers.push('spec_delta_renamed_missing_from_or_to');
|
|
796
|
+
}
|
|
797
|
+
if (seenBySection.renamedFrom.has(from)) {
|
|
798
|
+
blockers.push(`spec_delta_duplicate_renamed_from_${from}`);
|
|
799
|
+
}
|
|
800
|
+
if (seenBySection.renamedTo.has(to)) {
|
|
801
|
+
blockers.push(`spec_delta_duplicate_renamed_to_${to}`);
|
|
802
|
+
}
|
|
803
|
+
seenBySection.renamedFrom.add(from);
|
|
804
|
+
seenBySection.renamedTo.add(to);
|
|
805
|
+
}
|
|
806
|
+
for (const name of seenBySection.added) {
|
|
807
|
+
if (seenBySection.modified.has(name)) {
|
|
808
|
+
blockers.push(`spec_delta_conflict_added_modified_${name}`);
|
|
809
|
+
}
|
|
810
|
+
if (seenBySection.removed.has(name)) {
|
|
811
|
+
blockers.push(`spec_delta_conflict_added_removed_${name}`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
for (const name of seenBySection.modified) {
|
|
815
|
+
if (seenBySection.removed.has(name)) {
|
|
816
|
+
blockers.push(`spec_delta_conflict_modified_removed_${name}`);
|
|
817
|
+
}
|
|
818
|
+
if (seenBySection.renamedFrom.has(name)) {
|
|
819
|
+
blockers.push(`spec_delta_conflict_modified_renamed_from_${name}`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return { delta, blockers: dedupeStrings(blockers) };
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function requirementsForDelta(slug, plannerDraft) {
|
|
826
|
+
const requirements = String(plannerDraft.planText || '')
|
|
827
|
+
.split('\n')
|
|
828
|
+
.map((line) => line.trim())
|
|
829
|
+
.filter((line) => /^\d+\.\s+/.test(line))
|
|
830
|
+
.map((line) => line.replace(/^\d+\.\s+/, '').trim());
|
|
831
|
+
return dedupeStrings(requirements.length > 0 ? requirements : [
|
|
832
|
+
`Workflow ${slug} SHALL implement the approved loopx plan package.`,
|
|
833
|
+
]);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function verticalSlicesForChange(slug, plannerDraft) {
|
|
837
|
+
const requirements = requirementsForDelta(slug, plannerDraft);
|
|
838
|
+
const slices = requirements.slice(0, 8).map((requirement, index) => ({
|
|
839
|
+
id: `VS-${index + 1}`,
|
|
840
|
+
title: requirement.length > 90 ? `${requirement.slice(0, 87)}...` : requirement,
|
|
841
|
+
type: 'AFK',
|
|
842
|
+
blocked_by: index === 0 ? [] : [`VS-${index}`],
|
|
843
|
+
behavior: requirement,
|
|
844
|
+
acceptance_criteria: [
|
|
845
|
+
`完成端到端行为:${requirement}`,
|
|
846
|
+
'执行记录包含对应验证证据。',
|
|
847
|
+
],
|
|
848
|
+
verification_signal: 'execution-record.md verification evidence',
|
|
849
|
+
}));
|
|
850
|
+
return {
|
|
851
|
+
schema_version: 1,
|
|
852
|
+
philosophy: 'tracer-bullet-vertical-slices',
|
|
853
|
+
workflow: slug,
|
|
854
|
+
slices: slices.length > 0 ? slices : [{
|
|
855
|
+
id: 'VS-1',
|
|
856
|
+
title: `Implement approved workflow ${slug}`,
|
|
857
|
+
type: 'AFK',
|
|
858
|
+
blocked_by: [],
|
|
859
|
+
behavior: `Workflow ${slug} delivers the approved plan end-to-end.`,
|
|
860
|
+
acceptance_criteria: ['Execution record verifies the approved behavior.'],
|
|
861
|
+
verification_signal: 'execution-record.md verification evidence',
|
|
862
|
+
}],
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function changeArtifactGraph({ changeId, slug, artifacts }) {
|
|
867
|
+
const graph = {
|
|
868
|
+
schema_version: 1,
|
|
869
|
+
change: changeId,
|
|
870
|
+
workflow: slug,
|
|
871
|
+
philosophy: 'artifact-dependency-graph',
|
|
872
|
+
artifacts: {
|
|
873
|
+
proposal: {
|
|
874
|
+
path: artifacts.proposal,
|
|
875
|
+
status: existsSync(artifacts.proposal) ? 'done' : 'missing',
|
|
876
|
+
dependsOn: [],
|
|
877
|
+
},
|
|
878
|
+
specDelta: {
|
|
879
|
+
path: artifacts.specDelta,
|
|
880
|
+
status: existsSync(artifacts.specDelta) ? 'done' : 'missing',
|
|
881
|
+
dependsOn: ['proposal'],
|
|
882
|
+
},
|
|
883
|
+
design: {
|
|
884
|
+
path: artifacts.design,
|
|
885
|
+
status: existsSync(artifacts.design) ? 'done' : 'missing',
|
|
886
|
+
dependsOn: ['proposal', 'specDelta'],
|
|
887
|
+
},
|
|
888
|
+
tasks: {
|
|
889
|
+
path: artifacts.tasks,
|
|
890
|
+
status: existsSync(artifacts.tasks) ? 'done' : 'missing',
|
|
891
|
+
dependsOn: ['proposal', 'specDelta', 'design'],
|
|
892
|
+
},
|
|
893
|
+
slices: {
|
|
894
|
+
path: artifacts.slices,
|
|
895
|
+
status: existsSync(artifacts.slices) ? 'done' : 'missing',
|
|
896
|
+
dependsOn: ['proposal', 'specDelta', 'design'],
|
|
897
|
+
},
|
|
898
|
+
},
|
|
899
|
+
};
|
|
900
|
+
graph.nextReady = Object.entries(graph.artifacts)
|
|
901
|
+
.filter(([, node]) => node.status !== 'done')
|
|
902
|
+
.filter(([, node]) => node.dependsOn.every((dependency) => graph.artifacts[dependency]?.status === 'done'))
|
|
903
|
+
.map(([name]) => name);
|
|
904
|
+
return graph;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
async function writeChangeArtifacts(cwd, root, slug, sourceText, plannerDraft, changeId = changeIdForWorkflowSlug(slug)) {
|
|
908
|
+
const normalizedChangeId = normalizeSlug(changeId);
|
|
909
|
+
const changeRoot = resolveChangeRoot(cwd, normalizedChangeId);
|
|
910
|
+
const specsRoot = join(changeRoot, 'specs');
|
|
911
|
+
await ensureDir(specsRoot);
|
|
912
|
+
const paths = {
|
|
913
|
+
root: changeRoot,
|
|
914
|
+
proposal: join(changeRoot, 'proposal.md'),
|
|
915
|
+
specDelta: join(changeRoot, 'spec-delta.md'),
|
|
916
|
+
design: join(changeRoot, 'design.md'),
|
|
917
|
+
tasks: join(changeRoot, 'tasks.md'),
|
|
918
|
+
slices: join(changeRoot, 'slices.json'),
|
|
919
|
+
graph: join(changeRoot, 'artifact-graph.json'),
|
|
920
|
+
};
|
|
921
|
+
const domains = targetDomainsForChange(slug, sourceText);
|
|
922
|
+
const requirements = requirementsForDelta(slug, plannerDraft);
|
|
923
|
+
const slices = verticalSlicesForChange(slug, plannerDraft);
|
|
924
|
+
|
|
925
|
+
await writeText(paths.proposal, [
|
|
926
|
+
`# loopx Change Proposal: ${normalizedChangeId}`,
|
|
927
|
+
'',
|
|
928
|
+
'## Why',
|
|
929
|
+
'',
|
|
930
|
+
'- Preserve the approved workflow intent as a durable change proposal.',
|
|
931
|
+
'',
|
|
932
|
+
'## What Changes',
|
|
933
|
+
'',
|
|
934
|
+
...requirements.map((item) => `- ${item}`),
|
|
935
|
+
'',
|
|
936
|
+
'## Target Spec Domains',
|
|
937
|
+
'',
|
|
938
|
+
...domains.map((domain) => `- ${domain}`),
|
|
939
|
+
'',
|
|
940
|
+
'## Source',
|
|
941
|
+
'',
|
|
942
|
+
`- change id: ${normalizedChangeId}`,
|
|
943
|
+
`- workflow slug: ${slug}`,
|
|
944
|
+
`- workflow: ${artifactPath(root, 'state.json')}`,
|
|
945
|
+
`- source spec: ${artifactPath(root, 'spec.md')}`,
|
|
946
|
+
].join('\n'));
|
|
947
|
+
|
|
948
|
+
await writeText(paths.specDelta, [
|
|
949
|
+
'---',
|
|
950
|
+
`change_id: ${normalizedChangeId}`,
|
|
951
|
+
`slug: ${slug}`,
|
|
952
|
+
'target_domains:',
|
|
953
|
+
...domains.map((domain) => ` - ${domain}`),
|
|
954
|
+
'---',
|
|
955
|
+
'',
|
|
956
|
+
`# loopx Spec Delta: ${normalizedChangeId}`,
|
|
957
|
+
'',
|
|
958
|
+
'## ADDED Requirements',
|
|
959
|
+
'',
|
|
960
|
+
...requirements.flatMap((item, index) => [requirementBlockFromText({ slug, text: item, index }), '']),
|
|
961
|
+
].join('\n'));
|
|
962
|
+
|
|
963
|
+
await writeText(paths.design, [
|
|
964
|
+
`# loopx Change Design: ${normalizedChangeId}`,
|
|
965
|
+
'',
|
|
966
|
+
'## Technical Approach',
|
|
967
|
+
'',
|
|
968
|
+
plannerDraft.architectureText || '- See workflow architecture artifact.',
|
|
969
|
+
'',
|
|
970
|
+
'## Task Plan',
|
|
971
|
+
'',
|
|
972
|
+
plannerDraft.developmentPlanText || '- See workflow development plan artifact.',
|
|
973
|
+
].join('\n'));
|
|
974
|
+
|
|
975
|
+
await writeText(paths.tasks, [
|
|
976
|
+
`# loopx Change Tasks: ${normalizedChangeId}`,
|
|
977
|
+
'',
|
|
978
|
+
'## Vertical Slices',
|
|
979
|
+
'',
|
|
980
|
+
...slices.slices.map((slice) => `- [ ] ${slice.id} ${slice.title} (${slice.type}) - verification: ${slice.verification_signal}`),
|
|
981
|
+
'',
|
|
982
|
+
'## Tasks',
|
|
983
|
+
'',
|
|
984
|
+
...requirements.map((item, index) => `- [ ] ${index + 1}. ${item}`),
|
|
985
|
+
'',
|
|
986
|
+
'## Verification',
|
|
987
|
+
'',
|
|
988
|
+
plannerDraft.testPlanText || '- See workflow test plan artifact.',
|
|
989
|
+
].join('\n'));
|
|
990
|
+
|
|
991
|
+
await writeJson(paths.slices, slices);
|
|
992
|
+
await writeJson(paths.graph, changeArtifactGraph({ changeId: normalizedChangeId, slug, artifacts: paths }));
|
|
993
|
+
for (const domain of domains) {
|
|
994
|
+
const specDeltaPath = join(specsRoot, ...domain.split('/'), 'spec.md');
|
|
995
|
+
await ensureDir(dirname(specDeltaPath));
|
|
996
|
+
await copyArtifact(changeRoot, specDeltaPath, 'spec-delta.md');
|
|
997
|
+
}
|
|
998
|
+
return paths;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
async function readChangeArtifactStatus(paths) {
|
|
1002
|
+
if (!paths || typeof paths !== 'object') {
|
|
1003
|
+
return {
|
|
1004
|
+
status: 'missing',
|
|
1005
|
+
specDeltaStatus: 'missing',
|
|
1006
|
+
blockers: ['missing_change_artifacts'],
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
const blockers = [];
|
|
1010
|
+
for (const name of ['proposal', 'specDelta', 'design', 'tasks', 'slices', 'graph']) {
|
|
1011
|
+
const path = paths[name];
|
|
1012
|
+
if (!path || !existsSync(path)) {
|
|
1013
|
+
blockers.push(`missing_change_artifact_${name}`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
let specDeltaStatus = 'missing';
|
|
1017
|
+
if (paths.specDelta && existsSync(paths.specDelta)) {
|
|
1018
|
+
const text = await readFile(paths.specDelta, 'utf8');
|
|
1019
|
+
const parsedDelta = validateRequirementDelta(text);
|
|
1020
|
+
const declaredDomains = declaredTargetDomainsForDelta(text);
|
|
1021
|
+
const hasDomains = declaredDomains.length > 0;
|
|
1022
|
+
if (!text.trim()) {
|
|
1023
|
+
specDeltaStatus = 'partial';
|
|
1024
|
+
blockers.push('spec_delta_empty');
|
|
1025
|
+
} else if (!hasDomains || parsedDelta.blockers.length > 0) {
|
|
1026
|
+
specDeltaStatus = 'partial';
|
|
1027
|
+
if (!hasDomains) {
|
|
1028
|
+
blockers.push('spec_delta_missing_domains');
|
|
1029
|
+
}
|
|
1030
|
+
blockers.push(...parsedDelta.blockers);
|
|
1031
|
+
} else {
|
|
1032
|
+
specDeltaStatus = 'complete';
|
|
1033
|
+
}
|
|
1034
|
+
const specsRoot = paths.root ? join(paths.root, 'specs') : null;
|
|
1035
|
+
if (specsRoot && existsSync(specsRoot)) {
|
|
1036
|
+
const entries = await readdir(specsRoot, { withFileTypes: true });
|
|
1037
|
+
const declaredDomainSet = new Set(declaredDomains);
|
|
1038
|
+
for (const entry of entries) {
|
|
1039
|
+
if (!entry.isDirectory() || declaredDomainSet.has(entry.name)) {
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
const candidate = join(specsRoot, entry.name, 'spec.md');
|
|
1043
|
+
if (!existsSync(candidate)) {
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
const domainDelta = await readFile(candidate, 'utf8');
|
|
1047
|
+
const validation = validateRequirementDelta(domainDelta);
|
|
1048
|
+
if (validation.blockers.length > 0) {
|
|
1049
|
+
specDeltaStatus = 'partial';
|
|
1050
|
+
blockers.push(
|
|
1051
|
+
...validation.blockers.map((blocker) => `spec_delta_${entry.name}_${blocker.replace(/^spec_delta_/, '')}`),
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (paths.slices && existsSync(paths.slices)) {
|
|
1058
|
+
try {
|
|
1059
|
+
const payload = JSON.parse(await readFile(paths.slices, 'utf8'));
|
|
1060
|
+
const slices = Array.isArray(payload.slices) ? payload.slices : [];
|
|
1061
|
+
const valid = slices.length > 0 && slices.every((slice) => (
|
|
1062
|
+
slice
|
|
1063
|
+
&& typeof slice.id === 'string'
|
|
1064
|
+
&& slice.id
|
|
1065
|
+
&& ['AFK', 'HITL'].includes(slice.type)
|
|
1066
|
+
&& typeof slice.behavior === 'string'
|
|
1067
|
+
&& slice.behavior
|
|
1068
|
+
&& Array.isArray(slice.acceptance_criteria)
|
|
1069
|
+
&& slice.acceptance_criteria.length > 0
|
|
1070
|
+
&& typeof slice.verification_signal === 'string'
|
|
1071
|
+
&& slice.verification_signal
|
|
1072
|
+
));
|
|
1073
|
+
if (!valid) {
|
|
1074
|
+
blockers.push('vertical_slices_missing');
|
|
1075
|
+
}
|
|
1076
|
+
} catch {
|
|
1077
|
+
blockers.push('vertical_slices_invalid');
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
return {
|
|
1081
|
+
status: blockers.length > 0 ? 'partial' : 'complete',
|
|
1082
|
+
specDeltaStatus,
|
|
1083
|
+
sliceArtifactsStatus: blockers.some((blocker) => blocker.startsWith('missing_change_artifact_slices') || blocker.startsWith('vertical_slices_')) ? 'partial' : 'complete',
|
|
1084
|
+
blockers,
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
async function ensureArchiveSlicesArtifact(cwd, root, slug, state) {
|
|
1089
|
+
if (state.change_artifact_paths?.slices && existsSync(state.change_artifact_paths.slices)) {
|
|
1090
|
+
return state.change_artifact_paths;
|
|
1091
|
+
}
|
|
1092
|
+
if (!state.change_artifact_paths?.root || !existsSync(state.change_artifact_paths.root)) {
|
|
1093
|
+
return state.change_artifact_paths;
|
|
1094
|
+
}
|
|
1095
|
+
const slicesPath = join(state.change_artifact_paths.root, 'slices.json');
|
|
1096
|
+
const draft = {
|
|
1097
|
+
planText: existsSync(state.change_artifact_paths.tasks)
|
|
1098
|
+
? await readFile(state.change_artifact_paths.tasks, 'utf8')
|
|
1099
|
+
: `1. Archive approved workflow ${slug}`,
|
|
1100
|
+
};
|
|
1101
|
+
await writeJson(slicesPath, verticalSlicesForChange(slug, draft));
|
|
1102
|
+
const nextPaths = {
|
|
1103
|
+
...state.change_artifact_paths,
|
|
1104
|
+
slices: slicesPath,
|
|
1105
|
+
};
|
|
1106
|
+
if (nextPaths.graph && existsSync(nextPaths.graph)) {
|
|
1107
|
+
await writeJson(nextPaths.graph, changeArtifactGraph({
|
|
1108
|
+
changeId: state.change_id || changeIdForWorkflowSlug(slug),
|
|
1109
|
+
slug,
|
|
1110
|
+
artifacts: nextPaths,
|
|
1111
|
+
}));
|
|
1112
|
+
}
|
|
1113
|
+
await writeState(root, withRecommendedAction({
|
|
1114
|
+
...state,
|
|
1115
|
+
change_artifact_paths: nextPaths,
|
|
1116
|
+
slice_artifacts_status: 'complete',
|
|
1117
|
+
}));
|
|
1118
|
+
return nextPaths;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function parseSpecDelta(text) {
|
|
1122
|
+
const parsed = parseRequirementDelta(text);
|
|
1123
|
+
return {
|
|
1124
|
+
domains: declaredTargetDomainsForDelta(text),
|
|
1125
|
+
...parsed,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function specDomainPath(cwd, domain) {
|
|
1130
|
+
return join(resolveSpecsRoot(cwd), ...String(domain).split('/').map((part) => normalizeSlug(part)), 'spec.md');
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
async function writeAdrCandidate(cwd, changeId, state, archivedSpecPaths) {
|
|
1134
|
+
const path = join(resolveWorkspaceRoot(cwd), 'decisions', 'adr-candidates', `${normalizeSlug(changeId)}.md`);
|
|
1135
|
+
await ensureDir(dirname(path));
|
|
1136
|
+
await writeText(path, [
|
|
1137
|
+
`# ADR Candidate: ${normalizeSlug(changeId)}`,
|
|
1138
|
+
'',
|
|
1139
|
+
'## Decision',
|
|
1140
|
+
'',
|
|
1141
|
+
`- Archive accepted workflow ${state.slug} into long-lived loopx specs.`,
|
|
1142
|
+
'',
|
|
1143
|
+
'## Drivers',
|
|
1144
|
+
'',
|
|
1145
|
+
'- The reviewed change delta has reached done.',
|
|
1146
|
+
'- The change may affect future planning, build, and review context.',
|
|
1147
|
+
'',
|
|
1148
|
+
'## Alternatives Considered',
|
|
1149
|
+
'',
|
|
1150
|
+
'- Keep the decision only in workflow artifacts.',
|
|
1151
|
+
'- Promote the accepted behavior into long-lived specs and keep this ADR candidate as advisory memory.',
|
|
1152
|
+
'',
|
|
1153
|
+
'## Why Candidate Only',
|
|
1154
|
+
'',
|
|
1155
|
+
'- loopx should not make irreversible architectural decisions without human confirmation.',
|
|
1156
|
+
'- This file records the candidate so a future human can promote it to docs/adr if useful.',
|
|
1157
|
+
'',
|
|
1158
|
+
'## Consequences',
|
|
1159
|
+
'',
|
|
1160
|
+
...archivedSpecPaths.map((item) => `- Updated spec: ${item}`),
|
|
1161
|
+
'',
|
|
1162
|
+
'## Follow-ups',
|
|
1163
|
+
'',
|
|
1164
|
+
'- Promote to a real ADR only if the decision is hard to reverse, surprising, and trade-off-heavy.',
|
|
1165
|
+
].join('\n'));
|
|
1166
|
+
return path;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function splitSpecRequirements(existing) {
|
|
1170
|
+
const text = String(existing || '');
|
|
1171
|
+
const match = text.match(/^##\s+Requirements\s*$/im);
|
|
1172
|
+
if (!match) {
|
|
1173
|
+
return {
|
|
1174
|
+
before: text.trimEnd(),
|
|
1175
|
+
header: '## Requirements',
|
|
1176
|
+
body: '',
|
|
1177
|
+
after: '',
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
const headerStart = match.index;
|
|
1181
|
+
const bodyStart = headerStart + match[0].length;
|
|
1182
|
+
const rest = text.slice(bodyStart);
|
|
1183
|
+
const nextTopHeading = rest.search(/\n##\s+/);
|
|
1184
|
+
const body = nextTopHeading === -1 ? rest : rest.slice(0, nextTopHeading);
|
|
1185
|
+
const after = nextTopHeading === -1 ? '' : rest.slice(nextTopHeading);
|
|
1186
|
+
return {
|
|
1187
|
+
before: text.slice(0, headerStart).trimEnd(),
|
|
1188
|
+
header: match[0],
|
|
1189
|
+
body: body.trim(),
|
|
1190
|
+
after: after.trimEnd(),
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function requirementMapFromSpec(existing) {
|
|
1195
|
+
const parts = splitSpecRequirements(existing);
|
|
1196
|
+
const blocks = parseRequirementBlocks(parts.body);
|
|
1197
|
+
const map = new Map();
|
|
1198
|
+
const order = [];
|
|
1199
|
+
for (const block of blocks) {
|
|
1200
|
+
const key = normalizeRequirementName(block.name);
|
|
1201
|
+
if (!map.has(key)) {
|
|
1202
|
+
order.push(key);
|
|
1203
|
+
}
|
|
1204
|
+
map.set(key, block);
|
|
1205
|
+
}
|
|
1206
|
+
return { parts, map, order };
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function applyRequirementDelta(existing, delta, domain) {
|
|
1210
|
+
const { parts, map, order } = requirementMapFromSpec(existing);
|
|
1211
|
+
const ensureExisting = (key, label, name) => {
|
|
1212
|
+
if (!map.has(key)) {
|
|
1213
|
+
throw new Error(`${domain} ${label} failed for "### Requirement: ${name}" - not found`);
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
for (const item of delta.renamed) {
|
|
1218
|
+
const fromKey = normalizeRequirementName(item.from);
|
|
1219
|
+
const toKey = normalizeRequirementName(item.to);
|
|
1220
|
+
ensureExisting(fromKey, 'RENAMED', item.from);
|
|
1221
|
+
if (map.has(toKey)) {
|
|
1222
|
+
throw new Error(`${domain} RENAMED failed for "### Requirement: ${item.to}" - target already exists`);
|
|
1223
|
+
}
|
|
1224
|
+
const block = map.get(fromKey);
|
|
1225
|
+
const rawLines = block.raw.split('\n');
|
|
1226
|
+
rawLines[0] = `### Requirement: ${item.to}`;
|
|
1227
|
+
map.delete(fromKey);
|
|
1228
|
+
map.set(toKey, { name: item.to, raw: rawLines.join('\n') });
|
|
1229
|
+
const orderIndex = order.indexOf(fromKey);
|
|
1230
|
+
if (orderIndex !== -1) {
|
|
1231
|
+
order[orderIndex] = toKey;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
for (const name of delta.removed) {
|
|
1236
|
+
const key = normalizeRequirementName(name);
|
|
1237
|
+
ensureExisting(key, 'REMOVED', name);
|
|
1238
|
+
map.delete(key);
|
|
1239
|
+
const orderIndex = order.indexOf(key);
|
|
1240
|
+
if (orderIndex !== -1) {
|
|
1241
|
+
order.splice(orderIndex, 1);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
for (const block of delta.modified) {
|
|
1246
|
+
const key = normalizeRequirementName(block.name);
|
|
1247
|
+
ensureExisting(key, 'MODIFIED', block.name);
|
|
1248
|
+
map.set(key, block);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
for (const block of delta.added) {
|
|
1252
|
+
const key = normalizeRequirementName(block.name);
|
|
1253
|
+
if (map.has(key)) {
|
|
1254
|
+
if (map.get(key).raw.trim() === block.raw.trim()) {
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
throw new Error(`${domain} ADDED failed for "### Requirement: ${block.name}" - already exists`);
|
|
1258
|
+
}
|
|
1259
|
+
map.set(key, block);
|
|
1260
|
+
order.push(key);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
const requirementBody = order.map((key) => map.get(key)?.raw).filter(Boolean).join('\n\n').trimEnd();
|
|
1264
|
+
return [
|
|
1265
|
+
parts.before,
|
|
1266
|
+
parts.header,
|
|
1267
|
+
requirementBody,
|
|
1268
|
+
parts.after,
|
|
1269
|
+
].filter((part) => String(part || '').trim()).join('\n\n').replace(/\n{3,}/g, '\n\n');
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
async function specDeltaFilesForArchive(cwd, specDeltaPath) {
|
|
1273
|
+
const changeRoot = dirname(specDeltaPath);
|
|
1274
|
+
const mainText = await readFile(specDeltaPath, 'utf8');
|
|
1275
|
+
const files = new Map();
|
|
1276
|
+
const declaredDomains = declaredTargetDomainsForDelta(mainText);
|
|
1277
|
+
if (declaredDomains.length === 0) {
|
|
1278
|
+
throw new Error('archive_blocked:spec_delta_missing_domains');
|
|
1279
|
+
}
|
|
1280
|
+
for (const domain of declaredDomains) {
|
|
1281
|
+
files.set(domain, specDeltaPath);
|
|
1282
|
+
}
|
|
1283
|
+
const specsRoot = join(changeRoot, 'specs');
|
|
1284
|
+
if (existsSync(specsRoot)) {
|
|
1285
|
+
const entries = await readdir(specsRoot, { withFileTypes: true });
|
|
1286
|
+
for (const entry of entries) {
|
|
1287
|
+
if (!entry.isDirectory()) {
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
const candidate = join(specsRoot, entry.name, 'spec.md');
|
|
1291
|
+
if (existsSync(candidate) && !files.has(entry.name)) {
|
|
1292
|
+
files.set(entry.name, candidate);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
return files;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
async function mergeSpecDeltaIntoLongLivedSpecs(cwd, slug, specDeltaPath) {
|
|
1300
|
+
const deltaFiles = await specDeltaFilesForArchive(cwd, specDeltaPath);
|
|
1301
|
+
const updated = [];
|
|
1302
|
+
for (const [domain, deltaPath] of deltaFiles.entries()) {
|
|
1303
|
+
const deltaText = await readFile(deltaPath, 'utf8');
|
|
1304
|
+
const validation = validateRequirementDelta(deltaText);
|
|
1305
|
+
if (validation.blockers.length > 0) {
|
|
1306
|
+
throw new Error(`archive_blocked:${domain}:${validation.blockers.join(',')}`);
|
|
1307
|
+
}
|
|
1308
|
+
const domainDelta = parseSpecDelta(deltaText);
|
|
1309
|
+
const path = specDomainPath(cwd, domain);
|
|
1310
|
+
await ensureDir(dirname(path));
|
|
1311
|
+
const existing = await readTextIfExists(path);
|
|
1312
|
+
const base = existing || [
|
|
1313
|
+
`# loopx Spec Domain: ${domain}`,
|
|
1314
|
+
'',
|
|
1315
|
+
'## Purpose',
|
|
1316
|
+
'',
|
|
1317
|
+
`Long-lived accepted behavior for ${domain}.`,
|
|
1318
|
+
'',
|
|
1319
|
+
'## Requirements',
|
|
1320
|
+
].join('\n');
|
|
1321
|
+
const next = applyRequirementDelta(base, domainDelta, domain);
|
|
1322
|
+
await writeText(path, next);
|
|
1323
|
+
updated.push(path);
|
|
1324
|
+
}
|
|
1325
|
+
return updated;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
450
1328
|
function deriveSlugFromSpecPath(path, text) {
|
|
451
1329
|
const meta = parseFrontmatter(text);
|
|
452
1330
|
if (meta.workflow_id) {
|
|
@@ -457,7 +1335,13 @@ function deriveSlugFromSpecPath(path, text) {
|
|
|
457
1335
|
}
|
|
458
1336
|
|
|
459
1337
|
function containsChineseText(text) {
|
|
460
|
-
|
|
1338
|
+
const chineseChars = text.match(/[\u3400-\u9fff]/g) || [];
|
|
1339
|
+
const latinChars = text.match(/[A-Za-z]/g) || [];
|
|
1340
|
+
const signalChars = chineseChars.length + latinChars.length;
|
|
1341
|
+
if (signalChars === 0) {
|
|
1342
|
+
return false;
|
|
1343
|
+
}
|
|
1344
|
+
return chineseChars.length >= 40 || (chineseChars.length >= 8 && chineseChars.length / signalChars >= 0.2);
|
|
461
1345
|
}
|
|
462
1346
|
|
|
463
1347
|
async function ensurePlanWorkflowFromDirectSpec(cwd, directSpecPath, explicitSlug, options = {}) {
|
|
@@ -512,13 +1396,6 @@ async function writePlanArtifacts(root, cwd, slug, plannerDraft) {
|
|
|
512
1396
|
await writeText(artifactPath(root, 'architecture.md'), plannerDraft.architectureText);
|
|
513
1397
|
await writeText(artifactPath(root, 'development-plan.md'), plannerDraft.developmentPlanText);
|
|
514
1398
|
await writeText(artifactPath(root, 'test-plan.md'), plannerDraft.testPlanText);
|
|
515
|
-
|
|
516
|
-
const docPaths = resolvePlanDocPaths(cwd, slug);
|
|
517
|
-
await ensureDir(docPaths.docsRoot);
|
|
518
|
-
await writeText(docPaths.architecture, plannerDraft.docs.architecture);
|
|
519
|
-
await writeText(docPaths.design, plannerDraft.docs.design);
|
|
520
|
-
await writeText(docPaths.testPlan, plannerDraft.docs.testPlan);
|
|
521
|
-
return docPaths;
|
|
522
1399
|
}
|
|
523
1400
|
|
|
524
1401
|
async function writePlanReviewArtifacts(root, iteration, plannerDraft, architectReview, criticReview) {
|
|
@@ -566,14 +1443,36 @@ async function writePlanReviewArtifacts(root, iteration, plannerDraft, architect
|
|
|
566
1443
|
return paths;
|
|
567
1444
|
}
|
|
568
1445
|
|
|
1446
|
+
function planReviewSummary(iteration, architectReview, criticReview) {
|
|
1447
|
+
return {
|
|
1448
|
+
iteration,
|
|
1449
|
+
architectReview: {
|
|
1450
|
+
status: architectReview.status,
|
|
1451
|
+
verdict: architectReview.verdict,
|
|
1452
|
+
findings: Array.isArray(architectReview.findings) ? architectReview.findings : [],
|
|
1453
|
+
strongestObjection: architectReview.strongestObjection || null,
|
|
1454
|
+
tradeoffTension: architectReview.tradeoffTension || null,
|
|
1455
|
+
},
|
|
1456
|
+
criticReview: {
|
|
1457
|
+
verdict: criticReview.verdict,
|
|
1458
|
+
findings: Array.isArray(criticReview.findings) ? criticReview.findings : [],
|
|
1459
|
+
acceptanceCriteriaTestable: Boolean(criticReview.acceptanceCriteriaTestable),
|
|
1460
|
+
verificationStepsResolved: Boolean(criticReview.verificationStepsResolved),
|
|
1461
|
+
executionInputsResolved: Boolean(criticReview.executionInputsResolved),
|
|
1462
|
+
},
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
function initialPlanReviewHistory(state) {
|
|
1467
|
+
const history = Array.isArray(state.plan_review_history) ? state.plan_review_history : [];
|
|
1468
|
+
if (state.current_stage !== STAGES.PLAN || state.stage_status !== 'blocked' || history.length === 0) {
|
|
1469
|
+
return [];
|
|
1470
|
+
}
|
|
1471
|
+
return [history[history.length - 1]];
|
|
1472
|
+
}
|
|
1473
|
+
|
|
569
1474
|
async function readPlanCompletion(cwd, root, slug, state) {
|
|
570
1475
|
const blockers = [];
|
|
571
|
-
const docPaths = resolvePlanDocPaths(cwd, slug);
|
|
572
|
-
const docsPresent = {
|
|
573
|
-
architecture: existsSync(docPaths.architecture),
|
|
574
|
-
design: existsSync(docPaths.design),
|
|
575
|
-
testPlan: existsSync(docPaths.testPlan),
|
|
576
|
-
};
|
|
577
1476
|
if (state.plan_architect_review_status !== 'complete') {
|
|
578
1477
|
blockers.push('architect_review_incomplete');
|
|
579
1478
|
}
|
|
@@ -598,24 +1497,31 @@ async function readPlanCompletion(cwd, root, slug, state) {
|
|
|
598
1497
|
if (!state.test_spec_artifact_path || !existsSync(state.test_spec_artifact_path)) {
|
|
599
1498
|
blockers.push('missing_test_spec');
|
|
600
1499
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
1500
|
+
const workflowDocs = {
|
|
1501
|
+
plan: artifactPath(root, 'plan.md'),
|
|
1502
|
+
architecture: artifactPath(root, 'architecture.md'),
|
|
1503
|
+
developmentPlan: artifactPath(root, 'development-plan.md'),
|
|
1504
|
+
testPlan: artifactPath(root, 'test-plan.md'),
|
|
1505
|
+
};
|
|
1506
|
+
for (const [key, path] of Object.entries(workflowDocs)) {
|
|
1507
|
+
if (!existsSync(path)) {
|
|
1508
|
+
blockers.push(`missing_plan_artifact_${key}`);
|
|
604
1509
|
continue;
|
|
605
1510
|
}
|
|
606
|
-
const text = await readFile(
|
|
1511
|
+
const text = await readFile(path, 'utf8');
|
|
607
1512
|
if (!containsChineseText(text)) {
|
|
608
|
-
blockers.push(`
|
|
1513
|
+
blockers.push(`plan_artifact_not_chinese_${key}`);
|
|
609
1514
|
}
|
|
610
1515
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
&& blockers.every((blocker) => !blocker.startsWith('doc_not_chinese_') && !blocker.startsWith('missing_doc_'));
|
|
1516
|
+
const changeStatus = await readChangeArtifactStatus(state.change_artifact_paths);
|
|
1517
|
+
blockers.push(...changeStatus.blockers);
|
|
614
1518
|
|
|
615
1519
|
return {
|
|
616
1520
|
blockers,
|
|
617
|
-
docsStatus:
|
|
618
|
-
|
|
1521
|
+
docsStatus: blockers.some((blocker) => blocker.startsWith('missing_plan_artifact_') || blocker.startsWith('plan_artifact_not_chinese_')) ? 'partial' : 'complete',
|
|
1522
|
+
changeArtifactsStatus: changeStatus.status,
|
|
1523
|
+
specDeltaStatus: changeStatus.specDeltaStatus,
|
|
1524
|
+
sliceArtifactsStatus: changeStatus.sliceArtifactsStatus,
|
|
619
1525
|
};
|
|
620
1526
|
}
|
|
621
1527
|
|
|
@@ -641,6 +1547,174 @@ function buildIterationBlockers(iterationData, { noDeslop = false } = {}) {
|
|
|
641
1547
|
return blockers;
|
|
642
1548
|
}
|
|
643
1549
|
|
|
1550
|
+
function buildOwnerId(slug) {
|
|
1551
|
+
return `loopx-build-owner:${normalizeSlug(slug)}`;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function buildOwnerSessionId(slug, runId) {
|
|
1555
|
+
return `${buildOwnerId(slug)}:${runId || 'pending'}`;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
function normalizeBuildDelegations(iterationData = {}) {
|
|
1559
|
+
return Array.isArray(iterationData.delegations)
|
|
1560
|
+
? iterationData.delegations.map((item, index) => ({
|
|
1561
|
+
id: item?.id || `delegation-${index + 1}`,
|
|
1562
|
+
role: item?.role || 'implementation',
|
|
1563
|
+
status: ['active', 'complete', 'failed', 'blocked', 'pending', 'skipped'].includes(String(item?.status || '').trim().toLowerCase())
|
|
1564
|
+
? String(item.status).trim().toLowerCase()
|
|
1565
|
+
: 'pending',
|
|
1566
|
+
blocking: item?.blocking !== false,
|
|
1567
|
+
scope: Array.isArray(item?.scope) ? item.scope.map(String) : [],
|
|
1568
|
+
evidence_path: item?.evidence_path || item?.evidencePath || null,
|
|
1569
|
+
summary: item?.summary || 'Build delegation entry',
|
|
1570
|
+
}))
|
|
1571
|
+
: [];
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
function isBlockingDelegationOpen(item) {
|
|
1575
|
+
return item?.blocking && !['complete', 'skipped'].includes(String(item.status));
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
function buildDelegationLedger({ slug, ownerId, ownerSessionId, iterationData, previousLedger = null }) {
|
|
1579
|
+
const delegationsById = new Map();
|
|
1580
|
+
for (const item of previousLedger?.delegations || []) {
|
|
1581
|
+
if (isBlockingDelegationOpen(item)) {
|
|
1582
|
+
delegationsById.set(item.id, item);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
for (const item of normalizeBuildDelegations(iterationData)) {
|
|
1586
|
+
if (['complete', 'skipped'].includes(String(item.status))) {
|
|
1587
|
+
delegationsById.delete(item.id);
|
|
1588
|
+
} else {
|
|
1589
|
+
delegationsById.set(item.id, item);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
const delegations = [...delegationsById.values()];
|
|
1593
|
+
const activeBlocking = delegations.filter((item) => item.blocking && !['complete', 'skipped'].includes(String(item.status)));
|
|
1594
|
+
return {
|
|
1595
|
+
schema_version: WORKFLOW_SCHEMA_VERSION,
|
|
1596
|
+
slug,
|
|
1597
|
+
owner_id: ownerId,
|
|
1598
|
+
owner_session_id: ownerSessionId,
|
|
1599
|
+
updated_at: nowIso(),
|
|
1600
|
+
active_blocking_count: activeBlocking.length,
|
|
1601
|
+
status: activeBlocking.length > 0 ? 'active' : 'drained',
|
|
1602
|
+
delegations,
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
function buildDelegationBlockers(ledger) {
|
|
1607
|
+
return (ledger.delegations || [])
|
|
1608
|
+
.filter((item) => item.blocking && !['complete', 'skipped'].includes(String(item.status)))
|
|
1609
|
+
.map((item) => `delegation_active_${item.id}`);
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
async function readJsonIfExists(path) {
|
|
1613
|
+
if (!path || !existsSync(path)) {
|
|
1614
|
+
return null;
|
|
1615
|
+
}
|
|
1616
|
+
try {
|
|
1617
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
1618
|
+
} catch {
|
|
1619
|
+
return null;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
async function buildCompletionAudit({ cwd, root, slug, state, reviewReworkArtifactPath = null, iterationData, ledger, baseBlockers }) {
|
|
1624
|
+
const checklist = [];
|
|
1625
|
+
const iterationEvidence = [
|
|
1626
|
+
...(iterationData.executionEvidence || []),
|
|
1627
|
+
...(iterationData.verificationEvidence || []),
|
|
1628
|
+
].filter(Boolean).map(String);
|
|
1629
|
+
const addChecklistItem = (item) => {
|
|
1630
|
+
checklist.push({
|
|
1631
|
+
status: 'covered',
|
|
1632
|
+
evidence: [],
|
|
1633
|
+
...item,
|
|
1634
|
+
});
|
|
1635
|
+
};
|
|
1636
|
+
|
|
1637
|
+
addChecklistItem({
|
|
1638
|
+
id: 'approved-prd',
|
|
1639
|
+
source: 'approved-plan',
|
|
1640
|
+
requirement: state.plan_artifact_path || join(cwd, '.loopx', 'plans', `prd-${slug}.md`),
|
|
1641
|
+
evidence: [state.plan_artifact_path || 'approved plan artifact'],
|
|
1642
|
+
});
|
|
1643
|
+
addChecklistItem({
|
|
1644
|
+
id: 'test-spec',
|
|
1645
|
+
source: 'test-spec',
|
|
1646
|
+
requirement: state.test_spec_artifact_path || join(cwd, '.loopx', 'plans', `test-spec-${slug}.md`),
|
|
1647
|
+
evidence: iterationData.verificationEvidence || [],
|
|
1648
|
+
});
|
|
1649
|
+
const effectiveReviewReworkPath = reviewReworkArtifactPath || state.review_rework_artifact_path;
|
|
1650
|
+
if (effectiveReviewReworkPath) {
|
|
1651
|
+
addChecklistItem({
|
|
1652
|
+
id: 'review-rework',
|
|
1653
|
+
source: 'review-rework',
|
|
1654
|
+
requirement: effectiveReviewReworkPath,
|
|
1655
|
+
evidence: [
|
|
1656
|
+
effectiveReviewReworkPath,
|
|
1657
|
+
...(iterationData.executionEvidence || []),
|
|
1658
|
+
...(iterationData.verificationEvidence || []),
|
|
1659
|
+
].filter(Boolean),
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
const slicesPayload = await readJsonIfExists(state.change_artifact_paths?.slices);
|
|
1664
|
+
const slices = Array.isArray(slicesPayload?.slices) ? slicesPayload.slices : [];
|
|
1665
|
+
for (const slice of slices) {
|
|
1666
|
+
const signal = String(slice.verification_signal || '').trim();
|
|
1667
|
+
const usesLegacyGenericSignal = signal === 'execution-record.md verification evidence';
|
|
1668
|
+
const sliceEvidence = usesLegacyGenericSignal
|
|
1669
|
+
? iterationEvidence
|
|
1670
|
+
: iterationEvidence.filter((item) => item.includes(signal));
|
|
1671
|
+
addChecklistItem({
|
|
1672
|
+
id: slice.id || `slice-${checklist.length + 1}`,
|
|
1673
|
+
source: 'vertical-slice',
|
|
1674
|
+
status: sliceEvidence.length > 0 ? 'covered' : 'missing-evidence',
|
|
1675
|
+
requirement: slice.behavior || signal || 'vertical slice',
|
|
1676
|
+
evidence: sliceEvidence,
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
const verificationEvidence = [
|
|
1681
|
+
...(iterationData.verificationEvidence || []),
|
|
1682
|
+
...(iterationData.lanes || [])
|
|
1683
|
+
.flatMap((lane) => Array.isArray(lane.evidence) ? lane.evidence : [])
|
|
1684
|
+
.map((item) => `${item.kind}:${item.summary}:${item.ref}`),
|
|
1685
|
+
].filter(Boolean);
|
|
1686
|
+
const blockers = dedupeStrings([
|
|
1687
|
+
...baseBlockers,
|
|
1688
|
+
...buildDelegationBlockers(ledger),
|
|
1689
|
+
]);
|
|
1690
|
+
const missingEvidence = checklist.filter((item) => !Array.isArray(item.evidence) || item.evidence.length === 0);
|
|
1691
|
+
if (checklist.length === 0 || missingEvidence.length > 0 || verificationEvidence.length === 0) {
|
|
1692
|
+
blockers.push('completion_audit_missing_evidence');
|
|
1693
|
+
}
|
|
1694
|
+
const passed = blockers.length === 0;
|
|
1695
|
+
return {
|
|
1696
|
+
schema_version: WORKFLOW_SCHEMA_VERSION,
|
|
1697
|
+
slug,
|
|
1698
|
+
owner_id: ledger.owner_id,
|
|
1699
|
+
owner_session_id: ledger.owner_session_id,
|
|
1700
|
+
status: passed ? 'passed' : 'blocked',
|
|
1701
|
+
passed,
|
|
1702
|
+
updated_at: nowIso(),
|
|
1703
|
+
blockers: dedupeStrings(blockers),
|
|
1704
|
+
checklist,
|
|
1705
|
+
verification_evidence: verificationEvidence,
|
|
1706
|
+
lane_statuses: (iterationData.lanes || []).map((lane) => ({ name: lane.name, status: lane.status })),
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function buildHasInfrastructureFailure(iterationData) {
|
|
1711
|
+
const limitationText = [
|
|
1712
|
+
...(Array.isArray(iterationData.limitations) ? iterationData.limitations : []),
|
|
1713
|
+
...(Array.isArray(iterationData.lanes) ? iterationData.lanes.flatMap((lane) => [lane.summary, ...(Array.isArray(lane.limitations) ? lane.limitations : [])]) : []),
|
|
1714
|
+
].join('\n');
|
|
1715
|
+
return /codex_exec_failed:|codex_exec_invalid_json:|timeout/i.test(limitationText);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
644
1718
|
function buildExecutionRecordContent({ slug, iterationData, complete }) {
|
|
645
1719
|
const placeholder = complete ? null : 'TODO: build iteration is not review-ready yet.';
|
|
646
1720
|
return [
|
|
@@ -656,6 +1730,7 @@ function buildExecutionRecordContent({ slug, iterationData, complete }) {
|
|
|
656
1730
|
completed_at: nowIso(),
|
|
657
1731
|
checkpoint_count: iterationData.lanes.length,
|
|
658
1732
|
evidence_manifest: iterationData.lanes.flatMap((lane) => lane.evidence || []),
|
|
1733
|
+
changed_files: iterationData.changedFiles || [],
|
|
659
1734
|
}),
|
|
660
1735
|
`# loopx Execution Record: ${slug}`,
|
|
661
1736
|
'',
|
|
@@ -681,7 +1756,7 @@ function buildExecutionRecordContent({ slug, iterationData, complete }) {
|
|
|
681
1756
|
].join('\n');
|
|
682
1757
|
}
|
|
683
1758
|
|
|
684
|
-
async function writeBuildSupportArtifacts(root, iterationData, noDeslop) {
|
|
1759
|
+
async function writeBuildSupportArtifacts(root, iterationData, noDeslop, { delegationLedger = null, completionAudit = null } = {}) {
|
|
685
1760
|
const paths = resolveBuildSupportPaths(root, iterationData.iteration);
|
|
686
1761
|
await ensureDir(paths.supportRoot);
|
|
687
1762
|
await writeText(
|
|
@@ -718,6 +1793,22 @@ async function writeBuildSupportArtifacts(root, iterationData, noDeslop) {
|
|
|
718
1793
|
`- status: ${noDeslop ? 'skipped' : iterationData.regressionStatus}`,
|
|
719
1794
|
].join('\n'),
|
|
720
1795
|
);
|
|
1796
|
+
await writeJson(paths.delegationLedger, delegationLedger || {
|
|
1797
|
+
schema_version: WORKFLOW_SCHEMA_VERSION,
|
|
1798
|
+
slug: iterationData.slug,
|
|
1799
|
+
status: 'drained',
|
|
1800
|
+
active_blocking_count: 0,
|
|
1801
|
+
delegations: [],
|
|
1802
|
+
});
|
|
1803
|
+
await writeJson(paths.completionAudit, completionAudit || {
|
|
1804
|
+
schema_version: WORKFLOW_SCHEMA_VERSION,
|
|
1805
|
+
slug: iterationData.slug,
|
|
1806
|
+
status: 'blocked',
|
|
1807
|
+
passed: false,
|
|
1808
|
+
blockers: ['completion_audit_not_run'],
|
|
1809
|
+
checklist: [],
|
|
1810
|
+
verification_evidence: [],
|
|
1811
|
+
});
|
|
721
1812
|
return paths;
|
|
722
1813
|
}
|
|
723
1814
|
|
|
@@ -785,6 +1876,207 @@ function clarifyReadinessBlockers(state) {
|
|
|
785
1876
|
return blockers;
|
|
786
1877
|
}
|
|
787
1878
|
|
|
1879
|
+
function planReadinessBlockersSync(state) {
|
|
1880
|
+
const blockers = [];
|
|
1881
|
+
if (state.plan_architect_review_status !== 'complete') {
|
|
1882
|
+
blockers.push('architect_review_incomplete');
|
|
1883
|
+
}
|
|
1884
|
+
if (state.plan_critic_verdict !== 'approve') {
|
|
1885
|
+
blockers.push(`critic_verdict_${state.plan_critic_verdict}`);
|
|
1886
|
+
}
|
|
1887
|
+
if (state.plan_package_status !== 'complete') {
|
|
1888
|
+
blockers.push(`plan_package_${state.plan_package_status}`);
|
|
1889
|
+
}
|
|
1890
|
+
if (!state.plan_acceptance_criteria_testable) {
|
|
1891
|
+
blockers.push('acceptance_criteria_unresolved');
|
|
1892
|
+
}
|
|
1893
|
+
if (!state.plan_verification_steps_resolved) {
|
|
1894
|
+
blockers.push('verification_steps_unresolved');
|
|
1895
|
+
}
|
|
1896
|
+
if (!state.plan_execution_inputs_resolved) {
|
|
1897
|
+
blockers.push('execution_inputs_unresolved');
|
|
1898
|
+
}
|
|
1899
|
+
if (!state.plan_artifact_path) {
|
|
1900
|
+
blockers.push('missing_prd');
|
|
1901
|
+
}
|
|
1902
|
+
if (!state.test_spec_artifact_path) {
|
|
1903
|
+
blockers.push('missing_test_spec');
|
|
1904
|
+
}
|
|
1905
|
+
if (state.change_artifacts_status !== 'complete' && state.change_artifacts_status !== 'archived') {
|
|
1906
|
+
blockers.push(`change_artifacts_${state.change_artifacts_status || 'missing'}`);
|
|
1907
|
+
}
|
|
1908
|
+
if (state.spec_delta_status !== 'complete') {
|
|
1909
|
+
blockers.push(`spec_delta_${state.spec_delta_status || 'missing'}`);
|
|
1910
|
+
}
|
|
1911
|
+
if (Array.isArray(state.plan_blockers)) {
|
|
1912
|
+
blockers.push(...state.plan_blockers);
|
|
1913
|
+
}
|
|
1914
|
+
return dedupeStrings(blockers);
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
function buildReadinessBlockersSync(state) {
|
|
1918
|
+
const blockers = [];
|
|
1919
|
+
if (state.execution_record_status !== 'complete') {
|
|
1920
|
+
blockers.push(`execution_record_${state.execution_record_status || 'missing'}`);
|
|
1921
|
+
}
|
|
1922
|
+
if (Array.isArray(state.build_blockers)) {
|
|
1923
|
+
blockers.push(...state.build_blockers);
|
|
1924
|
+
}
|
|
1925
|
+
return dedupeStrings(blockers);
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
function reviewReadinessBlockersSync(state) {
|
|
1929
|
+
const blockers = [];
|
|
1930
|
+
if (state.review_status !== 'ready-for-review' && state.review_status !== 'in-review') {
|
|
1931
|
+
blockers.push(`review_status_${state.review_status || 'not-started'}`);
|
|
1932
|
+
}
|
|
1933
|
+
if (state.execution_record_status !== 'complete') {
|
|
1934
|
+
blockers.push(`execution_record_${state.execution_record_status || 'missing'}`);
|
|
1935
|
+
}
|
|
1936
|
+
if (Array.isArray(state.build_blockers)) {
|
|
1937
|
+
blockers.push(...state.build_blockers);
|
|
1938
|
+
}
|
|
1939
|
+
return dedupeStrings(blockers);
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
function doneReadinessBlockersSync(state) {
|
|
1943
|
+
const blockers = [];
|
|
1944
|
+
if (state.review_verdict !== 'approve') {
|
|
1945
|
+
blockers.push(`review_verdict_${state.review_verdict || 'none'}`);
|
|
1946
|
+
}
|
|
1947
|
+
return blockers;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
function archiveReadinessBlockersSync(state) {
|
|
1951
|
+
const blockers = [];
|
|
1952
|
+
if (state.current_stage !== STAGES.DONE || state.completion_confirmed !== true) {
|
|
1953
|
+
blockers.push('workflow_not_done');
|
|
1954
|
+
}
|
|
1955
|
+
if (state.spec_delta_status !== 'complete') {
|
|
1956
|
+
blockers.push(`spec_delta_${state.spec_delta_status || 'missing'}`);
|
|
1957
|
+
}
|
|
1958
|
+
if (!state.change_artifact_paths?.specDelta) {
|
|
1959
|
+
blockers.push('missing_spec_delta_path');
|
|
1960
|
+
}
|
|
1961
|
+
return dedupeStrings(blockers);
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
function readinessEntry(blockers) {
|
|
1965
|
+
const unique = dedupeStrings(blockers);
|
|
1966
|
+
return {
|
|
1967
|
+
ready: unique.length === 0,
|
|
1968
|
+
blockers: unique,
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
function authorizationEntry(state, key, transition) {
|
|
1973
|
+
return {
|
|
1974
|
+
authorized: state.approval?.[key] === APPROVAL_STATES.APPROVED,
|
|
1975
|
+
approval_status: state.approval?.[key] || APPROVAL_STATES.NOT_REQUESTED,
|
|
1976
|
+
transition,
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
function buildReadiness(state) {
|
|
1981
|
+
return {
|
|
1982
|
+
plan: readinessEntry(clarifyReadinessBlockers(state)),
|
|
1983
|
+
build: readinessEntry(planReadinessBlockersSync(state)),
|
|
1984
|
+
review: readinessEntry(buildReadinessBlockersSync(state)),
|
|
1985
|
+
done: readinessEntry(doneReadinessBlockersSync(state)),
|
|
1986
|
+
archive: readinessEntry(archiveReadinessBlockersSync(state)),
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
function buildAuthorization(state) {
|
|
1991
|
+
return {
|
|
1992
|
+
plan: authorizationEntry(state, 'plan', TRANSITIONS.CLARIFY_TO_PLAN),
|
|
1993
|
+
build: authorizationEntry(state, 'build', TRANSITIONS.PLAN_TO_BUILD),
|
|
1994
|
+
review: authorizationEntry(state, 'review', TRANSITIONS.BUILD_TO_REVIEW),
|
|
1995
|
+
done: authorizationEntry(state, 'complete', TRANSITIONS.REVIEW_TO_DONE),
|
|
1996
|
+
rollback: authorizationEntry(state, 'rollback', state.requested_transition || TRANSITIONS.NONE),
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
function evidenceEntry(claim, basis, implication) {
|
|
2001
|
+
return { claim, basis, implication };
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
function buildCurrentEvidenceChain(state, readiness = buildReadiness(state), authorization = buildAuthorization(state)) {
|
|
2005
|
+
const evidence = [];
|
|
2006
|
+
if (readiness.plan.ready) {
|
|
2007
|
+
evidence.push(evidenceEntry(
|
|
2008
|
+
'clarify_ready_for_plan',
|
|
2009
|
+
'Clarify ambiguity score, non-goals, decision boundaries, pressure pass, and unresolved ambiguity gates are satisfied.',
|
|
2010
|
+
authorization.plan.authorized ? 'The approved clarify -> plan transition can be consumed by plan.' : 'Plan readiness exists, but user authorization is still separate.',
|
|
2011
|
+
));
|
|
2012
|
+
}
|
|
2013
|
+
if (authorization.plan.authorized) {
|
|
2014
|
+
evidence.push(evidenceEntry(
|
|
2015
|
+
'plan_authorized',
|
|
2016
|
+
'approval.plan is approved for clarify -> plan.',
|
|
2017
|
+
'Planning may proceed without treating readiness alone as authorization.',
|
|
2018
|
+
));
|
|
2019
|
+
}
|
|
2020
|
+
if (readiness.build.ready) {
|
|
2021
|
+
evidence.push(evidenceEntry(
|
|
2022
|
+
'plan_ready_for_build',
|
|
2023
|
+
'Planner, architect, critic, plan artifacts, execution inputs, and change delta gates are satisfied.',
|
|
2024
|
+
authorization.build.authorized ? 'The approved plan -> build transition can be consumed by build.' : 'Build readiness exists, but user authorization is still separate.',
|
|
2025
|
+
));
|
|
2026
|
+
}
|
|
2027
|
+
if (authorization.build.authorized) {
|
|
2028
|
+
evidence.push(evidenceEntry(
|
|
2029
|
+
'build_authorized',
|
|
2030
|
+
'approval.build is approved for plan -> build or review-requested build rework.',
|
|
2031
|
+
'Build may consume the approved transition while preserving gate evidence.',
|
|
2032
|
+
));
|
|
2033
|
+
}
|
|
2034
|
+
if (readiness.review.ready) {
|
|
2035
|
+
evidence.push(evidenceEntry(
|
|
2036
|
+
'build_ready_for_review',
|
|
2037
|
+
'Execution record is complete and no build blockers remain.',
|
|
2038
|
+
authorization.review.authorized ? 'The approved build -> review transition can be consumed by review.' : 'Review readiness exists, but user authorization is still separate.',
|
|
2039
|
+
));
|
|
2040
|
+
}
|
|
2041
|
+
if (authorization.review.authorized) {
|
|
2042
|
+
evidence.push(evidenceEntry(
|
|
2043
|
+
'review_authorized',
|
|
2044
|
+
'approval.review is approved for build -> review.',
|
|
2045
|
+
'Review may proceed as an independent acceptance gate.',
|
|
2046
|
+
));
|
|
2047
|
+
}
|
|
2048
|
+
if (state.review_verdict === 'approve') {
|
|
2049
|
+
evidence.push(evidenceEntry(
|
|
2050
|
+
'review_approved',
|
|
2051
|
+
'Review verdict is approve.',
|
|
2052
|
+
authorization.done.authorized ? 'The approved review -> done transition can be consumed.' : 'Completion still requires explicit review -> done authorization.',
|
|
2053
|
+
));
|
|
2054
|
+
}
|
|
2055
|
+
if (state.archive_status === 'archived' && state.spec_sync_status === 'synced') {
|
|
2056
|
+
evidence.push(evidenceEntry(
|
|
2057
|
+
'change_delta_archived',
|
|
2058
|
+
'Archive synced the accepted spec delta into long-lived specs.',
|
|
2059
|
+
'The workflow has durable spec memory and can remain closed.',
|
|
2060
|
+
));
|
|
2061
|
+
}
|
|
2062
|
+
return evidence;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
function enrichRuntimeJudgment(state, legacy = false) {
|
|
2066
|
+
if (!state || legacy) {
|
|
2067
|
+
return state;
|
|
2068
|
+
}
|
|
2069
|
+
const readiness = buildReadiness(state);
|
|
2070
|
+
const authorization = buildAuthorization(state);
|
|
2071
|
+
return {
|
|
2072
|
+
...state,
|
|
2073
|
+
readiness,
|
|
2074
|
+
authorization,
|
|
2075
|
+
current_evidence_chain: buildCurrentEvidenceChain(state, readiness, authorization),
|
|
2076
|
+
recommended_next_action: recommendedAction(state, legacy),
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
|
|
788
2080
|
async function readExecutionRecordSummary(root) {
|
|
789
2081
|
const text = await readTextIfExists(artifactPath(root, 'execution-record.md'));
|
|
790
2082
|
if (!text) {
|
|
@@ -814,6 +2106,46 @@ async function readExecutionRecordSummary(root) {
|
|
|
814
2106
|
};
|
|
815
2107
|
}
|
|
816
2108
|
|
|
2109
|
+
function normalizeScopeList(value) {
|
|
2110
|
+
if (Array.isArray(value)) {
|
|
2111
|
+
return value.map((item) => String(item).trim()).filter(Boolean);
|
|
2112
|
+
}
|
|
2113
|
+
if (value === null || value === undefined || value === '') {
|
|
2114
|
+
return [];
|
|
2115
|
+
}
|
|
2116
|
+
return String(value)
|
|
2117
|
+
.split(/[,;\n]/)
|
|
2118
|
+
.map((item) => item.trim())
|
|
2119
|
+
.filter(Boolean);
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
function executionScopeGate(meta = {}) {
|
|
2123
|
+
const plannedScope = String(meta.planned_scope || '').trim();
|
|
2124
|
+
const implementedScope = String(meta.implemented_scope || '').trim();
|
|
2125
|
+
const completionClaim = String(meta.completion_claim || '').trim().toLowerCase();
|
|
2126
|
+
const remainingScope = normalizeScopeList(meta.remaining_scope);
|
|
2127
|
+
const blockers = [];
|
|
2128
|
+
|
|
2129
|
+
if (remainingScope.length > 0) {
|
|
2130
|
+
blockers.push('partial_scope_remaining');
|
|
2131
|
+
}
|
|
2132
|
+
if (completionClaim && !['full', 'complete', 'workflow', 'all'].includes(completionClaim)) {
|
|
2133
|
+
blockers.push(`completion_claim_${completionClaim}`);
|
|
2134
|
+
}
|
|
2135
|
+
if (plannedScope && implementedScope && plannedScope !== implementedScope && completionClaim !== 'full') {
|
|
2136
|
+
blockers.push('implemented_scope_mismatch');
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
return {
|
|
2140
|
+
ok: blockers.length === 0,
|
|
2141
|
+
blockers: dedupeStrings(blockers),
|
|
2142
|
+
plannedScope,
|
|
2143
|
+
implementedScope,
|
|
2144
|
+
completionClaim,
|
|
2145
|
+
remainingScope,
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
|
|
817
2149
|
function recommendedAction(state, legacy = false) {
|
|
818
2150
|
if (legacy) {
|
|
819
2151
|
return 'Legacy codex-helper workflow detected. Run loopx migrate or create a new loopx workflow.';
|
|
@@ -826,7 +2158,7 @@ function recommendedAction(state, legacy = false) {
|
|
|
826
2158
|
: `Resolve ambiguity in ${state.clarify_profile ?? 'standard'} clarify mode and approve clarify -> plan.`;
|
|
827
2159
|
case STAGES.PLAN:
|
|
828
2160
|
if (Array.isArray(state.plan_blockers) && state.plan_blockers.length > 0) {
|
|
829
|
-
return 'Run loopx plan to continue the planning review loop until architect, critic, and
|
|
2161
|
+
return 'Run loopx plan to continue the planning review loop until architect, critic, and planning artifact blockers are cleared.';
|
|
830
2162
|
}
|
|
831
2163
|
return state.approval.build === APPROVAL_STATES.APPROVED
|
|
832
2164
|
? 'Run loopx build to consume the approved plan -> build transition.'
|
|
@@ -845,15 +2177,31 @@ function recommendedAction(state, legacy = false) {
|
|
|
845
2177
|
: 'Approve review -> done to complete the workflow.';
|
|
846
2178
|
}
|
|
847
2179
|
if (state.review_verdict === 'request-changes') {
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
2180
|
+
if (state.requested_transition === TRANSITIONS.REVIEW_TO_BUILD && state.approval.build === APPROVAL_STATES.APPROVED) {
|
|
2181
|
+
return 'Run loopx build to consume the approved review -> build transition.';
|
|
2182
|
+
}
|
|
2183
|
+
if (state.requested_transition === TRANSITIONS.REVIEW_TO_PLAN && state.approval.rollback === APPROVAL_STATES.APPROVED) {
|
|
2184
|
+
return 'Run loopx plan to consume the approved review -> plan transition.';
|
|
2185
|
+
}
|
|
2186
|
+
if (state.requested_transition === TRANSITIONS.REVIEW_TO_CLARIFY && state.approval.rollback === APPROVAL_STATES.APPROVED) {
|
|
2187
|
+
return 'Run loopx clarify to consume the approved review -> clarify transition.';
|
|
2188
|
+
}
|
|
2189
|
+
if (state.rollback_target === STAGES.BUILD) {
|
|
2190
|
+
return 'Approve review -> build to fix implementation issues.';
|
|
2191
|
+
}
|
|
2192
|
+
if (state.rollback_target === STAGES.CLARIFY) {
|
|
2193
|
+
return 'Approve review -> clarify to resolve requirement ambiguity.';
|
|
2194
|
+
}
|
|
2195
|
+
return 'Approve review -> plan to revise the plan package.';
|
|
851
2196
|
}
|
|
852
2197
|
return 'Run loopx review after build completes.';
|
|
853
2198
|
case STAGES.DONE:
|
|
854
2199
|
if (state.autopilot_current_phase && state.autopilot_current_phase !== 'none' && state.autopilot_completed) {
|
|
855
2200
|
return 'Autopilot run is complete.';
|
|
856
2201
|
}
|
|
2202
|
+
if (state.archive_status !== 'archived') {
|
|
2203
|
+
return 'Run loopx archive to sync the approved change delta into long-lived specs.';
|
|
2204
|
+
}
|
|
857
2205
|
return 'Workflow is complete.';
|
|
858
2206
|
default:
|
|
859
2207
|
return 'Run loopx clarify to start a workflow.';
|
|
@@ -861,10 +2209,7 @@ function recommendedAction(state, legacy = false) {
|
|
|
861
2209
|
}
|
|
862
2210
|
|
|
863
2211
|
function withRecommendedAction(state, legacy = false) {
|
|
864
|
-
return
|
|
865
|
-
...state,
|
|
866
|
-
recommended_next_action: recommendedAction(state, legacy),
|
|
867
|
-
};
|
|
2212
|
+
return enrichRuntimeJudgment(state, legacy);
|
|
868
2213
|
}
|
|
869
2214
|
|
|
870
2215
|
async function loadWorkflowState(cwd, slug, { allowLegacy = true } = {}) {
|
|
@@ -897,7 +2242,10 @@ function approvalKeyForTransition(transition) {
|
|
|
897
2242
|
return 'build';
|
|
898
2243
|
case TRANSITIONS.BUILD_TO_REVIEW:
|
|
899
2244
|
return 'review';
|
|
2245
|
+
case TRANSITIONS.REVIEW_TO_BUILD:
|
|
2246
|
+
return 'build';
|
|
900
2247
|
case TRANSITIONS.REVIEW_TO_PLAN:
|
|
2248
|
+
case TRANSITIONS.REVIEW_TO_CLARIFY:
|
|
901
2249
|
return 'rollback';
|
|
902
2250
|
case TRANSITIONS.REVIEW_TO_DONE:
|
|
903
2251
|
return 'complete';
|
|
@@ -912,6 +2260,34 @@ function ensureApprovedTransition(state, expectedTransition, key) {
|
|
|
912
2260
|
}
|
|
913
2261
|
}
|
|
914
2262
|
|
|
2263
|
+
function ensureValidContextManifest(manifest, stage) {
|
|
2264
|
+
if (manifest?.status === 'invalid') {
|
|
2265
|
+
throw new Error(`context_manifest_invalid:${stage}:${manifest.error || 'unknown'}`);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
async function writeReviewJournal({ cwd, slug, verdict, reviewMessageZh, evidenceManifest = [], findings = [], followUps = [] }) {
|
|
2270
|
+
return appendWorkspaceJournal({
|
|
2271
|
+
cwd,
|
|
2272
|
+
workspaceRoot: resolveWorkspaceRoot(cwd),
|
|
2273
|
+
slug,
|
|
2274
|
+
stage: STAGES.REVIEW,
|
|
2275
|
+
verdict,
|
|
2276
|
+
reviewMessageZh,
|
|
2277
|
+
verificationEvidence: evidenceManifest.map((item) => item.summary || item.ref || JSON.stringify(item)),
|
|
2278
|
+
decisions: ['review 已执行 code review 与证据完整性检查。'],
|
|
2279
|
+
risks: verdict === 'APPROVE' ? ['暂无阻断风险。'] : findings,
|
|
2280
|
+
followUps,
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
async function writeReviewChangedFiles(root, changedFiles = []) {
|
|
2285
|
+
await ensureDir(join(root, 'review-support'));
|
|
2286
|
+
const path = join(root, 'review-support', 'changed-files.json');
|
|
2287
|
+
await writeText(path, `${JSON.stringify(Array.isArray(changedFiles) ? changedFiles : [], null, 2)}\n`);
|
|
2288
|
+
return path;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
915
2291
|
function executionRecordTemplate(slug, stage, actorId, runId) {
|
|
916
2292
|
const timestamp = nowIso();
|
|
917
2293
|
return [
|
|
@@ -952,7 +2328,116 @@ function executionRecordTemplate(slug, stage, actorId, runId) {
|
|
|
952
2328
|
].join('\n');
|
|
953
2329
|
}
|
|
954
2330
|
|
|
955
|
-
function
|
|
2331
|
+
function reviewVerdictLabel(verdict) {
|
|
2332
|
+
return verdict === 'APPROVE' ? '通过' : '要求修改';
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
function rollbackTargetLabel(rollbackTarget) {
|
|
2336
|
+
if (rollbackTarget === 'none') {
|
|
2337
|
+
return '无需回滚';
|
|
2338
|
+
}
|
|
2339
|
+
if (rollbackTarget === 'build') {
|
|
2340
|
+
return '回到 build 阶段修复实现问题';
|
|
2341
|
+
}
|
|
2342
|
+
if (rollbackTarget === 'plan') {
|
|
2343
|
+
return '回退到 plan 阶段';
|
|
2344
|
+
}
|
|
2345
|
+
if (rollbackTarget === 'clarify') {
|
|
2346
|
+
return '回到 clarify 阶段澄清需求';
|
|
2347
|
+
}
|
|
2348
|
+
return rollbackTarget;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
function transitionForRollbackTarget(target) {
|
|
2352
|
+
if (target === STAGES.BUILD) {
|
|
2353
|
+
return TRANSITIONS.REVIEW_TO_BUILD;
|
|
2354
|
+
}
|
|
2355
|
+
if (target === STAGES.CLARIFY) {
|
|
2356
|
+
return TRANSITIONS.REVIEW_TO_CLARIFY;
|
|
2357
|
+
}
|
|
2358
|
+
return TRANSITIONS.REVIEW_TO_PLAN;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
function nextCommandForRollbackTarget(slug, target) {
|
|
2362
|
+
if (target === STAGES.BUILD) {
|
|
2363
|
+
return [
|
|
2364
|
+
'Next:',
|
|
2365
|
+
reviewReworkBuildCommand(slug),
|
|
2366
|
+
].join('\n');
|
|
2367
|
+
}
|
|
2368
|
+
if (target === STAGES.CLARIFY) {
|
|
2369
|
+
return [
|
|
2370
|
+
'Next:',
|
|
2371
|
+
`loopx approve ${slug} --from review --to clarify`,
|
|
2372
|
+
`$clarify ${slug}`,
|
|
2373
|
+
].join('\n');
|
|
2374
|
+
}
|
|
2375
|
+
if (target === 'none') {
|
|
2376
|
+
return [
|
|
2377
|
+
'Next:',
|
|
2378
|
+
`loopx approve ${slug} --from review --to done`,
|
|
2379
|
+
].join('\n');
|
|
2380
|
+
}
|
|
2381
|
+
return [
|
|
2382
|
+
'Next:',
|
|
2383
|
+
`loopx approve ${slug} --from review --to plan`,
|
|
2384
|
+
`$plan ${slug}`,
|
|
2385
|
+
].join('\n');
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
function reviewUserMessageZh({ slug, verdict, rollbackTarget, findings }) {
|
|
2389
|
+
const label = reviewVerdictLabel(verdict);
|
|
2390
|
+
const next = verdict === 'APPROVE'
|
|
2391
|
+
? `下一步:批准 review -> done 后完成工作流。\n${nextCommandForRollbackTarget(slug, 'none')}`
|
|
2392
|
+
: `下一步:按审查发现处理,并${rollbackTargetLabel(rollbackTarget)}。\n${nextCommandForRollbackTarget(slug, rollbackTarget)}`;
|
|
2393
|
+
const findingText = Array.isArray(findings) && findings.length > 0 ? findings.join(';') : '无额外发现。';
|
|
2394
|
+
return `Review 结果:${slug} ${label}。审查发现:${findingText} ${next}`;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
function codeReviewFindingText(finding) {
|
|
2398
|
+
const location = finding.file ? `${finding.file}${finding.line ? `:${finding.line}` : ''}` : '未定位文件';
|
|
2399
|
+
return `[${finding.severity || 'medium'}] ${location}:${finding.message}`;
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
function codeReviewFailureResult(error) {
|
|
2403
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2404
|
+
return {
|
|
2405
|
+
status: 'failed',
|
|
2406
|
+
verdict: 'request-changes',
|
|
2407
|
+
summary: `code-review 子流程失败,review 不能接受本次运行:${message}`,
|
|
2408
|
+
rollbackTarget: STAGES.BUILD,
|
|
2409
|
+
changedFiles: [],
|
|
2410
|
+
findings: [{
|
|
2411
|
+
severity: 'high',
|
|
2412
|
+
file: 'review-support/code-review.raw.json',
|
|
2413
|
+
line: null,
|
|
2414
|
+
message: `code-review 子流程未返回有效结构化 JSON:${message}`,
|
|
2415
|
+
}],
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
function architectureReviewFailureResult(error) {
|
|
2420
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2421
|
+
return {
|
|
2422
|
+
status: 'failed',
|
|
2423
|
+
verdict: 'block',
|
|
2424
|
+
summary: `architecture-smell 子流程失败,review 不能接受本次运行:${message}`,
|
|
2425
|
+
rollbackTarget: STAGES.BUILD,
|
|
2426
|
+
findings: [{
|
|
2427
|
+
severity: 'high',
|
|
2428
|
+
file: 'review-support/architecture-smell.raw.json',
|
|
2429
|
+
line: null,
|
|
2430
|
+
message: `architecture-smell 子流程未返回有效结构化 JSON:${message}`,
|
|
2431
|
+
}],
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
function architectureReviewFindingText(finding) {
|
|
2436
|
+
const location = finding.file ? `${finding.file}${finding.line ? `:${finding.line}` : ''}` : '未定位文件';
|
|
2437
|
+
return `[${finding.severity || 'medium'}] ${location}:${finding.message}`;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
function reviewReportContent({ slug, reviewer, runId, verdict, rollbackTarget, rollbackRationale, inputManifest, evidenceManifest, findings, codeReview, architectureReview }) {
|
|
956
2441
|
return [
|
|
957
2442
|
frontmatterBlock({
|
|
958
2443
|
schema_version: WORKFLOW_SCHEMA_VERSION,
|
|
@@ -962,28 +2447,52 @@ function reviewReportContent({ slug, reviewer, runId, verdict, rollbackTarget, r
|
|
|
962
2447
|
reviewed_run_id: runId,
|
|
963
2448
|
input_manifest: inputManifest,
|
|
964
2449
|
evidence_manifest: evidenceManifest,
|
|
2450
|
+
code_review: codeReview ? {
|
|
2451
|
+
status: codeReview.status,
|
|
2452
|
+
verdict: codeReview.verdict,
|
|
2453
|
+
changed_files: codeReview.changedFiles,
|
|
2454
|
+
} : null,
|
|
2455
|
+
architecture_smell: architectureReview ? {
|
|
2456
|
+
status: architectureReview.status,
|
|
2457
|
+
verdict: architectureReview.verdict,
|
|
2458
|
+
} : null,
|
|
965
2459
|
verdict: verdict.toLowerCase().replace('request changes', 'request-changes'),
|
|
966
2460
|
rollback_target: rollbackTarget,
|
|
967
2461
|
rollback_rationale: rollbackRationale ?? null,
|
|
968
2462
|
}),
|
|
969
|
-
`# loopx Review
|
|
2463
|
+
`# loopx Review 结果:${slug}`,
|
|
970
2464
|
'',
|
|
971
|
-
'##
|
|
2465
|
+
'## 结论',
|
|
972
2466
|
'',
|
|
973
|
-
`- ${verdict}
|
|
2467
|
+
`- ${reviewVerdictLabel(verdict)}(${verdict})`,
|
|
974
2468
|
'',
|
|
975
|
-
'##
|
|
2469
|
+
'## 已审查证据',
|
|
976
2470
|
'',
|
|
977
2471
|
...inputManifest.map((item) => `- ${item}`),
|
|
978
2472
|
'',
|
|
979
|
-
'##
|
|
2473
|
+
'## 审查发现',
|
|
980
2474
|
'',
|
|
981
2475
|
...findings.map((item) => `- ${item}`),
|
|
982
2476
|
'',
|
|
983
|
-
'##
|
|
2477
|
+
'## 代码审查',
|
|
2478
|
+
'',
|
|
2479
|
+
codeReview ? `- 状态:${codeReview.status}` : '- 状态:未执行',
|
|
2480
|
+
codeReview ? `- 结论:${codeReview.verdict}` : '- 结论:未知',
|
|
2481
|
+
codeReview ? `- 摘要:${codeReview.summary}` : '- 摘要:无',
|
|
2482
|
+
codeReview && codeReview.changedFiles.length > 0 ? `- 变更文件:${codeReview.changedFiles.join(', ')}` : '- 变更文件:无',
|
|
2483
|
+
...(codeReview && codeReview.findings.length > 0 ? codeReview.findings.map((item) => `- ${codeReviewFindingText(item)}`) : ['- 未发现阻断性代码问题。']),
|
|
2484
|
+
'',
|
|
2485
|
+
'## Architecture Smell Scan',
|
|
2486
|
+
'',
|
|
2487
|
+
architectureReview ? `- 状态:${architectureReview.status}` : '- 状态:未执行',
|
|
2488
|
+
architectureReview ? `- 结论:${architectureReview.verdict}` : '- 结论:未知',
|
|
2489
|
+
architectureReview ? `- 摘要:${architectureReview.summary}` : '- 摘要:无',
|
|
2490
|
+
...(architectureReview && architectureReview.findings.length > 0 ? architectureReview.findings.map((item) => `- ${architectureReviewFindingText(item)}`) : ['- 架构 smell 扫描通过。']),
|
|
984
2491
|
'',
|
|
985
|
-
|
|
986
|
-
|
|
2492
|
+
'## 回退建议',
|
|
2493
|
+
'',
|
|
2494
|
+
`- ${rollbackTargetLabel(rollbackTarget)}`,
|
|
2495
|
+
rollbackRationale ? `- ${rollbackRationale}` : '- 无',
|
|
987
2496
|
].join('\n');
|
|
988
2497
|
}
|
|
989
2498
|
|
|
@@ -1002,17 +2511,22 @@ export async function initWorkspace(cwd, { slug } = {}) {
|
|
|
1002
2511
|
const workspaceRoot = resolveWorkspaceRoot(cwd);
|
|
1003
2512
|
await ensureLoopxRoot(cwd);
|
|
1004
2513
|
await ensureDir(join(workspaceRoot, 'context'));
|
|
2514
|
+
await ensureDir(join(workspaceRoot, 'intake'));
|
|
1005
2515
|
await ensureDir(join(workspaceRoot, 'workflows'));
|
|
1006
2516
|
await ensureDir(join(workspaceRoot, 'specs'));
|
|
2517
|
+
await ensureDir(join(workspaceRoot, 'changes'));
|
|
2518
|
+
await ensureDir(join(workspaceRoot, 'changes', 'active'));
|
|
2519
|
+
await ensureDir(join(workspaceRoot, 'changes', 'archive'));
|
|
1007
2520
|
await ensureDir(join(workspaceRoot, 'plans'));
|
|
1008
2521
|
await ensureDir(join(workspaceRoot, 'autopilot'));
|
|
2522
|
+
await setupWorkspaceContext(cwd);
|
|
1009
2523
|
|
|
1010
2524
|
const config = {
|
|
1011
2525
|
schema_version: WORKSPACE_SCHEMA_VERSION,
|
|
1012
2526
|
tool: 'loopx',
|
|
1013
2527
|
product_contract: 'skill-first-v1',
|
|
1014
|
-
default_flow: ['clarify', 'plan', 'build', 'review', 'done'],
|
|
1015
|
-
preferred_surface: ['clarify', 'plan', 'build', 'review', 'autopilot'],
|
|
2528
|
+
default_flow: ['clarify', 'plan', 'build', 'review', 'done', 'archive'],
|
|
2529
|
+
preferred_surface: ['clarify', 'plan', 'build', 'review', 'archive', 'autopilot'],
|
|
1016
2530
|
};
|
|
1017
2531
|
|
|
1018
2532
|
if (!existsSync(workspaceConfigPath(workspaceRoot))) {
|
|
@@ -1033,21 +2547,62 @@ export async function clarifyStage(cwd, slug, { profile = 'standard' } = {}) {
|
|
|
1033
2547
|
const normalized = normalizeSlug(slug);
|
|
1034
2548
|
const clarifyProfile = normalizeClarifyProfile(profile);
|
|
1035
2549
|
const root = resolveWorkflowRoot(cwd, normalized);
|
|
2550
|
+
const existing = await readState(cwd, normalized);
|
|
2551
|
+
const consumesReviewClarify = existing?.current_stage === STAGES.REVIEW
|
|
2552
|
+
&& existing?.requested_transition === TRANSITIONS.REVIEW_TO_CLARIFY
|
|
2553
|
+
&& existing?.approval?.rollback === APPROVAL_STATES.APPROVED
|
|
2554
|
+
&& existing?.review_verdict === 'request-changes';
|
|
2555
|
+
const resumesConsumedReviewClarify = existing?.current_stage === STAGES.CLARIFY
|
|
2556
|
+
&& existing?.last_confirmed_transition === TRANSITIONS.REVIEW_TO_CLARIFY
|
|
2557
|
+
&& existing?.approval?.rollback === APPROVAL_STATES.APPROVED;
|
|
2558
|
+
const preservesExistingClarifySpec = consumesReviewClarify || resumesConsumedReviewClarify;
|
|
1036
2559
|
await ensureLoopxRoot(cwd);
|
|
1037
2560
|
await ensureDir(root);
|
|
1038
2561
|
const stamp = nowStamp();
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
2562
|
+
if (!preservesExistingClarifySpec) {
|
|
2563
|
+
await writeTemplateArtifact(root, 'spec.md', {
|
|
2564
|
+
'task name': normalized,
|
|
2565
|
+
'workflow id': normalized,
|
|
2566
|
+
profile: clarifyProfile,
|
|
2567
|
+
'target ambiguity threshold': CLARIFY_PROFILES[clarifyProfile].threshold,
|
|
2568
|
+
'max rounds': CLARIFY_PROFILES[clarifyProfile].maxRounds,
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
1046
2571
|
const specArtifactPath = canonicalClarifySpecPath(cwd, normalized, stamp);
|
|
1047
2572
|
await copyArtifact(root, specArtifactPath, 'spec.md');
|
|
1048
|
-
|
|
1049
|
-
|
|
2573
|
+
const state = withRecommendedAction({
|
|
2574
|
+
...(preservesExistingClarifySpec ? existing : createInitialState(normalized, clarifyProfile)),
|
|
2575
|
+
current_stage: STAGES.CLARIFY,
|
|
2576
|
+
stage_status: 'blocked',
|
|
2577
|
+
clarify_profile: clarifyProfile,
|
|
2578
|
+
clarify_target_ambiguity_threshold: CLARIFY_PROFILES[clarifyProfile].threshold,
|
|
2579
|
+
clarify_max_rounds: CLARIFY_PROFILES[clarifyProfile].maxRounds,
|
|
2580
|
+
clarify_current_round: preservesExistingClarifySpec ? existing.clarify_current_round : 0,
|
|
2581
|
+
clarify_ambiguity_score: 1,
|
|
2582
|
+
clarify_pressure_pass_complete: false,
|
|
2583
|
+
clarify_non_goals_resolved: false,
|
|
2584
|
+
clarify_decision_boundaries_resolved: false,
|
|
2585
|
+
ambiguity_items: preservesExistingClarifySpec ? existing.ambiguity_items : [
|
|
2586
|
+
{
|
|
2587
|
+
id: 'A-1',
|
|
2588
|
+
question: 'What specific task should loopx execute in this workflow?',
|
|
2589
|
+
status: 'open',
|
|
2590
|
+
resolution: null,
|
|
2591
|
+
},
|
|
2592
|
+
],
|
|
2593
|
+
unresolved_ambiguity_count: preservesExistingClarifySpec ? Math.max(1, Number(existing.unresolved_ambiguity_count || 0)) : 1,
|
|
1050
2594
|
spec_artifact_path: specArtifactPath,
|
|
2595
|
+
pending_user_decision: TRANSITIONS.NONE,
|
|
2596
|
+
requested_transition: TRANSITIONS.NONE,
|
|
2597
|
+
last_confirmed_transition: preservesExistingClarifySpec ? TRANSITIONS.REVIEW_TO_CLARIFY : TRANSITIONS.NONE,
|
|
2598
|
+
approval: {
|
|
2599
|
+
...(preservesExistingClarifySpec ? existing.approval : createInitialState(normalized, clarifyProfile).approval),
|
|
2600
|
+
plan: APPROVAL_STATES.NOT_REQUESTED,
|
|
2601
|
+
build: APPROVAL_STATES.NOT_REQUESTED,
|
|
2602
|
+
review: APPROVAL_STATES.NOT_REQUESTED,
|
|
2603
|
+
rollback: preservesExistingClarifySpec ? APPROVAL_STATES.APPROVED : APPROVAL_STATES.NOT_REQUESTED,
|
|
2604
|
+
complete: APPROVAL_STATES.NOT_REQUESTED,
|
|
2605
|
+
},
|
|
1051
2606
|
});
|
|
1052
2607
|
await writeState(root, state);
|
|
1053
2608
|
return { root, state };
|
|
@@ -1087,7 +2642,10 @@ export async function approveStage(cwd, slug, { from, to }) {
|
|
|
1087
2642
|
next = {
|
|
1088
2643
|
...next,
|
|
1089
2644
|
plan_docs_status: completion.docsStatus,
|
|
1090
|
-
plan_docs_artifact_paths:
|
|
2645
|
+
plan_docs_artifact_paths: null,
|
|
2646
|
+
change_artifacts_status: completion.changeArtifactsStatus,
|
|
2647
|
+
spec_delta_status: completion.specDeltaStatus,
|
|
2648
|
+
slice_artifacts_status: completion.sliceArtifactsStatus,
|
|
1091
2649
|
plan_blockers: completion.blockers,
|
|
1092
2650
|
};
|
|
1093
2651
|
if (completion.blockers.length > 0) {
|
|
@@ -1121,6 +2679,22 @@ export async function approveStage(cwd, slug, { from, to }) {
|
|
|
1121
2679
|
}
|
|
1122
2680
|
|
|
1123
2681
|
if (transition === TRANSITIONS.REVIEW_TO_PLAN) {
|
|
2682
|
+
if (next.review_verdict !== 'request-changes' || next.rollback_target !== STAGES.PLAN) {
|
|
2683
|
+
throw new Error('review_plan_fix_not_requested');
|
|
2684
|
+
}
|
|
2685
|
+
if (!next.rollback_rationale) {
|
|
2686
|
+
throw new Error('rollback_rationale_required');
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
if (transition === TRANSITIONS.REVIEW_TO_BUILD) {
|
|
2690
|
+
if (next.review_verdict !== 'request-changes' || next.rollback_target !== STAGES.BUILD) {
|
|
2691
|
+
throw new Error('review_build_fix_not_requested');
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
if (transition === TRANSITIONS.REVIEW_TO_CLARIFY) {
|
|
2695
|
+
if (next.review_verdict !== 'request-changes' || next.rollback_target !== STAGES.CLARIFY) {
|
|
2696
|
+
throw new Error('review_clarify_fix_not_requested');
|
|
2697
|
+
}
|
|
1124
2698
|
if (!next.rollback_rationale) {
|
|
1125
2699
|
throw new Error('rollback_rationale_required');
|
|
1126
2700
|
}
|
|
@@ -1130,6 +2704,65 @@ export async function approveStage(cwd, slug, { from, to }) {
|
|
|
1130
2704
|
throw new Error('review_not_approved');
|
|
1131
2705
|
}
|
|
1132
2706
|
|
|
2707
|
+
if (transition === TRANSITIONS.REVIEW_TO_DONE) {
|
|
2708
|
+
const executionSummary = await readExecutionRecordSummary(root);
|
|
2709
|
+
const scopeGate = executionScopeGate(executionSummary.meta);
|
|
2710
|
+
if (!scopeGate.ok) {
|
|
2711
|
+
const blocked = withRecommendedAction({
|
|
2712
|
+
...next,
|
|
2713
|
+
stage_status: 'blocked',
|
|
2714
|
+
pending_user_decision: TRANSITIONS.REVIEW_TO_BUILD,
|
|
2715
|
+
requested_transition: TRANSITIONS.REVIEW_TO_BUILD,
|
|
2716
|
+
review_verdict: 'request-changes',
|
|
2717
|
+
rollback_target: STAGES.BUILD,
|
|
2718
|
+
rollback_rationale: 'execution-record.md scope gate blocked review -> done because remaining workflow scope is declared.',
|
|
2719
|
+
plan_blockers: dedupeStrings([...(next.plan_blockers || []), ...scopeGate.blockers]),
|
|
2720
|
+
approval: {
|
|
2721
|
+
...next.approval,
|
|
2722
|
+
build: APPROVAL_STATES.REQUESTED,
|
|
2723
|
+
complete: APPROVAL_STATES.NOT_REQUESTED,
|
|
2724
|
+
},
|
|
2725
|
+
});
|
|
2726
|
+
await writeState(root, blocked);
|
|
2727
|
+
throw new Error(`review_done_scope_blocked:${scopeGate.blockers.join(',')}`);
|
|
2728
|
+
}
|
|
2729
|
+
let doneJournal = null;
|
|
2730
|
+
let doneJournalWarning = null;
|
|
2731
|
+
if (next.workspace_journal_status !== 'written' || !next.workspace_journal_path) {
|
|
2732
|
+
try {
|
|
2733
|
+
doneJournal = await writeReviewJournal({
|
|
2734
|
+
cwd,
|
|
2735
|
+
slug: state.slug,
|
|
2736
|
+
verdict: 'APPROVE',
|
|
2737
|
+
reviewMessageZh: `Review 结果:${state.slug} 已批准完成,工作流进入 done。`,
|
|
2738
|
+
evidenceManifest: [],
|
|
2739
|
+
findings: [],
|
|
2740
|
+
followUps: ['工作流已完成。'],
|
|
2741
|
+
});
|
|
2742
|
+
} catch (error) {
|
|
2743
|
+
doneJournalWarning = error instanceof Error ? error.message : String(error);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
next = withRecommendedAction({
|
|
2747
|
+
...next,
|
|
2748
|
+
current_stage: STAGES.DONE,
|
|
2749
|
+
stage_status: 'completed',
|
|
2750
|
+
pending_user_decision: TRANSITIONS.NONE,
|
|
2751
|
+
requested_transition: TRANSITIONS.NONE,
|
|
2752
|
+
last_confirmed_transition: TRANSITIONS.REVIEW_TO_DONE,
|
|
2753
|
+
completion_confirmed: true,
|
|
2754
|
+
workspace_journal_status: doneJournal ? 'written' : (next.workspace_journal_status || 'failed'),
|
|
2755
|
+
workspace_journal_path: doneJournal?.journalPath || next.workspace_journal_path || null,
|
|
2756
|
+
workspace_journal_error: doneJournalWarning || next.workspace_journal_error || null,
|
|
2757
|
+
approval: {
|
|
2758
|
+
...next.approval,
|
|
2759
|
+
[approvalKey]: APPROVAL_STATES.APPROVED,
|
|
2760
|
+
},
|
|
2761
|
+
});
|
|
2762
|
+
await writeState(root, next);
|
|
2763
|
+
return { root, state: next };
|
|
2764
|
+
}
|
|
2765
|
+
|
|
1133
2766
|
next = withRecommendedAction({
|
|
1134
2767
|
...next,
|
|
1135
2768
|
stage_status: 'awaiting-approval',
|
|
@@ -1144,6 +2777,79 @@ export async function approveStage(cwd, slug, { from, to }) {
|
|
|
1144
2777
|
return { root, state: next };
|
|
1145
2778
|
}
|
|
1146
2779
|
|
|
2780
|
+
export async function archiveStage(cwd, slug) {
|
|
2781
|
+
const { root, state, slug: normalized } = await loadWorkflowState(cwd, slug, { allowLegacy: false });
|
|
2782
|
+
if (state.current_stage !== STAGES.DONE || !state.completion_confirmed) {
|
|
2783
|
+
throw new Error('archive_requires_done_workflow');
|
|
2784
|
+
}
|
|
2785
|
+
const executionSummary = await readExecutionRecordSummary(root);
|
|
2786
|
+
const scopeGate = executionScopeGate(executionSummary.meta);
|
|
2787
|
+
if (!scopeGate.ok) {
|
|
2788
|
+
const blocked = withRecommendedAction({
|
|
2789
|
+
...state,
|
|
2790
|
+
archive_status: 'blocked',
|
|
2791
|
+
plan_blockers: dedupeStrings([...(state.plan_blockers || []), ...scopeGate.blockers]),
|
|
2792
|
+
});
|
|
2793
|
+
await writeState(root, blocked);
|
|
2794
|
+
throw new Error(`archive_scope_blocked:${scopeGate.blockers.join(',')}`);
|
|
2795
|
+
}
|
|
2796
|
+
const effectiveChangeArtifactPaths = await ensureArchiveSlicesArtifact(cwd, root, normalized, state);
|
|
2797
|
+
const effectiveState = {
|
|
2798
|
+
...state,
|
|
2799
|
+
change_artifact_paths: effectiveChangeArtifactPaths,
|
|
2800
|
+
slice_artifacts_status: effectiveChangeArtifactPaths?.slices && existsSync(effectiveChangeArtifactPaths.slices) ? 'complete' : state.slice_artifacts_status,
|
|
2801
|
+
};
|
|
2802
|
+
const changeStatus = await readChangeArtifactStatus(effectiveState.change_artifact_paths);
|
|
2803
|
+
if (changeStatus.blockers.length > 0) {
|
|
2804
|
+
const blocked = withRecommendedAction({
|
|
2805
|
+
...effectiveState,
|
|
2806
|
+
archive_status: 'blocked',
|
|
2807
|
+
spec_sync_status: changeStatus.specDeltaStatus,
|
|
2808
|
+
plan_blockers: [...(effectiveState.plan_blockers || []), ...changeStatus.blockers],
|
|
2809
|
+
});
|
|
2810
|
+
await writeState(root, blocked);
|
|
2811
|
+
throw new Error(`archive_blocked:${changeStatus.blockers.join(',')}`);
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
const changeId = normalizeSlug(effectiveState.change_id || changeIdForWorkflowSlug(normalized));
|
|
2815
|
+
const archivedSpecPaths = await mergeSpecDeltaIntoLongLivedSpecs(cwd, changeId, effectiveState.change_artifact_paths.specDelta);
|
|
2816
|
+
const adrCandidatePath = await writeAdrCandidate(cwd, changeId, effectiveState, archivedSpecPaths);
|
|
2817
|
+
const archiveRoot = resolveArchivedChangeRoot(cwd, changeId);
|
|
2818
|
+
await ensureDir(dirname(archiveRoot));
|
|
2819
|
+
if (effectiveState.change_artifact_paths.root === archiveRoot) {
|
|
2820
|
+
// Already archived; keep paths stable and use merge as an idempotent re-sync.
|
|
2821
|
+
} else if (existsSync(archiveRoot)) {
|
|
2822
|
+
await cp(effectiveState.change_artifact_paths.root, archiveRoot, { recursive: true, force: true });
|
|
2823
|
+
} else {
|
|
2824
|
+
await rename(effectiveState.change_artifact_paths.root, archiveRoot);
|
|
2825
|
+
}
|
|
2826
|
+
const archivedPaths = {
|
|
2827
|
+
...effectiveState.change_artifact_paths,
|
|
2828
|
+
root: archiveRoot,
|
|
2829
|
+
proposal: join(archiveRoot, 'proposal.md'),
|
|
2830
|
+
specDelta: join(archiveRoot, 'spec-delta.md'),
|
|
2831
|
+
design: join(archiveRoot, 'design.md'),
|
|
2832
|
+
tasks: join(archiveRoot, 'tasks.md'),
|
|
2833
|
+
slices: join(archiveRoot, 'slices.json'),
|
|
2834
|
+
graph: join(archiveRoot, 'artifact-graph.json'),
|
|
2835
|
+
};
|
|
2836
|
+
const next = withRecommendedAction({
|
|
2837
|
+
...effectiveState,
|
|
2838
|
+
archive_status: 'archived',
|
|
2839
|
+
spec_sync_status: 'synced',
|
|
2840
|
+
spec_delta_status: 'complete',
|
|
2841
|
+
slice_artifacts_status: 'complete',
|
|
2842
|
+
change_id: changeId,
|
|
2843
|
+
change_artifacts_status: 'archived',
|
|
2844
|
+
archived_change_path: archiveRoot,
|
|
2845
|
+
archived_spec_paths: archivedSpecPaths,
|
|
2846
|
+
adr_candidate_path: adrCandidatePath,
|
|
2847
|
+
change_artifact_paths: archivedPaths,
|
|
2848
|
+
});
|
|
2849
|
+
await writeState(root, next);
|
|
2850
|
+
return { root, state: next };
|
|
2851
|
+
}
|
|
2852
|
+
|
|
1147
2853
|
export async function planStage(cwd, slug, options = {}) {
|
|
1148
2854
|
let normalized = slug ? normalizeSlug(slug) : null;
|
|
1149
2855
|
if (options.directSpecPath) {
|
|
@@ -1154,9 +2860,24 @@ export async function planStage(cwd, slug, options = {}) {
|
|
|
1154
2860
|
const loaded = await loadWorkflowState(cwd, normalized, { allowLegacy: false });
|
|
1155
2861
|
const { root } = loaded;
|
|
1156
2862
|
let { state } = loaded;
|
|
2863
|
+
const consumesReviewPlan = state.current_stage === STAGES.REVIEW
|
|
2864
|
+
&& state.requested_transition === TRANSITIONS.REVIEW_TO_PLAN
|
|
2865
|
+
&& state.approval.rollback === APPROVAL_STATES.APPROVED
|
|
2866
|
+
&& state.review_verdict === 'request-changes';
|
|
2867
|
+
const resumesConsumedReviewPlan = state.current_stage === STAGES.PLAN
|
|
2868
|
+
&& state.last_confirmed_transition === TRANSITIONS.REVIEW_TO_PLAN
|
|
2869
|
+
&& state.approval.rollback === APPROVAL_STATES.APPROVED;
|
|
2870
|
+
const resumesClarifyPlan = state.current_stage === STAGES.PLAN
|
|
2871
|
+
&& state.stage_status === 'blocked'
|
|
2872
|
+
&& state.last_confirmed_transition === TRANSITIONS.CLARIFY_TO_PLAN
|
|
2873
|
+
&& state.approval.plan === APPROVAL_STATES.APPROVED;
|
|
1157
2874
|
if (!options.directSpecPath) {
|
|
1158
|
-
|
|
1159
|
-
|
|
2875
|
+
if (consumesReviewPlan || resumesConsumedReviewPlan || resumesClarifyPlan) {
|
|
2876
|
+
// A no-go review or a blocked planning run may route back to plan; the printed Next command is $plan.
|
|
2877
|
+
} else {
|
|
2878
|
+
ensureApprovedTransition(state, TRANSITIONS.CLARIFY_TO_PLAN, 'plan');
|
|
2879
|
+
}
|
|
2880
|
+
if (!consumesReviewPlan && !resumesConsumedReviewPlan && !resumesClarifyPlan && state.spec_artifact_path) {
|
|
1160
2881
|
await copyArtifact(root, state.spec_artifact_path, 'spec.md');
|
|
1161
2882
|
}
|
|
1162
2883
|
}
|
|
@@ -1169,6 +2890,7 @@ export async function planStage(cwd, slug, options = {}) {
|
|
|
1169
2890
|
let architectReview = null;
|
|
1170
2891
|
let criticReview = null;
|
|
1171
2892
|
const reviewArtifactPaths = [];
|
|
2893
|
+
const reviewHistory = initialPlanReviewHistory(state);
|
|
1172
2894
|
|
|
1173
2895
|
while (iteration <= maxIterations) {
|
|
1174
2896
|
const plannerDraft = await adapter.planner({
|
|
@@ -1177,11 +2899,15 @@ export async function planStage(cwd, slug, options = {}) {
|
|
|
1177
2899
|
slug: normalized,
|
|
1178
2900
|
sourceText,
|
|
1179
2901
|
iteration,
|
|
2902
|
+
reviewHistory: [...reviewHistory],
|
|
1180
2903
|
deliberateMode: Boolean(options.deliberate),
|
|
1181
2904
|
interactiveMode: Boolean(options.interactive),
|
|
1182
2905
|
});
|
|
1183
|
-
|
|
2906
|
+
await writePlanArtifacts(root, cwd, normalized, plannerDraft);
|
|
1184
2907
|
const artifactPaths = await writeCanonicalPlanArtifacts(cwd, root, normalized);
|
|
2908
|
+
const changeId = state.change_id || changeIdForWorkflowSlug(normalized);
|
|
2909
|
+
const changeArtifactPaths = await writeChangeArtifacts(cwd, root, normalized, sourceText, plannerDraft, changeId);
|
|
2910
|
+
const changeArtifactStatus = await readChangeArtifactStatus(changeArtifactPaths);
|
|
1185
2911
|
|
|
1186
2912
|
architectReview = await adapter.architect({
|
|
1187
2913
|
cwd,
|
|
@@ -1204,6 +2930,7 @@ export async function planStage(cwd, slug, options = {}) {
|
|
|
1204
2930
|
});
|
|
1205
2931
|
const reviewPaths = await writePlanReviewArtifacts(root, iteration, plannerDraft, architectReview, criticReview);
|
|
1206
2932
|
reviewArtifactPaths.push(reviewPaths);
|
|
2933
|
+
reviewHistory.push(planReviewSummary(iteration, architectReview, criticReview));
|
|
1207
2934
|
|
|
1208
2935
|
state = {
|
|
1209
2936
|
...state,
|
|
@@ -1221,18 +2948,24 @@ export async function planStage(cwd, slug, options = {}) {
|
|
|
1221
2948
|
plan_verification_steps_resolved: criticReview.verificationStepsResolved,
|
|
1222
2949
|
plan_execution_inputs_resolved: criticReview.executionInputsResolved,
|
|
1223
2950
|
plan_package_status: 'complete',
|
|
1224
|
-
plan_docs_artifact_paths:
|
|
2951
|
+
plan_docs_artifact_paths: null,
|
|
1225
2952
|
plan_review_artifact_paths: reviewArtifactPaths,
|
|
2953
|
+
plan_review_history: reviewHistory,
|
|
1226
2954
|
plan_artifact_path: artifactPaths.planPath,
|
|
1227
2955
|
test_spec_artifact_path: artifactPaths.testSpecPath,
|
|
2956
|
+
change_id: normalizeSlug(changeId),
|
|
2957
|
+
change_artifacts_status: changeArtifactStatus.status,
|
|
2958
|
+
change_artifact_paths: changeArtifactPaths,
|
|
2959
|
+
spec_delta_status: changeArtifactStatus.specDeltaStatus,
|
|
2960
|
+
slice_artifacts_status: changeArtifactStatus.sliceArtifactsStatus,
|
|
1228
2961
|
plan_source_spec_path: sourceSpecPath,
|
|
1229
|
-
last_confirmed_transition: TRANSITIONS.CLARIFY_TO_PLAN,
|
|
2962
|
+
last_confirmed_transition: consumesReviewPlan || resumesConsumedReviewPlan ? TRANSITIONS.REVIEW_TO_PLAN : TRANSITIONS.CLARIFY_TO_PLAN,
|
|
1230
2963
|
approval: {
|
|
1231
2964
|
...state.approval,
|
|
1232
2965
|
plan: APPROVAL_STATES.APPROVED,
|
|
1233
2966
|
build: APPROVAL_STATES.NOT_REQUESTED,
|
|
1234
2967
|
review: APPROVAL_STATES.NOT_REQUESTED,
|
|
1235
|
-
rollback: APPROVAL_STATES.NOT_REQUESTED,
|
|
2968
|
+
rollback: consumesReviewPlan || resumesConsumedReviewPlan ? APPROVAL_STATES.APPROVED : APPROVAL_STATES.NOT_REQUESTED,
|
|
1236
2969
|
complete: APPROVAL_STATES.NOT_REQUESTED,
|
|
1237
2970
|
},
|
|
1238
2971
|
};
|
|
@@ -1244,23 +2977,54 @@ export async function planStage(cwd, slug, options = {}) {
|
|
|
1244
2977
|
}
|
|
1245
2978
|
|
|
1246
2979
|
const completion = await readPlanCompletion(cwd, root, normalized, state);
|
|
2980
|
+
const buildManifest = completion.blockers.length > 0
|
|
2981
|
+
? null
|
|
2982
|
+
: await generateBuildContextManifest({ cwd, root, state, slug: normalized });
|
|
1247
2983
|
const next = withRecommendedAction({
|
|
1248
2984
|
...state,
|
|
1249
2985
|
current_stage: STAGES.PLAN,
|
|
1250
2986
|
stage_status: completion.blockers.length > 0 ? 'blocked' : 'awaiting-approval',
|
|
1251
|
-
pending_user_decision: TRANSITIONS.NONE,
|
|
2987
|
+
pending_user_decision: completion.blockers.length > 0 ? TRANSITIONS.NONE : TRANSITIONS.PLAN_TO_BUILD,
|
|
1252
2988
|
requested_transition: TRANSITIONS.NONE,
|
|
1253
2989
|
plan_docs_status: completion.docsStatus,
|
|
1254
|
-
plan_docs_artifact_paths:
|
|
2990
|
+
plan_docs_artifact_paths: null,
|
|
2991
|
+
change_artifacts_status: completion.changeArtifactsStatus,
|
|
2992
|
+
spec_delta_status: completion.specDeltaStatus,
|
|
2993
|
+
slice_artifacts_status: completion.sliceArtifactsStatus,
|
|
1255
2994
|
plan_blockers: completion.blockers,
|
|
2995
|
+
context_manifest_status: buildManifest ? 'hit' : 'fallback',
|
|
2996
|
+
build_context_manifest_path: buildManifest?.path || buildContextManifestPath(root),
|
|
1256
2997
|
});
|
|
1257
2998
|
await writeState(root, next);
|
|
1258
2999
|
return { root, state: next, architectReview, criticReview };
|
|
1259
3000
|
}
|
|
1260
3001
|
|
|
1261
3002
|
export async function buildStage(cwd, slug, options = {}) {
|
|
1262
|
-
const
|
|
1263
|
-
|
|
3003
|
+
const explicitReviewReworkPath = options.fromReviewPath || (isReviewReworkArtifactInput(slug) ? slug : null);
|
|
3004
|
+
const buildSlug = explicitReviewReworkPath ? slugFromReviewReworkInput(explicitReviewReworkPath) : slugFromBuildInput(slug);
|
|
3005
|
+
const { root, state, slug: normalized } = await loadWorkflowState(cwd, buildSlug, { allowLegacy: false });
|
|
3006
|
+
const reviewReworkArtifactDisplayPath = explicitReviewReworkPath ? displayPath(cwd, explicitReviewReworkPath) : null;
|
|
3007
|
+
const reviewReworkArtifactResolvedPath = explicitReviewReworkPath ? resolve(cwd, explicitReviewReworkPath) : null;
|
|
3008
|
+
const effectiveReviewReworkArtifactPath = reviewReworkArtifactDisplayPath || state.review_rework_artifact_path || null;
|
|
3009
|
+
if (explicitReviewReworkPath && !existsSync(reviewReworkArtifactResolvedPath)) {
|
|
3010
|
+
throw new Error('build_from_review_artifact_missing');
|
|
3011
|
+
}
|
|
3012
|
+
const consumesReviewBuild = state.current_stage === STAGES.REVIEW
|
|
3013
|
+
&& state.review_verdict === 'request-changes'
|
|
3014
|
+
&& state.rollback_target === STAGES.BUILD
|
|
3015
|
+
&& (
|
|
3016
|
+
state.pending_user_decision === TRANSITIONS.REVIEW_TO_BUILD
|
|
3017
|
+
|| state.requested_transition === TRANSITIONS.REVIEW_TO_BUILD
|
|
3018
|
+
|| state.approval.build === APPROVAL_STATES.REQUESTED
|
|
3019
|
+
|| state.approval.build === APPROVAL_STATES.APPROVED
|
|
3020
|
+
)
|
|
3021
|
+
&& Boolean(explicitReviewReworkPath);
|
|
3022
|
+
const resumesConsumedReviewBuild = state.current_stage === STAGES.BUILD
|
|
3023
|
+
&& state.last_confirmed_transition === TRANSITIONS.REVIEW_TO_BUILD
|
|
3024
|
+
&& state.approval.build === APPROVAL_STATES.APPROVED;
|
|
3025
|
+
if (!consumesReviewBuild && !resumesConsumedReviewBuild) {
|
|
3026
|
+
ensureApprovedTransition(state, TRANSITIONS.PLAN_TO_BUILD, 'build');
|
|
3027
|
+
}
|
|
1264
3028
|
if (!PLAN_ARTIFACTS.every((name) => existsSync(artifactPath(root, name)))) {
|
|
1265
3029
|
throw new Error('build_requires_workflow_plan_artifacts');
|
|
1266
3030
|
}
|
|
@@ -1273,11 +3037,71 @@ export async function buildStage(cwd, slug, options = {}) {
|
|
|
1273
3037
|
const noDeslop = Boolean(options.noDeslop);
|
|
1274
3038
|
const progressArtifacts = [];
|
|
1275
3039
|
const supportArtifacts = [];
|
|
3040
|
+
const ownerId = buildOwnerId(normalized);
|
|
1276
3041
|
let iteration = 1;
|
|
1277
3042
|
let current = null;
|
|
3043
|
+
let accumulatedChangedFiles = [];
|
|
1278
3044
|
let blockers = ['build_not_started'];
|
|
3045
|
+
let delegationLedger = null;
|
|
3046
|
+
let completionAudit = null;
|
|
3047
|
+
let delegationLedgerPath = resolveBuildSupportPaths(root, 1).delegationLedger;
|
|
3048
|
+
let completionAuditPath = resolveBuildSupportPaths(root, 1).completionAudit;
|
|
3049
|
+
if (consumesReviewBuild || resumesConsumedReviewBuild) {
|
|
3050
|
+
await generateBuildContextManifest({
|
|
3051
|
+
cwd,
|
|
3052
|
+
root,
|
|
3053
|
+
state: {
|
|
3054
|
+
...state,
|
|
3055
|
+
current_stage: STAGES.BUILD,
|
|
3056
|
+
last_confirmed_transition: TRANSITIONS.REVIEW_TO_BUILD,
|
|
3057
|
+
review_rework_artifact_path: reviewReworkArtifactResolvedPath || state.review_rework_artifact_path || artifactPath(root, 'review-report.md'),
|
|
3058
|
+
},
|
|
3059
|
+
slug: normalized,
|
|
3060
|
+
});
|
|
3061
|
+
}
|
|
3062
|
+
const buildManifest = await readContextManifest(buildContextManifestPath(root), { cwd });
|
|
3063
|
+
ensureValidContextManifest(buildManifest, STAGES.BUILD);
|
|
3064
|
+
const contextManifestStatus = buildManifest.status;
|
|
3065
|
+
|
|
3066
|
+
await writeBuildActiveState(cwd, {
|
|
3067
|
+
active: true,
|
|
3068
|
+
slug: normalized,
|
|
3069
|
+
phase: 'starting',
|
|
3070
|
+
iteration: 0,
|
|
3071
|
+
max_iterations: maxIterations,
|
|
3072
|
+
review_handoff_ready: false,
|
|
3073
|
+
blockers,
|
|
3074
|
+
build_owner_id: ownerId,
|
|
3075
|
+
build_owner_session_id: buildOwnerSessionId(normalized, null),
|
|
3076
|
+
delegation_ledger_path: displayPath(cwd, delegationLedgerPath),
|
|
3077
|
+
active_delegation_count: 0,
|
|
3078
|
+
completion_audit_path: displayPath(cwd, completionAuditPath),
|
|
3079
|
+
completion_audit_status: 'pending',
|
|
3080
|
+
next_action: 'Run build execution lanes and write execution-record.md.',
|
|
3081
|
+
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.',
|
|
3082
|
+
workflow_root: root,
|
|
3083
|
+
execution_record_path: artifactPath(root, 'execution-record.md'),
|
|
3084
|
+
started_at: nowIso(),
|
|
3085
|
+
});
|
|
1279
3086
|
|
|
1280
3087
|
while (iteration <= maxIterations) {
|
|
3088
|
+
await writeBuildActiveState(cwd, {
|
|
3089
|
+
active: true,
|
|
3090
|
+
slug: normalized,
|
|
3091
|
+
phase: 'executing',
|
|
3092
|
+
iteration,
|
|
3093
|
+
max_iterations: maxIterations,
|
|
3094
|
+
review_handoff_ready: false,
|
|
3095
|
+
blockers,
|
|
3096
|
+
build_owner_id: ownerId,
|
|
3097
|
+
build_owner_session_id: buildOwnerSessionId(normalized, null),
|
|
3098
|
+
delegation_ledger_path: displayPath(cwd, delegationLedgerPath),
|
|
3099
|
+
active_delegation_count: delegationLedger?.active_blocking_count || 0,
|
|
3100
|
+
completion_audit_path: displayPath(cwd, completionAuditPath),
|
|
3101
|
+
completion_audit_status: completionAudit?.status || 'pending',
|
|
3102
|
+
next_action: 'Continue $build execution and gather fresh implementation evidence.',
|
|
3103
|
+
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.',
|
|
3104
|
+
});
|
|
1281
3105
|
current = await adapter.executeLanes({
|
|
1282
3106
|
cwd,
|
|
1283
3107
|
root,
|
|
@@ -1286,11 +3110,78 @@ export async function buildStage(cwd, slug, options = {}) {
|
|
|
1286
3110
|
noDeslop,
|
|
1287
3111
|
planArtifactPath: state.plan_artifact_path,
|
|
1288
3112
|
testSpecArtifactPath: state.test_spec_artifact_path,
|
|
3113
|
+
reviewReworkArtifactPath: reviewReworkArtifactDisplayPath || state.review_rework_artifact_path || null,
|
|
3114
|
+
contextManifestPath: buildContextManifestPath(root),
|
|
3115
|
+
contextManifestRows: buildManifest.rows,
|
|
3116
|
+
contextManifestStatus,
|
|
3117
|
+
});
|
|
3118
|
+
accumulatedChangedFiles = dedupeStrings([
|
|
3119
|
+
...accumulatedChangedFiles,
|
|
3120
|
+
...(Array.isArray(current.changedFiles) ? current.changedFiles : []),
|
|
3121
|
+
]);
|
|
3122
|
+
current = {
|
|
3123
|
+
...current,
|
|
3124
|
+
changedFiles: accumulatedChangedFiles,
|
|
3125
|
+
};
|
|
3126
|
+
const supportPaths = resolveBuildSupportPaths(root, current.iteration);
|
|
3127
|
+
delegationLedgerPath = supportPaths.delegationLedger;
|
|
3128
|
+
completionAuditPath = supportPaths.completionAudit;
|
|
3129
|
+
delegationLedger = buildDelegationLedger({
|
|
3130
|
+
slug: normalized,
|
|
3131
|
+
ownerId,
|
|
3132
|
+
ownerSessionId: buildOwnerSessionId(normalized, current?.runId || null),
|
|
3133
|
+
iterationData: current,
|
|
3134
|
+
previousLedger: delegationLedger,
|
|
3135
|
+
});
|
|
3136
|
+
const baseBlockers = buildIterationBlockers(current, { noDeslop });
|
|
3137
|
+
completionAudit = await buildCompletionAudit({
|
|
3138
|
+
cwd,
|
|
3139
|
+
root,
|
|
3140
|
+
slug: normalized,
|
|
3141
|
+
state,
|
|
3142
|
+
reviewReworkArtifactPath: effectiveReviewReworkArtifactPath,
|
|
3143
|
+
iterationData: current,
|
|
3144
|
+
ledger: delegationLedger,
|
|
3145
|
+
baseBlockers,
|
|
1289
3146
|
});
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
3147
|
+
const auditBlocksHandoff = !completionAudit.passed
|
|
3148
|
+
&& baseBlockers.length === 0;
|
|
3149
|
+
blockers = dedupeStrings([
|
|
3150
|
+
...baseBlockers,
|
|
3151
|
+
...buildDelegationBlockers(delegationLedger),
|
|
3152
|
+
...(auditBlocksHandoff ? ['completion_audit_blocked'] : []),
|
|
3153
|
+
]);
|
|
3154
|
+
await writeBuildActiveState(cwd, {
|
|
3155
|
+
active: true,
|
|
3156
|
+
slug: normalized,
|
|
3157
|
+
phase: blockers.length === 0 ? 'verifying' : 'fixing',
|
|
3158
|
+
iteration,
|
|
3159
|
+
max_iterations: maxIterations,
|
|
3160
|
+
review_handoff_ready: false,
|
|
3161
|
+
blockers,
|
|
3162
|
+
build_owner_id: ownerId,
|
|
3163
|
+
build_owner_session_id: buildOwnerSessionId(normalized, current?.runId || null),
|
|
3164
|
+
delegation_ledger_path: displayPath(cwd, delegationLedgerPath),
|
|
3165
|
+
active_delegation_count: delegationLedger.active_blocking_count,
|
|
3166
|
+
completion_audit_path: displayPath(cwd, completionAuditPath),
|
|
3167
|
+
completion_audit_status: completionAudit.status,
|
|
3168
|
+
next_action: blockers.length === 0
|
|
3169
|
+
? 'Verify execution evidence and prepare build -> review handoff.'
|
|
3170
|
+
: 'Continue $build to resolve blockers before review handoff.',
|
|
3171
|
+
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.',
|
|
3172
|
+
});
|
|
3173
|
+
const writtenSupportPaths = await writeBuildSupportArtifacts(root, current, noDeslop, {
|
|
3174
|
+
delegationLedger,
|
|
3175
|
+
completionAudit,
|
|
3176
|
+
});
|
|
3177
|
+
progressArtifacts.push(writtenSupportPaths.laneSummary);
|
|
3178
|
+
supportArtifacts.push(
|
|
3179
|
+
writtenSupportPaths.architect,
|
|
3180
|
+
writtenSupportPaths.deslop,
|
|
3181
|
+
writtenSupportPaths.regression,
|
|
3182
|
+
writtenSupportPaths.delegationLedger,
|
|
3183
|
+
writtenSupportPaths.completionAudit,
|
|
3184
|
+
);
|
|
1294
3185
|
await writeText(
|
|
1295
3186
|
artifactPath(root, 'execution-record.md'),
|
|
1296
3187
|
buildExecutionRecordContent({
|
|
@@ -1302,10 +3193,16 @@ export async function buildStage(cwd, slug, options = {}) {
|
|
|
1302
3193
|
if (blockers.length === 0) {
|
|
1303
3194
|
break;
|
|
1304
3195
|
}
|
|
3196
|
+
if (buildHasInfrastructureFailure(current)) {
|
|
3197
|
+
break;
|
|
3198
|
+
}
|
|
1305
3199
|
iteration += 1;
|
|
1306
3200
|
}
|
|
1307
3201
|
|
|
1308
3202
|
const finalBlocked = blockers.length > 0;
|
|
3203
|
+
const reviewManifest = finalBlocked
|
|
3204
|
+
? null
|
|
3205
|
+
: await generateReviewContextManifest({ cwd, root, state, slug: normalized });
|
|
1309
3206
|
const refreshed = await refreshExecutionStatus(root, state);
|
|
1310
3207
|
const next = withRecommendedAction({
|
|
1311
3208
|
...refreshed.state,
|
|
@@ -1313,6 +3210,7 @@ export async function buildStage(cwd, slug, options = {}) {
|
|
|
1313
3210
|
stage_status: finalBlocked ? 'blocked' : 'awaiting-approval',
|
|
1314
3211
|
execution_record_status: finalBlocked ? 'partial' : refreshed.state.execution_record_status,
|
|
1315
3212
|
review_status: finalBlocked ? 'pending-input' : 'ready-for-review',
|
|
3213
|
+
review_handoff_ready: !finalBlocked,
|
|
1316
3214
|
build_run_id: current?.runId || null,
|
|
1317
3215
|
build_current_iteration: current?.iteration || 0,
|
|
1318
3216
|
build_max_iterations: maxIterations,
|
|
@@ -1326,10 +3224,28 @@ export async function buildStage(cwd, slug, options = {}) {
|
|
|
1326
3224
|
build_progress_artifact_paths: progressArtifacts,
|
|
1327
3225
|
build_support_evidence_paths: supportArtifacts,
|
|
1328
3226
|
build_no_deslop: noDeslop,
|
|
3227
|
+
build_owner_id: ownerId,
|
|
3228
|
+
build_owner_session_id: buildOwnerSessionId(normalized, current?.runId || null),
|
|
3229
|
+
build_owner_status: finalBlocked ? 'blocked' : 'review-ready',
|
|
3230
|
+
build_delegation_status: delegationLedger?.status || 'drained',
|
|
3231
|
+
build_delegation_ledger_path: delegationLedgerPath,
|
|
3232
|
+
build_active_delegation_count: delegationLedger?.active_blocking_count || 0,
|
|
3233
|
+
build_completion_audit_status: completionAudit?.status || (finalBlocked ? 'blocked' : 'passed'),
|
|
3234
|
+
build_completion_audit_path: completionAuditPath,
|
|
3235
|
+
review_rework_artifact_path: reviewReworkArtifactDisplayPath || state.review_rework_artifact_path || null,
|
|
3236
|
+
context_manifest_status: contextManifestStatus,
|
|
3237
|
+
build_context_manifest_path: buildContextManifestPath(root),
|
|
3238
|
+
review_context_manifest_path: reviewManifest?.path || reviewContextManifestPath(root),
|
|
1329
3239
|
active_run_id: current?.runId || null,
|
|
1330
3240
|
pending_user_decision: finalBlocked ? TRANSITIONS.NONE : TRANSITIONS.BUILD_TO_REVIEW,
|
|
1331
3241
|
requested_transition: TRANSITIONS.NONE,
|
|
1332
|
-
last_confirmed_transition: TRANSITIONS.PLAN_TO_BUILD,
|
|
3242
|
+
last_confirmed_transition: consumesReviewBuild || resumesConsumedReviewBuild ? TRANSITIONS.REVIEW_TO_BUILD : TRANSITIONS.PLAN_TO_BUILD,
|
|
3243
|
+
review_verdict: 'none',
|
|
3244
|
+
rollback_target: null,
|
|
3245
|
+
rollback_rationale: null,
|
|
3246
|
+
workspace_journal_path: null,
|
|
3247
|
+
workspace_journal_status: 'skipped',
|
|
3248
|
+
workspace_journal_error: null,
|
|
1333
3249
|
approval: {
|
|
1334
3250
|
...state.approval,
|
|
1335
3251
|
build: APPROVAL_STATES.APPROVED,
|
|
@@ -1339,39 +3255,114 @@ export async function buildStage(cwd, slug, options = {}) {
|
|
|
1339
3255
|
},
|
|
1340
3256
|
});
|
|
1341
3257
|
await writeState(root, next);
|
|
3258
|
+
await writeBuildActiveState(cwd, {
|
|
3259
|
+
active: false,
|
|
3260
|
+
slug: normalized,
|
|
3261
|
+
phase: finalBlocked ? 'blocked' : 'review-ready',
|
|
3262
|
+
iteration: current?.iteration || 0,
|
|
3263
|
+
max_iterations: maxIterations,
|
|
3264
|
+
review_handoff_ready: !finalBlocked,
|
|
3265
|
+
blockers,
|
|
3266
|
+
build_owner_id: ownerId,
|
|
3267
|
+
build_owner_session_id: buildOwnerSessionId(normalized, current?.runId || null),
|
|
3268
|
+
delegation_ledger_path: displayPath(cwd, delegationLedgerPath),
|
|
3269
|
+
active_delegation_count: delegationLedger?.active_blocking_count || 0,
|
|
3270
|
+
completion_audit_path: displayPath(cwd, completionAuditPath),
|
|
3271
|
+
completion_audit_status: completionAudit?.status || (finalBlocked ? 'blocked' : 'passed'),
|
|
3272
|
+
next_action: finalBlocked ? 'Run $build again after resolving recorded blockers.' : 'Approve build -> review and run $review.',
|
|
3273
|
+
completion_signal: finalBlocked ? 'Build is stopped because real blockers remain recorded.' : 'execution-record.md is complete and build -> review handoff is ready.',
|
|
3274
|
+
execution_record_status: next.execution_record_status,
|
|
3275
|
+
execution_record_path: artifactPath(root, 'execution-record.md'),
|
|
3276
|
+
completed_at: nowIso(),
|
|
3277
|
+
});
|
|
1342
3278
|
return { root, state: next };
|
|
1343
3279
|
}
|
|
1344
3280
|
|
|
1345
|
-
function reviewFindings({ executionMeta, executionStatus, reviewer }) {
|
|
1346
|
-
const inputManifest = ['spec.md', ...PLAN_ARTIFACTS, 'execution-record.md'];
|
|
3281
|
+
function reviewFindings({ executionMeta, executionStatus, reviewer, codeReview, architectureReview }) {
|
|
3282
|
+
const inputManifest = ['spec.md', ...PLAN_ARTIFACTS, 'execution-record.md', 'review-support/code-review.json', 'review-support/architecture-smell.json'];
|
|
1347
3283
|
const evidenceManifest = Array.isArray(executionMeta.evidence_manifest) ? [...executionMeta.evidence_manifest] : [];
|
|
3284
|
+
const scopeGate = executionScopeGate(executionMeta);
|
|
1348
3285
|
const findings = [];
|
|
1349
3286
|
let verdict = 'APPROVE';
|
|
1350
3287
|
let rollbackTarget = 'none';
|
|
1351
3288
|
let rollbackRationale = null;
|
|
1352
3289
|
|
|
1353
3290
|
if (executionStatus !== 'complete') {
|
|
1354
|
-
findings.push('execution-record.md
|
|
3291
|
+
findings.push('execution-record.md 缺少必要的执行或验证证据。');
|
|
1355
3292
|
verdict = 'REQUEST CHANGES';
|
|
1356
|
-
rollbackTarget =
|
|
1357
|
-
rollbackRationale = '
|
|
3293
|
+
rollbackTarget = STAGES.BUILD;
|
|
3294
|
+
rollbackRationale = '执行证据不完整,工作流需要回到 build 阶段补齐执行和验证证据后重新 review。';
|
|
1358
3295
|
}
|
|
1359
3296
|
if (!Array.isArray(executionMeta.evidence_manifest) || executionMeta.evidence_manifest.length === 0) {
|
|
1360
|
-
findings.push('execution-record.md
|
|
3297
|
+
findings.push('execution-record.md 缺少必需的 evidence_manifest 结构。');
|
|
1361
3298
|
verdict = 'REQUEST CHANGES';
|
|
1362
|
-
rollbackTarget =
|
|
1363
|
-
rollbackRationale = '
|
|
3299
|
+
rollbackTarget = STAGES.BUILD;
|
|
3300
|
+
rollbackRationale = '执行证据结构不完整,review 不能接受本次运行,需要回到 build 阶段补齐 evidence_manifest。';
|
|
1364
3301
|
}
|
|
1365
3302
|
if (executionMeta.actor_id === reviewer) {
|
|
1366
|
-
findings.push('Reviewer
|
|
3303
|
+
findings.push('Reviewer 来源与执行者一致,不满足独立审查要求。');
|
|
1367
3304
|
verdict = 'REQUEST CHANGES';
|
|
1368
3305
|
rollbackTarget = 'plan';
|
|
1369
|
-
rollbackRationale = '
|
|
3306
|
+
rollbackRationale = 'review 独立性校验失败,因为 reviewer 与执行者来源一致。';
|
|
3307
|
+
}
|
|
3308
|
+
if (!scopeGate.ok) {
|
|
3309
|
+
findings.push(`execution-record.md 声明只完成了部分 scope,不能批准完整工作流完成:${scopeGate.blockers.join(', ')}`);
|
|
3310
|
+
if (scopeGate.plannedScope) {
|
|
3311
|
+
findings.push(`planned_scope=${scopeGate.plannedScope}`);
|
|
3312
|
+
}
|
|
3313
|
+
if (scopeGate.implementedScope) {
|
|
3314
|
+
findings.push(`implemented_scope=${scopeGate.implementedScope}`);
|
|
3315
|
+
}
|
|
3316
|
+
if (scopeGate.remainingScope.length > 0) {
|
|
3317
|
+
findings.push(`remaining_scope=${scopeGate.remainingScope.join(', ')}`);
|
|
3318
|
+
}
|
|
3319
|
+
verdict = 'REQUEST CHANGES';
|
|
3320
|
+
if (rollbackTarget === 'none') {
|
|
3321
|
+
rollbackTarget = STAGES.BUILD;
|
|
3322
|
+
rollbackRationale = '执行记录显示当前 build 只完成了部分 scope,需要回到 build 继续执行剩余工作,或回到 plan 重新拆分独立 slice。';
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
if (codeReview?.status === 'skipped') {
|
|
3326
|
+
findings.push(`代码审查已跳过:${codeReview.summary}`);
|
|
3327
|
+
}
|
|
3328
|
+
if (codeReview?.verdict === 'request-changes') {
|
|
3329
|
+
findings.push(`代码审查发现阻断问题:${codeReview.summary}`);
|
|
3330
|
+
for (const finding of codeReview.findings || []) {
|
|
3331
|
+
findings.push(codeReviewFindingText(finding));
|
|
3332
|
+
}
|
|
3333
|
+
verdict = 'REQUEST CHANGES';
|
|
3334
|
+
if (rollbackTarget === 'none' || rollbackTarget === STAGES.BUILD) {
|
|
3335
|
+
rollbackTarget = codeReview.rollbackTarget || STAGES.BUILD;
|
|
3336
|
+
}
|
|
3337
|
+
rollbackRationale = rollbackTarget === STAGES.BUILD
|
|
3338
|
+
? '代码审查发现实现问题,需要回到 build 阶段修复后重新 review。'
|
|
3339
|
+
: rollbackTarget === STAGES.CLARIFY
|
|
3340
|
+
? '代码审查暴露需求歧义,需要回到 clarify 阶段重新澄清。'
|
|
3341
|
+
: '代码审查发现计划或架构问题,需要回到 plan 阶段修订后重新执行。';
|
|
3342
|
+
}
|
|
3343
|
+
if (architectureReview?.verdict === 'warn') {
|
|
3344
|
+
findings.push(`架构 smell 扫描提示风险:${architectureReview.summary}`);
|
|
3345
|
+
for (const finding of architectureReview.findings || []) {
|
|
3346
|
+
findings.push(architectureReviewFindingText(finding));
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
if (architectureReview?.verdict === 'block') {
|
|
3350
|
+
findings.push(`架构 smell 扫描发现阻断问题:${architectureReview.summary}`);
|
|
3351
|
+
for (const finding of architectureReview.findings || []) {
|
|
3352
|
+
findings.push(architectureReviewFindingText(finding));
|
|
3353
|
+
}
|
|
3354
|
+
verdict = 'REQUEST CHANGES';
|
|
3355
|
+
rollbackTarget = architectureReview.rollbackTarget || STAGES.PLAN;
|
|
3356
|
+
rollbackRationale = rollbackTarget === STAGES.BUILD
|
|
3357
|
+
? '架构 smell 扫描发现实现边界问题,需要回到 build 阶段修复后重新 review。'
|
|
3358
|
+
: rollbackTarget === STAGES.CLARIFY
|
|
3359
|
+
? '架构 smell 扫描暴露需求或领域语言歧义,需要回到 clarify 阶段重新澄清。'
|
|
3360
|
+
: '架构 smell 扫描发现计划或模块 seam 问题,需要回到 plan 阶段修订。';
|
|
1370
3361
|
}
|
|
1371
3362
|
|
|
1372
3363
|
return {
|
|
1373
3364
|
verdict,
|
|
1374
|
-
findings: findings.length > 0 ? findings : ['
|
|
3365
|
+
findings: findings.length > 0 ? findings : ['结构化证据与来源独立性检查均已通过。'],
|
|
1375
3366
|
inputManifest,
|
|
1376
3367
|
evidenceManifest,
|
|
1377
3368
|
rollbackTarget,
|
|
@@ -1379,8 +3370,15 @@ function reviewFindings({ executionMeta, executionStatus, reviewer }) {
|
|
|
1379
3370
|
};
|
|
1380
3371
|
}
|
|
1381
3372
|
|
|
1382
|
-
export async function reviewStage(cwd, slug, { reviewer = 'independent-reviewer' } = {}) {
|
|
1383
|
-
const
|
|
3373
|
+
export async function reviewStage(cwd, slug, { reviewer = 'independent-reviewer', adapter } = {}) {
|
|
3374
|
+
const reviewSlug = String(slug || '').endsWith('execution-record.md')
|
|
3375
|
+
? basename(dirname(resolve(cwd, slug)))
|
|
3376
|
+
: slug;
|
|
3377
|
+
const { root, state, slug: normalized } = await loadWorkflowState(cwd, reviewSlug, { allowLegacy: false });
|
|
3378
|
+
const rerunsAwaitingCompletionReview = state.current_stage === STAGES.REVIEW
|
|
3379
|
+
&& state.review_verdict === 'approve'
|
|
3380
|
+
&& state.pending_user_decision === TRANSITIONS.REVIEW_TO_DONE
|
|
3381
|
+
&& state.requested_transition === TRANSITIONS.NONE;
|
|
1384
3382
|
|
|
1385
3383
|
if (state.current_stage === STAGES.REVIEW && state.approval.complete === APPROVAL_STATES.APPROVED && state.review_verdict === 'approve') {
|
|
1386
3384
|
const next = withRecommendedAction({
|
|
@@ -1393,36 +3391,204 @@ export async function reviewStage(cwd, slug, { reviewer = 'independent-reviewer'
|
|
|
1393
3391
|
completion_confirmed: true,
|
|
1394
3392
|
});
|
|
1395
3393
|
await writeState(root, next);
|
|
1396
|
-
return {
|
|
3394
|
+
return {
|
|
3395
|
+
root,
|
|
3396
|
+
state: next,
|
|
3397
|
+
verdict: 'APPROVE',
|
|
3398
|
+
rollbackTarget: 'none',
|
|
3399
|
+
reviewMessageZh: `Review 结果:${normalized} 已完成,工作流已进入 done。`,
|
|
3400
|
+
};
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
if (state.current_stage === STAGES.REVIEW && state.approval.build === APPROVAL_STATES.APPROVED && state.requested_transition === TRANSITIONS.REVIEW_TO_BUILD && state.review_verdict === 'request-changes') {
|
|
3404
|
+
const next = withRecommendedAction({
|
|
3405
|
+
...state,
|
|
3406
|
+
current_stage: STAGES.BUILD,
|
|
3407
|
+
stage_status: 'pending-rework',
|
|
3408
|
+
review_status: 'pending-fix',
|
|
3409
|
+
pending_user_decision: TRANSITIONS.NONE,
|
|
3410
|
+
requested_transition: TRANSITIONS.NONE,
|
|
3411
|
+
last_confirmed_transition: TRANSITIONS.REVIEW_TO_BUILD,
|
|
3412
|
+
execution_record_status: 'pending-rework',
|
|
3413
|
+
build_verification_status: 'pending',
|
|
3414
|
+
build_architect_verification_status: 'pending',
|
|
3415
|
+
build_deslop_status: state.build_no_deslop ? 'skipped' : 'pending',
|
|
3416
|
+
build_regression_status: state.build_no_deslop ? 'skipped' : 'pending',
|
|
3417
|
+
build_blockers: ['review_rework_required'],
|
|
3418
|
+
approval: {
|
|
3419
|
+
...state.approval,
|
|
3420
|
+
build: APPROVAL_STATES.APPROVED,
|
|
3421
|
+
review: APPROVAL_STATES.NOT_REQUESTED,
|
|
3422
|
+
rollback: APPROVAL_STATES.NOT_REQUESTED,
|
|
3423
|
+
complete: APPROVAL_STATES.NOT_REQUESTED,
|
|
3424
|
+
},
|
|
3425
|
+
});
|
|
3426
|
+
await writeState(root, next);
|
|
3427
|
+
return {
|
|
3428
|
+
root,
|
|
3429
|
+
state: next,
|
|
3430
|
+
verdict: 'REQUEST CHANGES',
|
|
3431
|
+
rollbackTarget: 'build',
|
|
3432
|
+
reviewMessageZh: `Review 结果:${normalized} 要求修改,已回到 build 阶段。\nNext:\n${reviewReworkBuildCommand(normalized)}`,
|
|
3433
|
+
};
|
|
1397
3434
|
}
|
|
1398
3435
|
|
|
1399
|
-
if (state.current_stage === STAGES.REVIEW && state.approval.rollback === APPROVAL_STATES.APPROVED && state.review_verdict === 'request-changes') {
|
|
3436
|
+
if (state.current_stage === STAGES.REVIEW && state.approval.rollback === APPROVAL_STATES.APPROVED && state.requested_transition === TRANSITIONS.REVIEW_TO_PLAN && state.review_verdict === 'request-changes') {
|
|
1400
3437
|
const next = withRecommendedAction({
|
|
1401
3438
|
...state,
|
|
1402
3439
|
current_stage: STAGES.PLAN,
|
|
1403
|
-
|
|
3440
|
+
stage_status: 'pending-rework',
|
|
3441
|
+
pending_user_decision: TRANSITIONS.NONE,
|
|
3442
|
+
requested_transition: TRANSITIONS.NONE,
|
|
3443
|
+
last_confirmed_transition: TRANSITIONS.REVIEW_TO_PLAN,
|
|
3444
|
+
plan_package_status: 'pending-rework',
|
|
3445
|
+
plan_principles_resolved: false,
|
|
3446
|
+
plan_options_reviewed: false,
|
|
3447
|
+
plan_architect_review_status: 'pending',
|
|
3448
|
+
plan_critic_verdict: 'pending',
|
|
3449
|
+
plan_acceptance_criteria_testable: false,
|
|
3450
|
+
plan_verification_steps_resolved: false,
|
|
3451
|
+
plan_execution_inputs_resolved: false,
|
|
3452
|
+
plan_docs_status: 'pending-rework',
|
|
3453
|
+
plan_blockers: ['review_rework_required'],
|
|
3454
|
+
plan_current_iteration: 0,
|
|
3455
|
+
build_blockers: ['plan_rework_required'],
|
|
3456
|
+
review_status: 'pending-fix',
|
|
3457
|
+
approval: {
|
|
3458
|
+
...state.approval,
|
|
3459
|
+
plan: APPROVAL_STATES.NOT_REQUESTED,
|
|
3460
|
+
build: APPROVAL_STATES.NOT_REQUESTED,
|
|
3461
|
+
review: APPROVAL_STATES.NOT_REQUESTED,
|
|
3462
|
+
rollback: APPROVAL_STATES.APPROVED,
|
|
3463
|
+
},
|
|
3464
|
+
});
|
|
3465
|
+
await writeState(root, next);
|
|
3466
|
+
return {
|
|
3467
|
+
root,
|
|
3468
|
+
state: next,
|
|
3469
|
+
verdict: 'REQUEST CHANGES',
|
|
3470
|
+
rollbackTarget: 'plan',
|
|
3471
|
+
reviewMessageZh: `Review 结果:${normalized} 要求修改,已回退到 plan 阶段。`,
|
|
3472
|
+
};
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
if (state.current_stage === STAGES.REVIEW && state.approval.rollback === APPROVAL_STATES.APPROVED && state.requested_transition === TRANSITIONS.REVIEW_TO_CLARIFY && state.review_verdict === 'request-changes') {
|
|
3476
|
+
const next = withRecommendedAction({
|
|
3477
|
+
...state,
|
|
3478
|
+
current_stage: STAGES.CLARIFY,
|
|
3479
|
+
stage_status: 'pending-rework',
|
|
3480
|
+
clarify_ambiguity_score: 1,
|
|
3481
|
+
clarify_pressure_pass_complete: false,
|
|
3482
|
+
clarify_non_goals_resolved: false,
|
|
3483
|
+
clarify_decision_boundaries_resolved: false,
|
|
3484
|
+
unresolved_ambiguity_count: Math.max(1, Number(state.unresolved_ambiguity_count || 0)),
|
|
3485
|
+
plan_package_status: 'pending-rework',
|
|
3486
|
+
plan_principles_resolved: false,
|
|
3487
|
+
plan_options_reviewed: false,
|
|
3488
|
+
plan_architect_review_status: 'pending',
|
|
3489
|
+
plan_critic_verdict: 'pending',
|
|
3490
|
+
plan_acceptance_criteria_testable: false,
|
|
3491
|
+
plan_verification_steps_resolved: false,
|
|
3492
|
+
plan_execution_inputs_resolved: false,
|
|
3493
|
+
plan_docs_status: 'pending-rework',
|
|
3494
|
+
plan_blockers: ['clarify_rework_required'],
|
|
3495
|
+
build_blockers: ['clarify_rework_required'],
|
|
3496
|
+
review_status: 'pending-fix',
|
|
1404
3497
|
pending_user_decision: TRANSITIONS.NONE,
|
|
1405
3498
|
requested_transition: TRANSITIONS.NONE,
|
|
1406
|
-
last_confirmed_transition: TRANSITIONS.
|
|
1407
|
-
plan_package_status: 'complete',
|
|
3499
|
+
last_confirmed_transition: TRANSITIONS.REVIEW_TO_CLARIFY,
|
|
1408
3500
|
approval: {
|
|
1409
3501
|
...state.approval,
|
|
3502
|
+
plan: APPROVAL_STATES.NOT_REQUESTED,
|
|
1410
3503
|
build: APPROVAL_STATES.NOT_REQUESTED,
|
|
1411
3504
|
review: APPROVAL_STATES.NOT_REQUESTED,
|
|
1412
3505
|
rollback: APPROVAL_STATES.APPROVED,
|
|
3506
|
+
complete: APPROVAL_STATES.NOT_REQUESTED,
|
|
1413
3507
|
},
|
|
1414
3508
|
});
|
|
1415
3509
|
await writeState(root, next);
|
|
1416
|
-
return {
|
|
3510
|
+
return {
|
|
3511
|
+
root,
|
|
3512
|
+
state: next,
|
|
3513
|
+
verdict: 'REQUEST CHANGES',
|
|
3514
|
+
rollbackTarget: 'clarify',
|
|
3515
|
+
reviewMessageZh: `Review 结果:${normalized} 要求修改,已回到 clarify 阶段。\nNext:\n$clarify ${normalized}`,
|
|
3516
|
+
};
|
|
1417
3517
|
}
|
|
1418
3518
|
|
|
1419
|
-
|
|
3519
|
+
if (!rerunsAwaitingCompletionReview) {
|
|
3520
|
+
ensureApprovedTransition(state, TRANSITIONS.BUILD_TO_REVIEW, 'review');
|
|
3521
|
+
}
|
|
1420
3522
|
const { state: refreshed, executionSummary } = await refreshExecutionStatus(root, state);
|
|
3523
|
+
const buildOwnedChangedFilesStatus = Object.hasOwn(executionSummary.meta, 'changed_files')
|
|
3524
|
+
? 'present'
|
|
3525
|
+
: 'unavailable';
|
|
3526
|
+
const buildOwnedChangedFiles = buildOwnedChangedFilesStatus === 'present' && Array.isArray(executionSummary.meta.changed_files)
|
|
3527
|
+
? executionSummary.meta.changed_files
|
|
3528
|
+
: [];
|
|
3529
|
+
const reviewManifest = await readContextManifest(reviewContextManifestPath(root), { cwd });
|
|
3530
|
+
ensureValidContextManifest(reviewManifest, STAGES.REVIEW);
|
|
3531
|
+
const reviewAdapter = adapter || createDefaultReviewAdapter();
|
|
3532
|
+
let codeReview = null;
|
|
3533
|
+
try {
|
|
3534
|
+
codeReview = await reviewAdapter.codeReview({
|
|
3535
|
+
cwd,
|
|
3536
|
+
root,
|
|
3537
|
+
slug: normalized,
|
|
3538
|
+
reviewer,
|
|
3539
|
+
executionRecordPath: artifactPath(root, 'execution-record.md'),
|
|
3540
|
+
planArtifactPath: refreshed.plan_artifact_path,
|
|
3541
|
+
testSpecArtifactPath: refreshed.test_spec_artifact_path,
|
|
3542
|
+
buildOwnedChangedFiles,
|
|
3543
|
+
contextManifestStatus: reviewManifest.status,
|
|
3544
|
+
contextManifestPath: reviewContextManifestPath(root),
|
|
3545
|
+
contextManifestRows: reviewManifest.rows,
|
|
3546
|
+
buildOwnedChangedFilesStatus,
|
|
3547
|
+
});
|
|
3548
|
+
} catch (error) {
|
|
3549
|
+
codeReview = codeReviewFailureResult(error);
|
|
3550
|
+
}
|
|
3551
|
+
await ensureDir(join(root, 'review-support'));
|
|
3552
|
+
await writeText(join(root, 'review-support', 'code-review.json'), JSON.stringify(codeReview, null, 2));
|
|
3553
|
+
await writeReviewChangedFiles(root, codeReview?.changedFiles || []);
|
|
3554
|
+
let architectureReview = null;
|
|
3555
|
+
if (reviewAdapter.architectureReview) {
|
|
3556
|
+
try {
|
|
3557
|
+
architectureReview = await reviewAdapter.architectureReview({
|
|
3558
|
+
cwd,
|
|
3559
|
+
root,
|
|
3560
|
+
slug: normalized,
|
|
3561
|
+
reviewer,
|
|
3562
|
+
executionRecordPath: artifactPath(root, 'execution-record.md'),
|
|
3563
|
+
planArtifactPath: refreshed.plan_artifact_path,
|
|
3564
|
+
testSpecArtifactPath: refreshed.test_spec_artifact_path,
|
|
3565
|
+
changeArtifactPaths: refreshed.change_artifact_paths,
|
|
3566
|
+
buildOwnedChangedFiles,
|
|
3567
|
+
contextManifestStatus: reviewManifest.status,
|
|
3568
|
+
contextManifestPath: reviewContextManifestPath(root),
|
|
3569
|
+
contextManifestRows: reviewManifest.rows,
|
|
3570
|
+
buildOwnedChangedFilesStatus,
|
|
3571
|
+
});
|
|
3572
|
+
} catch (error) {
|
|
3573
|
+
architectureReview = architectureReviewFailureResult(error);
|
|
3574
|
+
}
|
|
3575
|
+
} else {
|
|
3576
|
+
architectureReview = {
|
|
3577
|
+
status: 'complete',
|
|
3578
|
+
verdict: 'pass',
|
|
3579
|
+
summary: '架构 smell 扫描通过。',
|
|
3580
|
+
findings: [],
|
|
3581
|
+
};
|
|
3582
|
+
}
|
|
3583
|
+
await writeText(join(root, 'review-support', 'architecture-smell.json'), JSON.stringify(architectureReview, null, 2));
|
|
1421
3584
|
const reviewInput = reviewFindings({
|
|
1422
3585
|
executionMeta: executionSummary.meta,
|
|
1423
3586
|
executionStatus: refreshed.execution_record_status,
|
|
1424
3587
|
reviewer,
|
|
3588
|
+
codeReview,
|
|
3589
|
+
architectureReview,
|
|
1425
3590
|
});
|
|
3591
|
+
reviewInput.inputManifest = manifestRowsToInputManifest(reviewManifest.rows, reviewInput.inputManifest);
|
|
1426
3592
|
const runId = executionSummary.meta.run_id || refreshed.active_run_id || `${normalized}-unknown-run`;
|
|
1427
3593
|
|
|
1428
3594
|
await writeText(
|
|
@@ -1437,29 +3603,74 @@ export async function reviewStage(cwd, slug, { reviewer = 'independent-reviewer'
|
|
|
1437
3603
|
inputManifest: reviewInput.inputManifest,
|
|
1438
3604
|
evidenceManifest: reviewInput.evidenceManifest,
|
|
1439
3605
|
findings: reviewInput.findings,
|
|
3606
|
+
codeReview,
|
|
3607
|
+
architectureReview,
|
|
1440
3608
|
}),
|
|
1441
3609
|
);
|
|
1442
3610
|
|
|
3611
|
+
const reviewMessage = reviewUserMessageZh({
|
|
3612
|
+
slug: normalized,
|
|
3613
|
+
verdict: reviewInput.verdict,
|
|
3614
|
+
rollbackTarget: reviewInput.rollbackTarget,
|
|
3615
|
+
findings: reviewInput.findings,
|
|
3616
|
+
});
|
|
3617
|
+
let journal = null;
|
|
3618
|
+
let journalWarning = null;
|
|
3619
|
+
const shouldReuseReviewJournal = reviewInput.verdict === 'APPROVE'
|
|
3620
|
+
&& rerunsAwaitingCompletionReview
|
|
3621
|
+
&& refreshed.workspace_journal_status === 'written'
|
|
3622
|
+
&& refreshed.workspace_journal_path;
|
|
3623
|
+
const shouldWriteReviewJournal = reviewInput.verdict === 'APPROVE' && !shouldReuseReviewJournal;
|
|
3624
|
+
if (shouldReuseReviewJournal) {
|
|
3625
|
+
journal = { journalPath: refreshed.workspace_journal_path };
|
|
3626
|
+
}
|
|
3627
|
+
if (shouldWriteReviewJournal) {
|
|
3628
|
+
try {
|
|
3629
|
+
journal = await writeReviewJournal({
|
|
3630
|
+
cwd,
|
|
3631
|
+
slug: normalized,
|
|
3632
|
+
verdict: reviewInput.verdict,
|
|
3633
|
+
reviewMessageZh: reviewMessage,
|
|
3634
|
+
evidenceManifest: reviewInput.evidenceManifest,
|
|
3635
|
+
followUps: ['等待 review -> done 审批。'],
|
|
3636
|
+
});
|
|
3637
|
+
} catch (error) {
|
|
3638
|
+
journalWarning = error instanceof Error ? error.message : String(error);
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
|
|
1443
3642
|
const next = withRecommendedAction({
|
|
1444
3643
|
...refreshed,
|
|
1445
3644
|
current_stage: STAGES.REVIEW,
|
|
1446
3645
|
stage_status: 'awaiting-approval',
|
|
1447
3646
|
review_status: 'in-review',
|
|
1448
|
-
pending_user_decision: reviewInput.verdict === 'APPROVE' ? TRANSITIONS.REVIEW_TO_DONE :
|
|
3647
|
+
pending_user_decision: reviewInput.verdict === 'APPROVE' ? TRANSITIONS.REVIEW_TO_DONE : transitionForRollbackTarget(reviewInput.rollbackTarget),
|
|
1449
3648
|
requested_transition: TRANSITIONS.NONE,
|
|
1450
3649
|
last_confirmed_transition: TRANSITIONS.BUILD_TO_REVIEW,
|
|
1451
3650
|
review_verdict: reviewInput.verdict === 'APPROVE' ? 'approve' : 'request-changes',
|
|
1452
3651
|
rollback_target: reviewInput.rollbackTarget,
|
|
1453
3652
|
rollback_rationale: reviewInput.rollbackRationale,
|
|
3653
|
+
context_manifest_status: reviewManifest.status,
|
|
3654
|
+
review_context_manifest_path: reviewContextManifestPath(root),
|
|
3655
|
+
workspace_journal_status: reviewInput.verdict === 'APPROVE' ? (journal ? 'written' : 'failed') : 'skipped',
|
|
3656
|
+
workspace_journal_path: journal?.journalPath || null,
|
|
3657
|
+
workspace_journal_error: journalWarning,
|
|
1454
3658
|
approval: {
|
|
1455
3659
|
...refreshed.approval,
|
|
1456
3660
|
review: APPROVAL_STATES.APPROVED,
|
|
1457
|
-
|
|
3661
|
+
build: reviewInput.verdict === 'REQUEST CHANGES' && reviewInput.rollbackTarget === STAGES.BUILD ? APPROVAL_STATES.REQUESTED : refreshed.approval.build,
|
|
3662
|
+
rollback: reviewInput.verdict === 'APPROVE' || reviewInput.rollbackTarget === STAGES.BUILD ? APPROVAL_STATES.NOT_REQUESTED : APPROVAL_STATES.REQUESTED,
|
|
1458
3663
|
complete: reviewInput.verdict === 'APPROVE' ? APPROVAL_STATES.REQUESTED : APPROVAL_STATES.NOT_REQUESTED,
|
|
1459
3664
|
},
|
|
1460
3665
|
});
|
|
1461
3666
|
await writeState(root, next);
|
|
1462
|
-
return {
|
|
3667
|
+
return {
|
|
3668
|
+
root,
|
|
3669
|
+
state: next,
|
|
3670
|
+
verdict: reviewInput.verdict,
|
|
3671
|
+
rollbackTarget: reviewInput.rollbackTarget,
|
|
3672
|
+
reviewMessageZh: `${reviewMessage} 代码审查:${codeReview.summary} 架构扫描:${architectureReview.summary}${journalWarning ? ` journal 写入失败:${journalWarning}` : ''}`,
|
|
3673
|
+
};
|
|
1463
3674
|
}
|
|
1464
3675
|
|
|
1465
3676
|
async function writeAutopilotRun(rootPath, payload) {
|
|
@@ -1616,9 +3827,8 @@ export async function autopilotStage(cwd, slug, { reviewer = 'autopilot-reviewer
|
|
|
1616
3827
|
});
|
|
1617
3828
|
throw new Error('autopilot_review_failed');
|
|
1618
3829
|
}
|
|
1619
|
-
await approveStage(cwd, normalized, { from: STAGES.REVIEW, to: STAGES.DONE });
|
|
3830
|
+
const done = await approveStage(cwd, normalized, { from: STAGES.REVIEW, to: STAGES.DONE });
|
|
1620
3831
|
recordEvent(TRANSITIONS.REVIEW_TO_DONE);
|
|
1621
|
-
const done = await reviewStage(cwd, normalized, { reviewer });
|
|
1622
3832
|
await persistRun({
|
|
1623
3833
|
currentPhase: 'complete',
|
|
1624
3834
|
completed: true,
|
|
@@ -1647,6 +3857,7 @@ async function listWorkflowSummaries(workflowsRoot) {
|
|
|
1647
3857
|
workflows.push({
|
|
1648
3858
|
slug,
|
|
1649
3859
|
current_stage: state?.current_stage ?? null,
|
|
3860
|
+
archive_status: state?.archive_status ?? null,
|
|
1650
3861
|
contract: legacy ? 'legacy-codex-helper' : 'loopx-v1',
|
|
1651
3862
|
legacy,
|
|
1652
3863
|
schema_version: state?.schema_version ?? 0,
|
|
@@ -1678,6 +3889,8 @@ export async function statusSummary(cwd, slug) {
|
|
|
1678
3889
|
const initialized = existsSync(workspaceRoot);
|
|
1679
3890
|
const config = await readWorkspaceConfig(cwd);
|
|
1680
3891
|
const workflowsRoot = join(workspaceRoot, 'workflows');
|
|
3892
|
+
const { hook } = await doctorRuntime(cwd);
|
|
3893
|
+
const contextSetup = await inspectWorkspaceContext(cwd);
|
|
1681
3894
|
|
|
1682
3895
|
if (!slug) {
|
|
1683
3896
|
const workflows = await listWorkflowSummaries(workflowsRoot);
|
|
@@ -1688,6 +3901,8 @@ export async function statusSummary(cwd, slug) {
|
|
|
1688
3901
|
workflows,
|
|
1689
3902
|
workflow_count: workflows.length,
|
|
1690
3903
|
summary: summarizeWorkspace(workflows),
|
|
3904
|
+
hook,
|
|
3905
|
+
contextSetup,
|
|
1691
3906
|
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.',
|
|
1692
3907
|
};
|
|
1693
3908
|
}
|
|
@@ -1695,7 +3910,11 @@ export async function statusSummary(cwd, slug) {
|
|
|
1695
3910
|
const normalized = normalizeSlug(slug);
|
|
1696
3911
|
const root = resolveWorkflowRoot(cwd, normalized);
|
|
1697
3912
|
const state = await readState(cwd, normalized);
|
|
1698
|
-
|
|
3913
|
+
let effectiveState = state;
|
|
3914
|
+
if (state?.current_stage === STAGES.CLARIFY) {
|
|
3915
|
+
effectiveState = withClarifySummary(state, await readSpecSummary(root));
|
|
3916
|
+
}
|
|
3917
|
+
const legacy = detectLegacyContract(root, effectiveState);
|
|
1699
3918
|
const artifacts = collectArtifactPresence(root, legacy ? LEGACY_ARTIFACTS : V1_ARTIFACTS);
|
|
1700
3919
|
const missing = Object.entries(artifacts).filter(([, present]) => !present).map(([name]) => name);
|
|
1701
3920
|
return {
|
|
@@ -1704,12 +3923,14 @@ export async function statusSummary(cwd, slug) {
|
|
|
1704
3923
|
config,
|
|
1705
3924
|
slug: normalized,
|
|
1706
3925
|
root,
|
|
1707
|
-
state:
|
|
3926
|
+
state: effectiveState ? withRecommendedAction(effectiveState, legacy) : null,
|
|
1708
3927
|
legacy,
|
|
1709
3928
|
contract: legacy ? 'legacy-codex-helper' : 'loopx-v1',
|
|
1710
|
-
schema_version:
|
|
3929
|
+
schema_version: effectiveState?.schema_version ?? 0,
|
|
1711
3930
|
artifacts,
|
|
1712
3931
|
missing_artifacts: missing,
|
|
1713
|
-
|
|
3932
|
+
hook,
|
|
3933
|
+
contextSetup,
|
|
3934
|
+
next_action: effectiveState ? recommendedAction(effectiveState, legacy) : 'Run loopx clarify to start a workflow.',
|
|
1714
3935
|
};
|
|
1715
3936
|
}
|