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.
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.ja.md +448 -0
- package/README.md +520 -0
- package/agent-instructions/codex/AGENTS.vibepro.md +45 -0
- package/bin/vibepro.js +9 -0
- package/docs/assets/vibepro-header.png +0 -0
- package/package.json +51 -0
- package/skills/vibepro-diagnosis-packages/SKILL.md +133 -0
- package/skills/vibepro-human-review/SKILL.md +73 -0
- package/skills/vibepro-story-refactor/SKILL.md +89 -0
- package/skills/vibepro-workflow/SKILL.md +139 -0
- package/src/agent-harness-map.js +230 -0
- package/src/agent-harness-scanner.js +337 -0
- package/src/agent-review.js +2180 -0
- package/src/api-boundary-scanner.js +452 -0
- package/src/architecture-profiler.js +423 -0
- package/src/authorization-scoring.js +149 -0
- package/src/brainbase-importer.js +534 -0
- package/src/change-risk-classifier.js +195 -0
- package/src/check-packs.js +605 -0
- package/src/checkpoint-manager.js +233 -0
- package/src/cli.js +2213 -0
- package/src/code-quality-scanner.js +310 -0
- package/src/codex-manager.js +143 -0
- package/src/component-style-scanner.js +336 -0
- package/src/coverage-report.js +99 -0
- package/src/database-access-scanner.js +163 -0
- package/src/decision-records.js +315 -0
- package/src/design-modernize.js +1435 -0
- package/src/design-system.js +1732 -0
- package/src/diagnostic-engine.js +1945 -0
- package/src/diagram-requirement-resolver.js +194 -0
- package/src/doctor.js +677 -0
- package/src/environment-graph.js +424 -0
- package/src/execution-state.js +849 -0
- package/src/explore-evidence.js +425 -0
- package/src/flow-design-scanner.js +896 -0
- package/src/flow-verifier.js +887 -0
- package/src/gesture-interaction-scanner.js +330 -0
- package/src/graph-context.js +263 -0
- package/src/graphify-adapter.js +189 -0
- package/src/html-report.js +1035 -0
- package/src/journey-map.js +1299 -0
- package/src/language.js +48 -0
- package/src/lazy-pattern-detector.js +182 -0
- package/src/local-dev-scanner.js +135 -0
- package/src/managed-worktree-gate.js +187 -0
- package/src/managed-worktree.js +766 -0
- package/src/merge-manager.js +501 -0
- package/src/network-contract-scanner.js +442 -0
- package/src/nocodb-story-sync.js +386 -0
- package/src/oss-readiness-scanner.js +417 -0
- package/src/performance-evidence.js +756 -0
- package/src/performance-measurer.js +591 -0
- package/src/pr-manager.js +8220 -0
- package/src/presets.js +682 -0
- package/src/public-discovery-scanner.js +519 -0
- package/src/refactoring-delta-reporter.js +367 -0
- package/src/refactoring-opportunity-generator.js +797 -0
- package/src/regression-risk-scanner.js +146 -0
- package/src/repo-status.js +266 -0
- package/src/report-fingerprint.js +188 -0
- package/src/report-pr-body-prompt-template.md +108 -0
- package/src/report-pr-body-schema.json +95 -0
- package/src/report-store.js +135 -0
- package/src/report-validator.js +192 -0
- package/src/requirement-consistency.js +1066 -0
- package/src/runtime-info.js +134 -0
- package/src/self-dogfood-scanner.js +476 -0
- package/src/session-learning.js +164 -0
- package/src/skills-manager.js +157 -0
- package/src/spec-drift.js +378 -0
- package/src/spec-fingerprint.js +445 -0
- package/src/spec-prompt-template.md +155 -0
- package/src/spec-schema.json +219 -0
- package/src/spec-store.js +258 -0
- package/src/spec-validator.js +459 -0
- package/src/static-site-scanner.js +316 -0
- package/src/story-candidate-generator.js +85 -0
- package/src/story-catalog-generator.js +2813 -0
- package/src/story-html.js +156 -0
- package/src/story-manager.js +2144 -0
- package/src/story-task-generator.js +522 -0
- package/src/task-manager.js +1029 -0
- package/src/terminal-link-scanner.js +238 -0
- package/src/usage-report.js +417 -0
- package/src/verification-evidence.js +284 -0
- package/src/workspace.js +126 -0
|
@@ -0,0 +1,1029 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { getStoryStatus } from './story-manager.js';
|
|
5
|
+
import { readStoryTasks, renderStoryTasks } from './story-task-generator.js';
|
|
6
|
+
import { getWorkspaceDir, readManifest, toWorkspaceRelative, writeManifest } from './workspace.js';
|
|
7
|
+
import { localizedText } from './language.js';
|
|
8
|
+
|
|
9
|
+
export async function listTasks(repoRoot, options = {}) {
|
|
10
|
+
const context = await loadTaskContext(repoRoot, options.storyId);
|
|
11
|
+
return {
|
|
12
|
+
story: context.story,
|
|
13
|
+
source_run: context.taskState.source_run,
|
|
14
|
+
tasks: context.tasks
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function showTask(repoRoot, options = {}) {
|
|
19
|
+
const context = await loadTaskContext(repoRoot, options.storyId);
|
|
20
|
+
const task = findTask(context.tasks, options.taskId);
|
|
21
|
+
return {
|
|
22
|
+
story: context.story,
|
|
23
|
+
source_run: context.taskState.source_run,
|
|
24
|
+
task
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function createTasksFromPlan(repoRoot, options = {}) {
|
|
29
|
+
const root = path.resolve(repoRoot);
|
|
30
|
+
const manifest = await readManifest(root);
|
|
31
|
+
const plan = await readStoryPlan(root);
|
|
32
|
+
const selectedCandidates = selectPlanTaskCandidates(plan, options);
|
|
33
|
+
if (selectedCandidates.length === 0) {
|
|
34
|
+
throw new Error('No task candidates found in story plan. Run `vibepro story plan` first.');
|
|
35
|
+
}
|
|
36
|
+
const byStory = groupBy(selectedCandidates, (candidate) => candidate.story_id);
|
|
37
|
+
const results = [];
|
|
38
|
+
for (const [storyId, candidates] of Object.entries(byStory)) {
|
|
39
|
+
const story = resolvePlanStory(plan, storyId);
|
|
40
|
+
const taskState = buildPlanTaskState({ story, plan, candidates });
|
|
41
|
+
const tasksDir = path.join(getWorkspaceDir(root), 'stories', storyId, 'tasks');
|
|
42
|
+
await mkdir(tasksDir, { recursive: true });
|
|
43
|
+
const jsonPath = path.join(tasksDir, 'tasks.json');
|
|
44
|
+
const markdownPath = path.join(tasksDir, 'tasks.md');
|
|
45
|
+
await writeFile(jsonPath, `${JSON.stringify(taskState, null, 2)}\n`);
|
|
46
|
+
await writeFile(markdownPath, renderStoryTasks(taskState));
|
|
47
|
+
manifest.stories = {
|
|
48
|
+
...(manifest.stories ?? {}),
|
|
49
|
+
[storyId]: {
|
|
50
|
+
...(manifest.stories?.[storyId] ?? {}),
|
|
51
|
+
plan_tasks_json: toWorkspaceRelative(root, jsonPath),
|
|
52
|
+
plan_tasks_markdown: toWorkspaceRelative(root, markdownPath),
|
|
53
|
+
plan_tasks_generated_at: taskState.generated_at,
|
|
54
|
+
plan_tasks_source: '.vibepro/stories/story-plan.json'
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
results.push({
|
|
58
|
+
story,
|
|
59
|
+
taskState,
|
|
60
|
+
artifacts: {
|
|
61
|
+
json: toWorkspaceRelative(root, jsonPath),
|
|
62
|
+
markdown: toWorkspaceRelative(root, markdownPath)
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
await writeManifest(root, manifest);
|
|
67
|
+
return {
|
|
68
|
+
source_plan: '.vibepro/stories/story-plan.json',
|
|
69
|
+
created_story_count: results.length,
|
|
70
|
+
created_task_count: results.reduce((sum, result) => sum + result.taskState.tasks.length, 0),
|
|
71
|
+
results
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function createTaskBrief(repoRoot, options = {}) {
|
|
76
|
+
const root = path.resolve(repoRoot);
|
|
77
|
+
const { context, task, group } = await resolveTaskSelection(root, options);
|
|
78
|
+
const briefing = buildTaskBriefing({
|
|
79
|
+
story: context.story,
|
|
80
|
+
sourceRun: context.taskState.source_run,
|
|
81
|
+
task,
|
|
82
|
+
group
|
|
83
|
+
});
|
|
84
|
+
const briefDir = getTaskArtifactDir(root, context.story.story_id, task.id, group?.id);
|
|
85
|
+
await mkdir(briefDir, { recursive: true });
|
|
86
|
+
const jsonPath = path.join(briefDir, 'briefing.json');
|
|
87
|
+
const markdownPath = path.join(briefDir, 'briefing.md');
|
|
88
|
+
await writeFile(jsonPath, `${JSON.stringify(briefing, null, 2)}\n`);
|
|
89
|
+
await writeFile(markdownPath, renderTaskBriefing(briefing, options.language ?? 'ja'));
|
|
90
|
+
return {
|
|
91
|
+
story: context.story,
|
|
92
|
+
task,
|
|
93
|
+
group,
|
|
94
|
+
briefing,
|
|
95
|
+
artifacts: {
|
|
96
|
+
json: toWorkspaceRelative(root, jsonPath),
|
|
97
|
+
markdown: toWorkspaceRelative(root, markdownPath)
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function createTaskPlan(repoRoot, options = {}) {
|
|
103
|
+
const root = path.resolve(repoRoot);
|
|
104
|
+
const { context, task, group } = await resolveTaskSelection(root, options);
|
|
105
|
+
const briefing = buildTaskBriefing({
|
|
106
|
+
story: context.story,
|
|
107
|
+
sourceRun: context.taskState.source_run,
|
|
108
|
+
task,
|
|
109
|
+
group
|
|
110
|
+
});
|
|
111
|
+
const plan = buildTaskPlan({ briefing });
|
|
112
|
+
const planDir = getTaskArtifactDir(root, context.story.story_id, task.id, group?.id);
|
|
113
|
+
await mkdir(planDir, { recursive: true });
|
|
114
|
+
const jsonPath = path.join(planDir, 'plan.json');
|
|
115
|
+
const markdownPath = path.join(planDir, 'plan.md');
|
|
116
|
+
await writeFile(jsonPath, `${JSON.stringify(plan, null, 2)}\n`);
|
|
117
|
+
await writeFile(markdownPath, renderTaskPlan(plan, options.language ?? 'ja'));
|
|
118
|
+
return {
|
|
119
|
+
story: context.story,
|
|
120
|
+
task,
|
|
121
|
+
group,
|
|
122
|
+
plan,
|
|
123
|
+
artifacts: {
|
|
124
|
+
json: toWorkspaceRelative(root, jsonPath),
|
|
125
|
+
markdown: toWorkspaceRelative(root, markdownPath)
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function createTaskHandoff(repoRoot, options = {}) {
|
|
131
|
+
const root = path.resolve(repoRoot);
|
|
132
|
+
const briefingResult = await createTaskBrief(root, options);
|
|
133
|
+
const planResult = await createTaskPlan(root, options);
|
|
134
|
+
const handoff = buildTaskHandoff({
|
|
135
|
+
briefing: briefingResult.briefing,
|
|
136
|
+
plan: planResult.plan,
|
|
137
|
+
briefingArtifacts: briefingResult.artifacts,
|
|
138
|
+
planArtifacts: planResult.artifacts
|
|
139
|
+
});
|
|
140
|
+
const handoffDir = getTaskArtifactDir(root, handoff.story.story_id, handoff.task.id, handoff.group?.id);
|
|
141
|
+
await mkdir(handoffDir, { recursive: true });
|
|
142
|
+
const jsonPath = path.join(handoffDir, 'handoff.json');
|
|
143
|
+
const markdownPath = path.join(handoffDir, 'handoff.md');
|
|
144
|
+
await writeFile(jsonPath, `${JSON.stringify(handoff, null, 2)}\n`);
|
|
145
|
+
await writeFile(markdownPath, renderTaskHandoff(handoff, options.language ?? 'ja'));
|
|
146
|
+
return {
|
|
147
|
+
story: briefingResult.story,
|
|
148
|
+
task: briefingResult.task,
|
|
149
|
+
group: briefingResult.group,
|
|
150
|
+
handoff,
|
|
151
|
+
artifacts: {
|
|
152
|
+
json: toWorkspaceRelative(root, jsonPath),
|
|
153
|
+
markdown: toWorkspaceRelative(root, markdownPath)
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function createTaskExecution(repoRoot, options = {}) {
|
|
159
|
+
const root = path.resolve(repoRoot);
|
|
160
|
+
const handoffResult = await createTaskHandoff(root, options);
|
|
161
|
+
const task = handoffResult.task;
|
|
162
|
+
const group = handoffResult.group;
|
|
163
|
+
const story = handoffResult.story;
|
|
164
|
+
const suffix = group?.id ? `${task.id}-${group.id}` : task.id;
|
|
165
|
+
const prPrepareCommand = buildPrPrepareCommand({ story, task, group, baseRef: options.baseRef });
|
|
166
|
+
const prCreateCommand = buildPrCreateCommand({ story, task, group, baseRef: options.baseRef, dryRun: options.dryRunPrCreate });
|
|
167
|
+
const execution = {
|
|
168
|
+
schema_version: '0.1.0',
|
|
169
|
+
generated_at: new Date().toISOString(),
|
|
170
|
+
mode: 'task_execution_session',
|
|
171
|
+
story,
|
|
172
|
+
source_run: handoffResult.handoff.source_run,
|
|
173
|
+
task,
|
|
174
|
+
group,
|
|
175
|
+
warnings: normalizeWarnings([options.managedWorktreeWarning]),
|
|
176
|
+
execution: {
|
|
177
|
+
vibepro_mutates_repository: false,
|
|
178
|
+
implementation_agent_may_mutate_repository: true,
|
|
179
|
+
note: 'VibeProは実装の入口と証跡を管理する。対象コードの修正は人間またはAIエージェントが行う。'
|
|
180
|
+
},
|
|
181
|
+
references: {
|
|
182
|
+
handoff_json: handoffResult.artifacts.json,
|
|
183
|
+
handoff_markdown: handoffResult.artifacts.markdown,
|
|
184
|
+
plan_json: handoffResult.handoff.references.plan_json,
|
|
185
|
+
plan_markdown: handoffResult.handoff.references.plan_markdown,
|
|
186
|
+
briefing_json: handoffResult.handoff.references.briefing_json,
|
|
187
|
+
briefing_markdown: handoffResult.handoff.references.briefing_markdown
|
|
188
|
+
},
|
|
189
|
+
phases: [
|
|
190
|
+
{
|
|
191
|
+
id: 'read_context',
|
|
192
|
+
title: 'Handoffを読む',
|
|
193
|
+
required: true,
|
|
194
|
+
artifacts: [
|
|
195
|
+
handoffResult.artifacts.markdown,
|
|
196
|
+
handoffResult.handoff.references.plan_markdown,
|
|
197
|
+
handoffResult.handoff.references.briefing_markdown
|
|
198
|
+
]
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: 'implement',
|
|
202
|
+
title: '対象範囲を実装する',
|
|
203
|
+
required: true,
|
|
204
|
+
target_files: handoffResult.handoff.target_files,
|
|
205
|
+
instructions: handoffResult.handoff.implementation_instructions,
|
|
206
|
+
prohibited_actions: handoffResult.handoff.prohibited_actions
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: 'verify',
|
|
210
|
+
title: '検証する',
|
|
211
|
+
required: true,
|
|
212
|
+
commands: handoffResult.handoff.verification_commands
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: 'prepare_pr',
|
|
216
|
+
title: 'PR準備物を生成する',
|
|
217
|
+
required: true,
|
|
218
|
+
command: prPrepareCommand
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: 'create_pr',
|
|
222
|
+
title: 'PRを作成する',
|
|
223
|
+
required: false,
|
|
224
|
+
command: prCreateCommand
|
|
225
|
+
}
|
|
226
|
+
],
|
|
227
|
+
commands: {
|
|
228
|
+
pr_prepare: prPrepareCommand,
|
|
229
|
+
pr_create: prCreateCommand,
|
|
230
|
+
verify_diagnosis: `npx vibepro diagnose . --run-id verify-${suffix}`
|
|
231
|
+
},
|
|
232
|
+
completion_report_template: [
|
|
233
|
+
'変更したファイル',
|
|
234
|
+
'実行した検証コマンドと結果',
|
|
235
|
+
'vibepro pr prepare の成果物',
|
|
236
|
+
'作成したPR URL',
|
|
237
|
+
'未解決リスク'
|
|
238
|
+
]
|
|
239
|
+
};
|
|
240
|
+
const executionDir = getTaskArtifactDir(root, story.story_id, task.id, group?.id);
|
|
241
|
+
await mkdir(executionDir, { recursive: true });
|
|
242
|
+
const jsonPath = path.join(executionDir, 'execution.json');
|
|
243
|
+
const markdownPath = path.join(executionDir, 'execution.md');
|
|
244
|
+
await writeFile(jsonPath, `${JSON.stringify(execution, null, 2)}\n`);
|
|
245
|
+
await writeFile(markdownPath, renderTaskExecution(execution, options.language ?? 'ja'));
|
|
246
|
+
return {
|
|
247
|
+
story,
|
|
248
|
+
task,
|
|
249
|
+
group,
|
|
250
|
+
handoff: handoffResult.handoff,
|
|
251
|
+
execution,
|
|
252
|
+
artifacts: {
|
|
253
|
+
json: toWorkspaceRelative(root, jsonPath),
|
|
254
|
+
markdown: toWorkspaceRelative(root, markdownPath),
|
|
255
|
+
handoff_json: handoffResult.artifacts.json,
|
|
256
|
+
handoff_markdown: handoffResult.artifacts.markdown
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function renderTaskList(result, language = 'ja') {
|
|
262
|
+
const tasks = Array.isArray(result.tasks) ? result.tasks : [];
|
|
263
|
+
const title = localizedText(language, { ja: '# Storyタスク', en: '# Story Tasks' });
|
|
264
|
+
const headers = localizedText(language, {
|
|
265
|
+
ja: '| ID | 優先度 | 対象 | グループ | 状態 | タイトル |',
|
|
266
|
+
en: '| ID | Priority | Targets | Groups | Status | Title |'
|
|
267
|
+
});
|
|
268
|
+
return `${title}
|
|
269
|
+
|
|
270
|
+
| 項目 | 内容 |
|
|
271
|
+
|------|------|
|
|
272
|
+
| Story ID | ${result.story?.story_id ?? '-'} |
|
|
273
|
+
| Story | ${result.story?.title ?? '-'} |
|
|
274
|
+
| Run ID | ${result.source_run?.run_id ?? '-'} |
|
|
275
|
+
| Gate | ${result.source_run?.gate_status ?? '-'} |
|
|
276
|
+
| タスク数 | ${tasks.length} |
|
|
277
|
+
|
|
278
|
+
${headers}
|
|
279
|
+
|----|--------|------|----------|------|----------|
|
|
280
|
+
${tasks.length === 0 ? '| - | - | - | - | - | - |' : tasks.map((task) => `| ${task.id} | ${task.priority} | ${task.target_count ?? task.target_files?.length ?? 0}件 | ${formatTargetGroups(task.target_groups)} | ${task.status} | ${task.title} |`).join('\n')}
|
|
281
|
+
`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function renderTaskCreateSummary(result, language = 'ja') {
|
|
285
|
+
const rows = result.results.flatMap((item) => item.taskState.tasks.map((task) => `| ${item.story.story_id} | ${task.id} | ${task.priority} | ${task.title} | ${item.artifacts.markdown} |`));
|
|
286
|
+
return `${localizedText(language, { ja: '# Task作成', en: '# Task Create' })}
|
|
287
|
+
|
|
288
|
+
| 項目 | 内容 |
|
|
289
|
+
|------|------|
|
|
290
|
+
| Source plan | ${result.source_plan} |
|
|
291
|
+
| Story数 | ${result.created_story_count} |
|
|
292
|
+
| Task数 | ${result.created_task_count} |
|
|
293
|
+
|
|
294
|
+
| Story | Task | Priority | Title | Artifact |
|
|
295
|
+
|-------|------|----------|-------|----------|
|
|
296
|
+
${rows.length === 0 ? '| - | - | - | - | - |' : rows.join('\n')}
|
|
297
|
+
`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function renderTaskShow(result, language = 'ja') {
|
|
301
|
+
const task = result.task;
|
|
302
|
+
return `${localizedText(language, { ja: '# Storyタスク', en: '# Story Task' })}
|
|
303
|
+
|
|
304
|
+
| 項目 | 内容 |
|
|
305
|
+
|------|------|
|
|
306
|
+
| Story ID | ${result.story?.story_id ?? '-'} |
|
|
307
|
+
| Story | ${result.story?.title ?? '-'} |
|
|
308
|
+
| Run ID | ${result.source_run?.run_id ?? '-'} |
|
|
309
|
+
| Task ID | ${task.id} |
|
|
310
|
+
| ${localizedText(language, { ja: 'Title', en: 'Title' })} | ${task.title} |
|
|
311
|
+
| ${localizedText(language, { ja: 'Priority', en: 'Priority' })} | ${task.priority} |
|
|
312
|
+
| ${localizedText(language, { ja: 'Status', en: 'Status' })} | ${task.status} |
|
|
313
|
+
| Execution | ${task.execution_policy} / mutates_repository=${task.mutates_repository} |
|
|
314
|
+
| Strategy | ${task.recommended_strategy?.id ?? '-'} |
|
|
315
|
+
|
|
316
|
+
## ${localizedText(language, { ja: '対象ファイル', en: 'Target Files' })}
|
|
317
|
+
|
|
318
|
+
${formatList(task.target_files)}
|
|
319
|
+
|
|
320
|
+
## ${localizedText(language, { ja: '対象route', en: 'Target Routes' })}
|
|
321
|
+
|
|
322
|
+
${formatRoutes(task.target_routes)}
|
|
323
|
+
|
|
324
|
+
## ${localizedText(language, { ja: '対象グループ', en: 'Target Groups' })}
|
|
325
|
+
|
|
326
|
+
${formatGroups(task.target_groups)}
|
|
327
|
+
|
|
328
|
+
## ${localizedText(language, { ja: '先に読むもの', en: 'Read First' })}
|
|
329
|
+
|
|
330
|
+
${formatReadFirst(task.read_first_files)}
|
|
331
|
+
|
|
332
|
+
## ${localizedText(language, { ja: '完了条件', en: 'Acceptance Criteria' })}
|
|
333
|
+
|
|
334
|
+
${formatList(task.acceptance_criteria)}
|
|
335
|
+
`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function renderTaskBriefing(briefing, language = 'ja') {
|
|
339
|
+
return `# 修正前ブリーフィング
|
|
340
|
+
|
|
341
|
+
## 前提
|
|
342
|
+
|
|
343
|
+
- Story: ${briefing.story.title} (${briefing.story.story_id})
|
|
344
|
+
- Run ID: ${briefing.source_run?.run_id ?? '-'}
|
|
345
|
+
- Task: ${briefing.task.id} - ${briefing.task.title}
|
|
346
|
+
- Group: ${briefing.group?.id ?? '-'}
|
|
347
|
+
- Execution: ${briefing.execution_policy} / mutates_repository=${briefing.mutates_repository}
|
|
348
|
+
|
|
349
|
+
## ガードレール
|
|
350
|
+
|
|
351
|
+
${briefing.guardrails.map((item) => `- ${item}`).join('\n')}
|
|
352
|
+
|
|
353
|
+
## 対象route
|
|
354
|
+
|
|
355
|
+
${formatRoutes(briefing.target_routes)}
|
|
356
|
+
|
|
357
|
+
## 対象ファイル
|
|
358
|
+
|
|
359
|
+
${formatList(briefing.target_files)}
|
|
360
|
+
|
|
361
|
+
## 先に読むファイル
|
|
362
|
+
|
|
363
|
+
${formatReadFirst(briefing.read_first_files)}
|
|
364
|
+
|
|
365
|
+
## graphify文脈
|
|
366
|
+
|
|
367
|
+
- impact_score: ${briefing.graph_context?.impact_score ?? '-'}
|
|
368
|
+
- matched_route_count: ${briefing.graph_context?.matched_route_count ?? '-'}
|
|
369
|
+
- matched_node_count: ${briefing.graph_context?.matched_node_count ?? '-'}
|
|
370
|
+
- related_edge_count: ${briefing.graph_context?.related_edge_count ?? '-'}
|
|
371
|
+
- affected_communities: ${formatCommunities(briefing.graph_context?.affected_communities)}
|
|
372
|
+
- hub_nodes: ${formatHubNodes(briefing.graph_context?.hub_nodes)}
|
|
373
|
+
|
|
374
|
+
## 推奨方針
|
|
375
|
+
|
|
376
|
+
- ${briefing.recommended_strategy?.id ?? '-'}: ${briefing.recommended_strategy?.reason ?? '-'}
|
|
377
|
+
|
|
378
|
+
## ${localizedText(language, { ja: 'Source復旧', en: 'Source Recovery' })}
|
|
379
|
+
|
|
380
|
+
${renderSourceRecovery(briefing.source_recovery, briefing.recovery_drafts)}
|
|
381
|
+
|
|
382
|
+
## ${localizedText(language, { ja: 'Source整合性の検出事項', en: 'Source Alignment Findings' })}
|
|
383
|
+
|
|
384
|
+
${renderSourceAlignmentFindings(briefing.source_alignment_findings)}
|
|
385
|
+
|
|
386
|
+
## 実装手順候補
|
|
387
|
+
|
|
388
|
+
${briefing.implementation_steps.length === 0 ? '- なし' : briefing.implementation_steps.map((step, index) => `${index + 1}. ${step.title}: ${step.detail}`).join('\n')}
|
|
389
|
+
|
|
390
|
+
## 完了条件
|
|
391
|
+
|
|
392
|
+
${formatList(briefing.acceptance_criteria)}
|
|
393
|
+
`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function renderTaskPlan(plan, language = 'ja') {
|
|
397
|
+
return `# 実装修正計画
|
|
398
|
+
|
|
399
|
+
## 前提
|
|
400
|
+
|
|
401
|
+
- Story: ${plan.story.title} (${plan.story.story_id})
|
|
402
|
+
- Run ID: ${plan.source_run?.run_id ?? '-'}
|
|
403
|
+
- Task: ${plan.task.id} - ${plan.task.title}
|
|
404
|
+
- Group: ${plan.group?.id ?? '-'}
|
|
405
|
+
- このplanは修正可能な作業計画
|
|
406
|
+
- CLI自身は対象リポジトリのコードを変更しない
|
|
407
|
+
|
|
408
|
+
## 実行境界
|
|
409
|
+
|
|
410
|
+
- plan_allows_repository_changes: ${plan.execution.plan_allows_repository_changes}
|
|
411
|
+
- cli_mutates_repository: ${plan.execution.cli_mutates_repository}
|
|
412
|
+
|
|
413
|
+
## 変更対象ファイル
|
|
414
|
+
|
|
415
|
+
${formatList(plan.target_files)}
|
|
416
|
+
|
|
417
|
+
## 先に読むファイル
|
|
418
|
+
|
|
419
|
+
${formatReadFirst(plan.read_first_files)}
|
|
420
|
+
|
|
421
|
+
## 推奨修正方針
|
|
422
|
+
|
|
423
|
+
- ${plan.recommended_strategy?.id ?? '-'}: ${plan.recommended_strategy?.reason ?? '-'}
|
|
424
|
+
|
|
425
|
+
## 実装ステップ
|
|
426
|
+
|
|
427
|
+
${plan.implementation_steps.length === 0 ? '- なし' : plan.implementation_steps.map((step, index) => `${index + 1}. ${step.title}: ${step.detail}`).join('\n')}
|
|
428
|
+
|
|
429
|
+
## 検証コマンド候補
|
|
430
|
+
|
|
431
|
+
${plan.verification_commands.map((item) => `- \`${item.command}\`: ${item.reason}`).join('\n')}
|
|
432
|
+
|
|
433
|
+
## 完了条件
|
|
434
|
+
|
|
435
|
+
${formatList(plan.acceptance_criteria)}
|
|
436
|
+
|
|
437
|
+
## ロールバック観点
|
|
438
|
+
|
|
439
|
+
${formatList(plan.rollback_considerations)}
|
|
440
|
+
|
|
441
|
+
## ガードレール
|
|
442
|
+
|
|
443
|
+
${formatList(plan.guardrails)}
|
|
444
|
+
`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function renderSourceRecovery(sourceRecovery, drafts = []) {
|
|
448
|
+
if (!sourceRecovery) return '- なし';
|
|
449
|
+
const draftLines = drafts.length === 0
|
|
450
|
+
? '- Draft: なし'
|
|
451
|
+
: drafts.map((draft) => [
|
|
452
|
+
`- Draft: ${draft.kind} / ${draft.status}`,
|
|
453
|
+
` - suggested_path: ${draft.suggested_path ?? '-'}`,
|
|
454
|
+
` - title: ${draft.title ?? '-'}`,
|
|
455
|
+
` - evidence: ${(draft.evidence_files ?? []).slice(0, 5).join(', ') || '-'}`,
|
|
456
|
+
` - graph: ${formatSourceRecoveryGraphEvidence(draft.graph_evidence)}`,
|
|
457
|
+
` - unresolved: ${(draft.unresolved_questions ?? []).slice(0, 3).join(' / ') || '-'}`
|
|
458
|
+
].join('\n')).join('\n');
|
|
459
|
+
return [
|
|
460
|
+
`- status: ${sourceRecovery.status}`,
|
|
461
|
+
`- story: ${sourceRecovery.sources?.story?.status ?? '-'}`,
|
|
462
|
+
`- spec: ${sourceRecovery.sources?.spec?.status ?? '-'}`,
|
|
463
|
+
`- architecture: ${sourceRecovery.sources?.architecture?.status ?? '-'}`,
|
|
464
|
+
draftLines
|
|
465
|
+
].join('\n');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function formatSourceRecoveryGraphEvidence(graphEvidence) {
|
|
469
|
+
if (!graphEvidence) return '-';
|
|
470
|
+
const communities = (graphEvidence.affected_communities ?? []).map((community) => `${community.id}:${community.node_count ?? 0}`).slice(0, 3).join(', ') || '-';
|
|
471
|
+
const hubs = (graphEvidence.hub_nodes ?? []).map((node) => `${node.source_file ?? node.id}(${node.degree ?? 0})`).slice(0, 3).join(', ') || '-';
|
|
472
|
+
return `matched=${(graphEvidence.matched_files ?? []).length}, related=${(graphEvidence.related_files ?? []).length}, edges=${graphEvidence.related_edge_count ?? 0}, communities=${communities}, hubs=${hubs}`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function renderSourceAlignmentFindings(findings = []) {
|
|
476
|
+
if (!Array.isArray(findings) || findings.length === 0) return '- なし';
|
|
477
|
+
return findings.slice(0, 8).map((finding) => [
|
|
478
|
+
`- ${finding.severity}: ${finding.type}`,
|
|
479
|
+
` - potential_bug: ${finding.potential_bug}`,
|
|
480
|
+
` - review: ${finding.recommended_review}`,
|
|
481
|
+
` - evidence_files: ${(finding.evidence?.files ?? []).slice(0, 5).join(', ') || '-'}`
|
|
482
|
+
].join('\n')).join('\n');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function renderTaskHandoff(handoff, language = 'ja') {
|
|
486
|
+
return `# 実装依頼パッケージ
|
|
487
|
+
|
|
488
|
+
## 前提
|
|
489
|
+
|
|
490
|
+
- Story: ${handoff.story.title} (${handoff.story.story_id})
|
|
491
|
+
- Run ID: ${handoff.source_run?.run_id ?? '-'}
|
|
492
|
+
- Task: ${handoff.task.id} - ${handoff.task.title}
|
|
493
|
+
- Group: ${handoff.group?.id ?? '-'}
|
|
494
|
+
- VibeProは実装を実行しない
|
|
495
|
+
- 修正はhandoffを受けた人間/AIが行う
|
|
496
|
+
|
|
497
|
+
## 参照成果物
|
|
498
|
+
|
|
499
|
+
- briefing.json: ${handoff.references.briefing_json}
|
|
500
|
+
- briefing.md: ${handoff.references.briefing_markdown}
|
|
501
|
+
- plan.json: ${handoff.references.plan_json}
|
|
502
|
+
- plan.md: ${handoff.references.plan_markdown}
|
|
503
|
+
|
|
504
|
+
## plan要約
|
|
505
|
+
|
|
506
|
+
- 方針: ${handoff.plan_summary.recommended_strategy?.id ?? '-'} - ${handoff.plan_summary.recommended_strategy?.reason ?? '-'}
|
|
507
|
+
- 変更対象: ${handoff.plan_summary.target_file_count}ファイル
|
|
508
|
+
- 検証コマンド: ${handoff.plan_summary.verification_command_count}件
|
|
509
|
+
|
|
510
|
+
## 変更対象ファイル
|
|
511
|
+
|
|
512
|
+
${formatList(handoff.target_files)}
|
|
513
|
+
|
|
514
|
+
## 対象route
|
|
515
|
+
|
|
516
|
+
${formatRoutesWithProtection(handoff.target_routes)}
|
|
517
|
+
|
|
518
|
+
## 現在の保護判定
|
|
519
|
+
|
|
520
|
+
- route_statuses: ${formatObjectSummary(handoff.current_protection.route_statuses)}
|
|
521
|
+
- risk_hints: ${formatObjectSummary(handoff.current_protection.risk_hints)}
|
|
522
|
+
|
|
523
|
+
## 期待する修正後シグナル
|
|
524
|
+
|
|
525
|
+
${formatList(handoff.expected_fix_signals)}
|
|
526
|
+
|
|
527
|
+
## 実行環境前提
|
|
528
|
+
|
|
529
|
+
${formatList(handoff.environment_assumptions)}
|
|
530
|
+
|
|
531
|
+
## 先に読むファイル
|
|
532
|
+
|
|
533
|
+
${formatReadFirst(handoff.read_first_files)}
|
|
534
|
+
|
|
535
|
+
## 実装者への指示
|
|
536
|
+
|
|
537
|
+
${formatList(handoff.implementation_instructions)}
|
|
538
|
+
|
|
539
|
+
## 禁止事項
|
|
540
|
+
|
|
541
|
+
${formatList(handoff.prohibited_actions)}
|
|
542
|
+
|
|
543
|
+
## 検証コマンド
|
|
544
|
+
|
|
545
|
+
${handoff.verification_commands.map((item) => `- \`${item.command}\`: ${item.reason}`).join('\n')}
|
|
546
|
+
|
|
547
|
+
## 完了報告テンプレート
|
|
548
|
+
|
|
549
|
+
${handoff.completion_report_template.map((item) => `- ${item}`).join('\n')}
|
|
550
|
+
`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export function renderTaskExecution(execution, language = 'ja') {
|
|
554
|
+
const warnings = execution.warnings?.length
|
|
555
|
+
? execution.warnings.map((warning) => `- ${warning.id}: ${warning.reason}`).join('\n')
|
|
556
|
+
: '- none';
|
|
557
|
+
return `# 実行セッション
|
|
558
|
+
|
|
559
|
+
## 前提
|
|
560
|
+
|
|
561
|
+
- Story: ${execution.story.title} (${execution.story.story_id})
|
|
562
|
+
- Task: ${execution.task.id} - ${execution.task.title}
|
|
563
|
+
- Group: ${execution.group?.id ?? '-'}
|
|
564
|
+
- VibeProは実装の入口と証跡を管理する
|
|
565
|
+
- 対象コードの修正は人間またはAIエージェントが行う
|
|
566
|
+
|
|
567
|
+
## Warnings
|
|
568
|
+
|
|
569
|
+
${warnings}
|
|
570
|
+
|
|
571
|
+
## 参照成果物
|
|
572
|
+
|
|
573
|
+
- handoff.md: ${execution.references.handoff_markdown}
|
|
574
|
+
- plan.md: ${execution.references.plan_markdown}
|
|
575
|
+
- briefing.md: ${execution.references.briefing_markdown}
|
|
576
|
+
|
|
577
|
+
## 実行フェーズ
|
|
578
|
+
|
|
579
|
+
${execution.phases.map((phase, index) => `${index + 1}. ${phase.title}${phase.required ? ' (required)' : ' (optional)'}`).join('\n')}
|
|
580
|
+
|
|
581
|
+
## 実装対象
|
|
582
|
+
|
|
583
|
+
${formatList(execution.phases.find((phase) => phase.id === 'implement')?.target_files)}
|
|
584
|
+
|
|
585
|
+
## 検証コマンド
|
|
586
|
+
|
|
587
|
+
${execution.phases.find((phase) => phase.id === 'verify')?.commands.map((item) => `- \`${item.command}\`: ${item.reason}`).join('\n') ?? '- なし'}
|
|
588
|
+
|
|
589
|
+
## PR接続
|
|
590
|
+
|
|
591
|
+
- prepare: \`${execution.commands.pr_prepare}\`
|
|
592
|
+
- create: \`${execution.commands.pr_create}\`
|
|
593
|
+
|
|
594
|
+
## 完了報告テンプレート
|
|
595
|
+
|
|
596
|
+
${formatList(execution.completion_report_template)}
|
|
597
|
+
`;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function loadTaskContext(repoRoot, storyId = null) {
|
|
601
|
+
const root = path.resolve(repoRoot);
|
|
602
|
+
const status = await getStoryStatus(root, storyId);
|
|
603
|
+
const manifest = await readManifest(root);
|
|
604
|
+
const taskArtifact = status.artifacts?.story_tasks_json
|
|
605
|
+
?? manifest.stories?.[status.story.story_id]?.plan_tasks_json
|
|
606
|
+
?? toWorkspaceRelative(root, path.join(getWorkspaceDir(root), 'stories', status.story.story_id, 'tasks', 'tasks.json'));
|
|
607
|
+
const taskState = await readStoryTasks(root, taskArtifact);
|
|
608
|
+
const tasks = Array.isArray(taskState.tasks) ? taskState.tasks : [];
|
|
609
|
+
return {
|
|
610
|
+
story: status.story,
|
|
611
|
+
latestRun: status.latestRun,
|
|
612
|
+
taskState,
|
|
613
|
+
tasks
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function readStoryPlan(repoRoot) {
|
|
618
|
+
const planPath = path.join(getWorkspaceDir(repoRoot), 'stories', 'story-plan.json');
|
|
619
|
+
try {
|
|
620
|
+
return JSON.parse(await readFile(planPath, 'utf8'));
|
|
621
|
+
} catch (error) {
|
|
622
|
+
if (error.code === 'ENOENT') {
|
|
623
|
+
throw new Error('Story plan not found. Run `vibepro story plan` first.');
|
|
624
|
+
}
|
|
625
|
+
throw error;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function selectPlanTaskCandidates(plan, options = {}) {
|
|
630
|
+
const storyId = options.storyId ?? null;
|
|
631
|
+
const taskId = options.taskId ?? null;
|
|
632
|
+
const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : null;
|
|
633
|
+
let candidates = Array.isArray(plan.task_candidates) ? plan.task_candidates : [];
|
|
634
|
+
if (storyId) candidates = candidates.filter((candidate) => candidate.story_id === storyId);
|
|
635
|
+
if (taskId) candidates = candidates.filter((candidate) => candidate.id === taskId);
|
|
636
|
+
return limit ? candidates.slice(0, limit) : candidates;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function resolvePlanStory(plan, storyId) {
|
|
640
|
+
const story = (plan.priority_stories ?? []).find((item) => item.story_id === storyId);
|
|
641
|
+
return {
|
|
642
|
+
story_id: storyId,
|
|
643
|
+
title: story?.title ?? storyId,
|
|
644
|
+
ssot: 'local',
|
|
645
|
+
status: 'active',
|
|
646
|
+
horizon: story?.horizon ?? null,
|
|
647
|
+
view: story?.view ?? null,
|
|
648
|
+
period: story?.period ?? null,
|
|
649
|
+
category: story?.category ?? null
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function buildPlanTaskState({ story, plan, candidates }) {
|
|
654
|
+
const tasks = candidates.map((candidate, index) => ({
|
|
655
|
+
id: candidate.id,
|
|
656
|
+
source_type: candidate.source_type ?? 'story_plan_candidate',
|
|
657
|
+
source_id: candidate.id,
|
|
658
|
+
finding_id: null,
|
|
659
|
+
title: candidate.title,
|
|
660
|
+
priority: candidate.priority ?? 'medium',
|
|
661
|
+
status: 'todo',
|
|
662
|
+
order: (index + 1) * 10,
|
|
663
|
+
execution_policy: 'proposal_only',
|
|
664
|
+
mutates_repository: false,
|
|
665
|
+
target_count: Array.isArray(candidate.target_files) ? candidate.target_files.length : 0,
|
|
666
|
+
target_files: candidate.target_files ?? [],
|
|
667
|
+
target_routes: [],
|
|
668
|
+
target_groups: [],
|
|
669
|
+
read_first_files: candidate.read_first_files ?? [],
|
|
670
|
+
recommended_strategy: candidate.recommended_strategy ?? {
|
|
671
|
+
id: 'story-plan',
|
|
672
|
+
reason: candidate.purpose
|
|
673
|
+
},
|
|
674
|
+
implementation_steps: candidate.implementation_steps ?? [],
|
|
675
|
+
acceptance_criteria: candidate.acceptance ?? [],
|
|
676
|
+
source_recovery: candidate.source_recovery ?? null,
|
|
677
|
+
recovery_drafts: candidate.recovery_drafts ?? [],
|
|
678
|
+
source_alignment_findings: candidate.source_alignment_findings ?? [],
|
|
679
|
+
graph_context: candidate.graph_context ?? candidate.source_recovery?.graph_context ?? null,
|
|
680
|
+
pre_fix_briefing: null
|
|
681
|
+
}));
|
|
682
|
+
return {
|
|
683
|
+
schema_version: '0.1.0',
|
|
684
|
+
generated_at: new Date().toISOString(),
|
|
685
|
+
story,
|
|
686
|
+
source_run: {
|
|
687
|
+
run_id: plan.source?.run_id ?? 'story-plan',
|
|
688
|
+
gate_status: plan.summary?.coverage_status ?? 'unknown',
|
|
689
|
+
source_plan_generated_at: plan.generated_at
|
|
690
|
+
},
|
|
691
|
+
tasks
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
async function resolveTaskSelection(repoRoot, options = {}) {
|
|
696
|
+
const context = await loadTaskContext(repoRoot, options.storyId);
|
|
697
|
+
const task = findTask(context.tasks, options.taskId);
|
|
698
|
+
const group = options.groupId ? findTargetGroup(task, options.groupId) : null;
|
|
699
|
+
return { context, task, group };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function buildTaskBriefing({ story, sourceRun, task, group }) {
|
|
703
|
+
const targetRoutes = group ? group.routes ?? [] : task.target_routes ?? [];
|
|
704
|
+
const targetFiles = group ? group.target_files ?? [] : task.target_files ?? [];
|
|
705
|
+
const readFirstFiles = resolveBriefReadFirstFiles({ targetFiles, group, task });
|
|
706
|
+
return {
|
|
707
|
+
schema_version: '0.1.0',
|
|
708
|
+
generated_at: new Date().toISOString(),
|
|
709
|
+
mode: 'pre_fix_briefing',
|
|
710
|
+
story,
|
|
711
|
+
source_run: sourceRun,
|
|
712
|
+
task: {
|
|
713
|
+
id: task.id,
|
|
714
|
+
title: task.title,
|
|
715
|
+
priority: task.priority,
|
|
716
|
+
finding_id: task.finding_id,
|
|
717
|
+
source_type: task.source_type,
|
|
718
|
+
source_id: task.source_id
|
|
719
|
+
},
|
|
720
|
+
group: group ? {
|
|
721
|
+
id: group.id,
|
|
722
|
+
title: group.title,
|
|
723
|
+
route_count: group.route_count,
|
|
724
|
+
classification: group.classification
|
|
725
|
+
} : null,
|
|
726
|
+
execution_policy: task.execution_policy,
|
|
727
|
+
mutates_repository: false,
|
|
728
|
+
guardrails: [
|
|
729
|
+
'このCLIは対象リポジトリのコードを修正しない',
|
|
730
|
+
'修正に入る前の作業指示と確認対象だけを生成する',
|
|
731
|
+
'実装時は対象route、対象ファイル、完了条件を再確認する'
|
|
732
|
+
],
|
|
733
|
+
target_routes: targetRoutes,
|
|
734
|
+
target_files: targetFiles,
|
|
735
|
+
read_first_files: readFirstFiles,
|
|
736
|
+
graph_context: task.graph_context ?? null,
|
|
737
|
+
pre_fix_briefing: task.pre_fix_briefing ?? null,
|
|
738
|
+
source_recovery: task.source_recovery ?? null,
|
|
739
|
+
recovery_drafts: task.recovery_drafts ?? [],
|
|
740
|
+
source_alignment_findings: task.source_alignment_findings ?? [],
|
|
741
|
+
recommended_strategy: group?.recommended_strategy ?? task.recommended_strategy ?? null,
|
|
742
|
+
implementation_steps: task.implementation_steps ?? [],
|
|
743
|
+
acceptance_criteria: group?.acceptance_criteria ?? task.acceptance_criteria ?? []
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function buildTaskPlan({ briefing }) {
|
|
748
|
+
const suffix = briefing.group?.id ? `${briefing.task.id}-${briefing.group.id}` : briefing.task.id;
|
|
749
|
+
return {
|
|
750
|
+
schema_version: '0.1.0',
|
|
751
|
+
generated_at: new Date().toISOString(),
|
|
752
|
+
mode: 'implementation_plan',
|
|
753
|
+
story: briefing.story,
|
|
754
|
+
source_run: briefing.source_run,
|
|
755
|
+
task: briefing.task,
|
|
756
|
+
group: briefing.group,
|
|
757
|
+
execution: {
|
|
758
|
+
plan_allows_repository_changes: true,
|
|
759
|
+
cli_mutates_repository: false,
|
|
760
|
+
note: 'このplanは修正可能な作業計画。ただしCLI自身は対象リポジトリのコードを変更しない。'
|
|
761
|
+
},
|
|
762
|
+
target_routes: briefing.target_routes,
|
|
763
|
+
target_files: briefing.target_files,
|
|
764
|
+
read_first_files: briefing.read_first_files,
|
|
765
|
+
recommended_strategy: briefing.recommended_strategy,
|
|
766
|
+
implementation_steps: briefing.implementation_steps,
|
|
767
|
+
verification_commands: buildVerificationCommands(suffix),
|
|
768
|
+
acceptance_criteria: briefing.acceptance_criteria,
|
|
769
|
+
rollback_considerations: [
|
|
770
|
+
'対象ファイル単位で差分を確認し、対象グループ外の変更が混ざっていないか確認する',
|
|
771
|
+
'認証境界を変更する場合は、public API と webhook API を巻き込んでいないか確認する',
|
|
772
|
+
'診断再実行後に新しいCritical/Highが増えた場合は、変更を戻せる粒度でコミットを分ける'
|
|
773
|
+
],
|
|
774
|
+
guardrails: [
|
|
775
|
+
'このplanは修正可能な作業計画',
|
|
776
|
+
'CLI自身は対象リポジトリのコードを変更しない',
|
|
777
|
+
'実装修正は人間またはAIエージェントが別操作として行う',
|
|
778
|
+
'修正後はVibePro診断を再実行して完了条件を確認する'
|
|
779
|
+
],
|
|
780
|
+
source_briefing: {
|
|
781
|
+
mode: briefing.mode,
|
|
782
|
+
generated_at: briefing.generated_at
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function buildTaskHandoff({ briefing, plan, briefingArtifacts, planArtifacts }) {
|
|
788
|
+
return {
|
|
789
|
+
schema_version: '0.1.0',
|
|
790
|
+
generated_at: new Date().toISOString(),
|
|
791
|
+
mode: 'implementation_handoff',
|
|
792
|
+
story: plan.story,
|
|
793
|
+
source_run: plan.source_run,
|
|
794
|
+
task: plan.task,
|
|
795
|
+
group: plan.group,
|
|
796
|
+
execution: {
|
|
797
|
+
vibepro_mutates_repository: false,
|
|
798
|
+
recipient_may_mutate_repository: true,
|
|
799
|
+
note: 'VibeProは実装を実行しない。修正はhandoffを受けた人間/AIが行う。'
|
|
800
|
+
},
|
|
801
|
+
references: {
|
|
802
|
+
briefing_json: briefingArtifacts.json,
|
|
803
|
+
briefing_markdown: briefingArtifacts.markdown,
|
|
804
|
+
plan_json: planArtifacts.json,
|
|
805
|
+
plan_markdown: planArtifacts.markdown
|
|
806
|
+
},
|
|
807
|
+
plan_summary: {
|
|
808
|
+
recommended_strategy: plan.recommended_strategy,
|
|
809
|
+
target_file_count: plan.target_files.length,
|
|
810
|
+
verification_command_count: plan.verification_commands.length,
|
|
811
|
+
acceptance_criteria_count: plan.acceptance_criteria.length
|
|
812
|
+
},
|
|
813
|
+
target_routes: plan.target_routes,
|
|
814
|
+
target_files: plan.target_files,
|
|
815
|
+
read_first_files: plan.read_first_files,
|
|
816
|
+
current_protection: summarizeCurrentProtection(plan.target_routes),
|
|
817
|
+
expected_fix_signals: [
|
|
818
|
+
'対象routeのprotection_statusがprotected_by_routeまたはprotected_by_middlewareになる',
|
|
819
|
+
'対象routeからprivileged_route_unprotectedが消える',
|
|
820
|
+
'VibePro診断で対象タスクの完了条件を満たす'
|
|
821
|
+
],
|
|
822
|
+
environment_assumptions: [
|
|
823
|
+
'対象リポジトリのルートで実行する',
|
|
824
|
+
'VibePro CLIがPATHにない場合は npx vibepro を使う',
|
|
825
|
+
'npm test と npm run lint は対象リポジトリのpackage.jsonに定義されている場合に実行する'
|
|
826
|
+
],
|
|
827
|
+
implementation_instructions: buildImplementationInstructions({ briefing, plan, planArtifacts }),
|
|
828
|
+
prohibited_actions: [
|
|
829
|
+
'対象グループ外のファイルを同じ作業で変更しない',
|
|
830
|
+
'VibeProの生成物を根拠なく手編集しない',
|
|
831
|
+
'public API と webhook API を巻き込む認証境界変更を行わない',
|
|
832
|
+
'検証コマンドを省略したまま完了扱いにしない'
|
|
833
|
+
],
|
|
834
|
+
verification_commands: plan.verification_commands,
|
|
835
|
+
completion_report_template: [
|
|
836
|
+
'変更したファイル',
|
|
837
|
+
'採用した修正方針',
|
|
838
|
+
'実行した検証コマンドと結果',
|
|
839
|
+
'未解決のリスクまたは次に見るべき点'
|
|
840
|
+
],
|
|
841
|
+
guardrails: [
|
|
842
|
+
'VibeProは実装を実行しない',
|
|
843
|
+
'修正はhandoffを受けた人間/AIが行う',
|
|
844
|
+
'handoffは実装前の依頼パッケージであり、対象コードの変更結果ではない'
|
|
845
|
+
]
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function buildImplementationInstructions({ briefing, plan, planArtifacts }) {
|
|
850
|
+
return [
|
|
851
|
+
`${planArtifacts.markdown} を読み、対象ファイルと完了条件を確認する`,
|
|
852
|
+
'先に読むファイルを確認し、既存の認証境界とgraphify文脈を把握する',
|
|
853
|
+
`${plan.recommended_strategy?.id ?? 'recommended strategy'} 方針に沿って対象ファイルを修正する`,
|
|
854
|
+
'修正後に検証コマンドを実行し、完了条件を満たすことを確認する',
|
|
855
|
+
`${briefing.task.id}${briefing.group?.id ? ` / ${briefing.group.id}` : ''} の範囲だけを完了報告する`
|
|
856
|
+
];
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function buildVerificationCommands(suffix) {
|
|
860
|
+
return [
|
|
861
|
+
{
|
|
862
|
+
command: 'npm test',
|
|
863
|
+
reason: '対象リポジトリの既存テストを確認する'
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
command: 'npm run lint',
|
|
867
|
+
reason: 'lintが定義されている場合に静的チェックを確認する'
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
command: `npx vibepro diagnose . --run-id verify-${suffix}`,
|
|
871
|
+
reason: 'VibePro診断で対象タスクの検出事項が改善したか確認する'
|
|
872
|
+
}
|
|
873
|
+
];
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function buildPrPrepareCommand({ story, task, group, baseRef }) {
|
|
877
|
+
return [
|
|
878
|
+
'npx vibepro pr prepare .',
|
|
879
|
+
`--story-id ${shellQuote(story.story_id)}`,
|
|
880
|
+
`--task ${shellQuote(task.id)}`,
|
|
881
|
+
group?.id ? `--group ${shellQuote(group.id)}` : null,
|
|
882
|
+
baseRef ? `--base ${shellQuote(baseRef)}` : null
|
|
883
|
+
].filter(Boolean).join(' ');
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function buildPrCreateCommand({ story, task, group, baseRef, dryRun }) {
|
|
887
|
+
return [
|
|
888
|
+
'npx vibepro pr create .',
|
|
889
|
+
`--story-id ${shellQuote(story.story_id)}`,
|
|
890
|
+
`--task ${shellQuote(task.id)}`,
|
|
891
|
+
group?.id ? `--group ${shellQuote(group.id)}` : null,
|
|
892
|
+
baseRef ? `--base ${shellQuote(baseRef)}` : null,
|
|
893
|
+
dryRun ? '--dry-run' : null
|
|
894
|
+
].filter(Boolean).join(' ');
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function shellQuote(value) {
|
|
898
|
+
if (/^[a-zA-Z0-9_./:=@+-]+$/.test(value)) return value;
|
|
899
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function summarizeCurrentProtection(routes = []) {
|
|
903
|
+
const routeStatuses = {};
|
|
904
|
+
const riskHints = {};
|
|
905
|
+
for (const route of routes) {
|
|
906
|
+
const status = route.protection_status ?? 'unknown';
|
|
907
|
+
routeStatuses[status] = (routeStatuses[status] ?? 0) + 1;
|
|
908
|
+
for (const hint of route.risk_hints ?? []) {
|
|
909
|
+
riskHints[hint] = (riskHints[hint] ?? 0) + 1;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
route_statuses: routeStatuses,
|
|
914
|
+
risk_hints: riskHints,
|
|
915
|
+
routes: routes.map((route) => ({
|
|
916
|
+
route_path: route.route_path,
|
|
917
|
+
file: route.file,
|
|
918
|
+
methods: route.methods ?? [],
|
|
919
|
+
protection_status: route.protection_status ?? 'unknown',
|
|
920
|
+
protection_evidence: route.protection_evidence ?? [],
|
|
921
|
+
risk_hints: route.risk_hints ?? []
|
|
922
|
+
}))
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function resolveBriefReadFirstFiles({ targetFiles, group, task }) {
|
|
927
|
+
const selected = [];
|
|
928
|
+
const seen = new Set();
|
|
929
|
+
const add = (item) => {
|
|
930
|
+
if (!item?.file || seen.has(item.file)) return;
|
|
931
|
+
seen.add(item.file);
|
|
932
|
+
selected.push(item);
|
|
933
|
+
};
|
|
934
|
+
for (const item of group?.read_first_files ?? []) add(item);
|
|
935
|
+
for (const file of targetFiles) {
|
|
936
|
+
add({ file, reason: group ? `対象グループ ${group.id} の修正候補` : `対象タスク ${task.id} の修正候補` });
|
|
937
|
+
}
|
|
938
|
+
for (const item of task.read_first_files ?? []) {
|
|
939
|
+
if (group && item.reason?.startsWith('対象API route:')) continue;
|
|
940
|
+
add(item);
|
|
941
|
+
}
|
|
942
|
+
return selected;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function getTaskArtifactDir(repoRoot, storyId, taskId, groupId = null) {
|
|
946
|
+
const baseDir = path.join(getWorkspaceDir(repoRoot), 'stories', safeSegment(storyId), 'tasks', safeSegment(taskId));
|
|
947
|
+
return groupId ? path.join(baseDir, 'groups', safeSegment(groupId)) : baseDir;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function findTask(tasks, taskId) {
|
|
951
|
+
if (!taskId) throw new Error('--task is required');
|
|
952
|
+
const task = tasks.find((item) => item.id === taskId);
|
|
953
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
954
|
+
return task;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function findTargetGroup(task, groupId) {
|
|
958
|
+
const group = (task.target_groups ?? []).find((item) => item.id === groupId);
|
|
959
|
+
if (!group) throw new Error(`Target group not found: ${groupId}`);
|
|
960
|
+
return group;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function formatTargetGroups(groups = []) {
|
|
964
|
+
if (!Array.isArray(groups) || groups.length === 0) return '-';
|
|
965
|
+
return groups.map((group) => `${group.id}(${group.route_count})`).join(', ');
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function formatGroups(groups = []) {
|
|
969
|
+
if (!Array.isArray(groups) || groups.length === 0) return '- なし';
|
|
970
|
+
return groups.map((group) => `- ${group.id}: ${group.title ?? group.id} (${group.route_count}件)`).join('\n');
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function formatRoutes(routes = []) {
|
|
974
|
+
if (!Array.isArray(routes) || routes.length === 0) return '- なし';
|
|
975
|
+
return routes.map((route) => `- ${route.route_path} (${route.methods?.join(', ') || '-'}) - ${route.file}`).join('\n');
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function formatRoutesWithProtection(routes = []) {
|
|
979
|
+
if (!Array.isArray(routes) || routes.length === 0) return '- なし';
|
|
980
|
+
return routes.map((route) => [
|
|
981
|
+
`- ${route.route_path} (${route.methods?.join(', ') || '-'}) - ${route.file}`,
|
|
982
|
+
` - protection=${route.protection_status ?? 'unknown'}, evidence=${route.protection_evidence?.join(', ') || '-'}, risk=${route.risk_hints?.join(', ') || '-'}`
|
|
983
|
+
].join('\n')).join('\n');
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function formatReadFirst(files = []) {
|
|
987
|
+
if (!Array.isArray(files) || files.length === 0) return '- なし';
|
|
988
|
+
return files.map((item) => `- ${item.file}${item.reason ? `: ${item.reason}` : ''}`).join('\n');
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function formatList(items = []) {
|
|
992
|
+
if (!Array.isArray(items) || items.length === 0) return '- なし';
|
|
993
|
+
return items.map((item) => `- ${item}`).join('\n');
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function normalizeWarnings(warnings) {
|
|
997
|
+
return warnings.filter((warning) => warning && typeof warning === 'object');
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function formatCommunities(communities = []) {
|
|
1001
|
+
if (!Array.isArray(communities) || communities.length === 0) return '-';
|
|
1002
|
+
return communities.map((community) => `${community.id}(route:${community.route_count}, node:${community.node_count}, edge:${community.edge_count})`).join(', ');
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function formatHubNodes(nodes = []) {
|
|
1006
|
+
if (!Array.isArray(nodes) || nodes.length === 0) return '-';
|
|
1007
|
+
return nodes.map((node) => node.id ?? node.label ?? '-').join(', ');
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function formatObjectSummary(value = {}) {
|
|
1011
|
+
const entries = Object.entries(value);
|
|
1012
|
+
if (entries.length === 0) return '-';
|
|
1013
|
+
return entries.map(([key, count]) => `${key}:${count}`).join(', ');
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function groupBy(items, getKey) {
|
|
1017
|
+
return items.reduce((groups, item) => {
|
|
1018
|
+
const key = getKey(item);
|
|
1019
|
+
groups[key] = [...(groups[key] ?? []), item];
|
|
1020
|
+
return groups;
|
|
1021
|
+
}, {});
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function safeSegment(value) {
|
|
1025
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(value)) {
|
|
1026
|
+
throw new Error(`Invalid path segment: ${value}`);
|
|
1027
|
+
}
|
|
1028
|
+
return value;
|
|
1029
|
+
}
|