@openprd/cli 0.1.19 → 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 (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 +1 -0
  37. package/README.md +2 -0
  38. package/README_EN.md +2 -0
  39. package/package.json +1 -1
  40. package/skills/openprd-frontend-design/SKILL.md +16 -0
  41. package/skills/openprd-harness/SKILL.md +1 -1
  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 +1 -1
  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 +147 -42
  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 +65 -77
  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 +431 -37
  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
@@ -0,0 +1,290 @@
1
+ /*
2
+ * 核心功能
3
+ * 提供 Knowledge v3 的验证、晋升、废弃、替代与预算化检索生命周期。
4
+ *
5
+ * 输入
6
+ * 接收带作用域、证据、有效期、reviewer 和替代链的知识条目。
7
+ *
8
+ * 输出
9
+ * 返回不可变的新条目或带 included/excluded reason 的查询结果,不写 telemetry。
10
+ *
11
+ * 定位
12
+ * 位于项目事实与上下文编译器之间;只有 active 且未过期知识可进入 Agent 上下文。
13
+ *
14
+ * 维护规则
15
+ * promotion 必须保留 reviewer 与证据,supersede 必须双向可追溯,查询预算不得静默超限。
16
+ */
17
+
18
+ import { createHash } from 'node:crypto';
19
+ import { canonicalJson } from '../kernel/event-envelope.js';
20
+
21
+ const KNOWLEDGE_V3_SCHEMA_VERSION = 3;
22
+ const KNOWLEDGE_STATUSES = Object.freeze(['draft', 'active', 'deprecated', 'superseded']);
23
+ const KNOWLEDGE_SCOPE_TYPES = Object.freeze(['project', 'change', 'work-unit', 'task', 'lane']);
24
+
25
+ class KnowledgeLifecycleError extends Error {
26
+ constructor(message, details = {}) {
27
+ super(message);
28
+ this.name = 'KnowledgeLifecycleError';
29
+ this.code = 'OPENPRD_KNOWLEDGE_LIFECYCLE_INVALID';
30
+ Object.assign(this, details);
31
+ }
32
+ }
33
+
34
+ function stableId(value) {
35
+ return `knowledge_${createHash('sha256').update(canonicalJson(value)).digest('hex').slice(0, 24)}`;
36
+ }
37
+
38
+ function text(value, field) {
39
+ const normalized = String(value ?? '').trim();
40
+ if (!normalized || /[\0\r]/.test(normalized)) throw new KnowledgeLifecycleError(`${field} 必须是非空文本。`, { field });
41
+ return normalized;
42
+ }
43
+
44
+ function iso(value, field) {
45
+ const date = value instanceof Date ? value : new Date(value);
46
+ if (Number.isNaN(date.getTime())) throw new KnowledgeLifecycleError(`${field} 必须是有效时间。`, { field });
47
+ return date.toISOString();
48
+ }
49
+
50
+ function normalizeScope(scope) {
51
+ const type = text(scope?.type, 'scope.type');
52
+ if (!KNOWLEDGE_SCOPE_TYPES.includes(type)) throw new KnowledgeLifecycleError(`不支持的知识作用域:${type}。`);
53
+ return { type, id: text(scope?.id, 'scope.id') };
54
+ }
55
+
56
+ function normalizeEvidence(evidence = []) {
57
+ return evidence.map((item, index) => ({
58
+ ref: text(item?.ref, `evidence[${index}].ref`),
59
+ digest: text(item?.digest, `evidence[${index}].digest`),
60
+ kind: text(item?.kind ?? 'artifact', `evidence[${index}].kind`),
61
+ observedAt: item?.observedAt ? iso(item.observedAt, `evidence[${index}].observedAt`) : null,
62
+ }));
63
+ }
64
+
65
+ function normalizeReviewer(reviewer) {
66
+ if (!reviewer) return null;
67
+ return {
68
+ type: text(reviewer.type ?? 'user', 'reviewer.type'),
69
+ id: text(reviewer.id, 'reviewer.id'),
70
+ };
71
+ }
72
+
73
+ function createKnowledgeDraft(input, options = {}) {
74
+ const createdAt = iso(input?.createdAt ?? (options.clock?.() ?? new Date()), 'createdAt');
75
+ const base = {
76
+ schemaVersion: KNOWLEDGE_V3_SCHEMA_VERSION,
77
+ knowledgeId: input?.knowledgeId ?? null,
78
+ scope: normalizeScope(input?.scope),
79
+ title: text(input?.title, 'title'),
80
+ statement: text(input?.statement, 'statement'),
81
+ status: 'draft',
82
+ authority: Math.max(0, Math.min(100, Number(input?.authority ?? 50))),
83
+ tags: [...new Set((input?.tags ?? []).map((item) => text(item, 'tag'))) ],
84
+ evidence: normalizeEvidence(input?.evidence ?? []),
85
+ reviewer: null,
86
+ validFrom: input?.validFrom ? iso(input.validFrom, 'validFrom') : createdAt,
87
+ validUntil: input?.validUntil ? iso(input.validUntil, 'validUntil') : null,
88
+ supersedes: input?.supersedes ? text(input.supersedes, 'supersedes') : null,
89
+ supersededBy: null,
90
+ createdAt,
91
+ updatedAt: createdAt,
92
+ metadata: structuredClone(input?.metadata ?? {}),
93
+ };
94
+ if (!Number.isFinite(base.authority)) throw new KnowledgeLifecycleError('authority 必须是 0 到 100 的数字。');
95
+ if (base.validUntil && Date.parse(base.validUntil) <= Date.parse(base.validFrom)) {
96
+ throw new KnowledgeLifecycleError('validUntil 必须晚于 validFrom。');
97
+ }
98
+ base.knowledgeId = base.knowledgeId ? text(base.knowledgeId, 'knowledgeId') : stableId({ ...base, knowledgeId: null });
99
+ return base;
100
+ }
101
+
102
+ function validateKnowledgeEntry(entry, options = {}) {
103
+ const errors = [];
104
+ const add = (code, message) => errors.push({ code, message });
105
+ if (entry?.schemaVersion !== KNOWLEDGE_V3_SCHEMA_VERSION) add('schema-version', 'schemaVersion 必须为 3。');
106
+ if (!KNOWLEDGE_STATUSES.includes(entry?.status)) add('status', '知识状态无效。');
107
+ try { normalizeScope(entry?.scope); } catch (error) { add('scope', error.message); }
108
+ for (const field of ['knowledgeId', 'title', 'statement', 'createdAt', 'updatedAt']) {
109
+ try {
110
+ if (field.endsWith('At')) iso(entry?.[field], field);
111
+ else text(entry?.[field], field);
112
+ } catch (error) { add(field, error.message); }
113
+ }
114
+ if (options.requireEvidence !== false && (!Array.isArray(entry?.evidence) || entry.evidence.length === 0)) {
115
+ add('evidence-required', '知识晋升需要至少一条可追溯证据。');
116
+ } else {
117
+ try { normalizeEvidence(entry?.evidence ?? []); } catch (error) { add('evidence', error.message); }
118
+ }
119
+ if (options.requireReviewer && !entry?.reviewer) add('reviewer-required', '知识晋升需要明确 reviewer。');
120
+ if (entry?.status === 'superseded' && !entry?.supersededBy) add('superseded-by-required', '已替代知识必须记录 supersededBy。');
121
+ if (entry?.validUntil && Date.parse(entry.validUntil) <= Date.parse(entry.validFrom)) add('validity-window', '有效期窗口无效。');
122
+ return { ok: errors.length === 0, errors };
123
+ }
124
+
125
+ function assertValid(entry, options) {
126
+ const validation = validateKnowledgeEntry(entry, options);
127
+ if (!validation.ok) throw new KnowledgeLifecycleError(validation.errors.map((item) => item.message).join(';'), { errors: validation.errors });
128
+ return entry;
129
+ }
130
+
131
+ function promoteKnowledgeEntry(entry, input, options = {}) {
132
+ if (entry?.status !== 'draft' && entry?.status !== 'deprecated') {
133
+ throw new KnowledgeLifecycleError('只有 draft 或 deprecated 条目可以晋升。');
134
+ }
135
+ const promotedAt = iso(input?.promotedAt ?? (options.clock?.() ?? new Date()), 'promotedAt');
136
+ const promoted = {
137
+ ...structuredClone(entry),
138
+ status: 'active',
139
+ reviewer: normalizeReviewer(input?.reviewer),
140
+ updatedAt: promotedAt,
141
+ promotion: {
142
+ approvalId: text(input?.approvalId, 'approvalId'),
143
+ promotedAt,
144
+ },
145
+ };
146
+ return assertValid(promoted, { requireEvidence: true, requireReviewer: true });
147
+ }
148
+
149
+ function deprecateKnowledgeEntry(entry, input = {}, options = {}) {
150
+ if (entry?.status !== 'active') throw new KnowledgeLifecycleError('只有 active 条目可以废弃。');
151
+ const deprecatedAt = iso(input?.deprecatedAt ?? (options.clock?.() ?? new Date()), 'deprecatedAt');
152
+ return {
153
+ ...structuredClone(entry),
154
+ status: 'deprecated',
155
+ updatedAt: deprecatedAt,
156
+ deprecation: {
157
+ reason: text(input?.reason, 'reason'),
158
+ approvalId: text(input?.approvalId, 'approvalId'),
159
+ deprecatedAt,
160
+ },
161
+ };
162
+ }
163
+
164
+ function supersedeKnowledgeEntry(previous, replacement, input = {}, options = {}) {
165
+ if (previous?.status !== 'active') throw new KnowledgeLifecycleError('只有 active 条目可以被替代。');
166
+ if (replacement?.status !== 'active') throw new KnowledgeLifecycleError('替代条目必须先完成晋升。');
167
+ if (previous.knowledgeId === replacement.knowledgeId) throw new KnowledgeLifecycleError('条目不能替代自身。');
168
+ if (previous.scope?.type !== replacement.scope?.type || previous.scope?.id !== replacement.scope?.id) {
169
+ throw new KnowledgeLifecycleError('替代链必须保持同一知识作用域。');
170
+ }
171
+ const at = iso(input?.supersededAt ?? (options.clock?.() ?? new Date()), 'supersededAt');
172
+ const approvalId = text(input?.approvalId, 'approvalId');
173
+ return {
174
+ previous: {
175
+ ...structuredClone(previous),
176
+ status: 'superseded',
177
+ supersededBy: replacement.knowledgeId,
178
+ updatedAt: at,
179
+ supersession: { approvalId, at },
180
+ },
181
+ replacement: {
182
+ ...structuredClone(replacement),
183
+ supersedes: previous.knowledgeId,
184
+ updatedAt: at,
185
+ supersession: { approvalId, at },
186
+ },
187
+ };
188
+ }
189
+
190
+ function tokenize(value) {
191
+ const normalized = String(value ?? '').toLowerCase();
192
+ return [...new Set([
193
+ ...(normalized.match(/[a-z0-9][a-z0-9._-]{1,}/g) ?? []),
194
+ ...(normalized.match(/[\u4e00-\u9fa5]{2,}/g) ?? []),
195
+ ])];
196
+ }
197
+
198
+ function estimateTokens(value) {
199
+ return Math.max(1, Math.ceil(Buffer.byteLength(JSON.stringify(value), 'utf8') / 4));
200
+ }
201
+
202
+ function queryKnowledge(entries, request = {}, options = {}) {
203
+ const now = options.clock?.() ?? new Date();
204
+ const nowMs = new Date(now).getTime();
205
+ if (Number.isNaN(nowMs)) throw new KnowledgeLifecycleError('clock 返回了无效时间。');
206
+ const scope = normalizeScope(request.scope);
207
+ const queryTokens = tokenize(request.query ?? '');
208
+ const limit = Math.max(1, Math.min(50, Number(request.limit ?? 8)));
209
+ const tokenBudget = Math.max(1, Number(request.tokenBudget ?? 400));
210
+ if (!Number.isSafeInteger(limit) || !Number.isSafeInteger(tokenBudget)) {
211
+ throw new KnowledgeLifecycleError('limit 与 tokenBudget 必须是正整数。');
212
+ }
213
+ const candidates = [];
214
+ const excluded = [];
215
+
216
+ for (const entry of entries ?? []) {
217
+ let reason = null;
218
+ if (entry?.schemaVersion !== KNOWLEDGE_V3_SCHEMA_VERSION) reason = 'schema-mismatch';
219
+ else if (entry.status !== 'active') reason = `status-${entry.status ?? 'invalid'}`;
220
+ else if (entry.scope?.type !== scope.type || entry.scope?.id !== scope.id) reason = 'scope-mismatch';
221
+ else if (Date.parse(entry.validFrom) > nowMs) reason = 'not-yet-valid';
222
+ else if (entry.validUntil && Date.parse(entry.validUntil) <= nowMs) reason = 'expired';
223
+ if (reason) {
224
+ excluded.push({ knowledgeId: entry?.knowledgeId ?? null, reason });
225
+ continue;
226
+ }
227
+ const haystack = tokenize([entry.title, entry.statement, ...(entry.tags ?? [])].join(' '));
228
+ const relevance = queryTokens.length === 0
229
+ ? 1
230
+ : queryTokens.filter((token) => haystack.some((candidate) => (
231
+ candidate === token || candidate.includes(token) || token.includes(candidate)
232
+ ))).length;
233
+ if (queryTokens.length > 0 && relevance === 0) {
234
+ excluded.push({ knowledgeId: entry.knowledgeId, reason: 'not-relevant' });
235
+ continue;
236
+ }
237
+ candidates.push({
238
+ entry,
239
+ relevance,
240
+ authority: Number(entry.authority ?? 0),
241
+ updatedAt: Date.parse(entry.updatedAt) || 0,
242
+ tokens: estimateTokens({ title: entry.title, statement: entry.statement, evidence: entry.evidence }),
243
+ });
244
+ }
245
+
246
+ candidates.sort((left, right) => (
247
+ right.relevance - left.relevance
248
+ || right.authority - left.authority
249
+ || right.updatedAt - left.updatedAt
250
+ || left.entry.knowledgeId.localeCompare(right.entry.knowledgeId)
251
+ ));
252
+
253
+ const included = [];
254
+ let usedTokens = 0;
255
+ for (const candidate of candidates) {
256
+ if (included.length >= limit) {
257
+ excluded.push({ knowledgeId: candidate.entry.knowledgeId, reason: 'result-limit' });
258
+ continue;
259
+ }
260
+ if (usedTokens + candidate.tokens > tokenBudget) {
261
+ excluded.push({ knowledgeId: candidate.entry.knowledgeId, reason: 'token-budget' });
262
+ continue;
263
+ }
264
+ included.push({ ...structuredClone(candidate.entry), retrieval: { reason: 'scope-relevance-authority', tokens: candidate.tokens } });
265
+ usedTokens += candidate.tokens;
266
+ }
267
+
268
+ return {
269
+ schemaVersion: KNOWLEDGE_V3_SCHEMA_VERSION,
270
+ scope,
271
+ query: String(request.query ?? ''),
272
+ tokenBudget,
273
+ usedTokens,
274
+ included,
275
+ excluded,
276
+ };
277
+ }
278
+
279
+ export {
280
+ KNOWLEDGE_SCOPE_TYPES,
281
+ KNOWLEDGE_STATUSES,
282
+ KNOWLEDGE_V3_SCHEMA_VERSION,
283
+ KnowledgeLifecycleError,
284
+ createKnowledgeDraft,
285
+ deprecateKnowledgeEntry,
286
+ promoteKnowledgeEntry,
287
+ queryKnowledge,
288
+ supersedeKnowledgeEntry,
289
+ validateKnowledgeEntry,
290
+ };
package/src/knowledge.js CHANGED
@@ -366,8 +366,10 @@ async function ensureKnowledgeWorkspace(projectRoot) {
366
366
  }
367
367
  }
368
368
 
369
- async function readKnowledgeIndex(projectRoot) {
370
- await ensureKnowledgeWorkspace(projectRoot);
369
+ async function readKnowledgeIndex(projectRoot, options = {}) {
370
+ if (options.ensure !== false) {
371
+ await ensureKnowledgeWorkspace(projectRoot);
372
+ }
371
373
  const current = await readJson(knowledgePath(projectRoot, KNOWLEDGE_INDEX)).catch(() => defaultKnowledgeIndex());
372
374
  return {
373
375
  ...defaultKnowledgeIndex(),
@@ -496,8 +498,8 @@ function serializeComparable(value) {
496
498
  return JSON.stringify(value);
497
499
  }
498
500
 
499
- async function hydrateKnowledgeSkills(projectRoot) {
500
- const index = await readKnowledgeIndex(projectRoot);
501
+ async function hydrateKnowledgeSkills(projectRoot, options = {}) {
502
+ const index = await readKnowledgeIndex(projectRoot, { ensure: options.ensure !== false });
501
503
  const cache = new Map();
502
504
  const hydratedSkills = [];
503
505
  let changed = false;
@@ -508,7 +510,7 @@ async function hydrateKnowledgeSkills(projectRoot) {
508
510
  changed = true;
509
511
  }
510
512
  }
511
- if (changed) {
513
+ if (changed && options.writeBack !== false) {
512
514
  await writeKnowledgeIndex(projectRoot, {
513
515
  ...index,
514
516
  skills: hydratedSkills,
@@ -1861,8 +1863,13 @@ function scoreKnowledgeSkillMatch(skill, query) {
1861
1863
  }
1862
1864
 
1863
1865
  export async function resolveKnowledgeSkillMatches(projectRoot, options = {}) {
1864
- await ensureKnowledgeWorkspace(projectRoot);
1865
- const { skills } = await hydrateKnowledgeSkills(projectRoot);
1866
+ if (!options.readOnly) {
1867
+ await ensureKnowledgeWorkspace(projectRoot);
1868
+ }
1869
+ const { skills } = await hydrateKnowledgeSkills(projectRoot, {
1870
+ ensure: !options.readOnly,
1871
+ writeBack: !options.readOnly,
1872
+ });
1866
1873
  const query = buildKnowledgeMatchQuery(options);
1867
1874
  if (!query.normalizedText) {
1868
1875
  return {
package/src/openprd.js CHANGED
@@ -68,6 +68,7 @@ import { designStarterWorkspace } from './design-starter.js';
68
68
  import { canvasWorkspace } from './canvas-workspace.js';
69
69
  import { buildReviewPresentationTemplatePayload, reviewPresentationWorkspace } from './review-presentation.js';
70
70
  import { analyzeWorkspaceRegistryHygiene } from './registry-hygiene.js';
71
+ import { ensureProjectManifest, readProjectManifest } from './kernel/index.js';
71
72
  import { syncSessionBindingFromChange } from './session-binding.js';
72
73
  import { readSessionRegistry } from './session-registry.js';
73
74
  import { visualCompareWorkspace } from './visual-compare.js';
@@ -75,6 +76,10 @@ import { visualPrepareWorkspace } from './visual-prepare.js';
75
76
  import { captureWorkspace, clarifyWorkspace, classifyWorkspace, computeWorkspaceGuidance, diffWorkspace, historyWorkspace, interviewWorkspace, nextWorkspace, playgroundWorkspace, reviewWorkspace, synthesizeWorkspace } from './workspace-workflow.js';
76
77
  import { appendDecision, appendProgress, appendVerification, appendWorkflowEvent, buildCurrentStateSnapshot, buildWorkflowTaskGraph, computeWorkspaceDigest, CORE_TEMPLATE_FILES, ensureWorkspaceSkeleton, isSupportedProductType, loadCurrentLaneSnapshot, loadLatestVersionSnapshot, loadWorkspace, migrateWorkspaceSkeleton, normalizeVersionId, persistWorkspaceCurrentState, readVersionIndex, resolveActiveTemplatePack, resolveCurrentProductType, validateWorkspace } from './workspace-core.js';
77
78
  import { readWorkspaceRegistry } from './workspace-registry.js';
79
+ import { taskRuntimeWorkspace } from './runtime/workspace.js';
80
+ import { printTaskRuntimeResult } from './cli/runtime-print.js';
81
+ import { gateWorkspace } from './gates/workspace.js';
82
+ import { printGateResult } from './cli/gate-print.js';
78
83
 
79
84
  function buildInitTemplatePackGuidance(templatePack, options = {}) {
80
85
  const label = formatTemplatePackDisplay(templatePack, { fallback: '通用产品或工程场景' });
@@ -94,6 +99,7 @@ function buildInitTemplatePackGuidance(templatePack, options = {}) {
94
99
 
95
100
  async function initWorkspace(projectRoot, options) {
96
101
  const ws = await ensureWorkspaceSkeleton(projectRoot, options);
102
+ const projectManifest = await ensureProjectManifest(projectRoot);
97
103
  const workspace = await loadWorkspace(projectRoot);
98
104
  const standards = await initStandardsWorkspace(projectRoot, { force: Boolean(options.force) });
99
105
  const quality = await initQualityWorkspace(projectRoot, { force: Boolean(options.force) });
@@ -108,6 +114,8 @@ async function initWorkspace(projectRoot, options) {
108
114
  openprdHome: options.openprdHome,
109
115
  platform: options.platform,
110
116
  hookProfile: options.hookProfile,
117
+ symlinkStrategy: options.symlinkStrategy,
118
+ installSafetyBackupRoot: options.installSafetyBackupRoot,
111
119
  });
112
120
  const config = workspace.data.config ?? {};
113
121
  if (options.templatePack) {
@@ -158,7 +166,7 @@ async function initWorkspace(projectRoot, options) {
158
166
  templatePackGuidance.message,
159
167
  ]);
160
168
 
161
- return { ws: workspace, created: ws.created, currentState, templatePackGuidance, standards, quality, growth, agentIntegration };
169
+ return { ws: workspace, created: ws.created, currentState, templatePackGuidance, standards, quality, growth, agentIntegration, projectManifest };
162
170
  }
163
171
 
164
172
  async function setupAgentIntegrationWorkspace(projectRoot, options = {}) {
@@ -175,10 +183,12 @@ async function setupAgentIntegrationWorkspace(projectRoot, options = {}) {
175
183
  templatePack: initResult.currentState.templatePack,
176
184
  },
177
185
  growth: initResult.growth,
186
+ projectManifest: initResult.projectManifest,
178
187
  };
179
188
  }
180
189
 
181
190
  const migration = await migrateWorkspaceSkeleton(projectRoot, { recordEvent: true });
191
+ const projectManifest = await ensureProjectManifest(projectRoot);
182
192
  const standards = await initStandardsWorkspace(projectRoot, { force: Boolean(options.force) });
183
193
  const quality = await initQualityWorkspace(projectRoot, { force: false });
184
194
  const growth = await initGrowthWorkspace(projectRoot);
@@ -192,6 +202,8 @@ async function setupAgentIntegrationWorkspace(projectRoot, options = {}) {
192
202
  openprdHome: options.openprdHome,
193
203
  platform: options.platform,
194
204
  hookProfile: options.hookProfile,
205
+ symlinkStrategy: options.symlinkStrategy,
206
+ installSafetyBackupRoot: options.installSafetyBackupRoot,
195
207
  });
196
208
  return {
197
209
  ...agentIntegration,
@@ -201,12 +213,14 @@ async function setupAgentIntegrationWorkspace(projectRoot, options = {}) {
201
213
  standards,
202
214
  quality,
203
215
  growth,
216
+ projectManifest,
204
217
  errors: [...(agentIntegration.errors ?? []), ...(migration.errors ?? [])],
205
218
  };
206
219
  }
207
220
 
208
221
  async function updateAgentIntegrationWorkspace(projectRoot, options = {}) {
209
222
  const migration = await migrateWorkspaceSkeleton(projectRoot, { recordEvent: true });
223
+ const projectManifest = await ensureProjectManifest(projectRoot);
210
224
  const standards = await initStandardsWorkspace(projectRoot, { force: false });
211
225
  const quality = await initQualityWorkspace(projectRoot, { force: false });
212
226
  const growth = await initGrowthWorkspace(projectRoot);
@@ -219,6 +233,8 @@ async function updateAgentIntegrationWorkspace(projectRoot, options = {}) {
219
233
  openprdHome: options.openprdHome,
220
234
  platform: options.platform,
221
235
  hookProfile: options.hookProfile,
236
+ symlinkStrategy: options.symlinkStrategy,
237
+ installSafetyBackupRoot: options.installSafetyBackupRoot,
222
238
  });
223
239
  return {
224
240
  ...agentIntegration,
@@ -227,11 +243,20 @@ async function updateAgentIntegrationWorkspace(projectRoot, options = {}) {
227
243
  standards,
228
244
  quality,
229
245
  growth,
246
+ projectManifest,
230
247
  errors: [...(agentIntegration.errors ?? []), ...(migration.errors ?? [])],
231
248
  };
232
249
  }
233
250
 
234
251
  async function doctorWorkspace(projectRoot, options = {}) {
252
+ const projectManifest = await readProjectManifest(projectRoot, { mode: 'read' })
253
+ .then((manifest) => ({ ok: true, manifest }))
254
+ .catch((error) => ({
255
+ ok: false,
256
+ manifest: null,
257
+ code: error?.code ?? 'OPENPRD_MANIFEST_READ_FAILED',
258
+ error: error instanceof Error ? error.message : String(error),
259
+ }));
235
260
  const agentIntegration = await doctorOpenPrdAgentIntegration(projectRoot, {
236
261
  tools: options.tools ?? 'all',
237
262
  enableUserCodexConfig: Boolean(options.enableUserCodexConfig),
@@ -278,7 +303,7 @@ async function doctorWorkspace(projectRoot, options = {}) {
278
303
  ...((workspaceRegistry?.staleEntries ?? []).map((entry) => `registry: stale workspace ${entry.workspaceRoot} (${entry.reason})`)),
279
304
  ...((sessionRegistry?.staleEntries ?? []).map((entry) => `session-registry: stale session ${entry.sessionId} (${entry.reason})`)),
280
305
  ];
281
- const doctorOk = agentIntegration.ok && (codexRuntime?.ok ?? true) && standards.ok && validation.valid;
306
+ const doctorOk = projectManifest.ok && agentIntegration.ok && (codexRuntime?.ok ?? true) && standards.ok && validation.valid;
282
307
  const latestQuality = await readJson(cjoin(projectRoot, '.openprd', 'quality', 'reports', 'latest.json')).catch(() => null);
283
308
  const doctorSignal = doctorOk
284
309
  ? {
@@ -332,6 +357,7 @@ async function doctorWorkspace(projectRoot, options = {}) {
332
357
  projectRoot,
333
358
  tools: agentIntegration.tools,
334
359
  agentIntegration,
360
+ projectManifest,
335
361
  codexRuntime,
336
362
  standards,
337
363
  validation,
@@ -345,6 +371,7 @@ async function doctorWorkspace(projectRoot, options = {}) {
345
371
  knowledgeReview,
346
372
  errors: [
347
373
  ...agentIntegration.errors,
374
+ ...(!projectManifest.ok ? [`project-manifest: ${projectManifest.error}`] : []),
348
375
  ...(codexRuntime?.errors ?? []).map((error) => `codex-runtime: ${error}`),
349
376
  ...(standards.errors ?? []).map((error) => `standards: ${error}`),
350
377
  ...(validation.errors ?? []).map((error) => `validate: ${error}`),
@@ -796,6 +823,8 @@ export async function main(argv = process.argv.slice(2)) {
796
823
  templatePack: flags.templatePack,
797
824
  tools: flags.tools,
798
825
  hookProfile: flags.hookProfile,
826
+ symlinkStrategy: flags.symlinkStrategy,
827
+ installSafetyBackupRoot: flags.installSafetyBackupRoot,
799
828
  enableUserCodexConfig: true,
800
829
  });
801
830
  printInitResult(result, flags.json);
@@ -808,6 +837,8 @@ export async function main(argv = process.argv.slice(2)) {
808
837
  templatePack: flags.templatePack,
809
838
  tools: flags.tools,
810
839
  hookProfile: flags.hookProfile,
840
+ symlinkStrategy: flags.symlinkStrategy,
841
+ installSafetyBackupRoot: flags.installSafetyBackupRoot,
811
842
  enableUserCodexConfig: true,
812
843
  });
813
844
  printAgentIntegrationResult(result, flags.json);
@@ -819,6 +850,8 @@ export async function main(argv = process.argv.slice(2)) {
819
850
  force: flags.force,
820
851
  tools: flags.tools,
821
852
  hookProfile: flags.hookProfile,
853
+ symlinkStrategy: flags.symlinkStrategy,
854
+ installSafetyBackupRoot: flags.installSafetyBackupRoot,
822
855
  enableUserCodexConfig: true,
823
856
  });
824
857
  printAgentIntegrationResult(result, flags.json);
@@ -898,6 +931,42 @@ export async function main(argv = process.argv.slice(2)) {
898
931
  return result.ok ? 0 : 1;
899
932
  }
900
933
 
934
+ if (command === 'runtime') {
935
+ const result = await taskRuntimeWorkspace(projectPath, {
936
+ action: flags.runtimeAction ?? 'status',
937
+ lane: flags.lane,
938
+ task: flags.task,
939
+ title: flags.title,
940
+ claimId: flags.claimId,
941
+ client: flags.client ?? flags.agent,
942
+ session: flags.session,
943
+ thread: flags.thread,
944
+ worktree: flags.worktree,
945
+ branch: flags.branch,
946
+ baseCommit: flags.baseCommit,
947
+ leaseDurationMs: flags.leaseMs,
948
+ expectedRevision: flags.expectedRevision,
949
+ expectedLaneRevision: flags.expectedLaneRevision,
950
+ writeSet: flags.writeSet,
951
+ evidence: flags.evidence,
952
+ reason: flags.reason,
953
+ status: flags.status,
954
+ });
955
+ printTaskRuntimeResult(result, flags.json);
956
+ return result.ok ? 0 : 1;
957
+ }
958
+
959
+ if (command === 'gate') {
960
+ const result = await gateWorkspace(projectPath, {
961
+ action: flags.runtimeAction ?? 'status',
962
+ jsonFile: flags.jsonFile,
963
+ gateId: flags.gateId,
964
+ approvalId: flags.approvalId,
965
+ });
966
+ printGateResult(result, flags.json);
967
+ return result.ok ? 0 : 1;
968
+ }
969
+
901
970
  if (command === 'loop') {
902
971
  let result;
903
972
  const options = {