@openprd/cli 0.1.1 → 0.1.9

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 (137) hide show
  1. package/.openprd/README.md +43 -69
  2. package/.openprd/README_EN.md +84 -0
  3. package/.openprd/benchmarks/index.md +7 -0
  4. package/.openprd/benchmarks/sources.yaml +25 -3
  5. package/.openprd/discovery/config.json +16 -2
  6. package/.openprd/engagements/active/flows.md +19 -14
  7. package/.openprd/engagements/active/handoff.md +11 -4
  8. package/.openprd/engagements/active/prd.md +99 -71
  9. package/.openprd/engagements/active/review.html +4 -4
  10. package/.openprd/engagements/active/roles.md +9 -8
  11. package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +4 -4
  12. package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +18 -0
  13. package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +18 -0
  14. package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +18 -0
  15. package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +18 -0
  16. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +78 -0
  17. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +129 -0
  18. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +41 -0
  19. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +14 -0
  20. package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +49 -0
  21. package/.openprd/knowledge/index.json +44 -4
  22. package/.openprd/reviews/v0001.html +195 -129
  23. package/.openprd/reviews/v0002.html +1150 -0
  24. package/.openprd/reviews/v0003.html +1150 -0
  25. package/.openprd/reviews/v0004.html +1150 -0
  26. package/.openprd/reviews/v0005.html +1150 -0
  27. package/.openprd/standards/config.json +12 -9
  28. package/.openprd/state/changes.json +17 -2
  29. package/.openprd/state/current.json +399 -63
  30. package/.openprd/state/release-ledger.json +387 -0
  31. package/.openprd/state/version-index.json +52 -0
  32. package/.openprd/state/versions/v0002.json +264 -0
  33. package/.openprd/state/versions/v0002.md +183 -0
  34. package/.openprd/state/versions/v0003.json +269 -0
  35. package/.openprd/state/versions/v0003.md +188 -0
  36. package/.openprd/state/versions/v0004.json +274 -0
  37. package/.openprd/state/versions/v0004.md +193 -0
  38. package/.openprd/state/versions/v0005.json +299 -0
  39. package/.openprd/state/versions/v0005.md +189 -0
  40. package/.openprd/templates/agent/intake.md +5 -4
  41. package/.openprd/templates/b2b/intake.md +5 -4
  42. package/.openprd/templates/base/intake.md +10 -4
  43. package/.openprd/templates/company/README.md +9 -7
  44. package/.openprd/templates/company/README_EN.md +12 -0
  45. package/.openprd/templates/consumer/intake.md +5 -4
  46. package/.openprd/templates/industry/README.md +12 -10
  47. package/.openprd/templates/industry/README_EN.md +18 -0
  48. package/.openprd/templates/project/README.md +11 -9
  49. package/.openprd/templates/project/README_EN.md +16 -0
  50. package/.openprd/templates/session/README.md +11 -9
  51. package/.openprd/templates/session/README_EN.md +16 -0
  52. package/AGENTS.md +12 -8
  53. package/README.md +419 -438
  54. package/README_CN.md +4 -578
  55. package/README_EN.md +870 -0
  56. package/docs/assets/openprd-requirement-routing-en.png +0 -0
  57. package/docs/assets/openprd-requirement-routing-en.svg +102 -0
  58. package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
  59. package/docs/assets/openprd-requirement-routing-zh.png +0 -0
  60. package/docs/assets/openprd-requirement-routing-zh.svg +102 -0
  61. package/package.json +6 -2
  62. package/scripts/dev-check-wrapup-copy.mjs +110 -0
  63. package/scripts/openprd-github-release-notes.mjs +99 -0
  64. package/scripts/quality-perf-check.mjs +203 -0
  65. package/skills/openprd-benchmark-router/SKILL.md +1 -0
  66. package/skills/openprd-benchmark-router/references/benchmark-sources.md +1 -0
  67. package/skills/openprd-benchmark-router/references/source-policy.md +2 -0
  68. package/skills/openprd-discovery-loop/SKILL.md +2 -2
  69. package/skills/openprd-harness/SKILL.md +47 -25
  70. package/skills/openprd-harness/references/workflow-gates.md +15 -0
  71. package/skills/openprd-quality/SKILL.md +11 -5
  72. package/skills/openprd-requirement-intake/SKILL.md +31 -20
  73. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -6
  74. package/skills/openprd-requirement-intake/references/routing-rubric.md +10 -2
  75. package/skills/openprd-router/SKILL.md +2 -2
  76. package/skills/openprd-shared/SKILL.md +51 -23
  77. package/skills/openprd-standards/SKILL.md +2 -1
  78. package/src/agent-integration.js +271 -71
  79. package/src/benchmark/constants.js +107 -0
  80. package/src/benchmark/operations.js +235 -0
  81. package/src/benchmark/registry.js +64 -0
  82. package/src/benchmark/render.js +115 -0
  83. package/src/benchmark/source.js +617 -0
  84. package/src/benchmark/storage.js +121 -0
  85. package/src/benchmark/verify.js +235 -0
  86. package/src/benchmark.js +50 -851
  87. package/src/change-summary.js +339 -0
  88. package/src/cli/args.js +67 -6
  89. package/src/cli/basic-print.js +365 -0
  90. package/src/cli/benchmark-print.js +91 -0
  91. package/src/cli/change-print.js +221 -0
  92. package/src/cli/doctor-print.js +268 -0
  93. package/src/cli/growth-print.js +176 -0
  94. package/src/cli/print.js +73 -1384
  95. package/src/cli/quality-print.js +284 -0
  96. package/src/cli/run-print.js +297 -0
  97. package/src/cli/shared-print.js +127 -0
  98. package/src/cli/workflow-print.js +195 -0
  99. package/src/codex-hook-runner-template.mjs +659 -124
  100. package/src/codex-runtime.js +324 -0
  101. package/src/dev-standards.js +178 -5
  102. package/src/diagram-core.js +5 -5
  103. package/src/discovery.js +2 -1
  104. package/src/execution-strategy.js +369 -0
  105. package/src/fleet.js +4 -0
  106. package/src/github-release.js +156 -0
  107. package/src/growth.js +311 -13
  108. package/src/html-artifact-utils.js +25 -0
  109. package/src/html-artifacts.js +157 -1596
  110. package/src/knowledge.js +1321 -76
  111. package/src/language-policy.js +2 -112
  112. package/src/learning-html-artifact.js +1031 -0
  113. package/src/learning-review.js +3 -2
  114. package/src/loop.js +280 -9
  115. package/src/openprd.js +341 -38
  116. package/src/openspec/change-validate.js +0 -9
  117. package/src/openspec/execute.js +79 -3
  118. package/src/openspec/generate.js +33 -20
  119. package/src/openspec/tasks.js +33 -2
  120. package/src/prd-core.js +10 -9
  121. package/src/product-type-copy.js +69 -0
  122. package/src/quality-html-artifact.js +108 -9
  123. package/src/quality-learning.js +30 -0
  124. package/src/quality-visual-review.js +237 -0
  125. package/src/quality.js +329 -43
  126. package/src/registry-hygiene.js +54 -0
  127. package/src/release-ledger.js +413 -0
  128. package/src/review-presentation.js +12 -6
  129. package/src/run-harness.js +722 -48
  130. package/src/session-binding.js +40 -3
  131. package/src/session-registry.js +159 -0
  132. package/src/standards.js +5 -3
  133. package/src/test-strategy.js +386 -0
  134. package/src/visual-compare.js +915 -34
  135. package/src/work-unit-migration.js +5 -1
  136. package/src/workspace-core.js +343 -19
  137. package/src/workspace-workflow.js +538 -134
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import crypto from 'node:crypto';
4
4
  import { canonicalReviewPath, defaultReviewArtifactPath, renderReviewArtifact, renderReviewEntryHtml, writeHtmlArtifact } from './html-artifacts.js';
5
5
  import { summarizeSnapshot } from './prd-core.js';
6
+ import { buildReleaseLedgerSummary } from './release-ledger.js';
6
7
  import { appendWorkflowEvent, buildWorkflowTaskGraph, loadWorkspace, readVersionIndex, readVersionSnapshot, writeVersionIndex, writeVersionSnapshot } from './workspace-core.js';
7
8
  import { exists, readText, writeJson } from './fs-utils.js';
8
9
  import { writeWorkUnitBinding } from './work-unit.js';
@@ -82,7 +83,10 @@ function buildBackfilledSnapshot(ws, snapshot, isLatest) {
82
83
  async function writeReviewBundle(ws, snapshot, isLatest) {
83
84
  const activeReviewArtifact = defaultReviewArtifactPath(ws);
84
85
  const reviewPath = canonicalReviewPath(ws, snapshot.versionId);
85
- await writeHtmlArtifact(reviewPath, renderReviewArtifact({ snapshot }));
86
+ await writeHtmlArtifact(reviewPath, renderReviewArtifact({
87
+ snapshot,
88
+ projectRelease: buildReleaseLedgerSummary(ws.data.releaseLedger),
89
+ }));
86
90
 
87
91
  if (isLatest) {
88
92
  await writeHtmlArtifact(activeReviewArtifact, renderReviewEntryHtml({
@@ -2,11 +2,14 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import crypto from 'node:crypto';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import { buildSnapshotChangeSummary } from './change-summary.js';
6
+ import { formatProductTypeDisplay, formatTemplatePackDisplay } from './product-type-copy.js';
5
7
  import { buildDiagramArtifact, renderDiagramMermaidFromModel } from './diagram-core.js';
6
- import { analyzePrdSnapshot, buildPrdSnapshot, getRequiredFieldDescriptors, renderPrdMarkdown, summarizeSnapshot } from './prd-core.js';
8
+ import { analyzePrdSnapshot, buildPrdSnapshot, formatVersionId, getRequiredFieldDescriptors, renderPrdMarkdown, summarizeSnapshot } from './prd-core.js';
9
+ import { normalizeReleaseLedger } from './release-ledger.js';
7
10
  import { appendJsonl, appendText, cjoin, exists, readJson, readText, readYaml, stringifyYaml, writeJson, writeText, writeYaml } from './fs-utils.js';
8
11
  import { OPENSPEC_TASK_MAX_ITEMS_PER_FILE } from './openspec/constants.js';
9
- import { checkStandardsWorkspace } from './standards.js';
12
+ import { checkStandardsWorkspace, STANDARD_DOCS } from './standards.js';
10
13
  import { timestamp } from './time.js';
11
14
 
12
15
  const PACKAGE_ROOT = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
@@ -15,6 +18,7 @@ const REQUIRED_PRODUCT_TYPES = ['consumer', 'b2b', 'agent'];
15
18
  const REQUIRED_SECTIONS = ['meta', 'problem', 'users', 'goals', 'scope', 'scenarios', 'requirements', 'constraints', 'risks', 'handoff'];
16
19
  const CORE_TEMPLATE_FILES = [
17
20
  'README.md',
21
+ 'README_EN.md',
18
22
  'config.yaml',
19
23
  'schema/prd.schema.yaml',
20
24
  'schema/diagram-architecture.schema.yaml',
@@ -31,9 +35,13 @@ const CORE_TEMPLATE_FILES = [
31
35
  'templates/agent/prd.md',
32
36
  'templates/agent/intake.md',
33
37
  'templates/company/README.md',
38
+ 'templates/company/README_EN.md',
34
39
  'templates/industry/README.md',
40
+ 'templates/industry/README_EN.md',
35
41
  'templates/project/README.md',
42
+ 'templates/project/README_EN.md',
36
43
  'templates/session/README.md',
44
+ 'templates/session/README_EN.md',
37
45
  'standards/config.json',
38
46
  'standards/file-manual-template.md',
39
47
  'standards/folder-readme-template.md',
@@ -51,6 +59,7 @@ const CORE_TEMPLATE_FILES = [
51
59
  ];
52
60
  const WORKSPACE_SEED_REFRESH_FILES = [
53
61
  'README.md',
62
+ 'README_EN.md',
54
63
  'schema/prd.schema.yaml',
55
64
  'schema/diagram-architecture.schema.yaml',
56
65
  'schema/diagram-product-flow.schema.yaml',
@@ -66,9 +75,13 @@ const WORKSPACE_SEED_REFRESH_FILES = [
66
75
  'templates/agent/prd.md',
67
76
  'templates/agent/intake.md',
68
77
  'templates/company/README.md',
78
+ 'templates/company/README_EN.md',
69
79
  'templates/industry/README.md',
80
+ 'templates/industry/README_EN.md',
70
81
  'templates/project/README.md',
82
+ 'templates/project/README_EN.md',
71
83
  'templates/session/README.md',
84
+ 'templates/session/README_EN.md',
72
85
  'standards/file-manual-template.md',
73
86
  'standards/folder-readme-template.md',
74
87
  ];
@@ -112,6 +125,162 @@ const DEFAULT_DISCOVERY_CONFIG = {
112
125
  },
113
126
  };
114
127
 
128
+ const WORKSPACE_SCENARIO_IGNORED_ENTRIES = new Set(['.openprd', '.DS_Store', '.git', '.omx']);
129
+ const WORKSPACE_SCENARIO_BOOTSTRAP_FILE_MARKERS = new Map([
130
+ ['AGENTS.md', 'OPENPRD:AGENTS:START'],
131
+ ['CLAUDE.md', 'OPENPRD:CLAUDE:START'],
132
+ ]);
133
+ const OPENPRD_INSTALL_MANIFEST_PATH = path.join('.openprd', 'harness', 'install-manifest.json');
134
+ const OPENPRD_HARNESS_DIR = cjoin('.openprd', 'harness');
135
+ const OPENPRD_HARNESS_REQUIREMENT_GATE = cjoin(OPENPRD_HARNESS_DIR, 'requirement-gate.json');
136
+ const OPENPRD_HARNESS_REQUIREMENT_GATES_DIR = cjoin(OPENPRD_HARNESS_DIR, 'requirement-gates');
137
+ const OPENPRD_HARNESS_SESSION_BINDINGS_DIR = cjoin(OPENPRD_HARNESS_DIR, 'session-bindings');
138
+ const OPENPRD_HARNESS_SESSION_STATES_DIR = cjoin(OPENPRD_HARNESS_DIR, 'session-states');
139
+
140
+ function normalizeLaneSessionId(sessionId) {
141
+ const text = String(sessionId ?? '').trim();
142
+ return text || null;
143
+ }
144
+
145
+ function sessionLaneFileName(sessionId) {
146
+ const normalized = normalizeLaneSessionId(sessionId);
147
+ if (!normalized) {
148
+ return null;
149
+ }
150
+ return `${normalized.replace(/[^A-Za-z0-9._-]/g, '_')}.json`;
151
+ }
152
+
153
+ function sessionCurrentStatePath(projectRoot, sessionId) {
154
+ const fileName = sessionLaneFileName(sessionId);
155
+ if (!fileName) {
156
+ return null;
157
+ }
158
+ return cjoin(projectRoot, OPENPRD_HARNESS_SESSION_STATES_DIR, fileName);
159
+ }
160
+
161
+ function sessionRequirementGatePath(projectRoot, sessionId) {
162
+ const fileName = sessionLaneFileName(sessionId);
163
+ if (!fileName) {
164
+ return null;
165
+ }
166
+ return cjoin(projectRoot, OPENPRD_HARNESS_REQUIREMENT_GATES_DIR, fileName);
167
+ }
168
+
169
+ function sessionBindingPath(projectRoot, sessionId) {
170
+ const fileName = sessionLaneFileName(sessionId);
171
+ if (!fileName) {
172
+ return null;
173
+ }
174
+ return cjoin(projectRoot, OPENPRD_HARNESS_SESSION_BINDINGS_DIR, fileName);
175
+ }
176
+
177
+ async function readActiveRequirementLane(projectRoot, workspaceCurrentState = null, options = {}) {
178
+ const legacyGate = options.legacyGate ?? await readJson(cjoin(projectRoot, OPENPRD_HARNESS_REQUIREMENT_GATE)).catch(() => null);
179
+ const explicitSessionId = normalizeLaneSessionId(options.sessionId);
180
+ const activeSessionId = explicitSessionId
181
+ ?? normalizeLaneSessionId(legacyGate?.active ? legacyGate.sessionId : null)
182
+ ?? normalizeLaneSessionId(workspaceCurrentState?.laneSessionId);
183
+ if (!activeSessionId) {
184
+ return {
185
+ sessionId: null,
186
+ scope: 'workspace',
187
+ gate: legacyGate?.active ? legacyGate : null,
188
+ binding: null,
189
+ sessionState: null,
190
+ currentStatePath: null,
191
+ };
192
+ }
193
+ const [scopedGate, binding, sessionState] = await Promise.all([
194
+ readJson(sessionRequirementGatePath(projectRoot, activeSessionId)).catch(() => null),
195
+ readJson(sessionBindingPath(projectRoot, activeSessionId)).catch(() => null),
196
+ readJson(sessionCurrentStatePath(projectRoot, activeSessionId)).catch(() => null),
197
+ ]);
198
+ return {
199
+ sessionId: activeSessionId,
200
+ scope: 'session',
201
+ gate: scopedGate ?? (legacyGate?.active ? legacyGate : null),
202
+ binding,
203
+ sessionState,
204
+ currentStatePath: sessionCurrentStatePath(projectRoot, activeSessionId),
205
+ };
206
+ }
207
+
208
+ function materializePersistedCurrentState(currentState, sessionId = null) {
209
+ const normalized = normalizeLaneSessionId(sessionId);
210
+ return {
211
+ ...(currentState ?? {}),
212
+ laneScope: normalized ? 'session' : 'workspace',
213
+ laneSessionId: normalized,
214
+ };
215
+ }
216
+
217
+ async function persistWorkspaceCurrentState(ws, currentState, options = {}) {
218
+ const sessionId = normalizeLaneSessionId(options.sessionId ?? ws.data.currentSessionId);
219
+ const stored = materializePersistedCurrentState(currentState, sessionId);
220
+ if (sessionId) {
221
+ const scopedPath = ws.paths.sessionCurrentState ?? sessionCurrentStatePath(ws.projectRoot, sessionId);
222
+ await writeJson(scopedPath, stored);
223
+ }
224
+ if (!sessionId || options.writeWorkspaceMirror !== false) {
225
+ await writeJson(ws.paths.workspaceCurrentState ?? ws.paths.currentState, stored);
226
+ }
227
+ return stored;
228
+ }
229
+
230
+ function buildCurrentStateSnapshot(ws, currentState, versionIndex = []) {
231
+ const state = currentState ?? {};
232
+ return buildPrdSnapshot(ws, {
233
+ ...state,
234
+ versionNumber: state.prdVersion ?? (versionIndex.at(-1)?.versionNumber ?? 0),
235
+ versionId: state.prdVersion > 0
236
+ ? formatVersionId(state.prdVersion)
237
+ : (versionIndex.at(-1)?.versionId ?? 'v0000'),
238
+ productType: state.productType ?? resolveCurrentProductType(ws),
239
+ templatePack: state.templatePack ?? resolveActiveTemplatePack(ws),
240
+ status: state.status ?? 'draft',
241
+ });
242
+ }
243
+
244
+ async function loadCurrentLaneSnapshot(ws, options = {}) {
245
+ const versionIndex = await readVersionIndex(ws);
246
+ const currentState = ws.data.currentState ?? {};
247
+ const fallbackToLatest = options.fallbackToLatest !== false;
248
+ const candidateVersionIds = [
249
+ currentState.reviewStatus?.versionId,
250
+ currentState.latestVersionId,
251
+ currentState.versionId,
252
+ ws.data.sessionBinding?.versionId,
253
+ ].map((value) => normalizeVersionId(value)).filter(Boolean);
254
+ for (const versionId of candidateVersionIds) {
255
+ const snapshot = await readVersionSnapshot(ws, versionId);
256
+ if (snapshot) {
257
+ return {
258
+ snapshot,
259
+ indexEntry: versionIndex.find((entry) => normalizeVersionId(entry.versionId) === versionId) ?? null,
260
+ source: ws.data.currentSessionId ? 'session-version' : 'workspace-version',
261
+ };
262
+ }
263
+ }
264
+ if (Object.keys(currentState).length > 0) {
265
+ return {
266
+ snapshot: buildCurrentStateSnapshot(ws, currentState, versionIndex),
267
+ indexEntry: null,
268
+ source: ws.data.currentSessionId ? 'session-draft' : 'workspace-draft',
269
+ };
270
+ }
271
+ if (!fallbackToLatest) {
272
+ return null;
273
+ }
274
+ const latest = await loadLatestVersionSnapshot(ws);
275
+ if (latest) {
276
+ return {
277
+ ...latest,
278
+ source: ws.data.currentSessionId ? 'workspace-latest-fallback' : 'workspace-latest',
279
+ };
280
+ }
281
+ return null;
282
+ }
283
+
115
284
  function formatMarkdownLines(lines) {
116
285
  return lines.filter(Boolean).map((line) => `- ${line}`).join('\n');
117
286
  }
@@ -770,6 +939,9 @@ async function migrateWorkspaceSkeleton(projectRoot, options = {}) {
770
939
  const typeSpecificBlock = extractMarkdownSection(seedPrd, '## 类型专项模块') || seedPrd;
771
940
  await ensureActiveFileContains(projectRoot, 'engagements/active/intake.md', '我们要解决什么问题?', seedIntake, changes);
772
941
  await ensureActiveFileContains(projectRoot, 'engagements/active/prd.md', '类型专项模块', typeSpecificBlock, changes);
942
+ await ensureHeadingFile(projectRoot, 'engagements/active/flows.md', '# 流程', '# 流程\n\n- 已初始化 OpenPrd 流程说明。\n', changes);
943
+ await ensureHeadingFile(projectRoot, 'engagements/active/roles.md', '# 角色', '# 角色\n\n- 已初始化 OpenPrd 角色说明。\n', changes);
944
+ await ensureHeadingFile(projectRoot, 'engagements/active/handoff.md', '# 交接', '# 交接\n\n- 已初始化 OpenPrd 交接摘要。\n', changes);
773
945
  await ensureHeadingFile(projectRoot, 'engagements/active/decision-log.md', '# 决策记录', '# 决策记录\n\n- 已初始化 OpenPrd 决策跟踪。\n', changes);
774
946
  await ensureHeadingFile(projectRoot, 'engagements/active/open-questions.md', '# 开放问题', '# 开放问题\n\n- 已初始化 OpenPrd 问题跟踪。\n', changes);
775
947
  await ensureHeadingFile(projectRoot, 'engagements/active/progress.md', '# 进度', '# 进度\n\n- 已初始化 OpenPrd 进度跟踪。\n', changes);
@@ -824,7 +996,7 @@ async function migrateWorkspaceSkeleton(projectRoot, options = {}) {
824
996
  };
825
997
  }
826
998
 
827
- async function loadWorkspace(projectRoot) {
999
+ async function loadWorkspace(projectRoot, options = {}) {
828
1000
  const workspaceRoot = cjoin(projectRoot, '.openprd');
829
1001
  const paths = {
830
1002
  workspaceRoot,
@@ -886,7 +1058,13 @@ async function loadWorkspace(projectRoot) {
886
1058
  stateDir: cjoin(workspaceRoot, 'state'),
887
1059
  versionsDir: cjoin(workspaceRoot, 'state', 'versions'),
888
1060
  versionIndex: cjoin(workspaceRoot, 'state', 'version-index.json'),
1061
+ releaseLedger: cjoin(workspaceRoot, 'state', 'release-ledger.json'),
889
1062
  currentState: cjoin(workspaceRoot, 'state', 'current.json'),
1063
+ workspaceCurrentState: cjoin(workspaceRoot, 'state', 'current.json'),
1064
+ sessionStatesDir: cjoin(workspaceRoot, 'harness', 'session-states'),
1065
+ sessionBindingsDir: cjoin(workspaceRoot, 'harness', 'session-bindings'),
1066
+ requirementGate: cjoin(workspaceRoot, 'harness', 'requirement-gate.json'),
1067
+ requirementGatesDir: cjoin(workspaceRoot, 'harness', 'requirement-gates'),
890
1068
  freezeState: cjoin(workspaceRoot, 'state', 'freeze.json'),
891
1069
  taskGraph: cjoin(workspaceRoot, 'state', 'task-graph.json'),
892
1070
  eventsLog: cjoin(workspaceRoot, 'state', 'events.jsonl'),
@@ -899,15 +1077,27 @@ async function loadWorkspace(projectRoot) {
899
1077
  openspecHandoffMd: cjoin(workspaceRoot, 'exports', 'openspec', 'handoff.md'),
900
1078
  };
901
1079
 
1080
+ const workspaceCurrentState = await readJson(paths.workspaceCurrentState).catch(() => null);
1081
+ const lane = await readActiveRequirementLane(projectRoot, workspaceCurrentState, { sessionId: options.sessionId });
1082
+ if (lane.currentStatePath) {
1083
+ paths.sessionCurrentState = lane.currentStatePath;
1084
+ }
1085
+
902
1086
  const data = {
903
1087
  config: await readYaml(paths.config).catch(() => null),
904
1088
  schema: await readYaml(paths.schema).catch(() => null),
905
1089
  diagramArchitectureSchema: await readYaml(paths.diagramArchitectureSchema).catch(() => null),
906
1090
  diagramProductFlowSchema: await readYaml(paths.diagramProductFlowSchema).catch(() => null),
907
1091
  manifest: await readYaml(paths.manifest).catch(() => null),
908
- currentState: await readJson(paths.currentState).catch(() => null),
1092
+ currentState: lane.sessionId ? (lane.sessionState ?? {}) : workspaceCurrentState,
1093
+ workspaceCurrentState,
1094
+ currentSessionId: lane.sessionId,
1095
+ currentStateScope: lane.scope,
1096
+ sessionBinding: lane.binding,
1097
+ activeRequirementGate: lane.gate,
909
1098
  freezeState: await readJson(paths.freezeState).catch(() => null),
910
1099
  versionIndex: await readJson(paths.versionIndex).catch(() => []),
1100
+ releaseLedger: normalizeReleaseLedger(await readJson(paths.releaseLedger).catch(() => null)),
911
1101
  learningIndex: await readJson(paths.learningIndex).catch(() => null),
912
1102
  learningCurrent: await readJson(paths.learningCurrent).catch(() => null),
913
1103
  };
@@ -1031,7 +1221,7 @@ function listMissing(actual, expected) {
1031
1221
  return expected.filter((item) => !actualSet.has(item));
1032
1222
  }
1033
1223
 
1034
- async function validateWorkspace(projectRoot) {
1224
+ async function validateWorkspace(projectRoot, options = {}) {
1035
1225
  const report = {
1036
1226
  valid: true,
1037
1227
  errors: [],
@@ -1268,7 +1458,11 @@ async function validateWorkspace(projectRoot) {
1268
1458
  report.errors.push('state/events.jsonl is missing');
1269
1459
  }
1270
1460
 
1271
- const standards = await checkStandardsWorkspace(projectRoot, { optional: true });
1461
+ const standards = await checkStandardsWorkspace(projectRoot, {
1462
+ optional: true,
1463
+ sourceManuals: options.sourceManuals,
1464
+ docsContent: options.docsContent,
1465
+ });
1272
1466
  if (!standards.skipped) {
1273
1467
  if (standards.errors.length > 0) {
1274
1468
  report.valid = false;
@@ -1300,6 +1494,18 @@ async function validateWorkspace(projectRoot) {
1300
1494
  }
1301
1495
  }
1302
1496
 
1497
+ const releaseLedger = ws.data.releaseLedger ?? normalizeReleaseLedger(null);
1498
+ const currentRelease = releaseLedger.currentVersion
1499
+ ? releaseLedger.versions.find((entry) => entry.version === releaseLedger.currentVersion) ?? null
1500
+ : null;
1501
+ if (releaseLedger.enabled && !releaseLedger.currentVersion) {
1502
+ report.warnings.push('state/release-ledger.json 已启用,但还没有设置当前项目版本号');
1503
+ }
1504
+ if (releaseLedger.currentVersion && !currentRelease) {
1505
+ report.valid = false;
1506
+ report.errors.push(`state/release-ledger.json 当前版本 ${releaseLedger.currentVersion} 缺少对应条目`);
1507
+ }
1508
+
1303
1509
  report.checks.push({ name: 'workspace', ok: true });
1304
1510
  report.checks.push({ name: 'schema', ok: true });
1305
1511
  report.checks.push({ name: 'manifest', ok: true });
@@ -1424,6 +1630,114 @@ function coerceCapturedValue(pathString, rawValue, append = false) {
1424
1630
  return text;
1425
1631
  }
1426
1632
 
1633
+ function normalizeScenarioRelativePath(relativePath) {
1634
+ return String(relativePath ?? '').replaceAll('\\', '/');
1635
+ }
1636
+
1637
+ async function readScenarioManagedPaths(projectRoot) {
1638
+ const manifestPath = path.join(projectRoot, OPENPRD_INSTALL_MANIFEST_PATH);
1639
+ const manifest = await readJson(manifestPath).catch(() => null);
1640
+ const managedPaths = new Set();
1641
+ for (const entry of manifest?.managedFiles ?? []) {
1642
+ if (typeof entry?.path !== 'string') {
1643
+ continue;
1644
+ }
1645
+ const normalized = normalizeScenarioRelativePath(entry.path.trim());
1646
+ if (normalized) {
1647
+ managedPaths.add(normalized);
1648
+ }
1649
+ }
1650
+ return managedPaths;
1651
+ }
1652
+
1653
+ async function listScenarioDirectoryFiles(projectRoot, relativeDir) {
1654
+ const normalizedDir = normalizeScenarioRelativePath(relativeDir);
1655
+ const absoluteDir = path.join(projectRoot, normalizedDir);
1656
+ const entries = await fs.readdir(absoluteDir, { withFileTypes: true }).catch(() => []);
1657
+ const files = [];
1658
+ for (const entry of entries) {
1659
+ if (entry.name === '.DS_Store') {
1660
+ continue;
1661
+ }
1662
+ const childRelative = normalizeScenarioRelativePath(path.posix.join(normalizedDir, entry.name));
1663
+ if (entry.isDirectory()) {
1664
+ files.push(...await listScenarioDirectoryFiles(projectRoot, childRelative));
1665
+ continue;
1666
+ }
1667
+ files.push(childRelative);
1668
+ }
1669
+ return files;
1670
+ }
1671
+
1672
+ async function directoryContainsOnlyManagedBootstrap(projectRoot, relativeDir, managedPaths) {
1673
+ const files = await listScenarioDirectoryFiles(projectRoot, relativeDir);
1674
+ return files.length > 0 && files.every((file) => managedPaths.has(file));
1675
+ }
1676
+
1677
+ async function docsDirectoryIsOpenPrdBootstrap(projectRoot) {
1678
+ const docsRoot = path.join(projectRoot, 'docs');
1679
+ const docsEntries = await fs.readdir(docsRoot, { withFileTypes: true }).catch(() => null);
1680
+ if (!docsEntries) {
1681
+ return false;
1682
+ }
1683
+
1684
+ const visibleDocsEntries = docsEntries.filter((entry) => entry.name !== '.DS_Store');
1685
+ if (visibleDocsEntries.length !== 1 || visibleDocsEntries[0].name !== 'basic' || !visibleDocsEntries[0].isDirectory()) {
1686
+ return false;
1687
+ }
1688
+
1689
+ const basicRoot = path.join(docsRoot, 'basic');
1690
+ const basicEntries = await fs.readdir(basicRoot, { withFileTypes: true }).catch(() => null);
1691
+ if (!basicEntries) {
1692
+ return false;
1693
+ }
1694
+
1695
+ const visibleBasicEntries = basicEntries.filter((entry) => entry.name !== '.DS_Store');
1696
+ if (visibleBasicEntries.length !== STANDARD_DOCS.length || visibleBasicEntries.some((entry) => !entry.isFile())) {
1697
+ return false;
1698
+ }
1699
+
1700
+ const expectedDocs = new Map(STANDARD_DOCS.map((doc) => [doc.fileName, doc.body.trim()]));
1701
+ for (const entry of visibleBasicEntries) {
1702
+ const expectedBody = expectedDocs.get(entry.name);
1703
+ if (!expectedBody) {
1704
+ return false;
1705
+ }
1706
+ const actualBody = await readText(path.join(basicRoot, entry.name)).catch(() => null);
1707
+ if (actualBody?.trim() !== expectedBody) {
1708
+ return false;
1709
+ }
1710
+ }
1711
+
1712
+ return true;
1713
+ }
1714
+
1715
+ async function shouldIgnoreWorkspaceScenarioEntry(projectRoot, entry, managedPaths) {
1716
+ if (WORKSPACE_SCENARIO_IGNORED_ENTRIES.has(entry.name)) {
1717
+ return true;
1718
+ }
1719
+
1720
+ if (entry.isFile() && WORKSPACE_SCENARIO_BOOTSTRAP_FILE_MARKERS.has(entry.name)) {
1721
+ const marker = WORKSPACE_SCENARIO_BOOTSTRAP_FILE_MARKERS.get(entry.name);
1722
+ const content = await readText(path.join(projectRoot, entry.name)).catch(() => null);
1723
+ return Boolean(content?.includes(marker));
1724
+ }
1725
+
1726
+ if (!entry.isDirectory()) {
1727
+ return false;
1728
+ }
1729
+
1730
+ if (entry.name === 'docs') {
1731
+ return docsDirectoryIsOpenPrdBootstrap(projectRoot);
1732
+ }
1733
+
1734
+ if (['.codex', '.claude', '.cursor'].includes(entry.name)) {
1735
+ return directoryContainsOnlyManagedBootstrap(projectRoot, entry.name, managedPaths);
1736
+ }
1737
+
1738
+ return false;
1739
+ }
1740
+
1427
1741
  async function detectWorkspaceScenario(projectRoot, ws, versionIndex = []) {
1428
1742
  const currentStatus = ws.data.currentState?.status ?? 'unknown';
1429
1743
  if (versionIndex.length > 0 || ['synthesized', 'frozen', 'handed_off'].includes(currentStatus)) {
@@ -1436,13 +1750,14 @@ async function detectWorkspaceScenario(projectRoot, ws, versionIndex = []) {
1436
1750
  }
1437
1751
 
1438
1752
  const entries = await fs.readdir(projectRoot, { withFileTypes: true }).catch(() => []);
1439
- const meaningfulEntries = entries.filter((entry) => {
1440
- if (entry.name === '.openprd') return false;
1441
- if (entry.name === '.DS_Store') return false;
1442
- if (entry.name === '.git') return false;
1443
- if (entry.name === '.omx') return false;
1444
- return true;
1445
- });
1753
+ const managedPaths = await readScenarioManagedPaths(projectRoot);
1754
+ const meaningfulEntries = [];
1755
+ for (const entry of entries) {
1756
+ if (await shouldIgnoreWorkspaceScenarioEntry(projectRoot, entry, managedPaths)) {
1757
+ continue;
1758
+ }
1759
+ meaningfulEntries.push(entry);
1760
+ }
1446
1761
 
1447
1762
  if (meaningfulEntries.length === 0) {
1448
1763
  return {
@@ -1503,13 +1818,13 @@ function buildClarificationState({ snapshot, analysis, basePlan, scenario, captu
1503
1818
  {
1504
1819
  id: 'existing-project-goal',
1505
1820
  label: '已有项目范围',
1506
- prompt: '基于当前已有项目,这个 OpenPrd 工作区现在具体要定义或改进什么?',
1821
+ prompt: '基于当前已有项目,这一轮主要是为谁改善什么问题,第一版准备先落哪一块最有价值?',
1507
1822
  reason: 'kickoff',
1508
1823
  },
1509
1824
  {
1510
1825
  id: 'reuse-boundary',
1511
1826
  label: '复用边界',
1512
- prompt: '哪些既有能力应视为固定输入,哪些区域仍可调整?',
1827
+ prompt: '哪些既有数据、流程、体验或业务结果不能被破坏,哪些区域仍可调整?',
1513
1828
  reason: 'kickoff',
1514
1829
  },
1515
1830
  ...missingQuestions,
@@ -1552,9 +1867,9 @@ function buildClarificationPlan(snapshot, analysis) {
1552
1867
  const mustAsk = analysis.missingFields.filter((field) => USER_CLARIFICATION_PATHS.has(field.path));
1553
1868
  const derived = analysis.missingFields.filter((field) => !USER_CLARIFICATION_PATHS.has(field.path));
1554
1869
  const kickoffQuestions = [
1555
- { id: 'project-overview', label: 'Project overview', prompt: 'What are we building at a high level, and for whom?' },
1556
- { id: 'success-definition', label: 'Success definition', prompt: 'What outcome would make this first version successful?' },
1557
- { id: 'first-milestone', label: '首个里程碑', prompt: '我们希望 freeze 的第一个里程碑是什么?' },
1870
+ { id: 'project-overview', label: '项目轮廓', prompt: '这件事主要是给谁用的,他们会在什么场景下最先感受到价值?' },
1871
+ { id: 'first-slice', label: '首版切片', prompt: '如果先做第一版,最值得先让用户完成什么关键动作?' },
1872
+ { id: 'guardrails', label: '保护项', prompt: '这轮明确先不做什么,或者哪些既有体验、流程和业务结果不能被影响?' },
1558
1873
  ];
1559
1874
  return {
1560
1875
  totalRequiredFields: analysis.totalRequiredFields,
@@ -1666,7 +1981,11 @@ function renderRolesDoc(snapshot) {
1666
1981
 
1667
1982
  function renderHandoffDoc(snapshot) {
1668
1983
  const { handoff } = snapshot.sections;
1669
- return `# 交接\n\n- 版本: ${snapshot.versionId}\n- 产品类型: ${snapshot.productType ?? '未分类'}\n- 模板包: ${snapshot.templatePack}\n- Digest: ${snapshot.digest}\n- 负责人: ${handoff.owner}\n- 下一步: ${handoff.nextStep}\n- 目标系统: ${handoff.targetSystem}\n`;
1984
+ const changeSummary = buildSnapshotChangeSummary(snapshot, { limit: 4 });
1985
+ const summarySection = changeSummary.markdown
1986
+ ? `\n## 变化摘要\n\n${changeSummary.markdown}\n`
1987
+ : '';
1988
+ return `# 交接\n\n- 本次内容: ${snapshot.title}\n- 产品场景: ${formatProductTypeDisplay(snapshot.productType, { fallback: '待确认' })}\n- 场景模板: ${formatTemplatePackDisplay(snapshot.templatePack, { fallback: '待确认' })}\n- 负责人: ${handoff.owner}\n- 下一步: ${handoff.nextStep}\n- 交接去向: ${handoff.targetSystem}\n${summarySection}`;
1670
1989
  }
1671
1990
 
1672
1991
 
@@ -1675,6 +1994,7 @@ export {
1675
1994
  appendOpenQuestions,
1676
1995
  appendProgress,
1677
1996
  appendVerification,
1997
+ buildCurrentStateSnapshot,
1678
1998
  appendWorkflowEvent,
1679
1999
  buildClarificationPlan,
1680
2000
  buildClarificationState,
@@ -1689,11 +2009,15 @@ export {
1689
2009
  extractMarkdownSection,
1690
2010
  FIELD_PATH_TO_STATE_KEY,
1691
2011
  isSupportedProductType,
2012
+ loadCurrentLaneSnapshot,
1692
2013
  loadLatestVersionSnapshot,
1693
2014
  loadWorkspace,
1694
2015
  migrateWorkspaceSkeleton,
2016
+ normalizeLaneSessionId,
1695
2017
  normalizeVersionId,
2018
+ persistWorkspaceCurrentState,
1696
2019
  readVersionIndex,
2020
+ readActiveRequirementLane,
1697
2021
  readVersionSnapshot,
1698
2022
  renderFlowDoc,
1699
2023
  renderHandoffDoc,