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
@@ -0,0 +1,134 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { promisify } from 'node:util';
6
+
7
+ const execFileAsync = promisify(execFile);
8
+
9
+ export async function collectRuntimeInfo(options = {}) {
10
+ const packageRoot = options.packageRoot ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
11
+ const packageJson = await readPackageJson(packageRoot);
12
+ const git = await collectRuntimeGitInfo(packageRoot);
13
+ return {
14
+ schema_version: '0.1.0',
15
+ collected_at: new Date().toISOString(),
16
+ package: {
17
+ name: packageJson?.name ?? 'vibepro',
18
+ version: packageJson?.version ?? 'unknown',
19
+ root: packageRoot
20
+ },
21
+ cli: {
22
+ entrypoint: process.argv[1] ? path.resolve(process.argv[1]) : null,
23
+ runtime_module: fileURLToPath(import.meta.url)
24
+ },
25
+ source_git: git
26
+ };
27
+ }
28
+
29
+ export function buildRuntimeDoctorCheck(runtime) {
30
+ const git = runtime.source_git;
31
+ if (!git?.is_git_repo) {
32
+ return {
33
+ id: 'VP-DOCTOR-CLI-RUNTIME',
34
+ severity: 'info',
35
+ status: 'info',
36
+ fixable: false,
37
+ detail: `VibePro runtime: ${runtime.package.name}@${runtime.package.version} (${runtime.package.root})`,
38
+ recommendation: 'package環境のため、git commit比較は実行しない。',
39
+ items: [runtime],
40
+ next_actions: []
41
+ };
42
+ }
43
+
44
+ const stale = git.origin_main_relation === 'behind';
45
+ return {
46
+ id: 'VP-DOCTOR-CLI-RUNTIME',
47
+ severity: stale ? 'warning' : 'info',
48
+ status: stale ? 'manual' : 'info',
49
+ fixable: false,
50
+ detail: stale
51
+ ? `VibePro runtime HEAD ${shortSha(git.commit)} が origin/main ${shortSha(git.origin_main_commit)} と一致しない。`
52
+ : `VibePro runtime HEAD ${shortSha(git.commit)} を使用中。`,
53
+ recommendation: stale
54
+ ? 'CLI実体のcheckoutを最新のorigin/mainへ合わせるか、意図したcommitであることを確認する。'
55
+ : 'VibePro CLI実体はorigin/mainと一致している。',
56
+ items: [runtime],
57
+ next_actions: stale ? [{
58
+ command: `git -C ${runtime.package.root} switch --detach origin/main`,
59
+ reason: 'VibePro CLI実体をorigin/mainへ合わせる。',
60
+ expected_after: 'VP-DOCTOR-CLI-RUNTIME が stale warning を出さない。',
61
+ safe_to_run: false
62
+ }] : []
63
+ };
64
+ }
65
+
66
+ function shortSha(value) {
67
+ return value ? String(value).slice(0, 12) : 'unknown';
68
+ }
69
+
70
+ async function readPackageJson(packageRoot) {
71
+ try {
72
+ return JSON.parse(await readFile(path.join(packageRoot, 'package.json'), 'utf8'));
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ async function collectRuntimeGitInfo(packageRoot) {
79
+ const inside = await gitOptional(packageRoot, ['rev-parse', '--is-inside-work-tree']);
80
+ if (inside !== 'true') {
81
+ return {
82
+ is_git_repo: false,
83
+ commit: null,
84
+ branch: null,
85
+ origin_url: null,
86
+ origin_main_commit: null,
87
+ dirty: null
88
+ };
89
+ }
90
+ const commit = await gitOptional(packageRoot, ['rev-parse', 'HEAD']);
91
+ const branch = await gitOptional(packageRoot, ['branch', '--show-current']);
92
+ const originUrl = await gitOptional(packageRoot, ['config', '--get', 'remote.origin.url']);
93
+ const originMainCommit = await gitOptional(packageRoot, ['rev-parse', 'origin/main']);
94
+ const originMainRelation = await resolveOriginMainRelation(packageRoot, commit, originMainCommit);
95
+ const porcelain = await gitOptional(packageRoot, ['status', '--porcelain']);
96
+ return {
97
+ is_git_repo: true,
98
+ commit: commit || null,
99
+ branch: branch || null,
100
+ origin_url: originUrl || null,
101
+ origin_main_commit: originMainCommit || null,
102
+ origin_main_relation: originMainRelation,
103
+ dirty: Boolean(porcelain),
104
+ dirty_summary: porcelain ? porcelain.split('\n').filter(Boolean).slice(0, 20) : []
105
+ };
106
+ }
107
+
108
+ async function resolveOriginMainRelation(packageRoot, commit, originMainCommit) {
109
+ if (!commit || !originMainCommit) return null;
110
+ if (commit === originMainCommit) return 'same';
111
+ const headBehindOriginMain = await gitExitCode(packageRoot, ['merge-base', '--is-ancestor', commit, originMainCommit]);
112
+ if (headBehindOriginMain === 0) return 'behind';
113
+ const originMainBehindHead = await gitExitCode(packageRoot, ['merge-base', '--is-ancestor', originMainCommit, commit]);
114
+ if (originMainBehindHead === 0) return 'ahead';
115
+ return 'diverged';
116
+ }
117
+
118
+ async function gitOptional(cwd, args) {
119
+ try {
120
+ const { stdout } = await execFileAsync('git', args, { cwd });
121
+ return stdout.trim();
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+
127
+ async function gitExitCode(cwd, args) {
128
+ try {
129
+ await execFileAsync('git', args, { cwd });
130
+ return 0;
131
+ } catch (error) {
132
+ return typeof error.code === 'number' ? error.code : 1;
133
+ }
134
+ }
@@ -0,0 +1,476 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { access, readdir, readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { promisify } from 'node:util';
5
+
6
+ import { getWorkspaceDir, toWorkspaceRelative } from './workspace.js';
7
+
8
+ const execFileAsync = promisify(execFile);
9
+
10
+ const TEXT_TARGETS = [
11
+ 'docs',
12
+ 'skills',
13
+ 'agent-instructions',
14
+ '.github'
15
+ ];
16
+
17
+ export async function scanSelfDogfood(root, options = {}) {
18
+ const repoRoot = path.resolve(root);
19
+ const workspaceDir = getWorkspaceDir(repoRoot);
20
+ const storyFindings = await scanStoryGateArtifacts(repoRoot, workspaceDir, {
21
+ storyId: options.storyId
22
+ });
23
+ const instructionFindings = await scanInstructionBypassLanguage(repoRoot, {
24
+ storyId: options.storyId
25
+ });
26
+ const languageFindings = await scanHumanArtifactLanguage(repoRoot, workspaceDir, {
27
+ storyId: options.storyId
28
+ });
29
+ const githubPrFindings = await scanCurrentGitHubPr(repoRoot, workspaceDir, {
30
+ storyId: options.storyId,
31
+ env: options.env
32
+ });
33
+ const findings = [...storyFindings, ...instructionFindings, ...languageFindings, ...githubPrFindings];
34
+ const riskSummary = summarizeFindings(findings);
35
+ return {
36
+ schema_version: '0.1.0',
37
+ status: riskSummary.block > 0 ? 'fail' : riskSummary.review > 0 ? 'needs_review' : 'pass',
38
+ summary: {
39
+ findings: findings.length,
40
+ block: riskSummary.block,
41
+ review: riskSummary.review,
42
+ info: riskSummary.info
43
+ },
44
+ findings,
45
+ risk_summary: {
46
+ findings: riskSummary
47
+ }
48
+ };
49
+ }
50
+
51
+ async function scanHumanArtifactLanguage(repoRoot, workspaceDir, options = {}) {
52
+ const config = await readJson(path.join(workspaceDir, 'config.json'));
53
+ const language = config?.output?.language ?? 'ja';
54
+ if (language !== 'ja') return [];
55
+ if (!(await exists(workspaceDir))) return [];
56
+ const files = await collectHumanArtifactFiles(workspaceDir);
57
+ const scopedFiles = options.storyId
58
+ ? files.filter((filePath) => filePath.includes(`${path.sep}${options.storyId}${path.sep}`) || filePath.includes(`${path.sep}${options.storyId}.`))
59
+ : files;
60
+ const findings = [];
61
+ for (const filePath of scopedFiles) {
62
+ const content = await readFile(filePath, 'utf8').catch(() => '');
63
+ const matches = ENGLISH_FIXED_TEXT_PATTERNS
64
+ .filter((item) => item.pattern.test(content))
65
+ .map((item) => item.label);
66
+ if (matches.length === 0) continue;
67
+ findings.push({
68
+ id: `self_dogfood.human_doc_language.${sanitizeFindingId(toWorkspaceRelative(repoRoot, filePath))}`,
69
+ severity: 'medium',
70
+ gate_effect: 'review',
71
+ story_id: options.storyId ?? null,
72
+ path: toWorkspaceRelative(repoRoot, filePath),
73
+ detail: `Human-facing artifact was generated in a ja workspace but still contains fixed English text: ${matches.slice(0, 5).join(', ')}.`,
74
+ required_action: 'Route this artifact renderer through output.language and localized fixed labels, or mark the detected text as machine-readable/externally sourced.'
75
+ });
76
+ if (findings.length >= 25) break;
77
+ }
78
+ return findings;
79
+ }
80
+
81
+ const ENGLISH_FIXED_TEXT_PATTERNS = [
82
+ { label: 'VibePro Agent Review Request', pattern: /^# VibePro Agent Review Request/m },
83
+ { label: 'VibePro Parallel Agent Review Dispatch', pattern: /^# VibePro Parallel Agent Review Dispatch/m },
84
+ { label: 'Coordinator Instructions', pattern: /^## Coordinator Instructions/m },
85
+ { label: 'Review Focus', pattern: /^## Review Focus/m },
86
+ { label: 'Evidence Handling', pattern: /^## Evidence Handling/m },
87
+ { label: 'Investigation Guidelines', pattern: /^## Investigation Guidelines/m },
88
+ { label: 'If your coordinator runtime supports subagents', pattern: /If your coordinator runtime supports subagents/ },
89
+ { label: 'VibePro Explore Dispatch', pattern: /^# VibePro Explore Dispatch/m },
90
+ { label: 'VibePro Explore Request', pattern: /^# VibePro Explore Request/m },
91
+ { label: 'Dispatch these read-only exploration requests', pattern: /Dispatch these read-only exploration requests/ },
92
+ { label: 'Where To Look First', pattern: /Where To Look First/ },
93
+ { label: 'What To Add Next', pattern: /What To Add Next/ },
94
+ { label: 'Human-readable report', pattern: /Human-readable report/ },
95
+ { label: 'VibePro Check Pack', pattern: /^# VibePro Check Pack/m },
96
+ { label: 'Story Tasks', pattern: /^# Story Tasks/m },
97
+ { label: 'Task Create', pattern: /^# Task Create/m },
98
+ { label: 'Story Task', pattern: /^# Story Task/m },
99
+ { label: 'Target Groups', pattern: /^## Target Groups/m },
100
+ { label: 'Source Recovery', pattern: /^## Source Recovery/m },
101
+ { label: 'Source Alignment Findings', pattern: /^## Source Alignment Findings/m },
102
+ { label: 'Product Semantics', pattern: /^## Product Semantics/m },
103
+ { label: 'Evidence Coverage', pattern: /^## Evidence Coverage/m },
104
+ { label: 'Design System Validation', pattern: /^# Design System Validation:/m },
105
+ { label: 'Authority Boundary', pattern: /^## Authority Boundary/m },
106
+ { label: 'Design Language', pattern: /^## Design Language/m },
107
+ { label: 'Color Roles', pattern: /^## Color Roles/m }
108
+ ];
109
+
110
+ async function collectHumanArtifactFiles(dir) {
111
+ const results = [];
112
+ if (!(await exists(dir))) return results;
113
+ const entries = await readdir(dir, { withFileTypes: true });
114
+ for (const entry of entries) {
115
+ const entryPath = path.join(dir, entry.name);
116
+ if (entry.isDirectory()) {
117
+ results.push(...await collectHumanArtifactFiles(entryPath));
118
+ continue;
119
+ }
120
+ if (!entry.isFile()) continue;
121
+ if (entry.name.endsWith('.md') || entry.name.endsWith('.html')) {
122
+ results.push(entryPath);
123
+ }
124
+ }
125
+ return results;
126
+ }
127
+
128
+ async function scanStoryGateArtifacts(repoRoot, workspaceDir, options = {}) {
129
+ const prDir = path.join(workspaceDir, 'pr');
130
+ if (!(await exists(prDir))) return [];
131
+ const entries = await readdir(prDir, { withFileTypes: true });
132
+ const findings = [];
133
+ for (const entry of entries) {
134
+ if (!entry.isDirectory()) continue;
135
+ const storyId = entry.name;
136
+ if (options.storyId && storyId !== options.storyId) continue;
137
+ const storyPrDir = path.join(prDir, storyId);
138
+ const verificationPath = path.join(storyPrDir, 'verification-evidence.json');
139
+ const preparePath = path.join(storyPrDir, 'pr-prepare.json');
140
+ const gateDagPath = path.join(storyPrDir, 'gate-dag.json');
141
+ const createPath = path.join(storyPrDir, 'pr-create.json');
142
+ const hasVerification = await exists(verificationPath);
143
+ const hasPrepare = await exists(preparePath);
144
+ const hasGateDag = await exists(gateDagPath);
145
+ const hasCreate = await exists(createPath);
146
+ if (hasVerification && (!hasPrepare || !hasGateDag)) {
147
+ findings.push({
148
+ id: `self_dogfood.final_gate_missing.${storyId}`,
149
+ severity: 'high',
150
+ gate_effect: 'review',
151
+ story_id: storyId,
152
+ path: toWorkspaceRelative(repoRoot, storyPrDir),
153
+ detail: 'Verification evidence exists, but final pr-prepare/gate-dag artifacts are missing. Do not treat verify record as completion.',
154
+ required_action: `Run \`vibepro pr prepare . --story-id ${storyId} --base <base-ref>\` after recording verification evidence.`
155
+ });
156
+ }
157
+ if (hasGateDag) {
158
+ const gateDag = await readJson(gateDagPath);
159
+ if (!gateDag) {
160
+ findings.push({
161
+ id: `self_dogfood.invalid_gate_dag.${storyId}`,
162
+ severity: 'critical',
163
+ gate_effect: 'block',
164
+ story_id: storyId,
165
+ path: toWorkspaceRelative(repoRoot, gateDagPath),
166
+ detail: 'Final Gate DAG artifact exists but is not valid JSON. Do not treat malformed gate evidence as completion.',
167
+ required_action: 'Regenerate the final gate evidence with `vibepro pr prepare` and inspect any corrupt artifact before PR creation.'
168
+ });
169
+ continue;
170
+ }
171
+ if (gateDag.overall_status !== 'ready_for_review') {
172
+ findings.push({
173
+ id: `self_dogfood.unresolved_gate_dag.${storyId}`,
174
+ severity: isCriticalGateDag(gateDag) ? 'critical' : 'medium',
175
+ gate_effect: isCriticalGateDag(gateDag) ? 'block' : 'review',
176
+ story_id: storyId,
177
+ path: toWorkspaceRelative(repoRoot, gateDagPath),
178
+ detail: `Final Gate DAG is ${gateDag.overall_status}; unresolved required gates remain.`,
179
+ required_action: 'Resolve required gates, split scope, or record an auditable non-critical waiver through vibepro pr create.'
180
+ });
181
+ }
182
+ }
183
+ if (hasCreate) {
184
+ const prCreate = await readJson(createPath);
185
+ const gateDag = hasGateDag ? await readJson(gateDagPath) : null;
186
+ const gate = prCreate?.execution?.gate_dag?.overall_status
187
+ ?? prCreate?.gate_dag?.overall_status
188
+ ?? gateDag?.overall_status
189
+ ?? null;
190
+ const gateOverrideAllowed = isAuditableGateOverride(prCreate?.execution?.gate_override)
191
+ || isAuditableGateOverride(prCreate?.gate_override);
192
+ if (gate && gate !== 'ready_for_review' && !gateOverrideAllowed) {
193
+ findings.push({
194
+ id: `self_dogfood.pr_create_without_gate_override.${storyId}`,
195
+ severity: 'critical',
196
+ gate_effect: 'block',
197
+ story_id: storyId,
198
+ path: toWorkspaceRelative(repoRoot, createPath),
199
+ detail: `PR create evidence exists while Gate DAG is ${gate} and no VibePro waiver was recorded.`,
200
+ required_action: 'Use vibepro pr create so unresolved gates and waiver reasons are captured.'
201
+ });
202
+ }
203
+ }
204
+ }
205
+ return findings;
206
+ }
207
+
208
+ async function scanCurrentGitHubPr(repoRoot, workspaceDir, options = {}) {
209
+ const currentPr = await readCurrentGitHubPr(repoRoot, options);
210
+ if (!currentPr) return [];
211
+
212
+ const findings = [];
213
+ const body = typeof currentPr.body === 'string' ? currentPr.body : '';
214
+ const prLabel = currentPr.number ? `#${currentPr.number}` : currentPr.url ?? currentPr.headRefName ?? 'current branch';
215
+ const idSuffix = sanitizeFindingId(currentPr.headRefName ?? String(currentPr.number ?? 'current'));
216
+
217
+ if (body.trim().length === 0 || !isVibeProPrBody(body)) {
218
+ findings.push({
219
+ id: `self_dogfood.github_pr_non_vibepro_body.${idSuffix}`,
220
+ severity: 'critical',
221
+ gate_effect: 'block',
222
+ story_id: options.storyId ?? null,
223
+ path: currentPr.url ?? null,
224
+ detail: `GitHub PR ${prLabel} does not look like a VibePro PR body; decision brief, Gate DAG, or Execution Gate sections are missing.`,
225
+ required_action: 'Regenerate the PR body through `vibepro pr prepare`, then create or update the PR through `vibepro pr create` so Gate evidence is visible.'
226
+ });
227
+ }
228
+
229
+ if (hasEscapedNewlinePrBody(body)) {
230
+ findings.push({
231
+ id: `self_dogfood.github_pr_body_escaped_newlines.${idSuffix}`,
232
+ severity: 'critical',
233
+ gate_effect: 'block',
234
+ story_id: options.storyId ?? null,
235
+ path: currentPr.url ?? null,
236
+ detail: `GitHub PR ${prLabel} contains literal escaped newline sequences, which usually means the PR body was passed inline instead of through VibePro's body file.`,
237
+ required_action: 'Update the PR body from the generated VibePro `pr-body.md` artifact instead of passing a raw escaped string.'
238
+ });
239
+ }
240
+
241
+ const matchingEvidence = await findMatchingPrCreateEvidence(repoRoot, workspaceDir, currentPr, options);
242
+ if (!matchingEvidence) {
243
+ findings.push({
244
+ id: `self_dogfood.github_pr_missing_vibepro_create.${idSuffix}`,
245
+ severity: 'critical',
246
+ gate_effect: 'block',
247
+ story_id: options.storyId ?? null,
248
+ path: toWorkspaceRelative(repoRoot, path.join(workspaceDir, 'pr')),
249
+ detail: `GitHub PR ${prLabel} is visible, but no matching .vibepro/pr pr-create.json evidence was found for this PR or head branch.`,
250
+ required_action: 'Run `vibepro pr create` for the Story so GitHub PR creation, Gate DAG, waiver policy, and toolchain evidence are recorded together.'
251
+ });
252
+ }
253
+
254
+ return findings;
255
+ }
256
+
257
+ async function readCurrentGitHubPr(repoRoot, options = {}) {
258
+ try {
259
+ const result = await execFileAsync('gh', ['pr', 'view', '--json', 'number,url,headRefName,headRefOid,body'], {
260
+ cwd: repoRoot,
261
+ encoding: 'utf8',
262
+ env: options.env ?? process.env,
263
+ timeout: 5000,
264
+ maxBuffer: 1024 * 1024
265
+ });
266
+ const parsed = JSON.parse(result.stdout);
267
+ if (!parsed || typeof parsed !== 'object') return null;
268
+ return parsed;
269
+ } catch {
270
+ return null;
271
+ }
272
+ }
273
+
274
+ function isVibeProPrBody(body) {
275
+ const hasDecisionBrief = /(##\s+このPRで決めたいこと|##\s+What this PR needs to decide|このPRで閉じる問い|Review question)/i.test(body);
276
+ return hasDecisionBrief && /##\s+Gate DAG/i.test(body) && /##\s+Execution Gate/i.test(body);
277
+ }
278
+
279
+ function hasEscapedNewlinePrBody(body) {
280
+ if (!body.includes('\\n')) return false;
281
+ const realNewlines = (body.match(/\n/g) ?? []).length;
282
+ const escapedNewlines = (body.match(/\\n/g) ?? []).length;
283
+ return escapedNewlines > 0 && realNewlines <= 1;
284
+ }
285
+
286
+ async function findMatchingPrCreateEvidence(repoRoot, workspaceDir, currentPr, options = {}) {
287
+ const prDir = path.join(workspaceDir, 'pr');
288
+ if (!(await exists(prDir))) return null;
289
+ const entries = await readdir(prDir, { withFileTypes: true });
290
+ for (const entry of entries) {
291
+ if (!entry.isDirectory()) continue;
292
+ const storyId = entry.name;
293
+ if (options.storyId && storyId !== options.storyId) continue;
294
+ const createPath = path.join(prDir, storyId, 'pr-create.json');
295
+ if (!(await exists(createPath))) continue;
296
+ const prCreate = await readJson(createPath);
297
+ if (isValidPrCreateEvidence(prCreate, currentPr) && matchesCurrentPr(prCreate, currentPr)) {
298
+ return { story_id: storyId, path: toWorkspaceRelative(repoRoot, createPath), evidence: prCreate };
299
+ }
300
+ }
301
+ return null;
302
+ }
303
+
304
+ function isValidPrCreateEvidence(prCreate, currentPr) {
305
+ if (!prCreate || prCreate.mode !== 'pr_create') return false;
306
+ if (prCreate.dry_run === true) return false;
307
+ if (prCreate.status === 'failed' || prCreate.error) return false;
308
+
309
+ const currentUrl = normalizeUrl(currentPr?.url);
310
+ if (currentUrl && normalizeUrl(prCreate.pr_url) !== currentUrl) return false;
311
+
312
+ const currentHeadOid = typeof currentPr?.headRefOid === 'string' ? currentPr.headRefOid.trim() : '';
313
+ if (currentHeadOid) {
314
+ return prCreate.toolchain?.source_git?.commit === currentHeadOid;
315
+ }
316
+
317
+ const results = Array.isArray(prCreate.results) ? prCreate.results : [];
318
+ if (results.length === 0) return Boolean(normalizeUrl(prCreate.pr_url));
319
+ const ghCreateResult = results.find((result) => /(^|\s)gh pr create(\s|$)/.test(result?.command ?? ''));
320
+ return ghCreateResult?.exit_code === 0;
321
+ }
322
+
323
+ function matchesCurrentPr(prCreate, currentPr) {
324
+ if (!prCreate || !currentPr) return false;
325
+ const prUrl = normalizeUrl(prCreate.pr_url);
326
+ const currentUrl = normalizeUrl(currentPr.url);
327
+ if (prUrl && currentUrl && prUrl === currentUrl) return true;
328
+
329
+ const head = typeof prCreate.head === 'string' ? prCreate.head : '';
330
+ if (head && currentPr.headRefName && head === currentPr.headRefName) return true;
331
+
332
+ const branch = prCreate.toolchain?.source_git?.branch;
333
+ if (branch && currentPr.headRefName && branch === currentPr.headRefName) return true;
334
+
335
+ return false;
336
+ }
337
+
338
+ function normalizeUrl(value) {
339
+ return typeof value === 'string' ? value.trim().replace(/\/$/, '') : '';
340
+ }
341
+
342
+ function sanitizeFindingId(value) {
343
+ return String(value).trim().replace(/[^a-zA-Z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '') || 'current';
344
+ }
345
+
346
+ function isAuditableGateOverride(override) {
347
+ if (!override || override.allowed !== true) return false;
348
+ const reason = typeof override.reason === 'string' ? override.reason.trim() : '';
349
+ const policy = typeof override.waiver_policy === 'string' ? override.waiver_policy.trim() : '';
350
+ return reason.length > 0 && policy.length > 0;
351
+ }
352
+
353
+ async function scanInstructionBypassLanguage(repoRoot, options = {}) {
354
+ const files = [];
355
+ for (const target of TEXT_TARGETS) {
356
+ const targetPath = path.join(repoRoot, target);
357
+ if (await exists(targetPath)) {
358
+ await collectTextFiles(targetPath, files);
359
+ }
360
+ }
361
+ const findings = [];
362
+ for (const file of files) {
363
+ const text = await readFile(file, 'utf8');
364
+ const rel = toWorkspaceRelative(repoRoot, file);
365
+ if (options.storyId && !text.includes(options.storyId) && !rel.includes(options.storyId)) continue;
366
+ const checks = [
367
+ {
368
+ id: 'self_dogfood.agent_review_skip_language',
369
+ pattern: /Agent Review Gate[^\n]{0,80}\b(skip|スキップ)\b/i,
370
+ severity: 'critical',
371
+ gate_effect: 'block',
372
+ detail: 'Instruction text suggests skipping Agent Review Gate.'
373
+ },
374
+ {
375
+ id: 'self_dogfood.raw_gh_pr_create_guidance',
376
+ pattern: /(?:^|\n)[^\n]*(?:raw\s+)?`?gh pr create`?[^\n]*(?:use|使|作成|direct|直接)/i,
377
+ severity: 'high',
378
+ gate_effect: 'review',
379
+ detail: 'Instruction text can route PR creation through raw gh pr create instead of VibePro.'
380
+ },
381
+ {
382
+ id: 'self_dogfood.subagent_permission_waiting_language',
383
+ pattern: /(ask exactly|明示.*許可|explicit user authorization|explicit permission is still required|permission-request\.md)/i,
384
+ severity: 'high',
385
+ gate_effect: 'review',
386
+ detail: 'Instruction text can make Agent Review Gate look like a user-permission wait instead of an autonomous subagent dispatch contract.'
387
+ }
388
+ ];
389
+ for (const check of checks) {
390
+ if (hasInstructionFinding(text, check.pattern)) {
391
+ findings.push({
392
+ id: `${check.id}.${rel}`,
393
+ severity: check.severity,
394
+ gate_effect: check.gate_effect,
395
+ path: rel,
396
+ detail: check.detail,
397
+ required_action: 'Rewrite the instruction to dispatch permitted Codex/Claude Code subagents by default and reserve human approval for policy blockers or waivers.'
398
+ });
399
+ }
400
+ }
401
+ }
402
+ return findings;
403
+ }
404
+
405
+ function hasInstructionFinding(text, pattern) {
406
+ return text.split(/\r?\n/).some((line) => {
407
+ if (!pattern.test(line)) return false;
408
+ if (/\b(do not|don't|never|禁止|使わない|使わず|直接.*使わない|バイパスしない)\b/i.test(line)) return false;
409
+ if (/(使わない|使わず|直接.*使わない|ではなく|instead of|rather than|検出|検査|診断|finding|scanner|diagnostic|detect|flag|実行予定|標準出力|外部コマンド)/i.test(line)) return false;
410
+ return true;
411
+ });
412
+ }
413
+
414
+ async function collectTextFiles(dir, files) {
415
+ const entries = await readdir(dir, { withFileTypes: true });
416
+ for (const entry of entries) {
417
+ const fullPath = path.join(dir, entry.name);
418
+ if (entry.isDirectory()) {
419
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
420
+ await collectTextFiles(fullPath, files);
421
+ continue;
422
+ }
423
+ if (/\.(md|txt|yml|yaml|json)$/.test(entry.name)) files.push(fullPath);
424
+ }
425
+ }
426
+
427
+ function isCriticalGateDag(gateDag) {
428
+ return (gateDag.nodes ?? []).some((node) => node.required === true && [
429
+ 'story',
430
+ 'architecture_gate',
431
+ 'spec_gate',
432
+ 'verification_gate',
433
+ 'requirement_gate',
434
+ 'visual_qa_gate',
435
+ 'agent_review_gate',
436
+ 'pr_freshness_gate'
437
+ ].includes(node.type) && [
438
+ 'missing',
439
+ 'implicit',
440
+ 'inferred_empty',
441
+ 'needs_evidence',
442
+ 'needs_setup',
443
+ 'needs_review',
444
+ 'needs_changes',
445
+ 'contradicted',
446
+ 'stale',
447
+ 'needs_rebase',
448
+ 'block',
449
+ 'failed'
450
+ ].includes(node.status));
451
+ }
452
+
453
+ function summarizeFindings(findings) {
454
+ return findings.reduce((summary, finding) => {
455
+ const effect = ['block', 'review', 'info'].includes(finding.gate_effect) ? finding.gate_effect : 'info';
456
+ summary[effect] += 1;
457
+ return summary;
458
+ }, { block: 0, review: 0, info: 0 });
459
+ }
460
+
461
+ async function readJson(file) {
462
+ try {
463
+ return JSON.parse(await readFile(file, 'utf8'));
464
+ } catch {
465
+ return null;
466
+ }
467
+ }
468
+
469
+ async function exists(file) {
470
+ try {
471
+ await access(file);
472
+ return true;
473
+ } catch {
474
+ return false;
475
+ }
476
+ }