vibepro 0.1.0-alpha.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.
Files changed (89) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +9 -0
  3. package/README.ja.md +448 -0
  4. package/README.md +520 -0
  5. package/agent-instructions/codex/AGENTS.vibepro.md +45 -0
  6. package/bin/vibepro.js +9 -0
  7. package/docs/assets/vibepro-header.png +0 -0
  8. package/package.json +51 -0
  9. package/skills/vibepro-diagnosis-packages/SKILL.md +133 -0
  10. package/skills/vibepro-human-review/SKILL.md +73 -0
  11. package/skills/vibepro-story-refactor/SKILL.md +89 -0
  12. package/skills/vibepro-workflow/SKILL.md +139 -0
  13. package/src/agent-harness-map.js +230 -0
  14. package/src/agent-harness-scanner.js +337 -0
  15. package/src/agent-review.js +2180 -0
  16. package/src/api-boundary-scanner.js +452 -0
  17. package/src/architecture-profiler.js +423 -0
  18. package/src/authorization-scoring.js +149 -0
  19. package/src/brainbase-importer.js +534 -0
  20. package/src/change-risk-classifier.js +195 -0
  21. package/src/check-packs.js +605 -0
  22. package/src/checkpoint-manager.js +233 -0
  23. package/src/cli.js +2213 -0
  24. package/src/code-quality-scanner.js +310 -0
  25. package/src/codex-manager.js +143 -0
  26. package/src/component-style-scanner.js +336 -0
  27. package/src/coverage-report.js +99 -0
  28. package/src/database-access-scanner.js +163 -0
  29. package/src/decision-records.js +315 -0
  30. package/src/design-modernize.js +1435 -0
  31. package/src/design-system.js +1732 -0
  32. package/src/diagnostic-engine.js +1945 -0
  33. package/src/diagram-requirement-resolver.js +194 -0
  34. package/src/doctor.js +677 -0
  35. package/src/environment-graph.js +424 -0
  36. package/src/execution-state.js +849 -0
  37. package/src/explore-evidence.js +425 -0
  38. package/src/flow-design-scanner.js +896 -0
  39. package/src/flow-verifier.js +887 -0
  40. package/src/gesture-interaction-scanner.js +330 -0
  41. package/src/graph-context.js +263 -0
  42. package/src/graphify-adapter.js +189 -0
  43. package/src/html-report.js +1035 -0
  44. package/src/journey-map.js +1299 -0
  45. package/src/language.js +48 -0
  46. package/src/lazy-pattern-detector.js +182 -0
  47. package/src/local-dev-scanner.js +135 -0
  48. package/src/managed-worktree-gate.js +187 -0
  49. package/src/managed-worktree.js +766 -0
  50. package/src/merge-manager.js +501 -0
  51. package/src/network-contract-scanner.js +442 -0
  52. package/src/nocodb-story-sync.js +386 -0
  53. package/src/oss-readiness-scanner.js +417 -0
  54. package/src/performance-evidence.js +756 -0
  55. package/src/performance-measurer.js +591 -0
  56. package/src/pr-manager.js +8220 -0
  57. package/src/presets.js +682 -0
  58. package/src/public-discovery-scanner.js +519 -0
  59. package/src/refactoring-delta-reporter.js +367 -0
  60. package/src/refactoring-opportunity-generator.js +797 -0
  61. package/src/regression-risk-scanner.js +146 -0
  62. package/src/repo-status.js +266 -0
  63. package/src/report-fingerprint.js +188 -0
  64. package/src/report-pr-body-prompt-template.md +108 -0
  65. package/src/report-pr-body-schema.json +95 -0
  66. package/src/report-store.js +135 -0
  67. package/src/report-validator.js +192 -0
  68. package/src/requirement-consistency.js +1066 -0
  69. package/src/runtime-info.js +134 -0
  70. package/src/self-dogfood-scanner.js +476 -0
  71. package/src/session-learning.js +164 -0
  72. package/src/skills-manager.js +157 -0
  73. package/src/spec-drift.js +378 -0
  74. package/src/spec-fingerprint.js +445 -0
  75. package/src/spec-prompt-template.md +155 -0
  76. package/src/spec-schema.json +219 -0
  77. package/src/spec-store.js +258 -0
  78. package/src/spec-validator.js +459 -0
  79. package/src/static-site-scanner.js +316 -0
  80. package/src/story-candidate-generator.js +85 -0
  81. package/src/story-catalog-generator.js +2813 -0
  82. package/src/story-html.js +156 -0
  83. package/src/story-manager.js +2144 -0
  84. package/src/story-task-generator.js +522 -0
  85. package/src/task-manager.js +1029 -0
  86. package/src/terminal-link-scanner.js +238 -0
  87. package/src/usage-report.js +417 -0
  88. package/src/verification-evidence.js +284 -0
  89. package/src/workspace.js +126 -0
package/src/doctor.js ADDED
@@ -0,0 +1,677 @@
1
+ import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { buildRuntimeDoctorCheck, collectRuntimeInfo } from './runtime-info.js';
5
+ import { getWorkspaceDir, MANIFEST_FILE, SCHEMA_VERSION, toWorkspaceRelative, writeManifest, WORKSPACE_DIR } from './workspace.js';
6
+
7
+ const REQUIRED_GITIGNORE_LINE = `${WORKSPACE_DIR}/`;
8
+
9
+ export async function runDoctor(repoRoot, options = {}) {
10
+ const root = path.resolve(repoRoot);
11
+ const workspaceDir = getWorkspaceDir(root);
12
+ const manifestPath = path.join(workspaceDir, MANIFEST_FILE);
13
+ const configPath = path.join(workspaceDir, 'config.json');
14
+ const result = {
15
+ schema_version: SCHEMA_VERSION,
16
+ generated_at: new Date().toISOString(),
17
+ mode: 'doctor',
18
+ fix: Boolean(options.fix),
19
+ workspace: {
20
+ initialized: false,
21
+ path: '.vibepro'
22
+ },
23
+ overall_status: 'pass',
24
+ checks: [],
25
+ repairs: [],
26
+ next_commands: [],
27
+ next_actions: [],
28
+ artifacts: {},
29
+ toolchain: await collectRuntimeInfo()
30
+ };
31
+
32
+ const manifest = await readJsonIfExists(manifestPath);
33
+ if (!manifest) {
34
+ result.overall_status = 'uninitialized';
35
+ result.checks.push({
36
+ id: 'VP-DOCTOR-UNINITIALIZED',
37
+ severity: 'info',
38
+ status: 'info',
39
+ fixable: false,
40
+ detail: '.vibepro workspaceが見つからない。',
41
+ recommendation: 'vibepro init を実行してworkspaceを作成する。',
42
+ next_actions: [buildAction({
43
+ command: `vibepro init ${root}`,
44
+ reason: '.vibepro workspace が存在しないため初期化する。',
45
+ expected_after: 'vibepro status が initialized: true を返す。',
46
+ safe_to_run: true
47
+ })]
48
+ });
49
+ result.checks.push(buildRuntimeDoctorCheck(result.toolchain));
50
+ applyNextActions(result);
51
+ return result;
52
+ }
53
+
54
+ result.workspace.initialized = true;
55
+ const config = await readJsonIfExists(configPath);
56
+ let manifestChanged = false;
57
+ let configChanged = false;
58
+
59
+ if (!config) {
60
+ result.checks.push({
61
+ id: 'VP-DOCTOR-MISSING-CONFIG',
62
+ severity: 'warning',
63
+ status: 'manual',
64
+ fixable: false,
65
+ detail: '.vibepro/config.json が見つからない。',
66
+ recommendation: 'vibepro init の初期化状態を確認し、必要ならconfigを復元する。',
67
+ next_actions: [
68
+ buildAction({
69
+ command: `vibepro init ${root}`,
70
+ reason: '.vibepro/config.json が欠けているため初期化状態を復元する。',
71
+ expected_after: '.vibepro/config.json が存在する。',
72
+ safe_to_run: true
73
+ }),
74
+ buildAction({
75
+ command: `vibepro doctor ${root}`,
76
+ reason: 'config復元後に管理情報の整合性を再点検する。',
77
+ expected_after: 'VP-DOCTOR-MISSING-CONFIG が消える。',
78
+ safe_to_run: true
79
+ })
80
+ ]
81
+ });
82
+ }
83
+
84
+ const missingEvidence = await findMissingEvidenceRuns(root, manifest);
85
+ if (missingEvidence.length > 0) {
86
+ result.checks.push({
87
+ id: 'VP-DOCTOR-MISSING-EVIDENCE',
88
+ severity: 'warning',
89
+ status: options.fix ? 'fixed' : 'fixable',
90
+ fixable: true,
91
+ detail: `${missingEvidence.length} 件の診断runが存在しないevidenceを参照している。`,
92
+ recommendation: 'run成果物を復元するか、不要なrun参照をmanifestから整理する。',
93
+ next_actions: [buildAction({
94
+ command: `vibepro doctor ${root} --fix`,
95
+ reason: '存在しない evidence を参照する診断runを管理目録から整理する。',
96
+ expected_after: 'VP-DOCTOR-MISSING-EVIDENCE が消える。',
97
+ safe_to_run: true
98
+ })],
99
+ items: missingEvidence
100
+ });
101
+ }
102
+
103
+ if (options.fix && missingEvidence.length > 0) {
104
+ const repair = removeMissingEvidenceRuns(manifest, missingEvidence);
105
+ manifestChanged = true;
106
+ result.repairs.push(repair);
107
+ }
108
+
109
+ if (config) {
110
+ const missingCurrentStory = findMissingCurrentStory(config);
111
+ if (missingCurrentStory) {
112
+ result.checks.push({
113
+ id: 'VP-DOCTOR-CURRENT-STORY-MISSING',
114
+ severity: 'warning',
115
+ status: options.fix ? 'fixed' : 'fixable',
116
+ fixable: true,
117
+ detail: `current_story_id が存在しないStoryを参照している: ${missingCurrentStory.story_id}`,
118
+ recommendation: '存在するactive Storyを選択し直すか、不要なcurrent_story_idを解除する。',
119
+ next_actions: [buildAction({
120
+ command: `vibepro doctor ${root} --fix`,
121
+ reason: '存在しない current_story_id を解除する。',
122
+ expected_after: 'VP-DOCTOR-CURRENT-STORY-MISSING が消える。',
123
+ safe_to_run: true
124
+ })],
125
+ items: [missingCurrentStory]
126
+ });
127
+ if (options.fix) {
128
+ config.brainbase.current_story_id = null;
129
+ configChanged = true;
130
+ result.repairs.push({
131
+ id: 'clear-missing-current-story',
132
+ detail: '存在しないcurrent_story_idを解除した。',
133
+ story_id: missingCurrentStory.story_id
134
+ });
135
+ }
136
+ }
137
+ }
138
+
139
+ const staleLatestRunRefs = findStaleLatestRunRefs(manifest);
140
+ if (staleLatestRunRefs.length > 0) {
141
+ result.checks.push({
142
+ id: 'VP-DOCTOR-STALE-LATEST-RUN-REFS',
143
+ severity: 'warning',
144
+ status: options.fix ? 'fixed' : 'fixable',
145
+ fixable: true,
146
+ detail: `${staleLatestRunRefs.length} 件のlatest run参照が存在しないrunを指している。`,
147
+ recommendation: '不要なlatest_run参照をmanifestから整理する。',
148
+ next_actions: [buildAction({
149
+ command: `vibepro doctor ${root} --fix`,
150
+ reason: '存在しないrunを指す latest_run 参照を解除する。',
151
+ expected_after: 'VP-DOCTOR-STALE-LATEST-RUN-REFS が消える。',
152
+ safe_to_run: true
153
+ })],
154
+ items: staleLatestRunRefs
155
+ });
156
+ if (options.fix) {
157
+ result.repairs.push(removeStaleLatestRunRefs(manifest, staleLatestRunRefs));
158
+ manifestChanged = true;
159
+ }
160
+ }
161
+
162
+ const missingGraphifyArtifacts = await findMissingArtifactRefs(root, manifest, [
163
+ 'graphify_json',
164
+ 'graphify_report'
165
+ ]);
166
+ if (missingGraphifyArtifacts.length > 0) {
167
+ result.checks.push({
168
+ id: 'VP-DOCTOR-MISSING-GRAPHIFY-ARTIFACTS',
169
+ severity: 'warning',
170
+ status: options.fix ? 'fixed' : 'fixable',
171
+ fixable: true,
172
+ detail: `${missingGraphifyArtifacts.length} 件のgraphify成果物参照が存在しないファイルを指している。`,
173
+ recommendation: 'vibepro graph または vibepro story derive --run-graphify を実行してGraph成果物を作り直す。',
174
+ next_actions: [
175
+ buildAction({
176
+ command: `vibepro doctor ${root} --fix`,
177
+ reason: '存在しないgraphify成果物参照を管理目録から解除する。',
178
+ expected_after: '欠けたgraphify artifact参照がmanifestから消える。',
179
+ safe_to_run: true
180
+ }),
181
+ buildAction({
182
+ command: `vibepro story derive ${root} --run-graphify`,
183
+ reason: 'Graphを再生成してStory Mapの根拠を更新する。',
184
+ expected_after: '.vibepro/graphify/graph.json と story-catalog.json が更新される。',
185
+ safe_to_run: true
186
+ })
187
+ ],
188
+ items: missingGraphifyArtifacts
189
+ });
190
+ if (options.fix) {
191
+ result.repairs.push(removeMissingArtifactRefs(manifest, missingGraphifyArtifacts));
192
+ manifestChanged = true;
193
+ }
194
+ }
195
+
196
+ const storyCatalogDrift = config ? await findStoryCatalogDrift(root, config) : null;
197
+ if (storyCatalogDrift && (storyCatalogDrift.missing_in_config.length > 0 || storyCatalogDrift.stale_derived_config.length > 0)) {
198
+ result.checks.push({
199
+ id: 'VP-DOCTOR-STORY-CATALOG-DRIFT',
200
+ severity: 'warning',
201
+ status: options.fix ? 'fixed' : 'fixable',
202
+ fixable: true,
203
+ detail: `Story catalog と config stories に差分がある。missing=${storyCatalogDrift.missing_in_config.length}, stale=${storyCatalogDrift.stale_derived_config.length}`,
204
+ recommendation: 'vibepro story derive を再実行するか、config storiesをcatalogに合わせて整理する。',
205
+ next_actions: [
206
+ buildAction({
207
+ command: `vibepro doctor ${root} --fix`,
208
+ reason: 'Story catalog と config stories の差分を管理情報上で整理する。',
209
+ expected_after: 'VP-DOCTOR-STORY-CATALOG-DRIFT が消える。',
210
+ safe_to_run: true
211
+ }),
212
+ buildAction({
213
+ command: `vibepro story derive ${root}`,
214
+ reason: 'Story Mapを再生成して派生Storyの正本を更新する。',
215
+ expected_after: 'story-catalog.json と config stories が揃う。',
216
+ safe_to_run: true
217
+ })
218
+ ],
219
+ items: storyCatalogDrift
220
+ });
221
+ if (options.fix) {
222
+ result.repairs.push(applyStoryCatalogDriftFix(config, storyCatalogDrift));
223
+ configChanged = true;
224
+ }
225
+ }
226
+
227
+ const gitignoreState = await checkGitignore(root);
228
+ if (gitignoreState.needs_update) {
229
+ result.checks.push({
230
+ id: 'VP-DOCTOR-GITIGNORE-MISSING',
231
+ severity: 'warning',
232
+ status: options.fix ? 'fixed' : 'fixable',
233
+ fixable: true,
234
+ detail: gitignoreState.exists
235
+ ? `.gitignore に ${REQUIRED_GITIGNORE_LINE} が含まれていない。`
236
+ : '.gitignore が存在せず .vibepro/ が無視されない。',
237
+ recommendation: 'vibepro doctor --fix または vibepro init で .gitignore に .vibepro/ を追記する。',
238
+ next_actions: [buildAction({
239
+ command: `vibepro doctor ${root} --fix`,
240
+ reason: '.vibepro/ が git に含まれないように .gitignore を更新する。',
241
+ expected_after: 'VP-DOCTOR-GITIGNORE-MISSING が消える。',
242
+ safe_to_run: true
243
+ })],
244
+ items: [{ path: '.gitignore', required_line: REQUIRED_GITIGNORE_LINE }]
245
+ });
246
+ if (options.fix) {
247
+ await applyGitignoreFix(root, gitignoreState);
248
+ result.repairs.push({
249
+ id: 'ensure-gitignore-vibepro',
250
+ detail: `.gitignore に ${REQUIRED_GITIGNORE_LINE} を追記した。`,
251
+ path: '.gitignore'
252
+ });
253
+ }
254
+ }
255
+
256
+ const missingTaskRefs = await findMissingTaskWorkflowRefs(root);
257
+ if (missingTaskRefs.length > 0) {
258
+ result.checks.push({
259
+ id: 'VP-DOCTOR-MISSING-TASK-WORKFLOW-REFS',
260
+ severity: 'warning',
261
+ status: 'manual',
262
+ fixable: false,
263
+ detail: `${missingTaskRefs.length} 件のtask workflow成果物が存在しない参照を持っている。`,
264
+ recommendation: '該当taskで vibepro task brief / plan / handoff / execute を再実行する。',
265
+ next_actions: buildTaskWorkflowRepairActions(root, missingTaskRefs),
266
+ items: missingTaskRefs
267
+ });
268
+ }
269
+
270
+ if (configChanged) await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
271
+ if (manifestChanged) await writeManifest(root, manifest);
272
+
273
+ result.checks.push(buildRuntimeDoctorCheck(result.toolchain));
274
+ result.overall_status = resolveDoctorStatus(result);
275
+ applyNextActions(result);
276
+ if (options.writeArtifacts !== false) await writeDoctorArtifact(root, result);
277
+ return result;
278
+ }
279
+
280
+ export function renderDoctor(result) {
281
+ const checks = result.checks.length === 0
282
+ ? '- なし'
283
+ : result.checks.map((check) => `- ${check.id}: ${check.status} - ${check.detail}`).join('\n');
284
+ const repairs = result.repairs.length === 0
285
+ ? '- なし'
286
+ : result.repairs.map((repair) => `- ${repair.id}: ${repair.detail}`).join('\n');
287
+ const nextCommands = result.next_commands.length === 0
288
+ ? '- なし'
289
+ : result.next_actions.map((action) => `- \`${action.command}\`
290
+ - reason: ${action.reason}
291
+ - expected: ${action.expected_after}
292
+ - safe_to_run: ${action.safe_to_run}`).join('\n');
293
+ return `# VibePro Doctor
294
+
295
+ | 項目 | 内容 |
296
+ |------|------|
297
+ | Initialized | ${result.workspace.initialized ? 'yes' : 'no'} |
298
+ | Overall | ${result.overall_status} |
299
+ | Fix | ${result.fix ? 'yes' : 'no'} |
300
+
301
+ ## Checks
302
+
303
+ ${checks}
304
+
305
+ ## Repairs
306
+
307
+ ${repairs}
308
+
309
+ ## Next Commands
310
+
311
+ ${nextCommands}
312
+ `;
313
+ }
314
+
315
+ async function checkGitignore(repoRoot) {
316
+ const gitignorePath = path.join(repoRoot, '.gitignore');
317
+ let content = null;
318
+ try {
319
+ content = await readFile(gitignorePath, 'utf8');
320
+ } catch (error) {
321
+ if (error.code !== 'ENOENT') throw error;
322
+ }
323
+ if (content === null) {
324
+ return { exists: false, content: '', needs_update: true };
325
+ }
326
+ const lines = content.split(/\r?\n/).map((line) => line.trim());
327
+ const hasRequired = lines.includes(REQUIRED_GITIGNORE_LINE);
328
+ return { exists: true, content, needs_update: !hasRequired };
329
+ }
330
+
331
+ async function applyGitignoreFix(repoRoot, state) {
332
+ const gitignorePath = path.join(repoRoot, '.gitignore');
333
+ const existing = state.content ?? '';
334
+ const prefix = existing.trim().length > 0 ? `${existing.trimEnd()}\n` : '';
335
+ await writeFile(gitignorePath, `${prefix}${REQUIRED_GITIGNORE_LINE}\n`);
336
+ }
337
+
338
+ async function readJsonIfExists(filePath) {
339
+ try {
340
+ return JSON.parse(await readFile(filePath, 'utf8'));
341
+ } catch (error) {
342
+ if (error.code === 'ENOENT') return null;
343
+ throw error;
344
+ }
345
+ }
346
+
347
+ async function findMissingEvidenceRuns(repoRoot, manifest) {
348
+ const runs = Array.isArray(manifest.runs) ? manifest.runs : [];
349
+ const missing = [];
350
+ for (const run of runs) {
351
+ const evidencePath = run.artifacts?.evidence;
352
+ if (!evidencePath) continue;
353
+ if (!await fileExists(path.resolve(repoRoot, evidencePath))) {
354
+ missing.push({
355
+ run_id: run.run_id,
356
+ story_id: run.story_id ?? null,
357
+ path: evidencePath
358
+ });
359
+ }
360
+ }
361
+ return missing;
362
+ }
363
+
364
+ async function fileExists(filePath) {
365
+ try {
366
+ await stat(filePath);
367
+ return true;
368
+ } catch (error) {
369
+ if (error.code === 'ENOENT') return false;
370
+ throw error;
371
+ }
372
+ }
373
+
374
+ function removeMissingEvidenceRuns(manifest, missingEvidence) {
375
+ const missingIds = new Set(missingEvidence.map((item) => item.run_id));
376
+ const beforeCount = Array.isArray(manifest.runs) ? manifest.runs.length : 0;
377
+ manifest.runs = (manifest.runs ?? []).filter((run) => !missingIds.has(run.run_id));
378
+ if (missingIds.has(manifest.latest_run)) {
379
+ manifest.latest_run = manifest.runs[0]?.run_id ?? null;
380
+ }
381
+ if (manifest.latest_run_by_story) {
382
+ manifest.latest_run_by_story = Object.fromEntries(Object.entries(manifest.latest_run_by_story)
383
+ .filter(([, runId]) => !missingIds.has(runId)));
384
+ }
385
+ return {
386
+ id: 'remove-missing-evidence-runs',
387
+ detail: `${beforeCount - manifest.runs.length} 件の欠けた診断run参照をmanifestから除去した。`,
388
+ removed_run_ids: [...missingIds]
389
+ };
390
+ }
391
+
392
+ function findMissingCurrentStory(config) {
393
+ const storyId = config.brainbase?.current_story_id ?? null;
394
+ if (!storyId) return null;
395
+ const stories = Array.isArray(config.brainbase?.stories) ? config.brainbase.stories : [];
396
+ const exists = stories.some((story) => story.story_id === storyId && story.status !== 'archived');
397
+ return exists ? null : { story_id: storyId };
398
+ }
399
+
400
+ function findStaleLatestRunRefs(manifest) {
401
+ const runs = Array.isArray(manifest.runs) ? manifest.runs : [];
402
+ const runIds = new Set(runs.map((run) => run.run_id));
403
+ const stale = [];
404
+ if (manifest.latest_run && !runIds.has(manifest.latest_run)) {
405
+ stale.push({ field: 'latest_run', run_id: manifest.latest_run });
406
+ }
407
+ for (const [storyId, runId] of Object.entries(manifest.latest_run_by_story ?? {})) {
408
+ if (!runIds.has(runId)) {
409
+ stale.push({ field: 'latest_run_by_story', story_id: storyId, run_id: runId });
410
+ }
411
+ }
412
+ return stale;
413
+ }
414
+
415
+ function removeStaleLatestRunRefs(manifest, staleRefs) {
416
+ const staleLatest = staleRefs.find((item) => item.field === 'latest_run');
417
+ if (staleLatest) manifest.latest_run = manifest.runs?.[0]?.run_id ?? null;
418
+ const staleStories = new Set(staleRefs
419
+ .filter((item) => item.field === 'latest_run_by_story')
420
+ .map((item) => item.story_id));
421
+ if (manifest.latest_run_by_story && staleStories.size > 0) {
422
+ manifest.latest_run_by_story = Object.fromEntries(Object.entries(manifest.latest_run_by_story)
423
+ .filter(([storyId]) => !staleStories.has(storyId)));
424
+ }
425
+ return {
426
+ id: 'remove-stale-latest-run-refs',
427
+ detail: `${staleRefs.length} 件の存在しないlatest run参照をmanifestから除去した。`,
428
+ removed_refs: staleRefs
429
+ };
430
+ }
431
+
432
+ async function findMissingArtifactRefs(repoRoot, manifest, artifactKeys) {
433
+ const artifacts = manifest.artifacts ?? {};
434
+ const missing = [];
435
+ for (const key of artifactKeys) {
436
+ const artifactPath = artifacts[key];
437
+ if (!artifactPath) continue;
438
+ if (!await fileExists(path.resolve(repoRoot, artifactPath))) {
439
+ missing.push({ key, path: artifactPath });
440
+ }
441
+ }
442
+ return missing;
443
+ }
444
+
445
+ function removeMissingArtifactRefs(manifest, missingArtifacts) {
446
+ for (const item of missingArtifacts) {
447
+ delete manifest.artifacts?.[item.key];
448
+ }
449
+ return {
450
+ id: 'remove-missing-artifact-refs',
451
+ detail: `${missingArtifacts.length} 件の存在しないartifact参照をmanifestから除去した。`,
452
+ removed_artifacts: missingArtifacts
453
+ };
454
+ }
455
+
456
+ async function findStoryCatalogDrift(repoRoot, config) {
457
+ const catalogPath = path.join(getWorkspaceDir(repoRoot), 'stories', 'story-catalog.json');
458
+ const catalog = await readJsonIfExists(catalogPath);
459
+ if (!catalog) return null;
460
+ const catalogStories = Array.isArray(catalog.stories) ? catalog.stories : [];
461
+ const configStories = Array.isArray(config.brainbase?.stories) ? config.brainbase.stories : [];
462
+ const configIds = new Set(configStories.map((story) => story.story_id));
463
+ const catalogIds = new Set(catalogStories.map((story) => story.story_id));
464
+ return {
465
+ catalog: toWorkspaceRelative(repoRoot, catalogPath),
466
+ missing_in_config: catalogStories
467
+ .filter((story) => !configIds.has(story.story_id))
468
+ .map(toConfigStory),
469
+ stale_derived_config: configStories
470
+ .filter((story) => story.status !== 'archived')
471
+ .filter((story) => story.derived_by === 'vibepro-story-derive')
472
+ .filter((story) => !catalogIds.has(story.story_id))
473
+ .map((story) => ({ story_id: story.story_id, title: story.title }))
474
+ };
475
+ }
476
+
477
+ function applyStoryCatalogDriftFix(config, drift) {
478
+ const stories = Array.isArray(config.brainbase?.stories) ? config.brainbase.stories : [];
479
+ const staleIds = new Set(drift.stale_derived_config.map((story) => story.story_id));
480
+ for (const story of stories) {
481
+ if (staleIds.has(story.story_id)) story.status = 'archived';
482
+ }
483
+ config.brainbase = {
484
+ ...(config.brainbase ?? {}),
485
+ stories: [...stories, ...drift.missing_in_config]
486
+ };
487
+ if (staleIds.has(config.brainbase.current_story_id)) {
488
+ config.brainbase.current_story_id = null;
489
+ }
490
+ return {
491
+ id: 'sync-story-catalog-config',
492
+ detail: `catalogから ${drift.missing_in_config.length} Storyをconfigへ追加し、${drift.stale_derived_config.length} Storyをarchivedにした。`,
493
+ added_story_ids: drift.missing_in_config.map((story) => story.story_id),
494
+ archived_story_ids: drift.stale_derived_config.map((story) => story.story_id)
495
+ };
496
+ }
497
+
498
+ function toConfigStory(story) {
499
+ return {
500
+ story_id: story.story_id,
501
+ title: story.title,
502
+ ssot: story.ssot ?? 'local',
503
+ status: story.status ?? 'active',
504
+ horizon: story.horizon ?? null,
505
+ view: story.view ?? null,
506
+ period: story.period ?? null,
507
+ started_at: story.started_at ?? null,
508
+ due_at: story.due_at ?? null,
509
+ category: story.category ?? null,
510
+ derived_by: 'vibepro-story-derive'
511
+ };
512
+ }
513
+
514
+ async function findMissingTaskWorkflowRefs(repoRoot) {
515
+ const storiesDir = path.join(getWorkspaceDir(repoRoot), 'stories');
516
+ const storyDirs = await readDirectories(storiesDir);
517
+ const missing = [];
518
+ for (const storyId of storyDirs) {
519
+ const tasksPath = path.join(storiesDir, storyId, 'tasks', 'tasks.json');
520
+ const taskState = await readJsonIfExists(tasksPath);
521
+ const tasks = Array.isArray(taskState?.tasks) ? taskState.tasks : [];
522
+ for (const task of tasks) {
523
+ const taskDir = path.join(storiesDir, storyId, 'tasks', safeSegment(task.id));
524
+ missing.push(...await findMissingWorkflowRefsInDir(repoRoot, storyId, task.id, null, taskDir));
525
+ const groupsDir = path.join(taskDir, 'groups');
526
+ for (const groupId of await readDirectories(groupsDir)) {
527
+ missing.push(...await findMissingWorkflowRefsInDir(repoRoot, storyId, task.id, groupId, path.join(groupsDir, groupId)));
528
+ }
529
+ }
530
+ }
531
+ return missing;
532
+ }
533
+
534
+ async function findMissingWorkflowRefsInDir(repoRoot, storyId, taskId, groupId, workflowDir) {
535
+ const missing = [];
536
+ const handoffPath = path.join(workflowDir, 'handoff.json');
537
+ const handoff = await readJsonIfExists(handoffPath);
538
+ if (handoff) {
539
+ missing.push(...await findMissingReferencePaths(repoRoot, handoff.references, {
540
+ source: toWorkspaceRelative(repoRoot, handoffPath),
541
+ story_id: storyId,
542
+ task_id: taskId,
543
+ group_id: groupId,
544
+ artifact: 'handoff',
545
+ repair_command: buildTaskWorkflowRepairCommand(repoRoot, storyId, taskId, groupId, 'handoff')
546
+ }));
547
+ }
548
+ const executionPath = path.join(workflowDir, 'execution.json');
549
+ const execution = await readJsonIfExists(executionPath);
550
+ if (execution) {
551
+ missing.push(...await findMissingReferencePaths(repoRoot, execution.references, {
552
+ source: toWorkspaceRelative(repoRoot, executionPath),
553
+ story_id: storyId,
554
+ task_id: taskId,
555
+ group_id: groupId,
556
+ artifact: 'execution',
557
+ repair_command: buildTaskWorkflowRepairCommand(repoRoot, storyId, taskId, groupId, 'execute')
558
+ }));
559
+ }
560
+ return missing;
561
+ }
562
+
563
+ async function findMissingReferencePaths(repoRoot, references = {}, context) {
564
+ const missing = [];
565
+ for (const [key, referencePath] of Object.entries(references ?? {})) {
566
+ if (!referencePath || !key.endsWith('_json') && !key.endsWith('_markdown')) continue;
567
+ if (!await fileExists(path.resolve(repoRoot, referencePath))) {
568
+ missing.push({ ...context, key, path: referencePath });
569
+ }
570
+ }
571
+ return missing;
572
+ }
573
+
574
+ async function readDirectories(dirPath) {
575
+ try {
576
+ const entries = await readdir(dirPath, { withFileTypes: true });
577
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
578
+ } catch (error) {
579
+ if (error.code === 'ENOENT') return [];
580
+ throw error;
581
+ }
582
+ }
583
+
584
+ function safeSegment(value) {
585
+ return String(value ?? '').replace(/[\\/]/g, '_');
586
+ }
587
+
588
+ function buildTaskWorkflowRepairCommand(repoRoot, storyId, taskId, groupId, artifact) {
589
+ const subcommand = artifact === 'execution' ? 'execute' : 'handoff';
590
+ return [
591
+ 'vibepro task',
592
+ subcommand,
593
+ repoRoot,
594
+ '--task',
595
+ taskId,
596
+ '--id',
597
+ storyId,
598
+ groupId ? `--group ${groupId}` : null
599
+ ].filter(Boolean).join(' ');
600
+ }
601
+
602
+ function buildTaskWorkflowRepairCommands(repoRoot, missingTaskRefs) {
603
+ const commands = uniqueStrings(missingTaskRefs.map((item) => item.repair_command).filter(Boolean));
604
+ if (commands.length > 0) return commands;
605
+ const fallback = missingTaskRefs[0];
606
+ if (!fallback) return [];
607
+ return [buildTaskWorkflowRepairCommand(repoRoot, fallback.story_id, fallback.task_id, fallback.group_id, fallback.artifact)];
608
+ }
609
+
610
+ function buildTaskWorkflowRepairActions(repoRoot, missingTaskRefs) {
611
+ return buildTaskWorkflowRepairCommands(repoRoot, missingTaskRefs).map((command) => buildAction({
612
+ command,
613
+ reason: 'task workflow成果物が参照する briefing / plan / handoff が欠けている。',
614
+ expected_after: 'VP-DOCTOR-MISSING-TASK-WORKFLOW-REFS が消える。',
615
+ safe_to_run: true
616
+ }));
617
+ }
618
+
619
+ function applyNextActions(result) {
620
+ const actions = [];
621
+ for (const check of result.checks) {
622
+ for (const action of check.next_actions ?? []) {
623
+ actions.push(action);
624
+ }
625
+ }
626
+ if (result.workspace.initialized && result.overall_status === 'needs_maintenance' && actions.length === 0) {
627
+ actions.push(buildAction({
628
+ command: 'vibepro doctor <repo> --fix',
629
+ reason: '管理情報に修復可能な不整合がある。',
630
+ expected_after: 'doctor の fixable なチェックが消える。',
631
+ safe_to_run: true
632
+ }));
633
+ }
634
+ result.next_actions = uniqueActions(actions);
635
+ result.next_commands = result.next_actions.map((action) => action.command);
636
+ }
637
+
638
+ function buildAction({ command, reason, expected_after, safe_to_run }) {
639
+ return { command, reason, expected_after, safe_to_run };
640
+ }
641
+
642
+ function uniqueActions(actions) {
643
+ const seen = new Set();
644
+ const unique = [];
645
+ for (const action of actions) {
646
+ if (seen.has(action.command)) continue;
647
+ seen.add(action.command);
648
+ unique.push(action);
649
+ }
650
+ return unique;
651
+ }
652
+
653
+ function uniqueStrings(items) {
654
+ return [...new Set(items)];
655
+ }
656
+
657
+ function resolveDoctorStatus(result) {
658
+ if (!result.workspace.initialized) return 'uninitialized';
659
+ if (result.checks.some((check) => check.status === 'fixable')) return 'needs_maintenance';
660
+ if (result.checks.some((check) => check.status === 'manual')) return 'needs_maintenance';
661
+ if (result.checks.some((check) => check.status === 'fixed')) return 'fixed';
662
+ return 'pass';
663
+ }
664
+
665
+ async function writeDoctorArtifact(repoRoot, result) {
666
+ if (!result.workspace.initialized) return;
667
+ const doctorDir = path.join(getWorkspaceDir(repoRoot), 'doctor');
668
+ await mkdir(doctorDir, { recursive: true });
669
+ const jsonPath = path.join(doctorDir, 'doctor-result.json');
670
+ const markdownPath = path.join(doctorDir, 'doctor-result.md');
671
+ result.artifacts = {
672
+ json: toWorkspaceRelative(repoRoot, jsonPath),
673
+ markdown: toWorkspaceRelative(repoRoot, markdownPath)
674
+ };
675
+ await writeFile(jsonPath, `${JSON.stringify(result, null, 2)}\n`);
676
+ await writeFile(markdownPath, renderDoctor(result));
677
+ }