@openprd/cli 0.1.18 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/openprd-generated-change/.openprd.yaml +2 -0
- package/.openprd/changes/openprd-generated-change/design.md +50 -0
- package/.openprd/changes/openprd-generated-change/proposal.md +37 -0
- package/.openprd/changes/openprd-generated-change/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/openprd-generated-change/task-events.jsonl +21 -0
- package/.openprd/changes/openprd-generated-change/tasks.md +357 -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 +186 -100
- 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 +1 -0
- package/README.md +22 -0
- package/README_EN.md +27 -0
- package/package.json +1 -1
- package/skills/openprd-frontend-design/SKILL.md +16 -0
- package/skills/openprd-harness/SKILL.md +1 -1
- package/skills/openprd-quality/SKILL.md +1 -1
- package/skills/openprd-requirement-intake/SKILL.md +1 -1
- package/skills/openprd-shared/SKILL.md +1 -1
- 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 +6 -2
- package/src/agent-integration.js +147 -42
- package/src/canvas-app-client-app.js +838 -0
- package/src/canvas-app-client-helpers.js +1138 -0
- package/src/canvas-app-styles.js +898 -0
- package/src/canvas-app.html.js +6 -2781
- package/src/canvas-binding.js +414 -0
- package/src/canvas-codex-relay.js +430 -0
- package/src/canvas-i18n.js +8 -0
- package/src/canvas-scene.js +377 -0
- package/src/canvas-session-store.js +148 -0
- package/src/canvas-workspace.js +59 -1066
- 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 +87 -77
- 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 +431 -37
- 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,206 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ## 核心功能
|
|
3
|
+
* 规范化任务写入范围,并以安全优先的方式判断两个 write-set 是否可能相交。
|
|
4
|
+
*
|
|
5
|
+
* ## 输入
|
|
6
|
+
* 接收 include/exclude 路径或 glob;数组简写等价于 include,空范围按全项目处理。
|
|
7
|
+
*
|
|
8
|
+
* ## 输出
|
|
9
|
+
* 导出 normalizeWriteSet、writeSetsIntersect 与包含冲突路径的 explainWriteSetIntersection。
|
|
10
|
+
*
|
|
11
|
+
* ## 定位
|
|
12
|
+
* 位于 runtime 并发控制层;只计算路径集合,不读取文件系统或修改 claim。
|
|
13
|
+
*
|
|
14
|
+
* ## 依赖
|
|
15
|
+
* 仅使用 Node.js path 模块完成 POSIX 路径标准化。
|
|
16
|
+
*
|
|
17
|
+
* ## 维护规则
|
|
18
|
+
* 判定必须避免漏报写冲突;不能证明互斥的复杂 glob 应保守返回相交。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
|
|
23
|
+
const BROAD_PATTERN = '**/*';
|
|
24
|
+
const GLOB_TOKEN = /[*?[]/;
|
|
25
|
+
|
|
26
|
+
function uniqueSorted(values) {
|
|
27
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function listFrom(value) {
|
|
31
|
+
if (value === null || value === undefined) return [];
|
|
32
|
+
return Array.isArray(value) ? value : [value];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function normalizeWritePattern(value) {
|
|
36
|
+
const original = String(value ?? '').trim();
|
|
37
|
+
if (!original) return null;
|
|
38
|
+
|
|
39
|
+
const slashNormalized = original.replaceAll('\\', '/');
|
|
40
|
+
if (slashNormalized.includes('\0')) {
|
|
41
|
+
throw new TypeError('OpenPrd write-set patterns cannot contain null bytes.');
|
|
42
|
+
}
|
|
43
|
+
if (path.posix.isAbsolute(slashNormalized) || /^[A-Za-z]:\//.test(slashNormalized)) {
|
|
44
|
+
throw new TypeError(`OpenPrd write-set patterns must be project-relative: ${original}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const hadTrailingSlash = slashNormalized.endsWith('/');
|
|
48
|
+
let normalized = slashNormalized.replace(/^(?:\.\/)+/, '');
|
|
49
|
+
normalized = path.posix.normalize(normalized);
|
|
50
|
+
if (normalized === '.' || normalized === '') return BROAD_PATTERN;
|
|
51
|
+
if (normalized === '..' || normalized.startsWith('../')) {
|
|
52
|
+
throw new TypeError(`OpenPrd write-set patterns cannot escape the project root: ${original}`);
|
|
53
|
+
}
|
|
54
|
+
if (hadTrailingSlash) normalized = `${normalized.replace(/\/+$/, '')}/**`;
|
|
55
|
+
if (normalized === '**') return BROAD_PATTERN;
|
|
56
|
+
return normalized;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function normalizeWriteSet(value = null) {
|
|
60
|
+
const source = Array.isArray(value) || typeof value === 'string'
|
|
61
|
+
? { include: value }
|
|
62
|
+
: (value ?? {});
|
|
63
|
+
const includeSource = source.include ?? source.includes ?? source.paths;
|
|
64
|
+
const excludeSource = source.exclude ?? source.excludes;
|
|
65
|
+
const include = uniqueSorted(listFrom(includeSource).map(normalizeWritePattern).filter(Boolean));
|
|
66
|
+
const exclude = uniqueSorted(listFrom(excludeSource).map(normalizeWritePattern).filter(Boolean));
|
|
67
|
+
return {
|
|
68
|
+
include: include.length > 0 ? include : [BROAD_PATTERN],
|
|
69
|
+
exclude,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hasGlob(pattern) {
|
|
74
|
+
return GLOB_TOKEN.test(pattern);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function globRegExp(pattern) {
|
|
78
|
+
let source = '^';
|
|
79
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
80
|
+
const character = pattern[index];
|
|
81
|
+
const next = pattern[index + 1];
|
|
82
|
+
if (character === '*' && next === '*') {
|
|
83
|
+
const after = pattern[index + 2];
|
|
84
|
+
if (after === '/') {
|
|
85
|
+
source += '(?:.*/)?';
|
|
86
|
+
index += 2;
|
|
87
|
+
} else {
|
|
88
|
+
source += '.*';
|
|
89
|
+
index += 1;
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (character === '*') {
|
|
94
|
+
source += '[^/]*';
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (character === '?') {
|
|
98
|
+
source += '[^/]';
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if ('\\^$+?.()|{}[]'.includes(character)) source += `\\${character}`;
|
|
102
|
+
else source += character;
|
|
103
|
+
}
|
|
104
|
+
source += '$';
|
|
105
|
+
return new RegExp(source);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function patternMatches(pattern, candidate) {
|
|
109
|
+
return globRegExp(pattern).test(candidate);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function staticRoot(pattern) {
|
|
113
|
+
const wildcardIndex = pattern.search(GLOB_TOKEN);
|
|
114
|
+
const raw = wildcardIndex === -1 ? pattern : pattern.slice(0, wildcardIndex);
|
|
115
|
+
return raw.replace(/\/+$/, '');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function pathWithin(candidate, parent) {
|
|
119
|
+
if (!parent) return true;
|
|
120
|
+
return candidate === parent || candidate.startsWith(`${parent}/`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function prefixGlobRoot(pattern) {
|
|
124
|
+
if (pattern === BROAD_PATTERN) return '';
|
|
125
|
+
if (pattern.endsWith('/**/*')) return pattern.slice(0, -5).replace(/\/+$/, '');
|
|
126
|
+
if (pattern.endsWith('/**')) return pattern.slice(0, -3).replace(/\/+$/, '');
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function patternContainedBy(candidate, cover) {
|
|
131
|
+
if (candidate === cover || cover === BROAD_PATTERN) return true;
|
|
132
|
+
if (!hasGlob(candidate)) return patternMatches(cover, candidate);
|
|
133
|
+
|
|
134
|
+
const coverRoot = prefixGlobRoot(cover);
|
|
135
|
+
if (coverRoot !== null) {
|
|
136
|
+
const candidateRoot = staticRoot(candidate);
|
|
137
|
+
return pathWithin(candidateRoot, coverRoot);
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function patternsMayIntersect(left, right) {
|
|
143
|
+
if (left === right || left === BROAD_PATTERN || right === BROAD_PATTERN) return true;
|
|
144
|
+
if (!hasGlob(left)) {
|
|
145
|
+
if (!right.includes('[')) return patternMatches(right, left);
|
|
146
|
+
return pathWithin(staticRoot(left), staticRoot(right)) || pathWithin(staticRoot(right), staticRoot(left));
|
|
147
|
+
}
|
|
148
|
+
if (!hasGlob(right)) {
|
|
149
|
+
if (!left.includes('[')) return patternMatches(left, right);
|
|
150
|
+
return pathWithin(staticRoot(left), staticRoot(right)) || pathWithin(staticRoot(right), staticRoot(left));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const leftRoot = staticRoot(left);
|
|
154
|
+
const rightRoot = staticRoot(right);
|
|
155
|
+
if (!leftRoot || !rightRoot) return true;
|
|
156
|
+
return pathWithin(leftRoot, rightRoot) || pathWithin(rightRoot, leftRoot);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function overlapPattern(left, right) {
|
|
160
|
+
if (patternContainedBy(left, right)) return left;
|
|
161
|
+
if (patternContainedBy(right, left)) return right;
|
|
162
|
+
if (!hasGlob(left) && patternMatches(right, left)) return left;
|
|
163
|
+
if (!hasGlob(right) && patternMatches(left, right)) return right;
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function overlapIsExcluded(pattern, writeSet) {
|
|
168
|
+
if (!pattern) return false;
|
|
169
|
+
return writeSet.exclude.some((excluded) => patternContainedBy(pattern, excluded));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function explainWriteSetIntersection(leftValue, rightValue) {
|
|
173
|
+
const left = normalizeWriteSet(leftValue);
|
|
174
|
+
const right = normalizeWriteSet(rightValue);
|
|
175
|
+
for (const leftPattern of left.include) {
|
|
176
|
+
for (const rightPattern of right.include) {
|
|
177
|
+
if (!patternsMayIntersect(leftPattern, rightPattern)) continue;
|
|
178
|
+
const overlap = overlapPattern(leftPattern, rightPattern);
|
|
179
|
+
if (overlapIsExcluded(overlap, left) || overlapIsExcluded(overlap, right)) continue;
|
|
180
|
+
return {
|
|
181
|
+
intersects: true,
|
|
182
|
+
left,
|
|
183
|
+
right,
|
|
184
|
+
leftPattern,
|
|
185
|
+
rightPattern,
|
|
186
|
+
overlapPattern: overlap,
|
|
187
|
+
conservative: overlap === null,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
intersects: false,
|
|
193
|
+
left,
|
|
194
|
+
right,
|
|
195
|
+
leftPattern: null,
|
|
196
|
+
rightPattern: null,
|
|
197
|
+
overlapPattern: null,
|
|
198
|
+
conservative: false,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function writeSetsIntersect(left, right) {
|
|
203
|
+
return explainWriteSetIntersection(left, right).intersects;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export { BROAD_PATTERN };
|
package/src/session-binding.js
CHANGED
|
@@ -8,6 +8,16 @@ const OPENPRD_HARNESS_REQUIREMENT_GATE = path.join(OPENPRD_HARNESS_DIR, 'require
|
|
|
8
8
|
const OPENPRD_HARNESS_SESSION_BINDINGS_DIR = path.join(OPENPRD_HARNESS_DIR, 'session-bindings');
|
|
9
9
|
const OPENPRD_STATE_CURRENT = path.join('.openprd', 'state', 'current.json');
|
|
10
10
|
|
|
11
|
+
let sessionBindingWriteEnabled = true;
|
|
12
|
+
|
|
13
|
+
function setSessionBindingWriteEnabled(enabled) {
|
|
14
|
+
sessionBindingWriteEnabled = enabled;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isSessionBindingWriteEnabled() {
|
|
18
|
+
return sessionBindingWriteEnabled;
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
function normalizeSessionId(sessionId) {
|
|
12
22
|
const text = String(sessionId ?? '').trim();
|
|
13
23
|
return text || null;
|
|
@@ -57,6 +67,9 @@ async function upsertSessionBinding(projectRoot, sessionId, patch = {}) {
|
|
|
57
67
|
if (!normalized) {
|
|
58
68
|
return null;
|
|
59
69
|
}
|
|
70
|
+
if (!sessionBindingWriteEnabled) {
|
|
71
|
+
return { sessionId: normalized, path: null, skipped: true, reason: 'session-binding-write-disabled' };
|
|
72
|
+
}
|
|
60
73
|
const filePath = sessionBindingPath(projectRoot, normalized);
|
|
61
74
|
const previous = await readJson(filePath).catch(() => null);
|
|
62
75
|
const next = {
|
|
@@ -84,9 +97,7 @@ async function upsertSessionBinding(projectRoot, sessionId, patch = {}) {
|
|
|
84
97
|
gateStatus: patch.gateStatus ?? next.gateStatus ?? null,
|
|
85
98
|
gateActive: patch.gateActive ?? next.gateActive ?? false,
|
|
86
99
|
bindingPath: filePath,
|
|
87
|
-
statePath: normalized
|
|
88
|
-
? path.join(projectRoot, '.openprd', 'harness', 'session-states', `${normalized.replace(/[^A-Za-z0-9._-]/g, '_')}.json`)
|
|
89
|
-
: null,
|
|
100
|
+
statePath: path.join(projectRoot, '.openprd', 'harness', 'session-states', `${normalized.replace(/[^A-Za-z0-9._-]/g, '_')}.json`),
|
|
90
101
|
updatedAt: next.updatedAt,
|
|
91
102
|
});
|
|
92
103
|
return {
|
|
@@ -167,9 +178,11 @@ async function syncSessionBindingFromChange(projectRoot, changeId, options = {})
|
|
|
167
178
|
}
|
|
168
179
|
|
|
169
180
|
export {
|
|
181
|
+
isSessionBindingWriteEnabled,
|
|
170
182
|
readLegacyRequirementGate,
|
|
171
183
|
readSessionBinding,
|
|
172
184
|
sessionBindingPath,
|
|
185
|
+
setSessionBindingWriteEnabled,
|
|
173
186
|
syncSessionBindingFromChange,
|
|
174
187
|
syncSessionBindingFromReview,
|
|
175
188
|
syncSessionBindingFromSnapshot,
|
package/src/session-registry.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { appendJsonl, cjoin, exists, readJsonl } from './fs-utils.js';
|
|
4
|
+
import { atomicWriteText, withFileLock } from './kernel/atomic-store.js';
|
|
4
5
|
import { timestamp } from './time.js';
|
|
5
6
|
import { resolveOpenPrdHome } from './workspace-registry.js';
|
|
6
7
|
|
|
7
8
|
const OPENPRD_SESSION_REGISTRY = cjoin('registry', 'sessions.jsonl');
|
|
8
9
|
|
|
10
|
+
let registryWriteEnabled = true;
|
|
11
|
+
|
|
12
|
+
function setRegistryWriteEnabled(enabled) {
|
|
13
|
+
registryWriteEnabled = enabled;
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
function normalizeSessionId(sessionId) {
|
|
10
17
|
const text = String(sessionId ?? '').trim();
|
|
11
18
|
return text || null;
|
|
@@ -84,6 +91,20 @@ async function readSessionRegistry(options = {}) {
|
|
|
84
91
|
...entry,
|
|
85
92
|
reason: 'missing-openprd-marker',
|
|
86
93
|
});
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (entry.bindingPath && !(await exists(entry.bindingPath))) {
|
|
97
|
+
staleEntries.push({
|
|
98
|
+
...entry,
|
|
99
|
+
reason: 'missing-binding',
|
|
100
|
+
});
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (entry.statePath && !(await exists(entry.statePath))) {
|
|
104
|
+
staleEntries.push({
|
|
105
|
+
...entry,
|
|
106
|
+
reason: 'missing-state',
|
|
107
|
+
});
|
|
87
108
|
}
|
|
88
109
|
}
|
|
89
110
|
return {
|
|
@@ -109,6 +130,9 @@ async function upsertSessionRegistryEntry(projectRoot, sessionId, patch = {}, op
|
|
|
109
130
|
if (!normalized) {
|
|
110
131
|
return null;
|
|
111
132
|
}
|
|
133
|
+
if (!registryWriteEnabled) {
|
|
134
|
+
return { home: null, registryPath: null, entry: { sessionId: normalized }, status: 'skipped', reason: 'registry-write-disabled' };
|
|
135
|
+
}
|
|
112
136
|
const registry = await readSessionRegistry(options);
|
|
113
137
|
const workspaceRoot = path.resolve(projectRoot);
|
|
114
138
|
const realpath = await fs.realpath(workspaceRoot).catch(() => workspaceRoot);
|
|
@@ -140,7 +164,7 @@ async function upsertSessionRegistryEntry(projectRoot, sessionId, patch = {}, op
|
|
|
140
164
|
lastUpdatedAt: patch.updatedAt ?? recordedAt,
|
|
141
165
|
recordedAt,
|
|
142
166
|
});
|
|
143
|
-
await appendJsonl(registry.registryPath, entry);
|
|
167
|
+
await withFileLock(`${registry.registryPath}.lock`, () => appendJsonl(registry.registryPath, entry));
|
|
144
168
|
return {
|
|
145
169
|
home: registry.home,
|
|
146
170
|
registryPath: registry.registryPath,
|
|
@@ -150,10 +174,44 @@ async function upsertSessionRegistryEntry(projectRoot, sessionId, patch = {}, op
|
|
|
150
174
|
};
|
|
151
175
|
}
|
|
152
176
|
|
|
177
|
+
function registryBackupName() {
|
|
178
|
+
return `sessions-${new Date().toISOString().replace(/[:.]/g, '-')}.jsonl`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function compactSessionRegistry(options = {}) {
|
|
182
|
+
const registryPath = sessionRegistryFilePath(options);
|
|
183
|
+
return withFileLock(`${registryPath}.lock`, async () => {
|
|
184
|
+
const registry = await readSessionRegistry(options);
|
|
185
|
+
const staleIds = new Set((registry.staleEntries ?? []).map((entry) => entry.sessionId));
|
|
186
|
+
const entries = options.pruneStale === false
|
|
187
|
+
? registry.entries
|
|
188
|
+
: registry.entries.filter((entry) => !staleIds.has(entry.sessionId));
|
|
189
|
+
const backupDir = path.resolve(options.backupDir ?? cjoin(registry.home, 'registry', 'backups'));
|
|
190
|
+
await fs.mkdir(backupDir, { recursive: true });
|
|
191
|
+
const backupPath = cjoin(backupDir, registryBackupName());
|
|
192
|
+
if (await exists(registryPath)) await fs.copyFile(registryPath, backupPath);
|
|
193
|
+
else await atomicWriteText(backupPath, '');
|
|
194
|
+
const body = entries.length > 0 ? `${entries.map((entry) => JSON.stringify(entry)).join('\n')}\n` : '';
|
|
195
|
+
await atomicWriteText(registryPath, body);
|
|
196
|
+
return {
|
|
197
|
+
ok: true,
|
|
198
|
+
registryPath,
|
|
199
|
+
backupPath,
|
|
200
|
+
beforeEvents: registry.totalEvents,
|
|
201
|
+
afterEvents: entries.length,
|
|
202
|
+
kept: entries.length,
|
|
203
|
+
pruned: registry.entries.length - entries.length,
|
|
204
|
+
stale: registry.staleEntries.length,
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
153
209
|
export {
|
|
154
210
|
normalizeSessionId,
|
|
211
|
+
compactSessionRegistry,
|
|
155
212
|
readSessionRegistry,
|
|
156
213
|
readSessionRegistryEntry,
|
|
157
214
|
sessionRegistryFilePath,
|
|
215
|
+
setRegistryWriteEnabled,
|
|
158
216
|
upsertSessionRegistryEntry,
|
|
159
217
|
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 核心功能
|
|
3
|
+
* 在 fleet 事务 staging 工作区中编排 update/setup/backfill,并把成功结果映射回兼容的 fleet 项目报告。
|
|
4
|
+
*
|
|
5
|
+
* 输入
|
|
6
|
+
* 接收 plannedAction、staging 项目路径、fleet options、workspace dependencies 与隔离 OPENPRD_HOME。
|
|
7
|
+
*
|
|
8
|
+
* 输出
|
|
9
|
+
* 返回统一 mutation 结果,或把成功结果写入原有 fleet item 字段。
|
|
10
|
+
*
|
|
11
|
+
* 定位
|
|
12
|
+
* 位于 fleet 外层循环和底层项目事务之间,保持 CLI 返回字段兼容。
|
|
13
|
+
*
|
|
14
|
+
* 依赖
|
|
15
|
+
* 依赖调用方注入的 setup/update/doctor/backfill workspace 函数,不直接导入应用层 openprd.js。
|
|
16
|
+
*
|
|
17
|
+
* 维护规则
|
|
18
|
+
* staging 永远关闭 user-scope 写入;新增动作时必须保证失败后不继续执行该项目的后续步骤。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
async function collectFleetProjectHealth(projectPath, options, doctorWorkspace) {
|
|
22
|
+
if (!doctorWorkspace) return { ok: true, errors: [] };
|
|
23
|
+
try {
|
|
24
|
+
const doctor = await doctorWorkspace(projectPath, {
|
|
25
|
+
tools: options.tools ?? 'all',
|
|
26
|
+
hookProfile: options.hookProfile,
|
|
27
|
+
enableUserCodexConfig: false,
|
|
28
|
+
codexHome: options.codexHome,
|
|
29
|
+
openprdHome: options.openprdHome,
|
|
30
|
+
docsContent: false,
|
|
31
|
+
sourceManuals: false,
|
|
32
|
+
});
|
|
33
|
+
return { ok: doctor.ok, errors: doctor.errors ?? [] };
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return { ok: false, errors: [error instanceof Error ? error.message : String(error)] };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function runStagedFleetMutation(plannedAction, projectPath, options, dependencies, staging) {
|
|
40
|
+
const stagedOptions = {
|
|
41
|
+
tools: options.tools ?? 'all',
|
|
42
|
+
hookProfile: options.hookProfile,
|
|
43
|
+
force: Boolean(options.force),
|
|
44
|
+
enableUserCodexConfig: false,
|
|
45
|
+
codexHome: options.codexHome,
|
|
46
|
+
openprdHome: staging.openprdHome,
|
|
47
|
+
};
|
|
48
|
+
if (plannedAction === 'update') {
|
|
49
|
+
const actionResult = await dependencies.updateAgentIntegrationWorkspace(projectPath, stagedOptions);
|
|
50
|
+
if (!actionResult.ok) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
message: 'Staged OpenPrd update failed',
|
|
54
|
+
actionResult,
|
|
55
|
+
errors: actionResult.errors ?? [],
|
|
56
|
+
migration: { workspace: actionResult.migration ?? null, workUnits: null },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const backfill = dependencies.backfillWorkUnitsWorkspace
|
|
60
|
+
? await dependencies.backfillWorkUnitsWorkspace(projectPath, {})
|
|
61
|
+
: { ok: true, changes: [], errors: [], totalVersions: 0, changedVersions: 0 };
|
|
62
|
+
if (!backfill.ok) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
message: 'Staged work-unit backfill failed',
|
|
66
|
+
actionResult,
|
|
67
|
+
backfill,
|
|
68
|
+
errors: backfill.errors ?? [],
|
|
69
|
+
migration: {
|
|
70
|
+
workspace: actionResult.migration ?? null,
|
|
71
|
+
workUnits: { totalVersions: backfill.totalVersions, changedVersions: backfill.changedVersions, ok: false },
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const health = await collectFleetProjectHealth(projectPath, {
|
|
76
|
+
...options,
|
|
77
|
+
openprdHome: staging.openprdHome,
|
|
78
|
+
}, dependencies.doctorWorkspace);
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
actionResult,
|
|
82
|
+
backfill,
|
|
83
|
+
health,
|
|
84
|
+
migration: {
|
|
85
|
+
workspace: actionResult.migration ?? null,
|
|
86
|
+
workUnits: {
|
|
87
|
+
totalVersions: backfill.totalVersions,
|
|
88
|
+
changedVersions: backfill.changedVersions,
|
|
89
|
+
ok: true,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (plannedAction === 'setup') {
|
|
95
|
+
const actionResult = await dependencies.setupAgentIntegrationWorkspace(projectPath, stagedOptions);
|
|
96
|
+
return {
|
|
97
|
+
ok: actionResult.ok,
|
|
98
|
+
message: actionResult.ok ? null : 'Staged OpenPrd setup failed',
|
|
99
|
+
actionResult,
|
|
100
|
+
errors: actionResult.errors ?? actionResult.doctor?.errors ?? [],
|
|
101
|
+
migration: { workspace: actionResult.migration ?? null, workUnits: null },
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (plannedAction === 'backfill-work-units') {
|
|
105
|
+
if (!dependencies.backfillWorkUnitsWorkspace) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
message: 'Missing fleet dependency: backfillWorkUnitsWorkspace',
|
|
109
|
+
errors: [],
|
|
110
|
+
migration: { workspace: null, workUnits: null },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const backfill = await dependencies.backfillWorkUnitsWorkspace(projectPath, {});
|
|
114
|
+
return {
|
|
115
|
+
ok: backfill.ok,
|
|
116
|
+
message: backfill.ok ? null : 'Staged work-unit backfill failed',
|
|
117
|
+
backfill,
|
|
118
|
+
errors: backfill.errors ?? [],
|
|
119
|
+
migration: {
|
|
120
|
+
workspace: null,
|
|
121
|
+
workUnits: {
|
|
122
|
+
totalVersions: backfill.totalVersions,
|
|
123
|
+
changedVersions: backfill.changedVersions,
|
|
124
|
+
ok: backfill.ok,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { ok: false, message: `Unsupported staged fleet action: ${plannedAction}`, errors: [] };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function applySuccessfulFleetMutation(item, plannedAction, transaction) {
|
|
133
|
+
const mutation = transaction.mutationResult;
|
|
134
|
+
if (plannedAction === 'update') {
|
|
135
|
+
const update = mutation.actionResult;
|
|
136
|
+
const backfill = mutation.backfill;
|
|
137
|
+
item.status = 'updated';
|
|
138
|
+
item.registry = transaction.afterApplyResult?.registry ?? null;
|
|
139
|
+
item.changes = [
|
|
140
|
+
...(update.migration?.changes ?? []).map((change) => ({ ...change, source: 'workspace' })),
|
|
141
|
+
...(update.changes ?? []).map((change) => ({ ...change, source: 'agent' })),
|
|
142
|
+
...(backfill.changes ?? []).map((change) => ({ ...change, source: 'work-unit' })),
|
|
143
|
+
];
|
|
144
|
+
item.doctorOk = mutation.health.ok;
|
|
145
|
+
item.healthOk = mutation.health.ok;
|
|
146
|
+
item.healthErrors = mutation.health.errors;
|
|
147
|
+
item.workUnits = { totalVersions: backfill.totalVersions, changedVersions: backfill.changedVersions };
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (plannedAction === 'setup') {
|
|
151
|
+
const setup = mutation.actionResult;
|
|
152
|
+
item.status = 'setup';
|
|
153
|
+
item.registry = transaction.afterApplyResult?.registry ?? null;
|
|
154
|
+
item.changes = [
|
|
155
|
+
...(setup.migration?.changes ?? []).map((change) => ({ ...change, source: 'workspace' })),
|
|
156
|
+
...(setup.changes ?? []).map((change) => ({ ...change, source: 'agent' })),
|
|
157
|
+
];
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const backfill = mutation.backfill;
|
|
161
|
+
item.status = backfill.totalVersions > 0 ? 'backfilled' : 'skipped';
|
|
162
|
+
item.workUnits = { totalVersions: backfill.totalVersions, changedVersions: backfill.changedVersions };
|
|
163
|
+
item.changes = (backfill.changes ?? []).map((change) => ({ ...change, source: 'work-unit' }));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export { applySuccessfulFleetMutation, runStagedFleetMutation };
|