@openprd/cli 0.1.19 → 0.1.22

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 (102) 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/remove-session-tracking-humane-approval/.openprd.yaml +2 -0
  9. package/.openprd/changes/remove-session-tracking-humane-approval/design.md +52 -0
  10. package/.openprd/changes/remove-session-tracking-humane-approval/proposal.md +35 -0
  11. package/.openprd/changes/remove-session-tracking-humane-approval/specs/agent-requirements/spec.md +16 -0
  12. package/.openprd/changes/remove-session-tracking-humane-approval/task-events.jsonl +1 -0
  13. package/.openprd/changes/remove-session-tracking-humane-approval/tasks.md +170 -0
  14. package/.openprd/design/active/asset-spec.md +19 -14
  15. package/.openprd/design/active/direction-plan.md +19 -3
  16. package/.openprd/design/active/facts-sheet.md +16 -7
  17. package/.openprd/design/active/image-preflight.md +6 -5
  18. package/.openprd/design/active/review-studio-v020-directions/compare-plan.json +34 -0
  19. package/.openprd/design/active/review-studio-v020-directions/contact-sheet.jpg +0 -0
  20. package/.openprd/design/active/review-studio-v020-directions/crops/01.png +0 -0
  21. package/.openprd/design/active/review-studio-v020-directions/crops/02.png +0 -0
  22. package/.openprd/design/active/review-studio-v020-directions/crops/03.png +0 -0
  23. package/.openprd/design/active/review-studio-v020-directions/focus-board.template.json +69 -0
  24. package/.openprd/design/active/review-studio-v020-directions/parallel-board.template.json +45 -0
  25. package/.openprd/design/active/review-studio-v020-directions/reference-set.json +143 -0
  26. package/.openprd/design/active/review-studio-v020-directions/source.png +0 -0
  27. package/.openprd/design/active/selected-direction.md +23 -9
  28. package/.openprd/engagements/active/control-plane-architecture.json +203 -0
  29. package/.openprd/engagements/active/control-plane-intake.json +418 -0
  30. package/.openprd/engagements/active/prd.md +183 -119
  31. package/.openprd/engagements/active/review-presentation-v0018.json +176 -0
  32. package/.openprd/i18n-config.json +12 -0
  33. package/.openprd/ledger/events.jsonl +6 -0
  34. package/.openprd/ledger/heads/ebea1a71a9daa566f1c91b53.json +10 -0
  35. package/.openprd/manifest.json +21 -0
  36. package/AGENTS.md +7 -6
  37. package/README.md +31 -29
  38. package/README_EN.md +36 -39
  39. package/package.json +1 -1
  40. package/skills/openprd-frontend-design/SKILL.md +16 -0
  41. package/skills/openprd-harness/SKILL.md +7 -7
  42. package/skills/openprd-quality/SKILL.md +1 -1
  43. package/skills/openprd-requirement-intake/SKILL.md +1 -1
  44. package/skills/openprd-shared/SKILL.md +6 -6
  45. package/src/adapters/adapter-spi.js +193 -0
  46. package/src/adapters/capability-envelope.js +98 -0
  47. package/src/adapters/event-normalizer.js +55 -0
  48. package/src/adapters/index.js +4 -0
  49. package/src/adapters/install-safety.js +249 -0
  50. package/src/agent-canonical-content.js +4 -2
  51. package/src/agent-integration.js +169 -46
  52. package/src/cli/args.js +63 -4
  53. package/src/cli/gate-print.js +17 -0
  54. package/src/cli/quality-commands.js +18 -0
  55. package/src/cli/quality-print.js +10 -0
  56. package/src/cli/runtime-print.js +24 -0
  57. package/src/codex-hook-runner-template.mjs +129 -199
  58. package/src/codex-runtime.js +48 -5
  59. package/src/context/cache.js +245 -0
  60. package/src/context/compiler.js +438 -0
  61. package/src/context/constants.js +30 -0
  62. package/src/context/index.js +39 -0
  63. package/src/context/redaction.js +84 -0
  64. package/src/context/stable.js +69 -0
  65. package/src/context/telemetry.js +42 -0
  66. package/src/dev-standards.js +57 -0
  67. package/src/fleet.js +112 -95
  68. package/src/gates/index.js +2 -0
  69. package/src/gates/scoped-gates.js +256 -0
  70. package/src/gates/store.js +126 -0
  71. package/src/gates/workspace.js +41 -0
  72. package/src/html-artifacts.js +725 -28
  73. package/src/kernel/atomic-store.js +299 -0
  74. package/src/kernel/event-envelope.js +166 -0
  75. package/src/kernel/index.js +4 -0
  76. package/src/kernel/project-ledger.js +467 -0
  77. package/src/kernel/project-manifest.js +205 -0
  78. package/src/knowledge-v3/index.js +1 -0
  79. package/src/knowledge-v3/lifecycle.js +290 -0
  80. package/src/knowledge.js +14 -7
  81. package/src/openprd.js +71 -2
  82. package/src/review-model.js +413 -0
  83. package/src/review-presentation.js +1 -1
  84. package/src/run-harness.js +432 -38
  85. package/src/runtime/cli_runtime_README.md +28 -0
  86. package/src/runtime/errors.js +66 -0
  87. package/src/runtime/index.js +44 -0
  88. package/src/runtime/lane-schema.js +141 -0
  89. package/src/runtime/lane-store.js +279 -0
  90. package/src/runtime/task-runtime.js +449 -0
  91. package/src/runtime/workspace.js +179 -0
  92. package/src/runtime/write-set.js +206 -0
  93. package/src/session-binding.js +16 -3
  94. package/src/session-registry.js +59 -1
  95. package/src/upgrade/fleet-mutation.js +166 -0
  96. package/src/upgrade/fleet-transaction.js +398 -0
  97. package/src/upgrade/transaction-store.js +416 -0
  98. package/src/visual-compare-core.js +66 -27
  99. package/src/visual-compare.js +18 -12
  100. package/src/workspace-core.js +109 -7
  101. package/src/workspace-registry.js +39 -1
  102. package/src/workspace-workflow.js +18 -15
@@ -1,11 +1,18 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { detectRuntime } from './adapters/index.js';
4
+ import {
5
+ compileContextCapsule,
6
+ createContextTelemetryEvent,
7
+ digestStable,
8
+ verifyContextCapsule,
9
+ } from './context/index.js';
3
10
  import { labelExecutionMode, taskExecutionStrategy, describeExecutionStrategy } from './execution-strategy.js';
4
11
  import { appendJsonl, appendText, cjoin, exists, readJson, readJsonl, writeJson, writeText } from './fs-utils.js';
12
+ import { appendJsonlAtomic, atomicWriteJson, readJsonIfExists, withFileLock } from './kernel/index.js';
5
13
  import {
6
14
  OPENPRD_HARNESS_TURN_STATE,
7
15
  recordKnowledgeReviewSignal,
8
- recordKnowledgeSkillAdoption,
9
16
  resolveKnowledgeSkillMatches,
10
17
  reviewKnowledgeWorkspace,
11
18
  } from './knowledge.js';
@@ -26,6 +33,8 @@ const OPENPRD_HARNESS_REQUIREMENT_GATE = cjoin(OPENPRD_HARNESS_DIR, 'requirement
26
33
  const OPENPRD_HARNESS_REQUIREMENT_GATES_DIR = cjoin(OPENPRD_HARNESS_DIR, 'requirement-gates');
27
34
  const OPENPRD_HARNESS_SESSION_BINDINGS_DIR = cjoin(OPENPRD_HARNESS_DIR, 'session-bindings');
28
35
  const OPENPRD_HARNESS_EVENTS = cjoin(OPENPRD_HARNESS_DIR, 'events.jsonl');
36
+ const OPENPRD_HARNESS_CONTEXT_CACHE_DIR = cjoin(OPENPRD_HARNESS_DIR, 'context-cache');
37
+ const OPENPRD_HARNESS_CONTEXT_TELEMETRY = cjoin(OPENPRD_HARNESS_DIR, 'context-telemetry.jsonl');
29
38
  const OPENPRD_WORK_UNITS_DIR = cjoin('.openprd', 'engagements', 'work-units');
30
39
  const OPENPRD_PARALLEL_WORKER_IMPLEMENTATION_TASK_THRESHOLD = 3;
31
40
  const OPENPRD_LOOP_REQUIRED_IMPLEMENTATION_TASK_THRESHOLD = 10;
@@ -85,13 +94,46 @@ async function ensureRunHarness(projectRoot) {
85
94
  }
86
95
  }
87
96
 
97
+ function normalizeAdapterClient(value) {
98
+ const client = String(value ?? '').trim().toLowerCase();
99
+ if (client === 'claude-code') return 'claude';
100
+ return ['codex', 'claude', 'cursor'].includes(client) ? client : null;
101
+ }
102
+
103
+ function runtimeEvidenceCandidate(value, source) {
104
+ const client = normalizeAdapterClient(value?.activeClient ?? value?.client ?? value?.agentType);
105
+ if (!client) return null;
106
+ return {
107
+ source,
108
+ client,
109
+ surface: value?.surface ?? 'unknown',
110
+ executionMode: value?.executionMode ?? 'interactive',
111
+ sessionId: value?.sessionId ?? null,
112
+ threadId: value?.threadId ?? null,
113
+ observedCapabilities: Array.isArray(value?.observedCapabilities) ? value.observedCapabilities : [],
114
+ confidence: Number.isFinite(value?.confidence) ? value.confidence : null,
115
+ turnId: value?.turnId ?? null,
116
+ agentType: value?.agentType ?? null,
117
+ model: value?.model ?? null,
118
+ };
119
+ }
120
+
88
121
  async function readRuntimeEnvironmentContext(projectRoot) {
89
122
  const [runtimeEnvironment, installManifest, hookState] = await Promise.all([
90
123
  readJson(harnessFile(projectRoot, OPENPRD_HARNESS_RUNTIME_ENVIRONMENT)).catch(() => null),
91
124
  readJson(harnessFile(projectRoot, OPENPRD_HARNESS_INSTALL_MANIFEST)).catch(() => null),
92
125
  readJson(harnessFile(projectRoot, OPENPRD_HARNESS_HOOK_STATE)).catch(() => null),
93
126
  ]);
94
- const active = runtimeEnvironment?.active
127
+ const candidates = [
128
+ runtimeEvidenceCandidate(hookState?.runtimeEnvironment, 'hook-payload'),
129
+ runtimeEvidenceCandidate(runtimeEnvironment?.active, 'session-binding'),
130
+ ].filter(Boolean);
131
+ const detectedRuntime = detectRuntime(candidates);
132
+ const selected = candidates.find((candidate) => (
133
+ candidate.client === detectedRuntime.activeClient
134
+ && (detectedRuntime.sessionId == null || candidate.sessionId === detectedRuntime.sessionId)
135
+ )) ?? null;
136
+ const fallbackActive = runtimeEnvironment?.active
95
137
  ?? hookState?.runtimeEnvironment
96
138
  ?? {
97
139
  activeClient: 'unknown',
@@ -101,8 +143,26 @@ async function readRuntimeEnvironmentContext(projectRoot) {
101
143
  evidence: [],
102
144
  observedCapabilities: [],
103
145
  };
146
+ const active = detectedRuntime.activeClient === 'unknown'
147
+ ? fallbackActive
148
+ : {
149
+ ...fallbackActive,
150
+ activeClient: detectedRuntime.activeClient,
151
+ surface: detectedRuntime.surface,
152
+ executionMode: detectedRuntime.executionMode,
153
+ sessionId: detectedRuntime.sessionId,
154
+ threadId: detectedRuntime.threadId,
155
+ confidence: detectedRuntime.confidence,
156
+ evidence: detectedRuntime.evidence,
157
+ observedCapabilities: selected?.observedCapabilities ?? [],
158
+ capabilityEnvelope: detectedRuntime.capabilityEnvelope,
159
+ turnId: selected?.turnId ?? fallbackActive?.turnId ?? null,
160
+ agentType: selected?.agentType ?? fallbackActive?.agentType ?? null,
161
+ model: selected?.model ?? fallbackActive?.model ?? null,
162
+ };
104
163
  return {
105
164
  active,
165
+ detectedRuntime,
106
166
  runtimeDetection: installManifest?.runtimeDetection ?? runtimeEnvironment?.runtimeDetection ?? null,
107
167
  platformCapabilityPacks: installManifest?.platformCapabilityPacks ?? runtimeEnvironment?.platformCapabilityPacks ?? [],
108
168
  files: {
@@ -113,8 +173,10 @@ async function readRuntimeEnvironmentContext(projectRoot) {
113
173
  };
114
174
  }
115
175
 
116
- async function readRunState(projectRoot) {
117
- await ensureRunHarness(projectRoot);
176
+ async function readRunState(projectRoot, options = {}) {
177
+ if (options.ensure !== false) {
178
+ await ensureRunHarness(projectRoot);
179
+ }
118
180
  return readJson(harnessFile(projectRoot, OPENPRD_HARNESS_RUN_STATE)).catch(() => ({
119
181
  version: 1,
120
182
  active: true,
@@ -122,6 +184,349 @@ async function readRunState(projectRoot) {
122
184
  }));
123
185
  }
124
186
 
187
+ function compactContextLine(label, value) {
188
+ const text = String(value ?? '').replace(/\s+/g, ' ').trim();
189
+ return text ? `${label}: ${text}` : null;
190
+ }
191
+
192
+ function boundedContextText(value, maxLength = 240) {
193
+ const text = String(value ?? '').replace(/\s+/g, ' ').trim();
194
+ if (!text) return null;
195
+ return text.length > maxLength ? `${text.slice(0, Math.max(1, maxLength - 1))}…` : text;
196
+ }
197
+
198
+ function runContextRevision(context, message) {
199
+ return digestStable({
200
+ messageDigest: message ? digestStable(String(message)) : null,
201
+ valid: context.validation?.valid ?? false,
202
+ latestPrd: context.latestPrd,
203
+ activeChange: context.activeChange,
204
+ activeRequirementGate: context.activeRequirementGate,
205
+ focus: context.focus,
206
+ taskSummary: context.taskSummary,
207
+ nextTask: context.nextTask,
208
+ discovery: context.discovery,
209
+ lane: context.lane,
210
+ recommendation: context.recommendation,
211
+ runtimeEnvironment: context.runtimeEnvironment?.active ?? null,
212
+ knowledgeSkills: (context.knowledgeSkills?.matched ?? []).map((skill) => ({
213
+ skillName: skill.skillName,
214
+ path: skill.path,
215
+ score: skill.score,
216
+ sourceRef: skill.sourceRef,
217
+ })),
218
+ });
219
+ }
220
+
221
+ function buildRunContextCapsule(context, options = {}) {
222
+ const activeClient = context.runtimeEnvironment?.active?.activeClient ?? 'unknown';
223
+ const surface = context.runtimeEnvironment?.active?.surface ?? 'unknown';
224
+ const taskProgress = context.taskSummary
225
+ ? `${context.taskSummary.completed}/${context.taskSummary.total} 完成,${context.taskSummary.pending} 待处理,${context.taskSummary.blocked} 阻塞`
226
+ : null;
227
+ const knowledge = (context.knowledgeSkills?.matched ?? []).slice(0, 3).map((skill) => ({
228
+ name: skill.skillName,
229
+ useWhen: boundedContextText(skill.useWhen ?? skill.description, 180),
230
+ reviewFirst: (skill.reviewFirst ?? []).slice(0, 2).map((item) => boundedContextText(item, 120)),
231
+ antiPatterns: (skill.antiPatterns ?? []).slice(0, 1).map((item) => boundedContextText(item, 120)),
232
+ sourceRef: skill.sourceRef ?? null,
233
+ }));
234
+ const activeCapabilityPack = (context.runtimeEnvironment?.platformCapabilityPacks ?? [])
235
+ .find((pack) => normalizeAdapterClient(pack?.client ?? pack?.id) === activeClient);
236
+ const compactNextTask = context.nextTask
237
+ ? {
238
+ id: context.nextTask.id ?? context.nextTask.taskId ?? null,
239
+ taskHandle: context.nextTask.taskHandle ?? null,
240
+ title: boundedContextText(context.nextTask.title ?? context.nextTask.summary, 180),
241
+ status: context.nextTask.status ?? null,
242
+ }
243
+ : null;
244
+ const confirmation = context.recommendation?.executionConfirmationChecklist;
245
+ const compactConfirmation = confirmation?.required
246
+ ? {
247
+ required: true,
248
+ objective: boundedContextText(confirmation.objective, 180),
249
+ scope: (confirmation.scope ?? []).slice(0, 2).map((item) => boundedContextText(item, 140)),
250
+ implementation: (confirmation.implementationItems ?? []).slice(0, 2).map((item) => boundedContextText(item, 140)),
251
+ verification: (confirmation.verification ?? []).slice(0, 2).map((item) => boundedContextText(item, 140)),
252
+ risks: (confirmation.risks ?? []).slice(0, 2).map((item) => boundedContextText(item, 140)),
253
+ }
254
+ : null;
255
+ const clarifyReplyRequired = (context.recommendation?.nextAction ?? '') === 'clarify-user'
256
+ || context.recommendation?.title === 'clarify-user'
257
+ || context.recommendation?.title === '继续本轮需求入口澄清';
258
+ const candidates = [
259
+ {
260
+ id: 'requirement-gate',
261
+ authority: 'user-confirmed',
262
+ freshness: 'current',
263
+ relevance: context.activeRequirementGate ? 100 : 0,
264
+ value: context.activeRequirementGate
265
+ ? {
266
+ status: context.activeRequirementGate.status,
267
+ relevance: context.activeRequirementGate.relevance,
268
+ promptPreview: context.activeRequirementGate.promptPreview,
269
+ }
270
+ : null,
271
+ },
272
+ {
273
+ id: 'current-focus',
274
+ authority: 'project-canonical',
275
+ freshness: 'current',
276
+ relevance: 95,
277
+ value: {
278
+ activeChange: context.activeChange,
279
+ focus: {
280
+ changeId: context.focus?.changeId ?? null,
281
+ workUnitId: context.focus?.workUnitId ?? null,
282
+ taskHandle: context.focus?.taskHandle ?? null,
283
+ sessionId: context.focus?.sessionId ?? null,
284
+ source: context.focus?.source ?? null,
285
+ },
286
+ latestPrd: context.latestPrd
287
+ ? {
288
+ versionId: context.latestPrd.versionId ?? null,
289
+ workUnitId: context.latestPrd.workUnitId ?? null,
290
+ title: boundedContextText(context.latestPrd.title, 180),
291
+ status: context.latestPrd.status ?? null,
292
+ }
293
+ : null,
294
+ },
295
+ },
296
+ {
297
+ id: 'project-knowledge',
298
+ authority: 'project-canonical',
299
+ freshness: 'latest',
300
+ relevance: knowledge.length > 0 ? 90 : 0,
301
+ value: knowledge.length > 0 ? knowledge : null,
302
+ },
303
+ {
304
+ id: 'applicable-scoped-gates',
305
+ authority: 'project-canonical',
306
+ freshness: 'current',
307
+ relevance: (context.scopedGates ?? []).length > 0 ? 100 : 0,
308
+ value: (context.scopedGates ?? []).length > 0 ? context.scopedGates : null,
309
+ },
310
+ {
311
+ id: 'agent-interaction-contract',
312
+ authority: 'project-canonical',
313
+ freshness: 'current',
314
+ relevance: 92,
315
+ value: {
316
+ expression: '面向用户使用普通人能理解的结果、影响和下一步;内部编号、命令和路径只在确有帮助时出现。',
317
+ replyBoundary: clarifyReplyRequired
318
+ ? '本轮只输出需求摘要或追问一个最高价值澄清点,不承诺直接实现。'
319
+ : null,
320
+ approvalBoundary: context.recommendation?.executionConfirmationChecklist?.required
321
+ ? '执行前必须使用当前任务绑定的确认清单,不能复用其他任务的泛化确认。'
322
+ : null,
323
+ },
324
+ },
325
+ {
326
+ id: 'runtime-environment',
327
+ authority: 'runtime-observed',
328
+ freshness: 'current',
329
+ relevance: activeClient === 'unknown' ? 20 : 90,
330
+ value: {
331
+ activeClient,
332
+ surface,
333
+ executionMode: context.runtimeEnvironment?.active?.executionMode ?? null,
334
+ confidence: context.runtimeEnvironment?.active?.confidence ?? null,
335
+ observedCapabilities: context.runtimeEnvironment?.active?.observedCapabilities ?? [],
336
+ capabilityPack: activeCapabilityPack
337
+ ? {
338
+ id: activeCapabilityPack.id,
339
+ capabilities: (activeCapabilityPack.capabilities ?? []).map((capability) => ({
340
+ id: capability.id,
341
+ status: capability.status,
342
+ })),
343
+ }
344
+ : null,
345
+ },
346
+ },
347
+ {
348
+ id: 'task-progress',
349
+ authority: 'project-derived',
350
+ freshness: 'current',
351
+ relevance: context.taskSummary ? 85 : 10,
352
+ value: context.taskSummary
353
+ ? { summary: taskProgress, nextTask: compactNextTask, lane: boundedContextText(context.lane?.summary, 180) }
354
+ : { lane: boundedContextText(context.lane?.summary, 180), nextTask: compactNextTask },
355
+ },
356
+ {
357
+ id: 'recommended-next-step',
358
+ authority: 'project-derived',
359
+ freshness: 'current',
360
+ relevance: 100,
361
+ value: {
362
+ title: boundedContextText(context.recommendation?.title, 180),
363
+ reason: boundedContextText(context.recommendation?.reason, 260),
364
+ nextAction: context.recommendation?.nextAction ?? null,
365
+ executionMode: context.recommendation?.executionMode ?? null,
366
+ executionCommand: boundedContextText(
367
+ context.recommendation?.executionCommand ?? context.recommendation?.preparationCommand,
368
+ 260,
369
+ ),
370
+ verifyCommand: boundedContextText(context.recommendation?.verifyCommand, 220),
371
+ confirmation: compactConfirmation,
372
+ },
373
+ },
374
+ {
375
+ id: 'project-health',
376
+ authority: 'project-derived',
377
+ freshness: 'current',
378
+ relevance: context.validation?.valid ? 50 : 100,
379
+ value: {
380
+ valid: context.validation?.valid ?? false,
381
+ errors: (context.validation?.errors ?? []).slice(0, 3).map((item) => boundedContextText(item, 180)),
382
+ warnings: (context.validation?.warnings ?? []).slice(0, 3).map((item) => boundedContextText(item, 180)),
383
+ },
384
+ },
385
+ ];
386
+ const request = {
387
+ workspaceId: context.projectRoot,
388
+ revision: runContextRevision(context, options.message),
389
+ taskId: context.focus?.taskHandle ?? context.nextTask?.taskHandle ?? context.nextTask?.id ?? null,
390
+ intent: options.message ?? null,
391
+ delivery: options.previousCapsule ? 'delta' : 'full',
392
+ tokenBudget: options.tokenBudget ?? 800,
393
+ deltaTokenBudget: options.deltaTokenBudget ?? 250,
394
+ candidates,
395
+ };
396
+ const capsule = compileContextCapsule(request, { previousCapsule: options.previousCapsule ?? null });
397
+ const renderedLines = [
398
+ `OpenPrd ContextCapsule v1 · ${capsule.delivery} · ${capsule.budget.estimatedTokens}/${capsule.budget.limitTokens} tokens · ${String(capsule.digest).slice(-12)}`,
399
+ ...capsule.entries.map((entry) => {
400
+ if (entry.id === 'recommended-next-step') {
401
+ return [
402
+ compactContextLine('建议下一步', entry.value?.title),
403
+ compactContextLine('这样安排的原因', entry.value?.reason),
404
+ compactContextLine('内部执行参考', entry.value?.executionCommand ?? entry.value?.preparationCommand),
405
+ ].filter(Boolean).join('\n');
406
+ }
407
+ if (entry.id === 'task-progress') {
408
+ return [
409
+ compactContextLine('当前处理路径', entry.value?.lane),
410
+ compactContextLine('任务进度', entry.value?.summary),
411
+ ].filter(Boolean).join('\n');
412
+ }
413
+ if (entry.id === 'runtime-environment') return compactContextLine('当前 Agent 环境', `${entry.value?.activeClient ?? 'unknown'} / ${entry.value?.surface ?? 'unknown'}`);
414
+ if (entry.id === 'requirement-gate') return compactContextLine('当前处理阶段', `${entry.value?.status ?? 'active'}(${entry.value?.relevance ?? 'primary'})`);
415
+ if (entry.id === 'project-health') return compactContextLine('基础检查', entry.value?.valid ? '通过' : '失败');
416
+ if (entry.id === 'current-focus') return compactContextLine('当前聚焦事项', entry.value?.focus?.changeId ?? entry.value?.activeChange ?? entry.value?.latestPrd?.title);
417
+ if (entry.id === 'project-knowledge') return compactContextLine('项目经验', `${entry.value?.length ?? 0} 条已按作用域匹配`);
418
+ if (entry.id === 'applicable-scoped-gates') {
419
+ return compactContextLine('适用门禁', (entry.value ?? []).map((gate) => (
420
+ `${gate.operation}@${gate.scope?.type}:${gate.scope?.id}${gate.latestDecision?.status ? `=${gate.latestDecision.status}` : ''}`
421
+ )).join(';'));
422
+ }
423
+ if (entry.id === 'agent-interaction-contract') {
424
+ return [
425
+ compactContextLine('表达约束', entry.value?.expression),
426
+ compactContextLine('当前回复边界', entry.value?.replyBoundary),
427
+ compactContextLine('批准边界', entry.value?.approvalBoundary),
428
+ ].filter(Boolean).join('\n');
429
+ }
430
+ return null;
431
+ }).filter(Boolean),
432
+ ];
433
+ if (capsule.delivery === 'delta' && capsule.entries.length === 0) {
434
+ renderedLines.push(`上下文无变化,沿用 ${String(capsule.delta?.previousCapsuleDigest ?? '上一份 ContextCapsule').slice(-12)}`);
435
+ } else if (capsule.delivery === 'delta' && capsule.delta.removed.length > 0) {
436
+ renderedLines.push(`已移出上下文: ${capsule.delta.removed.join('、')}`);
437
+ }
438
+ return {
439
+ capsule,
440
+ renderedText: renderedLines.join('\n'),
441
+ };
442
+ }
443
+
444
+ function scopedGateMatchesContext(gate, projectRoot, context) {
445
+ const scope = gate?.scope;
446
+ if (!scope?.type || !scope?.id) return false;
447
+ if (scope.type === 'project') return true;
448
+ const ids = new Set([
449
+ context.activeChange,
450
+ context.focus?.changeId,
451
+ context.focus?.workUnitId,
452
+ context.focus?.taskHandle,
453
+ context.nextTask?.id,
454
+ context.nextTask?.taskId,
455
+ context.nextTask?.taskHandle,
456
+ context.lane?.kind,
457
+ context.lane?.id,
458
+ context.recommendation?.nextAction,
459
+ context.recommendation?.title,
460
+ path.resolve(projectRoot),
461
+ path.basename(path.resolve(projectRoot)),
462
+ ].filter(Boolean).map(String));
463
+ return ids.has(String(scope.id));
464
+ }
465
+
466
+ async function readApplicableScopedGates(projectRoot, context) {
467
+ const state = await readJson(path.join(projectRoot, '.openprd', 'gates', 'state.json')).catch(() => null);
468
+ if (!state?.gates || typeof state.gates !== 'object') return [];
469
+ const decisions = Array.isArray(state.decisions) ? state.decisions : [];
470
+ return Object.values(state.gates)
471
+ .filter((gate) => scopedGateMatchesContext(gate, projectRoot, context))
472
+ .map((gate) => ({
473
+ gateId: gate.gateId,
474
+ scope: gate.scope,
475
+ operation: gate.operation,
476
+ risk: gate.risk,
477
+ requiredEvidence: gate.requiredEvidence,
478
+ approval: gate.approval,
479
+ latestDecision: [...decisions].reverse().find((decision) => decision.gateId === gate.gateId) ?? null,
480
+ }))
481
+ .sort((left, right) => String(left.gateId).localeCompare(String(right.gateId)));
482
+ }
483
+
484
+ function runContextCacheFile(projectRoot, context) {
485
+ const active = context.runtimeEnvironment?.active ?? {};
486
+ const identity = digestStable({
487
+ projectRoot: path.resolve(projectRoot),
488
+ client: active.activeClient ?? 'unknown',
489
+ sessionId: active.sessionId ?? context.focus?.sessionId ?? null,
490
+ threadId: active.threadId ?? null,
491
+ }).replace(/^sha256:/, '').slice(0, 24);
492
+ return harnessFile(projectRoot, cjoin(OPENPRD_HARNESS_CONTEXT_CACHE_DIR, `last-${identity}.json`));
493
+ }
494
+
495
+ async function compileRunContextDelivery(projectRoot, context, options = {}) {
496
+ if (!options.hookInject) {
497
+ return {
498
+ ...buildRunContextCapsule(context, options),
499
+ cacheStatus: 'not-used',
500
+ cachePath: null,
501
+ telemetry: null,
502
+ };
503
+ }
504
+ const cachePath = runContextCacheFile(projectRoot, context);
505
+ return withFileLock(`${cachePath}.lock`, async () => {
506
+ const cached = await readJsonIfExists(cachePath, null).catch(() => null);
507
+ const cachedCapsule = verifyContextCapsule(cached?.capsule) ? cached.capsule : null;
508
+ const explicitPrevious = verifyContextCapsule(options.previousCapsule) ? options.previousCapsule : null;
509
+ const previousCapsule = explicitPrevious ?? cachedCapsule;
510
+ const delivery = buildRunContextCapsule(context, { ...options, previousCapsule });
511
+ const cacheStatus = explicitPrevious ? 'explicit-previous' : cachedCapsule ? 'session-hit' : 'session-miss';
512
+ const telemetry = createContextTelemetryEvent({ capsule: delivery.capsule, cacheStatus });
513
+ await atomicWriteJson(cachePath, {
514
+ schemaVersion: 1,
515
+ workspaceId: path.resolve(projectRoot),
516
+ client: context.runtimeEnvironment?.active?.activeClient ?? 'unknown',
517
+ sessionId: context.runtimeEnvironment?.active?.sessionId ?? context.focus?.sessionId ?? null,
518
+ capsule: delivery.capsule,
519
+ });
520
+ await appendJsonlAtomic(harnessFile(projectRoot, OPENPRD_HARNESS_CONTEXT_TELEMETRY), telemetry);
521
+ return {
522
+ ...delivery,
523
+ cacheStatus,
524
+ cachePath,
525
+ telemetry,
526
+ };
527
+ });
528
+ }
529
+
125
530
  async function readActiveRequirementGate(projectRoot) {
126
531
  const gate = await readJson(harnessFile(projectRoot, OPENPRD_HARNESS_REQUIREMENT_GATE)).catch(() => null);
127
532
  return gate?.active ? gate : null;
@@ -256,7 +661,7 @@ function executionGate() {
256
661
  confirmationChecklistRequired: true,
257
662
  allowedIntents: ['开发', '实现', '修复', '继续任务', '落地执行', '深度调研', '深度对标', '复刻落地', '提交'],
258
663
  readOnlyIntents: ['看看', '规划', '梳理', '分析', '评估', '预计动哪些文件', '怎么改', '代码审查'],
259
- rule: '只有当用户当前明确要求实现、继续、深度调研、对标或提交时,才运行 executionCommand。单纯的“请帮我实现/继续实现”只表示有执行意图,不表示可以跳过 requirement 摘要确认、`capture/classify/synthesize` 写入路径或 review;只有用户明确表示“不需要进行任何确认”时,才允许静默走完整 requirement write path,并对当前精确匹配的稳定 review artifact 记录确认。即使用户授权实现,也不等于授权云端热修复、生产远端写入、业务兜底、写死逻辑或缓存补行;这些动作必须由当前用户明确批准,并说明如何同步回本地源码、配置或迁移。若还需要向用户索取执行授权,先展示 executionConfirmationChecklist,再请求明确确认;规划、分析、文件影响范围和审查类请求保持只读,并基于证据回答。',
664
+ rule: '用户明确要求实现、继续、深度调研、对标、部署、发布或提交时才运行 executionCommandrequirement 摘要、capture/classify/synthesize、review、change、tasks、设计合同和验证材料由 Agent 在后台自动维护,不得要求用户补固定口令、digest、work-unit id 或命令,也不得仅因材料缺失阻断正常实现。云端、生产、支付、删除、账号切换、兜底、写死、权益或额度等信号只用于提醒 Agent 优先选择配置化、迁移、标准状态机、幂等脚本或可回滚发布路径;OpenPrd 不承担授权审批、不阻断工具调用、不索取用户确认,是否需要确认由当前 Agent 依据宿主安全规则和真实上下文自行判断。规划、分析、文件影响范围和审查类请求保持只读,并基于证据回答。',
260
665
  };
261
666
  }
262
667
 
@@ -1846,8 +2251,7 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
1846
2251
  resumeOpenSpecDiscoveryWorkspace,
1847
2252
  validateWorkspace,
1848
2253
  } = dependencies;
1849
- await ensureRunHarness(projectRoot);
1850
- const runState = await readRunState(projectRoot);
2254
+ const runState = await readRunState(projectRoot, { ensure: false });
1851
2255
  const currentTurn = await readJson(harnessFile(projectRoot, OPENPRD_HARNESS_TURN_STATE)).catch(() => null);
1852
2256
  const runtimeEnvironment = await readRuntimeEnvironmentContext(projectRoot);
1853
2257
  const currentTurnPrompt = String(currentTurn?.prompt ?? '').trim();
@@ -1866,7 +2270,7 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
1866
2270
  warnings: [],
1867
2271
  checks: [],
1868
2272
  }));
1869
- const next = await nextWorkspace(projectRoot).catch(() => null);
2273
+ const next = await nextWorkspace(projectRoot, { readOnly: true }).catch(() => null);
1870
2274
  const requirementGate = await readActiveRequirementGate(projectRoot);
1871
2275
  const changes = await listOpenPrdChangesWorkspace(projectRoot).catch(() => null);
1872
2276
  const activeChange = changes?.activeChange ?? null;
@@ -1980,32 +2384,9 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
1980
2384
  activeChange ? `.openprd/changes/${activeChange}` : null,
1981
2385
  ].filter(Boolean),
1982
2386
  limit: options.hookInject ? 4 : 3,
2387
+ readOnly: true,
1983
2388
  }).catch(() => ({ matched: [], summary: { matched: 0 } }));
1984
- const knowledgeAdoption = knowledgeSkillMatches.matched?.length > 0
1985
- ? await recordKnowledgeSkillAdoption(projectRoot, {
1986
- matches: knowledgeSkillMatches.matched,
1987
- stages: options.hookInject ? ['hit', 'referenced', 'injected'] : ['hit', 'referenced'],
1988
- source: options.hookInject ? 'run-context-hook' : 'run-context',
1989
- sessionId: lane.target?.sessionId ?? resolvedTarget?.sessionId ?? requirementGate?.sessionId ?? null,
1990
- promptPreview: effectiveMessage,
1991
- }).catch(() => null)
1992
- : null;
1993
- const knowledgeStageBump = {
1994
- hitCount: options.hookInject ? 1 : 1,
1995
- referencedCount: 1,
1996
- injectedCount: options.hookInject ? 1 : 0,
1997
- };
1998
- const renderedKnowledgeSkills = (knowledgeSkillMatches.matched ?? []).map((skill) => ({
1999
- ...skill,
2000
- adoption: skill.adoption
2001
- ? {
2002
- ...skill.adoption,
2003
- hitCount: Number(skill.adoption.hitCount ?? 0) + knowledgeStageBump.hitCount,
2004
- referencedCount: Number(skill.adoption.referencedCount ?? 0) + knowledgeStageBump.referencedCount,
2005
- injectedCount: Number(skill.adoption.injectedCount ?? 0) + knowledgeStageBump.injectedCount,
2006
- }
2007
- : skill.adoption,
2008
- }));
2389
+ const renderedKnowledgeSkills = knowledgeSkillMatches.matched ?? [];
2009
2390
 
2010
2391
  const context = {
2011
2392
  ok: validation.valid,
@@ -2078,7 +2459,8 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
2078
2459
  hookInjected: Boolean(options.hookInject && renderedKnowledgeSkills.length > 0),
2079
2460
  reviewRequired: Boolean(knowledgeSkillMatches.mandatoryCheck?.required),
2080
2461
  reviewMode: knowledgeSkillMatches.mandatoryCheck?.mode ?? null,
2081
- adoption: knowledgeAdoption?.summary ?? null,
2462
+ adoption: null,
2463
+ recorded: false,
2082
2464
  },
2083
2465
  },
2084
2466
  files: {
@@ -2089,11 +2471,23 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
2089
2471
  },
2090
2472
  };
2091
2473
 
2092
- await writeRunState(projectRoot, {
2093
- ...runState,
2094
- lastContextAt: context.generatedAt,
2095
- lastRecommendation: effectiveRecommendation,
2474
+ context.scopedGates = await readApplicableScopedGates(projectRoot, context);
2475
+ const contextDelivery = await compileRunContextDelivery(projectRoot, context, {
2476
+ hookInject: options.hookInject,
2477
+ message: effectiveMessage,
2478
+ previousCapsule: options.previousCapsule ?? null,
2479
+ tokenBudget: options.tokenBudget,
2480
+ deltaTokenBudget: options.deltaTokenBudget,
2096
2481
  });
2482
+ context.contextCapsule = contextDelivery.capsule;
2483
+ context.contextText = contextDelivery.renderedText;
2484
+ context.contextCache = {
2485
+ status: contextDelivery.cacheStatus,
2486
+ path: contextDelivery.cachePath,
2487
+ };
2488
+ context.contextTelemetry = contextDelivery.telemetry;
2489
+ context.readOnly = true;
2490
+ context.sideEffects = options.hookInject ? ['context-cache', 'context-telemetry'] : [];
2097
2491
 
2098
2492
  return context;
2099
2493
  }
@@ -0,0 +1,28 @@
1
+ # Lane/Task Runtime 文件夹说明书
2
+
3
+ ## 核心功能
4
+
5
+ 为同一 OpenPrd 项目中的多 Agent、多线程和多工作树提供可持久化、可并发控制的 Lane/Task 运行时。
6
+
7
+ ## 输入
8
+
9
+ 接收项目级 lane、task、claim 身份、租约、write-set、revision 与验证证据。
10
+
11
+ ## 输出
12
+
13
+ 输出原子 task claim、心跳续租、释放、完成、过期恢复、CAS 冲突和 write-set 冲突结果。
14
+
15
+ ## 定位
16
+
17
+ 本目录是 OpenPrd 项目控制面的独立运行时内核;当前不直接接管 CLI、loop、session 或平台 Adapter。
18
+
19
+ ## 依赖
20
+
21
+ 仅依赖 Node.js 20 内建模块和目录内纯 JavaScript 模块,不依赖第三方包。
22
+
23
+ ## 维护规则
24
+
25
+ - 所有状态 read-modify-write 必须通过 `LaneStore` 原子事务。
26
+ - claim token、lease 与 revision 校验不得被调用端绕过。
27
+ - write-set 不能证明互斥时必须保守判冲突。
28
+ - CLI、Hook、loop 和平台 Adapter 接线应从 `index.js` 入口导入,不复制 runtime 状态逻辑。
@@ -0,0 +1,66 @@
1
+ /*
2
+ * ## 核心功能
3
+ * 为 Lane/Task Runtime 提供稳定、可机器判断的结构化错误契约。
4
+ *
5
+ * ## 输入
6
+ * 接收错误码、面向人的错误说明,以及冲突任务、claim 或 revision 等诊断细节。
7
+ *
8
+ * ## 输出
9
+ * 导出带 code/details/toJSON 的 OpenPrdRuntimeError 与三类并发冲突构造器。
10
+ *
11
+ * ## 定位
12
+ * 位于 runtime 子系统的协议边界,不负责存储、重试或冲突判定。
13
+ *
14
+ * ## 依赖
15
+ * 仅依赖 JavaScript Error 基类,不依赖第三方包。
16
+ *
17
+ * ## 维护规则
18
+ * 错误码属于跨 CLI、Hook 与 Adapter 的稳定协议;新增错误时保持旧 code 与 details 字段兼容。
19
+ */
20
+
21
+ export const OPENPRD_TASK_ALREADY_CLAIMED = 'OPENPRD_TASK_ALREADY_CLAIMED';
22
+ export const OPENPRD_WRITE_SET_CONFLICT = 'OPENPRD_WRITE_SET_CONFLICT';
23
+ export const OPENPRD_REVISION_CONFLICT = 'OPENPRD_REVISION_CONFLICT';
24
+
25
+ export class OpenPrdRuntimeError extends Error {
26
+ constructor(code, message, details = {}, options = {}) {
27
+ super(message, options);
28
+ this.name = 'OpenPrdRuntimeError';
29
+ this.code = code;
30
+ this.details = structuredClone(details ?? {});
31
+ }
32
+
33
+ toJSON() {
34
+ return {
35
+ name: this.name,
36
+ code: this.code,
37
+ message: this.message,
38
+ details: structuredClone(this.details),
39
+ };
40
+ }
41
+ }
42
+
43
+ export function taskAlreadyClaimedError(details = {}) {
44
+ const taskId = details.taskId ? ` ${details.taskId}` : '';
45
+ return new OpenPrdRuntimeError(
46
+ OPENPRD_TASK_ALREADY_CLAIMED,
47
+ `OpenPrd task${taskId} is already claimed or the supplied claim is no longer current.`,
48
+ details,
49
+ );
50
+ }
51
+
52
+ export function writeSetConflictError(details = {}) {
53
+ return new OpenPrdRuntimeError(
54
+ OPENPRD_WRITE_SET_CONFLICT,
55
+ 'OpenPrd cannot claim this task because its write-set overlaps an active claim.',
56
+ details,
57
+ );
58
+ }
59
+
60
+ export function revisionConflictError(details = {}) {
61
+ return new OpenPrdRuntimeError(
62
+ OPENPRD_REVISION_CONFLICT,
63
+ `OpenPrd revision conflict: expected ${details.expectedRevision}, current ${details.actualRevision}.`,
64
+ details,
65
+ );
66
+ }