@openprd/cli 0.1.0
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/README.md +82 -0
- package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +14 -0
- package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +14 -0
- package/.openprd/benchmarks/index.md +37 -0
- package/.openprd/benchmarks/sources.yaml +56 -0
- package/.openprd/config.yaml +50 -0
- package/.openprd/discovery/config.json +21 -0
- package/.openprd/engagements/active/flows.md +30 -0
- package/.openprd/engagements/active/handoff.md +9 -0
- package/.openprd/engagements/active/intake.md +15 -0
- package/.openprd/engagements/active/prd.md +161 -0
- package/.openprd/engagements/active/review.html +61 -0
- package/.openprd/engagements/active/roles.md +21 -0
- package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +23 -0
- package/.openprd/exports/.gitkeep +0 -0
- package/.openprd/knowledge/index.json +7 -0
- package/.openprd/quality/config.json +229 -0
- package/.openprd/reviews/v0001.html +1256 -0
- package/.openprd/schema/diagram-architecture.schema.yaml +49 -0
- package/.openprd/schema/diagram-product-flow.schema.yaml +52 -0
- package/.openprd/schema/prd.schema.yaml +121 -0
- package/.openprd/sessions/.gitkeep +0 -0
- package/.openprd/standards/config.json +88 -0
- package/.openprd/standards/file-manual-template.md +28 -0
- package/.openprd/standards/folder-readme-template.md +28 -0
- package/.openprd/state/.gitkeep +0 -0
- package/.openprd/state/changes.json +12 -0
- package/.openprd/state/current.json +169 -0
- package/.openprd/state/version-index.json +15 -0
- package/.openprd/state/versions/.gitkeep +0 -0
- package/.openprd/state/versions/v0001.json +121 -0
- package/.openprd/state/versions/v0001.md +161 -0
- package/.openprd/templates/agent/intake.md +6 -0
- package/.openprd/templates/agent/prd.md +21 -0
- package/.openprd/templates/b2b/intake.md +6 -0
- package/.openprd/templates/b2b/prd.md +24 -0
- package/.openprd/templates/base/intake.md +18 -0
- package/.openprd/templates/base/prd.md +67 -0
- package/.openprd/templates/company/README.md +10 -0
- package/.openprd/templates/consumer/intake.md +6 -0
- package/.openprd/templates/consumer/prd.md +19 -0
- package/.openprd/templates/diagram/architecture.contract.json +53 -0
- package/.openprd/templates/diagram/product-flow.contract.json +76 -0
- package/.openprd/templates/industry/README.md +16 -0
- package/.openprd/templates/manifest.yaml +27 -0
- package/.openprd/templates/project/README.md +14 -0
- package/.openprd/templates/session/README.md +14 -0
- package/AGENTS.md +44 -0
- package/CONTRIBUTING.md +30 -0
- package/LICENSE +21 -0
- package/README.md +727 -0
- package/README_CN.md +583 -0
- package/SECURITY.md +23 -0
- package/bin/openprd.js +5 -0
- package/docs/assets/openprd-capability-overview-en.png +0 -0
- package/docs/assets/openprd-capability-overview-zh.png +0 -0
- package/docs/assets/openprd-learning-html.png +0 -0
- package/docs/assets/openprd-quality-html.png +0 -0
- package/docs/assets/openprd-review-html.png +0 -0
- package/docs/assets/openprd-scenario-overview.png +0 -0
- package/docs/assets/openprd-scenario-overview.svg +114 -0
- package/docs/assets/openprd-self-evolving-mechanisms-en.png +0 -0
- package/docs/assets/openprd-self-evolving-mechanisms-zh.png +0 -0
- package/docs/assets/openprd-visual-compare-case-study-en.png +0 -0
- package/docs/assets/openprd-visual-compare-case-study-zh.png +0 -0
- package/package.json +59 -0
- package/scripts/openprd-dev-check.mjs +5 -0
- package/scripts/openprd-review-presentation.mjs +82 -0
- package/skills/openprd-benchmark-router/SKILL.md +92 -0
- package/skills/openprd-benchmark-router/agents/openai.yaml +4 -0
- package/skills/openprd-benchmark-router/references/benchmark-sources.md +74 -0
- package/skills/openprd-benchmark-router/references/evaluation-lenses.md +66 -0
- package/skills/openprd-benchmark-router/references/source-policy.md +35 -0
- package/skills/openprd-diagram-review/SKILL.md +91 -0
- package/skills/openprd-diagram-review/agents/openai.yaml +4 -0
- package/skills/openprd-diagram-review/examples/architecture-zh.md +8 -0
- package/skills/openprd-diagram-review/examples/product-flow-zh.md +7 -0
- package/skills/openprd-diagram-review/references/cocoon-patterns.md +17 -0
- package/skills/openprd-diagram-review/references/diagram-contracts.md +126 -0
- package/skills/openprd-diagram-review/references/review-checklist.md +10 -0
- package/skills/openprd-discovery-loop/SKILL.md +196 -0
- package/skills/openprd-discovery-loop/agents/openai.yaml +3 -0
- package/skills/openprd-harness/SKILL.md +179 -0
- package/skills/openprd-harness/agents/openai.yaml +4 -0
- package/skills/openprd-harness/examples/full-workflow-zh.md +9 -0
- package/skills/openprd-harness/references/command-map.md +71 -0
- package/skills/openprd-harness/references/examples.md +26 -0
- package/skills/openprd-harness/references/usage-guide.md +335 -0
- package/skills/openprd-harness/references/workflow-gates.md +51 -0
- package/skills/openprd-learning-review/SKILL.md +75 -0
- package/skills/openprd-learning-review/agents/openai.yaml +4 -0
- package/skills/openprd-learning-review/references/content-contract.md +125 -0
- package/skills/openprd-learning-review/references/ebook-reader.md +46 -0
- package/skills/openprd-learning-review/references/evidence-manifest.md +55 -0
- package/skills/openprd-learning-review/references/genre-library.md +43 -0
- package/skills/openprd-learning-review/references/prompt-engineering.md +71 -0
- package/skills/openprd-learning-review/references/quality-rubric.md +28 -0
- package/skills/openprd-learning-review/references/retrieval-worked-example.md +40 -0
- package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +67 -0
- package/skills/openprd-quality/SKILL.md +101 -0
- package/skills/openprd-requirement-intake/SKILL.md +76 -0
- package/skills/openprd-requirement-intake/agents/openai.yaml +4 -0
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +105 -0
- package/skills/openprd-requirement-intake/references/routing-rubric.md +64 -0
- package/skills/openprd-router/SKILL.md +40 -0
- package/skills/openprd-shared/SKILL.md +142 -0
- package/skills/openprd-shared/agents/openai.yaml +4 -0
- package/skills/openprd-shared/references/language-and-review.md +50 -0
- package/skills/openprd-shared/references/operating-rules.md +65 -0
- package/skills/openprd-shared/references/skill-architecture.md +70 -0
- package/skills/openprd-standards/SKILL.md +79 -0
- package/skills/openprd-standards/agents/openai.yaml +4 -0
- package/src/agent-integration.js +1717 -0
- package/src/benchmark.js +873 -0
- package/src/cli/args.js +460 -0
- package/src/cli/print.js +1423 -0
- package/src/codex-hook-runner-template.mjs +2422 -0
- package/src/dev-standards.js +372 -0
- package/src/diagram-core.js +1047 -0
- package/src/diagram-workspace.js +262 -0
- package/src/discovery.js +709 -0
- package/src/fleet.js +531 -0
- package/src/fs-utils.js +83 -0
- package/src/growth.js +545 -0
- package/src/html-artifacts.js +3803 -0
- package/src/knowledge.js +668 -0
- package/src/language-policy.js +142 -0
- package/src/learning-review.js +1655 -0
- package/src/loop.js +1290 -0
- package/src/openprd.js +1136 -0
- package/src/openspec/change-lifecycle.js +359 -0
- package/src/openspec/change-validate.js +248 -0
- package/src/openspec/constants.js +12 -0
- package/src/openspec/execute.js +300 -0
- package/src/openspec/generate.js +692 -0
- package/src/openspec/paths.js +111 -0
- package/src/openspec/tasks.js +352 -0
- package/src/prd-core.js +656 -0
- package/src/quality-html-artifact.js +1414 -0
- package/src/quality-learning.js +658 -0
- package/src/quality.js +1262 -0
- package/src/review-presentation.js +240 -0
- package/src/run-harness.js +1470 -0
- package/src/self-update.js +329 -0
- package/src/session-binding.js +140 -0
- package/src/source-inventory.js +224 -0
- package/src/standards.js +914 -0
- package/src/time.js +33 -0
- package/src/visual-compare.js +216 -0
- package/src/work-unit-migration.js +232 -0
- package/src/work-unit.js +88 -0
- package/src/workspace-core.js +1706 -0
- package/src/workspace-registry.js +162 -0
- package/src/workspace-workflow.js +1797 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_SELF_UPDATE_SOURCE = 'git+https://github.com/mileson/openprd.git';
|
|
7
|
+
|
|
8
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const DEFAULT_PACKAGE_ROOT = path.resolve(MODULE_DIR, '..');
|
|
10
|
+
|
|
11
|
+
function shellQuote(value) {
|
|
12
|
+
const text = String(value);
|
|
13
|
+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(text)) {
|
|
14
|
+
return text;
|
|
15
|
+
}
|
|
16
|
+
return `'${text.replace(/'/g, `'\\''`)}'`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatCommand(command, args = []) {
|
|
20
|
+
return [command, ...args].map(shellQuote).join(' ');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function readPackageInfo(packageRoot = DEFAULT_PACKAGE_ROOT) {
|
|
24
|
+
const packageJsonPath = path.join(packageRoot, 'package.json');
|
|
25
|
+
const raw = await fs.readFile(packageJsonPath, 'utf8');
|
|
26
|
+
const packageJson = JSON.parse(raw);
|
|
27
|
+
return {
|
|
28
|
+
name: packageJson.name ?? null,
|
|
29
|
+
version: packageJson.version ?? null,
|
|
30
|
+
packageRoot,
|
|
31
|
+
packageJsonPath,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function isLocalSourceCheckout(packageRoot = DEFAULT_PACKAGE_ROOT) {
|
|
36
|
+
const gitDir = path.join(packageRoot, '.git');
|
|
37
|
+
return fs.stat(gitDir).then((stat) => stat.isDirectory() || stat.isFile(), () => false);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function runProcess(command, args = [], options = {}) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const child = spawn(command, args, {
|
|
43
|
+
cwd: options.cwd ?? process.cwd(),
|
|
44
|
+
env: options.env ?? process.env,
|
|
45
|
+
shell: Boolean(options.shell),
|
|
46
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
47
|
+
});
|
|
48
|
+
let stdout = '';
|
|
49
|
+
let stderr = '';
|
|
50
|
+
child.stdout.on('data', (chunk) => {
|
|
51
|
+
stdout = `${stdout}${chunk}`.slice(-64000);
|
|
52
|
+
});
|
|
53
|
+
child.stderr.on('data', (chunk) => {
|
|
54
|
+
stderr = `${stderr}${chunk}`.slice(-64000);
|
|
55
|
+
});
|
|
56
|
+
child.on('error', (error) => {
|
|
57
|
+
resolve({
|
|
58
|
+
ok: false,
|
|
59
|
+
command,
|
|
60
|
+
args,
|
|
61
|
+
display: formatCommand(command, args),
|
|
62
|
+
exitCode: null,
|
|
63
|
+
stdout,
|
|
64
|
+
stderr: `${stderr}${error.message}`,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
child.on('close', (exitCode) => {
|
|
68
|
+
resolve({
|
|
69
|
+
ok: exitCode === 0,
|
|
70
|
+
command,
|
|
71
|
+
args,
|
|
72
|
+
display: formatCommand(command, args),
|
|
73
|
+
exitCode,
|
|
74
|
+
stdout,
|
|
75
|
+
stderr,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function resolveOpenPrdExecutable(deps = {}) {
|
|
82
|
+
if (typeof deps.resolveOpenPrdExecutable === 'function') {
|
|
83
|
+
return deps.resolveOpenPrdExecutable();
|
|
84
|
+
}
|
|
85
|
+
const command = process.platform === 'win32' ? 'where openprd' : 'command -v openprd';
|
|
86
|
+
const result = await runProcess(command, [], { shell: true });
|
|
87
|
+
const executable = result.stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? null;
|
|
88
|
+
if (!result.ok || !executable) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
executable: null,
|
|
92
|
+
command,
|
|
93
|
+
error: result.stderr.trim() || 'Unable to resolve openprd executable from PATH.',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
executable,
|
|
99
|
+
command,
|
|
100
|
+
error: null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildSelfUpdatePlan(options = {}) {
|
|
105
|
+
const source = options.source ?? DEFAULT_SELF_UPDATE_SOURCE;
|
|
106
|
+
const command = options.packageManager ?? 'npm';
|
|
107
|
+
const args = ['install', '-g', source];
|
|
108
|
+
return {
|
|
109
|
+
source,
|
|
110
|
+
install: {
|
|
111
|
+
command,
|
|
112
|
+
args,
|
|
113
|
+
display: formatCommand(command, args),
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildProjectRefreshPlan(targetPath, options = {}) {
|
|
119
|
+
const executable = options.executable ?? 'openprd';
|
|
120
|
+
const args = options.fleet
|
|
121
|
+
? ['fleet', targetPath, '--update-openprd']
|
|
122
|
+
: ['update', targetPath];
|
|
123
|
+
|
|
124
|
+
if (options.fleet && options.maxDepth) {
|
|
125
|
+
args.push('--max-depth', String(options.maxDepth));
|
|
126
|
+
}
|
|
127
|
+
if (options.fleet && options.include) {
|
|
128
|
+
args.push('--include', String(options.include));
|
|
129
|
+
}
|
|
130
|
+
if (options.fleet && options.exclude) {
|
|
131
|
+
args.push('--exclude', String(options.exclude));
|
|
132
|
+
}
|
|
133
|
+
if (options.fleet && options.report) {
|
|
134
|
+
args.push('--report');
|
|
135
|
+
if (typeof options.report === 'string') {
|
|
136
|
+
args.push(options.report);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (options.tools) {
|
|
140
|
+
args.push('--tools', options.tools);
|
|
141
|
+
}
|
|
142
|
+
if (options.hookProfile) {
|
|
143
|
+
args.push('--hook-profile', options.hookProfile);
|
|
144
|
+
}
|
|
145
|
+
if (options.force && !options.fleet) {
|
|
146
|
+
args.push('--force');
|
|
147
|
+
}
|
|
148
|
+
if (options.childJson) {
|
|
149
|
+
args.push('--json');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
mode: options.fleet ? 'fleet' : 'project',
|
|
154
|
+
targetPath,
|
|
155
|
+
refresh: {
|
|
156
|
+
command: executable,
|
|
157
|
+
args,
|
|
158
|
+
display: formatCommand(executable, args),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function createSelfUpdateWorkspace(deps = {}) {
|
|
164
|
+
const runCommand = deps.runCommand ?? runProcess;
|
|
165
|
+
const packageRoot = deps.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
|
166
|
+
|
|
167
|
+
async function selfUpdateWorkspace(options = {}) {
|
|
168
|
+
const packageInfo = await (deps.readPackageInfo ?? readPackageInfo)(packageRoot);
|
|
169
|
+
const localCheckout = await (deps.isLocalSourceCheckout ?? isLocalSourceCheckout)(packageRoot);
|
|
170
|
+
const plan = buildSelfUpdatePlan(options);
|
|
171
|
+
const base = {
|
|
172
|
+
ok: true,
|
|
173
|
+
action: 'self-update',
|
|
174
|
+
dryRun: Boolean(options.dryRun),
|
|
175
|
+
source: plan.source,
|
|
176
|
+
package: packageInfo,
|
|
177
|
+
localCheckout,
|
|
178
|
+
installCommand: plan.install,
|
|
179
|
+
result: null,
|
|
180
|
+
resolvedExecutable: null,
|
|
181
|
+
errors: [],
|
|
182
|
+
nextActions: [],
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
if (options.dryRun) {
|
|
186
|
+
return {
|
|
187
|
+
...base,
|
|
188
|
+
skipped: true,
|
|
189
|
+
skipReason: 'dry-run',
|
|
190
|
+
nextActions: ['Run without --dry-run to update the OpenPrd CLI.'],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (localCheckout && !options.allowLocalCheckout) {
|
|
195
|
+
return {
|
|
196
|
+
...base,
|
|
197
|
+
ok: false,
|
|
198
|
+
skipped: true,
|
|
199
|
+
skipReason: 'local-source-checkout',
|
|
200
|
+
errors: [
|
|
201
|
+
'This openprd command is running from a local source checkout. Reinstall the global CLI with npm, or use your local development workflow.',
|
|
202
|
+
],
|
|
203
|
+
nextActions: [
|
|
204
|
+
`Run ${plan.install.display} from outside the source checkout, or update this checkout manually.`,
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const installResult = await runCommand(plan.install.command, plan.install.args, {
|
|
210
|
+
cwd: options.cwd ?? packageInfo.packageRoot,
|
|
211
|
+
env: options.env ?? process.env,
|
|
212
|
+
});
|
|
213
|
+
const resolvedExecutable = installResult.ok
|
|
214
|
+
? await resolveOpenPrdExecutable(deps)
|
|
215
|
+
: null;
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
...base,
|
|
219
|
+
ok: installResult.ok && (resolvedExecutable?.ok ?? false),
|
|
220
|
+
result: installResult,
|
|
221
|
+
resolvedExecutable,
|
|
222
|
+
errors: [
|
|
223
|
+
...(installResult.ok ? [] : [installResult.stderr.trim() || `Self-update command failed with exit code ${installResult.exitCode}.`]),
|
|
224
|
+
...(installResult.ok && !resolvedExecutable?.ok ? [resolvedExecutable?.error ?? 'Unable to resolve updated openprd executable.'] : []),
|
|
225
|
+
],
|
|
226
|
+
nextActions: installResult.ok && !resolvedExecutable?.ok
|
|
227
|
+
? ['Check that the global npm bin directory is on PATH, then run openprd update <project>.']
|
|
228
|
+
: [],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function upgradeWorkspace(targetPath, options = {}) {
|
|
233
|
+
const resolvedTarget = path.resolve(targetPath ?? process.cwd());
|
|
234
|
+
const selfUpdate = await selfUpdateWorkspace(options);
|
|
235
|
+
const dryRun = Boolean(options.dryRun);
|
|
236
|
+
const executable = selfUpdate.resolvedExecutable?.executable ?? 'openprd';
|
|
237
|
+
const refreshPlan = buildProjectRefreshPlan(resolvedTarget, {
|
|
238
|
+
...options,
|
|
239
|
+
executable,
|
|
240
|
+
childJson: Boolean(options.json),
|
|
241
|
+
});
|
|
242
|
+
const base = {
|
|
243
|
+
ok: true,
|
|
244
|
+
action: 'upgrade',
|
|
245
|
+
dryRun,
|
|
246
|
+
mode: refreshPlan.mode,
|
|
247
|
+
targetPath: resolvedTarget,
|
|
248
|
+
selfUpdate,
|
|
249
|
+
projectRefresh: {
|
|
250
|
+
ok: true,
|
|
251
|
+
skipped: false,
|
|
252
|
+
command: refreshPlan.refresh,
|
|
253
|
+
result: null,
|
|
254
|
+
errors: [],
|
|
255
|
+
},
|
|
256
|
+
stages: {
|
|
257
|
+
selfUpdateOk: selfUpdate.ok,
|
|
258
|
+
projectRefreshOk: true,
|
|
259
|
+
},
|
|
260
|
+
errors: [],
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
if (dryRun) {
|
|
264
|
+
return {
|
|
265
|
+
...base,
|
|
266
|
+
projectRefresh: {
|
|
267
|
+
...base.projectRefresh,
|
|
268
|
+
skipped: true,
|
|
269
|
+
skipReason: 'dry-run',
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!selfUpdate.ok) {
|
|
275
|
+
return {
|
|
276
|
+
...base,
|
|
277
|
+
ok: false,
|
|
278
|
+
projectRefresh: {
|
|
279
|
+
...base.projectRefresh,
|
|
280
|
+
ok: false,
|
|
281
|
+
skipped: true,
|
|
282
|
+
skipReason: 'self-update-failed',
|
|
283
|
+
errors: ['Project refresh was not run because self-update did not complete.'],
|
|
284
|
+
},
|
|
285
|
+
stages: {
|
|
286
|
+
selfUpdateOk: false,
|
|
287
|
+
projectRefreshOk: false,
|
|
288
|
+
},
|
|
289
|
+
errors: selfUpdate.errors,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const refreshResult = await runCommand(refreshPlan.refresh.command, refreshPlan.refresh.args, {
|
|
294
|
+
cwd: options.cwd ?? resolvedTarget,
|
|
295
|
+
env: options.env ?? process.env,
|
|
296
|
+
});
|
|
297
|
+
const projectRefresh = {
|
|
298
|
+
ok: refreshResult.ok,
|
|
299
|
+
skipped: false,
|
|
300
|
+
command: refreshPlan.refresh,
|
|
301
|
+
result: refreshResult,
|
|
302
|
+
errors: refreshResult.ok ? [] : [refreshResult.stderr.trim() || `Project refresh failed with exit code ${refreshResult.exitCode}.`],
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
...base,
|
|
307
|
+
ok: selfUpdate.ok && projectRefresh.ok,
|
|
308
|
+
projectRefresh,
|
|
309
|
+
stages: {
|
|
310
|
+
selfUpdateOk: selfUpdate.ok,
|
|
311
|
+
projectRefreshOk: projectRefresh.ok,
|
|
312
|
+
},
|
|
313
|
+
errors: [
|
|
314
|
+
...selfUpdate.errors,
|
|
315
|
+
...projectRefresh.errors,
|
|
316
|
+
],
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
selfUpdateWorkspace,
|
|
322
|
+
upgradeWorkspace,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const defaultWorkspace = createSelfUpdateWorkspace();
|
|
327
|
+
|
|
328
|
+
export const selfUpdateWorkspace = defaultWorkspace.selfUpdateWorkspace;
|
|
329
|
+
export const upgradeWorkspace = defaultWorkspace.upgradeWorkspace;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readJson, writeJson } from './fs-utils.js';
|
|
3
|
+
import { timestamp } from './time.js';
|
|
4
|
+
|
|
5
|
+
const OPENPRD_HARNESS_DIR = path.join('.openprd', 'harness');
|
|
6
|
+
const OPENPRD_HARNESS_REQUIREMENT_GATE = path.join(OPENPRD_HARNESS_DIR, 'requirement-gate.json');
|
|
7
|
+
const OPENPRD_HARNESS_SESSION_BINDINGS_DIR = path.join(OPENPRD_HARNESS_DIR, 'session-bindings');
|
|
8
|
+
|
|
9
|
+
function normalizeSessionId(sessionId) {
|
|
10
|
+
const text = String(sessionId ?? '').trim();
|
|
11
|
+
return text || null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function sessionBindingPath(projectRoot, sessionId) {
|
|
15
|
+
const normalized = normalizeSessionId(sessionId);
|
|
16
|
+
if (!normalized) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return path.join(
|
|
20
|
+
projectRoot,
|
|
21
|
+
OPENPRD_HARNESS_SESSION_BINDINGS_DIR,
|
|
22
|
+
`${normalized.replace(/[^A-Za-z0-9._-]/g, '_')}.json`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function readLegacyRequirementGate(projectRoot) {
|
|
27
|
+
return readJson(path.join(projectRoot, OPENPRD_HARNESS_REQUIREMENT_GATE)).catch(() => null);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function readSessionBinding(projectRoot, sessionId) {
|
|
31
|
+
const filePath = sessionBindingPath(projectRoot, sessionId);
|
|
32
|
+
if (!filePath) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const binding = await readJson(filePath).catch(() => null);
|
|
36
|
+
return binding ? { ...binding, path: filePath } : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function upsertSessionBinding(projectRoot, sessionId, patch = {}) {
|
|
40
|
+
const normalized = normalizeSessionId(sessionId);
|
|
41
|
+
if (!normalized) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const filePath = sessionBindingPath(projectRoot, normalized);
|
|
45
|
+
const previous = await readJson(filePath).catch(() => null);
|
|
46
|
+
const next = {
|
|
47
|
+
...(previous ?? {}),
|
|
48
|
+
version: 1,
|
|
49
|
+
sessionId: normalized,
|
|
50
|
+
...patch,
|
|
51
|
+
createdAt: previous?.createdAt ?? patch.createdAt ?? timestamp(),
|
|
52
|
+
updatedAt: patch.updatedAt ?? timestamp(),
|
|
53
|
+
};
|
|
54
|
+
await writeJson(filePath, next);
|
|
55
|
+
return {
|
|
56
|
+
...next,
|
|
57
|
+
path: filePath,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function syncSessionBindingFromSnapshot(projectRoot, snapshot, options = {}) {
|
|
62
|
+
const legacyGate = await readLegacyRequirementGate(projectRoot);
|
|
63
|
+
const sessionId = normalizeSessionId(options.sessionId ?? legacyGate?.sessionId);
|
|
64
|
+
if (!sessionId || !snapshot?.versionId) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return upsertSessionBinding(projectRoot, sessionId, {
|
|
68
|
+
promptPreview: options.promptPreview ?? legacyGate?.promptPreview ?? null,
|
|
69
|
+
gateStatus: legacyGate?.status ?? null,
|
|
70
|
+
gateActive: Boolean(legacyGate?.active),
|
|
71
|
+
title: snapshot.title ?? null,
|
|
72
|
+
versionId: snapshot.versionId,
|
|
73
|
+
digest: snapshot.digest ?? null,
|
|
74
|
+
workUnitId: snapshot.workUnitId ?? null,
|
|
75
|
+
targetRoot: options.targetRoot ?? snapshot.targetRoot ?? null,
|
|
76
|
+
reviewStatus: options.reviewStatus ?? 'pending-confirmation',
|
|
77
|
+
reviewPath: options.reviewPath ?? options.stableReviewArtifact ?? null,
|
|
78
|
+
activeReviewPath: options.activeReviewPath ?? options.reviewArtifact ?? null,
|
|
79
|
+
reviewArtifact: options.reviewArtifact ?? options.activeReviewPath ?? null,
|
|
80
|
+
stableReviewArtifact: options.stableReviewArtifact ?? options.reviewPath ?? null,
|
|
81
|
+
changeId: options.preserveChangeId ? (options.changeId ?? null) : null,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function syncSessionBindingFromReview(projectRoot, snapshot, options = {}) {
|
|
86
|
+
const legacyGate = await readLegacyRequirementGate(projectRoot);
|
|
87
|
+
const sessionId = normalizeSessionId(options.sessionId ?? legacyGate?.sessionId);
|
|
88
|
+
if (!sessionId || !snapshot?.versionId) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return upsertSessionBinding(projectRoot, sessionId, {
|
|
92
|
+
promptPreview: options.promptPreview ?? legacyGate?.promptPreview ?? null,
|
|
93
|
+
gateStatus: legacyGate?.status ?? null,
|
|
94
|
+
gateActive: Boolean(legacyGate?.active),
|
|
95
|
+
title: snapshot.title ?? null,
|
|
96
|
+
versionId: snapshot.versionId,
|
|
97
|
+
digest: snapshot.digest ?? null,
|
|
98
|
+
workUnitId: snapshot.workUnitId ?? null,
|
|
99
|
+
targetRoot: options.targetRoot ?? snapshot.targetRoot ?? null,
|
|
100
|
+
reviewStatus: options.reviewStatus ?? null,
|
|
101
|
+
reviewPath: options.reviewPath ?? options.stableReviewArtifact ?? null,
|
|
102
|
+
activeReviewPath: options.activeReviewPath ?? options.reviewArtifact ?? null,
|
|
103
|
+
reviewArtifact: options.reviewArtifact ?? options.activeReviewPath ?? null,
|
|
104
|
+
stableReviewArtifact: options.stableReviewArtifact ?? options.reviewPath ?? null,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function syncSessionBindingFromChange(projectRoot, changeId, options = {}) {
|
|
109
|
+
const legacyGate = await readLegacyRequirementGate(projectRoot);
|
|
110
|
+
const sessionId = normalizeSessionId(options.sessionId ?? legacyGate?.sessionId);
|
|
111
|
+
if (!sessionId || !changeId) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return upsertSessionBinding(projectRoot, sessionId, {
|
|
115
|
+
promptPreview: options.promptPreview ?? legacyGate?.promptPreview ?? null,
|
|
116
|
+
gateStatus: legacyGate?.status ?? null,
|
|
117
|
+
gateActive: Boolean(legacyGate?.active),
|
|
118
|
+
title: options.title ?? null,
|
|
119
|
+
versionId: options.versionId ?? null,
|
|
120
|
+
digest: options.digest ?? null,
|
|
121
|
+
workUnitId: options.workUnitId ?? null,
|
|
122
|
+
targetRoot: options.targetRoot ?? null,
|
|
123
|
+
reviewStatus: options.reviewStatus ?? null,
|
|
124
|
+
reviewPath: options.reviewPath ?? options.stableReviewArtifact ?? null,
|
|
125
|
+
activeReviewPath: options.activeReviewPath ?? options.reviewArtifact ?? null,
|
|
126
|
+
reviewArtifact: options.reviewArtifact ?? options.activeReviewPath ?? null,
|
|
127
|
+
stableReviewArtifact: options.stableReviewArtifact ?? options.reviewPath ?? null,
|
|
128
|
+
changeId,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
readLegacyRequirementGate,
|
|
134
|
+
readSessionBinding,
|
|
135
|
+
sessionBindingPath,
|
|
136
|
+
syncSessionBindingFromChange,
|
|
137
|
+
syncSessionBindingFromReview,
|
|
138
|
+
syncSessionBindingFromSnapshot,
|
|
139
|
+
upsertSessionBinding,
|
|
140
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { cjoin, exists } from './fs-utils.js';
|
|
4
|
+
import { timestamp } from './time.js';
|
|
5
|
+
|
|
6
|
+
const SOURCE_INVENTORY_IGNORE_DIRS = new Set([
|
|
7
|
+
'.git',
|
|
8
|
+
'.hg',
|
|
9
|
+
'.svn',
|
|
10
|
+
'.openprd',
|
|
11
|
+
'.openspec',
|
|
12
|
+
'.next',
|
|
13
|
+
'.turbo',
|
|
14
|
+
'.cache',
|
|
15
|
+
'.env',
|
|
16
|
+
'.eggs',
|
|
17
|
+
'.mypy_cache',
|
|
18
|
+
'.parcel-cache',
|
|
19
|
+
'.pytest_cache',
|
|
20
|
+
'.ruff_cache',
|
|
21
|
+
'.tox',
|
|
22
|
+
'.venv',
|
|
23
|
+
'.vite',
|
|
24
|
+
'.vscode',
|
|
25
|
+
'coverage',
|
|
26
|
+
'dist',
|
|
27
|
+
'env',
|
|
28
|
+
'build',
|
|
29
|
+
'node_modules',
|
|
30
|
+
'vendor',
|
|
31
|
+
'venv',
|
|
32
|
+
'__pycache__',
|
|
33
|
+
]);
|
|
34
|
+
const SOURCE_INVENTORY_EXTENSIONS = new Set([
|
|
35
|
+
'.cjs',
|
|
36
|
+
'.css',
|
|
37
|
+
'.go',
|
|
38
|
+
'.html',
|
|
39
|
+
'.java',
|
|
40
|
+
'.js',
|
|
41
|
+
'.json',
|
|
42
|
+
'.jsx',
|
|
43
|
+
'.kt',
|
|
44
|
+
'.md',
|
|
45
|
+
'.mjs',
|
|
46
|
+
'.php',
|
|
47
|
+
'.py',
|
|
48
|
+
'.rb',
|
|
49
|
+
'.rs',
|
|
50
|
+
'.scss',
|
|
51
|
+
'.sh',
|
|
52
|
+
'.sql',
|
|
53
|
+
'.swift',
|
|
54
|
+
'.toml',
|
|
55
|
+
'.ts',
|
|
56
|
+
'.tsx',
|
|
57
|
+
'.vue',
|
|
58
|
+
'.yaml',
|
|
59
|
+
'.yml',
|
|
60
|
+
]);
|
|
61
|
+
const SOURCE_INVENTORY_SPECIAL_FILES = new Set([
|
|
62
|
+
'Dockerfile',
|
|
63
|
+
'Makefile',
|
|
64
|
+
'Procfile',
|
|
65
|
+
'README',
|
|
66
|
+
'LICENSE',
|
|
67
|
+
'AGENTS.md',
|
|
68
|
+
'CLAUDE.md',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
function shouldIgnoreSourceDirectory(name) {
|
|
72
|
+
const normalized = String(name ?? '').toLowerCase();
|
|
73
|
+
return SOURCE_INVENTORY_IGNORE_DIRS.has(normalized)
|
|
74
|
+
|| /^\.?venv(?:[-_].*)?$/.test(normalized)
|
|
75
|
+
|| /^env(?:[-_].*)?$/.test(normalized);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function shouldInventorySourceFile(name) {
|
|
79
|
+
if (SOURCE_INVENTORY_SPECIAL_FILES.has(name)) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
return SOURCE_INVENTORY_EXTENSIONS.has(path.extname(name).toLowerCase());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function compareSourceInventoryEntries(a, b) {
|
|
86
|
+
const aHidden = a.name.startsWith('.');
|
|
87
|
+
const bHidden = b.name.startsWith('.');
|
|
88
|
+
if (aHidden !== bHidden) {
|
|
89
|
+
return aHidden ? 1 : -1;
|
|
90
|
+
}
|
|
91
|
+
if (a.isDirectory() !== b.isDirectory()) {
|
|
92
|
+
return a.isDirectory() ? -1 : 1;
|
|
93
|
+
}
|
|
94
|
+
return a.name.localeCompare(b.name);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function classifyInventoryFile(relativePath) {
|
|
98
|
+
const lower = relativePath.toLowerCase();
|
|
99
|
+
const base = path.basename(lower);
|
|
100
|
+
if (base.includes('test') || base.includes('spec') || lower.includes('/test/') || lower.includes('/tests/')) {
|
|
101
|
+
return 'test';
|
|
102
|
+
}
|
|
103
|
+
if (lower.endsWith('.md') || lower.includes('/docs/') || base === 'readme') {
|
|
104
|
+
return 'document';
|
|
105
|
+
}
|
|
106
|
+
if (['package.json', 'tsconfig.json', 'vite.config.ts', 'next.config.js', 'dockerfile', 'makefile'].includes(base) || lower.endsWith('.yaml') || lower.endsWith('.yml') || lower.endsWith('.toml')) {
|
|
107
|
+
return 'configuration';
|
|
108
|
+
}
|
|
109
|
+
if (lower.includes('/schema/') || lower.endsWith('.sql')) {
|
|
110
|
+
return 'schema';
|
|
111
|
+
}
|
|
112
|
+
return 'implementation';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function sourceLanguage(relativePath) {
|
|
116
|
+
const ext = path.extname(relativePath).toLowerCase().replace('.', '');
|
|
117
|
+
if (!ext) {
|
|
118
|
+
return path.basename(relativePath);
|
|
119
|
+
}
|
|
120
|
+
if (['js', 'jsx', 'mjs', 'cjs'].includes(ext)) return 'javascript';
|
|
121
|
+
if (['ts', 'tsx'].includes(ext)) return 'typescript';
|
|
122
|
+
if (['yml', 'yaml'].includes(ext)) return 'yaml';
|
|
123
|
+
if (ext === 'md') return 'markdown';
|
|
124
|
+
return ext;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function collectSourceInventory(sourceRoot, options = {}) {
|
|
128
|
+
const maxDepth = Number(options.maxDepth ?? 6);
|
|
129
|
+
const maxFiles = Number(options.maxFiles ?? 250);
|
|
130
|
+
const files = [];
|
|
131
|
+
const directories = [];
|
|
132
|
+
const languageBreakdown = {};
|
|
133
|
+
let truncated = false;
|
|
134
|
+
|
|
135
|
+
if (!(await exists(sourceRoot))) {
|
|
136
|
+
throw new Error(`Missing source root for OpenPrd discovery: ${sourceRoot}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function walk(currentDir, depth) {
|
|
140
|
+
if (files.length >= maxFiles) {
|
|
141
|
+
truncated = true;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let entries = [];
|
|
146
|
+
try {
|
|
147
|
+
entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
148
|
+
} catch {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
entries.sort(compareSourceInventoryEntries);
|
|
153
|
+
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
if (files.length >= maxFiles) {
|
|
156
|
+
truncated = true;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const absolutePath = cjoin(currentDir, entry.name);
|
|
161
|
+
const relativePath = path.relative(sourceRoot, absolutePath);
|
|
162
|
+
if (!relativePath || relativePath.startsWith('..')) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (entry.isDirectory()) {
|
|
167
|
+
if (shouldIgnoreSourceDirectory(entry.name)) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
directories.push(relativePath);
|
|
171
|
+
if (depth < maxDepth) {
|
|
172
|
+
await walk(absolutePath, depth + 1);
|
|
173
|
+
} else {
|
|
174
|
+
truncated = true;
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!entry.isFile() || !shouldInventorySourceFile(entry.name)) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let stats = null;
|
|
184
|
+
try {
|
|
185
|
+
stats = await fs.stat(absolutePath);
|
|
186
|
+
} catch {
|
|
187
|
+
stats = null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const language = sourceLanguage(relativePath);
|
|
191
|
+
languageBreakdown[language] = (languageBreakdown[language] ?? 0) + 1;
|
|
192
|
+
files.push({
|
|
193
|
+
path: relativePath,
|
|
194
|
+
kind: classifyInventoryFile(relativePath),
|
|
195
|
+
language,
|
|
196
|
+
sizeBytes: stats?.size ?? null,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await walk(sourceRoot, 0);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
version: 1,
|
|
205
|
+
generatedAt: timestamp(),
|
|
206
|
+
sourceRoot,
|
|
207
|
+
summary: {
|
|
208
|
+
files: files.length,
|
|
209
|
+
directories: directories.length,
|
|
210
|
+
truncated,
|
|
211
|
+
languageBreakdown,
|
|
212
|
+
topLevelDirectories: [...new Set(directories.map((dir) => dir.split(path.sep)[0]))].slice(0, 30),
|
|
213
|
+
},
|
|
214
|
+
files,
|
|
215
|
+
directories,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
export {
|
|
221
|
+
SOURCE_INVENTORY_IGNORE_DIRS,
|
|
222
|
+
collectSourceInventory,
|
|
223
|
+
shouldIgnoreSourceDirectory
|
|
224
|
+
};
|