@openprd/cli 0.1.10 → 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.
Files changed (146) hide show
  1. package/.openprd/benchmarks/index.md +17 -15
  2. package/.openprd/benchmarks/sources.yaml +53 -1
  3. package/.openprd/changes/adaptive-project-framing/.openprd.yaml +2 -0
  4. package/.openprd/changes/adaptive-project-framing/design.md +47 -0
  5. package/.openprd/changes/adaptive-project-framing/proposal.md +35 -0
  6. package/.openprd/changes/adaptive-project-framing/specs/agent-requirements/spec.md +16 -0
  7. package/.openprd/changes/adaptive-project-framing/task-events.jsonl +20 -0
  8. package/.openprd/changes/adaptive-project-framing/tasks.md +340 -0
  9. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/.openprd.yaml +2 -0
  10. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/design.md +34 -0
  11. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/proposal.md +23 -0
  12. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/specs/agent-requirements/spec.md +16 -0
  13. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/task-events.jsonl +14 -0
  14. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/tasks.md +238 -0
  15. package/.openprd/changes/loop-isolated-worktree-commit/.openprd.yaml +2 -0
  16. package/.openprd/changes/loop-isolated-worktree-commit/design.md +53 -0
  17. package/.openprd/changes/loop-isolated-worktree-commit/proposal.md +38 -0
  18. package/.openprd/changes/loop-isolated-worktree-commit/specs/agent-requirements/spec.md +16 -0
  19. package/.openprd/changes/loop-isolated-worktree-commit/task-events.jsonl +22 -0
  20. package/.openprd/changes/loop-isolated-worktree-commit/tasks.md +357 -0
  21. package/.openprd/changes/openprd/.openprd.yaml +2 -0
  22. package/.openprd/changes/openprd/design.md +46 -0
  23. package/.openprd/changes/openprd/proposal.md +36 -0
  24. package/.openprd/changes/openprd/specs/agent-requirements/spec.md +16 -0
  25. package/.openprd/changes/openprd/task-events.jsonl +23 -0
  26. package/.openprd/changes/openprd/tasks.md +242 -0
  27. package/.openprd/changes/pm-human-in-the-loop/.openprd.yaml +2 -0
  28. package/.openprd/changes/pm-human-in-the-loop/design.md +38 -0
  29. package/.openprd/changes/pm-human-in-the-loop/proposal.md +29 -0
  30. package/.openprd/changes/pm-human-in-the-loop/specs/agent-requirements/spec.md +16 -0
  31. package/.openprd/changes/pm-human-in-the-loop/task-events.jsonl +18 -0
  32. package/.openprd/changes/pm-human-in-the-loop/tasks.md +306 -0
  33. package/.openprd/changes/test-strategy-router/.openprd.yaml +2 -0
  34. package/.openprd/changes/test-strategy-router/design.md +65 -0
  35. package/.openprd/changes/test-strategy-router/proposal.md +45 -0
  36. package/.openprd/changes/test-strategy-router/specs/agent-requirements/spec.md +16 -0
  37. package/.openprd/changes/test-strategy-router/task-events.jsonl +1 -0
  38. package/.openprd/changes/test-strategy-router/tasks.md +132 -0
  39. package/.openprd/changes/verify-boundaries-and-historical-refresh/.openprd.yaml +2 -0
  40. package/.openprd/changes/verify-boundaries-and-historical-refresh/design.md +28 -0
  41. package/.openprd/changes/verify-boundaries-and-historical-refresh/proposal.md +23 -0
  42. package/.openprd/changes/verify-boundaries-and-historical-refresh/specs/agent-requirements/spec.md +16 -0
  43. package/.openprd/changes/verify-boundaries-and-historical-refresh/task-events.jsonl +11 -0
  44. package/.openprd/changes/verify-boundaries-and-historical-refresh/tasks.md +66 -0
  45. package/.openprd/changes/vibe-coding-dev-check/.openprd.yaml +2 -0
  46. package/.openprd/changes/vibe-coding-dev-check/design.md +37 -0
  47. package/.openprd/changes/vibe-coding-dev-check/proposal.md +28 -0
  48. package/.openprd/changes/vibe-coding-dev-check/specs/agent-requirements/spec.md +16 -0
  49. package/.openprd/changes/vibe-coding-dev-check/task-events.jsonl +18 -0
  50. package/.openprd/changes/vibe-coding-dev-check/tasks.md +306 -0
  51. package/.openprd/design/README.md +11 -6
  52. package/.openprd/design/README_EN.md +4 -1
  53. package/.openprd/design/active/asset-spec.md +6 -0
  54. package/.openprd/design/active/direction-plan.md +5 -5
  55. package/.openprd/design/active/selected-direction.md +2 -0
  56. package/.openprd/design/anti-slop.md +26 -1
  57. package/.openprd/design/assets/surface-presets.md +6 -0
  58. package/.openprd/design/checklists/ui-quality-gate.md +7 -0
  59. package/.openprd/design/lenses/frontend-lenses.md +28 -0
  60. package/.openprd/design/recipes/content-experience.md +8 -1
  61. package/.openprd/design/recipes/operational-tool.md +7 -0
  62. package/.openprd/design/recipes/product-launch.md +8 -1
  63. package/.openprd/design/templates/README.md +6 -3
  64. package/.openprd/design/templates/README_EN.md +6 -3
  65. package/.openprd/design/templates/ops-dashboard.html +8 -1
  66. package/.openprd/design/templates/product-launch.html +2 -1
  67. package/.openprd/design/themes/theme-catalog.json +23 -5
  68. package/.openprd/engagements/active/prd.md +103 -100
  69. package/.openprd/schema/prd.schema.yaml +15 -0
  70. package/.openprd/standards/config.json +7 -1
  71. package/.openprd/templates/base/intake.md +13 -0
  72. package/.openprd/templates/base/prd.md +15 -0
  73. package/AGENTS.md +14 -4
  74. package/README.md +107 -14
  75. package/README_EN.md +131 -13
  76. package/package.json +1 -1
  77. package/scripts/dev-check-wrapup-copy.mjs +46 -21
  78. package/skills/openprd-benchmark-router/SKILL.md +2 -1
  79. package/skills/openprd-diagram-review/SKILL.md +27 -2
  80. package/skills/openprd-diagram-review/references/explanation-svg-patterns.md +131 -0
  81. package/skills/openprd-discovery-loop/SKILL.md +2 -2
  82. package/skills/openprd-frontend-design/SKILL.md +44 -20
  83. package/skills/openprd-frontend-design/references/design-asset-contract.md +6 -0
  84. package/skills/openprd-frontend-design/references/direction-engine.md +29 -4
  85. package/skills/openprd-harness/SKILL.md +22 -16
  86. package/skills/openprd-quality/SKILL.md +10 -8
  87. package/skills/openprd-requirement-intake/SKILL.md +5 -3
  88. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +2 -2
  89. package/skills/openprd-requirement-intake/references/startup-validation-lens.md +98 -13
  90. package/skills/openprd-router/SKILL.md +2 -1
  91. package/skills/openprd-shared/SKILL.md +27 -17
  92. package/skills/openprd-standards/SKILL.md +4 -2
  93. package/src/agent-canonical-content.js +808 -0
  94. package/src/agent-integration.js +259 -778
  95. package/src/brainstorm-artifacts.js +156 -8
  96. package/src/brainstorm-presentation.js +14 -4
  97. package/src/brainstorm.js +165 -2
  98. package/src/canvas-app.html.js +2910 -0
  99. package/src/canvas-i18n.js +274 -0
  100. package/src/canvas-workspace.js +2271 -0
  101. package/src/cli/args.js +72 -4
  102. package/src/cli/basic-print.js +3 -0
  103. package/src/cli/canvas-print.js +106 -0
  104. package/src/cli/doctor-print.js +20 -0
  105. package/src/cli/print.js +2 -0
  106. package/src/cli/quality-commands.js +222 -0
  107. package/src/cli/quality-print.js +37 -5
  108. package/src/cli/run-print.js +5 -0
  109. package/src/cli/workflow-print.js +3 -0
  110. package/src/codex-app-server-relay.js +251 -0
  111. package/src/codex-hook-runner-template.mjs +963 -65
  112. package/src/design-starter-support.js +2 -2
  113. package/src/design-starter.js +39 -3
  114. package/src/dev-standards.js +125 -10
  115. package/src/diagram-core.js +390 -171
  116. package/src/discovery.js +2 -8
  117. package/src/docs-compact.js +125 -0
  118. package/src/execution-strategy.js +1 -1
  119. package/src/fleet.js +9 -3
  120. package/src/guide.js +227 -0
  121. package/src/html-artifacts.js +825 -51
  122. package/src/knowledge.js +101 -20
  123. package/src/language-policy.js +63 -3
  124. package/src/learning-review.js +1 -1
  125. package/src/loop.js +1 -1
  126. package/src/openprd.js +68 -131
  127. package/src/openspec/change-validate.js +2 -2
  128. package/src/openspec/constants.js +6 -7
  129. package/src/openspec/execute.js +2 -2
  130. package/src/openspec/generate.js +318 -102
  131. package/src/openspec/migration.js +168 -0
  132. package/src/openspec/paths.js +1 -22
  133. package/src/prd-core.js +102 -3
  134. package/src/quality-html-artifact.js +30 -5
  135. package/src/quality-visual-review.js +257 -14
  136. package/src/quality.js +66 -18
  137. package/src/review-presentation.js +60 -0
  138. package/src/run-harness.js +41 -3
  139. package/src/session-evidence.js +363 -0
  140. package/src/standards.js +64 -0
  141. package/src/test-strategy.js +7 -4
  142. package/src/visual-compare-centering.js +622 -0
  143. package/src/visual-compare-core.js +1123 -0
  144. package/src/visual-compare.js +620 -698
  145. package/src/workspace-core.js +132 -4
  146. package/src/workspace-workflow.js +172 -10
package/src/knowledge.js CHANGED
@@ -1,7 +1,15 @@
1
+ import crypto from 'node:crypto';
1
2
  import fs from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import { appendJsonl, cjoin, exists, readJson, readJsonl, writeJson, writeText } from './fs-utils.js';
4
5
  import { resolveQualityLearningSource } from './quality-learning.js';
6
+ import {
7
+ buildSessionEvidenceReviewSource,
8
+ extractSessionEvidenceRecords,
9
+ filterGenericFrameworkTokens,
10
+ normalizeSessionEvidenceRecords,
11
+ sanitizeEvidenceText,
12
+ } from './session-evidence.js';
5
13
  import { timestamp } from './time.js';
6
14
 
7
15
  const KNOWLEDGE_DIR = cjoin('.openprd', 'knowledge');
@@ -210,7 +218,7 @@ function normalizeSearchTokens(value) {
210
218
  .map((item) => item.trim())
211
219
  .filter((item) => item.length >= 3);
212
220
  const hanTokens = String(value ?? '').match(/[\u4e00-\u9fa5]{2,}/g) ?? [];
213
- return uniq([...asciiTokens, ...hanTokens]);
221
+ return filterGenericFrameworkTokens(uniq([...asciiTokens, ...hanTokens]));
214
222
  }
215
223
 
216
224
  function sortByLength(items = []) {
@@ -252,16 +260,17 @@ function scoreQueryAgainstFields(queryText, queryTokens, fields = []) {
252
260
  let score = 0;
253
261
  const matchedOn = [];
254
262
  for (const field of fields) {
255
- const text = String(field ?? '').trim();
263
+ const text = sanitizeEvidenceText(field);
256
264
  if (!text) continue;
257
265
  const normalized = normalizeSearchText(text);
258
266
  if (!normalized) continue;
267
+ const fieldTokens = normalizeSearchTokens(text);
268
+ if (fieldTokens.length === 0) continue;
259
269
  let matched = false;
260
270
  if (normalized.length >= 6 && queryText.includes(normalized)) {
261
271
  matched = true;
262
272
  score += normalized.length >= 18 ? 10 : 7;
263
273
  } else {
264
- const fieldTokens = normalizeSearchTokens(text);
265
274
  const overlap = fieldTokens.filter((token) => queryTokens.includes(token));
266
275
  if (overlap.length > 0) {
267
276
  matched = true;
@@ -858,16 +867,56 @@ function deriveKnowledgeNames(source, options = {}) {
858
867
  return { incidentId, patternId, skillName };
859
868
  }
860
869
 
861
- function buildTurnReviewTitle(raw, source) {
862
- return firstString(
863
- raw?.title,
864
- raw?.summary?.title,
865
- raw?.promptPreview,
866
- raw?.prompt,
867
- source.title,
868
- source.sourceId,
869
- '项目经验草案',
870
- ) ?? '项目经验草案';
870
+ const LOW_SIGNAL_TITLE_PATTERN = /^(只回复|回复|继续|可以|好的|没问题|嗯|哦|行|是|对|确认|同意|执行|开始|ok|okay|yes|done|go)([\s,。,.!!??]|$)/i;
871
+
872
+ function titleLooksLikeRawInstruction(text) {
873
+ const value = String(text ?? '').trim();
874
+ if (!value) return true;
875
+ if (value.length < 8) return true;
876
+ return LOW_SIGNAL_TITLE_PATTERN.test(value);
877
+ }
878
+
879
+ function describeTouchedFilesTitle(touchedFiles) {
880
+ const files = [...new Set((touchedFiles ?? []).map((file) => String(file ?? '').trim()).filter(Boolean))];
881
+ if (files.length === 0) return null;
882
+ const primary = files[0].split('/').filter(Boolean).at(-1);
883
+ return files.length > 1
884
+ ? `围绕 ${primary} 等 ${files.length} 个文件的实现经验`
885
+ : `围绕 ${primary} 的实现经验`;
886
+ }
887
+
888
+ function buildTurnReviewTitle(raw, source, touchedFiles = []) {
889
+ const structured = firstString(raw?.title, raw?.summary?.title, source.title);
890
+ if (structured && !titleLooksLikeRawInstruction(structured)) return structured;
891
+ const rootCause = source.rootCauseCandidates?.[0]?.title;
892
+ if (rootCause && !titleLooksLikeRawInstruction(rootCause)) return rootCause;
893
+ const prompt = firstString(raw?.promptPreview, raw?.prompt);
894
+ if (prompt && !titleLooksLikeRawInstruction(prompt)) return prompt;
895
+ return describeTouchedFilesTitle(touchedFiles)
896
+ ?? firstString(structured, source.sourceId, '项目经验草案')
897
+ ?? '项目经验草案';
898
+ }
899
+
900
+ function knowledgeCandidateFingerprint(categories = [], touchedFiles = [], sourceSeed = null) {
901
+ const normalizedCategories = [...new Set((categories ?? []).map((item) => String(item ?? '').trim()).filter(Boolean))].sort();
902
+ const normalizedFiles = [...new Set((touchedFiles ?? []).map((item) => String(item ?? '').trim()).filter(Boolean))].sort();
903
+ const normalizedSourceSeed = String(sourceSeed ?? '').trim();
904
+ return crypto.createHash('sha256').update(JSON.stringify([normalizedCategories, normalizedFiles, normalizedSourceSeed])).digest('hex').slice(0, 16);
905
+ }
906
+
907
+ async function findPendingCandidateByFingerprint(projectRoot, fingerprint, excludeId) {
908
+ const index = await readKnowledgeIndex(projectRoot);
909
+ for (const entry of index.candidates ?? []) {
910
+ if (!entry?.candidateId || entry.candidateId === excludeId) continue;
911
+ const candidate = await readCandidateById(projectRoot, entry.candidateId);
912
+ if (!candidate || !isPendingKnowledgeCandidateStatus(normalizeCandidateStatus(candidate.status))) continue;
913
+ const existingFingerprint = candidate.fingerprint
914
+ ?? knowledgeCandidateFingerprint(candidate.categories, candidate.touchedFiles);
915
+ if (existingFingerprint === fingerprint) {
916
+ return candidate;
917
+ }
918
+ }
919
+ return null;
871
920
  }
872
921
 
873
922
  async function loadRawReviewInput(projectRoot, from) {
@@ -1155,6 +1204,7 @@ function buildKnowledgeCandidateMeta({
1155
1204
  updatedAt: timestamp(),
1156
1205
  sourceKind: source.kind,
1157
1206
  sourceRef: source.sourceId,
1207
+ evidenceOnly: source.evidenceOnly === true,
1158
1208
  title,
1159
1209
  summary,
1160
1210
  categories,
@@ -1365,6 +1415,11 @@ export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
1365
1415
  const turnStateSource = (await exists(knowledgePath(projectRoot, OPENPRD_HARNESS_TURN_STATE))) ? OPENPRD_HARNESS_TURN_STATE : null;
1366
1416
  const from = firstString(options.from, turnStateSource, latestReportPath);
1367
1417
  const rawInput = from ? await loadRawReviewInput(projectRoot, from) : { sourcePath: null, raw: null };
1418
+ const evidenceRecords = normalizeSessionEvidenceRecords([
1419
+ ...normalizeSessionEvidenceRecords(options.evidenceRecords),
1420
+ ...extractSessionEvidenceRecords(options.sessionEvents ?? []),
1421
+ ]);
1422
+ const cleanEvidenceRecords = evidenceRecords.filter((record) => record.sourceKind !== 'context-only');
1368
1423
  const resolved = from
1369
1424
  ? await resolveQualityLearningSource(projectRoot, {
1370
1425
  from,
@@ -1374,13 +1429,20 @@ export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
1374
1429
  : { ok: false, error: 'no-review-source' };
1375
1430
  const source = resolved.ok
1376
1431
  ? resolved.source
1432
+ : (cleanEvidenceRecords.length > 0
1433
+ ? buildSessionEvidenceReviewSource({
1434
+ records: cleanEvidenceRecords,
1435
+ sourceId: firstString(options.sourceId, options.title, cleanEvidenceRecords[0]?.text),
1436
+ sourcePath: rawInput.sourcePath ?? latestReportPath ?? KNOWLEDGE_REVIEW_SIGNAL_LOG,
1437
+ title: firstString(options.title, cleanEvidenceRecords.find((record) => record.sourceKind === 'user-direct')?.text),
1438
+ })
1377
1439
  : buildSyntheticReviewSource(projectRoot, {
1378
1440
  signal: options.signal,
1379
1441
  recentSignals,
1380
1442
  touchedFiles: options.touchedFiles,
1381
1443
  sourcePath: rawInput.sourcePath ?? latestReportPath ?? KNOWLEDGE_REVIEW_SIGNAL_LOG,
1382
1444
  title: firstString(options.title, readJsonObject(rawInput.raw)?.title),
1383
- });
1445
+ }));
1384
1446
  if (!resolved.ok && source.evidenceSources.length === 0 && source.rootCauseCandidates.length === 0) {
1385
1447
  return {
1386
1448
  ok: true,
@@ -1415,28 +1477,39 @@ export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
1415
1477
  const reviewSignals = reviewContext.reviewSignals;
1416
1478
  const categories = buildKnowledgeCategories({ source, touchedFiles: substantiveTouchedFiles, reviewSignals });
1417
1479
  const reasons = categories.map(categoryReason);
1480
+ const allowEvidenceOnly = options.allowEvidenceOnly === true || source.evidenceOnly === true || source.kind === 'session-evidence-review';
1418
1481
  const hasStrongSignal = categories.length > 0
1419
1482
  || source.rootCauseCandidates.length > 0
1420
1483
  || source.symptoms.length > 1
1421
1484
  || source.kind === 'completion-review'
1485
+ || source.kind === 'session-evidence-review'
1422
1486
  || reviewSignals.some((signal) => signal.ok === true || signal.productionReady === true);
1423
1487
 
1424
- if (substantiveTouchedFiles.length === 0 || !hasStrongSignal) {
1488
+ if ((substantiveTouchedFiles.length === 0 && !allowEvidenceOnly) || !hasStrongSignal) {
1425
1489
  return {
1426
1490
  ok: true,
1427
1491
  action: 'quality-knowledge-review',
1428
1492
  skipped: true,
1429
- reason: substantiveTouchedFiles.length === 0 ? 'no-substantive-touched-files' : 'no-knowledge-signal',
1493
+ reason: substantiveTouchedFiles.length === 0 && !allowEvidenceOnly ? 'no-substantive-touched-files' : 'no-knowledge-signal',
1430
1494
  sourceKind: source.kind,
1431
1495
  sourcePath: source.sourcePath,
1432
1496
  };
1433
1497
  }
1434
1498
 
1435
- const title = buildTurnReviewTitle(raw, source);
1499
+ const title = buildTurnReviewTitle(raw, source, substantiveTouchedFiles);
1436
1500
  const rawCandidateRef = firstString(raw.knowledgeCandidateId, raw.id);
1437
- const candidateId = rawCandidateRef
1501
+ let candidateId = rawCandidateRef
1438
1502
  ? (rawCandidateRef.startsWith('candidate-') ? rawCandidateRef : `candidate-${slugify(rawCandidateRef, 'knowledge')}`)
1439
1503
  : `candidate-${slugify(source.sourceId ?? title, 'knowledge')}`;
1504
+ const fingerprint = knowledgeCandidateFingerprint(
1505
+ categories,
1506
+ substantiveTouchedFiles,
1507
+ allowEvidenceOnly ? firstString(source.sourceId, source.title, source.sourcePath) : null,
1508
+ );
1509
+ const duplicateCandidate = await findPendingCandidateByFingerprint(projectRoot, fingerprint, candidateId);
1510
+ if (duplicateCandidate?.candidateId) {
1511
+ candidateId = duplicateCandidate.candidateId;
1512
+ }
1440
1513
  const promotedSource = { ...source, sourceId: candidateId };
1441
1514
  const names = deriveKnowledgeNames(promotedSource, { stablePattern: false });
1442
1515
  const candidateDir = knowledgePath(projectRoot, cjoin(KNOWLEDGE_CANDIDATES_DIR, candidateId));
@@ -1448,11 +1521,15 @@ export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
1448
1521
  const existingCandidate = await readJson(candidatePath).catch(() => null);
1449
1522
  const relativeCandidateDir = path.relative(projectRoot, candidateDir).split(path.sep).join('/');
1450
1523
  const relativeDraftSkillPath = path.relative(projectRoot, draftSkillPath).split(path.sep).join('/');
1524
+ const scopeSummary = substantiveTouchedFiles.length > 0
1525
+ ? `本轮围绕 ${substantiveTouchedFiles.length} 个可沉淀文件生成回顾。`
1526
+ : '本轮基于只读证据生成项目经验候选。';
1451
1527
  const reviewSummary = [
1452
- `本轮围绕 ${substantiveTouchedFiles.length} 个可沉淀文件生成回顾。`,
1528
+ scopeSummary,
1453
1529
  reasons[0] ?? '这次实现已经具备项目级经验抽象价值。',
1454
1530
  reviewSignals.length > 0 ? `已记录 ${reviewSignals.length} 条回顾信号。` : null,
1455
1531
  touchedFileSource === 'inferred-recent-files' ? '本轮 touched files 来自最近修改文件推断。' : null,
1532
+ allowEvidenceOnly && substantiveTouchedFiles.length === 0 ? '本轮没有把 AGENTS、system 或 tool schema 命中当成用户证据。' : null,
1456
1533
  ].filter(Boolean).join(' ');
1457
1534
  const draftCandidate = buildKnowledgeCandidateMeta({
1458
1535
  projectRoot,
@@ -1482,6 +1559,8 @@ export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
1482
1559
  const candidate = {
1483
1560
  ...draftCandidate,
1484
1561
  abstraction,
1562
+ fingerprint,
1563
+ occurrences: (readJsonObject(existingCandidate)?.occurrences ?? 0) + 1,
1485
1564
  };
1486
1565
  const userFacingExperience = buildKnowledgeUserFacingExperience({
1487
1566
  candidate,
@@ -1693,7 +1772,9 @@ function buildKnowledgeMatchQuery(options = {}) {
1693
1772
  options.activeChange,
1694
1773
  options.nextTaskTitle,
1695
1774
  ...(normalizeStringList(options.relatedFiles)),
1696
- ].filter(Boolean);
1775
+ ]
1776
+ .map((field) => sanitizeEvidenceText(field))
1777
+ .filter(Boolean);
1697
1778
  const text = candidateFields.join('\n');
1698
1779
  return {
1699
1780
  text,
@@ -1,11 +1,13 @@
1
- export const OPENPRD_CONTENT_LOCALE = 'zh-CN';
1
+ export const OPENPRD_FALLBACK_LOCALE = 'zh-CN';
2
+ export const OPENPRD_CONTENT_LOCALE = OPENPRD_FALLBACK_LOCALE;
2
3
 
3
4
  export const OPENPRD_LANGUAGE_POLICY =
4
- 'OpenPrd 默认用简体中文生成用户可见内容以及 Agent 产出的 spec、tasks 和说明文案;PRD、OpenPrd、OpenSpec、API、SDK、CLI、TypeScript、JSON、HTTP、WebSocket、字段 key、命令名、品牌名、产品名和协议名等必要专有名词按原文保留。';
5
+ 'OpenPrd 用户可见内容以及 Agent 产出的 spec、tasks 和说明文案应跟随用户当前主语言;无法判断时才使用简体中文兜底。PRD、OpenPrd、OpenSpec、API、SDK、CLI、TypeScript、JSON、HTTP、WebSocket、字段 key、命令名、品牌名、产品名和协议名等必要专有名词按原文保留。';
5
6
 
6
7
  export const TBD_ZH = '待补充';
7
8
 
8
9
  const CJK_RE = /[\u3400-\u9fff]/;
10
+ const LATIN_WORD_RE = /[A-Za-z][A-Za-z0-9+_.-]*/g;
9
11
 
10
12
  export function scalarZh(value, fallback = TBD_ZH) {
11
13
  if (value === null || value === undefined) {
@@ -19,14 +21,72 @@ export function hasSimplifiedChinese(value) {
19
21
  return CJK_RE.test(String(value ?? ''));
20
22
  }
21
23
 
24
+ export function englishWords(value) {
25
+ return String(value ?? '').match(LATIN_WORD_RE) ?? [];
26
+ }
27
+
28
+ export function isChineseLocale(locale) {
29
+ return normalizeOutputLocale(locale).startsWith('zh');
30
+ }
31
+
32
+ export function normalizeOutputLocale(locale, fallback = OPENPRD_FALLBACK_LOCALE) {
33
+ const text = String(locale ?? '').trim();
34
+ if (!text) {
35
+ return fallback;
36
+ }
37
+ const lower = text.toLowerCase();
38
+ if (lower === 'cn' || lower.startsWith('zh')) {
39
+ return 'zh-CN';
40
+ }
41
+ if (lower.startsWith('en')) {
42
+ return 'en';
43
+ }
44
+ return text;
45
+ }
46
+
47
+ export function detectPrimaryLanguage(values, fallback = OPENPRD_FALLBACK_LOCALE) {
48
+ const text = (Array.isArray(values) ? values : [values])
49
+ .map((value) => String(value ?? ''))
50
+ .join('\n');
51
+ const cjkCount = (text.match(CJK_RE) ?? []).length;
52
+ const latinCount = englishWords(text).length;
53
+ if (cjkCount > 0) {
54
+ return 'zh-CN';
55
+ }
56
+ if (latinCount >= 4) {
57
+ return 'en';
58
+ }
59
+ return normalizeOutputLocale(fallback);
60
+ }
61
+
62
+ export function isEnglishHeavyText(value) {
63
+ const text = String(value ?? '').trim();
64
+ if (!text || hasSimplifiedChinese(text)) {
65
+ return false;
66
+ }
67
+ return englishWords(text).length >= 4;
68
+ }
69
+
22
70
  export function preferSimplifiedChinese(value, fallback = TBD_ZH) {
23
71
  const text = scalarZh(value, '');
24
72
  return hasSimplifiedChinese(text) ? text : fallback;
25
73
  }
26
74
 
75
+ export function preferUserLanguage(value, locale = OPENPRD_FALLBACK_LOCALE, fallbacks = {}) {
76
+ const text = scalarZh(value, '');
77
+ if (text) {
78
+ return text;
79
+ }
80
+ const normalizedLocale = normalizeOutputLocale(locale);
81
+ if (!isChineseLocale(normalizedLocale) && fallbacks.en) {
82
+ return fallbacks.en;
83
+ }
84
+ return fallbacks.zh ?? fallbacks.default ?? TBD_ZH;
85
+ }
86
+
27
87
  export function languagePolicyLines() {
28
88
  return [
29
- '> 语言规则:默认用简体中文生成 PRD、spectasks 和用户可见说明;除 PRD、OpenPrd、OpenSpec、API、SDK、CLI、TypeScript、JSON、HTTP、WebSocket、字段 key、命令名、品牌名、产品名和协议名等必要专有名词外,其余内容优先写成简体中文。',
89
+ '> 语言规则:用户可见说明、PRD、spectasks 跟随用户当前主语言;无法判断时才使用简体中文兜底。PRD、OpenPrd、OpenSpec、API、SDK、CLI、TypeScript、JSON、HTTP、WebSocket、字段 key、命令名、品牌名、产品名和协议名等必要专有名词保留原文。',
30
90
  '',
31
91
  ];
32
92
  }
@@ -622,7 +622,7 @@ async function buildEvidenceManifest(ws, snapshot, options = {}) {
622
622
  ];
623
623
 
624
624
  if (changeId) {
625
- const changeRoot = path.join(ws.projectRoot, 'openprd', 'changes', changeId);
625
+ const changeRoot = path.join(ws.projectRoot, '.openprd', 'changes', changeId);
626
626
  candidateDefs.push(
627
627
  makeEvidenceCandidate({
628
628
  id: 'change-proposal',
package/src/loop.js CHANGED
@@ -550,7 +550,7 @@ async function syncLoopIsolationArtifacts(sourceRoot, targetRoot, options = {})
550
550
  LOOP_STATE,
551
551
  LOOP_BOOTSTRAP,
552
552
  RELEASE_LEDGER,
553
- options.change ? path.join('openprd', 'changes', options.change) : null,
553
+ options.change ? path.join('.openprd', 'changes', options.change) : null,
554
554
  ].filter(Boolean);
555
555
  for (const relativePath of relativePaths) {
556
556
  if (await copyRelativePath(sourceRoot, targetRoot, relativePath, { force: Boolean(options.force) })) {
package/src/openprd.js CHANGED
@@ -33,7 +33,8 @@ import { doctorOpenPrdAgentIntegration, setupOpenPrdAgentIntegration, updateOpen
33
33
  import { finishLoopWorkspace, initLoopWorkspace, nextLoopWorkspace, planLoopWorkspace, promptLoopWorkspace, runLoopWorkspace, statusLoopWorkspace, verifyLoopWorkspace } from './loop.js';
34
34
  import { timestamp } from './time.js';
35
35
  import { parseCommandArgs, usage } from './cli/args.js';
36
- import { printAcceptedSpecsResult, printAgentIntegrationResult, printBenchmarkResult, printBrainstormResult, printCaptureResult, printClarifyResult, printClassifyResult, printDesignStarterResult, printDevelopmentStandardsResult, printDiagramResult, printDiffResult, printDoctorResult, printFleetResult, printFreezeResult, printGrowthResult, printHandoffResult, printHistoryResult, printInitResult, printInterviewResult, printKnowledgeResult, printLearningResult, printLoopResult, printNextResult, printOpenPrdChangeActionResult, printOpenPrdChangesResult, printOpenSpecChangeValidationResult, printOpenSpecDiscoveryResult, printOpenSpecGenerateResult, printOpenSpecTaskResult, printPlaygroundResult, printQualityResult, printReleaseResult, printReviewResult, printRunResult, printSelfUpdateResult, printStandardsResult, printStatus, printSynthesizeResult, printUpgradeResult, printValidation, printVisualCompareResult, printVisualPrepareResult } from './cli/print.js';
36
+ import { runQualityCommand } from './cli/quality-commands.js';
37
+ import { printAcceptedSpecsResult, printAgentIntegrationResult, printBenchmarkResult, printBrainstormResult, printCanvasResult, printCaptureResult, printClarifyResult, printClassifyResult, printDesignStarterResult, printDevelopmentStandardsResult, printDiagramResult, printDiffResult, printDoctorResult, printFleetResult, printFreezeResult, printGrowthResult, printHandoffResult, printHistoryResult, printInitResult, printInterviewResult, printKnowledgeResult, printLearningResult, printLoopResult, printNextResult, printOpenPrdChangeActionResult, printOpenPrdChangesResult, printOpenSpecChangeValidationResult, printOpenSpecDiscoveryResult, printOpenSpecGenerateResult, printOpenSpecTaskResult, printPlaygroundResult, printQualityResult, printReleaseResult, printReviewResult, printRunResult, printSelfUpdateResult, printStandardsResult, printStatus, printSynthesizeResult, printUpgradeResult, printValidation, printVisualCompareResult, printVisualPrepareResult } from './cli/print.js';
37
38
  import { cjoin, exists, readJson, writeJson, writeText, writeYaml } from './fs-utils.js';
38
39
  import { diagramWorkspace } from './diagram-workspace.js';
39
40
  import { createOpenSpecDiscoveryWorkspace } from './discovery.js';
@@ -64,6 +65,7 @@ import {
64
65
  import { brainstormWorkspace } from './brainstorm.js';
65
66
  import { buildBrainstormPresentationTemplatePayload, brainstormPresentationWorkspace } from './brainstorm-presentation.js';
66
67
  import { designStarterWorkspace } from './design-starter.js';
68
+ import { canvasWorkspace } from './canvas-workspace.js';
67
69
  import { buildReviewPresentationTemplatePayload, reviewPresentationWorkspace } from './review-presentation.js';
68
70
  import { analyzeWorkspaceRegistryHygiene } from './registry-hygiene.js';
69
71
  import { syncSessionBindingFromChange } from './session-binding.js';
@@ -74,6 +76,22 @@ import { captureWorkspace, clarifyWorkspace, classifyWorkspace, computeWorkspace
74
76
  import { appendDecision, appendProgress, appendVerification, appendWorkflowEvent, buildCurrentStateSnapshot, buildWorkflowTaskGraph, computeWorkspaceDigest, CORE_TEMPLATE_FILES, ensureWorkspaceSkeleton, isSupportedProductType, loadCurrentLaneSnapshot, loadLatestVersionSnapshot, loadWorkspace, migrateWorkspaceSkeleton, normalizeVersionId, persistWorkspaceCurrentState, readVersionIndex, resolveActiveTemplatePack, resolveCurrentProductType, validateWorkspace } from './workspace-core.js';
75
77
  import { readWorkspaceRegistry } from './workspace-registry.js';
76
78
 
79
+ function buildInitTemplatePackGuidance(templatePack, options = {}) {
80
+ const label = formatTemplatePackDisplay(templatePack, { fallback: '通用产品或工程场景' });
81
+ if (options.explicit) {
82
+ return {
83
+ source: 'explicit',
84
+ templatePack,
85
+ message: `已按指定场景模板使用${label}起步;后续需求澄清或场景分类时,仍可按用户确认调整产品场景。`,
86
+ };
87
+ }
88
+ return {
89
+ source: 'default',
90
+ templatePack,
91
+ message: `未指定场景模板,已使用${label}起步;后续需求澄清或场景分类时,可再锁定为个人消费者、企业服务或 Agent 场景。`,
92
+ };
93
+ }
94
+
77
95
  async function initWorkspace(projectRoot, options) {
78
96
  const ws = await ensureWorkspaceSkeleton(projectRoot, options);
79
97
  const workspace = await loadWorkspace(projectRoot);
@@ -131,12 +149,16 @@ async function initWorkspace(projectRoot, options) {
131
149
  templatePack: currentState.templatePack,
132
150
  projectRoot,
133
151
  });
152
+ const templatePackGuidance = buildInitTemplatePackGuidance(currentState.templatePack, {
153
+ explicit: Boolean(options.templatePack),
154
+ });
134
155
  await appendProgress(workspace, [
135
156
  `已初始化工作区: ${workspace.workspaceRoot}。`,
136
157
  `场景模板: ${formatTemplatePackDisplay(currentState.templatePack, { fallback: '待确认' })}。`,
158
+ templatePackGuidance.message,
137
159
  ]);
138
160
 
139
- return { ws: workspace, created: ws.created, currentState, standards, quality, growth, agentIntegration };
161
+ return { ws: workspace, created: ws.created, currentState, templatePackGuidance, standards, quality, growth, agentIntegration };
140
162
  }
141
163
 
142
164
  async function setupAgentIntegrationWorkspace(projectRoot, options = {}) {
@@ -171,7 +193,16 @@ async function setupAgentIntegrationWorkspace(projectRoot, options = {}) {
171
193
  platform: options.platform,
172
194
  hookProfile: options.hookProfile,
173
195
  });
174
- return { ...agentIntegration, initialized: false, migration, standards, quality, growth };
196
+ return {
197
+ ...agentIntegration,
198
+ ok: agentIntegration.ok && migration.ok,
199
+ initialized: false,
200
+ migration,
201
+ standards,
202
+ quality,
203
+ growth,
204
+ errors: [...(agentIntegration.errors ?? []), ...(migration.errors ?? [])],
205
+ };
175
206
  }
176
207
 
177
208
  async function updateAgentIntegrationWorkspace(projectRoot, options = {}) {
@@ -189,7 +220,15 @@ async function updateAgentIntegrationWorkspace(projectRoot, options = {}) {
189
220
  platform: options.platform,
190
221
  hookProfile: options.hookProfile,
191
222
  });
192
- return { ...agentIntegration, migration, standards, quality, growth };
223
+ return {
224
+ ...agentIntegration,
225
+ ok: agentIntegration.ok && migration.ok,
226
+ migration,
227
+ standards,
228
+ quality,
229
+ growth,
230
+ errors: [...(agentIntegration.errors ?? []), ...(migration.errors ?? [])],
231
+ };
193
232
  }
194
233
 
195
234
  async function doctorWorkspace(projectRoot, options = {}) {
@@ -897,138 +936,34 @@ export async function main(argv = process.argv.slice(2)) {
897
936
  return result.ok ? 0 : 1;
898
937
  }
899
938
 
900
- if (command === 'standards') {
901
- const result = flags.init
902
- ? await initStandardsWorkspace(projectPath, { force: flags.force })
903
- : flags.classifyExternal !== null
904
- ? await classifyExternalReferenceWorkspace(projectPath, { externalReference: flags.classifyExternal })
905
- : await checkStandardsWorkspace(projectPath);
906
- printStandardsResult(result, flags.json);
907
- return result.ok ? 0 : 1;
908
- }
909
-
910
- if (command === 'quality') {
911
- const result = await qualityWorkspace(projectPath, {
912
- init: flags.init,
913
- verify: flags.verify,
914
- report: flags.report,
915
- html: flags.html,
916
- learn: Boolean(flags.learn),
917
- review: flags.review,
918
- from: flags.from,
919
- force: flags.force,
920
- });
921
- printQualityResult(result, flags.json);
922
- return result.ok ? 0 : 1;
923
- }
924
-
925
- if (command === 'knowledge') {
926
- const subcommand = positionals[0] ?? 'candidates';
927
- const firstArgIsProjectPath = !flags.path && positionals.length > 1 && await isDirectoryPath(positionals[1]);
928
- const knowledgeProjectPath = path.resolve(flags.path ?? (firstArgIsProjectPath ? positionals[1] : process.cwd()));
929
- let result;
930
- if (subcommand === 'candidates' || subcommand === 'list') {
931
- result = await listKnowledgeCandidates(knowledgeProjectPath, {
932
- status: flags.status ?? 'pending-review',
933
- });
934
- } else if (subcommand === 'reject') {
935
- result = await rejectKnowledgeCandidate(knowledgeProjectPath, {
936
- id: flags.id ?? positionals[1] ?? null,
937
- reason: flags.reason ?? flags.notes,
938
- });
939
- } else if (subcommand === 'archive') {
940
- result = await archiveKnowledgeCandidate(knowledgeProjectPath, {
941
- id: flags.id ?? positionals[1] ?? null,
942
- reason: flags.reason ?? flags.notes,
943
- });
944
- } else if (subcommand === 'restore') {
945
- result = await restoreKnowledgeCandidate(knowledgeProjectPath, {
946
- id: flags.id ?? positionals[1] ?? null,
947
- });
948
- } else {
949
- console.log('Usage: openprd knowledge <candidates|reject|archive|restore> [path-or-id] [--status <pending-review|all|rejected|archived|promoted|merged>] [--id <candidate-id>] [--reason <text>] [--json]');
950
- return 1;
951
- }
952
- printKnowledgeResult(result, flags.json);
953
- return result.ok ? 0 : 1;
954
- }
955
-
956
- if (command === 'visual-prepare') {
957
- const result = await visualPrepareWorkspace(projectPath, {
958
- reference: flags.reference,
959
- grid: flags.grid,
960
- boxes: flags.boxes,
961
- include: flags.include,
939
+ if (command === 'canvas') {
940
+ const result = await canvasWorkspace(projectPath, {
941
+ thread: flags.thread,
942
+ threadTitle: flags.threadTitle,
943
+ conversationTitle: flags.conversationTitle,
944
+ session: flags.session,
945
+ sessionTitle: flags.sessionTitle,
946
+ host: flags.host,
947
+ port: flags.port,
948
+ daemon: flags.daemon,
949
+ stop: flags.stop,
950
+ bridgeOutbox: flags.bridgeOutbox,
951
+ bridgeSent: flags.bridgeSent,
952
+ bridgeFailed: flags.bridgeFailed,
953
+ claim: flags.claim,
962
954
  id: flags.id,
963
- title: flags.title,
964
- out: flags.out,
965
- });
966
- printVisualPrepareResult(result, flags.json);
967
- return result.ok ? 0 : 1;
968
- }
969
-
970
- if (command === 'visual-compare') {
971
- const result = await visualCompareWorkspace(projectPath, {
972
- reference: flags.reference,
973
- actual: flags.actual,
974
- before: flags.before,
975
- after: flags.after,
976
- board: flags.board,
977
- out: flags.out,
978
- format: flags.format,
979
- quality: flags.quality,
980
- maxPanelWidth: flags.maxPanelWidth,
981
- referenceLabel: flags.referenceLabel,
982
- actualLabel: flags.actualLabel,
983
- });
984
- printVisualCompareResult(result, flags.json);
985
- return result.ok ? 0 : 1;
986
- }
987
-
988
- if (command === 'design-starter') {
989
- const result = await designStarterWorkspace(projectPath, {
990
- starter: flags.starter,
991
- out: flags.out,
992
- title: flags.title,
993
- brief: flags.brief,
994
- sections: flags.sections,
995
- noExternalFacts: flags.noExternalFacts,
996
- noBrandAssets: flags.noBrandAssets,
997
- noRealImages: flags.noRealImages,
998
- force: flags.force,
955
+ reason: flags.reason,
956
+ open: flags.open,
957
+ openTarget: flags.openTarget,
958
+ dryRun: flags.dryRun,
999
959
  });
1000
- printDesignStarterResult(result, flags.json);
960
+ printCanvasResult(result, flags.json);
1001
961
  return result.ok ? 0 : 1;
1002
962
  }
1003
963
 
1004
- if (command === 'grow') {
1005
- const firstArgIsProjectPath = !flags.path && positionals.length > 0 && await isDirectoryPath(positionals[0]);
1006
- const growthProjectPath = path.resolve(flags.path ?? (firstArgIsProjectPath ? positionals[0] : process.cwd()));
1007
- let result;
1008
- if (flags.init) {
1009
- result = await initGrowthWorkspace(growthProjectPath);
1010
- } else if (flags.apply) {
1011
- result = await applyGrowthCandidateWorkspace(growthProjectPath, { id: flags.id });
1012
- } else if (flags.reject) {
1013
- result = await rejectGrowthCandidateWorkspace(growthProjectPath, { id: flags.id, notes: flags.notes });
1014
- } else if (flags.check) {
1015
- result = await checkGrowthWorkspace(growthProjectPath);
1016
- } else {
1017
- result = await reviewGrowthWorkspace(growthProjectPath);
1018
- }
1019
- printGrowthResult(result, flags.json);
1020
- return result.ok ? 0 : 1;
1021
- }
1022
-
1023
- if (command === 'dev-check') {
1024
- const firstArgIsProjectPath = !flags.path && positionals.length > 1 && await isDirectoryPath(positionals[0]);
1025
- const devProjectPath = path.resolve(flags.path ?? (firstArgIsProjectPath ? positionals[0] : process.cwd()));
1026
- const files = flags.path
1027
- ? positionals
1028
- : (firstArgIsProjectPath ? positionals.slice(1) : positionals);
1029
- const result = await checkDevelopmentStandardsWorkspace(devProjectPath, { files });
1030
- printDevelopmentStandardsResult(result, flags.json);
1031
- return result.ok ? 0 : 1;
964
+ const qualityCommandExit = await runQualityCommand(command, { projectPath, flags, positionals });
965
+ if (qualityCommandExit !== null) {
966
+ return qualityCommandExit;
1032
967
  }
1033
968
 
1034
969
  if (command === 'benchmark') {
@@ -1171,6 +1106,7 @@ export async function main(argv = process.argv.slice(2)) {
1171
1106
  version: flags.version,
1172
1107
  digest: flags.digest,
1173
1108
  workUnit: flags.workUnit,
1109
+ direction: flags.direction,
1174
1110
  });
1175
1111
  printReviewResult(result, flags.json);
1176
1112
  return result.ok ? 0 : 1;
@@ -1454,6 +1390,7 @@ export {
1454
1390
  fleetWorkspace,
1455
1391
  backfillWorkUnitsWorkspace,
1456
1392
  runWorkspace,
1393
+ canvasWorkspace,
1457
1394
  initLoopWorkspace,
1458
1395
  planLoopWorkspace,
1459
1396
  statusLoopWorkspace,
@@ -47,7 +47,7 @@ export async function resolveOpenSpecChangeId(projectRoot, requestedChange) {
47
47
  return changeDirs[0];
48
48
  }
49
49
  if (changeDirs.length === 0) {
50
- throw new Error('openprd/changes 下没有找到 OpenPrd change。');
50
+ throw new Error('.openprd/changes 下没有找到 OpenPrd change。');
51
51
  }
52
52
  throw new Error(`找到多个 OpenPrd change;请传入 --change <id>。已找到: ${changeDirs.join(', ')}`);
53
53
  }
@@ -60,7 +60,7 @@ function extractProposalCapabilities(text) {
60
60
  return capabilities;
61
61
  }
62
62
 
63
- const SPEC_SECTION_HEADING_RE = /^##\s+(?:(ADDED|MODIFIED|REMOVED)\s+Requirements|(?:新增|修改|移除)需求)\s*$/gim;
63
+ const SPEC_SECTION_HEADING_RE = /^##\s+(?:(ADDED|MODIFIED|REMOVED|NEW|UPDATED|CHANGED)\s+Requirements|(?:新增|修改|移除)需求)\s*$/gim;
64
64
  const REQUIREMENT_HEADING_RE = /^###\s+(?:Requirement|需求)[::]\s*(.+)$/gm;
65
65
  const SCENARIO_HEADING_RE = /^####\s+(?:Scenario|场景)[::]\s*(.+)$/gm;
66
66
  const WHEN_STEP_RE = /^-\s+\*\*(?:WHEN|当)\*\*/im;
@@ -1,12 +1,11 @@
1
1
  export const OPENPRD_DISCOVERY_DIR = ['.openprd', 'discovery'];
2
2
  export const OPENPRD_DISCOVERY_CONFIG_PATH = ['.openprd', 'discovery', 'config.json'];
3
- export const LEGACY_OPENSPEC_DISCOVERY_DIR = ['.openspec', 'discovery'];
4
- export const LEGACY_OPENSPEC_DISCOVERY_CONFIG_PATH = ['.openspec', 'discovery', 'config.json'];
5
- export const OPENSPEC_DISCOVERY_CONFIG_PATH = LEGACY_OPENSPEC_DISCOVERY_CONFIG_PATH;
6
- export const OPENPRD_CHANGE_ROOT = ['openprd', 'changes'];
7
- export const LEGACY_OPENSPEC_CHANGE_ROOT = ['openspec', 'changes'];
8
- export const OPENPRD_ACCEPTED_SPEC_ROOT = ['openprd', 'specs'];
9
- export const OPENPRD_ARCHIVE_CHANGE_ROOT = ['openprd', 'archive', 'changes'];
3
+ export const OPENPRD_CHANGE_ROOT = ['.openprd', 'changes'];
4
+ export const LEGACY_OPENPRD_CHANGE_ROOT = ['openprd', 'changes'];
5
+ export const OPENPRD_ACCEPTED_SPEC_ROOT = ['.openprd', 'specs'];
6
+ export const LEGACY_OPENPRD_ACCEPTED_SPEC_ROOT = ['openprd', 'specs'];
7
+ export const OPENPRD_ARCHIVE_CHANGE_ROOT = ['.openprd', 'archive', 'changes'];
8
+ export const LEGACY_OPENPRD_ARCHIVE_CHANGE_ROOT = ['openprd', 'archive', 'changes'];
10
9
  export const OPENSPEC_TASK_MAX_ITEMS_PER_FILE = 25;
11
10
  export const OPENSPEC_TASK_FILE_PATTERN = /^tasks(?:-\d{3})?\.md$/;
12
11
  export const OPENSPEC_TASK_ID_PATTERN = /^T\d{3}\.\d+$/;
@@ -96,9 +96,9 @@ function resolveTaskSelection(state, options = {}) {
96
96
 
97
97
  if (!task) {
98
98
  if (requestedId) {
99
- throw new Error(`Unknown OpenSpec task: ${requestedId}`);
99
+ throw new Error(`Unknown OpenPrd task: ${requestedId}`);
100
100
  }
101
- throw new Error('No ready OpenSpec task is available.');
101
+ throw new Error('No ready OpenPrd task is available.');
102
102
  }
103
103
 
104
104
  return task;