@galda/cli 0.10.68 → 0.10.70
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/app/index.html +434 -118
- package/app/review-merge-celebration-preview.html +21 -0
- package/engine/chrome-safety.mjs +34 -0
- package/engine/lib.mjs +563 -3
- package/engine/server.mjs +479 -113
- package/engine/shot.mjs +4 -0
- package/engine/verify.mjs +27 -6
- package/package.json +1 -1
package/engine/lib.mjs
CHANGED
|
@@ -157,6 +157,215 @@ export function sumUsageForToday(tasks, nowMs) {
|
|
|
157
157
|
return sumUsage(usages);
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
// Internal quality metrics -------------------------------------------------------
|
|
161
|
+
// These definitions intentionally describe sanitized state transitions only.
|
|
162
|
+
// Prompt/request bodies, worker reports, file contents, names, and emails do not
|
|
163
|
+
// belong in the metric payload; server.mjs writes only ids, status/category and
|
|
164
|
+
// timestamps to the internal event log.
|
|
165
|
+
export const INTERNAL_QUALITY_EVENT_DEFINITIONS = Object.freeze([
|
|
166
|
+
{ type: 'goal_created', description: '依頼が goal として受け付けられた。prompt 本文は含めない。' },
|
|
167
|
+
{ type: 'goal_doing_started', description: 'goal が Doing として実作業を開始した。' },
|
|
168
|
+
{ type: 'goal_review_reached', description: 'goal が Review に到達した。' },
|
|
169
|
+
{ type: 'goal_failed', description: 'goal が failed / blocked / interrupted / partial のいずれかに入った。' },
|
|
170
|
+
{ type: 'goal_folded', description: 'goal が既存 Review goal に統合された。' },
|
|
171
|
+
{ type: 'goal_fold_unfolded', description: '統合済み goal が人間操作で戻された。誤統合補正として数える。' },
|
|
172
|
+
{ type: 'goal_reverted', description: 'goal が revert された。' },
|
|
173
|
+
{ type: 'task_created', description: 'worker task が作られた。title/detail/prompt は含めない。' },
|
|
174
|
+
{ type: 'task_doing_started', description: 'worker task が実行開始した。' },
|
|
175
|
+
{ type: 'task_finished', description: 'worker task が terminal status になった。status と error category だけを含める。' },
|
|
176
|
+
{ type: 'task_retry_requested', description: '人間または内部処理が task の再試行を要求した。' },
|
|
177
|
+
{ type: 'task_skipped', description: 'task が skip された。理由本文は含めない。' },
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
const INTERNAL_TERMINAL_TASK_STATUSES = new Set(['done', 'failed', 'interrupted', 'skipped']);
|
|
181
|
+
const INTERNAL_FAILURE_GOAL_STATUSES = new Set(['failed', 'blocked', 'interrupted', 'partial']);
|
|
182
|
+
const INTERNAL_OPEN_GOAL_STATUSES = new Set(['stacked', 'planning', 'running', 'needsInput', 'blocked', 'partial', 'retesting']);
|
|
183
|
+
|
|
184
|
+
function timeMs(value) {
|
|
185
|
+
if (!value) return null;
|
|
186
|
+
const n = Date.parse(value);
|
|
187
|
+
return Number.isFinite(n) ? n : null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function firstTime(current, value) {
|
|
191
|
+
const n = timeMs(value);
|
|
192
|
+
if (n == null) return current ?? null;
|
|
193
|
+
return current == null ? n : Math.min(current, n);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function summarizeMs(values) {
|
|
197
|
+
const list = values.filter((n) => Number.isFinite(n) && n >= 0).sort((a, b) => a - b);
|
|
198
|
+
if (!list.length) return { count: 0, avg: null, p50: null, p90: null, min: null, max: null };
|
|
199
|
+
const pick = (q) => list[Math.min(list.length - 1, Math.floor((list.length - 1) * q))];
|
|
200
|
+
return {
|
|
201
|
+
count: list.length,
|
|
202
|
+
avg: Math.round(list.reduce((a, b) => a + b, 0) / list.length),
|
|
203
|
+
p50: pick(0.5),
|
|
204
|
+
p90: pick(0.9),
|
|
205
|
+
min: list[0],
|
|
206
|
+
max: list[list.length - 1],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function rate(count, total) {
|
|
211
|
+
return { count, total, rate: total > 0 ? count / total : null };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function classifyInternalQualityTaskError(task = {}) {
|
|
215
|
+
if (!['failed', 'interrupted'].includes(task.status)) return null;
|
|
216
|
+
const haystack = [
|
|
217
|
+
task.result,
|
|
218
|
+
task.error,
|
|
219
|
+
task.prError,
|
|
220
|
+
task.testResult?.detail,
|
|
221
|
+
...(Array.isArray(task.testResult?.failingNames) ? task.testResult.failingNames : []),
|
|
222
|
+
].filter(Boolean).join('\n');
|
|
223
|
+
if (task.testResult?.infraOnly || /server exited|server did not report ready|TimeoutError|Target closed|ECONNREFUSED|ENOSPC|EACCES|EROFS|net::|Navigation timeout|Waiting failed|timed out waiting/i.test(haystack)) {
|
|
224
|
+
return 'infra';
|
|
225
|
+
}
|
|
226
|
+
if (/claude|codex|worker|agent|auth|credential|logged out|bad credentials|not connected|command not found|spawn|rate limit|quota|usage limit|session unavailable|warm session|exit code|run limit|SIGTERM|SIGKILL/i.test(haystack)) {
|
|
227
|
+
return 'agent';
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function computeInternalQualityMetrics({ goals = [], tasks = [], events = [], nowMs = Date.now(), staleAfterMs = 24 * 60 * 60 * 1000 } = {}) {
|
|
233
|
+
const goalRows = new Map();
|
|
234
|
+
const taskRows = new Map();
|
|
235
|
+
const goal = (id) => {
|
|
236
|
+
if (id == null) return null;
|
|
237
|
+
if (!goalRows.has(id)) goalRows.set(id, { id, statuses: new Set(), folded: false, mergeCorrected: false });
|
|
238
|
+
return goalRows.get(id);
|
|
239
|
+
};
|
|
240
|
+
const task = (id) => {
|
|
241
|
+
if (id == null) return null;
|
|
242
|
+
if (!taskRows.has(id)) taskRows.set(id, { id, statuses: new Set(), retry: false });
|
|
243
|
+
return taskRows.get(id);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
for (const g of goals ?? []) {
|
|
247
|
+
const row = goal(g?.id);
|
|
248
|
+
if (!row) continue;
|
|
249
|
+
row.projectId = g.projectId ?? row.projectId ?? null;
|
|
250
|
+
row.createdAt = firstTime(row.createdAt, g.createdAt);
|
|
251
|
+
row.doingAt = firstTime(row.doingAt, g.startedAt);
|
|
252
|
+
row.status = g.status ?? row.status ?? null;
|
|
253
|
+
if (g.status) row.statuses.add(g.status);
|
|
254
|
+
if (g.status === 'review') row.reviewAt = firstTime(row.reviewAt, g.reviewReachedAt ?? g.updatedAt);
|
|
255
|
+
if (g.status === 'folded' || g.foldedInto != null) row.folded = true;
|
|
256
|
+
if (g.noFold || g.status === 'reverted') row.mergeCorrected = true;
|
|
257
|
+
}
|
|
258
|
+
for (const t of tasks ?? []) {
|
|
259
|
+
const row = task(t?.id);
|
|
260
|
+
if (!row) continue;
|
|
261
|
+
row.goalId = t.goalId ?? row.goalId ?? null;
|
|
262
|
+
row.projectId = t.projectId ?? row.projectId ?? null;
|
|
263
|
+
row.createdAt = firstTime(row.createdAt, t.createdAt);
|
|
264
|
+
row.doingAt = firstTime(row.doingAt, t.startedAt);
|
|
265
|
+
row.finishedAt = firstTime(row.finishedAt, t.finishedAt);
|
|
266
|
+
row.status = t.status ?? row.status ?? null;
|
|
267
|
+
if (t.status) row.statuses.add(t.status);
|
|
268
|
+
if ((t.runAttempts ?? 0) > 1 || (t.retryCount ?? 0) > 0) row.retry = true;
|
|
269
|
+
row.errorCategory = row.errorCategory ?? classifyInternalQualityTaskError(t);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const e of events ?? []) {
|
|
273
|
+
const at = e?.at;
|
|
274
|
+
if (e?.goalId != null) {
|
|
275
|
+
const row = goal(e.goalId);
|
|
276
|
+
if (row) {
|
|
277
|
+
row.projectId = e.projectId ?? row.projectId ?? null;
|
|
278
|
+
if (e.type === 'goal_created') row.createdAt = firstTime(row.createdAt, at);
|
|
279
|
+
if (e.type === 'goal_doing_started') row.doingAt = firstTime(row.doingAt, at);
|
|
280
|
+
if (e.type === 'goal_review_reached') row.reviewAt = firstTime(row.reviewAt, at);
|
|
281
|
+
if (e.type === 'goal_failed') row.statuses.add(e.status ?? 'failed');
|
|
282
|
+
if (e.type === 'goal_folded') row.folded = true;
|
|
283
|
+
if (e.type === 'goal_fold_unfolded' || e.type === 'goal_reverted') row.mergeCorrected = true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (e?.taskId != null) {
|
|
287
|
+
const row = task(e.taskId);
|
|
288
|
+
if (row) {
|
|
289
|
+
row.goalId = e.goalId ?? row.goalId ?? null;
|
|
290
|
+
row.projectId = e.projectId ?? row.projectId ?? null;
|
|
291
|
+
if (e.type === 'task_created') row.createdAt = firstTime(row.createdAt, at);
|
|
292
|
+
if (e.type === 'task_doing_started') row.doingAt = firstTime(row.doingAt, at);
|
|
293
|
+
if (e.type === 'task_finished') {
|
|
294
|
+
row.finishedAt = firstTime(row.finishedAt, at);
|
|
295
|
+
row.status = e.status ?? row.status ?? null;
|
|
296
|
+
if (e.status) row.statuses.add(e.status);
|
|
297
|
+
row.errorCategory = e.category ?? row.errorCategory ?? null;
|
|
298
|
+
}
|
|
299
|
+
if (e.type === 'task_retry_requested') row.retry = true;
|
|
300
|
+
if (e.type === 'task_skipped') {
|
|
301
|
+
row.status = 'skipped';
|
|
302
|
+
row.statuses.add('skipped');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const goalList = [...goalRows.values()];
|
|
309
|
+
const taskList = [...taskRows.values()];
|
|
310
|
+
const startedGoals = goalList.filter((g) => g.doingAt != null);
|
|
311
|
+
const reviewGoals = startedGoals.filter((g) => g.reviewAt != null || g.statuses.has('review'));
|
|
312
|
+
const failedGoals = startedGoals.filter((g) => [...g.statuses].some((s) => INTERNAL_FAILURE_GOAL_STATUSES.has(s)));
|
|
313
|
+
const terminalTasks = taskList.filter((t) => INTERNAL_TERMINAL_TASK_STATUSES.has(t.status) || [...t.statuses].some((s) => INTERNAL_TERMINAL_TASK_STATUSES.has(s)));
|
|
314
|
+
const failedTasks = taskList.filter((t) => t.statuses.has('failed') || t.statuses.has('interrupted') || ['failed', 'interrupted'].includes(t.status));
|
|
315
|
+
const retryTasks = taskList.filter((t) => t.retry);
|
|
316
|
+
const skippedTasks = taskList.filter((t) => t.statuses.has('skipped') || t.status === 'skipped');
|
|
317
|
+
const infraTasks = terminalTasks.filter((t) => t.errorCategory === 'infra');
|
|
318
|
+
const agentTasks = terminalTasks.filter((t) => t.errorCategory === 'agent');
|
|
319
|
+
const foldedGoals = goalList.filter((g) => g.folded);
|
|
320
|
+
const mergeCorrectedGoals = foldedGoals.filter((g) => g.mergeCorrected);
|
|
321
|
+
const staleGoals = goalList.filter((g) => {
|
|
322
|
+
if (!INTERNAL_OPEN_GOAL_STATUSES.has(g.status)) return false;
|
|
323
|
+
const base = g.doingAt ?? g.createdAt;
|
|
324
|
+
return base != null && nowMs - base >= staleAfterMs;
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
generatedAt: new Date(nowMs).toISOString(),
|
|
329
|
+
privacy: {
|
|
330
|
+
promptIncluded: false,
|
|
331
|
+
personalInfoIncluded: false,
|
|
332
|
+
fields: ['ids', 'projectId', 'status', 'category', 'timestamps'],
|
|
333
|
+
},
|
|
334
|
+
staleAfterMs,
|
|
335
|
+
eventDefinitions: INTERNAL_QUALITY_EVENT_DEFINITIONS,
|
|
336
|
+
counts: {
|
|
337
|
+
goals: goalList.length,
|
|
338
|
+
startedGoals: startedGoals.length,
|
|
339
|
+
reviewReachedGoals: reviewGoals.length,
|
|
340
|
+
failedGoals: failedGoals.length,
|
|
341
|
+
tasks: taskList.length,
|
|
342
|
+
terminalTasks: terminalTasks.length,
|
|
343
|
+
failedTasks: failedTasks.length,
|
|
344
|
+
retryTasks: retryTasks.length,
|
|
345
|
+
skippedTasks: skippedTasks.length,
|
|
346
|
+
infraErrorTasks: infraTasks.length,
|
|
347
|
+
agentErrorTasks: agentTasks.length,
|
|
348
|
+
foldedGoals: foldedGoals.length,
|
|
349
|
+
mergeCorrectedGoals: mergeCorrectedGoals.length,
|
|
350
|
+
staleGoals: staleGoals.length,
|
|
351
|
+
},
|
|
352
|
+
rates: {
|
|
353
|
+
reviewReach: rate(reviewGoals.length, startedGoals.length),
|
|
354
|
+
failure: rate(failedGoals.length, startedGoals.length),
|
|
355
|
+
retry: rate(retryTasks.length, failedTasks.length || terminalTasks.length),
|
|
356
|
+
infraError: rate(infraTasks.length, terminalTasks.length),
|
|
357
|
+
agentError: rate(agentTasks.length, terminalTasks.length),
|
|
358
|
+
misintegration: rate(mergeCorrectedGoals.length, foldedGoals.length),
|
|
359
|
+
stale: rate(staleGoals.length, goalList.length),
|
|
360
|
+
skip: rate(skippedTasks.length, terminalTasks.length || taskList.length),
|
|
361
|
+
},
|
|
362
|
+
durations: {
|
|
363
|
+
requestToDoingMs: summarizeMs(startedGoals.map((g) => g.doingAt - g.createdAt)),
|
|
364
|
+
doingToReviewMs: summarizeMs(reviewGoals.map((g) => g.reviewAt == null ? null : g.reviewAt - g.doingAt)),
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
160
369
|
// Token-frugal execution planning -------------------------------------------------
|
|
161
370
|
// A visible Project is the user's mental workspace. Internally, Manager can
|
|
162
371
|
// route goals into smaller workstreams and session policies so unrelated work
|
|
@@ -268,6 +477,86 @@ export function buildContextHandoffSummary({ goal = null, tasks = [] } = {}) {
|
|
|
268
477
|
].filter(Boolean).join('\n').slice(0, 4000);
|
|
269
478
|
}
|
|
270
479
|
|
|
480
|
+
export function goalSessionFor(goal, agent) {
|
|
481
|
+
if (!goal) return null;
|
|
482
|
+
const a = String(agent || goal.agent || 'claude-code');
|
|
483
|
+
const map = goal.sessions && typeof goal.sessions === 'object' ? goal.sessions : null;
|
|
484
|
+
if (map?.[a]) return String(map[a]);
|
|
485
|
+
if (goal.sessionId && (!goal.sessionAgent || goal.sessionAgent === a || !map)) return String(goal.sessionId);
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function setGoalAgentSession(goal, agent, sessionId) {
|
|
490
|
+
if (!goal || !sessionId) return null;
|
|
491
|
+
const a = String(agent || goal.agent || 'claude-code');
|
|
492
|
+
goal.sessions = { ...(goal.sessions && typeof goal.sessions === 'object' ? goal.sessions : {}), [a]: String(sessionId) };
|
|
493
|
+
if (!goal.agent || goal.agent === a) {
|
|
494
|
+
goal.sessionId = String(sessionId);
|
|
495
|
+
goal.sessionAgent = a;
|
|
496
|
+
}
|
|
497
|
+
return String(sessionId);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export function switchGoalAgentSession(goal, nextAgent) {
|
|
501
|
+
if (!goal) return null;
|
|
502
|
+
if (goal.sessionId && goal.agent) setGoalAgentSession(goal, goal.agent, goal.sessionId);
|
|
503
|
+
goal.agent = nextAgent;
|
|
504
|
+
const sessionId = goalSessionFor(goal, nextAgent);
|
|
505
|
+
goal.sessionId = sessionId;
|
|
506
|
+
goal.sessionAgent = nextAgent;
|
|
507
|
+
return sessionId;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function compactList(items, { label, empty = 'not detected', max = 12 } = {}) {
|
|
511
|
+
const vals = [...new Set((items ?? []).map((x) => String(x ?? '').trim()).filter(Boolean))];
|
|
512
|
+
if (!vals.length) return `${label}: ${empty}`;
|
|
513
|
+
const shown = vals.slice(0, max);
|
|
514
|
+
const rest = vals.length - shown.length;
|
|
515
|
+
return `${label}: ${shown.join(', ')}${rest > 0 ? ` (+${rest} more)` : ''}`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export function buildWorkerContextBlock({
|
|
519
|
+
agentName = 'claude-code',
|
|
520
|
+
model = null,
|
|
521
|
+
effort = null,
|
|
522
|
+
permissionMode = null,
|
|
523
|
+
cwd = null,
|
|
524
|
+
resumeSession = null,
|
|
525
|
+
sessionMode = null,
|
|
526
|
+
projectRules = [],
|
|
527
|
+
skills = [],
|
|
528
|
+
mcpSettings = [],
|
|
529
|
+
images = [],
|
|
530
|
+
selectedSkill = null,
|
|
531
|
+
} = {}) {
|
|
532
|
+
const displayName = agentName === 'codex' ? 'Codex' : 'Claude Code';
|
|
533
|
+
const rules = (projectRules ?? []).map((r) => typeof r === 'string' ? r : r?.name).filter(Boolean);
|
|
534
|
+
const skillNames = (skills ?? []).map((s) => {
|
|
535
|
+
if (typeof s === 'string') return s;
|
|
536
|
+
const prefix = s?.kind === 'command' ? '/' : '';
|
|
537
|
+
const suffix = s?.source ? ` (${s.source})` : '';
|
|
538
|
+
return s?.name ? `${prefix}${s.name}${suffix}` : '';
|
|
539
|
+
});
|
|
540
|
+
const session = resumeSession
|
|
541
|
+
? `resume ${resumeSession}`
|
|
542
|
+
: sessionMode === 'cold-handoff' || sessionMode === 'compact'
|
|
543
|
+
? `${sessionMode}; no hidden transcript assumed`
|
|
544
|
+
: 'fresh for this agent/goal';
|
|
545
|
+
return [
|
|
546
|
+
'MANAGER-RUN CONTEXT (explicitly inherited; missing items are shown instead of silently dropped):',
|
|
547
|
+
`Agent: ${displayName} (${agentName})`,
|
|
548
|
+
`Selected model/effort: ${model || 'not specified'} / ${effort || 'not specified'}`,
|
|
549
|
+
`Permission mode: ${permissionMode || 'not specified'}`,
|
|
550
|
+
`Session: ${session}`,
|
|
551
|
+
`Working directory: ${cwd || 'not specified'}`,
|
|
552
|
+
compactList(rules, { label: 'Project rules files', empty: 'CLAUDE.md/AGENTS.md not detected in this worktree' }),
|
|
553
|
+
compactList(skillNames, { label: 'Available skills/commands' }),
|
|
554
|
+
selectedSkill ? `Selected skill: /${selectedSkill}` : 'Selected skill: none',
|
|
555
|
+
compactList(mcpSettings, { label: 'MCP/settings', empty: 'not detected or not introspected' }),
|
|
556
|
+
(images ?? []).length ? `Attached images: ${images.length} (${images.join(', ')})` : 'Attached images: none',
|
|
557
|
+
].join('\n');
|
|
558
|
+
}
|
|
559
|
+
|
|
271
560
|
// task 6 (2026-07-16): "saved X%" used to be a proxy for cache-hit ratio —
|
|
272
561
|
// this goal's own cache-read tokens discounted 0.1x, compared against this
|
|
273
562
|
// same goal's raw total. That's real math, but it has nothing to do with
|
|
@@ -879,6 +1168,42 @@ export function workerResultText({ result = '', err = '', code = 0, timedOut = f
|
|
|
879
1168
|
return tail && isForcedStop(code, timedOut) ? `${reason}\n最後の作業 / last step: ${tail}` : reason;
|
|
880
1169
|
}
|
|
881
1170
|
|
|
1171
|
+
export function classifyWorkerFailure({ agent = 'claude-code', result = '', err = '', code = 0, timedOut = false, phase = '' } = {}) {
|
|
1172
|
+
const text = [result, err, phase].map((v) => String(v ?? '')).filter(Boolean).join('\n');
|
|
1173
|
+
const low = text.toLowerCase();
|
|
1174
|
+
const infra = (reason, actionIds = ['retry', 'view-log']) => ({
|
|
1175
|
+
owner: 'galda-infra',
|
|
1176
|
+
reason,
|
|
1177
|
+
actionIds,
|
|
1178
|
+
});
|
|
1179
|
+
if (timedOut || /run limit|実行上限|timed out|timeout|タイムアウト/.test(low)) {
|
|
1180
|
+
return infra('timeout', ['retry', 'run-with-codex', 'view-log']);
|
|
1181
|
+
}
|
|
1182
|
+
if (/spawn|enoent|eacces|command not found|executable|worker tool exited early|作業ツールが途中で終了/.test(low)) {
|
|
1183
|
+
return infra('worker-start', ['retry', 'run-with-codex', 'view-log']);
|
|
1184
|
+
}
|
|
1185
|
+
if (/auth probe|preflight|not logged in|logged out|not authenticated|authentication|unauthorized|invalid api key|bad credentials|401\b|403\b|接続が必要|認証/.test(low)) {
|
|
1186
|
+
return infra('auth', ['connect', 'check-connection', 'run-with-codex', 'view-log']);
|
|
1187
|
+
}
|
|
1188
|
+
if (/relay|websocket|socket|econnreset|econnrefused|billing-api unreachable|cf access|cloudflare access/.test(low)) {
|
|
1189
|
+
return infra('relay', ['retry', 'connect', 'view-log']);
|
|
1190
|
+
}
|
|
1191
|
+
if (/chrome|chromium|puppeteer|target closed|browser|navigation timeout|waiting failed|screenshot|headless/.test(low)) {
|
|
1192
|
+
return infra('chrome', ['retry', 'view-log']);
|
|
1193
|
+
}
|
|
1194
|
+
if (/test runner|node --test|npm test|project tests|server exited \(code \d+\) before it was ready|did not report ready in time|test timed out after|tap|not ok \d+/.test(low)) {
|
|
1195
|
+
return infra('test-runner', ['retry-tests', 'view-log']);
|
|
1196
|
+
}
|
|
1197
|
+
if (/pr step failed|openpr|pull request|github|gh:|gh auth|git push|git fetch|git .*failed|pr作成|pr failed/.test(low)) {
|
|
1198
|
+
return infra('pr', ['retry-pr', 'view-log']);
|
|
1199
|
+
}
|
|
1200
|
+
return {
|
|
1201
|
+
owner: 'agent',
|
|
1202
|
+
reason: agent === 'codex' ? 'codex' : 'claude-code',
|
|
1203
|
+
actionIds: ['retry', agent === 'claude-code' ? 'run-with-codex' : 'run-with-claude', 'view-log'],
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
|
|
882
1207
|
// Was the worker forcibly STOPPED (timeout or SIGTERM) rather than failing on
|
|
883
1208
|
// its own? Such a task is 'interrupted' (retryable, not a code failure) — never
|
|
884
1209
|
// a bare 'failed', which reads as "the worker's code was wrong". Mirrors the
|
|
@@ -1000,6 +1325,7 @@ export function buildGoalProofMd({ goalText, items, lang = 'ja' }) {
|
|
|
1000
1325
|
// doesn't have to re-read the raw worker log to know what to look at.
|
|
1001
1326
|
export function buildGoalPrBody({ lang, goal, goalTasks, files, owner, branch, proofRel, proofItemsCount, reviewDescription, reviewSummary }) {
|
|
1002
1327
|
const L = (en, ja) => (lang === 'en' ? en : ja);
|
|
1328
|
+
const workspaceMode = workspaceModeLabel(goal?.workspaceMode ?? goal?.worktree, lang);
|
|
1003
1329
|
return [
|
|
1004
1330
|
...((reviewSummary?.check || reviewSummary?.changed) ? [
|
|
1005
1331
|
L('## Check first', '## 確認ポイント'),
|
|
@@ -1018,6 +1344,9 @@ export function buildGoalPrBody({ lang, goal, goalTasks, files, owner, branch, p
|
|
|
1018
1344
|
'',
|
|
1019
1345
|
goal.text.slice(0, 1500),
|
|
1020
1346
|
'',
|
|
1347
|
+
L(`Workspace mode: ${workspaceMode}`, `workspace mode: ${workspaceMode}`),
|
|
1348
|
+
...(goal.workspaceWarning ? ['', L(`Workspace warning: ${goal.workspaceWarning}`, `workspace warning: ${goal.workspaceWarning}`)] : []),
|
|
1349
|
+
'',
|
|
1021
1350
|
...buildReviewRuleSection(reviewDescription),
|
|
1022
1351
|
L(`Implemented by Manager for AI: the goal was decomposed into ${goalTasks.length} task(s), each executed by a Claude Code worker (flat-rate, no extra API), tests written and run per task.${proofItemsCount ? ' Tasks with a pass condition were verified independently in a headless browser.' : ''}`,
|
|
1023
1352
|
`Manager for AI が実装: ゴールを${goalTasks.length}個のタスクに分解し、Claude Code worker が順に実行(既存サブスク・追加API無し・タスク毎にテストを作成し実行)。${proofItemsCount ? '成功条件つきタスクは headless ブラウザで独立検証済み。' : ''}`),
|
|
@@ -1147,6 +1476,54 @@ export function classifyTestGate({ current = [], baseline = [] } = {}) {
|
|
|
1147
1476
|
return { newFailures, preExisting, blocked: newFailures.length > 0 };
|
|
1148
1477
|
}
|
|
1149
1478
|
|
|
1479
|
+
const PR_SAFETY_SCRATCH_RE = /(^|\/)\.tmp|\.log$|(^|\/)\.manager-wt(\/|$)|(^|\/)node_modules(\/|$)|(^|\/)secret\.key$/;
|
|
1480
|
+
|
|
1481
|
+
export function classifyPrSafety({ baseline = null, files = [], currentFiles = [], currentBranch = null } = {}) {
|
|
1482
|
+
if (!baseline?.sharedCheckout) return null;
|
|
1483
|
+
const dirty = (baseline.dirtyFiles ?? []).filter((f) => f && !PR_SAFETY_SCRATCH_RE.test(f));
|
|
1484
|
+
if (dirty.length) {
|
|
1485
|
+
return {
|
|
1486
|
+
code: 'dirty-baseline',
|
|
1487
|
+
message: [
|
|
1488
|
+
'PR creation skipped: this goal started in a dirty shared workspace, so Manager cannot prove the PR would contain only this task.',
|
|
1489
|
+
`Existing dirty files at goal start: ${dirty.slice(0, 8).join(', ')}${dirty.length > 8 ? ` +${dirty.length - 8} more` : ''}.`,
|
|
1490
|
+
'Repair: commit/stash/revert those unrelated changes, then rerun this reply with worktree on or from a clean checkout.',
|
|
1491
|
+
].join(' '),
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
if ((Number(baseline.aheadOfDefault) || 0) > 0) {
|
|
1495
|
+
return {
|
|
1496
|
+
code: 'branch-history',
|
|
1497
|
+
message: [
|
|
1498
|
+
'PR creation skipped: this goal started from a shared checkout with local commits ahead of the default branch.',
|
|
1499
|
+
'Repair: switch to a clean default-branch checkout or enable worktree isolation, then rerun the task/reply so only this goal is in the PR.',
|
|
1500
|
+
].join(' '),
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
if (baseline.branch && currentBranch && baseline.branch !== currentBranch) {
|
|
1504
|
+
return {
|
|
1505
|
+
code: 'branch-changed',
|
|
1506
|
+
message: [
|
|
1507
|
+
`PR creation skipped: the shared checkout branch changed from ${baseline.branch} to ${currentBranch} during this goal.`,
|
|
1508
|
+
'Repair: return to the baseline branch or rerun from a clean isolated worktree.',
|
|
1509
|
+
].join(' '),
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
const allowed = new Set(files ?? []);
|
|
1513
|
+
const extra = (currentFiles ?? []).filter((f) => f && !allowed.has(f) && !PR_SAFETY_SCRATCH_RE.test(f));
|
|
1514
|
+
if (extra.length) {
|
|
1515
|
+
return {
|
|
1516
|
+
code: 'extra-dirty-files',
|
|
1517
|
+
message: [
|
|
1518
|
+
'PR creation skipped: the shared checkout contains files outside this goal/task change list.',
|
|
1519
|
+
`Extra files: ${extra.slice(0, 8).join(', ')}${extra.length > 8 ? ` +${extra.length - 8} more` : ''}.`,
|
|
1520
|
+
'Repair: move those unrelated changes away, then rerun PR creation from a clean checkout.',
|
|
1521
|
+
].join(' '),
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
return null;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1150
1527
|
// A manager test-run that was KILLED before it could finish is inconclusive, not
|
|
1151
1528
|
// a red. When the machine is saturated (prod + several worker goals all standing
|
|
1152
1529
|
// up real servers + headless Chrome), `npm test` gets killed at its execFile
|
|
@@ -1232,6 +1609,40 @@ export function classifyVerifyGate({
|
|
|
1232
1609
|
return { blocked, kind, proofAdvisory };
|
|
1233
1610
|
}
|
|
1234
1611
|
|
|
1612
|
+
export function buildVerificationInfraError({ source = 'verification', detail = '', retryOnly = true } = {}) {
|
|
1613
|
+
const text = String(detail ?? '').replace(/\s+/g, ' ').trim();
|
|
1614
|
+
if (!text) return null;
|
|
1615
|
+
return {
|
|
1616
|
+
source,
|
|
1617
|
+
title: '検証基盤エラー',
|
|
1618
|
+
detail: text.slice(0, 600),
|
|
1619
|
+
retryOnly: !!retryOnly,
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
export function verificationInfraErrorFrom({ testResult = null, proofNote = null } = {}) {
|
|
1624
|
+
if (testResult?.inconclusive) {
|
|
1625
|
+
return buildVerificationInfraError({
|
|
1626
|
+
source: 'tests',
|
|
1627
|
+
detail: 'Project tests could not complete because the verification runner was interrupted or saturated. The implementation result is still ready for human review; press 再検証 to rerun verification only.',
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
if (testResult?.infraOnly) {
|
|
1631
|
+
const n = testResult.infraFlakes || testResult.failed || '';
|
|
1632
|
+
return buildVerificationInfraError({
|
|
1633
|
+
source: 'tests',
|
|
1634
|
+
detail: `Project tests had ${n} infrastructure-only failure(s): a server could not boot, Chrome closed, or a wait timed out under machine load. This is not treated as an implementation failure; press 再検証 to rerun verification only.`,
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
if (proofNote) {
|
|
1638
|
+
return buildVerificationInfraError({
|
|
1639
|
+
source: 'proof',
|
|
1640
|
+
detail: `${proofNote}。実装結果はReviewで確認できます。再検証ではコードを再実装せず、検証だけを再実行します。`,
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
return null;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1235
1646
|
// Serializes async work so at most one item runs at a time. The goal-verify
|
|
1236
1647
|
// pipeline uses this to stop two goals' `npm test` runs (each spins up real
|
|
1237
1648
|
// headless Chrome + real servers) from ever overlapping — running two full
|
|
@@ -1533,6 +1944,7 @@ export function buildReviewDigest({ goals, tasks } = {}) {
|
|
|
1533
1944
|
checkLine: plan ? `PLAN — approve to execute: ${checkLine}`.slice(0, 90) : checkLine,
|
|
1534
1945
|
pr: g.pr ?? null,
|
|
1535
1946
|
testResult: g.testResult ?? null,
|
|
1947
|
+
verificationInfraError: g.verificationInfraError ?? null,
|
|
1536
1948
|
proof: goalTasks.find((t) => t.proof)?.proof ?? null,
|
|
1537
1949
|
plan,
|
|
1538
1950
|
planText: plan ? (g.planText ?? '') : null,
|
|
@@ -1545,6 +1957,7 @@ export function buildReviewDigest({ goals, tasks } = {}) {
|
|
|
1545
1957
|
}
|
|
1546
1958
|
if (existing.proof == null) existing.proof = row.proof;
|
|
1547
1959
|
if (existing.testResult == null) existing.testResult = row.testResult;
|
|
1960
|
+
if (existing.verificationInfraError == null) existing.verificationInfraError = row.verificationInfraError;
|
|
1548
1961
|
continue;
|
|
1549
1962
|
}
|
|
1550
1963
|
if (row.pr) byPr.set(row.pr, row);
|
|
@@ -2098,6 +2511,25 @@ export function hasGitChanges(porcelainStatus) {
|
|
|
2098
2511
|
return Boolean(String(porcelainStatus ?? '').trim());
|
|
2099
2512
|
}
|
|
2100
2513
|
|
|
2514
|
+
export function resolveWorkspaceMode(input) {
|
|
2515
|
+
if (input === 'direct-workspace' || input === 'direct') return 'direct-workspace';
|
|
2516
|
+
if (input === 'isolated-worktree' || input === 'worktree' || input === true) return 'isolated-worktree';
|
|
2517
|
+
if (input === false) return 'direct-workspace';
|
|
2518
|
+
return 'isolated-worktree';
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
export function workspaceModeLabel(mode, lang = 'en') {
|
|
2522
|
+
const m = resolveWorkspaceMode(mode);
|
|
2523
|
+
if (m === 'direct-workspace') {
|
|
2524
|
+
return lang === 'ja'
|
|
2525
|
+
? 'direct workspace(現在の作業状態を引き継ぐ)'
|
|
2526
|
+
: 'direct workspace (inherits the current working state)';
|
|
2527
|
+
}
|
|
2528
|
+
return lang === 'ja'
|
|
2529
|
+
? 'isolated worktree(cleanな分離環境)'
|
|
2530
|
+
: 'isolated worktree (clean separate checkout)';
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2101
2533
|
// ---- project rules files (ONE-CONVERSATION §1.5a) --------------------------
|
|
2102
2534
|
// The files that hold a project's working rules — the instructions the connected
|
|
2103
2535
|
// AI actually reads (Claude Code reads CLAUDE.md from cwd; Codex reads AGENTS.md).
|
|
@@ -2139,6 +2571,67 @@ export function goalsConflict(filesA, filesB) {
|
|
|
2139
2571
|
return b.some((f) => setA.has(f));
|
|
2140
2572
|
}
|
|
2141
2573
|
|
|
2574
|
+
const REVIEW_FOLD_STOPWORDS = new Set([
|
|
2575
|
+
'the', 'and', 'for', 'with', 'that', 'this', 'from', 'into', 'onto', 'task', 'todo', 'goal',
|
|
2576
|
+
'fix', 'add', 'update', 'change', 'make', 'view', 'list', 'section', 'button', 'status',
|
|
2577
|
+
'する', 'した', 'して', 'こと', 'ため', 'よう', 'これ', 'それ', 'ところ', 'タスク', '表示', '追加', '修正',
|
|
2578
|
+
'セクション', 'カード',
|
|
2579
|
+
]);
|
|
2580
|
+
function foldTextTokens(text) {
|
|
2581
|
+
const s = String(text ?? '').toLowerCase();
|
|
2582
|
+
const ascii = s.match(/[a-z0-9][a-z0-9+#._-]{1,}/g) ?? [];
|
|
2583
|
+
const cjkRuns = s.match(/[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}ー]{2,}/gu) ?? [];
|
|
2584
|
+
const cjk = [];
|
|
2585
|
+
for (const run of cjkRuns) {
|
|
2586
|
+
if (run.length <= 4) cjk.push(run);
|
|
2587
|
+
else for (let i = 0; i < run.length - 1; i++) cjk.push(run.slice(i, i + 2));
|
|
2588
|
+
}
|
|
2589
|
+
return [...new Set([...ascii, ...cjk]
|
|
2590
|
+
.map((t) => t.replace(/^#+/, '').trim())
|
|
2591
|
+
.filter((t) => t.length >= 2 && !REVIEW_FOLD_STOPWORDS.has(t)))];
|
|
2592
|
+
}
|
|
2593
|
+
export function foldObjectivesRelated(a, b) {
|
|
2594
|
+
const ta = foldTextTokens([a?.text, a?.title, a?.summary].filter(Boolean).join(' '));
|
|
2595
|
+
const tb = foldTextTokens([b?.text, b?.title, b?.summary].filter(Boolean).join(' '));
|
|
2596
|
+
if (!ta.length || !tb.length) return false;
|
|
2597
|
+
const sb = new Set(tb);
|
|
2598
|
+
const overlap = ta.filter((t) => sb.has(t));
|
|
2599
|
+
if (overlap.length < 2) return false;
|
|
2600
|
+
if (!overlap.some((t) => /[a-z0-9]/i.test(t))) return false;
|
|
2601
|
+
const smaller = Math.min(ta.length, tb.length);
|
|
2602
|
+
const ratio = overlap.length / Math.max(1, smaller);
|
|
2603
|
+
return ratio >= 0.45 || overlap.length >= 4;
|
|
2604
|
+
}
|
|
2605
|
+
export function parseGitUnifiedDiffLocations(diffText) {
|
|
2606
|
+
const out = [];
|
|
2607
|
+
let file = null;
|
|
2608
|
+
for (const raw of String(diffText ?? '').split('\n')) {
|
|
2609
|
+
const plus = raw.match(/^\+\+\+ b\/(.+)$/);
|
|
2610
|
+
if (plus) { file = plus[1]; continue; }
|
|
2611
|
+
if (raw.startsWith('+++ /dev/null')) { file = null; continue; }
|
|
2612
|
+
const h = raw.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
|
2613
|
+
if (!h || !file) continue;
|
|
2614
|
+
const oldStart = Number(h[1]), oldLen = h[2] == null ? 1 : Number(h[2]);
|
|
2615
|
+
const newStart = Number(h[3]), newLen = h[4] == null ? 1 : Number(h[4]);
|
|
2616
|
+
out.push({
|
|
2617
|
+
file,
|
|
2618
|
+
oldStart, oldEnd: oldStart + Math.max(1, oldLen) - 1,
|
|
2619
|
+
newStart, newEnd: newStart + Math.max(1, newLen) - 1,
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
return out;
|
|
2623
|
+
}
|
|
2624
|
+
function rangesOverlap(aStart, aEnd, bStart, bEnd, margin = 2) {
|
|
2625
|
+
return Math.max(aStart, bStart) <= Math.min(aEnd, bEnd) + margin;
|
|
2626
|
+
}
|
|
2627
|
+
export function foldLocationsOverlap(a, b) {
|
|
2628
|
+
const la = a?.changeLocations ?? [], lb = b?.changeLocations ?? [];
|
|
2629
|
+
if (!la.length || !lb.length) return false;
|
|
2630
|
+
return la.some((x) => lb.some((y) => x.file === y.file
|
|
2631
|
+
&& (rangesOverlap(x.newStart, x.newEnd, y.newStart, y.newEnd)
|
|
2632
|
+
|| rangesOverlap(x.oldStart, x.oldEnd, y.oldStart, y.oldEnd))));
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2142
2635
|
// Which of `otherGoals` (each `{ id, changedFiles }`) touch at least one path
|
|
2143
2636
|
// this goal (`{ id, changedFiles }`) also touched? Returns their ids, in the
|
|
2144
2637
|
// order they appear in `otherGoals` — server.mjs stamps these onto
|
|
@@ -2187,7 +2680,15 @@ export function pickFoldTarget(goal, candidates) {
|
|
|
2187
2680
|
const overlapping = (candidates ?? []).filter(
|
|
2188
2681
|
(g) => g && g.id !== goal.id && goalsConflict(files, g.changedFiles),
|
|
2189
2682
|
);
|
|
2190
|
-
|
|
2683
|
+
const eligible = overlapping.filter((g) => foldObjectivesRelated(goal, g) || foldLocationsOverlap(goal, g));
|
|
2684
|
+
return eligible.length === 1 ? eligible[0].id : null;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
// Two requests remain two human-review decisions by default, even when they
|
|
2688
|
+
// touch the same files. Legacy automatic folding is available only when a
|
|
2689
|
+
// project explicitly opts into it.
|
|
2690
|
+
export function autoFoldReviewsEnabled(project) {
|
|
2691
|
+
return project?.autoFoldReviews === true;
|
|
2191
2692
|
}
|
|
2192
2693
|
|
|
2193
2694
|
// Which of `commits` (newest-first, as returned by parseGitLog) are new
|
|
@@ -2526,11 +3027,21 @@ export function buildGoalTask(goal, { id, num, passCondition = null, createdAt }
|
|
|
2526
3027
|
title: goalTaskTitle(goal.text), detail: goal.text, passCondition,
|
|
2527
3028
|
model: goal.model, effort: goal.effort, agent: goal.agent,
|
|
2528
3029
|
mode: goal.mode ?? 'auto', skill: goal.skill ?? null,
|
|
3030
|
+
workspaceMode: resolveWorkspaceMode(goal.workspaceMode ?? goal.worktree),
|
|
3031
|
+
worktree: goal.worktree === false ? false : undefined,
|
|
2529
3032
|
status: 'queued', createdAt, priority: '中',
|
|
2530
3033
|
result: null, changedFiles: [], secs: null, activity: [], proof: null, usage: null,
|
|
2531
3034
|
};
|
|
2532
3035
|
}
|
|
2533
3036
|
|
|
3037
|
+
// Stability roadmap 03/08: the intake planner is no longer on the default
|
|
3038
|
+
// implementation route. It is reserved for the user explicitly selecting Plan
|
|
3039
|
+
// mode, where the product is allowed to ask the planning layer for structure or
|
|
3040
|
+
// clarification before the worker still receives the verbatim request.
|
|
3041
|
+
export function shouldUsePlannerForGoal(goal) {
|
|
3042
|
+
return goal?.mode === 'plan';
|
|
3043
|
+
}
|
|
3044
|
+
|
|
2534
3045
|
// ---- worker prompt ---------------------------------------------------------
|
|
2535
3046
|
// The instruction the worker (Claude Code / Codex) actually reads. Pure:
|
|
2536
3047
|
// (task, goal, lastFailure, agentName) -> string, so it is unit-testable.
|
|
@@ -2544,13 +3055,14 @@ export function buildGoalTask(goal, { id, num, passCondition = null, createdAt }
|
|
|
2544
3055
|
// Together they turned "design 3 patterns as an artifact" into one minimal
|
|
2545
3056
|
// tweak. North star: we never re-judge the request — the user's verbatim words
|
|
2546
3057
|
// are the source of truth and the worker delivers exactly what they ask for.
|
|
2547
|
-
export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code') {
|
|
3058
|
+
export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code', runContext = null) {
|
|
2548
3059
|
// Optional skill the user attached to this goal. Empirically (scratch worker,
|
|
2549
3060
|
// haiku, custom fixture skill) the reliably-firing form is an explicit Skill-tool
|
|
2550
3061
|
// instruction as the FIRST line; a bare "/name" first line did not consistently
|
|
2551
3062
|
// trigger the skill in `claude -p`. See commit body for the run evidence.
|
|
2552
3063
|
const skill = task?.skill ?? goal?.skill;
|
|
2553
3064
|
const displayName = agentName === 'codex' ? 'Codex' : 'Claude Code';
|
|
3065
|
+
const workspaceMode = workspaceModeLabel(task?.workspaceMode ?? goal?.workspaceMode ?? goal?.worktree, 'en');
|
|
2554
3066
|
// Report back to the human in the SAME language they wrote the goal in (Masa
|
|
2555
3067
|
// 2026-07-15: "撃った言語で返す"). JP characters → Japanese, otherwise English.
|
|
2556
3068
|
const replyLang = detectRequestLanguage(goal?.text || task?.title || '') === 'ja' ? '日本語' : 'English';
|
|
@@ -2563,9 +3075,11 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code')
|
|
|
2563
3075
|
// TASK/DETAIL below is a paraphrase from the planner — when they differ,
|
|
2564
3076
|
// follow the user's words.
|
|
2565
3077
|
goal?.text ? `USER'S REQUEST (verbatim — the source of truth; deliver exactly this):\n${goal.text}` : '',
|
|
3078
|
+
runContext ? (typeof runContext === 'string' ? runContext : buildWorkerContextBlock(runContext)) : '',
|
|
2566
3079
|
'',
|
|
2567
3080
|
`TASK ${task.num}: ${task.title}`,
|
|
2568
3081
|
task.detail ? `DETAIL: ${task.detail}` : '',
|
|
3082
|
+
`WORKSPACE MODE: ${workspaceMode}.`,
|
|
2569
3083
|
goal?.images?.length ? `ATTACHED IMAGES (absolute paths, use the Read tool to view them): ${goal.images.join(' ')}` : '',
|
|
2570
3084
|
task.passCondition ? `\nEXPLICIT PASS CONDITION (verified externally by a headless browser after you finish — NOT by you): ${task.passCondition}` : '',
|
|
2571
3085
|
lastFailure ? `\nPREVIOUS ATTEMPT FAILED EXTERNAL VERIFICATION: ${lastFailure}\nYour previous changes are on disk. Diagnose and fix.` : '',
|
|
@@ -2585,6 +3099,7 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code')
|
|
|
2585
3099
|
// instead of expanding scope"): the request, not our fear of scope, sets
|
|
2586
3100
|
// the size of the work.
|
|
2587
3101
|
'- Deliver what the request actually asks for. If it asks for multiple options, N variations, a visual, an artifact, or a full exploration, produce ALL of them — do not reduce it to one, and do not cut it short to finish faster.',
|
|
3102
|
+
'- デザイン案・UI案・レイアウト案を求められた場合は、文章だけで完了にしない。画像を使わない案でも、比較して確認できるHTML等のartifactをプロジェクト内に作り、実際に開ける .manager-run.json を残す。',
|
|
2588
3103
|
'- Keep the work bounded to the request: inspect only files needed; avoid broad repository scans unless they are necessary.',
|
|
2589
3104
|
"- You may use the user's installed skills (Skill tool, e.g. /galda-doc) when one clearly fits the request.",
|
|
2590
3105
|
// Token-frugality + the 10-min run cap: the worker used to run the WHOLE suite
|
|
@@ -2605,6 +3120,7 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code')
|
|
|
2605
3120
|
// (research, summaries, answers) simply skip this — the worker decides,
|
|
2606
3121
|
// never us.
|
|
2607
3122
|
`- 人が実際に動かして結果を見られる依頼なら、最後にプロジェクト直下へ ${RUN_DECL_FILE} を書く: {"cmd": "<起動コマンド>", "url": "<開くURL>", "note": "<何が見えるか1行>", "check": "<人に何を見て判断してほしいか1行>", "title": "<この作業のチケット名。対象が分かる程度に説明的で動作を含む短い名前。例「看板モードの追加」「ヘッダのズレの修正」。依頼文をそのまま写さない>"}。Manager はこれを使って人に「動かして見る」と「何を見ればいいか」を出す。動かして見せるものが無い依頼(調査・要約・質問への回答など)では書かなくてよい。`,
|
|
3123
|
+
`- Markdown・設計文書・調査メモなど文章そのものが成果物なら、${RUN_DECL_FILE} に別のHTMLや過去のURLを書かない。最終回答に成果物の本文も貼り、保存先のMarkdownリンクを添えること。「ファイルに書きました」だけで終わらない。`,
|
|
2608
3124
|
`- 「ローカルで見れない」というフィードバックには、ターミナル操作や npx の再実行を案内して終わらないこと。${RUN_DECL_FILE} を実際に開ける内容へ更新し、可能なら自分でURLへ接続確認してから完了すること。`,
|
|
2609
3125
|
// Links are rendered as links. The product cannot tell a real one from a
|
|
2610
3126
|
// decorative one, so the only place this can be held is here.
|
|
@@ -2972,7 +3488,15 @@ export function visibleTodos(list, showAll, limit = 10) {
|
|
|
2972
3488
|
// Japanese lines covering ①何をやったか ②変更点 ③確認方法, even when tasks
|
|
2973
3489
|
// are mixed done/failed/interrupted or there is only a single task.
|
|
2974
3490
|
export function buildGoalSummary({ goalText, tasks } = {}) {
|
|
2975
|
-
const
|
|
3491
|
+
const all = tasks ?? [];
|
|
3492
|
+
const requirements = all.filter((t) => !t.reply);
|
|
3493
|
+
const latestOutcome = latestGoalOutcomeTask(all);
|
|
3494
|
+
// A follow-up can be the run that finally delivered the work after the original
|
|
3495
|
+
// requirement was skipped/interrupted. Keep the requirement count stable, but
|
|
3496
|
+
// summarize the effective outcome instead of preserving a stale "Skipped.".
|
|
3497
|
+
const work = requirements.length === 1 && latestOutcome?.reply
|
|
3498
|
+
? [{ ...requirements[0], ...latestOutcome, title: requirements[0].title }]
|
|
3499
|
+
: requirements;
|
|
2976
3500
|
const done = work.filter((t) => ['done', 'skipped'].includes(t.status));
|
|
2977
3501
|
const failed = work.filter((t) => ['failed', 'interrupted'].includes(t.status));
|
|
2978
3502
|
const files = [...new Set(work.flatMap((t) => t.changedFiles ?? []))];
|
|
@@ -3009,6 +3533,42 @@ export function buildGoalSummary({ goalText, tasks } = {}) {
|
|
|
3009
3533
|
return lines.join('\n');
|
|
3010
3534
|
}
|
|
3011
3535
|
|
|
3536
|
+
// The Review card represents the goal's latest completed worker outcome. A reply is
|
|
3537
|
+
// a real worker run and may supersede an earlier skipped/failed requirement.
|
|
3538
|
+
export function latestGoalOutcomeTask(tasks = []) {
|
|
3539
|
+
return tasks
|
|
3540
|
+
.filter((t) => t && t.status === 'done' && String(t.result ?? '').trim())
|
|
3541
|
+
.sort((a, b) => Date.parse(b.finishedAt || b.startedAt || b.createdAt || 0)
|
|
3542
|
+
- Date.parse(a.finishedAt || a.startedAt || a.createdAt || 0))[0] ?? null;
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
export function buildDirtyWorkspacePrBlock({ dirtyFiles = [], branch = null, base = null, ahead = 0 } = {}) {
|
|
3546
|
+
const files = [...new Set(dirtyFiles)].filter(Boolean);
|
|
3547
|
+
const branchRisk = branch && base && branch !== base;
|
|
3548
|
+
const aheadCount = Number(ahead) || 0;
|
|
3549
|
+
if (!files.length && !branchRisk && aheadCount === 0) return null;
|
|
3550
|
+
const pieces = [];
|
|
3551
|
+
if (files.length) pieces.push(`workspace was already dirty at goal start (${files.slice(0, 6).join(', ')}${files.length > 6 ? ` +${files.length - 6} more` : ''})`);
|
|
3552
|
+
if (branchRisk) pieces.push(`current branch is ${branch}, not ${base}`);
|
|
3553
|
+
if (aheadCount > 0) pieces.push(`current checkout has ${aheadCount} commit(s) ahead of ${base ?? 'the default branch'}`);
|
|
3554
|
+
return {
|
|
3555
|
+
message: `Not opening a PR because this run used the shared checkout and ${pieces.join('; ')}. Clean/stash unrelated changes or rerun with isolated worktree, then retry PR.`,
|
|
3556
|
+
repair: {
|
|
3557
|
+
kind: 'dirty-workspace',
|
|
3558
|
+
title: 'PRを止めました / PR blocked',
|
|
3559
|
+
steps: [
|
|
3560
|
+
'既存のdirty差分をcommit/stash/破棄して、このtask以外の変更を片付ける',
|
|
3561
|
+
'WorktreeをONにして同じreplyを再実行する',
|
|
3562
|
+
'状態がきれいになったら「PR再試行」を押す',
|
|
3563
|
+
],
|
|
3564
|
+
dirtyFiles: files.slice(0, 20),
|
|
3565
|
+
branch,
|
|
3566
|
+
base,
|
|
3567
|
+
ahead: aheadCount,
|
|
3568
|
+
},
|
|
3569
|
+
};
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3012
3572
|
// Plain-text snapshot of Review/Attention/Done fed to the /api/ask LLM pass —
|
|
3013
3573
|
// same three buckets app/index.html's renderTasks() renders (goal.status
|
|
3014
3574
|
// 'review'/'blocked' + task.status failed/interrupted + task.status
|