@openprd/cli 0.1.18 → 0.1.21

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 (118) hide show
  1. package/.openprd/changes/openprd-control-plane-v020/.openprd.yaml +2 -0
  2. package/.openprd/changes/openprd-control-plane-v020/design.md +78 -0
  3. package/.openprd/changes/openprd-control-plane-v020/proposal.md +54 -0
  4. package/.openprd/changes/openprd-control-plane-v020/specs/agent-requirements/spec.md +16 -0
  5. package/.openprd/changes/openprd-control-plane-v020/task-events.jsonl +27 -0
  6. package/.openprd/changes/openprd-control-plane-v020/tasks-002.md +35 -0
  7. package/.openprd/changes/openprd-control-plane-v020/tasks.md +427 -0
  8. package/.openprd/changes/openprd-generated-change/.openprd.yaml +2 -0
  9. package/.openprd/changes/openprd-generated-change/design.md +50 -0
  10. package/.openprd/changes/openprd-generated-change/proposal.md +37 -0
  11. package/.openprd/changes/openprd-generated-change/specs/agent-requirements/spec.md +16 -0
  12. package/.openprd/changes/openprd-generated-change/task-events.jsonl +21 -0
  13. package/.openprd/changes/openprd-generated-change/tasks.md +357 -0
  14. package/.openprd/changes/remove-session-tracking-humane-approval/.openprd.yaml +2 -0
  15. package/.openprd/changes/remove-session-tracking-humane-approval/design.md +52 -0
  16. package/.openprd/changes/remove-session-tracking-humane-approval/proposal.md +35 -0
  17. package/.openprd/changes/remove-session-tracking-humane-approval/specs/agent-requirements/spec.md +16 -0
  18. package/.openprd/changes/remove-session-tracking-humane-approval/task-events.jsonl +1 -0
  19. package/.openprd/changes/remove-session-tracking-humane-approval/tasks.md +170 -0
  20. package/.openprd/design/active/asset-spec.md +19 -14
  21. package/.openprd/design/active/direction-plan.md +19 -3
  22. package/.openprd/design/active/facts-sheet.md +16 -7
  23. package/.openprd/design/active/image-preflight.md +6 -5
  24. package/.openprd/design/active/review-studio-v020-directions/compare-plan.json +34 -0
  25. package/.openprd/design/active/review-studio-v020-directions/contact-sheet.jpg +0 -0
  26. package/.openprd/design/active/review-studio-v020-directions/crops/01.png +0 -0
  27. package/.openprd/design/active/review-studio-v020-directions/crops/02.png +0 -0
  28. package/.openprd/design/active/review-studio-v020-directions/crops/03.png +0 -0
  29. package/.openprd/design/active/review-studio-v020-directions/focus-board.template.json +69 -0
  30. package/.openprd/design/active/review-studio-v020-directions/parallel-board.template.json +45 -0
  31. package/.openprd/design/active/review-studio-v020-directions/reference-set.json +143 -0
  32. package/.openprd/design/active/review-studio-v020-directions/source.png +0 -0
  33. package/.openprd/design/active/selected-direction.md +23 -9
  34. package/.openprd/engagements/active/control-plane-architecture.json +203 -0
  35. package/.openprd/engagements/active/control-plane-intake.json +418 -0
  36. package/.openprd/engagements/active/prd.md +186 -100
  37. package/.openprd/engagements/active/review-presentation-v0018.json +176 -0
  38. package/.openprd/i18n-config.json +12 -0
  39. package/.openprd/ledger/events.jsonl +6 -0
  40. package/.openprd/ledger/heads/ebea1a71a9daa566f1c91b53.json +10 -0
  41. package/.openprd/manifest.json +21 -0
  42. package/AGENTS.md +1 -0
  43. package/README.md +22 -0
  44. package/README_EN.md +27 -0
  45. package/package.json +1 -1
  46. package/skills/openprd-frontend-design/SKILL.md +16 -0
  47. package/skills/openprd-harness/SKILL.md +1 -1
  48. package/skills/openprd-quality/SKILL.md +1 -1
  49. package/skills/openprd-requirement-intake/SKILL.md +1 -1
  50. package/skills/openprd-shared/SKILL.md +1 -1
  51. package/src/adapters/adapter-spi.js +193 -0
  52. package/src/adapters/capability-envelope.js +98 -0
  53. package/src/adapters/event-normalizer.js +55 -0
  54. package/src/adapters/index.js +4 -0
  55. package/src/adapters/install-safety.js +249 -0
  56. package/src/agent-canonical-content.js +6 -2
  57. package/src/agent-integration.js +147 -42
  58. package/src/canvas-app-client-app.js +838 -0
  59. package/src/canvas-app-client-helpers.js +1138 -0
  60. package/src/canvas-app-styles.js +898 -0
  61. package/src/canvas-app.html.js +6 -2781
  62. package/src/canvas-binding.js +414 -0
  63. package/src/canvas-codex-relay.js +430 -0
  64. package/src/canvas-i18n.js +8 -0
  65. package/src/canvas-scene.js +377 -0
  66. package/src/canvas-session-store.js +148 -0
  67. package/src/canvas-workspace.js +59 -1066
  68. package/src/cli/args.js +63 -4
  69. package/src/cli/gate-print.js +17 -0
  70. package/src/cli/quality-commands.js +18 -0
  71. package/src/cli/quality-print.js +10 -0
  72. package/src/cli/runtime-print.js +24 -0
  73. package/src/codex-hook-runner-template.mjs +87 -77
  74. package/src/codex-runtime.js +48 -5
  75. package/src/context/cache.js +245 -0
  76. package/src/context/compiler.js +438 -0
  77. package/src/context/constants.js +30 -0
  78. package/src/context/index.js +39 -0
  79. package/src/context/redaction.js +84 -0
  80. package/src/context/stable.js +69 -0
  81. package/src/context/telemetry.js +42 -0
  82. package/src/dev-standards.js +57 -0
  83. package/src/fleet.js +112 -95
  84. package/src/gates/index.js +2 -0
  85. package/src/gates/scoped-gates.js +256 -0
  86. package/src/gates/store.js +126 -0
  87. package/src/gates/workspace.js +41 -0
  88. package/src/html-artifacts.js +725 -28
  89. package/src/kernel/atomic-store.js +299 -0
  90. package/src/kernel/event-envelope.js +166 -0
  91. package/src/kernel/index.js +4 -0
  92. package/src/kernel/project-ledger.js +467 -0
  93. package/src/kernel/project-manifest.js +205 -0
  94. package/src/knowledge-v3/index.js +1 -0
  95. package/src/knowledge-v3/lifecycle.js +290 -0
  96. package/src/knowledge.js +14 -7
  97. package/src/openprd.js +71 -2
  98. package/src/review-model.js +413 -0
  99. package/src/review-presentation.js +1 -1
  100. package/src/run-harness.js +431 -37
  101. package/src/runtime/cli_runtime_README.md +28 -0
  102. package/src/runtime/errors.js +66 -0
  103. package/src/runtime/index.js +44 -0
  104. package/src/runtime/lane-schema.js +141 -0
  105. package/src/runtime/lane-store.js +279 -0
  106. package/src/runtime/task-runtime.js +449 -0
  107. package/src/runtime/workspace.js +179 -0
  108. package/src/runtime/write-set.js +206 -0
  109. package/src/session-binding.js +16 -3
  110. package/src/session-registry.js +59 -1
  111. package/src/upgrade/fleet-mutation.js +166 -0
  112. package/src/upgrade/fleet-transaction.js +398 -0
  113. package/src/upgrade/transaction-store.js +416 -0
  114. package/src/visual-compare-core.js +66 -27
  115. package/src/visual-compare.js +18 -12
  116. package/src/workspace-core.js +109 -7
  117. package/src/workspace-registry.js +39 -1
  118. package/src/workspace-workflow.js +18 -15
@@ -17,6 +17,7 @@ import {
17
17
  DEFAULT_PARALLEL_CARD_WIDTH,
18
18
  DEFAULT_PARALLEL_COLUMNS,
19
19
  FOCUS_COLORS,
20
+ VISUAL_BOARD_PALETTE,
20
21
  defaultOutputPath,
21
22
  encodePipeline,
22
23
  escapeXml,
@@ -75,7 +76,7 @@ async function renderStandardComparison(projectRoot, comparison, options = {}) {
75
76
  width: canvasWidth,
76
77
  height: canvasHeight,
77
78
  channels: 3,
78
- background: '#111827',
79
+ background: VISUAL_BOARD_PALETTE.canvas,
79
80
  },
80
81
  }).composite([
81
82
  { input: referencePanel.input, left: referenceLeft, top: referenceTop },
@@ -278,7 +279,7 @@ async function renderFocusBoard(projectRoot, board, options = {}) {
278
279
  width: contentWidth,
279
280
  height: canvasHeight,
280
281
  channels: 3,
281
- background: '#111827',
282
+ background: VISUAL_BOARD_PALETTE.canvas,
282
283
  },
283
284
  }).composite(composites);
284
285
 
@@ -398,7 +399,7 @@ async function renderParallelBoard(projectRoot, board, options = {}) {
398
399
  width: canvasWidth,
399
400
  height: canvasHeight,
400
401
  channels: 3,
401
- background: '#111827',
402
+ background: VISUAL_BOARD_PALETTE.canvas,
402
403
  },
403
404
  }).composite(composites);
404
405
 
@@ -458,11 +459,12 @@ async function renderVerificationBoard(projectRoot, board, options = {}) {
458
459
  }
459
460
  const cardWidth = parsePositiveInteger(board.payload.cardWidth ?? options.maxPanelWidth, DEFAULT_PARALLEL_CARD_WIDTH, 'cardWidth');
460
461
  const columns = Math.max(1, Math.min(parsePositiveInteger(board.payload.columns, Math.min(DEFAULT_PARALLEL_COLUMNS, items.length), 'columns'), 4));
461
- const margin = 24;
462
- const gap = 24;
462
+ const compactResult = board.payload.presentation !== 'detailed';
463
+ const margin = compactResult ? 16 : 24;
464
+ const gap = compactResult ? 16 : 24;
463
465
  const route = String(board.payload.route ?? board.payload.flow ?? board.payload.method ?? '').trim();
464
466
  const summary = String(board.payload.summary ?? copy.verificationSummary(route));
465
- const titleBlock = titleBlockSvg(
467
+ const titleBlock = compactResult ? null : titleBlockSvg(
466
468
  columns * cardWidth + (columns - 1) * gap,
467
469
  String(board.payload.title ?? copy.verificationTitle),
468
470
  summary,
@@ -475,6 +477,7 @@ async function renderVerificationBoard(projectRoot, board, options = {}) {
475
477
  cardWidth,
476
478
  eyebrow: copy.verificationCardEyebrow,
477
479
  locale,
480
+ compactEvidence: compactResult,
478
481
  }));
479
482
  }
480
483
 
@@ -483,11 +486,12 @@ async function renderVerificationBoard(projectRoot, board, options = {}) {
483
486
  rowHeights.push(Math.max(...renderedCards.slice(index, index + columns).map((card) => card.height)));
484
487
  }
485
488
  const canvasWidth = margin * 2 + columns * cardWidth + (columns - 1) * gap;
486
- const canvasHeight = margin * 2 + titleBlock.height + 18 + rowHeights.reduce((sum, value) => sum + value, 0) + Math.max(0, rowHeights.length - 1) * gap;
487
- const composites = [
489
+ const headerHeight = titleBlock ? titleBlock.height + 18 : 0;
490
+ const canvasHeight = margin * 2 + headerHeight + rowHeights.reduce((sum, value) => sum + value, 0) + Math.max(0, rowHeights.length - 1) * gap;
491
+ const composites = titleBlock ? [
488
492
  { input: titleBlock.input, left: margin, top: margin },
489
- ];
490
- let currentTop = margin + titleBlock.height + 18;
493
+ ] : [];
494
+ let currentTop = margin + headerHeight;
491
495
  for (let row = 0; row < rowHeights.length; row += 1) {
492
496
  const rowCards = renderedCards.slice(row * columns, row * columns + columns);
493
497
  for (const [column, card] of rowCards.entries()) {
@@ -505,7 +509,7 @@ async function renderVerificationBoard(projectRoot, board, options = {}) {
505
509
  width: canvasWidth,
506
510
  height: canvasHeight,
507
511
  channels: 3,
508
- background: '#111827',
512
+ background: VISUAL_BOARD_PALETTE.canvas,
509
513
  },
510
514
  }).composite(composites);
511
515
 
@@ -522,6 +526,8 @@ async function renderVerificationBoard(projectRoot, board, options = {}) {
522
526
  format,
523
527
  boardSource: toWorkspacePath(projectRoot, board.sourcePath),
524
528
  title: String(board.payload.title ?? copy.verificationTitle),
529
+ presentation: compactResult ? 'compact-result' : 'detailed',
530
+ alignmentGuidesApplied: false,
525
531
  route: route || null,
526
532
  layout: {
527
533
  columns,
@@ -896,7 +902,7 @@ async function renderAlignmentBoard(projectRoot, board, options = {}) {
896
902
  width: contentWidth,
897
903
  height: canvasHeight,
898
904
  channels: 3,
899
- background: '#111827',
905
+ background: VISUAL_BOARD_PALETTE.canvas,
900
906
  },
901
907
  }).composite(composites);
902
908
 
@@ -9,6 +9,7 @@ import { migrateLegacyOpenPrdArtifacts } from './openspec/migration.js';
9
9
  import { analyzePrdSnapshot, buildPrdSnapshot, formatVersionId, getRequiredFieldDescriptors, renderPrdMarkdown, summarizeSnapshot } from './prd-core.js';
10
10
  import { normalizeReleaseLedger } from './release-ledger.js';
11
11
  import { appendJsonl, appendText, cjoin, exists, readJson, readText, readYaml, stringifyYaml, writeJson, writeText, writeYaml } from './fs-utils.js';
12
+ import { openProjectLedger } from './kernel/index.js';
12
13
  import { OPENSPEC_TASK_MAX_ITEMS_PER_FILE } from './openspec/constants.js';
13
14
  import { checkStandardsWorkspace, STANDARD_DOCS } from './standards.js';
14
15
  import { timestamp } from './time.js';
@@ -100,11 +101,6 @@ const WORKSPACE_SEED_REFRESH_FILES = [
100
101
  'design/templates/content-home.html',
101
102
  'design/templates/product-launch.html',
102
103
  'design/templates/ops-dashboard.html',
103
- 'design/active/facts-sheet.md',
104
- 'design/active/asset-spec.md',
105
- 'design/active/image-preflight.md',
106
- 'design/active/direction-plan.md',
107
- 'design/active/selected-direction.md',
108
104
  'schema/prd.schema.yaml',
109
105
  'schema/diagram-architecture.schema.yaml',
110
106
  'schema/diagram-product-flow.schema.yaml',
@@ -138,6 +134,7 @@ const WORKSPACE_SEED_COPY_IGNORE = new Set([
138
134
  'benchmarks/inbox',
139
135
  'changes',
140
136
  'discovery',
137
+ 'design/active',
141
138
  'growth',
142
139
  'harness',
143
140
  'knowledge',
@@ -162,6 +159,76 @@ const WORKSPACE_SEED_COPY_IGNORE = new Set([
162
159
  'engagements/active/review.html',
163
160
  'engagements/active/verification.md',
164
161
  ]);
162
+ const ACTIVE_DESIGN_TEMPLATE_FILES = {
163
+ 'facts-sheet.md': `# Facts Sheet
164
+
165
+ | 字段 | 当前值 | 来源 | verifiedAt | 状态 |
166
+ | --- | --- | --- | --- | --- |
167
+ | 产品 / 项目名 | 待填写 | 待确认 | - | pending |
168
+ | 版本 / 发布状态 | 待填写 | 待确认 | - | pending |
169
+ | 关键事实 1 | 待填写 | 待确认 | - | pending |
170
+ | 关键事实 2 | 待填写 | 待确认 | - | pending |
171
+
172
+ ## 备注
173
+
174
+ - 没核实到就保留 \`pending\`
175
+ - 不要把记忆当来源
176
+ `,
177
+ 'asset-spec.md': `# Asset Spec
178
+
179
+ | 类型 | 资产 | 来源 | 状态 | 备注 |
180
+ | --- | --- | --- | --- | --- |
181
+ | logo | 待填写 | 待确认 | pending | |
182
+ | 产品图 | 待填写 | 待确认 | pending | |
183
+ | UI 图 | 待填写 | 待确认 | pending | |
184
+ | 功能图标 | 新入口/按钮/空态默认配图标 | UI 图标 Phosphor;实现库 Lucide/Tabler/React Icons;AI 品牌 LobeHub;技术栈 Tech Icons | pending | 开发新功能时 Agent 自动找图标并登记,不等用户要求 |
185
+ | 摄影 / 插图 | 待填写 | 待确认 | pending | |
186
+ | 色板 / 字体 | 待填写 | 待确认 | pending | |
187
+ | 动效节奏 | 待填写 | 待确认 | pending | |
188
+ | 背景 / 表面 | 待填写 | 待确认 | pending | |
189
+ | 构图记忆点 | 待填写 | 待确认 | pending | |
190
+
191
+ ## 冻结变量
192
+
193
+ - lens: 待填写
194
+ - theme: 待填写
195
+ - layout: 待填写
196
+ - aesthetic: 待填写
197
+ - memory-point: 待填写
198
+ `,
199
+ 'image-preflight.md': `# Image Preflight
200
+
201
+ | 问题 | 回答 |
202
+ | --- | --- |
203
+ | 真实图片是不是页面成立前提 | 待填写 |
204
+ | 必需图片类型 | 待填写 |
205
+ | 计划来源 | 待填写 |
206
+ | 缺失风险 | 待填写 |
207
+ | 降级方案 | 待填写 |
208
+ `,
209
+ 'direction-plan.md': `# Direction Plan
210
+
211
+ | 方向 | 生成逻辑 | lens | theme | layout | 审美主张 | 记忆点 | 为什么不同 | 适用场景 | 主要风险 |
212
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
213
+ | 方向 1 | contrast | 待填写 | 待填写 | 待填写 | 待填写 | 待填写 | 待填写 | 待填写 | 待填写 |
214
+ | 方向 2 | reference-transfer | 待填写 | 待填写 | 待填写 | 待填写 | 待填写 | 待填写 | 待填写 | 待填写 |
215
+ | 方向 3 | design-lens | 待填写 | 待填写 | 待填写 | 待填写 | 待填写 | 待填写 | 待填写 | 待填写 |
216
+ `,
217
+ 'selected-direction.md': `# Selected Direction
218
+
219
+ - selected: 待填写
220
+ - reason: 待填写
221
+ - lens: 待填写
222
+ - theme: 待填写
223
+ - layout: 待填写
224
+ - aesthetic: 待填写
225
+ - memory-point: 待填写
226
+ - components:
227
+ - 待填写
228
+ - follow-up risks:
229
+ - 待填写
230
+ `,
231
+ };
165
232
  const OPERATIONAL_GITIGNORE_ENTRIES = [
166
233
  '.openprd/harness/',
167
234
  '.openprd/learning/',
@@ -662,10 +729,32 @@ function buildWorkflowTaskGraph(snapshot = null, analysis = null, options = {})
662
729
  }
663
730
 
664
731
  async function appendWorkflowEvent(ws, type, payload = {}) {
732
+ const at = timestamp();
733
+ const eventTypeSuffix = String(type ?? 'event')
734
+ .toLowerCase()
735
+ .replace(/[^a-z0-9]+/g, '.')
736
+ .replace(/^\.+|\.+$/g, '') || 'event';
737
+ const jsonPayload = JSON.parse(JSON.stringify(payload ?? {}));
738
+ const ledger = openProjectLedger(ws.projectRoot);
739
+ await ledger.append(
740
+ `workflow:${ws.data?.currentSessionId ?? 'workspace'}`,
741
+ {
742
+ eventType: `workflow.${eventTypeSuffix}`,
743
+ actor: { type: 'openprd-cli' },
744
+ payload: {
745
+ type,
746
+ at,
747
+ ...jsonPayload,
748
+ },
749
+ metadata: {
750
+ compatibilityProjection: path.relative(ws.projectRoot, ws.paths.eventsLog).split(path.sep).join('/'),
751
+ },
752
+ },
753
+ );
665
754
  await appendJsonl(ws.paths.eventsLog, {
666
755
  type,
667
- at: timestamp(),
668
- ...payload,
756
+ at,
757
+ ...jsonPayload,
669
758
  });
670
759
  }
671
760
 
@@ -751,6 +840,12 @@ async function ensureWorkspaceSkeleton(projectRoot, options = {}) {
751
840
  await fs.mkdir(cjoin(workspaceRoot, 'design', 'assets'), { recursive: true });
752
841
  await fs.mkdir(cjoin(workspaceRoot, 'design', 'templates'), { recursive: true });
753
842
  await fs.mkdir(cjoin(workspaceRoot, 'design', 'active'), { recursive: true });
843
+ for (const [filename, content] of Object.entries(ACTIVE_DESIGN_TEMPLATE_FILES)) {
844
+ const targetPath = cjoin(workspaceRoot, 'design', 'active', filename);
845
+ if (options.force || !(await exists(targetPath))) {
846
+ await writeText(targetPath, content);
847
+ }
848
+ }
754
849
  await fs.mkdir(cjoin(workspaceRoot, 'growth'), { recursive: true });
755
850
  await fs.mkdir(cjoin(workspaceRoot, 'knowledge', 'incidents'), { recursive: true });
756
851
  await fs.mkdir(cjoin(workspaceRoot, 'knowledge', 'patterns'), { recursive: true });
@@ -1082,6 +1177,13 @@ async function migrateWorkspaceSkeleton(projectRoot, options = {}) {
1082
1177
  await fs.mkdir(cjoin(workspaceRoot, 'design', 'assets'), { recursive: true });
1083
1178
  await fs.mkdir(cjoin(workspaceRoot, 'design', 'templates'), { recursive: true });
1084
1179
  await fs.mkdir(cjoin(workspaceRoot, 'design', 'active'), { recursive: true });
1180
+ for (const [filename, content] of Object.entries(ACTIVE_DESIGN_TEMPLATE_FILES)) {
1181
+ const targetPath = cjoin(workspaceRoot, 'design', 'active', filename);
1182
+ if (!(await exists(targetPath))) {
1183
+ await writeText(targetPath, content);
1184
+ changes.push({ path: cjoin('.openprd', 'design', 'active', filename), status: 'created' });
1185
+ }
1186
+ }
1085
1187
  await fs.mkdir(cjoin(workspaceRoot, 'growth'), { recursive: true });
1086
1188
  await fs.mkdir(cjoin(workspaceRoot, 'knowledge', 'incidents'), { recursive: true });
1087
1189
  await fs.mkdir(cjoin(workspaceRoot, 'knowledge', 'patterns'), { recursive: true });
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { appendJsonl, cjoin, exists, readJson, readJsonl } from './fs-utils.js';
5
+ import { atomicWriteText, withFileLock } from './kernel/atomic-store.js';
5
6
  import { timestamp } from './time.js';
6
7
 
7
8
  const OPENPRD_WORKSPACE_MARKER = '.openprd';
@@ -56,9 +57,13 @@ function normalizeWorkspaceRegistryEntry(entry) {
56
57
  }
57
58
 
58
59
  function resolveOpenPrdHome(options = {}) {
60
+ const testHome = process.env.NODE_TEST_CONTEXT
61
+ ? cjoin(os.tmpdir(), 'openprd-node-test-home', String(process.pid))
62
+ : null;
59
63
  return path.resolve(
60
64
  options.openprdHome
61
65
  ?? process.env.OPENPRD_HOME
66
+ ?? testHome
62
67
  ?? cjoin(os.homedir(), '.openprd'),
63
68
  );
64
69
  }
@@ -144,7 +149,7 @@ async function upsertWorkspaceRegistryEntry(projectRoot, options = {}) {
144
149
  lastAction: snapshot.lastAction ?? existing?.lastAction ?? 'detected',
145
150
  };
146
151
 
147
- await appendJsonl(registry.registryPath, entry);
152
+ await withFileLock(`${registry.registryPath}.lock`, () => appendJsonl(registry.registryPath, entry));
148
153
  return {
149
154
  home: registry.home,
150
155
  registryPath: registry.registryPath,
@@ -154,9 +159,42 @@ async function upsertWorkspaceRegistryEntry(projectRoot, options = {}) {
154
159
  };
155
160
  }
156
161
 
162
+ function registryBackupName(prefix = 'workspaces') {
163
+ return `${prefix}-${new Date().toISOString().replace(/[:.]/g, '-')}.jsonl`;
164
+ }
165
+
166
+ async function compactWorkspaceRegistry(options = {}) {
167
+ const registryPath = workspaceRegistryFilePath(options);
168
+ return withFileLock(`${registryPath}.lock`, async () => {
169
+ const registry = await readWorkspaceRegistry(options);
170
+ const staleRoots = new Set((registry.staleEntries ?? []).map((entry) => entry.workspaceRoot));
171
+ const entries = options.pruneStale === false
172
+ ? registry.entries
173
+ : registry.entries.filter((entry) => !staleRoots.has(entry.workspaceRoot));
174
+ const backupDir = path.resolve(options.backupDir ?? cjoin(registry.home, 'registry', 'backups'));
175
+ await fs.mkdir(backupDir, { recursive: true });
176
+ const backupPath = cjoin(backupDir, registryBackupName('workspaces'));
177
+ if (await exists(registryPath)) await fs.copyFile(registryPath, backupPath);
178
+ else await atomicWriteText(backupPath, '');
179
+ const body = entries.length > 0 ? `${entries.map((entry) => JSON.stringify(entry)).join('\n')}\n` : '';
180
+ await atomicWriteText(registryPath, body);
181
+ return {
182
+ ok: true,
183
+ registryPath,
184
+ backupPath,
185
+ beforeEvents: registry.totalEvents,
186
+ afterEvents: entries.length,
187
+ kept: entries.length,
188
+ pruned: registry.entries.length - entries.length,
189
+ stale: registry.staleEntries.length,
190
+ };
191
+ });
192
+ }
193
+
157
194
  export {
158
195
  readWorkspaceRegistry,
159
196
  resolveOpenPrdHome,
197
+ compactWorkspaceRegistry,
160
198
  upsertWorkspaceRegistryEntry,
161
199
  workspaceRegistryFilePath,
162
200
  };
@@ -2350,7 +2350,7 @@ async function computeWorkspaceGuidance(ws, options = {}) {
2350
2350
  }
2351
2351
 
2352
2352
 
2353
- async function nextWorkspace(projectRoot) {
2353
+ async function nextWorkspace(projectRoot, options = {}) {
2354
2354
  const ws = await loadWorkspace(projectRoot);
2355
2355
  if (!(await exists(ws.workspaceRoot))) {
2356
2356
  throw new Error(`Missing workspace: ${ws.workspaceRoot}`);
@@ -2373,22 +2373,24 @@ async function nextWorkspace(projectRoot) {
2373
2373
  gates,
2374
2374
  } = guidance;
2375
2375
 
2376
- await writeJson(ws.paths.taskGraph, taskGraph);
2377
- await appendWorkflowEvent(ws, 'next', {
2378
- nextAction,
2379
- reason,
2380
- missingRequiredFields: analysis.missingRequiredFields,
2381
- });
2382
- if (analysis.missingRequiredFields > 0) {
2383
- await appendOpenQuestions(ws, [
2384
- `还有 ${analysis.missingRequiredFields} 个关键信息需要确认。`,
2385
- ...analysis.suggestedQuestions,
2376
+ if (!options.readOnly) {
2377
+ await writeJson(ws.paths.taskGraph, taskGraph);
2378
+ await appendWorkflowEvent(ws, 'next', {
2379
+ nextAction,
2380
+ reason,
2381
+ missingRequiredFields: analysis.missingRequiredFields,
2382
+ });
2383
+ if (analysis.missingRequiredFields > 0) {
2384
+ await appendOpenQuestions(ws, [
2385
+ `还有 ${analysis.missingRequiredFields} 个关键信息需要确认。`,
2386
+ ...analysis.suggestedQuestions,
2387
+ ]);
2388
+ }
2389
+ await appendProgress(ws, [
2390
+ `建议下一步: ${nextAction}。`,
2391
+ `原因: ${reason}`,
2386
2392
  ]);
2387
2393
  }
2388
- await appendProgress(ws, [
2389
- `建议下一步: ${nextAction}。`,
2390
- `原因: ${reason}`,
2391
- ]);
2392
2394
 
2393
2395
  return {
2394
2396
  ws,
@@ -2411,6 +2413,7 @@ async function nextWorkspace(projectRoot) {
2411
2413
  },
2412
2414
  brainstormSuggestion: guidance.brainstormSuggestion,
2413
2415
  workflow: ['brainstorm', 'clarify', 'classify', 'interview', 'synthesize', 'diagram', 'review', 'freeze', 'handoff'],
2416
+ readOnly: Boolean(options.readOnly),
2414
2417
  };
2415
2418
  }
2416
2419