@openprd/cli 0.1.9 → 0.1.10
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/.openprd/benchmarks/index.md +15 -1
- package/.openprd/config.yaml +10 -0
- package/.openprd/design/README.md +47 -0
- package/.openprd/design/README_EN.md +25 -0
- package/.openprd/design/active/asset-spec.md +15 -0
- package/.openprd/design/active/direction-plan.md +7 -0
- package/.openprd/design/active/facts-sheet.md +13 -0
- package/.openprd/design/active/image-preflight.md +9 -0
- package/.openprd/design/active/selected-direction.md +11 -0
- package/.openprd/design/anti-slop.md +16 -0
- package/.openprd/design/assets/dark-mesh.svg +14 -0
- package/.openprd/design/assets/paper-grid.svg +14 -0
- package/.openprd/design/assets/surface-presets.md +21 -0
- package/.openprd/design/assets/workbench-grid.svg +16 -0
- package/.openprd/design/checklists/ui-quality-gate.md +22 -0
- package/.openprd/design/components/component-catalog.md +42 -0
- package/.openprd/design/layouts/layout-catalog.json +45 -0
- package/.openprd/design/lenses/frontend-lenses.md +31 -0
- package/.openprd/design/recipes/content-experience.md +26 -0
- package/.openprd/design/recipes/operational-tool.md +25 -0
- package/.openprd/design/recipes/product-launch.md +26 -0
- package/.openprd/design/templates/README.md +36 -0
- package/.openprd/design/templates/README_EN.md +31 -0
- package/.openprd/design/templates/content-home.html +415 -0
- package/.openprd/design/templates/ops-dashboard.html +340 -0
- package/.openprd/design/templates/product-launch.html +360 -0
- package/.openprd/design/themes/theme-catalog.json +77 -0
- package/.openprd/engagements/active/prd.md +111 -100
- package/.openprd/schema/prd.schema.yaml +10 -0
- package/.openprd/templates/base/intake.md +4 -0
- package/.openprd/templates/base/prd.md +16 -6
- package/AGENTS.md +10 -7
- package/README.md +26 -28
- package/README_EN.md +3 -3
- package/package.json +41 -2
- package/scripts/openprd-codex-isolated-worker.mjs +799 -0
- package/skills/openprd-benchmark-router/SKILL.md +4 -0
- package/skills/openprd-frontend-design/SKILL.md +161 -0
- package/skills/openprd-frontend-design/references/design-asset-contract.md +34 -0
- package/skills/openprd-frontend-design/references/direction-engine.md +46 -0
- package/skills/openprd-harness/SKILL.md +37 -20
- package/skills/openprd-learning-review/SKILL.md +2 -1
- package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +1 -1
- package/skills/openprd-quality/SKILL.md +5 -3
- package/skills/openprd-requirement-intake/SKILL.md +27 -5
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -3
- package/skills/openprd-requirement-intake/references/routing-rubric.md +9 -5
- package/skills/openprd-requirement-intake/references/startup-validation-lens.md +23 -0
- package/skills/openprd-router/SKILL.md +13 -5
- package/skills/openprd-shared/SKILL.md +38 -25
- package/src/agent-integration.js +120 -69
- package/src/brainstorm-artifacts.js +1657 -0
- package/src/brainstorm-presentation.js +223 -0
- package/src/brainstorm.js +603 -0
- package/src/cli/args.js +51 -3
- package/src/cli/doctor-print.js +34 -8
- package/src/cli/print.js +6 -0
- package/src/cli/quality-print.js +70 -0
- package/src/cli/run-print.js +43 -1
- package/src/cli/shared-print.js +33 -2
- package/src/cli/workflow-print.js +21 -0
- package/src/codex-hook-runner-template.mjs +838 -58
- package/src/design-starter-support.js +462 -0
- package/src/design-starter.js +1207 -0
- package/src/knowledge.js +232 -20
- package/src/learning-review.js +3 -3
- package/src/loop.js +839 -101
- package/src/openprd.js +86 -1
- package/src/openspec/change-validate.js +1 -0
- package/src/prd-core.js +18 -0
- package/src/quality-learning.js +50 -6
- package/src/quality.js +2 -2
- package/src/run-harness.js +191 -29
- package/src/self-update.js +301 -4
- package/src/standards.js +46 -5
- package/src/visual-prepare.js +940 -0
- package/src/workspace-core.js +137 -0
- package/src/workspace-workflow.js +190 -7
- package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +0 -14
- package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +0 -14
- package/.openprd/discovery/config.json +0 -35
- package/.openprd/engagements/active/flows.md +0 -35
- package/.openprd/engagements/active/handoff.md +0 -16
- package/.openprd/engagements/active/review.html +0 -61
- package/.openprd/engagements/active/roles.md +0 -22
- package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +0 -23
- package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +0 -18
- package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +0 -18
- package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +0 -18
- package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +0 -18
- package/.openprd/exports/.gitkeep +0 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +0 -78
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +0 -129
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +0 -41
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +0 -14
- package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +0 -49
- package/.openprd/knowledge/index.json +0 -47
- package/.openprd/reviews/v0001.html +0 -1322
- package/.openprd/reviews/v0002.html +0 -1150
- package/.openprd/reviews/v0003.html +0 -1150
- package/.openprd/reviews/v0004.html +0 -1150
- package/.openprd/reviews/v0005.html +0 -1150
- package/.openprd/sessions/.gitkeep +0 -0
- package/.openprd/state/changes.json +0 -27
- package/.openprd/state/current.json +0 -505
- package/.openprd/state/release-ledger.json +0 -387
- package/.openprd/state/version-index.json +0 -67
- package/.openprd/state/versions/.gitkeep +0 -0
- package/.openprd/state/versions/v0001.json +0 -121
- package/.openprd/state/versions/v0001.md +0 -161
- package/.openprd/state/versions/v0002.json +0 -264
- package/.openprd/state/versions/v0002.md +0 -183
- package/.openprd/state/versions/v0003.json +0 -269
- package/.openprd/state/versions/v0003.md +0 -188
- package/.openprd/state/versions/v0004.json +0 -274
- package/.openprd/state/versions/v0004.md +0 -193
- package/.openprd/state/versions/v0005.json +0 -299
- package/.openprd/state/versions/v0005.md +0 -189
- package/docs/assets/openprd-capability-overview-en.png +0 -0
- package/docs/assets/openprd-capability-overview-zh.png +0 -0
- package/docs/assets/openprd-learning-html.png +0 -0
- package/docs/assets/openprd-quality-html.png +0 -0
- package/docs/assets/openprd-requirement-routing-en.png +0 -0
- package/docs/assets/openprd-requirement-routing-en.svg +0 -102
- package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.svg +0 -102
- package/docs/assets/openprd-review-html.png +0 -0
- package/docs/assets/openprd-scenario-overview.png +0 -0
- package/docs/assets/openprd-scenario-overview.svg +0 -114
- package/docs/assets/openprd-self-evolving-mechanisms-en.png +0 -0
- package/docs/assets/openprd-self-evolving-mechanisms-zh.png +0 -0
- package/docs/assets/openprd-visual-compare-case-study-en.png +0 -0
- package/docs/assets/openprd-visual-compare-case-study-zh.png +0 -0
package/src/loop.js
CHANGED
|
@@ -42,6 +42,24 @@ const LOOP_SESSIONS = path.join('.openprd', 'harness', 'agent-sessions.jsonl');
|
|
|
42
42
|
const LOOP_BOOTSTRAP = path.join('.openprd', 'harness', 'bootstrap.sh');
|
|
43
43
|
const LOOP_PROMPTS_DIR = path.join('.openprd', 'harness', 'loop-prompts');
|
|
44
44
|
const LOOP_TEST_REPORTS_DIR = path.join('.openprd', 'harness', 'test-reports');
|
|
45
|
+
const RELEASE_LEDGER = path.join('.openprd', 'state', 'release-ledger.json');
|
|
46
|
+
const LOOP_OPERATIONAL_ARTIFACT_PATTERNS = [
|
|
47
|
+
'.openprd/engagements/**',
|
|
48
|
+
'.openprd/growth/events.jsonl',
|
|
49
|
+
'.openprd/growth/ledger.json',
|
|
50
|
+
'.openprd/knowledge/**',
|
|
51
|
+
'.openprd/learning/**',
|
|
52
|
+
'.openprd/quality/reports/**',
|
|
53
|
+
'.openprd/state/changes.json',
|
|
54
|
+
'.openprd/state/current.json',
|
|
55
|
+
'.openprd/state/events.jsonl',
|
|
56
|
+
'.openprd/state/task-graph.json',
|
|
57
|
+
'.openprd/state/version-index.json',
|
|
58
|
+
];
|
|
59
|
+
const LOOP_INTERNAL_STATE_PATTERNS = [
|
|
60
|
+
'.openprd/harness/**',
|
|
61
|
+
...LOOP_OPERATIONAL_ARTIFACT_PATTERNS,
|
|
62
|
+
];
|
|
45
63
|
const LOOP_AGENT_VALUES = ['codex', 'claude'];
|
|
46
64
|
|
|
47
65
|
function cjoin(...parts) {
|
|
@@ -115,6 +133,42 @@ function normalizeTaskReference(value) {
|
|
|
115
133
|
.toLowerCase();
|
|
116
134
|
}
|
|
117
135
|
|
|
136
|
+
function toPosixPath(value) {
|
|
137
|
+
return String(value ?? '').split(path.sep).join('/');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function normalizeLoopModuleName(raw, fallback = 'project') {
|
|
141
|
+
const candidate = String(raw ?? '').trim().split('/').filter(Boolean).pop() ?? '';
|
|
142
|
+
const normalized = candidate.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
143
|
+
return normalized || fallback;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function globToRegExp(pattern) {
|
|
147
|
+
const escaped = toPosixPath(pattern)
|
|
148
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
149
|
+
.replace(/\*\*/g, '\u0000')
|
|
150
|
+
.replace(/\*/g, '[^/]*')
|
|
151
|
+
.replace(/\u0000/g, '.*');
|
|
152
|
+
return new RegExp(`^${escaped}$`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function matchesPattern(relativePath, patterns = []) {
|
|
156
|
+
const normalized = toPosixPath(relativePath).replace(/^\/+/, '');
|
|
157
|
+
return patterns.some((pattern) => globToRegExp(pattern).test(normalized));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function uniquePaths(paths = []) {
|
|
161
|
+
const seen = new Set();
|
|
162
|
+
const ordered = [];
|
|
163
|
+
for (const item of paths) {
|
|
164
|
+
const normalized = toPosixPath(item).replace(/^\/+/, '');
|
|
165
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
166
|
+
seen.add(normalized);
|
|
167
|
+
ordered.push(normalized);
|
|
168
|
+
}
|
|
169
|
+
return ordered;
|
|
170
|
+
}
|
|
171
|
+
|
|
118
172
|
function taskReferenceCandidates(task) {
|
|
119
173
|
return new Set([
|
|
120
174
|
task.id,
|
|
@@ -169,6 +223,12 @@ async function ensureLoopFiles(projectRoot) {
|
|
|
169
223
|
currentTaskId: null,
|
|
170
224
|
currentTaskHandle: null,
|
|
171
225
|
currentTaskTitle: null,
|
|
226
|
+
currentTaskBaselinePaths: [],
|
|
227
|
+
currentWorktreePath: path.resolve(projectRoot),
|
|
228
|
+
currentBranch: null,
|
|
229
|
+
lastWorktreePath: path.resolve(projectRoot),
|
|
230
|
+
lastBranch: null,
|
|
231
|
+
lastCommitSha: null,
|
|
172
232
|
completedTaskIds: [],
|
|
173
233
|
lastAgent: null,
|
|
174
234
|
lastSessionAt: null,
|
|
@@ -294,6 +354,32 @@ async function readFeatureList(projectRoot) {
|
|
|
294
354
|
return readJson(filePath);
|
|
295
355
|
}
|
|
296
356
|
|
|
357
|
+
async function readLoopState(projectRoot) {
|
|
358
|
+
const filePath = harnessPath(projectRoot, LOOP_STATE);
|
|
359
|
+
if (!(await exists(filePath))) return null;
|
|
360
|
+
return readJson(filePath);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function resolveLoopWorkspace(projectRoot, options = {}) {
|
|
364
|
+
const workspace = await createOrAttachLoopWorktree(projectRoot, options);
|
|
365
|
+
if (!workspace.ok) {
|
|
366
|
+
return {
|
|
367
|
+
ok: false,
|
|
368
|
+
projectRoot: path.resolve(projectRoot),
|
|
369
|
+
sourceProjectRoot: path.resolve(projectRoot),
|
|
370
|
+
created: false,
|
|
371
|
+
syncedPaths: [],
|
|
372
|
+
git: null,
|
|
373
|
+
errors: workspace.errors ?? ['无法准备 loop 工作区。'],
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
await ensureLoopFiles(workspace.projectRoot);
|
|
377
|
+
return {
|
|
378
|
+
...workspace,
|
|
379
|
+
git: workspace.git?.ok ? workspace.git : null,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
297
383
|
function buildLoopSummary(featureList) {
|
|
298
384
|
const tasks = featureList?.tasks ?? [];
|
|
299
385
|
return {
|
|
@@ -371,6 +457,303 @@ async function updateLoopState(projectRoot, patch) {
|
|
|
371
457
|
return next;
|
|
372
458
|
}
|
|
373
459
|
|
|
460
|
+
function parseGitStatusLine(line) {
|
|
461
|
+
const status = line.slice(0, 2);
|
|
462
|
+
const payload = line.slice(3).trim();
|
|
463
|
+
const filePath = payload.includes(' -> ')
|
|
464
|
+
? payload.split(' -> ').at(-1)
|
|
465
|
+
: payload;
|
|
466
|
+
return {
|
|
467
|
+
status,
|
|
468
|
+
path: toPosixPath(filePath),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function gitStatusEntries(projectRoot) {
|
|
473
|
+
const status = await runCommand('git', ['status', '--porcelain=1', '--untracked-files=all'], { cwd: projectRoot });
|
|
474
|
+
if (!status.ok) {
|
|
475
|
+
return { ok: false, entries: [], status };
|
|
476
|
+
}
|
|
477
|
+
const entries = status.stdout
|
|
478
|
+
.split(/\r?\n/u)
|
|
479
|
+
.map((line) => line.trimEnd())
|
|
480
|
+
.filter(Boolean)
|
|
481
|
+
.map((line) => parseGitStatusLine(line));
|
|
482
|
+
return { ok: true, entries, status };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function inspectGitWorkspace(projectRoot) {
|
|
486
|
+
const topLevel = await runCommand('git', ['rev-parse', '--show-toplevel'], { cwd: projectRoot });
|
|
487
|
+
if (!topLevel.ok) {
|
|
488
|
+
return {
|
|
489
|
+
ok: false,
|
|
490
|
+
projectRoot,
|
|
491
|
+
error: trimOutput(topLevel.stderr || topLevel.stdout) || '当前目录不是 Git 工作区。',
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
const branchResult = await runCommand('git', ['symbolic-ref', '--quiet', '--short', 'HEAD'], { cwd: projectRoot });
|
|
495
|
+
const gitDirResult = await runCommand('git', ['rev-parse', '--path-format=absolute', '--git-dir'], { cwd: projectRoot });
|
|
496
|
+
const commonDirResult = await runCommand('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], { cwd: projectRoot });
|
|
497
|
+
const headResult = await runCommand('git', ['rev-parse', '--short', 'HEAD'], { cwd: projectRoot });
|
|
498
|
+
const gitLinkPath = path.join(projectRoot, '.git');
|
|
499
|
+
let isMainWorktree = false;
|
|
500
|
+
try {
|
|
501
|
+
const stat = await fs.lstat(gitLinkPath);
|
|
502
|
+
isMainWorktree = stat.isDirectory();
|
|
503
|
+
} catch {
|
|
504
|
+
isMainWorktree = false;
|
|
505
|
+
}
|
|
506
|
+
const normalizeExistingPath = async (value) => {
|
|
507
|
+
const resolved = path.resolve(projectRoot, value);
|
|
508
|
+
return fs.realpath(resolved).catch(() => resolved);
|
|
509
|
+
};
|
|
510
|
+
const branch = branchResult.ok ? branchResult.stdout.trim() || null : null;
|
|
511
|
+
const repoRoot = await normalizeExistingPath(topLevel.stdout.trim());
|
|
512
|
+
const worktreePath = await normalizeExistingPath(projectRoot);
|
|
513
|
+
const gitDir = gitDirResult.ok ? await normalizeExistingPath(gitDirResult.stdout.trim()) : null;
|
|
514
|
+
const commonDir = commonDirResult.ok ? await normalizeExistingPath(commonDirResult.stdout.trim()) : null;
|
|
515
|
+
return {
|
|
516
|
+
ok: true,
|
|
517
|
+
projectRoot: worktreePath,
|
|
518
|
+
repoRoot,
|
|
519
|
+
worktreePath,
|
|
520
|
+
branch,
|
|
521
|
+
detachedHead: !branch,
|
|
522
|
+
headSha: headResult.ok ? headResult.stdout.trim() || null : null,
|
|
523
|
+
gitDir,
|
|
524
|
+
commonDir,
|
|
525
|
+
isMainWorktree,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function copyRelativePath(sourceRoot, targetRoot, relativePath, options = {}) {
|
|
530
|
+
const sourcePath = harnessPath(sourceRoot, relativePath);
|
|
531
|
+
const targetPath = harnessPath(targetRoot, relativePath);
|
|
532
|
+
if (!(await exists(sourcePath))) return false;
|
|
533
|
+
if (!options.force && await exists(targetPath)) return false;
|
|
534
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
535
|
+
const stat = await fs.stat(sourcePath);
|
|
536
|
+
await fs.cp(sourcePath, targetPath, { recursive: stat.isDirectory(), force: true });
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function syncLoopIsolationArtifacts(sourceRoot, targetRoot, options = {}) {
|
|
541
|
+
if (path.resolve(sourceRoot) === path.resolve(targetRoot)) {
|
|
542
|
+
return [];
|
|
543
|
+
}
|
|
544
|
+
const copied = [];
|
|
545
|
+
const relativePaths = [
|
|
546
|
+
LOOP_FEATURE_LIST,
|
|
547
|
+
LOOP_PROGRESS,
|
|
548
|
+
LOOP_FAILED_APPROACHES,
|
|
549
|
+
LOOP_SESSIONS,
|
|
550
|
+
LOOP_STATE,
|
|
551
|
+
LOOP_BOOTSTRAP,
|
|
552
|
+
RELEASE_LEDGER,
|
|
553
|
+
options.change ? path.join('openprd', 'changes', options.change) : null,
|
|
554
|
+
].filter(Boolean);
|
|
555
|
+
for (const relativePath of relativePaths) {
|
|
556
|
+
if (await copyRelativePath(sourceRoot, targetRoot, relativePath, { force: Boolean(options.force) })) {
|
|
557
|
+
copied.push(relativePath);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return copied;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function createOrAttachLoopWorktree(projectRoot, options = {}) {
|
|
564
|
+
const sourceRoot = path.resolve(projectRoot);
|
|
565
|
+
const requestedWorktree = options.worktree
|
|
566
|
+
? path.resolve(projectRoot, options.worktree)
|
|
567
|
+
: null;
|
|
568
|
+
if (options.branch && !requestedWorktree) {
|
|
569
|
+
return {
|
|
570
|
+
ok: false,
|
|
571
|
+
projectRoot: sourceRoot,
|
|
572
|
+
errors: ['--branch 需要和 --worktree 一起使用。'],
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
if (!requestedWorktree) {
|
|
576
|
+
return {
|
|
577
|
+
ok: true,
|
|
578
|
+
projectRoot: sourceRoot,
|
|
579
|
+
sourceProjectRoot: sourceRoot,
|
|
580
|
+
created: false,
|
|
581
|
+
syncedPaths: [],
|
|
582
|
+
git: await inspectGitWorkspace(sourceRoot),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
if (!options.branch && !(await exists(requestedWorktree))) {
|
|
586
|
+
return {
|
|
587
|
+
ok: false,
|
|
588
|
+
projectRoot: sourceRoot,
|
|
589
|
+
errors: ['首次接入隔离 worktree 时,需要同时传入 --branch,或者先手动准备好目标 worktree。'],
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const sourceGit = await inspectGitWorkspace(sourceRoot);
|
|
594
|
+
if (!sourceGit.ok) {
|
|
595
|
+
return {
|
|
596
|
+
ok: false,
|
|
597
|
+
projectRoot: sourceRoot,
|
|
598
|
+
errors: [sourceGit.error ?? '当前目录不是 Git 工作区,无法创建隔离 worktree。'],
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
let created = false;
|
|
603
|
+
if (!(await exists(requestedWorktree))) {
|
|
604
|
+
await fs.mkdir(path.dirname(requestedWorktree), { recursive: true });
|
|
605
|
+
const branchRef = `refs/heads/${options.branch}`;
|
|
606
|
+
const branchExists = await runCommand('git', ['show-ref', '--verify', '--quiet', branchRef], { cwd: sourceRoot });
|
|
607
|
+
const args = branchExists.ok
|
|
608
|
+
? ['worktree', 'add', requestedWorktree, options.branch]
|
|
609
|
+
: ['worktree', 'add', '-b', options.branch, requestedWorktree, 'HEAD'];
|
|
610
|
+
const createdWorktree = await runCommand('git', args, { cwd: sourceRoot });
|
|
611
|
+
if (!createdWorktree.ok) {
|
|
612
|
+
return {
|
|
613
|
+
ok: false,
|
|
614
|
+
projectRoot: sourceRoot,
|
|
615
|
+
errors: [`创建隔离 worktree 失败: ${trimOutput(createdWorktree.stderr || createdWorktree.stdout)}`],
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
created = true;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const targetGit = await inspectGitWorkspace(requestedWorktree);
|
|
622
|
+
if (!targetGit.ok) {
|
|
623
|
+
return {
|
|
624
|
+
ok: false,
|
|
625
|
+
projectRoot: sourceRoot,
|
|
626
|
+
errors: [targetGit.error ?? '目标路径不是可用的 Git worktree。'],
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
if (sourceGit.commonDir && targetGit.commonDir && sourceGit.commonDir !== targetGit.commonDir) {
|
|
630
|
+
return {
|
|
631
|
+
ok: false,
|
|
632
|
+
projectRoot: sourceRoot,
|
|
633
|
+
errors: ['目标 worktree 不属于当前仓库,不能用于这次 loop 运行。'],
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
if (options.branch && targetGit.branch !== options.branch) {
|
|
637
|
+
return {
|
|
638
|
+
ok: false,
|
|
639
|
+
projectRoot: sourceRoot,
|
|
640
|
+
errors: [`目标 worktree 当前在分支 ${targetGit.branch ?? 'detached HEAD'},与请求的 ${options.branch} 不一致。`],
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
const syncedPaths = await syncLoopIsolationArtifacts(sourceRoot, requestedWorktree, {
|
|
644
|
+
change: options.change ?? null,
|
|
645
|
+
force: created,
|
|
646
|
+
});
|
|
647
|
+
return {
|
|
648
|
+
ok: true,
|
|
649
|
+
projectRoot: requestedWorktree,
|
|
650
|
+
sourceProjectRoot: sourceRoot,
|
|
651
|
+
created,
|
|
652
|
+
syncedPaths,
|
|
653
|
+
git: targetGit,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function buildCommitPlan(task, statusEntries, baselinePaths = [], options = {}) {
|
|
658
|
+
const changedPaths = uniquePaths(statusEntries.map((entry) => entry.path))
|
|
659
|
+
.filter((item) => !matchesPattern(item, LOOP_INTERNAL_STATE_PATTERNS));
|
|
660
|
+
const baseline = new Set(uniquePaths(baselinePaths).filter((item) => !matchesPattern(item, LOOP_INTERNAL_STATE_PATTERNS)));
|
|
661
|
+
const writeScope = task?.executionStrategy?.writeScope ?? taskExecutionStrategy(task ?? {}).writeScope ?? [];
|
|
662
|
+
const taskEventPath = task?.sourcePath
|
|
663
|
+
? toPosixPath(path.join(path.dirname(task.sourcePath), 'task-events.jsonl'))
|
|
664
|
+
: null;
|
|
665
|
+
const alwaysInclude = new Set(uniquePaths([
|
|
666
|
+
task?.sourcePath,
|
|
667
|
+
taskEventPath,
|
|
668
|
+
...(options.alwaysIncludePaths ?? []),
|
|
669
|
+
]));
|
|
670
|
+
const touchedPaths = changedPaths.filter((item) => !baseline.has(item));
|
|
671
|
+
const outOfScopeTouched = touchedPaths.filter((item) => !alwaysInclude.has(item) && !matchesPattern(item, writeScope));
|
|
672
|
+
if (outOfScopeTouched.length > 0) {
|
|
673
|
+
return {
|
|
674
|
+
ok: false,
|
|
675
|
+
changedPaths,
|
|
676
|
+
touchedPaths,
|
|
677
|
+
stagedPaths: [],
|
|
678
|
+
excludedPaths: changedPaths,
|
|
679
|
+
outOfScopeTouched,
|
|
680
|
+
error: `发现超出当前任务 write-scope 的改动: ${outOfScopeTouched.join(', ')}。请先清理、拆分任务,或改用更明确的隔离 worktree。`,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
const candidateSource = touchedPaths.length > 0 ? touchedPaths : changedPaths;
|
|
684
|
+
const stagedPaths = uniquePaths([
|
|
685
|
+
...candidateSource.filter((item) => alwaysInclude.has(item) || writeScope.length === 0 || matchesPattern(item, writeScope)),
|
|
686
|
+
...changedPaths.filter((item) => alwaysInclude.has(item)),
|
|
687
|
+
]);
|
|
688
|
+
const excludedPaths = changedPaths.filter((item) => !stagedPaths.includes(item));
|
|
689
|
+
return {
|
|
690
|
+
ok: true,
|
|
691
|
+
changedPaths,
|
|
692
|
+
touchedPaths,
|
|
693
|
+
stagedPaths,
|
|
694
|
+
excludedPaths,
|
|
695
|
+
outOfScopeTouched: [],
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
async function gitAddPaths(projectRoot, filePaths) {
|
|
701
|
+
if (filePaths.length === 0) {
|
|
702
|
+
return { ok: true, skipped: true };
|
|
703
|
+
}
|
|
704
|
+
return runCommand('git', ['add', '--', ...filePaths], { cwd: projectRoot });
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async function gitRestorePaths(projectRoot, filePaths) {
|
|
708
|
+
if (filePaths.length === 0) {
|
|
709
|
+
return { ok: true, skipped: true };
|
|
710
|
+
}
|
|
711
|
+
return runCommand('git', ['restore', '--source=HEAD', '--staged', '--worktree', '--', ...filePaths], {
|
|
712
|
+
cwd: projectRoot,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function resolveLoopFolderManualModuleName(workspace) {
|
|
717
|
+
const sourceRoot = workspace?.sourceProjectRoot ? path.resolve(workspace.sourceProjectRoot) : null;
|
|
718
|
+
const projectRoot = workspace?.projectRoot ? path.resolve(workspace.projectRoot) : null;
|
|
719
|
+
if (!sourceRoot || !projectRoot || sourceRoot === projectRoot) {
|
|
720
|
+
return '';
|
|
721
|
+
}
|
|
722
|
+
return normalizeLoopModuleName(path.basename(sourceRoot));
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function cleanupLoopOperationalArtifacts(projectRoot) {
|
|
726
|
+
const status = await gitStatusEntries(projectRoot);
|
|
727
|
+
if (!status.ok) {
|
|
728
|
+
return {
|
|
729
|
+
ok: false,
|
|
730
|
+
restoredPaths: [],
|
|
731
|
+
removedPaths: [],
|
|
732
|
+
error: trimOutput(status.status?.stderr || status.status?.stdout) || '无法读取 Git 状态。',
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
const candidates = status.entries.filter((entry) => matchesPattern(entry.path, LOOP_OPERATIONAL_ARTIFACT_PATTERNS));
|
|
736
|
+
const restoredPaths = uniquePaths(candidates.filter((entry) => entry.status !== '??').map((entry) => entry.path));
|
|
737
|
+
const removedPaths = uniquePaths(candidates.filter((entry) => entry.status === '??').map((entry) => entry.path));
|
|
738
|
+
const restore = await gitRestorePaths(projectRoot, restoredPaths);
|
|
739
|
+
if (!restore.ok) {
|
|
740
|
+
return {
|
|
741
|
+
ok: false,
|
|
742
|
+
restoredPaths: [],
|
|
743
|
+
removedPaths: [],
|
|
744
|
+
error: trimOutput(restore.stderr || restore.stdout) || '无法回滚 loop 运行态产物。',
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
for (const relativePath of removedPaths) {
|
|
748
|
+
await fs.rm(harnessPath(projectRoot, relativePath), { recursive: true, force: true });
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
ok: true,
|
|
752
|
+
restoredPaths,
|
|
753
|
+
removedPaths,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
374
757
|
function renderProgressEntry(title, lines) {
|
|
375
758
|
return `\n## ${title}\n\n${lines.filter(Boolean).map((line) => `- ${line}`).join('\n')}\n`;
|
|
376
759
|
}
|
|
@@ -515,28 +898,140 @@ async function runCommand(command, args, options = {}) {
|
|
|
515
898
|
});
|
|
516
899
|
}
|
|
517
900
|
|
|
518
|
-
async function
|
|
519
|
-
const
|
|
901
|
+
async function planGitCommit(projectRoot, options = {}) {
|
|
902
|
+
const git = options.git ?? await inspectGitWorkspace(projectRoot);
|
|
903
|
+
const status = await gitStatusEntries(projectRoot);
|
|
904
|
+
const branch = git?.ok ? (git.branch ?? null) : null;
|
|
905
|
+
const worktreePath = git?.ok ? (git.worktreePath ?? path.resolve(projectRoot)) : path.resolve(projectRoot);
|
|
520
906
|
if (!status.ok) {
|
|
521
|
-
return {
|
|
907
|
+
return {
|
|
908
|
+
ok: false,
|
|
909
|
+
skipped: false,
|
|
910
|
+
message: 'git status 执行失败',
|
|
911
|
+
status: status.status,
|
|
912
|
+
git: git?.ok ? git : null,
|
|
913
|
+
branch,
|
|
914
|
+
worktreePath,
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
if (status.entries.length === 0) {
|
|
918
|
+
return {
|
|
919
|
+
ok: true,
|
|
920
|
+
skipped: true,
|
|
921
|
+
message: '没有需要提交的 Git 变更。',
|
|
922
|
+
git: git?.ok ? git : null,
|
|
923
|
+
branch,
|
|
924
|
+
worktreePath,
|
|
925
|
+
commitPlan: {
|
|
926
|
+
ok: true,
|
|
927
|
+
changedPaths: [],
|
|
928
|
+
touchedPaths: [],
|
|
929
|
+
stagedPaths: [],
|
|
930
|
+
excludedPaths: [],
|
|
931
|
+
outOfScopeTouched: [],
|
|
932
|
+
},
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
if (!git?.ok) {
|
|
936
|
+
return {
|
|
937
|
+
ok: false,
|
|
938
|
+
skipped: false,
|
|
939
|
+
message: '当前目录不是 Git 工作区,无法自动提交。',
|
|
940
|
+
status: status.status,
|
|
941
|
+
git: null,
|
|
942
|
+
branch,
|
|
943
|
+
worktreePath,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
if (git.detachedHead) {
|
|
947
|
+
return {
|
|
948
|
+
ok: false,
|
|
949
|
+
skipped: false,
|
|
950
|
+
message: '当前 worktree 处于 detached HEAD,不能为 loop 任务自动提交。请先切到命名分支后重试。',
|
|
951
|
+
git,
|
|
952
|
+
branch: null,
|
|
953
|
+
worktreePath,
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
const commitPlan = buildCommitPlan(options.task ?? null, status.entries, options.baselinePaths ?? [], {
|
|
957
|
+
alwaysIncludePaths: options.alwaysIncludePaths ?? [],
|
|
958
|
+
});
|
|
959
|
+
if (!commitPlan.ok) {
|
|
960
|
+
return {
|
|
961
|
+
ok: false,
|
|
962
|
+
skipped: false,
|
|
963
|
+
message: commitPlan.error,
|
|
964
|
+
git,
|
|
965
|
+
branch,
|
|
966
|
+
worktreePath,
|
|
967
|
+
commitPlan,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
if (git.isMainWorktree && !options.allowDirtyMain && commitPlan.excludedPaths.length > 0) {
|
|
971
|
+
return {
|
|
972
|
+
ok: false,
|
|
973
|
+
skipped: false,
|
|
974
|
+
message: `当前主工作区还有未纳入本任务提交的改动: ${commitPlan.excludedPaths.join(', ')}。请改用 --worktree/--branch,或确认后显式传入 --allow-dirty-main。`,
|
|
975
|
+
git,
|
|
976
|
+
branch,
|
|
977
|
+
worktreePath,
|
|
978
|
+
commitPlan,
|
|
979
|
+
};
|
|
522
980
|
}
|
|
523
|
-
if (
|
|
524
|
-
return {
|
|
981
|
+
if (commitPlan.stagedPaths.length === 0) {
|
|
982
|
+
return {
|
|
983
|
+
ok: true,
|
|
984
|
+
skipped: true,
|
|
985
|
+
message: '没有属于当前任务的可提交变更。',
|
|
986
|
+
git,
|
|
987
|
+
branch,
|
|
988
|
+
worktreePath,
|
|
989
|
+
commitPlan,
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
return {
|
|
993
|
+
ok: true,
|
|
994
|
+
skipped: false,
|
|
995
|
+
message: 'ready',
|
|
996
|
+
git,
|
|
997
|
+
branch,
|
|
998
|
+
worktreePath,
|
|
999
|
+
commitPlan,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
async function gitCommit(projectRoot, message, options = {}) {
|
|
1004
|
+
const prepared = options.prepared ?? await planGitCommit(projectRoot, options);
|
|
1005
|
+
if (!prepared.ok || prepared.skipped) {
|
|
1006
|
+
return prepared;
|
|
525
1007
|
}
|
|
526
|
-
const add = await
|
|
1008
|
+
const add = await gitAddPaths(projectRoot, prepared.commitPlan.stagedPaths);
|
|
527
1009
|
if (!add.ok) {
|
|
528
|
-
return {
|
|
1010
|
+
return {
|
|
1011
|
+
...prepared,
|
|
1012
|
+
ok: false,
|
|
1013
|
+
skipped: false,
|
|
1014
|
+
message: 'git add 执行失败',
|
|
1015
|
+
add,
|
|
1016
|
+
};
|
|
529
1017
|
}
|
|
530
|
-
const commit = await runCommand('git', ['commit', '-m', message], { cwd: projectRoot });
|
|
1018
|
+
const commit = await runCommand('git', ['commit', '-m', message, '--', ...prepared.commitPlan.stagedPaths], { cwd: projectRoot });
|
|
531
1019
|
if (!commit.ok) {
|
|
532
|
-
return {
|
|
1020
|
+
return {
|
|
1021
|
+
...prepared,
|
|
1022
|
+
ok: false,
|
|
1023
|
+
skipped: false,
|
|
1024
|
+
message: 'git commit 执行失败',
|
|
1025
|
+
commit,
|
|
1026
|
+
};
|
|
533
1027
|
}
|
|
534
1028
|
const rev = await runCommand('git', ['rev-parse', '--short', 'HEAD'], { cwd: projectRoot });
|
|
535
1029
|
return {
|
|
1030
|
+
...prepared,
|
|
536
1031
|
ok: true,
|
|
537
1032
|
skipped: false,
|
|
538
1033
|
message: '已提交',
|
|
539
|
-
sha: rev.stdout.trim() || null,
|
|
1034
|
+
sha: rev.ok ? (rev.stdout.trim() || null) : null,
|
|
540
1035
|
commit,
|
|
541
1036
|
};
|
|
542
1037
|
}
|
|
@@ -669,6 +1164,18 @@ async function updateReleaseLedgerAfterFinish(projectRoot, task, commitSha = nul
|
|
|
669
1164
|
ledger = appended.ledger;
|
|
670
1165
|
|
|
671
1166
|
let tag = null;
|
|
1167
|
+
if (!commitSha && !current.tag?.name) {
|
|
1168
|
+
const tagged = updateReleaseTag(ledger, {
|
|
1169
|
+
version: current.version,
|
|
1170
|
+
name: current.version,
|
|
1171
|
+
localSha: null,
|
|
1172
|
+
remoteSha: null,
|
|
1173
|
+
remoteStatus: null,
|
|
1174
|
+
warning: null,
|
|
1175
|
+
updatedAt: timestamp(),
|
|
1176
|
+
});
|
|
1177
|
+
ledger = tagged.ledger;
|
|
1178
|
+
}
|
|
672
1179
|
if (commitSha) {
|
|
673
1180
|
tag = await syncLocalVersionTag(projectRoot, current.version, commitSha);
|
|
674
1181
|
const tagged = updateReleaseTag(ledger, {
|
|
@@ -742,12 +1249,14 @@ function parseEvidenceArtifacts(projectRoot, evidenceText) {
|
|
|
742
1249
|
return { screenshots, textualEvidence };
|
|
743
1250
|
}
|
|
744
1251
|
|
|
745
|
-
async function writeTestReport(projectRoot, { task, agent, advanced, change }) {
|
|
1252
|
+
async function writeTestReport(projectRoot, { task, agent, advanced, change, workspace = null, commit = null }) {
|
|
746
1253
|
const relativePath = cjoin(LOOP_TEST_REPORTS_DIR, reportFileName(task.id));
|
|
747
|
-
const htmlRelativePath = cjoin(LOOP_TEST_REPORTS_DIR, `${task.id.replace(/[^a-zA-Z0-9._-]/g, '_')}.html`);
|
|
748
1254
|
const evidenceText = advanced.evidence ?? inferUiVerificationHint(task, agent);
|
|
749
1255
|
const notesText = advanced.notes ?? '无';
|
|
750
1256
|
const evidenceArtifacts = parseEvidenceArtifacts(projectRoot, evidenceText);
|
|
1257
|
+
const worktreePath = workspace?.path ?? path.resolve(projectRoot);
|
|
1258
|
+
const branch = workspace?.branch ?? null;
|
|
1259
|
+
const commitSha = commit?.sha ?? null;
|
|
751
1260
|
const report = {
|
|
752
1261
|
version: 1,
|
|
753
1262
|
generatedAt: timestamp(),
|
|
@@ -755,6 +1264,9 @@ async function writeTestReport(projectRoot, { task, agent, advanced, change }) {
|
|
|
755
1264
|
verifyCommand: advanced.verification?.command ?? task.verify ?? '未指定',
|
|
756
1265
|
oracle: task.oracle ?? null,
|
|
757
1266
|
testStrategy: task.testStrategy ?? taskTestStrategy(task),
|
|
1267
|
+
worktreePath,
|
|
1268
|
+
branch,
|
|
1269
|
+
commitSha,
|
|
758
1270
|
summary: {
|
|
759
1271
|
total: 1,
|
|
760
1272
|
passed: advanced.verification?.ok ? 1 : 0,
|
|
@@ -780,6 +1292,9 @@ async function writeTestReport(projectRoot, { task, agent, advanced, change }) {
|
|
|
780
1292
|
'',
|
|
781
1293
|
`- 测试时间: ${timestamp()}`,
|
|
782
1294
|
`- 变更: ${task.changeId}`,
|
|
1295
|
+
`- 工作区: ${worktreePath}`,
|
|
1296
|
+
`- 分支: ${branch ?? '未命名分支或 detached HEAD'}`,
|
|
1297
|
+
`- 提交: ${commitSha ?? '未提交'}`,
|
|
783
1298
|
`- 完成条件: ${task.done ?? '未指定'}`,
|
|
784
1299
|
`- 自测命令: ${advanced.verification?.command ?? task.verify ?? '未指定'}`,
|
|
785
1300
|
`- 测试策略: ${task.testStrategyDescription ?? describeTestStrategy(task.testStrategy ?? taskTestStrategy(task))}`,
|
|
@@ -787,7 +1302,7 @@ async function writeTestReport(projectRoot, { task, agent, advanced, change }) {
|
|
|
787
1302
|
`- 自测结果: ${advanced.verification?.ok ? '通过' : '失败或未运行'}`,
|
|
788
1303
|
`- Change 校验: ${change.ok ? '通过' : '失败'}`,
|
|
789
1304
|
`- EVO 冒烟证据: ${advanced.verification?.ok ? 'smoke pass via task verify command' : 'smoke failed or missing'}`,
|
|
790
|
-
|
|
1305
|
+
'- EVO 功能覆盖证据: feature coverage checked against OpenPrd task completion',
|
|
791
1306
|
`- 界面验证策略: ${inferUiVerificationHint(task, agent)}`,
|
|
792
1307
|
`- 补充证据: ${evidenceText}`,
|
|
793
1308
|
`- 备注: ${notesText}`,
|
|
@@ -825,7 +1340,7 @@ async function writeTestReport(projectRoot, { task, agent, advanced, change }) {
|
|
|
825
1340
|
await writeHtmlArtifact(htmlPath, renderRegressionArtifact({ task, report }));
|
|
826
1341
|
return {
|
|
827
1342
|
markdownPath: relativePath,
|
|
828
|
-
htmlPath:
|
|
1343
|
+
htmlPath: path.relative(projectRoot, htmlPath),
|
|
829
1344
|
report,
|
|
830
1345
|
};
|
|
831
1346
|
}
|
|
@@ -965,18 +1480,36 @@ export async function nextLoopWorkspace(projectRoot, options = {}) {
|
|
|
965
1480
|
}
|
|
966
1481
|
|
|
967
1482
|
export async function promptLoopWorkspace(projectRoot, options = {}) {
|
|
968
|
-
await
|
|
1483
|
+
const workspace = await resolveLoopWorkspace(projectRoot, options);
|
|
1484
|
+
if (!workspace.ok) {
|
|
1485
|
+
return {
|
|
1486
|
+
ok: false,
|
|
1487
|
+
action: 'loop-prompt',
|
|
1488
|
+
projectRoot: path.resolve(projectRoot),
|
|
1489
|
+
agent: options.agent ?? 'codex',
|
|
1490
|
+
errors: workspace.errors,
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
const effectiveRoot = workspace.projectRoot;
|
|
969
1494
|
const agent = normalizeAgent(options.agent ?? 'codex');
|
|
970
|
-
const featureList = await readFeatureList(
|
|
1495
|
+
const featureList = await readFeatureList(effectiveRoot);
|
|
971
1496
|
if (!featureList) {
|
|
972
1497
|
throw new Error('Loop feature list is missing. Run openprd loop . --plan --change <id>.');
|
|
973
1498
|
}
|
|
974
1499
|
const { task, dependencyState: state } = nextLoopTask(featureList, options.item);
|
|
1500
|
+
const workspaceInfo = {
|
|
1501
|
+
path: workspace.git?.worktreePath ?? path.resolve(effectiveRoot),
|
|
1502
|
+
branch: workspace.git?.branch ?? null,
|
|
1503
|
+
created: workspace.created,
|
|
1504
|
+
syncedPaths: workspace.syncedPaths,
|
|
1505
|
+
};
|
|
975
1506
|
if (!task) {
|
|
976
1507
|
return {
|
|
977
1508
|
ok: false,
|
|
978
1509
|
action: 'loop-prompt',
|
|
979
|
-
projectRoot,
|
|
1510
|
+
projectRoot: effectiveRoot,
|
|
1511
|
+
sourceProjectRoot: workspace.sourceProjectRoot,
|
|
1512
|
+
workspace: workspaceInfo,
|
|
980
1513
|
agent,
|
|
981
1514
|
errors: ['当前没有可执行的 loop 任务。'],
|
|
982
1515
|
};
|
|
@@ -985,7 +1518,9 @@ export async function promptLoopWorkspace(projectRoot, options = {}) {
|
|
|
985
1518
|
return {
|
|
986
1519
|
ok: false,
|
|
987
1520
|
action: 'loop-prompt',
|
|
988
|
-
projectRoot,
|
|
1521
|
+
projectRoot: effectiveRoot,
|
|
1522
|
+
sourceProjectRoot: workspace.sourceProjectRoot,
|
|
1523
|
+
workspace: workspaceInfo,
|
|
989
1524
|
agent,
|
|
990
1525
|
task,
|
|
991
1526
|
dependencyState: state,
|
|
@@ -994,58 +1529,111 @@ export async function promptLoopWorkspace(projectRoot, options = {}) {
|
|
|
994
1529
|
}
|
|
995
1530
|
const prompt = renderLoopPrompt({
|
|
996
1531
|
agent,
|
|
997
|
-
projectRoot,
|
|
1532
|
+
projectRoot: effectiveRoot,
|
|
998
1533
|
featureList,
|
|
999
1534
|
task,
|
|
1000
1535
|
dependency: state,
|
|
1001
1536
|
mode: options.mode ?? 'manual',
|
|
1002
1537
|
});
|
|
1003
1538
|
const promptFileName = `${task.id.replace(/[^a-zA-Z0-9._-]/g, '_')}-${agent}-${Date.now()}.md`;
|
|
1004
|
-
const promptPath = harnessPath(
|
|
1539
|
+
const promptPath = harnessPath(effectiveRoot, cjoin(LOOP_PROMPTS_DIR, promptFileName));
|
|
1005
1540
|
await writeText(promptPath, prompt);
|
|
1006
|
-
const invocation = defaultAgentInvocation(agent,
|
|
1541
|
+
const invocation = defaultAgentInvocation(agent, effectiveRoot, path.relative(effectiveRoot, promptPath));
|
|
1542
|
+
const baselineStatus = await gitStatusEntries(effectiveRoot);
|
|
1543
|
+
const baselinePaths = baselineStatus.ok
|
|
1544
|
+
? uniquePaths(baselineStatus.entries.map((entry) => entry.path))
|
|
1545
|
+
.filter((item) => !matchesPattern(item, LOOP_INTERNAL_STATE_PATTERNS))
|
|
1546
|
+
: [];
|
|
1547
|
+
await updateLoopState(effectiveRoot, {
|
|
1548
|
+
currentTaskId: task.id,
|
|
1549
|
+
currentTaskHandle: task.taskHandle,
|
|
1550
|
+
currentTaskTitle: task.title,
|
|
1551
|
+
currentTaskBaselinePaths: baselinePaths,
|
|
1552
|
+
currentWorktreePath: workspaceInfo.path,
|
|
1553
|
+
currentBranch: workspaceInfo.branch,
|
|
1554
|
+
lastWorktreePath: workspaceInfo.path,
|
|
1555
|
+
lastBranch: workspaceInfo.branch,
|
|
1556
|
+
});
|
|
1007
1557
|
return {
|
|
1008
1558
|
ok: true,
|
|
1009
1559
|
action: 'loop-prompt',
|
|
1010
|
-
projectRoot,
|
|
1560
|
+
projectRoot: effectiveRoot,
|
|
1561
|
+
sourceProjectRoot: workspace.sourceProjectRoot,
|
|
1562
|
+
workspace: workspaceInfo,
|
|
1011
1563
|
agent,
|
|
1012
1564
|
task,
|
|
1013
1565
|
dependencyState: state,
|
|
1014
1566
|
prompt,
|
|
1015
|
-
promptPath: path.relative(
|
|
1567
|
+
promptPath: path.relative(effectiveRoot, promptPath),
|
|
1016
1568
|
invocation,
|
|
1017
1569
|
};
|
|
1018
1570
|
}
|
|
1019
1571
|
|
|
1020
1572
|
export async function verifyLoopWorkspace(projectRoot, options = {}) {
|
|
1021
|
-
await
|
|
1022
|
-
|
|
1573
|
+
const workspace = await resolveLoopWorkspace(projectRoot, options);
|
|
1574
|
+
if (!workspace.ok) {
|
|
1575
|
+
return {
|
|
1576
|
+
ok: false,
|
|
1577
|
+
action: 'loop-verify',
|
|
1578
|
+
projectRoot: path.resolve(projectRoot),
|
|
1579
|
+
errors: workspace.errors,
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
const effectiveRoot = workspace.projectRoot;
|
|
1583
|
+
const featureList = await readFeatureList(effectiveRoot);
|
|
1023
1584
|
if (!featureList) {
|
|
1024
1585
|
throw new Error('Loop feature list is missing. Run openprd loop . --plan --change <id>.');
|
|
1025
1586
|
}
|
|
1026
1587
|
const { task, dependencyState: state } = nextLoopTask(featureList, options.item);
|
|
1588
|
+
const workspaceInfo = {
|
|
1589
|
+
path: workspace.git?.worktreePath ?? path.resolve(effectiveRoot),
|
|
1590
|
+
branch: workspace.git?.branch ?? null,
|
|
1591
|
+
created: workspace.created,
|
|
1592
|
+
syncedPaths: workspace.syncedPaths,
|
|
1593
|
+
};
|
|
1027
1594
|
if (!task) {
|
|
1028
1595
|
const summary = buildLoopSummary(featureList);
|
|
1029
1596
|
if (summary.total > 0 && summary.done === summary.total) {
|
|
1030
1597
|
return {
|
|
1031
1598
|
ok: true,
|
|
1032
1599
|
action: 'loop-verify',
|
|
1033
|
-
projectRoot,
|
|
1600
|
+
projectRoot: effectiveRoot,
|
|
1601
|
+
sourceProjectRoot: workspace.sourceProjectRoot,
|
|
1602
|
+
workspace: workspaceInfo,
|
|
1034
1603
|
summary,
|
|
1035
1604
|
errors: [],
|
|
1036
1605
|
checks: ['所有 OpenPrd loop 任务均已完成。'],
|
|
1037
1606
|
};
|
|
1038
1607
|
}
|
|
1039
|
-
return {
|
|
1608
|
+
return {
|
|
1609
|
+
ok: false,
|
|
1610
|
+
action: 'loop-verify',
|
|
1611
|
+
projectRoot: effectiveRoot,
|
|
1612
|
+
sourceProjectRoot: workspace.sourceProjectRoot,
|
|
1613
|
+
workspace: workspaceInfo,
|
|
1614
|
+
summary,
|
|
1615
|
+
errors: ['当前没有可执行的 loop 任务。'],
|
|
1616
|
+
};
|
|
1040
1617
|
}
|
|
1041
1618
|
if (!state.ready) {
|
|
1042
|
-
return {
|
|
1619
|
+
return {
|
|
1620
|
+
ok: false,
|
|
1621
|
+
action: 'loop-verify',
|
|
1622
|
+
projectRoot: effectiveRoot,
|
|
1623
|
+
sourceProjectRoot: workspace.sourceProjectRoot,
|
|
1624
|
+
workspace: workspaceInfo,
|
|
1625
|
+
task,
|
|
1626
|
+
dependencyState: state,
|
|
1627
|
+
errors: [`任务 ${task.id} 尚未就绪。`],
|
|
1628
|
+
};
|
|
1043
1629
|
}
|
|
1044
|
-
const verify = await verifyOpenSpecTaskWorkspace(
|
|
1630
|
+
const verify = await verifyOpenSpecTaskWorkspace(effectiveRoot, { change: task.changeId, item: task.sourceTaskId });
|
|
1045
1631
|
return {
|
|
1046
1632
|
ok: verify.ok,
|
|
1047
1633
|
action: 'loop-verify',
|
|
1048
|
-
projectRoot,
|
|
1634
|
+
projectRoot: effectiveRoot,
|
|
1635
|
+
sourceProjectRoot: workspace.sourceProjectRoot,
|
|
1636
|
+
workspace: workspaceInfo,
|
|
1049
1637
|
task,
|
|
1050
1638
|
dependencyState: state,
|
|
1051
1639
|
verify,
|
|
@@ -1064,32 +1652,57 @@ function updateTask(featureList, taskId, patch) {
|
|
|
1064
1652
|
}
|
|
1065
1653
|
|
|
1066
1654
|
export async function finishLoopWorkspace(projectRoot, options = {}) {
|
|
1067
|
-
await
|
|
1068
|
-
|
|
1655
|
+
const workspace = await resolveLoopWorkspace(projectRoot, options);
|
|
1656
|
+
if (!workspace.ok) {
|
|
1657
|
+
return {
|
|
1658
|
+
ok: false,
|
|
1659
|
+
action: 'loop-finish',
|
|
1660
|
+
projectRoot: path.resolve(projectRoot),
|
|
1661
|
+
errors: workspace.errors,
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
const effectiveRoot = workspace.projectRoot;
|
|
1665
|
+
const featureList = await readFeatureList(effectiveRoot);
|
|
1069
1666
|
if (!featureList) {
|
|
1070
1667
|
throw new Error('Loop feature list is missing. Run openprd loop . --plan --change <id>.');
|
|
1071
1668
|
}
|
|
1072
1669
|
const { task, dependencyState: state } = nextLoopTask(featureList, options.item);
|
|
1670
|
+
const workspaceInfo = {
|
|
1671
|
+
path: workspace.git?.worktreePath ?? path.resolve(effectiveRoot),
|
|
1672
|
+
branch: workspace.git?.branch ?? null,
|
|
1673
|
+
created: workspace.created,
|
|
1674
|
+
syncedPaths: workspace.syncedPaths,
|
|
1675
|
+
};
|
|
1073
1676
|
if (!task) {
|
|
1074
|
-
return { ok: false, action: 'loop-finish', projectRoot, errors: ['当前没有可执行的 loop 任务。'] };
|
|
1677
|
+
return { ok: false, action: 'loop-finish', projectRoot: effectiveRoot, sourceProjectRoot: workspace.sourceProjectRoot, workspace: workspaceInfo, errors: ['当前没有可执行的 loop 任务。'] };
|
|
1075
1678
|
}
|
|
1076
1679
|
if (!state.ready) {
|
|
1077
|
-
return { ok: false, action: 'loop-finish', projectRoot, task, dependencyState: state, errors: [`任务 ${task.id} 尚未就绪。`] };
|
|
1680
|
+
return { ok: false, action: 'loop-finish', projectRoot: effectiveRoot, sourceProjectRoot: workspace.sourceProjectRoot, workspace: workspaceInfo, task, dependencyState: state, errors: [`任务 ${task.id} 尚未就绪。`] };
|
|
1078
1681
|
}
|
|
1079
1682
|
|
|
1080
|
-
const
|
|
1683
|
+
const loopState = await readLoopState(effectiveRoot);
|
|
1684
|
+
const baselinePaths = loopState?.currentTaskId === task.id
|
|
1685
|
+
? uniquePaths(loopState.currentTaskBaselinePaths ?? [])
|
|
1686
|
+
: [];
|
|
1687
|
+
|
|
1688
|
+
const beforeChange = await validateOpenSpecChangeWorkspace(effectiveRoot, {
|
|
1689
|
+
change: task.changeId,
|
|
1690
|
+
folderManualModuleName: resolveLoopFolderManualModuleName(workspace),
|
|
1691
|
+
});
|
|
1081
1692
|
if (!beforeChange.ok) {
|
|
1082
1693
|
return {
|
|
1083
1694
|
ok: false,
|
|
1084
1695
|
action: 'loop-finish',
|
|
1085
|
-
projectRoot,
|
|
1696
|
+
projectRoot: effectiveRoot,
|
|
1697
|
+
sourceProjectRoot: workspace.sourceProjectRoot,
|
|
1698
|
+
workspace: workspaceInfo,
|
|
1086
1699
|
task,
|
|
1087
1700
|
change: beforeChange,
|
|
1088
1701
|
errors: beforeChange.errors,
|
|
1089
1702
|
};
|
|
1090
1703
|
}
|
|
1091
1704
|
|
|
1092
|
-
const verification = await verifyOpenSpecTaskWorkspace(
|
|
1705
|
+
const verification = await verifyOpenSpecTaskWorkspace(effectiveRoot, {
|
|
1093
1706
|
change: task.changeId,
|
|
1094
1707
|
item: task.sourceTaskId,
|
|
1095
1708
|
evidence: options.evidence,
|
|
@@ -1098,8 +1711,8 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
|
|
|
1098
1711
|
if (!verification.ok) {
|
|
1099
1712
|
const failureReason = verification.verification?.stderr || verification.verification?.stdout || '自测失败';
|
|
1100
1713
|
const failedList = updateTask(featureList, task.id, { status: 'failed', lastError: failureReason });
|
|
1101
|
-
await writeFeatureList(
|
|
1102
|
-
await appendFailedApproach(
|
|
1714
|
+
await writeFeatureList(effectiveRoot, failedList);
|
|
1715
|
+
await appendFailedApproach(effectiveRoot, {
|
|
1103
1716
|
task,
|
|
1104
1717
|
stage: 'task-verify',
|
|
1105
1718
|
reason: failureReason,
|
|
@@ -1110,7 +1723,9 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
|
|
|
1110
1723
|
return {
|
|
1111
1724
|
ok: false,
|
|
1112
1725
|
action: 'loop-finish',
|
|
1113
|
-
projectRoot,
|
|
1726
|
+
projectRoot: effectiveRoot,
|
|
1727
|
+
sourceProjectRoot: workspace.sourceProjectRoot,
|
|
1728
|
+
workspace: workspaceInfo,
|
|
1114
1729
|
task,
|
|
1115
1730
|
verification,
|
|
1116
1731
|
errors: [failureReason || `任务 ${task.id} 自测失败。`],
|
|
@@ -1120,8 +1735,8 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
|
|
|
1120
1735
|
const finishEvidence = validateLoopFinishEvidence(task, options);
|
|
1121
1736
|
if (!finishEvidence.ok) {
|
|
1122
1737
|
const failedList = updateTask(featureList, task.id, { status: 'failed', lastError: finishEvidence.error });
|
|
1123
|
-
await writeFeatureList(
|
|
1124
|
-
await appendFailedApproach(
|
|
1738
|
+
await writeFeatureList(effectiveRoot, failedList);
|
|
1739
|
+
await appendFailedApproach(effectiveRoot, {
|
|
1125
1740
|
task,
|
|
1126
1741
|
stage: 'finish-evidence',
|
|
1127
1742
|
reason: finishEvidence.error,
|
|
@@ -1132,14 +1747,16 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
|
|
|
1132
1747
|
return {
|
|
1133
1748
|
ok: false,
|
|
1134
1749
|
action: 'loop-finish',
|
|
1135
|
-
projectRoot,
|
|
1750
|
+
projectRoot: effectiveRoot,
|
|
1751
|
+
sourceProjectRoot: workspace.sourceProjectRoot,
|
|
1752
|
+
workspace: workspaceInfo,
|
|
1136
1753
|
task,
|
|
1137
1754
|
verification,
|
|
1138
1755
|
errors: [finishEvidence.error],
|
|
1139
1756
|
};
|
|
1140
1757
|
}
|
|
1141
1758
|
|
|
1142
|
-
const advanced = await advanceOpenSpecTaskWorkspace(
|
|
1759
|
+
const advanced = await advanceOpenSpecTaskWorkspace(effectiveRoot, {
|
|
1143
1760
|
change: task.changeId,
|
|
1144
1761
|
item: task.sourceTaskId,
|
|
1145
1762
|
verify: false,
|
|
@@ -1149,8 +1766,8 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
|
|
|
1149
1766
|
if (!advanced.ok) {
|
|
1150
1767
|
const failureReason = advanced.errors?.[0] ?? `任务 ${task.id} 标记完成失败。`;
|
|
1151
1768
|
const failedList = updateTask(featureList, task.id, { status: 'failed', lastError: failureReason });
|
|
1152
|
-
await writeFeatureList(
|
|
1153
|
-
await appendFailedApproach(
|
|
1769
|
+
await writeFeatureList(effectiveRoot, failedList);
|
|
1770
|
+
await appendFailedApproach(effectiveRoot, {
|
|
1154
1771
|
task,
|
|
1155
1772
|
stage: 'task-advance',
|
|
1156
1773
|
reason: failureReason,
|
|
@@ -1161,7 +1778,9 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
|
|
|
1161
1778
|
return {
|
|
1162
1779
|
ok: false,
|
|
1163
1780
|
action: 'loop-finish',
|
|
1164
|
-
projectRoot,
|
|
1781
|
+
projectRoot: effectiveRoot,
|
|
1782
|
+
sourceProjectRoot: workspace.sourceProjectRoot,
|
|
1783
|
+
workspace: workspaceInfo,
|
|
1165
1784
|
task,
|
|
1166
1785
|
verification,
|
|
1167
1786
|
advanced,
|
|
@@ -1175,23 +1794,54 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
|
|
|
1175
1794
|
};
|
|
1176
1795
|
|
|
1177
1796
|
let commit = null;
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
agent: options.agent ?? 'codex',
|
|
1181
|
-
advanced: {
|
|
1182
|
-
...finishResult,
|
|
1183
|
-
evidence: options.evidence ?? null,
|
|
1184
|
-
notes: options.notes ?? null,
|
|
1185
|
-
},
|
|
1186
|
-
change,
|
|
1187
|
-
});
|
|
1797
|
+
let projectRelease = null;
|
|
1798
|
+
let pendingRelease = null;
|
|
1188
1799
|
if (options.commit) {
|
|
1189
|
-
|
|
1800
|
+
const preparedCommit = await planGitCommit(effectiveRoot, {
|
|
1801
|
+
task,
|
|
1802
|
+
baselinePaths,
|
|
1803
|
+
allowDirtyMain: Boolean(options.allowDirtyMain),
|
|
1804
|
+
git: workspace.git,
|
|
1805
|
+
});
|
|
1806
|
+
if (!preparedCommit.ok) {
|
|
1807
|
+
return {
|
|
1808
|
+
ok: false,
|
|
1809
|
+
action: 'loop-finish',
|
|
1810
|
+
projectRoot: effectiveRoot,
|
|
1811
|
+
sourceProjectRoot: workspace.sourceProjectRoot,
|
|
1812
|
+
workspace: workspaceInfo,
|
|
1813
|
+
task,
|
|
1814
|
+
advanced: finishResult,
|
|
1815
|
+
change,
|
|
1816
|
+
commit: preparedCommit,
|
|
1817
|
+
errors: [preparedCommit.message],
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
if (!preparedCommit.skipped) {
|
|
1821
|
+
pendingRelease = await updateReleaseLedgerAfterFinish(effectiveRoot, task, null).catch((error) => ({
|
|
1822
|
+
skipped: true,
|
|
1823
|
+
warnings: [error instanceof Error ? error.message : String(error)],
|
|
1824
|
+
tag: null,
|
|
1825
|
+
version: null,
|
|
1826
|
+
}));
|
|
1827
|
+
}
|
|
1828
|
+
const alwaysIncludePaths = pendingRelease?.version && !pendingRelease.skipped
|
|
1829
|
+
? [RELEASE_LEDGER]
|
|
1830
|
+
: [];
|
|
1831
|
+
commit = await gitCommit(effectiveRoot, options.message ?? task.commitMessage, {
|
|
1832
|
+
task,
|
|
1833
|
+
baselinePaths,
|
|
1834
|
+
allowDirtyMain: Boolean(options.allowDirtyMain),
|
|
1835
|
+
git: preparedCommit.git ?? workspace.git,
|
|
1836
|
+
alwaysIncludePaths,
|
|
1837
|
+
});
|
|
1190
1838
|
if (!commit.ok) {
|
|
1191
1839
|
return {
|
|
1192
1840
|
ok: false,
|
|
1193
1841
|
action: 'loop-finish',
|
|
1194
|
-
projectRoot,
|
|
1842
|
+
projectRoot: effectiveRoot,
|
|
1843
|
+
sourceProjectRoot: workspace.sourceProjectRoot,
|
|
1844
|
+
workspace: workspaceInfo,
|
|
1195
1845
|
task,
|
|
1196
1846
|
advanced: finishResult,
|
|
1197
1847
|
change,
|
|
@@ -1199,17 +1849,32 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
|
|
|
1199
1849
|
errors: [commit.message],
|
|
1200
1850
|
};
|
|
1201
1851
|
}
|
|
1852
|
+
projectRelease = pendingRelease;
|
|
1853
|
+
if (projectRelease?.version && !projectRelease.skipped && !commit.skipped) {
|
|
1854
|
+
const tag = await syncLocalVersionTag(effectiveRoot, projectRelease.version, commit.sha);
|
|
1855
|
+
projectRelease = {
|
|
1856
|
+
...projectRelease,
|
|
1857
|
+
tag,
|
|
1858
|
+
warnings: [...(projectRelease.warnings ?? []), ...(tag?.warning ? [tag.warning] : [])],
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1202
1861
|
}
|
|
1203
1862
|
|
|
1204
|
-
const
|
|
1205
|
-
projectRoot,
|
|
1863
|
+
const testReport = await writeTestReport(effectiveRoot, {
|
|
1206
1864
|
task,
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1865
|
+
agent: options.agent ?? 'codex',
|
|
1866
|
+
advanced: {
|
|
1867
|
+
...finishResult,
|
|
1868
|
+
evidence: options.evidence ?? null,
|
|
1869
|
+
notes: options.notes ?? null,
|
|
1870
|
+
},
|
|
1871
|
+
change,
|
|
1872
|
+
workspace: {
|
|
1873
|
+
path: commit?.worktreePath ?? workspaceInfo.path,
|
|
1874
|
+
branch: commit?.branch ?? workspaceInfo.branch,
|
|
1875
|
+
},
|
|
1876
|
+
commit,
|
|
1877
|
+
});
|
|
1213
1878
|
|
|
1214
1879
|
const updatedList = updateTask(featureList, task.id, {
|
|
1215
1880
|
status: 'done',
|
|
@@ -1221,7 +1886,7 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
|
|
|
1221
1886
|
const nextAfterFinish = nextLoopTask(updatedList).task;
|
|
1222
1887
|
let quality = null;
|
|
1223
1888
|
if (!nextAfterFinish) {
|
|
1224
|
-
quality = await verifyQualityWorkspace(
|
|
1889
|
+
quality = await verifyQualityWorkspace(effectiveRoot, { strict: true }).catch((error) => ({
|
|
1225
1890
|
ok: false,
|
|
1226
1891
|
action: 'quality-verify',
|
|
1227
1892
|
errors: [error instanceof Error ? error.message : String(error)],
|
|
@@ -1232,15 +1897,15 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
|
|
|
1232
1897
|
const qualityError = [
|
|
1233
1898
|
'Final EVO quality gate is not production-ready.',
|
|
1234
1899
|
attentionGates.length > 0 ? `Attention gates: ${attentionGates.join(', ')}` : null,
|
|
1235
|
-
quality.htmlPath ? `HTML report: ${path.relative(
|
|
1900
|
+
quality.htmlPath ? `HTML report: ${path.relative(effectiveRoot, quality.htmlPath)}` : null,
|
|
1236
1901
|
].filter(Boolean).join(' ');
|
|
1237
1902
|
const failedList = updateTask(featureList, task.id, {
|
|
1238
1903
|
status: 'failed',
|
|
1239
1904
|
lastError: qualityError,
|
|
1240
1905
|
lastTestReport: testReport.markdownPath,
|
|
1241
1906
|
});
|
|
1242
|
-
await writeFeatureList(
|
|
1243
|
-
await appendFailedApproach(
|
|
1907
|
+
await writeFeatureList(effectiveRoot, failedList);
|
|
1908
|
+
await appendFailedApproach(effectiveRoot, {
|
|
1244
1909
|
task,
|
|
1245
1910
|
stage: 'final-quality',
|
|
1246
1911
|
reason: qualityError,
|
|
@@ -1248,10 +1913,20 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
|
|
|
1248
1913
|
notes: options.notes ?? null,
|
|
1249
1914
|
evidence: options.evidence ?? null,
|
|
1250
1915
|
});
|
|
1916
|
+
const operationalCleanup = options.commit
|
|
1917
|
+
? await cleanupLoopOperationalArtifacts(effectiveRoot).catch((error) => ({
|
|
1918
|
+
ok: false,
|
|
1919
|
+
restoredPaths: [],
|
|
1920
|
+
removedPaths: [],
|
|
1921
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1922
|
+
}))
|
|
1923
|
+
: null;
|
|
1251
1924
|
return {
|
|
1252
1925
|
ok: false,
|
|
1253
1926
|
action: 'loop-finish',
|
|
1254
|
-
projectRoot,
|
|
1927
|
+
projectRoot: effectiveRoot,
|
|
1928
|
+
sourceProjectRoot: workspace.sourceProjectRoot,
|
|
1929
|
+
workspace: workspaceInfo,
|
|
1255
1930
|
task,
|
|
1256
1931
|
advanced: finishResult,
|
|
1257
1932
|
change,
|
|
@@ -1259,14 +1934,15 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
|
|
|
1259
1934
|
testReport: testReport.markdownPath,
|
|
1260
1935
|
regressionHtml: testReport.htmlPath,
|
|
1261
1936
|
quality,
|
|
1937
|
+
operationalCleanup,
|
|
1262
1938
|
errors: [qualityError, ...(quality.errors ?? [])],
|
|
1263
1939
|
};
|
|
1264
1940
|
}
|
|
1265
1941
|
}
|
|
1266
|
-
await writeFeatureList(
|
|
1942
|
+
await writeFeatureList(effectiveRoot, updatedList);
|
|
1267
1943
|
let learningReview = null;
|
|
1268
1944
|
try {
|
|
1269
|
-
learningReview = await generateLearningReviewWorkspace(
|
|
1945
|
+
learningReview = await generateLearningReviewWorkspace(effectiveRoot, {
|
|
1270
1946
|
trigger: 'loop-finish',
|
|
1271
1947
|
topic: `${task.id} ${task.title}`,
|
|
1272
1948
|
sourceScope: 'loop',
|
|
@@ -1292,8 +1968,8 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
|
|
|
1292
1968
|
? [
|
|
1293
1969
|
`复盘学习包: ${learningReview.packageId}。`,
|
|
1294
1970
|
`复盘写作状态: ${learningReview.packageMeta?.authoringStatus ?? 'unknown'}。`,
|
|
1295
|
-
`学习阅读器: ${path.relative(
|
|
1296
|
-
...(learningReview.packagePaths?.agentPrompt ? [`Agent 写作提示: ${path.relative(
|
|
1971
|
+
`学习阅读器: ${path.relative(effectiveRoot, learningReview.packagePaths.readerHtml)}。`,
|
|
1972
|
+
...(learningReview.packagePaths?.agentPrompt ? [`Agent 写作提示: ${path.relative(effectiveRoot, learningReview.packagePaths.agentPrompt)}。`] : []),
|
|
1297
1973
|
]
|
|
1298
1974
|
: [`复盘学习: 生成失败 (${learningReview?.errors?.[0] ?? 'unknown'})。`];
|
|
1299
1975
|
const knowledgeSignal = {
|
|
@@ -1303,11 +1979,11 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
|
|
|
1303
1979
|
attentionGates: quality?.report?.readiness?.attentionGates ?? [],
|
|
1304
1980
|
summary: `loop finish ${task.id}: ${task.title}`,
|
|
1305
1981
|
};
|
|
1306
|
-
await recordKnowledgeReviewSignal(
|
|
1307
|
-
const knowledgeReviewSource = (await exists(cjoin(
|
|
1982
|
+
await recordKnowledgeReviewSignal(effectiveRoot, knowledgeSignal).catch(() => null);
|
|
1983
|
+
const knowledgeReviewSource = (await exists(cjoin(effectiveRoot, OPENPRD_HARNESS_TURN_STATE)))
|
|
1308
1984
|
? OPENPRD_HARNESS_TURN_STATE
|
|
1309
1985
|
: (quality?.reportPath ?? null);
|
|
1310
|
-
const knowledgeReview = await reviewKnowledgeWorkspace(
|
|
1986
|
+
const knowledgeReview = await reviewKnowledgeWorkspace(effectiveRoot, {
|
|
1311
1987
|
from: knowledgeReviewSource,
|
|
1312
1988
|
signal: knowledgeSignal,
|
|
1313
1989
|
}).catch((error) => ({
|
|
@@ -1316,26 +1992,28 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
|
|
|
1316
1992
|
skipped: false,
|
|
1317
1993
|
errors: [error instanceof Error ? error.message : String(error)],
|
|
1318
1994
|
}));
|
|
1319
|
-
await appendText(harnessPath(
|
|
1995
|
+
await appendText(harnessPath(effectiveRoot, LOOP_PROGRESS), renderProgressEntry(timestamp(), [
|
|
1320
1996
|
`已完成 ${task.id}: ${task.title}。`,
|
|
1321
1997
|
`自测: ${finishResult.verification?.ok ? '通过' : '未运行'}。`,
|
|
1322
1998
|
task.oracle ? `对照基准: ${task.oracle}。` : null,
|
|
1999
|
+
`工作区: ${workspaceInfo.path}。`,
|
|
2000
|
+
`分支: ${commit?.branch ?? workspaceInfo.branch ?? '未命名分支或 detached HEAD'}。`,
|
|
1323
2001
|
`测试报告: ${testReport.markdownPath}。`,
|
|
1324
2002
|
`HTML 回归报告: ${testReport.htmlPath}。`,
|
|
1325
2003
|
...(quality ? [
|
|
1326
2004
|
`最终 EVO: ${quality.report?.readiness?.productionReady ? 'production-ready' : 'needs-attention'}。`,
|
|
1327
|
-
...(quality.htmlPath ? [`EVO 报告: ${path.relative(
|
|
2005
|
+
...(quality.htmlPath ? [`EVO 报告: ${path.relative(effectiveRoot, quality.htmlPath)}。`] : []),
|
|
1328
2006
|
] : []),
|
|
1329
2007
|
...learningProgress,
|
|
1330
2008
|
knowledgeReview?.skipped
|
|
1331
2009
|
? null
|
|
1332
|
-
: `项目经验草案: ${path.relative(
|
|
2010
|
+
: `项目经验草案: ${path.relative(effectiveRoot, knowledgeReview.files?.draftSkill ?? knowledgeReview.files?.candidateDir ?? '') || '已生成'}。`,
|
|
1333
2011
|
commit ? `Commit: ${commit.skipped ? '跳过' : commit.sha}` : 'Commit: 未请求。',
|
|
1334
2012
|
projectRelease?.version ? `项目版本: ${projectRelease.version}。` : null,
|
|
1335
2013
|
projectRelease?.tag?.tagName ? `版本 tag: ${projectRelease.tag.tagName}${projectRelease.tag.localSha ? ` -> ${projectRelease.tag.localSha}` : ''}。` : null,
|
|
1336
2014
|
...(projectRelease?.warnings ?? []).map((warning) => `版本轨道: ${warning}`),
|
|
1337
2015
|
]));
|
|
1338
|
-
await appendJsonl(harnessPath(
|
|
2016
|
+
await appendJsonl(harnessPath(effectiveRoot, LOOP_SESSIONS), {
|
|
1339
2017
|
version: 1,
|
|
1340
2018
|
at: timestamp(),
|
|
1341
2019
|
action: 'finish',
|
|
@@ -1345,7 +2023,18 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
|
|
|
1345
2023
|
changeId: task.changeId,
|
|
1346
2024
|
ok: true,
|
|
1347
2025
|
oracle: task.oracle ?? null,
|
|
1348
|
-
|
|
2026
|
+
worktreePath: commit?.worktreePath ?? workspaceInfo.path,
|
|
2027
|
+
branch: commit?.branch ?? workspaceInfo.branch,
|
|
2028
|
+
commitSha: commit?.sha ?? null,
|
|
2029
|
+
commit: commit ? {
|
|
2030
|
+
ok: commit.ok,
|
|
2031
|
+
skipped: commit.skipped,
|
|
2032
|
+
sha: commit.sha ?? null,
|
|
2033
|
+
branch: commit.branch ?? null,
|
|
2034
|
+
worktreePath: commit.worktreePath ?? null,
|
|
2035
|
+
stagedPaths: commit.commitPlan?.stagedPaths ?? [],
|
|
2036
|
+
excludedPaths: commit.commitPlan?.excludedPaths ?? [],
|
|
2037
|
+
} : null,
|
|
1349
2038
|
projectRelease: projectRelease ?? null,
|
|
1350
2039
|
testReport: testReport.markdownPath,
|
|
1351
2040
|
regressionHtml: testReport.htmlPath,
|
|
@@ -1386,16 +2075,35 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
|
|
|
1386
2075
|
errors: learningReview?.errors ?? [],
|
|
1387
2076
|
},
|
|
1388
2077
|
});
|
|
1389
|
-
await updateLoopState(
|
|
2078
|
+
await updateLoopState(effectiveRoot, {
|
|
1390
2079
|
currentTaskId: nextAfterFinish?.id ?? null,
|
|
1391
2080
|
currentTaskHandle: nextAfterFinish?.taskHandle ?? null,
|
|
1392
2081
|
currentTaskTitle: nextAfterFinish?.title ?? null,
|
|
2082
|
+
currentTaskBaselinePaths: [],
|
|
2083
|
+
currentWorktreePath: commit?.worktreePath ?? workspaceInfo.path,
|
|
2084
|
+
currentBranch: commit?.branch ?? workspaceInfo.branch,
|
|
2085
|
+
lastWorktreePath: commit?.worktreePath ?? workspaceInfo.path,
|
|
2086
|
+
lastBranch: commit?.branch ?? workspaceInfo.branch,
|
|
2087
|
+
lastCommitSha: commit?.sha ?? null,
|
|
1393
2088
|
completedTaskIds: updatedList.tasks.filter((item) => item.status === 'done').map((item) => item.id),
|
|
1394
2089
|
});
|
|
2090
|
+
const operationalCleanup = options.commit
|
|
2091
|
+
? await cleanupLoopOperationalArtifacts(effectiveRoot).catch((error) => ({
|
|
2092
|
+
ok: false,
|
|
2093
|
+
restoredPaths: [],
|
|
2094
|
+
removedPaths: [],
|
|
2095
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2096
|
+
}))
|
|
2097
|
+
: null;
|
|
1395
2098
|
return {
|
|
1396
2099
|
ok: true,
|
|
1397
2100
|
action: 'loop-finish',
|
|
1398
|
-
projectRoot,
|
|
2101
|
+
projectRoot: effectiveRoot,
|
|
2102
|
+
sourceProjectRoot: workspace.sourceProjectRoot,
|
|
2103
|
+
workspace: {
|
|
2104
|
+
...workspaceInfo,
|
|
2105
|
+
branch: commit?.branch ?? workspaceInfo.branch,
|
|
2106
|
+
},
|
|
1399
2107
|
task,
|
|
1400
2108
|
advanced,
|
|
1401
2109
|
change,
|
|
@@ -1404,6 +2112,7 @@ export async function finishLoopWorkspace(projectRoot, options = {}) {
|
|
|
1404
2112
|
testReport: testReport.markdownPath,
|
|
1405
2113
|
regressionHtml: testReport.htmlPath,
|
|
1406
2114
|
quality,
|
|
2115
|
+
operationalCleanup,
|
|
1407
2116
|
knowledgeReview,
|
|
1408
2117
|
learningReview,
|
|
1409
2118
|
summary: buildLoopSummary(updatedList),
|
|
@@ -1416,7 +2125,8 @@ export async function runLoopWorkspace(projectRoot, options = {}) {
|
|
|
1416
2125
|
const promptResult = await promptLoopWorkspace(projectRoot, { ...options, agent, mode: 'loop-run' });
|
|
1417
2126
|
if (!promptResult.ok) return promptResult;
|
|
1418
2127
|
|
|
1419
|
-
const
|
|
2128
|
+
const effectiveRoot = promptResult.projectRoot;
|
|
2129
|
+
const absolutePromptPath = harnessPath(effectiveRoot, promptResult.promptPath);
|
|
1420
2130
|
const prompt = await readText(absolutePromptPath);
|
|
1421
2131
|
const invocation = options.agentCommand
|
|
1422
2132
|
? {
|
|
@@ -1426,10 +2136,10 @@ export async function runLoopWorkspace(projectRoot, options = {}) {
|
|
|
1426
2136
|
display: `${options.agentCommand} < ${shellJoin([promptResult.promptPath])}`,
|
|
1427
2137
|
shell: true,
|
|
1428
2138
|
}
|
|
1429
|
-
: defaultAgentInvocation(agent,
|
|
2139
|
+
: defaultAgentInvocation(agent, effectiveRoot, promptResult.promptPath);
|
|
1430
2140
|
const codexPreflight = agent === 'codex' && !options.agentCommand && !options.dryRun
|
|
1431
2141
|
? await ensureCodexCliReady({
|
|
1432
|
-
cwd:
|
|
2142
|
+
cwd: effectiveRoot,
|
|
1433
2143
|
repair: Boolean(options.repairAgent),
|
|
1434
2144
|
runCommand: options.codexRunCommand,
|
|
1435
2145
|
packageManager: options.packageManager,
|
|
@@ -1437,7 +2147,7 @@ export async function runLoopWorkspace(projectRoot, options = {}) {
|
|
|
1437
2147
|
: null;
|
|
1438
2148
|
|
|
1439
2149
|
if (codexPreflight && !codexPreflight.ok) {
|
|
1440
|
-
await appendJsonl(harnessPath(
|
|
2150
|
+
await appendJsonl(harnessPath(effectiveRoot, LOOP_SESSIONS), {
|
|
1441
2151
|
version: 1,
|
|
1442
2152
|
at: timestamp(),
|
|
1443
2153
|
action: codexPreflight.repairAttempted ? 'agent-preflight-repair-failed' : 'agent-preflight-failed',
|
|
@@ -1446,6 +2156,8 @@ export async function runLoopWorkspace(projectRoot, options = {}) {
|
|
|
1446
2156
|
taskHandle: promptResult.task.taskHandle,
|
|
1447
2157
|
taskTitle: promptResult.task.title,
|
|
1448
2158
|
ok: false,
|
|
2159
|
+
worktreePath: promptResult.workspace?.path ?? effectiveRoot,
|
|
2160
|
+
branch: promptResult.workspace?.branch ?? null,
|
|
1449
2161
|
preflight: {
|
|
1450
2162
|
ok: codexPreflight.preflight.ok,
|
|
1451
2163
|
diagnosticType: codexPreflight.preflight.diagnostic?.type ?? null,
|
|
@@ -1456,7 +2168,9 @@ export async function runLoopWorkspace(projectRoot, options = {}) {
|
|
|
1456
2168
|
return {
|
|
1457
2169
|
ok: false,
|
|
1458
2170
|
action: 'loop-run',
|
|
1459
|
-
projectRoot,
|
|
2171
|
+
projectRoot: effectiveRoot,
|
|
2172
|
+
sourceProjectRoot: promptResult.sourceProjectRoot,
|
|
2173
|
+
workspace: promptResult.workspace,
|
|
1460
2174
|
agent,
|
|
1461
2175
|
task: promptResult.task,
|
|
1462
2176
|
promptPath: promptResult.promptPath,
|
|
@@ -1480,17 +2194,24 @@ export async function runLoopWorkspace(projectRoot, options = {}) {
|
|
|
1480
2194
|
changeId: promptResult.task.changeId,
|
|
1481
2195
|
promptPath: promptResult.promptPath,
|
|
1482
2196
|
invocation: invocation.display,
|
|
2197
|
+
worktreePath: promptResult.workspace?.path ?? effectiveRoot,
|
|
2198
|
+
branch: promptResult.workspace?.branch ?? null,
|
|
2199
|
+
createdWorktree: Boolean(promptResult.workspace?.created),
|
|
1483
2200
|
preflight: codexPreflight ? {
|
|
1484
2201
|
ok: codexPreflight.preflight.ok,
|
|
1485
2202
|
command: codexPreflight.preflight.command.display,
|
|
1486
2203
|
repairAttempted: codexPreflight.repairAttempted,
|
|
1487
2204
|
} : null,
|
|
1488
2205
|
};
|
|
1489
|
-
await appendJsonl(harnessPath(
|
|
1490
|
-
await updateLoopState(
|
|
2206
|
+
await appendJsonl(harnessPath(effectiveRoot, LOOP_SESSIONS), sessionEvent);
|
|
2207
|
+
await updateLoopState(effectiveRoot, {
|
|
1491
2208
|
currentTaskId: promptResult.task.id,
|
|
1492
2209
|
currentTaskHandle: promptResult.task.taskHandle,
|
|
1493
2210
|
currentTaskTitle: promptResult.task.title,
|
|
2211
|
+
currentWorktreePath: promptResult.workspace?.path ?? effectiveRoot,
|
|
2212
|
+
currentBranch: promptResult.workspace?.branch ?? null,
|
|
2213
|
+
lastWorktreePath: promptResult.workspace?.path ?? effectiveRoot,
|
|
2214
|
+
lastBranch: promptResult.workspace?.branch ?? null,
|
|
1494
2215
|
lastAgent: agent,
|
|
1495
2216
|
lastSessionAt: sessionEvent.at,
|
|
1496
2217
|
});
|
|
@@ -1500,7 +2221,9 @@ export async function runLoopWorkspace(projectRoot, options = {}) {
|
|
|
1500
2221
|
ok: true,
|
|
1501
2222
|
action: 'loop-run',
|
|
1502
2223
|
dryRun: true,
|
|
1503
|
-
projectRoot,
|
|
2224
|
+
projectRoot: effectiveRoot,
|
|
2225
|
+
sourceProjectRoot: promptResult.sourceProjectRoot,
|
|
2226
|
+
workspace: promptResult.workspace,
|
|
1504
2227
|
agent,
|
|
1505
2228
|
task: promptResult.task,
|
|
1506
2229
|
promptPath: promptResult.promptPath,
|
|
@@ -1513,9 +2236,9 @@ export async function runLoopWorkspace(projectRoot, options = {}) {
|
|
|
1513
2236
|
|
|
1514
2237
|
const runAgentCommand = options.agentRunCommand ?? runCommand;
|
|
1515
2238
|
const run = invocation.shell
|
|
1516
|
-
? await runAgentCommand(invocation.command, [], { cwd:
|
|
1517
|
-
: await runAgentCommand(invocation.command, invocation.args, { cwd:
|
|
1518
|
-
await appendJsonl(harnessPath(
|
|
2239
|
+
? await runAgentCommand(invocation.command, [], { cwd: effectiveRoot, shell: true, stdin: prompt })
|
|
2240
|
+
: await runAgentCommand(invocation.command, invocation.args, { cwd: effectiveRoot, stdin: prompt });
|
|
2241
|
+
await appendJsonl(harnessPath(effectiveRoot, LOOP_SESSIONS), {
|
|
1519
2242
|
version: 1,
|
|
1520
2243
|
at: timestamp(),
|
|
1521
2244
|
action: 'agent-exit',
|
|
@@ -1525,31 +2248,46 @@ export async function runLoopWorkspace(projectRoot, options = {}) {
|
|
|
1525
2248
|
taskTitle: promptResult.task.title,
|
|
1526
2249
|
ok: run.ok,
|
|
1527
2250
|
status: run.status,
|
|
2251
|
+
worktreePath: promptResult.workspace?.path ?? effectiveRoot,
|
|
2252
|
+
branch: promptResult.workspace?.branch ?? null,
|
|
1528
2253
|
});
|
|
1529
2254
|
if (!run.ok) {
|
|
1530
2255
|
return {
|
|
1531
2256
|
ok: false,
|
|
1532
2257
|
action: 'loop-run',
|
|
1533
|
-
projectRoot,
|
|
2258
|
+
projectRoot: effectiveRoot,
|
|
2259
|
+
sourceProjectRoot: promptResult.sourceProjectRoot,
|
|
2260
|
+
workspace: promptResult.workspace,
|
|
1534
2261
|
agent,
|
|
1535
2262
|
task: promptResult.task,
|
|
2263
|
+
promptPath: promptResult.promptPath,
|
|
1536
2264
|
run,
|
|
1537
2265
|
errors: [run.stderr || run.stdout || 'Agent 命令执行失败。'],
|
|
1538
2266
|
};
|
|
1539
2267
|
}
|
|
1540
2268
|
|
|
1541
|
-
const
|
|
2269
|
+
const finishNotes = [options.notes, `Finished by openprd loop run --agent ${agent}.`]
|
|
2270
|
+
.filter(Boolean)
|
|
2271
|
+
.join('\n');
|
|
2272
|
+
const finish = await finishLoopWorkspace(effectiveRoot, {
|
|
1542
2273
|
item: promptResult.task.id,
|
|
1543
2274
|
commit: options.commit,
|
|
1544
2275
|
message: options.message ?? promptResult.task.commitMessage,
|
|
1545
|
-
notes:
|
|
2276
|
+
notes: finishNotes,
|
|
2277
|
+
evidence: options.evidence,
|
|
2278
|
+
agent,
|
|
2279
|
+
allowDirtyMain: Boolean(options.allowDirtyMain),
|
|
1546
2280
|
});
|
|
1547
2281
|
return {
|
|
1548
2282
|
ok: finish.ok,
|
|
1549
2283
|
action: 'loop-run',
|
|
1550
|
-
projectRoot,
|
|
2284
|
+
projectRoot: effectiveRoot,
|
|
2285
|
+
sourceProjectRoot: promptResult.sourceProjectRoot,
|
|
2286
|
+
workspace: promptResult.workspace,
|
|
1551
2287
|
agent,
|
|
1552
2288
|
task: promptResult.task,
|
|
2289
|
+
promptPath: promptResult.promptPath,
|
|
2290
|
+
invocation,
|
|
1553
2291
|
run,
|
|
1554
2292
|
codexRuntime: codexPreflight,
|
|
1555
2293
|
preflight: codexPreflight?.preflight ?? null,
|