@openprd/cli 0.1.11 → 0.1.18
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/changes/adaptive-project-framing/.openprd.yaml +2 -0
- package/.openprd/changes/adaptive-project-framing/design.md +47 -0
- package/.openprd/changes/adaptive-project-framing/proposal.md +35 -0
- package/.openprd/changes/adaptive-project-framing/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/adaptive-project-framing/task-events.jsonl +20 -0
- package/.openprd/changes/adaptive-project-framing/tasks.md +340 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/.openprd.yaml +2 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/design.md +34 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/proposal.md +23 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/task-events.jsonl +14 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/tasks.md +238 -0
- package/.openprd/changes/loop-isolated-worktree-commit/.openprd.yaml +2 -0
- package/.openprd/changes/loop-isolated-worktree-commit/design.md +53 -0
- package/.openprd/changes/loop-isolated-worktree-commit/proposal.md +38 -0
- package/.openprd/changes/loop-isolated-worktree-commit/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/loop-isolated-worktree-commit/task-events.jsonl +22 -0
- package/.openprd/changes/loop-isolated-worktree-commit/tasks.md +357 -0
- package/.openprd/changes/openprd/.openprd.yaml +2 -0
- package/.openprd/changes/openprd/design.md +46 -0
- package/.openprd/changes/openprd/proposal.md +36 -0
- package/.openprd/changes/openprd/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/openprd/task-events.jsonl +23 -0
- package/.openprd/changes/openprd/tasks.md +242 -0
- package/.openprd/changes/pm-human-in-the-loop/.openprd.yaml +2 -0
- package/.openprd/changes/pm-human-in-the-loop/design.md +38 -0
- package/.openprd/changes/pm-human-in-the-loop/proposal.md +29 -0
- package/.openprd/changes/pm-human-in-the-loop/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/pm-human-in-the-loop/task-events.jsonl +18 -0
- package/.openprd/changes/pm-human-in-the-loop/tasks.md +306 -0
- package/.openprd/changes/test-strategy-router/.openprd.yaml +2 -0
- package/.openprd/changes/test-strategy-router/design.md +65 -0
- package/.openprd/changes/test-strategy-router/proposal.md +45 -0
- package/.openprd/changes/test-strategy-router/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/test-strategy-router/task-events.jsonl +1 -0
- package/.openprd/changes/test-strategy-router/tasks.md +132 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/.openprd.yaml +2 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/design.md +28 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/proposal.md +23 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/task-events.jsonl +11 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/tasks.md +66 -0
- package/.openprd/changes/vibe-coding-dev-check/.openprd.yaml +2 -0
- package/.openprd/changes/vibe-coding-dev-check/design.md +37 -0
- package/.openprd/changes/vibe-coding-dev-check/proposal.md +28 -0
- package/.openprd/changes/vibe-coding-dev-check/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/vibe-coding-dev-check/task-events.jsonl +18 -0
- package/.openprd/changes/vibe-coding-dev-check/tasks.md +306 -0
- package/.openprd/design/README.md +11 -6
- package/.openprd/design/README_EN.md +4 -1
- package/.openprd/design/active/asset-spec.md +6 -0
- package/.openprd/design/active/direction-plan.md +5 -5
- package/.openprd/design/active/selected-direction.md +2 -0
- package/.openprd/design/anti-slop.md +26 -1
- package/.openprd/design/assets/surface-presets.md +6 -0
- package/.openprd/design/checklists/ui-quality-gate.md +7 -0
- package/.openprd/design/lenses/frontend-lenses.md +28 -0
- package/.openprd/design/recipes/content-experience.md +8 -1
- package/.openprd/design/recipes/operational-tool.md +7 -0
- package/.openprd/design/recipes/product-launch.md +8 -1
- package/.openprd/design/templates/README.md +6 -3
- package/.openprd/design/templates/README_EN.md +6 -3
- package/.openprd/design/templates/ops-dashboard.html +8 -1
- package/.openprd/design/templates/product-launch.html +2 -1
- package/.openprd/design/themes/theme-catalog.json +23 -5
- package/.openprd/engagements/active/prd.md +103 -100
- package/.openprd/standards/config.json +7 -1
- package/AGENTS.md +13 -3
- package/README.md +107 -14
- package/README_EN.md +131 -13
- package/package.json +1 -1
- package/scripts/dev-check-wrapup-copy.mjs +46 -21
- package/skills/openprd-diagram-review/SKILL.md +27 -2
- package/skills/openprd-diagram-review/references/explanation-svg-patterns.md +131 -0
- package/skills/openprd-discovery-loop/SKILL.md +2 -2
- package/skills/openprd-frontend-design/SKILL.md +44 -20
- package/skills/openprd-frontend-design/references/design-asset-contract.md +6 -0
- package/skills/openprd-frontend-design/references/direction-engine.md +29 -4
- package/skills/openprd-harness/SKILL.md +22 -16
- package/skills/openprd-quality/SKILL.md +10 -8
- package/skills/openprd-requirement-intake/SKILL.md +2 -0
- package/skills/openprd-router/SKILL.md +2 -1
- package/skills/openprd-shared/SKILL.md +26 -16
- package/skills/openprd-standards/SKILL.md +4 -2
- package/src/agent-canonical-content.js +808 -0
- package/src/agent-integration.js +258 -777
- package/src/canvas-app.html.js +2910 -0
- package/src/canvas-i18n.js +274 -0
- package/src/canvas-workspace.js +2271 -0
- package/src/cli/args.js +72 -4
- package/src/cli/basic-print.js +3 -0
- package/src/cli/canvas-print.js +106 -0
- package/src/cli/doctor-print.js +20 -0
- package/src/cli/print.js +2 -0
- package/src/cli/quality-commands.js +222 -0
- package/src/cli/quality-print.js +28 -2
- package/src/cli/run-print.js +5 -0
- package/src/cli/workflow-print.js +3 -0
- package/src/codex-app-server-relay.js +251 -0
- package/src/codex-hook-runner-template.mjs +957 -59
- package/src/design-starter-support.js +2 -2
- package/src/design-starter.js +39 -3
- package/src/dev-standards.js +125 -10
- package/src/diagram-core.js +6 -20
- package/src/discovery.js +2 -8
- package/src/docs-compact.js +125 -0
- package/src/execution-strategy.js +1 -1
- package/src/fleet.js +9 -3
- package/src/guide.js +227 -0
- package/src/html-artifacts.js +428 -7
- package/src/knowledge.js +45 -11
- package/src/language-policy.js +63 -3
- package/src/learning-review.js +1 -1
- package/src/loop.js +1 -1
- package/src/openprd.js +68 -131
- package/src/openspec/change-validate.js +2 -2
- package/src/openspec/constants.js +6 -7
- package/src/openspec/execute.js +2 -2
- package/src/openspec/generate.js +318 -102
- package/src/openspec/migration.js +168 -0
- package/src/openspec/paths.js +1 -22
- package/src/prd-core.js +1 -1
- package/src/quality-visual-review.js +257 -14
- package/src/quality.js +46 -8
- package/src/review-presentation.js +60 -0
- package/src/run-harness.js +41 -3
- package/src/session-evidence.js +363 -0
- package/src/standards.js +64 -0
- package/src/test-strategy.js +7 -4
- package/src/visual-compare-centering.js +622 -0
- package/src/visual-compare-core.js +1123 -0
- package/src/visual-compare.js +620 -698
- package/src/workspace-core.js +94 -4
- package/src/workspace-workflow.js +46 -1
package/src/quality.js
CHANGED
|
@@ -108,7 +108,7 @@ const EVIDENCE_TOKENS = {
|
|
|
108
108
|
'test-strategy': ['test-layer', 'test-size', 'test-scope', 'evidence-plan', 'testing pyramid', '测试策略', '测试分流', '单元测试', '集成测试', '端到端'],
|
|
109
109
|
smoke: ['smoke', 'e2e', 'playwright', 'cypress', 'main flow', 'happy path', '冒烟', '主流程'],
|
|
110
110
|
'feature-coverage': ['feature coverage', 'acceptance', 'tasks done', 'openprd tasks', '验收', '功能覆盖', '任务完成'],
|
|
111
|
-
'visual-review': ['visual-compare', 'visual-before-after', 'visual-focus-board', 'visual-parallel-board', 'reference-actual', 'before-after', 'focus-board', 'parallel-board', '效果图', '实现截图', '修改前', '修改后', '视觉对比', '局部焦点证据板', '并行实验证据板'],
|
|
111
|
+
'visual-review': ['visual-compare', 'visual-before-after', 'visual-focus-board', 'visual-parallel-board', 'visual-verification-board', 'visual-alignment-board', 'visual-centering-board', 'reference-actual', 'before-after', 'focus-board', 'parallel-board', 'verification-board', 'alignment-board', 'centering-board', 'center-board', '效果图', '实现截图', '修改前', '修改后', '视觉对比', '局部焦点证据板', '并行实验证据板', '截图实测证据板', '对齐辅助线证据板', '内部居中证据板', '内容槽位', '内部槽位', '视觉重心', '主体外接框', '画布中心', '偏心'],
|
|
112
112
|
'normal-performance': ['performance', 'perf', 'benchmark', 'latency', 'p95', 'lighthouse', 'k6', '性能', '耗时'],
|
|
113
113
|
'extreme-performance': ['extreme', 'stress', 'load test', 'large-data', 'pressure', 'k6', '压力', '极端', '大数据'],
|
|
114
114
|
knowledge: ['quality learn', 'incident', 'pattern', 'skill', '复盘', '经验', '沉淀'],
|
|
@@ -253,6 +253,10 @@ function defaultQualityConfig() {
|
|
|
253
253
|
ledgerPath: OPENPRD_GROWTH_LEDGER,
|
|
254
254
|
completionCheckpointRequired: true,
|
|
255
255
|
},
|
|
256
|
+
reports: {
|
|
257
|
+
retentionMaxReports: 60,
|
|
258
|
+
retentionEnabled: true,
|
|
259
|
+
},
|
|
256
260
|
};
|
|
257
261
|
}
|
|
258
262
|
|
|
@@ -301,6 +305,10 @@ function normalizeQualityConfig(config = {}) {
|
|
|
301
305
|
...defaults.growth,
|
|
302
306
|
...(config.growth ?? {}),
|
|
303
307
|
},
|
|
308
|
+
reports: {
|
|
309
|
+
...defaults.reports,
|
|
310
|
+
...(config.reports ?? {}),
|
|
311
|
+
},
|
|
304
312
|
};
|
|
305
313
|
}
|
|
306
314
|
|
|
@@ -442,8 +450,7 @@ async function readActiveChangeContext(projectRoot, activeChange) {
|
|
|
442
450
|
return { activeChange: null, files: [], text: '' };
|
|
443
451
|
}
|
|
444
452
|
const roots = [
|
|
445
|
-
cjoin('openprd', 'changes', activeChange),
|
|
446
|
-
cjoin('openspec', 'changes', activeChange),
|
|
453
|
+
cjoin('.openprd', 'changes', activeChange),
|
|
447
454
|
];
|
|
448
455
|
const files = [];
|
|
449
456
|
async function walk(root, dir) {
|
|
@@ -904,7 +911,9 @@ async function readActiveTasks(projectRoot) {
|
|
|
904
911
|
return { activeChange: null, total: 0, done: 0, pending: 0, blocked: 0, tasks: [] };
|
|
905
912
|
}
|
|
906
913
|
const taskFiles = [];
|
|
907
|
-
for (const root of [
|
|
914
|
+
for (const root of [
|
|
915
|
+
cjoin('.openprd', 'changes', activeChange),
|
|
916
|
+
]) {
|
|
908
917
|
const dir = cjoin(projectRoot, root);
|
|
909
918
|
const entries = await fs.readdir(dir).catch(() => []);
|
|
910
919
|
for (const entry of entries) {
|
|
@@ -1283,7 +1292,35 @@ async function buildQualityReport(projectRoot, config, options = {}) {
|
|
|
1283
1292
|
};
|
|
1284
1293
|
}
|
|
1285
1294
|
|
|
1286
|
-
async function
|
|
1295
|
+
export async function pruneQualityReports(projectRoot, retainedReports, config) {
|
|
1296
|
+
const retention = config?.reports ?? {};
|
|
1297
|
+
if (retention.retentionEnabled === false) {
|
|
1298
|
+
return { pruned: 0 };
|
|
1299
|
+
}
|
|
1300
|
+
const maxReports = Number.isInteger(retention.retentionMaxReports) && retention.retentionMaxReports > 0
|
|
1301
|
+
? retention.retentionMaxReports
|
|
1302
|
+
: 60;
|
|
1303
|
+
const reportsDir = qualityPath(projectRoot, QUALITY_REPORTS_DIR);
|
|
1304
|
+
const entries = await fs.readdir(reportsDir).catch(() => []);
|
|
1305
|
+
const keepBases = new Set(
|
|
1306
|
+
retainedReports.slice(0, maxReports).map((item) => String(item.reportId ?? '').replace(/[^a-zA-Z0-9._-]/g, '_')),
|
|
1307
|
+
);
|
|
1308
|
+
let pruned = 0;
|
|
1309
|
+
for (const name of entries) {
|
|
1310
|
+
const match = name.match(/^(eval-[a-zA-Z0-9._-]+)\.(json|html)$/);
|
|
1311
|
+
if (!match) {
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
if (keepBases.has(match[1])) {
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
await fs.rm(cjoin(reportsDir, name), { force: true }).catch(() => {});
|
|
1318
|
+
pruned += 1;
|
|
1319
|
+
}
|
|
1320
|
+
return { pruned };
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
async function writeReport(projectRoot, report, config = null) {
|
|
1287
1324
|
await ensureQualityDirs(projectRoot);
|
|
1288
1325
|
const reportBase = report.id.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
1289
1326
|
const jsonPath = qualityPath(projectRoot, cjoin(QUALITY_REPORTS_DIR, `${reportBase}.json`));
|
|
@@ -1304,7 +1341,8 @@ async function writeReport(projectRoot, report) {
|
|
|
1304
1341
|
...(Array.isArray(index.reports) ? index.reports.filter((item) => item.reportId !== report.id) : []),
|
|
1305
1342
|
].slice(0, 100);
|
|
1306
1343
|
await writeJson(indexPath, { version: 1, updatedAt: timestamp(), reports });
|
|
1307
|
-
|
|
1344
|
+
const retention = await pruneQualityReports(projectRoot, reports, config ?? normalizeQualityConfig());
|
|
1345
|
+
return { jsonPath, htmlPath, indexPath, prunedReports: retention.pruned };
|
|
1308
1346
|
}
|
|
1309
1347
|
|
|
1310
1348
|
export async function initQualityWorkspace(projectRoot, options = {}) {
|
|
@@ -1349,7 +1387,7 @@ export async function verifyQualityWorkspace(projectRoot, options = {}) {
|
|
|
1349
1387
|
await ensureQualityDirs(projectRoot);
|
|
1350
1388
|
const initialReport = await buildQualityReport(projectRoot, config);
|
|
1351
1389
|
let report = initialReport;
|
|
1352
|
-
let paths = await writeReport(projectRoot, report);
|
|
1390
|
+
let paths = await writeReport(projectRoot, report, config);
|
|
1353
1391
|
const knowledgeSignal = {
|
|
1354
1392
|
kind: 'quality-verify',
|
|
1355
1393
|
ok: report.readiness.productionReady,
|
|
@@ -1408,7 +1446,7 @@ export async function verifyQualityWorkspace(projectRoot, options = {}) {
|
|
|
1408
1446
|
reason: 'reusable-knowledge-artifact-already-exists',
|
|
1409
1447
|
};
|
|
1410
1448
|
report = await buildQualityReport(projectRoot, config, { reportId: initialReport.id });
|
|
1411
|
-
paths = await writeReport(projectRoot, report);
|
|
1449
|
+
paths = await writeReport(projectRoot, report, config);
|
|
1412
1450
|
const strict = options.strict === true;
|
|
1413
1451
|
const blocking = (strict || config.enforcement === 'blocking') && !report.readiness.productionReady;
|
|
1414
1452
|
const featureCoverageOnly = report.readiness.failingGates.length === 0
|
|
@@ -48,6 +48,32 @@ export const REVIEW_PRESENTATION_TEMPLATE = {
|
|
|
48
48
|
USER_CHANGE_SUMMARY_GUIDE.panelExamples.risk,
|
|
49
49
|
],
|
|
50
50
|
},
|
|
51
|
+
designDirections: {
|
|
52
|
+
note: '可选;只有需求涉及界面/页面/交互时提供。先用 imagegen 生成 3 个差异足够大的交互稿,把图片放到 .openprd/design/active/ 后写入路径;不涉及界面时删除本字段。',
|
|
53
|
+
directions: [
|
|
54
|
+
{
|
|
55
|
+
id: 'direction-1',
|
|
56
|
+
title: '15 字以内方向名',
|
|
57
|
+
image: '.openprd/design/active/direction-1.png',
|
|
58
|
+
summary: '30 字以内一句话说明这个方向的核心体验',
|
|
59
|
+
tradeoff: '30 字以内说明取舍或适用场景',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'direction-2',
|
|
63
|
+
title: '15 字以内方向名',
|
|
64
|
+
image: '.openprd/design/active/direction-2.png',
|
|
65
|
+
summary: '30 字以内一句话说明这个方向的核心体验',
|
|
66
|
+
tradeoff: '30 字以内说明取舍或适用场景',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'direction-3',
|
|
70
|
+
title: '15 字以内方向名',
|
|
71
|
+
image: '.openprd/design/active/direction-3.png',
|
|
72
|
+
summary: '30 字以内一句话说明这个方向的核心体验',
|
|
73
|
+
tradeoff: '30 字以内说明取舍或适用场景',
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
51
77
|
};
|
|
52
78
|
|
|
53
79
|
export function buildReviewPresentationTemplatePayload() {
|
|
@@ -182,6 +208,39 @@ function stableStringify(value) {
|
|
|
182
208
|
return JSON.stringify(value);
|
|
183
209
|
}
|
|
184
210
|
|
|
211
|
+
const ACTIVE_ARTIFACT_RETAIN_VERSIONS = 12;
|
|
212
|
+
|
|
213
|
+
export async function archiveStaleActiveArtifacts(projectRoot, options = {}) {
|
|
214
|
+
const retain = Number.isInteger(options.retain) && options.retain > 0
|
|
215
|
+
? options.retain
|
|
216
|
+
: ACTIVE_ARTIFACT_RETAIN_VERSIONS;
|
|
217
|
+
const activeDir = path.join(projectRoot, '.openprd', 'artifacts', 'active');
|
|
218
|
+
const archiveDir = path.join(projectRoot, '.openprd', 'artifacts', 'archive');
|
|
219
|
+
const entries = await fs.readdir(activeDir, { withFileTypes: true }).catch(() => []);
|
|
220
|
+
const versioned = entries
|
|
221
|
+
.filter((entry) => entry.isDirectory() && /^v\d{4}-/.test(entry.name))
|
|
222
|
+
.map((entry) => entry.name)
|
|
223
|
+
.sort();
|
|
224
|
+
const stale = versioned.slice(0, Math.max(0, versioned.length - retain));
|
|
225
|
+
if (stale.length === 0) {
|
|
226
|
+
return { archived: [] };
|
|
227
|
+
}
|
|
228
|
+
await fs.mkdir(archiveDir, { recursive: true });
|
|
229
|
+
const archived = [];
|
|
230
|
+
for (const name of stale) {
|
|
231
|
+
const from = path.join(activeDir, name);
|
|
232
|
+
const to = path.join(archiveDir, name);
|
|
233
|
+
try {
|
|
234
|
+
await fs.rm(to, { recursive: true, force: true });
|
|
235
|
+
await fs.rename(from, to);
|
|
236
|
+
archived.push(name);
|
|
237
|
+
} catch {
|
|
238
|
+
// 归档失败不阻断评审生成
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return { archived };
|
|
242
|
+
}
|
|
243
|
+
|
|
185
244
|
async function renderValidatedReviewPresentation(projectRoot, snapshot) {
|
|
186
245
|
const workspaceRoot = path.join(projectRoot, '.openprd');
|
|
187
246
|
const canonicalReview = canonicalReviewPath({ workspaceRoot }, snapshot.versionId);
|
|
@@ -191,6 +250,7 @@ async function renderValidatedReviewPresentation(projectRoot, snapshot) {
|
|
|
191
250
|
snapshot,
|
|
192
251
|
projectRelease: buildReleaseLedgerSummary(releaseLedger.ledger),
|
|
193
252
|
}));
|
|
253
|
+
await archiveStaleActiveArtifacts(projectRoot).catch(() => null);
|
|
194
254
|
|
|
195
255
|
const versionIndexPath = path.join(workspaceRoot, 'state', 'version-index.json');
|
|
196
256
|
const versionIndex = await readJson(versionIndexPath);
|
package/src/run-harness.js
CHANGED
|
@@ -19,6 +19,9 @@ const OPENPRD_HARNESS_RUN_STATE = cjoin(OPENPRD_HARNESS_DIR, 'run-state.json');
|
|
|
19
19
|
const OPENPRD_HARNESS_ITERATIONS = cjoin(OPENPRD_HARNESS_DIR, 'iterations.jsonl');
|
|
20
20
|
const OPENPRD_HARNESS_LEARNINGS = cjoin(OPENPRD_HARNESS_DIR, 'learnings.md');
|
|
21
21
|
const OPENPRD_HARNESS_LOOP_FEATURE_LIST = cjoin(OPENPRD_HARNESS_DIR, 'feature-list.json');
|
|
22
|
+
const OPENPRD_HARNESS_INSTALL_MANIFEST = cjoin(OPENPRD_HARNESS_DIR, 'install-manifest.json');
|
|
23
|
+
const OPENPRD_HARNESS_HOOK_STATE = cjoin(OPENPRD_HARNESS_DIR, 'hook-state.json');
|
|
24
|
+
const OPENPRD_HARNESS_RUNTIME_ENVIRONMENT = cjoin(OPENPRD_HARNESS_DIR, 'runtime-environment.json');
|
|
22
25
|
const OPENPRD_HARNESS_REQUIREMENT_GATE = cjoin(OPENPRD_HARNESS_DIR, 'requirement-gate.json');
|
|
23
26
|
const OPENPRD_HARNESS_REQUIREMENT_GATES_DIR = cjoin(OPENPRD_HARNESS_DIR, 'requirement-gates');
|
|
24
27
|
const OPENPRD_HARNESS_SESSION_BINDINGS_DIR = cjoin(OPENPRD_HARNESS_DIR, 'session-bindings');
|
|
@@ -82,6 +85,34 @@ async function ensureRunHarness(projectRoot) {
|
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
async function readRuntimeEnvironmentContext(projectRoot) {
|
|
89
|
+
const [runtimeEnvironment, installManifest, hookState] = await Promise.all([
|
|
90
|
+
readJson(harnessFile(projectRoot, OPENPRD_HARNESS_RUNTIME_ENVIRONMENT)).catch(() => null),
|
|
91
|
+
readJson(harnessFile(projectRoot, OPENPRD_HARNESS_INSTALL_MANIFEST)).catch(() => null),
|
|
92
|
+
readJson(harnessFile(projectRoot, OPENPRD_HARNESS_HOOK_STATE)).catch(() => null),
|
|
93
|
+
]);
|
|
94
|
+
const active = runtimeEnvironment?.active
|
|
95
|
+
?? hookState?.runtimeEnvironment
|
|
96
|
+
?? {
|
|
97
|
+
activeClient: 'unknown',
|
|
98
|
+
surface: 'unknown',
|
|
99
|
+
confidence: 0,
|
|
100
|
+
sessionId: null,
|
|
101
|
+
evidence: [],
|
|
102
|
+
observedCapabilities: [],
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
active,
|
|
106
|
+
runtimeDetection: installManifest?.runtimeDetection ?? runtimeEnvironment?.runtimeDetection ?? null,
|
|
107
|
+
platformCapabilityPacks: installManifest?.platformCapabilityPacks ?? runtimeEnvironment?.platformCapabilityPacks ?? [],
|
|
108
|
+
files: {
|
|
109
|
+
runtimeEnvironment: OPENPRD_HARNESS_RUNTIME_ENVIRONMENT,
|
|
110
|
+
installManifest: OPENPRD_HARNESS_INSTALL_MANIFEST,
|
|
111
|
+
hookState: OPENPRD_HARNESS_HOOK_STATE,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
85
116
|
async function readRunState(projectRoot) {
|
|
86
117
|
await ensureRunHarness(projectRoot);
|
|
87
118
|
return readJson(harnessFile(projectRoot, OPENPRD_HARNESS_RUN_STATE)).catch(() => ({
|
|
@@ -225,7 +256,7 @@ function executionGate() {
|
|
|
225
256
|
confirmationChecklistRequired: true,
|
|
226
257
|
allowedIntents: ['开发', '实现', '修复', '继续任务', '落地执行', '深度调研', '深度对标', '复刻落地', '提交'],
|
|
227
258
|
readOnlyIntents: ['看看', '规划', '梳理', '分析', '评估', '预计动哪些文件', '怎么改', '代码审查'],
|
|
228
|
-
rule: '只有当用户当前明确要求实现、继续、深度调研、对标或提交时,才运行 executionCommand。单纯的“请帮我实现/继续实现”只表示有执行意图,不表示可以跳过 requirement 摘要确认、`capture/classify/synthesize` 写入路径或 review;只有用户明确表示“不需要进行任何确认”时,才允许静默走完整 requirement write path,并对当前精确匹配的稳定 review artifact
|
|
259
|
+
rule: '只有当用户当前明确要求实现、继续、深度调研、对标或提交时,才运行 executionCommand。单纯的“请帮我实现/继续实现”只表示有执行意图,不表示可以跳过 requirement 摘要确认、`capture/classify/synthesize` 写入路径或 review;只有用户明确表示“不需要进行任何确认”时,才允许静默走完整 requirement write path,并对当前精确匹配的稳定 review artifact 记录确认。即使用户授权实现,也不等于授权云端热修复、生产远端写入、业务兜底、写死逻辑或缓存补行;这些动作必须由当前用户明确批准,并说明如何同步回本地源码、配置或迁移。若还需要向用户索取执行授权,先展示 executionConfirmationChecklist,再请求明确确认;规划、分析、文件影响范围和审查类请求保持只读,并基于证据回答。',
|
|
229
260
|
};
|
|
230
261
|
}
|
|
231
262
|
|
|
@@ -251,6 +282,7 @@ function buildExecutionConfirmationChecklist(recommendation) {
|
|
|
251
282
|
outOfScope: [
|
|
252
283
|
'不会默认处理这次范围以外的历史问题。',
|
|
253
284
|
'不会默认顺带做提交、发布或额外的全局调整。',
|
|
285
|
+
'不会默认做云端热修复、生产远端写入、业务兜底、写死逻辑或缓存补行。',
|
|
254
286
|
'如果真的需要扩大范围,我会先单独说明。',
|
|
255
287
|
].filter(Boolean),
|
|
256
288
|
verification: [
|
|
@@ -261,6 +293,7 @@ function buildExecutionConfirmationChecklist(recommendation) {
|
|
|
261
293
|
risks: [
|
|
262
294
|
'这一步会正式写入当前工作区。',
|
|
263
295
|
'如果牵出和本次无关的历史遗留问题,我会单列说明,不把它混成本次失败。',
|
|
296
|
+
'如果确实需要临时兜底或线上热修复,我会先单独说明影响、回滚和本地同步计划,等你明确批准。',
|
|
264
297
|
parallelPlan?.eligible ? '如果多人同时推进,我会避免范围互相踩踏。' : null,
|
|
265
298
|
],
|
|
266
299
|
confirmationPrompt: '如果你希望我现在就按这次范围继续,我就直接往下做。',
|
|
@@ -1816,6 +1849,7 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
|
|
|
1816
1849
|
await ensureRunHarness(projectRoot);
|
|
1817
1850
|
const runState = await readRunState(projectRoot);
|
|
1818
1851
|
const currentTurn = await readJson(harnessFile(projectRoot, OPENPRD_HARNESS_TURN_STATE)).catch(() => null);
|
|
1852
|
+
const runtimeEnvironment = await readRuntimeEnvironmentContext(projectRoot);
|
|
1819
1853
|
const currentTurnPrompt = String(currentTurn?.prompt ?? '').trim();
|
|
1820
1854
|
const fallbackPrompt = !options.hookInject
|
|
1821
1855
|
? (inferPromptDrivenLightweightRecommendation(currentTurnPrompt) ? currentTurnPrompt : null)
|
|
@@ -1942,8 +1976,8 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
|
|
|
1942
1976
|
activeChange,
|
|
1943
1977
|
nextTaskTitle: nextTask?.title,
|
|
1944
1978
|
relatedFiles: [
|
|
1945
|
-
focusedChangeId ?
|
|
1946
|
-
activeChange ?
|
|
1979
|
+
focusedChangeId ? `.openprd/changes/${focusedChangeId}/tasks.md` : null,
|
|
1980
|
+
activeChange ? `.openprd/changes/${activeChange}` : null,
|
|
1947
1981
|
].filter(Boolean),
|
|
1948
1982
|
limit: options.hookInject ? 4 : 3,
|
|
1949
1983
|
}).catch(() => ({ matched: [], summary: { matched: 0 } }));
|
|
@@ -2035,6 +2069,7 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
|
|
|
2035
2069
|
: null,
|
|
2036
2070
|
lane,
|
|
2037
2071
|
recommendation: effectiveRecommendation,
|
|
2072
|
+
runtimeEnvironment,
|
|
2038
2073
|
knowledgeSkills: {
|
|
2039
2074
|
matched: renderedKnowledgeSkills,
|
|
2040
2075
|
mandatoryCheck: knowledgeSkillMatches.mandatoryCheck ?? null,
|
|
@@ -2050,6 +2085,7 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
|
|
|
2050
2085
|
runState: OPENPRD_HARNESS_RUN_STATE,
|
|
2051
2086
|
iterations: OPENPRD_HARNESS_ITERATIONS,
|
|
2052
2087
|
learnings: OPENPRD_HARNESS_LEARNINGS,
|
|
2088
|
+
runtimeEnvironment: OPENPRD_HARNESS_RUNTIME_ENVIRONMENT,
|
|
2053
2089
|
},
|
|
2054
2090
|
};
|
|
2055
2091
|
|
|
@@ -2065,6 +2101,7 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
|
|
|
2065
2101
|
async function recordRunHook(projectRoot, options = {}) {
|
|
2066
2102
|
await ensureRunHarness(projectRoot);
|
|
2067
2103
|
const state = await readRunState(projectRoot);
|
|
2104
|
+
const runtimeEnvironment = await readRuntimeEnvironmentContext(projectRoot);
|
|
2068
2105
|
const currentIteration = Number(state.currentIteration ?? 0) + 1;
|
|
2069
2106
|
const event = {
|
|
2070
2107
|
version: 1,
|
|
@@ -2075,6 +2112,7 @@ async function recordRunHook(projectRoot, options = {}) {
|
|
|
2075
2112
|
risk: options.risk ?? 'unknown',
|
|
2076
2113
|
outcome: options.outcome ?? 'unknown',
|
|
2077
2114
|
preview: options.preview ?? null,
|
|
2115
|
+
runtimeEnvironment: runtimeEnvironment.active,
|
|
2078
2116
|
};
|
|
2079
2117
|
await appendJsonl(harnessFile(projectRoot, OPENPRD_HARNESS_ITERATIONS), event);
|
|
2080
2118
|
await writeRunState(projectRoot, {
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 核心功能
|
|
3
|
+
* 从 Codex 原始会话事件中抽取可审查证据,并隔离系统提示、AGENTS 和工具 schema 噪声。
|
|
4
|
+
*
|
|
5
|
+
* 输入
|
|
6
|
+
* 接收 JSONL 事件对象、消息对象或文本片段。
|
|
7
|
+
*
|
|
8
|
+
* 输出
|
|
9
|
+
* 输出带 sourceKind 的 evidence records,以及供 knowledge matching 使用的清理后文本。
|
|
10
|
+
*
|
|
11
|
+
* 定位
|
|
12
|
+
* 位于会话证据层,服务 daily review、knowledge review 和项目经验召回,不直接读写工作区。
|
|
13
|
+
*
|
|
14
|
+
* 依赖
|
|
15
|
+
* 仅依赖标准 JavaScript,不依赖 CLI、文件系统或 OpenPrd workspace 状态。
|
|
16
|
+
*
|
|
17
|
+
* 维护规则
|
|
18
|
+
* 这里负责证据卫生,不负责判断需求类型;新增来源时先补 sourceKind 和污染样例测试。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const CONTEXT_ONLY_PATTERNS = [
|
|
22
|
+
/\bAGENTS\.md instructions\b/i,
|
|
23
|
+
/<INSTRUCTIONS>/i,
|
|
24
|
+
/<\/INSTRUCTIONS>/i,
|
|
25
|
+
/<environment_context>/i,
|
|
26
|
+
/# Tools\b/i,
|
|
27
|
+
/## Skills\b/i,
|
|
28
|
+
/### Available skills\b/i,
|
|
29
|
+
/\bnamespace\s+\w+\s*\{/i,
|
|
30
|
+
/\btool schema\b/i,
|
|
31
|
+
/\bYou are Codex\b/i,
|
|
32
|
+
/\bOpenPrd Harness\b/i,
|
|
33
|
+
/\bproject-doc\b/i,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const CONTEXT_ROLE_SET = new Set(['system', 'developer']);
|
|
37
|
+
|
|
38
|
+
const GENERIC_FRAMEWORK_TERMS = new Set([
|
|
39
|
+
'agent',
|
|
40
|
+
'agents',
|
|
41
|
+
'agentruntime',
|
|
42
|
+
'codex',
|
|
43
|
+
'context',
|
|
44
|
+
'developer',
|
|
45
|
+
'hook',
|
|
46
|
+
'hooks',
|
|
47
|
+
'harness',
|
|
48
|
+
'instruction',
|
|
49
|
+
'instructions',
|
|
50
|
+
'openprd',
|
|
51
|
+
'schema',
|
|
52
|
+
'skill',
|
|
53
|
+
'skills',
|
|
54
|
+
'system',
|
|
55
|
+
'tool',
|
|
56
|
+
'tools',
|
|
57
|
+
'workflow',
|
|
58
|
+
'workflows',
|
|
59
|
+
'项目级',
|
|
60
|
+
'项目经验',
|
|
61
|
+
'沉淀',
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
function normalizeWhitespace(value) {
|
|
65
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function trimEvidenceText(value, max = 1200) {
|
|
69
|
+
const text = normalizeWhitespace(value);
|
|
70
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeEventType(event = {}) {
|
|
74
|
+
return String(
|
|
75
|
+
event.type
|
|
76
|
+
?? event.event
|
|
77
|
+
?? event.kind
|
|
78
|
+
?? event.eventType
|
|
79
|
+
?? event.name
|
|
80
|
+
?? event.item?.type
|
|
81
|
+
?? event.event_msg?.type
|
|
82
|
+
?? '',
|
|
83
|
+
).trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeRole(event = {}) {
|
|
87
|
+
return String(
|
|
88
|
+
event.role
|
|
89
|
+
?? event.message?.role
|
|
90
|
+
?? event.item?.role
|
|
91
|
+
?? event.event_msg?.role
|
|
92
|
+
?? '',
|
|
93
|
+
).trim().toLowerCase();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function firstString(...values) {
|
|
97
|
+
for (const value of values) {
|
|
98
|
+
if (typeof value === 'string' && value.trim()) {
|
|
99
|
+
return value.trim();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function extractTextFromContent(content) {
|
|
106
|
+
if (typeof content === 'string') return content;
|
|
107
|
+
if (Array.isArray(content)) {
|
|
108
|
+
return content
|
|
109
|
+
.map((part) => firstString(part?.text, part?.content, part?.input, part?.output))
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.join('\n');
|
|
112
|
+
}
|
|
113
|
+
if (content && typeof content === 'object') {
|
|
114
|
+
return firstString(content.text, content.content, content.input, content.output) ?? '';
|
|
115
|
+
}
|
|
116
|
+
return '';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function extractEventText(event = {}) {
|
|
120
|
+
if (typeof event === 'string') return event;
|
|
121
|
+
const eventMsg = event.event_msg ?? {};
|
|
122
|
+
const item = event.item ?? {};
|
|
123
|
+
const message = event.message ?? {};
|
|
124
|
+
return firstString(
|
|
125
|
+
event.text,
|
|
126
|
+
event.content,
|
|
127
|
+
event.output,
|
|
128
|
+
event.final_answer,
|
|
129
|
+
event.summary,
|
|
130
|
+
event.prompt,
|
|
131
|
+
event.user_message,
|
|
132
|
+
event.agent_message,
|
|
133
|
+
eventMsg.user_message,
|
|
134
|
+
eventMsg.agent_message,
|
|
135
|
+
eventMsg.final_answer,
|
|
136
|
+
eventMsg.summary,
|
|
137
|
+
eventMsg.message,
|
|
138
|
+
item.final_answer,
|
|
139
|
+
item.summary,
|
|
140
|
+
item.text,
|
|
141
|
+
item.output,
|
|
142
|
+
message.text,
|
|
143
|
+
extractTextFromContent(message.content),
|
|
144
|
+
extractTextFromContent(item.content),
|
|
145
|
+
) ?? '';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function looksLikeContextOnly(text, event = {}) {
|
|
149
|
+
const role = normalizeRole(event);
|
|
150
|
+
if (CONTEXT_ROLE_SET.has(role)) return true;
|
|
151
|
+
return CONTEXT_ONLY_PATTERNS.some((pattern) => pattern.test(text));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function classifyEvent(event = {}, text = '') {
|
|
155
|
+
const role = normalizeRole(event);
|
|
156
|
+
const type = normalizeEventType(event).toLowerCase();
|
|
157
|
+
const eventMsg = event.event_msg ?? {};
|
|
158
|
+
const item = event.item ?? {};
|
|
159
|
+
if (looksLikeContextOnly(text, event)) {
|
|
160
|
+
return { sourceKind: 'context-only', reason: 'system-context' };
|
|
161
|
+
}
|
|
162
|
+
if (role === 'user' || eventMsg.user_message) {
|
|
163
|
+
return { sourceKind: 'user-direct', reason: 'user-message' };
|
|
164
|
+
}
|
|
165
|
+
if (
|
|
166
|
+
role === 'assistant'
|
|
167
|
+
|| eventMsg.agent_message
|
|
168
|
+
|| eventMsg.final_answer
|
|
169
|
+
|| item.final_answer
|
|
170
|
+
|| type.includes('final')
|
|
171
|
+
) {
|
|
172
|
+
return { sourceKind: 'assistant-final', reason: 'assistant-message' };
|
|
173
|
+
}
|
|
174
|
+
if (
|
|
175
|
+
type.includes('task_complete')
|
|
176
|
+
|| event.task_complete
|
|
177
|
+
|| eventMsg.task_complete
|
|
178
|
+
|| item.type === 'task_complete'
|
|
179
|
+
) {
|
|
180
|
+
return { sourceKind: 'assistant-final', reason: 'task-complete' };
|
|
181
|
+
}
|
|
182
|
+
if (
|
|
183
|
+
type.includes('function_call')
|
|
184
|
+
|| type.includes('tool')
|
|
185
|
+
|| event.tool
|
|
186
|
+
|| event.name
|
|
187
|
+
|| item.name
|
|
188
|
+
) {
|
|
189
|
+
return { sourceKind: 'tool-action', reason: 'tool-event' };
|
|
190
|
+
}
|
|
191
|
+
if (/\b(git status|npm test|node --test|openprd .*--verify|doctor)\b/i.test(text)) {
|
|
192
|
+
return { sourceKind: 'tool-action', reason: 'command-evidence' };
|
|
193
|
+
}
|
|
194
|
+
return { sourceKind: 'context-only', reason: 'unclassified' };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeEvidenceRecord(record = {}, index = 0) {
|
|
198
|
+
const text = trimEvidenceText(record.text);
|
|
199
|
+
if (!text) return null;
|
|
200
|
+
const sourceKind = String(record.sourceKind ?? 'context-only').trim() || 'context-only';
|
|
201
|
+
return {
|
|
202
|
+
sourceKind,
|
|
203
|
+
text,
|
|
204
|
+
reason: String(record.reason ?? '').trim() || null,
|
|
205
|
+
role: String(record.role ?? '').trim() || null,
|
|
206
|
+
eventType: String(record.eventType ?? '').trim() || null,
|
|
207
|
+
eventIndex: Number.isFinite(Number(record.eventIndex)) ? Number(record.eventIndex) : index,
|
|
208
|
+
confidence: Number.isFinite(Number(record.confidence)) ? Number(record.confidence) : (sourceKind === 'context-only' ? 0.2 : 0.8),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function extractSessionEvidenceRecords(events = [], options = {}) {
|
|
213
|
+
const maxRecords = Math.max(1, Number(options.maxRecords ?? 80));
|
|
214
|
+
return (Array.isArray(events) ? events : [events])
|
|
215
|
+
.map((event, index) => {
|
|
216
|
+
const text = extractEventText(event);
|
|
217
|
+
if (!normalizeWhitespace(text)) return null;
|
|
218
|
+
const classification = classifyEvent(event, text);
|
|
219
|
+
return normalizeEvidenceRecord({
|
|
220
|
+
...classification,
|
|
221
|
+
text,
|
|
222
|
+
role: normalizeRole(event),
|
|
223
|
+
eventType: normalizeEventType(event),
|
|
224
|
+
eventIndex: index,
|
|
225
|
+
}, index);
|
|
226
|
+
})
|
|
227
|
+
.filter(Boolean)
|
|
228
|
+
.slice(0, maxRecords);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function normalizeSessionEvidenceRecords(records = []) {
|
|
232
|
+
return (Array.isArray(records) ? records : [records])
|
|
233
|
+
.map((record, index) => normalizeEvidenceRecord(record, index))
|
|
234
|
+
.filter(Boolean);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function cleanEvidenceRecords(records = [], options = {}) {
|
|
238
|
+
const includeContextOnly = options.includeContextOnly === true;
|
|
239
|
+
return normalizeSessionEvidenceRecords(records)
|
|
240
|
+
.filter((record) => includeContextOnly || record.sourceKind !== 'context-only');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function buildCleanEvidenceText(records = [], options = {}) {
|
|
244
|
+
const maxRecords = Math.max(1, Number(options.maxRecords ?? 24));
|
|
245
|
+
return cleanEvidenceRecords(records, options)
|
|
246
|
+
.slice(0, maxRecords)
|
|
247
|
+
.map((record) => `[${record.sourceKind}] ${record.text}`)
|
|
248
|
+
.join('\n');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function normalizeSearchText(value) {
|
|
252
|
+
return String(value ?? '')
|
|
253
|
+
.toLowerCase()
|
|
254
|
+
.replace(/[`"'()[\]{}:;,!?]/g, ' ')
|
|
255
|
+
.replace(/[_/\\.-]+/g, ' ')
|
|
256
|
+
.replace(/\s+/g, ' ')
|
|
257
|
+
.trim();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function sanitizeEvidenceText(value) {
|
|
261
|
+
const text = String(value ?? '');
|
|
262
|
+
if (!text.trim()) return '';
|
|
263
|
+
const lines = text.split(/\r?\n/);
|
|
264
|
+
const kept = [];
|
|
265
|
+
let skipBlock = false;
|
|
266
|
+
for (const line of lines) {
|
|
267
|
+
const startsContextBlock = /<INSTRUCTIONS>|<environment_context>|# Tools\b|### Available skills\b/i.test(line);
|
|
268
|
+
if (startsContextBlock) {
|
|
269
|
+
skipBlock = true;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (skipBlock && /<\/INSTRUCTIONS>|<\/environment_context>/i.test(line)) {
|
|
273
|
+
skipBlock = false;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (skipBlock) continue;
|
|
277
|
+
if (CONTEXT_ONLY_PATTERNS.some((pattern) => pattern.test(line))) continue;
|
|
278
|
+
kept.push(line);
|
|
279
|
+
}
|
|
280
|
+
return normalizeWhitespace(kept.join('\n'));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isGenericFrameworkTerm(token) {
|
|
284
|
+
const normalized = normalizeSearchText(token);
|
|
285
|
+
return !normalized || GENERIC_FRAMEWORK_TERMS.has(normalized);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function filterGenericFrameworkTokens(tokens = []) {
|
|
289
|
+
return [...new Set(tokens.filter((token) => !isGenericFrameworkTerm(token)))];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function buildSessionEvidenceReviewSource(options = {}) {
|
|
293
|
+
const records = cleanEvidenceRecords(options.records ?? []);
|
|
294
|
+
const title = firstString(options.title, records.find((record) => record.sourceKind === 'user-direct')?.text, '只读复盘证据候选');
|
|
295
|
+
const sourceId = firstString(options.sourceId, title, 'session-evidence-review');
|
|
296
|
+
const sourcePath = firstString(options.sourcePath, 'session-evidence');
|
|
297
|
+
const userRecords = records.filter((record) => record.sourceKind === 'user-direct');
|
|
298
|
+
const assistantRecords = records.filter((record) => record.sourceKind === 'assistant-final');
|
|
299
|
+
const toolRecords = records.filter((record) => record.sourceKind === 'tool-action');
|
|
300
|
+
const symptoms = [...userRecords, ...assistantRecords]
|
|
301
|
+
.map((record) => record.text)
|
|
302
|
+
.slice(0, 8);
|
|
303
|
+
const rootCauseCandidates = records
|
|
304
|
+
.filter((record) => ['assistant-final', 'tool-action', 'repo-state'].includes(record.sourceKind))
|
|
305
|
+
.slice(0, 6)
|
|
306
|
+
.map((record) => ({
|
|
307
|
+
title: record.text,
|
|
308
|
+
nextSteps: ['把这条证据和用户目标重新核对,只在确认可复用时推荐项目级沉淀。'],
|
|
309
|
+
sourceKind: record.sourceKind,
|
|
310
|
+
}));
|
|
311
|
+
return {
|
|
312
|
+
kind: 'session-evidence-review',
|
|
313
|
+
evidenceOnly: true,
|
|
314
|
+
sourceId,
|
|
315
|
+
sourcePath,
|
|
316
|
+
primaryPath: sourcePath,
|
|
317
|
+
sourcePaths: [sourcePath],
|
|
318
|
+
title,
|
|
319
|
+
status: 'needs-review',
|
|
320
|
+
symptoms,
|
|
321
|
+
attentionGates: [],
|
|
322
|
+
correlationFields: [],
|
|
323
|
+
extraContextFields: [],
|
|
324
|
+
missingCorrelationFields: [],
|
|
325
|
+
eventNames: [...new Set(records.map((record) => record.sourceKind))],
|
|
326
|
+
rootCauseCandidates,
|
|
327
|
+
evidenceSources: records.map((record) => ({
|
|
328
|
+
kind: record.sourceKind,
|
|
329
|
+
path: `${sourcePath}#${record.eventIndex}`,
|
|
330
|
+
reason: record.reason,
|
|
331
|
+
})),
|
|
332
|
+
queryExamples: [
|
|
333
|
+
'先只看 user-direct、assistant-final、tool-action 证据,不把 AGENTS 或系统提示当成用户诉求。',
|
|
334
|
+
'如果复盘证据跨多轮重复出现,再推荐用户把它保留为当前项目经验。',
|
|
335
|
+
],
|
|
336
|
+
abstractPattern: '从只读复盘证据中先隔离系统上下文噪声,再把重复摩擦或可复用判断沉淀为项目级候选。',
|
|
337
|
+
triggers: records
|
|
338
|
+
.filter((record) => record.sourceKind !== 'context-only')
|
|
339
|
+
.map((record) => `${record.sourceKind}: ${record.text}`)
|
|
340
|
+
.slice(0, 8),
|
|
341
|
+
prevention: [
|
|
342
|
+
'raw session 先抽取 clean evidence,再进入候选判断。',
|
|
343
|
+
'AGENTS、system、developer、tool schema 和 skill list 只能作为 context-only 背景,不触发用户诉求或项目经验候选。',
|
|
344
|
+
'项目级沉淀先作为候选推荐给用户确认,不自动 promote。',
|
|
345
|
+
],
|
|
346
|
+
verificationSteps: [
|
|
347
|
+
'确认每条证据都有 sourceKind。',
|
|
348
|
+
'确认候选推荐没有依赖 context-only 命中。',
|
|
349
|
+
'确认用户确认前只生成候选,不修改正式项目 skill。',
|
|
350
|
+
],
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export {
|
|
355
|
+
buildCleanEvidenceText,
|
|
356
|
+
buildSessionEvidenceReviewSource,
|
|
357
|
+
cleanEvidenceRecords,
|
|
358
|
+
extractSessionEvidenceRecords,
|
|
359
|
+
filterGenericFrameworkTokens,
|
|
360
|
+
isGenericFrameworkTerm,
|
|
361
|
+
normalizeSessionEvidenceRecords,
|
|
362
|
+
sanitizeEvidenceText,
|
|
363
|
+
};
|