@openprd/cli 0.1.19 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/.openprd/changes/openprd-control-plane-v020/.openprd.yaml +2 -0
  2. package/.openprd/changes/openprd-control-plane-v020/design.md +78 -0
  3. package/.openprd/changes/openprd-control-plane-v020/proposal.md +54 -0
  4. package/.openprd/changes/openprd-control-plane-v020/specs/agent-requirements/spec.md +16 -0
  5. package/.openprd/changes/openprd-control-plane-v020/task-events.jsonl +27 -0
  6. package/.openprd/changes/openprd-control-plane-v020/tasks-002.md +35 -0
  7. package/.openprd/changes/openprd-control-plane-v020/tasks.md +427 -0
  8. package/.openprd/changes/remove-session-tracking-humane-approval/.openprd.yaml +2 -0
  9. package/.openprd/changes/remove-session-tracking-humane-approval/design.md +52 -0
  10. package/.openprd/changes/remove-session-tracking-humane-approval/proposal.md +35 -0
  11. package/.openprd/changes/remove-session-tracking-humane-approval/specs/agent-requirements/spec.md +16 -0
  12. package/.openprd/changes/remove-session-tracking-humane-approval/task-events.jsonl +1 -0
  13. package/.openprd/changes/remove-session-tracking-humane-approval/tasks.md +170 -0
  14. package/.openprd/design/active/asset-spec.md +19 -14
  15. package/.openprd/design/active/direction-plan.md +19 -3
  16. package/.openprd/design/active/facts-sheet.md +16 -7
  17. package/.openprd/design/active/image-preflight.md +6 -5
  18. package/.openprd/design/active/review-studio-v020-directions/compare-plan.json +34 -0
  19. package/.openprd/design/active/review-studio-v020-directions/contact-sheet.jpg +0 -0
  20. package/.openprd/design/active/review-studio-v020-directions/crops/01.png +0 -0
  21. package/.openprd/design/active/review-studio-v020-directions/crops/02.png +0 -0
  22. package/.openprd/design/active/review-studio-v020-directions/crops/03.png +0 -0
  23. package/.openprd/design/active/review-studio-v020-directions/focus-board.template.json +69 -0
  24. package/.openprd/design/active/review-studio-v020-directions/parallel-board.template.json +45 -0
  25. package/.openprd/design/active/review-studio-v020-directions/reference-set.json +143 -0
  26. package/.openprd/design/active/review-studio-v020-directions/source.png +0 -0
  27. package/.openprd/design/active/selected-direction.md +23 -9
  28. package/.openprd/engagements/active/control-plane-architecture.json +203 -0
  29. package/.openprd/engagements/active/control-plane-intake.json +418 -0
  30. package/.openprd/engagements/active/prd.md +183 -119
  31. package/.openprd/engagements/active/review-presentation-v0018.json +176 -0
  32. package/.openprd/i18n-config.json +12 -0
  33. package/.openprd/ledger/events.jsonl +6 -0
  34. package/.openprd/ledger/heads/ebea1a71a9daa566f1c91b53.json +10 -0
  35. package/.openprd/manifest.json +21 -0
  36. package/AGENTS.md +7 -6
  37. package/README.md +31 -29
  38. package/README_EN.md +36 -39
  39. package/package.json +1 -1
  40. package/skills/openprd-frontend-design/SKILL.md +16 -0
  41. package/skills/openprd-harness/SKILL.md +7 -7
  42. package/skills/openprd-quality/SKILL.md +1 -1
  43. package/skills/openprd-requirement-intake/SKILL.md +1 -1
  44. package/skills/openprd-shared/SKILL.md +6 -6
  45. package/src/adapters/adapter-spi.js +193 -0
  46. package/src/adapters/capability-envelope.js +98 -0
  47. package/src/adapters/event-normalizer.js +55 -0
  48. package/src/adapters/index.js +4 -0
  49. package/src/adapters/install-safety.js +249 -0
  50. package/src/agent-canonical-content.js +4 -2
  51. package/src/agent-integration.js +169 -46
  52. package/src/cli/args.js +63 -4
  53. package/src/cli/gate-print.js +17 -0
  54. package/src/cli/quality-commands.js +18 -0
  55. package/src/cli/quality-print.js +10 -0
  56. package/src/cli/runtime-print.js +24 -0
  57. package/src/codex-hook-runner-template.mjs +129 -199
  58. package/src/codex-runtime.js +48 -5
  59. package/src/context/cache.js +245 -0
  60. package/src/context/compiler.js +438 -0
  61. package/src/context/constants.js +30 -0
  62. package/src/context/index.js +39 -0
  63. package/src/context/redaction.js +84 -0
  64. package/src/context/stable.js +69 -0
  65. package/src/context/telemetry.js +42 -0
  66. package/src/dev-standards.js +57 -0
  67. package/src/fleet.js +112 -95
  68. package/src/gates/index.js +2 -0
  69. package/src/gates/scoped-gates.js +256 -0
  70. package/src/gates/store.js +126 -0
  71. package/src/gates/workspace.js +41 -0
  72. package/src/html-artifacts.js +725 -28
  73. package/src/kernel/atomic-store.js +299 -0
  74. package/src/kernel/event-envelope.js +166 -0
  75. package/src/kernel/index.js +4 -0
  76. package/src/kernel/project-ledger.js +467 -0
  77. package/src/kernel/project-manifest.js +205 -0
  78. package/src/knowledge-v3/index.js +1 -0
  79. package/src/knowledge-v3/lifecycle.js +290 -0
  80. package/src/knowledge.js +14 -7
  81. package/src/openprd.js +71 -2
  82. package/src/review-model.js +413 -0
  83. package/src/review-presentation.js +1 -1
  84. package/src/run-harness.js +432 -38
  85. package/src/runtime/cli_runtime_README.md +28 -0
  86. package/src/runtime/errors.js +66 -0
  87. package/src/runtime/index.js +44 -0
  88. package/src/runtime/lane-schema.js +141 -0
  89. package/src/runtime/lane-store.js +279 -0
  90. package/src/runtime/task-runtime.js +449 -0
  91. package/src/runtime/workspace.js +179 -0
  92. package/src/runtime/write-set.js +206 -0
  93. package/src/session-binding.js +16 -3
  94. package/src/session-registry.js +59 -1
  95. package/src/upgrade/fleet-mutation.js +166 -0
  96. package/src/upgrade/fleet-transaction.js +398 -0
  97. package/src/upgrade/transaction-store.js +416 -0
  98. package/src/visual-compare-core.js +66 -27
  99. package/src/visual-compare.js +18 -12
  100. package/src/workspace-core.js +109 -7
  101. package/src/workspace-registry.js +39 -1
  102. package/src/workspace-workflow.js +18 -15
@@ -89,6 +89,29 @@ function runProcess(command, args = [], options = {}) {
89
89
  });
90
90
  let stdout = '';
91
91
  let stderr = '';
92
+ let settled = false;
93
+ let timedOut = false;
94
+ const timeoutMs = Number.isFinite(options.timeoutMs)
95
+ ? Math.max(1, Number(options.timeoutMs))
96
+ : 15000;
97
+ const killGraceMs = Number.isFinite(options.killGraceMs)
98
+ ? Math.max(1, Number(options.killGraceMs))
99
+ : 1000;
100
+ let killTimer = null;
101
+ const timeoutTimer = setTimeout(() => {
102
+ timedOut = true;
103
+ child.kill('SIGTERM');
104
+ killTimer = setTimeout(() => child.kill('SIGKILL'), killGraceMs);
105
+ killTimer.unref?.();
106
+ }, timeoutMs);
107
+ timeoutTimer.unref?.();
108
+ const finish = (payload) => {
109
+ if (settled) return;
110
+ settled = true;
111
+ clearTimeout(timeoutTimer);
112
+ if (killTimer) clearTimeout(killTimer);
113
+ resolve(payload);
114
+ };
92
115
  child.stdout.on('data', (chunk) => {
93
116
  stdout = `${stdout}${chunk}`.slice(-64000);
94
117
  });
@@ -96,7 +119,7 @@ function runProcess(command, args = [], options = {}) {
96
119
  stderr = `${stderr}${chunk}`.slice(-64000);
97
120
  });
98
121
  child.on('error', (error) => {
99
- resolve({
122
+ finish({
100
123
  ok: false,
101
124
  command,
102
125
  args,
@@ -104,19 +127,22 @@ function runProcess(command, args = [], options = {}) {
104
127
  exitCode: null,
105
128
  stdout,
106
129
  stderr,
130
+ timedOut,
107
131
  error: error.message,
108
132
  });
109
133
  });
110
- child.on('close', (exitCode) => {
111
- resolve({
112
- ok: exitCode === 0,
134
+ child.on('close', (exitCode, signal) => {
135
+ finish({
136
+ ok: !timedOut && exitCode === 0,
113
137
  command,
114
138
  args,
115
139
  display: invocation.display,
116
140
  exitCode,
141
+ signal,
117
142
  stdout,
118
143
  stderr,
119
- error: null,
144
+ timedOut,
145
+ error: timedOut ? `Command timed out after ${timeoutMs}ms.` : null,
120
146
  });
121
147
  });
122
148
  });
@@ -131,6 +157,8 @@ function normalizeRunResult(command, args, result = {}) {
131
157
  exitCode: result.exitCode ?? result.status ?? null,
132
158
  stdout: String(result.stdout ?? ''),
133
159
  stderr: String(result.stderr ?? ''),
160
+ timedOut: Boolean(result.timedOut),
161
+ signal: result.signal ? String(result.signal) : null,
134
162
  error: result.error ? String(result.error) : null,
135
163
  };
136
164
  }
@@ -178,6 +206,17 @@ export function diagnoseCodexVersionFailure(result, options = {}) {
178
206
  };
179
207
  }
180
208
 
209
+ if (result.timedOut) {
210
+ return {
211
+ type: 'version-check-timeout',
212
+ summary: `Codex CLI 健康检查超时:codex --version 在 ${options.timeoutMs ?? 15000}ms 内没有返回。`,
213
+ missingPackage: null,
214
+ repairCommand,
215
+ manualCommand: repairCommand.display,
216
+ output,
217
+ };
218
+ }
219
+
181
220
  if (
182
221
  result.exitCode === null
183
222
  && /(ENOENT|not found|command not found|no such file|spawn .*enoent)/i.test(output)
@@ -212,6 +251,8 @@ export async function checkCodexCliHealth(options = {}) {
212
251
  await runner(command, args, {
213
252
  cwd: options.cwd,
214
253
  env: options.env ?? process.env,
254
+ timeoutMs: options.timeoutMs ?? 15000,
255
+ killGraceMs: options.killGraceMs,
215
256
  }),
216
257
  );
217
258
  const repairCommand = buildCodexRepairCommand(options);
@@ -257,6 +298,8 @@ export async function repairCodexCli(options = {}) {
257
298
  await runner(commandInfo.command, commandInfo.args, {
258
299
  cwd: options.cwd,
259
300
  env: options.env ?? process.env,
301
+ timeoutMs: options.repairTimeoutMs ?? 300000,
302
+ killGraceMs: options.killGraceMs,
260
303
  }),
261
304
  );
262
305
  const recheck = result.ok ? await checkCodexCliHealth(options) : null;
@@ -0,0 +1,245 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+
5
+ import {
6
+ buildContextRequestFingerprint,
7
+ compileContextCapsule,
8
+ verifyContextCapsule,
9
+ } from './compiler.js';
10
+ import {
11
+ CONTEXT_CACHE_SCHEMA_VERSION,
12
+ CONTEXT_COMPILER_VERSION,
13
+ } from './constants.js';
14
+ import { cloneStable, compareStableText, digestStable } from './stable.js';
15
+
16
+ function assertCacheKey(key) {
17
+ const normalized = String(key ?? '');
18
+ if (!/^context-v1-[a-f0-9]{64}$/.test(normalized)) {
19
+ throw new TypeError(`Invalid context cache key: ${normalized}`);
20
+ }
21
+ return normalized;
22
+ }
23
+
24
+ export function buildContextCacheKey(input, { previousCapsule = null } = {}) {
25
+ const fingerprint = buildContextRequestFingerprint(input, { previousCapsule });
26
+ const hash = digestStable({
27
+ compilerVersion: CONTEXT_COMPILER_VERSION,
28
+ fingerprint,
29
+ }).slice('sha256:'.length);
30
+ return `context-v1-${hash}`;
31
+ }
32
+
33
+ export function contextCachePath(cacheDir, key) {
34
+ return path.join(cacheDir, `${assertCacheKey(key)}.json`);
35
+ }
36
+
37
+ function cacheEnvelope(key, capsule) {
38
+ if (!verifyContextCapsule(capsule)) {
39
+ throw new TypeError('Refusing to cache an invalid ContextCapsule.');
40
+ }
41
+ return {
42
+ schemaVersion: CONTEXT_CACHE_SCHEMA_VERSION,
43
+ key: assertCacheKey(key),
44
+ revision: capsule.revision,
45
+ capsuleDigest: capsule.digest,
46
+ capsule,
47
+ };
48
+ }
49
+
50
+ function validEnvelope(value, key) {
51
+ return Boolean(
52
+ value
53
+ && value.schemaVersion === CONTEXT_CACHE_SCHEMA_VERSION
54
+ && value.key === key
55
+ && value.capsuleDigest === value.capsule?.digest
56
+ && verifyContextCapsule(value.capsule),
57
+ );
58
+ }
59
+
60
+ export async function readContextCache(cacheDir, key) {
61
+ const normalizedKey = assertCacheKey(key);
62
+ try {
63
+ const parsed = JSON.parse(await fs.readFile(contextCachePath(cacheDir, normalizedKey), 'utf8'));
64
+ return validEnvelope(parsed, normalizedKey) ? parsed.capsule : null;
65
+ } catch (error) {
66
+ if (error?.code === 'ENOENT' || error instanceof SyntaxError) return null;
67
+ throw error;
68
+ }
69
+ }
70
+
71
+ export async function writeContextCache(cacheDir, key, capsule) {
72
+ const normalizedKey = assertCacheKey(key);
73
+ const target = contextCachePath(cacheDir, normalizedKey);
74
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
75
+ const envelope = cacheEnvelope(normalizedKey, capsule);
76
+ await fs.mkdir(cacheDir, { recursive: true });
77
+ try {
78
+ await fs.writeFile(temporary, `${JSON.stringify(envelope)}\n`, 'utf8');
79
+ await fs.rename(temporary, target);
80
+ } finally {
81
+ await fs.rm(temporary, { force: true }).catch(() => {});
82
+ }
83
+ return target;
84
+ }
85
+
86
+ function normalizeInvalidationSelector(selector = {}) {
87
+ const keys = new Set();
88
+ if (selector.key) keys.add(assertCacheKey(selector.key));
89
+ for (const key of selector.keys ?? []) keys.add(assertCacheKey(key));
90
+ return {
91
+ keys,
92
+ revision: selector.revision == null ? null : String(selector.revision),
93
+ all: selector.all === true,
94
+ };
95
+ }
96
+
97
+ export async function invalidateContextCache(cacheDir, selector = {}) {
98
+ const normalized = normalizeInvalidationSelector(selector);
99
+ let names;
100
+ try {
101
+ names = await fs.readdir(cacheDir);
102
+ } catch (error) {
103
+ if (error?.code === 'ENOENT') return { removed: [], kept: [], errors: [] };
104
+ throw error;
105
+ }
106
+ const removed = [];
107
+ const kept = [];
108
+ const errors = [];
109
+ for (const name of names.filter((item) => /^context-v1-[a-f0-9]{64}\.json$/.test(item)).sort()) {
110
+ const key = name.slice(0, -'.json'.length);
111
+ let shouldRemove = normalized.all || normalized.keys.has(key);
112
+ if (!shouldRemove && normalized.revision != null) {
113
+ try {
114
+ const parsed = JSON.parse(await fs.readFile(path.join(cacheDir, name), 'utf8'));
115
+ shouldRemove = parsed.revision === normalized.revision;
116
+ } catch (error) {
117
+ errors.push({ key, error: error?.code ?? error?.name ?? 'read-error' });
118
+ }
119
+ }
120
+ if (!shouldRemove) {
121
+ kept.push(key);
122
+ continue;
123
+ }
124
+ try {
125
+ await fs.rm(path.join(cacheDir, name), { force: true });
126
+ removed.push(key);
127
+ } catch (error) {
128
+ errors.push({ key, error: error?.code ?? error?.name ?? 'remove-error' });
129
+ }
130
+ }
131
+ return { removed, kept, errors };
132
+ }
133
+
134
+ export function createMemoryContextCache() {
135
+ const records = new Map();
136
+ const counters = { reads: 0, writes: 0, hits: 0, misses: 0, invalidations: 0 };
137
+ return {
138
+ async read(key) {
139
+ const normalizedKey = assertCacheKey(key);
140
+ counters.reads += 1;
141
+ if (!records.has(normalizedKey)) {
142
+ counters.misses += 1;
143
+ return null;
144
+ }
145
+ counters.hits += 1;
146
+ return cloneStable(records.get(normalizedKey).capsule);
147
+ },
148
+ async write(key, capsule) {
149
+ const normalizedKey = assertCacheKey(key);
150
+ records.set(normalizedKey, cacheEnvelope(normalizedKey, cloneStable(capsule)));
151
+ counters.writes += 1;
152
+ },
153
+ async invalidate(selector = {}) {
154
+ const normalized = normalizeInvalidationSelector(selector);
155
+ const removed = [];
156
+ const kept = [];
157
+ for (const [key, envelope] of [...records.entries()].sort(([left], [right]) => compareStableText(left, right))) {
158
+ const shouldRemove = normalized.all
159
+ || normalized.keys.has(key)
160
+ || (normalized.revision != null && envelope.revision === normalized.revision);
161
+ if (shouldRemove) {
162
+ records.delete(key);
163
+ removed.push(key);
164
+ } else {
165
+ kept.push(key);
166
+ }
167
+ }
168
+ counters.invalidations += removed.length;
169
+ return { removed, kept, errors: [] };
170
+ },
171
+ stats() {
172
+ return { ...counters, size: records.size };
173
+ },
174
+ };
175
+ }
176
+
177
+ export function createFileContextCache(cacheDir) {
178
+ const counters = { reads: 0, writes: 0, hits: 0, misses: 0, invalidations: 0 };
179
+ return {
180
+ async read(key) {
181
+ counters.reads += 1;
182
+ const capsule = await readContextCache(cacheDir, key);
183
+ if (capsule) counters.hits += 1;
184
+ else counters.misses += 1;
185
+ return capsule;
186
+ },
187
+ async write(key, capsule) {
188
+ counters.writes += 1;
189
+ return writeContextCache(cacheDir, key, capsule);
190
+ },
191
+ async invalidate(selector = {}) {
192
+ const result = await invalidateContextCache(cacheDir, selector);
193
+ counters.invalidations += result.removed.length;
194
+ return result;
195
+ },
196
+ stats() {
197
+ return { ...counters };
198
+ },
199
+ };
200
+ }
201
+
202
+ export async function compileContextWithCache(input, {
203
+ cache,
204
+ previousCapsule = null,
205
+ } = {}) {
206
+ if (!cache || typeof cache.read !== 'function' || typeof cache.write !== 'function') {
207
+ throw new TypeError('compileContextWithCache requires a cache with read() and write().');
208
+ }
209
+ const cacheKey = buildContextCacheKey(input, { previousCapsule });
210
+ const cached = await cache.read(cacheKey);
211
+ if (cached && verifyContextCapsule(cached)) {
212
+ return { capsule: cached, cacheKey, cacheStatus: 'hit', compiled: false };
213
+ }
214
+ const capsule = compileContextCapsule(input, { previousCapsule });
215
+ await cache.write(cacheKey, capsule);
216
+ return { capsule, cacheKey, cacheStatus: 'miss', compiled: true };
217
+ }
218
+
219
+ /**
220
+ * A structural cold/warm benchmark. It deliberately reports operation counts
221
+ * rather than wall-clock thresholds, which keeps CI stable across machines.
222
+ */
223
+ export async function runContextCacheBenchmark(input, {
224
+ cache = createMemoryContextCache(),
225
+ previousCapsule = null,
226
+ } = {}) {
227
+ const cold = await compileContextWithCache(input, { cache, previousCapsule });
228
+ const warm = await compileContextWithCache(input, { cache, previousCapsule });
229
+ return {
230
+ cold: {
231
+ cacheStatus: cold.cacheStatus,
232
+ compiled: cold.compiled,
233
+ digest: cold.capsule.digest,
234
+ includedCount: cold.capsule.entries.length,
235
+ },
236
+ warm: {
237
+ cacheStatus: warm.cacheStatus,
238
+ compiled: warm.compiled,
239
+ digest: warm.capsule.digest,
240
+ includedCount: warm.capsule.entries.length,
241
+ },
242
+ sameDigest: cold.capsule.digest === warm.capsule.digest,
243
+ cacheStats: typeof cache.stats === 'function' ? cache.stats() : null,
244
+ };
245
+ }