@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.
- package/.openprd/changes/openprd-control-plane-v020/.openprd.yaml +2 -0
- package/.openprd/changes/openprd-control-plane-v020/design.md +78 -0
- package/.openprd/changes/openprd-control-plane-v020/proposal.md +54 -0
- package/.openprd/changes/openprd-control-plane-v020/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/openprd-control-plane-v020/task-events.jsonl +27 -0
- package/.openprd/changes/openprd-control-plane-v020/tasks-002.md +35 -0
- package/.openprd/changes/openprd-control-plane-v020/tasks.md +427 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/.openprd.yaml +2 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/design.md +52 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/proposal.md +35 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/task-events.jsonl +1 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/tasks.md +170 -0
- package/.openprd/design/active/asset-spec.md +19 -14
- package/.openprd/design/active/direction-plan.md +19 -3
- package/.openprd/design/active/facts-sheet.md +16 -7
- package/.openprd/design/active/image-preflight.md +6 -5
- package/.openprd/design/active/review-studio-v020-directions/compare-plan.json +34 -0
- package/.openprd/design/active/review-studio-v020-directions/contact-sheet.jpg +0 -0
- package/.openprd/design/active/review-studio-v020-directions/crops/01.png +0 -0
- package/.openprd/design/active/review-studio-v020-directions/crops/02.png +0 -0
- package/.openprd/design/active/review-studio-v020-directions/crops/03.png +0 -0
- package/.openprd/design/active/review-studio-v020-directions/focus-board.template.json +69 -0
- package/.openprd/design/active/review-studio-v020-directions/parallel-board.template.json +45 -0
- package/.openprd/design/active/review-studio-v020-directions/reference-set.json +143 -0
- package/.openprd/design/active/review-studio-v020-directions/source.png +0 -0
- package/.openprd/design/active/selected-direction.md +23 -9
- package/.openprd/engagements/active/control-plane-architecture.json +203 -0
- package/.openprd/engagements/active/control-plane-intake.json +418 -0
- package/.openprd/engagements/active/prd.md +183 -119
- package/.openprd/engagements/active/review-presentation-v0018.json +176 -0
- package/.openprd/i18n-config.json +12 -0
- package/.openprd/ledger/events.jsonl +6 -0
- package/.openprd/ledger/heads/ebea1a71a9daa566f1c91b53.json +10 -0
- package/.openprd/manifest.json +21 -0
- package/AGENTS.md +7 -6
- package/README.md +31 -29
- package/README_EN.md +36 -39
- package/package.json +1 -1
- package/skills/openprd-frontend-design/SKILL.md +16 -0
- package/skills/openprd-harness/SKILL.md +7 -7
- package/skills/openprd-quality/SKILL.md +1 -1
- package/skills/openprd-requirement-intake/SKILL.md +1 -1
- package/skills/openprd-shared/SKILL.md +6 -6
- package/src/adapters/adapter-spi.js +193 -0
- package/src/adapters/capability-envelope.js +98 -0
- package/src/adapters/event-normalizer.js +55 -0
- package/src/adapters/index.js +4 -0
- package/src/adapters/install-safety.js +249 -0
- package/src/agent-canonical-content.js +4 -2
- package/src/agent-integration.js +169 -46
- package/src/cli/args.js +63 -4
- package/src/cli/gate-print.js +17 -0
- package/src/cli/quality-commands.js +18 -0
- package/src/cli/quality-print.js +10 -0
- package/src/cli/runtime-print.js +24 -0
- package/src/codex-hook-runner-template.mjs +129 -199
- package/src/codex-runtime.js +48 -5
- package/src/context/cache.js +245 -0
- package/src/context/compiler.js +438 -0
- package/src/context/constants.js +30 -0
- package/src/context/index.js +39 -0
- package/src/context/redaction.js +84 -0
- package/src/context/stable.js +69 -0
- package/src/context/telemetry.js +42 -0
- package/src/dev-standards.js +57 -0
- package/src/fleet.js +112 -95
- package/src/gates/index.js +2 -0
- package/src/gates/scoped-gates.js +256 -0
- package/src/gates/store.js +126 -0
- package/src/gates/workspace.js +41 -0
- package/src/html-artifacts.js +725 -28
- package/src/kernel/atomic-store.js +299 -0
- package/src/kernel/event-envelope.js +166 -0
- package/src/kernel/index.js +4 -0
- package/src/kernel/project-ledger.js +467 -0
- package/src/kernel/project-manifest.js +205 -0
- package/src/knowledge-v3/index.js +1 -0
- package/src/knowledge-v3/lifecycle.js +290 -0
- package/src/knowledge.js +14 -7
- package/src/openprd.js +71 -2
- package/src/review-model.js +413 -0
- package/src/review-presentation.js +1 -1
- package/src/run-harness.js +432 -38
- package/src/runtime/cli_runtime_README.md +28 -0
- package/src/runtime/errors.js +66 -0
- package/src/runtime/index.js +44 -0
- package/src/runtime/lane-schema.js +141 -0
- package/src/runtime/lane-store.js +279 -0
- package/src/runtime/task-runtime.js +449 -0
- package/src/runtime/workspace.js +179 -0
- package/src/runtime/write-set.js +206 -0
- package/src/session-binding.js +16 -3
- package/src/session-registry.js +59 -1
- package/src/upgrade/fleet-mutation.js +166 -0
- package/src/upgrade/fleet-transaction.js +398 -0
- package/src/upgrade/transaction-store.js +416 -0
- package/src/visual-compare-core.js +66 -27
- package/src/visual-compare.js +18 -12
- package/src/workspace-core.js +109 -7
- package/src/workspace-registry.js +39 -1
- 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
|
-
|
|
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
|
-
|
|
1865
|
-
|
|
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 = {
|