@ai-content-space/loopx 0.1.3 → 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 +84 -6
- package/README.zh-CN.md +103 -10
- package/assets/logo.svg +89 -0
- package/package.json +2 -1
- package/plugins/loopx/scripts/plugin-install.test.mjs +13 -0
- package/plugins/loopx/skills/archive/SKILL.md +10 -0
- package/plugins/loopx/skills/clarify/SKILL.md +9 -8
- package/plugins/loopx/skills/plan/SKILL.md +4 -3
- package/scripts/codex-workflow-hook.mjs +101 -6
- package/skills/archive/SKILL.md +10 -0
- package/skills/clarify/SKILL.md +9 -8
- package/skills/plan/SKILL.md +4 -3
- package/src/build-runtime.mjs +8 -0
- package/src/cli.mjs +10 -0
- package/src/context-manifest.mjs +1 -1
- package/src/html-views.mjs +316 -0
- package/src/plan-runtime.mjs +23 -0
- package/src/review-runtime.mjs +203 -23
- package/src/runtime-maintenance.mjs +1 -0
- package/src/workflow.mjs +491 -94
package/src/plan-runtime.mjs
CHANGED
|
@@ -214,6 +214,25 @@ function reviewArtifact(kind, iteration, verdict, findings, extras = {}) {
|
|
|
214
214
|
};
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
function reviewHistoryText(reviewHistory = []) {
|
|
218
|
+
if (!Array.isArray(reviewHistory) || reviewHistory.length === 0) {
|
|
219
|
+
return 'None.';
|
|
220
|
+
}
|
|
221
|
+
return reviewHistory.map((entry) => [
|
|
222
|
+
`Iteration ${entry.iteration}:`,
|
|
223
|
+
`- Architect status: ${entry.architectReview?.status ?? 'unknown'}`,
|
|
224
|
+
`- Architect verdict: ${entry.architectReview?.verdict ?? 'unknown'}`,
|
|
225
|
+
`- Architect findings: ${(entry.architectReview?.findings || []).join(' | ') || 'none'}`,
|
|
226
|
+
`- Strongest objection: ${entry.architectReview?.strongestObjection || 'none'}`,
|
|
227
|
+
`- Tradeoff tension: ${entry.architectReview?.tradeoffTension || 'none'}`,
|
|
228
|
+
`- Critic verdict: ${entry.criticReview?.verdict ?? 'unknown'}`,
|
|
229
|
+
`- Critic findings: ${(entry.criticReview?.findings || []).join(' | ') || 'none'}`,
|
|
230
|
+
`- Acceptance criteria testable: ${Boolean(entry.criticReview?.acceptanceCriteriaTestable)}`,
|
|
231
|
+
`- Verification steps resolved: ${Boolean(entry.criticReview?.verificationStepsResolved)}`,
|
|
232
|
+
`- Execution inputs resolved: ${Boolean(entry.criticReview?.executionInputsResolved)}`,
|
|
233
|
+
].join('\n')).join('\n\n');
|
|
234
|
+
}
|
|
235
|
+
|
|
217
236
|
function defaultArchitectReview({ plannerDraft, iteration }) {
|
|
218
237
|
const findings = [
|
|
219
238
|
'Real planning orchestration needs an adapter seam so production runtime and deterministic tests can share one state machine.',
|
|
@@ -345,8 +364,12 @@ export function createRealPlanAdapter({ model } = {}) {
|
|
|
345
364
|
`Deliberate mode: ${Boolean(context.deliberateMode)}`,
|
|
346
365
|
'',
|
|
347
366
|
'Use Chinese for planText / architectureText / developmentPlanText / testPlanText.',
|
|
367
|
+
'If previous review feedback is present, revise the plan to explicitly resolve it. Do not repeat the same plan unchanged.',
|
|
348
368
|
'Do not ask questions. Do not wrap JSON in markdown.',
|
|
349
369
|
'',
|
|
370
|
+
'Previous review feedback:',
|
|
371
|
+
reviewHistoryText(context.reviewHistory),
|
|
372
|
+
'',
|
|
350
373
|
'Source requirements:',
|
|
351
374
|
context.sourceText,
|
|
352
375
|
].join('\n');
|
package/src/review-runtime.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { readdir, stat, mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
-
import {
|
|
2
|
+
import { readdir, stat, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { isAbsolute, join } from 'node:path';
|
|
4
5
|
import { promisify } from 'node:util';
|
|
5
6
|
|
|
6
7
|
import { runCodexReviewJson } from './codex-exec-runtime.mjs';
|
|
@@ -52,6 +53,14 @@ export function parseUntrackedFiles(statusText) {
|
|
|
52
53
|
.filter((file) => file && !file.startsWith('.loopx/') && !file.startsWith('.codex-helper/') && !file.startsWith('.LoopX/'));
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
function normalizeChangedFiles(files = []) {
|
|
57
|
+
return [...new Set((Array.isArray(files) ? files : [])
|
|
58
|
+
.map((file) => String(file || '').trim())
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.filter((file) => !isAbsolute(file) && !file.split(/[\\/]+/).includes('..'))
|
|
61
|
+
.filter((file) => !file.startsWith('.loopx/') && !file.startsWith('.codex-helper/') && !file.startsWith('.LoopX/')))];
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
async function expandUntrackedPath(cwd, file) {
|
|
56
65
|
const fullPath = join(cwd, file);
|
|
57
66
|
const info = await stat(fullPath);
|
|
@@ -65,9 +74,24 @@ async function expandUntrackedPath(cwd, file) {
|
|
|
65
74
|
return nested.flat();
|
|
66
75
|
}
|
|
67
76
|
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
77
|
+
async function scopedUntrackedFiles(cwd, scopedFiles) {
|
|
78
|
+
const batches = await Promise.all(scopedFiles.map(async (file) => {
|
|
79
|
+
const output = await gitOutputAllowExit(cwd, ['ls-files', '--others', '--exclude-standard', '--', file]);
|
|
80
|
+
return output.split('\n').map((item) => item.trim()).filter(Boolean);
|
|
81
|
+
}));
|
|
82
|
+
return normalizeChangedFiles(batches.flat());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function buildReviewDiffEvidence(cwd, statusText, { changedFiles = null, includeUntracked = false } = {}) {
|
|
86
|
+
const scopedFiles = normalizeChangedFiles(changedFiles);
|
|
87
|
+
const pathspec = scopedFiles.length > 0 ? scopedFiles : [];
|
|
88
|
+
const trackedDiff = await gitOutput(cwd, ['diff', 'HEAD', '--', ...pathspec]);
|
|
89
|
+
if (!includeUntracked && scopedFiles.length === 0) {
|
|
90
|
+
return trackedDiff;
|
|
91
|
+
}
|
|
92
|
+
const untrackedFiles = scopedFiles.length > 0
|
|
93
|
+
? await scopedUntrackedFiles(cwd, scopedFiles)
|
|
94
|
+
: parseUntrackedFiles(statusText).filter(() => includeUntracked);
|
|
71
95
|
const untrackedDiffs = [];
|
|
72
96
|
for (const file of untrackedFiles) {
|
|
73
97
|
for (const expandedFile of await expandUntrackedPath(cwd, file)) {
|
|
@@ -77,6 +101,21 @@ export async function buildReviewDiffEvidence(cwd, statusText) {
|
|
|
77
101
|
return [trackedDiff, ...untrackedDiffs].filter(Boolean).join('\n\n');
|
|
78
102
|
}
|
|
79
103
|
|
|
104
|
+
async function buildGitDiffStat(cwd, changedFiles = []) {
|
|
105
|
+
const scopedFiles = normalizeChangedFiles(changedFiles);
|
|
106
|
+
return gitOutput(cwd, ['diff', '--stat', 'HEAD', '--', ...scopedFiles]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function buildDiffCheck(cwd, changedFiles = []) {
|
|
110
|
+
const scopedFiles = normalizeChangedFiles(changedFiles);
|
|
111
|
+
try {
|
|
112
|
+
await gitOutput(cwd, ['diff', '--check', 'HEAD', '--', ...scopedFiles]);
|
|
113
|
+
return '';
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return error?.stdout || error?.stderr || error?.message || String(error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
80
119
|
function normalizeFinding(item) {
|
|
81
120
|
if (typeof item === 'string') {
|
|
82
121
|
return {
|
|
@@ -94,6 +133,56 @@ function normalizeFinding(item) {
|
|
|
94
133
|
};
|
|
95
134
|
}
|
|
96
135
|
|
|
136
|
+
function unavailableChangedFilesReview() {
|
|
137
|
+
return {
|
|
138
|
+
status: 'complete',
|
|
139
|
+
verdict: 'request-changes',
|
|
140
|
+
summary: 'build 执行记录未提供可审查的 changed_files 范围,review 已阻断以避免扩大到全工作区。',
|
|
141
|
+
rollbackTarget: 'build',
|
|
142
|
+
changedFiles: [],
|
|
143
|
+
findings: [{
|
|
144
|
+
severity: 'medium',
|
|
145
|
+
file: 'execution-record.md',
|
|
146
|
+
line: null,
|
|
147
|
+
message: 'execution-record.md 缺少 changed_files 或标记为 unavailable;需要 build 重新生成明确的 changed_files 后再 review。',
|
|
148
|
+
}],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function directoryChangedFilesReview(directoryFiles = []) {
|
|
153
|
+
return {
|
|
154
|
+
status: 'complete',
|
|
155
|
+
verdict: 'request-changes',
|
|
156
|
+
summary: 'build 执行记录的 changed_files 包含目录项,review 已阻断以避免扩大到目录内所有文件。',
|
|
157
|
+
rollbackTarget: 'build',
|
|
158
|
+
changedFiles: [],
|
|
159
|
+
findings: [{
|
|
160
|
+
severity: 'medium',
|
|
161
|
+
file: 'execution-record.md',
|
|
162
|
+
line: null,
|
|
163
|
+
message: `changed_files 必须列出具体文件,不能使用目录项:${directoryFiles.join(', ')}`,
|
|
164
|
+
}],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function findDirectoryChangedFiles(cwd, changedFiles = []) {
|
|
169
|
+
const directories = [];
|
|
170
|
+
for (const file of changedFiles) {
|
|
171
|
+
if (file.endsWith('/') || file.endsWith('\\')) {
|
|
172
|
+
directories.push(file);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
if ((await stat(join(cwd, file))).isDirectory()) {
|
|
177
|
+
directories.push(file);
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// Missing paths can be valid deleted/renamed files in git diffs.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return directories;
|
|
184
|
+
}
|
|
185
|
+
|
|
97
186
|
function normalizeArchitectureReview(raw = {}) {
|
|
98
187
|
const normalizedVerdict = normalizeToken(raw?.verdict || 'pass');
|
|
99
188
|
const verdict = normalizedVerdict === 'block'
|
|
@@ -132,6 +221,46 @@ function normalizeCodeReview(raw, changedFiles) {
|
|
|
132
221
|
};
|
|
133
222
|
}
|
|
134
223
|
|
|
224
|
+
async function withArchitectureReviewSchema(fn) {
|
|
225
|
+
const schemaDir = await mkdtemp(join(tmpdir(), 'loopx-architecture-review-schema-'));
|
|
226
|
+
const schemaPath = join(schemaDir, 'schema.json');
|
|
227
|
+
try {
|
|
228
|
+
await writeFile(schemaPath, JSON.stringify({
|
|
229
|
+
type: 'object',
|
|
230
|
+
additionalProperties: false,
|
|
231
|
+
required: ['status', 'verdict', 'summary', 'rollbackTarget', 'findings'],
|
|
232
|
+
properties: {
|
|
233
|
+
status: { enum: ['complete', 'skipped'] },
|
|
234
|
+
verdict: { enum: ['pass', 'warn', 'block'] },
|
|
235
|
+
summary: { type: 'string' },
|
|
236
|
+
rollbackTarget: {
|
|
237
|
+
anyOf: [
|
|
238
|
+
{ enum: ['build', 'plan', 'clarify'] },
|
|
239
|
+
{ type: 'null' },
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
findings: {
|
|
243
|
+
type: 'array',
|
|
244
|
+
items: {
|
|
245
|
+
type: 'object',
|
|
246
|
+
additionalProperties: false,
|
|
247
|
+
required: ['severity', 'file', 'line', 'message'],
|
|
248
|
+
properties: {
|
|
249
|
+
severity: { enum: ['high', 'medium', 'low'] },
|
|
250
|
+
file: { anyOf: [{ type: 'string' }, { type: 'null' }] },
|
|
251
|
+
line: { anyOf: [{ type: 'number' }, { type: 'null' }] },
|
|
252
|
+
message: { type: 'string' },
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
}));
|
|
258
|
+
return await fn(schemaPath);
|
|
259
|
+
} finally {
|
|
260
|
+
await rm(schemaDir, { recursive: true, force: true });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
135
264
|
export function reviewContextPromptLines(context) {
|
|
136
265
|
return [
|
|
137
266
|
`reviewContextManifestStatus: ${context.contextManifestStatus || 'fallback'}`,
|
|
@@ -157,6 +286,14 @@ function truncateForPrompt(text, maxChars = MAX_DIFF_PROMPT_CHARS) {
|
|
|
157
286
|
return `${value.slice(0, maxChars)}\n[truncated ${value.length - maxChars} chars]`;
|
|
158
287
|
}
|
|
159
288
|
|
|
289
|
+
function gitStatusPreview(statusText, changedFiles = []) {
|
|
290
|
+
const scopedFiles = normalizeChangedFiles(changedFiles);
|
|
291
|
+
if (scopedFiles.length === 0) {
|
|
292
|
+
return statusText || '';
|
|
293
|
+
}
|
|
294
|
+
return scopedFiles.map((file) => `build-owned ${file}`).join('\n');
|
|
295
|
+
}
|
|
296
|
+
|
|
160
297
|
export function buildCodeReviewPrompt(context, changedFiles, diffCheck = '') {
|
|
161
298
|
const gitStatusShort = truncateForPrompt(context.gitStatusShort || '');
|
|
162
299
|
const gitDiffStat = truncateForPrompt(context.gitDiffStat || '');
|
|
@@ -164,7 +301,9 @@ export function buildCodeReviewPrompt(context, changedFiles, diffCheck = '') {
|
|
|
164
301
|
const gitDiffEvidencePath = context.gitDiffEvidencePath || '';
|
|
165
302
|
return [
|
|
166
303
|
`你是 loopx workflow "${context.slug}" 的独立 code reviewer。`,
|
|
167
|
-
|
|
304
|
+
context.buildOwnedScope
|
|
305
|
+
? '请审查 build 记录的变更文件;这些文件可以包含 tracked 修改和 build 新建的 untracked 文件,但不要审查未被 build 归属的本地文件。'
|
|
306
|
+
: '请审查当前 git 工作区相对 HEAD 的代码差异,包括 staged、unstaged 和 untracked 文件。',
|
|
168
307
|
gitDiffEvidencePath
|
|
169
308
|
? '必须以本 prompt 中的当前 git status/diff 预览和完整 git diff evidence 文件为事实来源;不要把既有 review-report.md 或 review-support/code-review.json 当作当前事实来源。'
|
|
170
309
|
: '必须以本 prompt 中的当前 git status/diff 为事实来源;不要把既有 review-report.md 或 review-support/code-review.json 当作当前事实来源。',
|
|
@@ -213,6 +352,9 @@ export function buildArchitectureReviewPrompt(context, changedFiles) {
|
|
|
213
352
|
return [
|
|
214
353
|
`你是 loopx workflow "${context.slug}" 的 architecture smell reviewer。`,
|
|
215
354
|
'这是 `$review` 内部的轻量架构检查 lane,不是新阶段,不要修改文件,不要运行 build。',
|
|
355
|
+
context.buildOwnedScope
|
|
356
|
+
? '审查范围限制为 build 记录的变更文件;不要审查未被 build 归属的本地文件。'
|
|
357
|
+
: '审查范围为当前 git 工作区相对 HEAD 的代码差异。',
|
|
216
358
|
'你的目标是发现会影响长期可维护性、测试 seam、领域边界或 plan 架构假设落地的真实问题。',
|
|
217
359
|
'只在问题足够严重、需要回退 plan/build/clarify 时返回 verdict "block";普通建议用 "warn";没有实质问题用 "pass"。',
|
|
218
360
|
'',
|
|
@@ -256,7 +398,7 @@ export function buildArchitectureReviewPrompt(context, changedFiles) {
|
|
|
256
398
|
].join('\n');
|
|
257
399
|
}
|
|
258
400
|
|
|
259
|
-
export function createRealReviewAdapter({ model, codexReviewJson = runCodexReviewJson } = {}) {
|
|
401
|
+
export function createRealReviewAdapter({ model = DEFAULT_REVIEW_MODEL, codexReviewJson = runCodexReviewJson } = {}) {
|
|
260
402
|
return {
|
|
261
403
|
async codeReview(context) {
|
|
262
404
|
if (!(await isGitWorktree(context.cwd))) {
|
|
@@ -270,7 +412,20 @@ export function createRealReviewAdapter({ model, codexReviewJson = runCodexRevie
|
|
|
270
412
|
}
|
|
271
413
|
|
|
272
414
|
const statusText = await gitOutput(context.cwd, ['status', '--short']);
|
|
273
|
-
|
|
415
|
+
if (context.buildOwnedChangedFilesStatus === 'unavailable') {
|
|
416
|
+
return unavailableChangedFilesReview();
|
|
417
|
+
}
|
|
418
|
+
const hasBuildOwnedScope = Object.hasOwn(context, 'buildOwnedChangedFiles') || context.buildOwnedChangedFilesStatus === 'present';
|
|
419
|
+
const buildOwnedChangedFiles = normalizeChangedFiles(context.buildOwnedChangedFiles);
|
|
420
|
+
const changedFiles = hasBuildOwnedScope
|
|
421
|
+
? buildOwnedChangedFiles
|
|
422
|
+
: parseChangedFiles(statusText);
|
|
423
|
+
if (hasBuildOwnedScope) {
|
|
424
|
+
const directoryFiles = await findDirectoryChangedFiles(context.cwd, changedFiles);
|
|
425
|
+
if (directoryFiles.length > 0) {
|
|
426
|
+
return directoryChangedFilesReview(directoryFiles);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
274
429
|
if (changedFiles.length === 0) {
|
|
275
430
|
return {
|
|
276
431
|
status: 'complete',
|
|
@@ -281,14 +436,9 @@ export function createRealReviewAdapter({ model, codexReviewJson = runCodexRevie
|
|
|
281
436
|
};
|
|
282
437
|
}
|
|
283
438
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
} catch (error) {
|
|
288
|
-
diffCheck = error?.stdout || error?.stderr || error?.message || String(error);
|
|
289
|
-
}
|
|
290
|
-
const gitDiffStat = await gitOutput(context.cwd, ['diff', '--stat', 'HEAD', '--']);
|
|
291
|
-
const gitDiff = await buildReviewDiffEvidence(context.cwd, statusText);
|
|
439
|
+
const diffCheck = await buildDiffCheck(context.cwd, changedFiles);
|
|
440
|
+
const gitDiffStat = await buildGitDiffStat(context.cwd, changedFiles);
|
|
441
|
+
const gitDiff = await buildReviewDiffEvidence(context.cwd, statusText, { changedFiles });
|
|
292
442
|
|
|
293
443
|
await mkdir(join(context.root, 'review-support'), { recursive: true });
|
|
294
444
|
const outputPath = join(context.root, 'review-support', 'code-review.raw.json');
|
|
@@ -296,7 +446,8 @@ export function createRealReviewAdapter({ model, codexReviewJson = runCodexRevie
|
|
|
296
446
|
await writeFile(gitDiffEvidencePath, gitDiff || '');
|
|
297
447
|
const prompt = buildCodeReviewPrompt({
|
|
298
448
|
...context,
|
|
299
|
-
|
|
449
|
+
buildOwnedScope: hasBuildOwnedScope,
|
|
450
|
+
gitStatusShort: gitStatusPreview(statusText, changedFiles),
|
|
300
451
|
gitDiffStat,
|
|
301
452
|
gitDiff,
|
|
302
453
|
gitDiffEvidencePath,
|
|
@@ -322,7 +473,34 @@ export function createRealReviewAdapter({ model, codexReviewJson = runCodexRevie
|
|
|
322
473
|
});
|
|
323
474
|
}
|
|
324
475
|
const statusText = await gitOutput(context.cwd, ['status', '--short']);
|
|
325
|
-
|
|
476
|
+
if (context.buildOwnedChangedFilesStatus === 'unavailable') {
|
|
477
|
+
return normalizeArchitectureReview({
|
|
478
|
+
status: 'complete',
|
|
479
|
+
verdict: 'block',
|
|
480
|
+
summary: 'build 执行记录未提供 changed_files 范围,架构 smell 扫描已阻断以避免扩大到全工作区。',
|
|
481
|
+
rollbackTarget: 'build',
|
|
482
|
+
findings: [{
|
|
483
|
+
severity: 'medium',
|
|
484
|
+
file: 'execution-record.md',
|
|
485
|
+
line: null,
|
|
486
|
+
message: 'execution-record.md 缺少 changed_files 或标记为 unavailable;需要 build 重新生成明确范围。',
|
|
487
|
+
}],
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
const hasBuildOwnedScope = Object.hasOwn(context, 'buildOwnedChangedFiles') || context.buildOwnedChangedFilesStatus === 'present';
|
|
491
|
+
const buildOwnedChangedFiles = normalizeChangedFiles(context.buildOwnedChangedFiles);
|
|
492
|
+
const changedFiles = hasBuildOwnedScope
|
|
493
|
+
? buildOwnedChangedFiles
|
|
494
|
+
: parseChangedFiles(statusText);
|
|
495
|
+
if (hasBuildOwnedScope) {
|
|
496
|
+
const directoryFiles = await findDirectoryChangedFiles(context.cwd, changedFiles);
|
|
497
|
+
if (directoryFiles.length > 0) {
|
|
498
|
+
return normalizeArchitectureReview({
|
|
499
|
+
...directoryChangedFilesReview(directoryFiles),
|
|
500
|
+
verdict: 'block',
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
326
504
|
if (changedFiles.length === 0) {
|
|
327
505
|
return normalizeArchitectureReview({
|
|
328
506
|
status: 'complete',
|
|
@@ -331,26 +509,28 @@ export function createRealReviewAdapter({ model, codexReviewJson = runCodexRevie
|
|
|
331
509
|
findings: [],
|
|
332
510
|
});
|
|
333
511
|
}
|
|
334
|
-
const gitDiffStat = await
|
|
335
|
-
const gitDiff = await buildReviewDiffEvidence(context.cwd, statusText);
|
|
512
|
+
const gitDiffStat = await buildGitDiffStat(context.cwd, changedFiles);
|
|
513
|
+
const gitDiff = await buildReviewDiffEvidence(context.cwd, statusText, { changedFiles });
|
|
336
514
|
await mkdir(join(context.root, 'review-support'), { recursive: true });
|
|
337
515
|
const outputPath = join(context.root, 'review-support', 'architecture-smell.raw.json');
|
|
338
516
|
const gitDiffEvidencePath = join(context.root, 'review-support', 'code-review-diff.patch');
|
|
339
517
|
const prompt = buildArchitectureReviewPrompt({
|
|
340
518
|
...context,
|
|
341
|
-
|
|
519
|
+
buildOwnedScope: hasBuildOwnedScope,
|
|
520
|
+
gitStatusShort: gitStatusPreview(statusText, changedFiles),
|
|
342
521
|
gitDiffStat,
|
|
343
522
|
gitDiff,
|
|
344
523
|
gitDiffEvidencePath,
|
|
345
524
|
}, changedFiles);
|
|
346
|
-
const raw = await codexReviewJson({
|
|
525
|
+
const raw = await withArchitectureReviewSchema((outputSchema) => codexReviewJson({
|
|
347
526
|
cwd: context.cwd,
|
|
348
527
|
prompt,
|
|
349
528
|
outputPath,
|
|
350
529
|
model,
|
|
351
530
|
reviewMode: true,
|
|
352
531
|
uncommitted: true,
|
|
353
|
-
|
|
532
|
+
outputSchema,
|
|
533
|
+
}));
|
|
354
534
|
return normalizeArchitectureReview(raw);
|
|
355
535
|
},
|
|
356
536
|
};
|
|
@@ -274,6 +274,7 @@ function createMigratedWorkflowBaseState(slug, legacyState, change) {
|
|
|
274
274
|
plan_docs_status: 'missing',
|
|
275
275
|
plan_docs_artifact_paths: null,
|
|
276
276
|
plan_review_artifact_paths: [],
|
|
277
|
+
plan_review_history: [],
|
|
277
278
|
plan_blockers: [],
|
|
278
279
|
plan_source_spec_path: null,
|
|
279
280
|
change_id: change?.changeId || `chg-${slug}`,
|