@galda/cli 0.10.87 → 0.10.89
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/CLAUDE.md +2 -0
- package/app/index.html +51 -29
- package/engine/completion-contract.mjs +19 -0
- package/engine/lib.mjs +11 -4
- package/engine/server.mjs +32 -3
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -105,6 +105,8 @@
|
|
|
105
105
|
- Worker/API へは**必ず所有ドメイン `https://galda.app`(custom domain)経由**で参照する。`*.workers.dev` サブドメインを製品コードに焼き込まない(`MANAGER_BILLING_API_URL` 等の既定値は `https://galda.app`)。
|
|
106
106
|
- **Cloudflare/wrangler にデプロイする前に `wrangler whoami` でデプロイ先アカウントを確認**し、Galda/Kodo 所有であること・workers.dev サブドメイン名に客先名や別プロジェクト名が出ていないことを確かめる。怪しければ止めて Masa に確認(本番デプロイは Masa 明示許可制)。
|
|
107
107
|
- **リリースは4面**(① npm ② Fly ③ Worker ④ Masaの常駐インスタンス `~/manager-for-ai`:4400)。①だけ出して④を忘れると「直したのに直ってない」+relay相互kickループ再発の実例あり(2026-07-16/17)。全文=`docs/RELEASE-WORKFLOW.md`の「本番は1つじゃない」表。
|
|
108
|
+
- **常駐Managerを直接 `kill`/`launchctl kickstart -k` しない**(2026-07-28、実行中3 workerを同時中断した実例)。再起動は対象checkoutで
|
|
109
|
+
`node tools/safe-restart-manager.mjs` を使う。このコマンドはDoing/Finalizingが1件でもあれば拒否する。拒否時は完了を待って再実行し、Agent切替で回避しない。
|
|
108
110
|
|
|
109
111
|
## relay接続(`app.galda.app`)を伴う作業の掟(Masa明言 2026-07-17・全レーン必読)
|
|
110
112
|
|
package/app/index.html
CHANGED
|
@@ -1558,6 +1558,14 @@
|
|
|
1558
1558
|
line-height:1.15;padding:2.5px 10px;border-radius:7px;box-shadow:inset 0 0 0 1px var(--hair2)}
|
|
1559
1559
|
#fsRoot .acts2 .errbtn:first-child{color:var(--ink2)}
|
|
1560
1560
|
#fsRoot .errbtn:hover{background:var(--hover);color:var(--ink)}
|
|
1561
|
+
/* The review/attention dialog is rendered in #seeall, outside #fsRoot.
|
|
1562
|
+
Keep its recovery actions on the same designed component instead of
|
|
1563
|
+
falling back to the browser's unstyled native button. */
|
|
1564
|
+
#seeall .acts2{display:flex;gap:6px;margin-top:3px;align-items:center}
|
|
1565
|
+
#seeall .errbtn{border:0;background:transparent;color:var(--ink3);font-family:var(--mono);font-size:10px;letter-spacing:.02em;cursor:pointer;
|
|
1566
|
+
line-height:1.15;padding:2.5px 10px;border-radius:7px;box-shadow:inset 0 0 0 1px var(--hair2)}
|
|
1567
|
+
#seeall .acts2 .errbtn:first-child{color:var(--ink2)}
|
|
1568
|
+
#seeall .errbtn:hover{background:var(--hover);color:var(--ink)}
|
|
1561
1569
|
/* AI question in the feed (q2 — §6.5: while current it lives in the chat feed).
|
|
1562
1570
|
H18 / CDO HANDOFF-question-ui-2026-07-13: QUESTION card = hairline frame +
|
|
1563
1571
|
2px --blue left rule + mono label + option chips + an always-on free-text
|
|
@@ -3633,10 +3641,7 @@ function reviewCardConfigFor(projectId){ return { ...DEFAULT_REVIEW_DEFINITION.r
|
|
|
3633
3641
|
// project to one language regardless of what a given goal was typed in.
|
|
3634
3642
|
function reviewLangFor(projectId){ const l = reviewDefinitionFor(projectId).language; return (l === 'en' || l === 'ja') ? l : 'auto'; }
|
|
3635
3643
|
function retryCopy(projectId){
|
|
3636
|
-
|
|
3637
|
-
return ja
|
|
3638
|
-
? { infra: '検証基盤エラー', retry: '再検証', retrying: '再検証中…', failed: '再検証に失敗' }
|
|
3639
|
-
: { infra: 'Verification infrastructure error', retry: 'Retry verification', retrying: 'Retrying…', failed: 'Verification retry failed' };
|
|
3644
|
+
return { infra: 'Verification infrastructure error', retry: 'Retry verification', retrying: 'Retrying…', failed: 'Verification retry failed' };
|
|
3640
3645
|
}
|
|
3641
3646
|
|
|
3642
3647
|
function esc(s){ const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
|
@@ -4891,7 +4896,8 @@ function renderTasks(){
|
|
|
4891
4896
|
for (const b of $('tasklist').querySelectorAll('[data-goalreverify]')) {
|
|
4892
4897
|
b.onclick = async (e) => { e.stopPropagation(); const copy = retryCopy(state.active); b.disabled = true; b.textContent = copy.retrying;
|
|
4893
4898
|
let r; try { r = await fetch(withKey(`/api/goals/${b.dataset.goalreverify}/reverify`), { method: 'POST' }); }
|
|
4894
|
-
catch { showErr(copy.failed); return; }
|
|
4899
|
+
catch { showErr(copy.failed); b.disabled = false; b.textContent = copy.retry; return; }
|
|
4900
|
+
if (!r.ok) { showErr(copy.failed); b.disabled = false; b.textContent = copy.retry; return; }
|
|
4895
4901
|
qrefresh(r, copy.failed); };
|
|
4896
4902
|
}
|
|
4897
4903
|
for (const b of $('tasklist').querySelectorAll('[data-qreply]')) {
|
|
@@ -5557,8 +5563,8 @@ function renderReviewChecklist(){
|
|
|
5557
5563
|
b.disabled = true;
|
|
5558
5564
|
b.textContent = retryCopy(goal.projectId).retrying;
|
|
5559
5565
|
let r; try { r = await fetch(withKey(`/api/goals/${b.dataset.goalreverify}/reverify`), { method: 'POST' }); }
|
|
5560
|
-
catch { showErr(retryCopy(goal.projectId).failed); b.disabled = false; return; }
|
|
5561
|
-
if (r.ok) await refresh(); else showErr(retryCopy(goal.projectId).failed);
|
|
5566
|
+
catch { showErr(retryCopy(goal.projectId).failed); b.disabled = false; b.textContent = retryCopy(goal.projectId).retry; return; }
|
|
5567
|
+
if (r.ok) await refresh(); else { showErr(retryCopy(goal.projectId).failed); b.disabled = false; b.textContent = retryCopy(goal.projectId).retry; }
|
|
5562
5568
|
};
|
|
5563
5569
|
}
|
|
5564
5570
|
for (const b of $('reviewList').querySelectorAll('.reqsend')) {
|
|
@@ -7191,6 +7197,7 @@ const FAILURE_COPY = {
|
|
|
7191
7197
|
infra: {
|
|
7192
7198
|
title: 'Galda基盤エラー',
|
|
7193
7199
|
timeout: 'Galda側の実行上限や待ち時間で止まりました。変更内容の失敗とは限りません。',
|
|
7200
|
+
interrupted: 'ローカルの実行が中断されました。変更内容やAgentの失敗とは限らないため、同じ作業を再開できます。',
|
|
7194
7201
|
auth: '接続や認証確認で止まりました。接続を直してから再開できます。',
|
|
7195
7202
|
relay: 'relay接続で止まりました。接続状態を確認してから再試行できます。',
|
|
7196
7203
|
chrome: 'Chrome/ブラウザ確認で止まりました。画面確認環境の問題で、実装失敗とは限りません。',
|
|
@@ -7200,8 +7207,8 @@ const FAILURE_COPY = {
|
|
|
7200
7207
|
unknown: 'Galdaの実行基盤で止まりました。ログを確認し、原因に応じて再試行できます。',
|
|
7201
7208
|
},
|
|
7202
7209
|
actions: {
|
|
7203
|
-
retry: '
|
|
7204
|
-
other: '
|
|
7210
|
+
retry: 'Retry', resume: 'Resume', connect: 'Connect', check: 'Check connection',
|
|
7211
|
+
other: 'Try another Agent', log: 'View log', tests: 'Re-run tests', pr: 'Retry PR',
|
|
7205
7212
|
},
|
|
7206
7213
|
},
|
|
7207
7214
|
en: {
|
|
@@ -7209,6 +7216,7 @@ const FAILURE_COPY = {
|
|
|
7209
7216
|
infra: {
|
|
7210
7217
|
title: 'Galda system issue',
|
|
7211
7218
|
timeout: 'Galda stopped this at the run limit or a wait timed out. This is not necessarily a problem with the change.',
|
|
7219
|
+
interrupted: 'The local run was interrupted. This does not necessarily reflect a problem with the change or agent; resume the same work.',
|
|
7212
7220
|
auth: 'Connection or authentication failed. Reconnect, then continue.',
|
|
7213
7221
|
relay: 'The relay connection failed. Check the connection, then retry.',
|
|
7214
7222
|
chrome: 'Chrome/browser verification failed. The UI check environment stopped, not necessarily the implementation.',
|
|
@@ -7229,17 +7237,23 @@ function failureLang(){
|
|
|
7229
7237
|
function inferErrorClass(text, agent){
|
|
7230
7238
|
const low = String(text || '').toLowerCase();
|
|
7231
7239
|
const infra = (reason, actionIds) => ({ owner: 'galda-infra', reason, actionIds });
|
|
7232
|
-
if (/run limit|実行上限|timed out|timeout|タイムアウト/.test(low)) return infra('timeout', ['retry', '
|
|
7233
|
-
if (/
|
|
7234
|
-
if (/
|
|
7240
|
+
if (/run limit|実行上限|timed out|timeout|タイムアウト/.test(low)) return infra('timeout', ['retry', 'view-log']);
|
|
7241
|
+
if (/worker was interrupted|作業が途中で中断|exit 143|sigterm|sigint/.test(low)) return infra('interrupted', ['retry', 'view-log']);
|
|
7242
|
+
if (/spawn|enoent|eacces|command not found|worker tool exited early|作業ツールが途中で終了/.test(low)) return infra('worker-start', ['retry', 'view-log']);
|
|
7243
|
+
if (/auth probe|preflight|not logged in|logged out|not authenticated|authentication|unauthorized|invalid api key|bad credentials|401\b|403\b|接続が必要|認証/.test(low)) return infra('auth', ['connect', 'check-connection', 'view-log']);
|
|
7235
7244
|
if (/relay|websocket|socket|econnreset|econnrefused|billing-api unreachable|cf access|cloudflare access/.test(low)) return infra('relay', ['retry', 'connect', 'view-log']);
|
|
7236
7245
|
if (/chrome|chromium|puppeteer|target closed|browser|navigation timeout|waiting failed|screenshot|headless/.test(low)) return infra('chrome', ['retry', 'view-log']);
|
|
7237
7246
|
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)) return infra('test-runner', ['retry-tests', 'view-log']);
|
|
7238
7247
|
if (/pr step failed|openpr|pull request|github|gh:|gh auth|git push|git fetch|git .*failed|pr作成|pr failed/.test(low)) return infra('pr', ['retry-pr', 'view-log']);
|
|
7239
|
-
return { owner: 'agent', reason: agent === 'codex' ? 'codex' : 'claude-code', actionIds: ['retry', '
|
|
7248
|
+
return { owner: 'agent', reason: agent === 'codex' ? 'codex' : 'claude-code', actionIds: ['retry', 'view-log'] };
|
|
7240
7249
|
}
|
|
7241
7250
|
function taskErrorMeta(t, goal){
|
|
7242
|
-
|
|
7251
|
+
// Reclassify interrupted tasks from their concrete exit text instead of
|
|
7252
|
+
// trusting stale persisted metadata. Older records labelled an external
|
|
7253
|
+
// SIGTERM/restart as an Agent failure and permanently carried a Codex switch.
|
|
7254
|
+
const inferred = inferErrorClass(t.result, t.agent || goal?.agent);
|
|
7255
|
+
const raw = t.status === 'interrupted' ? inferred : (t.errorClass || inferred);
|
|
7256
|
+
const meta = { ...raw, actionIds: (raw.actionIds || []).filter((id) => !['run-with-codex', 'run-with-claude'].includes(id)) };
|
|
7243
7257
|
const lang = failureLang(), C = FAILURE_COPY[lang];
|
|
7244
7258
|
const isInfra = meta.owner === 'galda-infra';
|
|
7245
7259
|
return {
|
|
@@ -7253,20 +7267,16 @@ function goalBlockedErrorMeta(g){
|
|
|
7253
7267
|
const raw = g.blocked?.errorClass || (g.blocked?.kind === 'test'
|
|
7254
7268
|
? { owner: 'galda-infra', reason: 'test-runner', actionIds: ['retry-tests', 'view-log'] }
|
|
7255
7269
|
: inferErrorClass(g.blocked?.reason, g.agent));
|
|
7270
|
+
const meta = { ...raw, actionIds: (raw.actionIds || []).filter((id) => !['run-with-codex', 'run-with-claude'].includes(id)) };
|
|
7256
7271
|
const lang = failureLang(), C = FAILURE_COPY[lang];
|
|
7257
|
-
const isInfra =
|
|
7258
|
-
return { ...
|
|
7272
|
+
const isInfra = meta.owner === 'galda-infra';
|
|
7273
|
+
return { ...meta, title: isInfra ? C.infra.title : C.agent.title, body: isInfra ? (C.infra[meta.reason] || C.infra.unknown) : C.agent.body, copy: C };
|
|
7259
7274
|
}
|
|
7260
7275
|
function errorButtonsHtml(meta, { taskId = null, goalId = null, interrupted = false, agent = null, pr = false } = {}){
|
|
7261
7276
|
const C = meta.copy.actions;
|
|
7262
|
-
const canCodex = goalId != null && agent !== 'codex' && (state.agents || []).includes('codex');
|
|
7263
7277
|
const out = [];
|
|
7264
7278
|
for (const id of meta.actionIds || []) {
|
|
7265
7279
|
if (id === 'retry' && taskId != null) out.push(`<button class="errbtn" data-fsretry="${taskId}">${interrupted ? C.resume : C.retry}</button>`);
|
|
7266
|
-
// This keeps the existing agent-switch endpoint, but says what it really does
|
|
7267
|
-
// for an interrupted run: continue the saved goal with another agent rather
|
|
7268
|
-
// than imply a brand-new, contextless task will be created.
|
|
7269
|
-
else if (id === 'run-with-codex' && canCodex) out.push(`<button class="errbtn" data-fscodex="${goalId}">${interrupted ? (failureLang() === 'ja' ? 'Codexで再開' : 'Resume with Codex') : C.other}</button>`);
|
|
7270
7280
|
else if (id === 'connect') out.push(`<button class="errbtn" data-fsconnectworker="1">${C.connect}</button>`);
|
|
7271
7281
|
else if (id === 'check-connection') out.push(`<button class="errbtn" data-fscheckworker="1">${C.check}</button>`);
|
|
7272
7282
|
else if (id === 'retry-tests' && goalId != null) out.push(`<button class="errbtn" data-fsreverify="${goalId}">${C.tests}</button>`);
|
|
@@ -8530,7 +8540,7 @@ let sseUp = false; // live engine link — the honest signal behind the MCP tab'
|
|
|
8530
8540
|
// Connection status line (queue tray): grace-debounce the offline state ~800ms so
|
|
8531
8541
|
// sub-second SSE blips don't flash a red line. connOffline is the debounced flag
|
|
8532
8542
|
// renderQueue() reads; connSetOnline/Offline flip it around real sseUp changes.
|
|
8533
|
-
let connOffline = false, _connGraceT = null, _sseReconnectT = null, _es = null, _healthT = null;
|
|
8543
|
+
let connOffline = false, _connGraceT = null, _sseReconnectT = null, _es = null, _healthT = null, _healthFailStreak = 0;
|
|
8534
8544
|
function connSetOnline(){ if (_connGraceT) { clearTimeout(_connGraceT); _connGraceT = null; } if (connOffline) { connOffline = false; renderQueue(); } }
|
|
8535
8545
|
function connSetOffline(){ if (_connGraceT || connOffline) return; _connGraceT = setTimeout(() => { _connGraceT = null; connOffline = true; renderQueue(); }, 800); }
|
|
8536
8546
|
// Reconnect button: drop any pending backoff timer and reopen the EventSource now.
|
|
@@ -8545,15 +8555,23 @@ async function checkHealth(){
|
|
|
8545
8555
|
if (document.hidden) return;
|
|
8546
8556
|
let ok = false;
|
|
8547
8557
|
try {
|
|
8548
|
-
|
|
8558
|
+
// Mobile browsers and DevTools device emulation can briefly starve this low-priority
|
|
8559
|
+
// probe even while the already-open SSE and POST path are healthy. Do not turn one
|
|
8560
|
+
// transient probe into a user-visible outage.
|
|
8561
|
+
const c = new AbortController(); const to = setTimeout(() => c.abort(), 5000);
|
|
8549
8562
|
const r = await fetch(withKey('/'), { method: 'HEAD', cache: 'no-store', signal: c.signal });
|
|
8550
8563
|
clearTimeout(to); ok = r.ok;
|
|
8551
8564
|
} catch { ok = false; }
|
|
8552
8565
|
if (!ok) {
|
|
8566
|
+
_healthFailStreak++;
|
|
8567
|
+
if (_healthFailStreak < 2) return;
|
|
8553
8568
|
connSetOffline(); // grace-debounced ~800ms
|
|
8554
8569
|
if (!_sseReconnectT) { try { _es && _es.close(); } catch {} sseUp = false; esRetryMs = Math.min(esRetryMs, 4000); _sseReconnectT = setTimeout(connectSSE, esRetryMs); }
|
|
8555
|
-
} else
|
|
8570
|
+
} else {
|
|
8571
|
+
_healthFailStreak = 0;
|
|
8572
|
+
if (connOffline && !_sseReconnectT) {
|
|
8556
8573
|
forceReconnect(); // server reachable again but the line is still up → reconnect to clear + flush
|
|
8574
|
+
}
|
|
8557
8575
|
}
|
|
8558
8576
|
}
|
|
8559
8577
|
function startHealthPoll(){ clearInterval(_healthT); _healthT = setInterval(checkHealth, 4000); }
|
|
@@ -8773,6 +8791,10 @@ async function boot(){
|
|
|
8773
8791
|
flushOutbox();
|
|
8774
8792
|
connectSSE();
|
|
8775
8793
|
startHealthPoll();
|
|
8794
|
+
// A Wi-Fi ⇄ cellular switch can leave EventSource waiting for its normal retry
|
|
8795
|
+
// budget. The browser has already confirmed connectivity here, so reconnect now
|
|
8796
|
+
// instead of making a mobile send wait for the next health poll/backoff cycle.
|
|
8797
|
+
window.addEventListener('online', () => { _healthFailStreak = 0; forceReconnect(); });
|
|
8776
8798
|
document.addEventListener('visibilitychange', () => { if (!document.hidden) { refresh(); flushOutbox(); checkHealth(); } });
|
|
8777
8799
|
// belt and suspenders for the never-lose guarantee: even if every event
|
|
8778
8800
|
// path misfires, unsent memos are retried on a slow heartbeat
|
|
@@ -10130,13 +10152,12 @@ function saItemHtml(it, i){
|
|
|
10130
10152
|
${coverageSec}
|
|
10131
10153
|
${evidenceSec}
|
|
10132
10154
|
${reqVerifySec}
|
|
10133
|
-
${S('Open it', openBody)}
|
|
10155
|
+
${S('Open it', openBody + infraBody)}
|
|
10134
10156
|
${S('Run state', interruptedBody)}
|
|
10135
10157
|
${S('PR', prRepairBody)}
|
|
10136
10158
|
${S('Also carries', carriesBody)}
|
|
10137
10159
|
${S('What to check', checkBody)}
|
|
10138
10160
|
${S('Result', reportBody)}
|
|
10139
|
-
${S(infra?.title || '検証基盤エラー', infraBody)}
|
|
10140
10161
|
${S('Screenshots', shotsBody)}
|
|
10141
10162
|
${S('Steps', stepsBody)}
|
|
10142
10163
|
${S('Proof', proofInner)}
|
|
@@ -10629,11 +10650,12 @@ $('seeall').addEventListener('click', async (e) => {
|
|
|
10629
10650
|
// meta-row toggle (H23): tap the token chip to reveal the technique breakdown.
|
|
10630
10651
|
if (k === 'tok') { b.classList.toggle('open'); $('seeall').querySelector(`#tokdetail${i}`)?.classList.toggle('open'); return; }
|
|
10631
10652
|
if (k === 'reverify') {
|
|
10653
|
+
const copy = retryCopy(it.goal?.projectId || state.active);
|
|
10632
10654
|
b.disabled = true;
|
|
10633
|
-
b.textContent =
|
|
10655
|
+
b.textContent = copy.retrying;
|
|
10634
10656
|
let r; try { r = await fetch(withKey(`/api/goals/${it.goalId}/reverify`), { method: 'POST' }); }
|
|
10635
|
-
catch { showErr(
|
|
10636
|
-
if (r.ok) await refresh(); else showErr(
|
|
10657
|
+
catch { showErr(copy.failed); b.disabled = false; b.textContent = copy.retry; return; }
|
|
10658
|
+
if (r.ok) await refresh(); else { showErr(copy.failed); b.disabled = false; b.textContent = copy.retry; }
|
|
10637
10659
|
return;
|
|
10638
10660
|
}
|
|
10639
10661
|
// Open it (P1, Masa 2026-07-21): start the thing the worker said to run, then open it.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const CHANGE_REQUEST_RE = /(?:修正|直して|解決して|改善して|一致させ|統一して|実装して|追加して|削除して|変更して|作って|fix\b|solve\b|resolve\b|implement\b|change\b|make\b)/i;
|
|
2
|
+
const REFUSAL_RESULT_RE = /(?:直せません|修正できません|解決できません|実現できません|不可能です|cannot\s+(?:be\s+)?(?:fix(?:ed)?|solv(?:e|ed)|implement(?:ed)?|done)|can't\s+(?:fix|solve|implement|do)|unable\s+to\s+(?:fix|solve|implement))/i;
|
|
3
|
+
|
|
4
|
+
export const COMPLETION_CONTRACT_PROMPT = [
|
|
5
|
+
'MANAGER COMPLETION CONTRACT:',
|
|
6
|
+
'- The latest explicit user instruction can revise an older product decision or code comment. Treat old comments as context, not proof that the requested change is impossible. If the change has consequences, explain them and implement the latest request; only a real safety, permission, legal, or external-system constraint is a blocker.',
|
|
7
|
+
'- A test must assert the observable outcome the user requested. Never replace the request with a tooltip, explanation, or a test that permanently preserves the opposite behavior.',
|
|
8
|
+
'- Do not report a change request as complete with “cannot fix”, “直せません”, or equivalent. If a real external blocker remains, state the concrete blocker and leave the work explicitly unfinished instead of presenting it for Review as completed.',
|
|
9
|
+
].join('\n');
|
|
10
|
+
|
|
11
|
+
export function completionContractIssue({ request = '', followUp = '', result = '', status = '' } = {}) {
|
|
12
|
+
if (status !== 'done') return null;
|
|
13
|
+
const requested = `${request}\n${followUp}`;
|
|
14
|
+
if (!CHANGE_REQUEST_RE.test(requested) || !REFUSAL_RESULT_RE.test(String(result))) return null;
|
|
15
|
+
return {
|
|
16
|
+
kind: 'unresolved-refusal',
|
|
17
|
+
reason: '変更・解決依頼に対して「対応不能」で終了しており、要求した結果が実現されていません。',
|
|
18
|
+
};
|
|
19
|
+
}
|
package/engine/lib.mjs
CHANGED
|
@@ -1258,13 +1258,20 @@ export function classifyWorkerFailure({ agent = 'claude-code', result = '', err
|
|
|
1258
1258
|
actionIds,
|
|
1259
1259
|
});
|
|
1260
1260
|
if (timedOut || /run limit|実行上限|timed out|timeout|タイムアウト/.test(low)) {
|
|
1261
|
-
return infra('timeout', ['retry', '
|
|
1261
|
+
return infra('timeout', ['retry', 'view-log']);
|
|
1262
|
+
}
|
|
1263
|
+
// SIGTERM/SIGINT means the local worker was stopped from outside the task
|
|
1264
|
+
// (for example while the local Manager was restarting). It says nothing
|
|
1265
|
+
// about either the requested change or the chosen agent, so never turn this
|
|
1266
|
+
// recovery path into a suggestion to switch agents.
|
|
1267
|
+
if (code === 143 || code === 130 || /worker was interrupted|作業が途中で中断|exit 143|sigterm|sigint/.test(low)) {
|
|
1268
|
+
return infra('interrupted', ['retry', 'view-log']);
|
|
1262
1269
|
}
|
|
1263
1270
|
if (/spawn|enoent|eacces|command not found|executable|worker tool exited early|作業ツールが途中で終了/.test(low)) {
|
|
1264
|
-
return infra('worker-start', ['retry', '
|
|
1271
|
+
return infra('worker-start', ['retry', 'view-log']);
|
|
1265
1272
|
}
|
|
1266
1273
|
if (/auth probe|preflight|not logged in|logged out|not authenticated|authentication|unauthorized|invalid api key|bad credentials|401\b|403\b|接続が必要|認証/.test(low)) {
|
|
1267
|
-
return infra('auth', ['connect', 'check-connection', '
|
|
1274
|
+
return infra('auth', ['connect', 'check-connection', 'view-log']);
|
|
1268
1275
|
}
|
|
1269
1276
|
if (/relay|websocket|socket|econnreset|econnrefused|billing-api unreachable|cf access|cloudflare access/.test(low)) {
|
|
1270
1277
|
return infra('relay', ['retry', 'connect', 'view-log']);
|
|
@@ -1281,7 +1288,7 @@ export function classifyWorkerFailure({ agent = 'claude-code', result = '', err
|
|
|
1281
1288
|
return {
|
|
1282
1289
|
owner: 'agent',
|
|
1283
1290
|
reason: agent === 'codex' ? 'codex' : 'claude-code',
|
|
1284
|
-
actionIds: ['retry',
|
|
1291
|
+
actionIds: ['retry', 'view-log'],
|
|
1285
1292
|
};
|
|
1286
1293
|
}
|
|
1287
1294
|
|
package/engine/server.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { createSerialQueue } from './lib.mjs';
|
|
|
26
26
|
import { parseRequirementEvidence, unmappedChangedFiles } from './lib.mjs';
|
|
27
27
|
import { isPrClosed } from './lib.mjs';
|
|
28
28
|
import { collectProjectRules } from './lib.mjs';
|
|
29
|
+
import { COMPLETION_CONTRACT_PROMPT, completionContractIssue } from './completion-contract.mjs';
|
|
29
30
|
import { FALLBACK_CODEX_MODELS, loadCodexModels } from './codex-models.mjs';
|
|
30
31
|
import { CLAUDE_MODELS } from './claude-models.mjs';
|
|
31
32
|
import { classifyWorkerPreflight, shouldProbeClaudeAuth, workerCanStart } from './worker-availability.mjs';
|
|
@@ -2378,8 +2379,13 @@ async function startEphemeralApp(projectDir) {
|
|
|
2378
2379
|
const home = join(logDir, `ephemeral-${Date.now()}`);
|
|
2379
2380
|
mkdirSync(home, { recursive: true });
|
|
2380
2381
|
seedEphemeralHistory(home, projectDir);
|
|
2382
|
+
// Managed worktrees intentionally do not copy node_modules. An ephemeral
|
|
2383
|
+
// preview is still this Manager package, so let it resolve the already
|
|
2384
|
+
// installed runtime dependencies from the parent rather than crashing before
|
|
2385
|
+
// its readiness endpoint can answer (for example `puppeteer-core`).
|
|
2386
|
+
const nodePath = testNodePath(projectDir);
|
|
2381
2387
|
const child = spawn('node', [join(projectDir, 'engine', 'server.mjs')], {
|
|
2382
|
-
env: { ...process.env, MANAGER_PORT: String(port), MANAGER_HOME: home, MANAGER_OPEN_BROWSER: '0', MANAGER_SEED_RUNNING_FIXTURE: '1' },
|
|
2388
|
+
env: { ...process.env, NODE_PATH: nodePath, MANAGER_PORT: String(port), MANAGER_HOME: home, MANAGER_OPEN_BROWSER: '0', MANAGER_SEED_RUNNING_FIXTURE: '1' },
|
|
2383
2389
|
cwd: projectDir, stdio: 'ignore',
|
|
2384
2390
|
});
|
|
2385
2391
|
ephemeralChildren.add(child);
|
|
@@ -2721,6 +2727,7 @@ async function runTask(task) {
|
|
|
2721
2727
|
const prompt = [
|
|
2722
2728
|
managerHandoffPrompt(goal, plan),
|
|
2723
2729
|
workerPrompt(task, goal, null, runAgent, runContext),
|
|
2730
|
+
COMPLETION_CONTRACT_PROMPT,
|
|
2724
2731
|
'(User follow-up on the goal you worked on. Apply it to the same goal and deliver the requested result.)',
|
|
2725
2732
|
].filter(Boolean).join('\n\n');
|
|
2726
2733
|
const callT0 = Date.now();
|
|
@@ -2765,6 +2772,17 @@ async function runTask(task) {
|
|
|
2765
2772
|
task.unmappedChangedFiles = unmappedChangedFiles(task.changedFiles, task.requirementEvidence);
|
|
2766
2773
|
task.requirementVerification = { proof: task.proof ?? null, run: task.run ?? null, changedFiles: task.changedFiles };
|
|
2767
2774
|
sendProcessAct(task, `Detected ${task.changedFiles.length} changed file${task.changedFiles.length === 1 ? '' : 's'} from this follow-up.`);
|
|
2775
|
+
const completionIssue = completionContractIssue({
|
|
2776
|
+
request: goal?.text,
|
|
2777
|
+
followUp: task.detail,
|
|
2778
|
+
result: task.result,
|
|
2779
|
+
status: task.status,
|
|
2780
|
+
});
|
|
2781
|
+
if (completionIssue) {
|
|
2782
|
+
task.status = 'interrupted';
|
|
2783
|
+
task.result = `[未解決: ${completionIssue.reason}]\n\n${task.result}`.slice(0, RESULT_MAX);
|
|
2784
|
+
sendProcessAct(task, 'Completion contract rejected an unresolved refusal — kept out of Review for retry.');
|
|
2785
|
+
}
|
|
2768
2786
|
task.finishedAt = new Date().toISOString();
|
|
2769
2787
|
task.resultAttemptId = task.attemptId;
|
|
2770
2788
|
saveTask(task);
|
|
@@ -2799,7 +2817,7 @@ async function runTask(task) {
|
|
|
2799
2817
|
const runEffort = task.effort ?? goal?.effort;
|
|
2800
2818
|
const resumeSession = shouldFreshSessionForPlan(plan) ? undefined : (goalSessionFor(goal, runAgent) ?? undefined);
|
|
2801
2819
|
const runContext = workerRunContext({ goal, task, agent: runAgent, model: runModel, effort: runEffort, permissionMode, cwd: workDir, resumeSession, plan });
|
|
2802
|
-
const prompt = [managerHandoffPrompt(goal, plan), workerPrompt(task, goal, null, runAgent, runContext)].filter(Boolean).join('\n\n');
|
|
2820
|
+
const prompt = [managerHandoffPrompt(goal, plan), workerPrompt(task, goal, null, runAgent, runContext), COMPLETION_CONTRACT_PROMPT].filter(Boolean).join('\n\n');
|
|
2803
2821
|
sendWorkerContextAct(task, runContext);
|
|
2804
2822
|
sendProcessAct(task, `Starting ${runAgent === 'codex' ? 'Codex' : 'Claude Code'} worker (attempt ${attempt}/${maxAttempts}).`);
|
|
2805
2823
|
let r = await runWorkerAgent({
|
|
@@ -2833,7 +2851,7 @@ async function runTask(task) {
|
|
|
2833
2851
|
sendWorkerContextAct(task, freshContext);
|
|
2834
2852
|
r = await runWorkerAgent({
|
|
2835
2853
|
agent: runAgent,
|
|
2836
|
-
prompt: workerPrompt(task, goal, null, runAgent, freshContext), cwd: workDir, tools: WORKER_TOOLS,
|
|
2854
|
+
prompt: [workerPrompt(task, goal, null, runAgent, freshContext), COMPLETION_CONTRACT_PROMPT].join('\n\n'), cwd: workDir, tools: WORKER_TOOLS,
|
|
2837
2855
|
permissionMode,
|
|
2838
2856
|
model: runModel, effort: runEffort, onEvent: (line) => sendAct(task, line),
|
|
2839
2857
|
images: goal?.images,
|
|
@@ -2984,6 +3002,17 @@ async function runTask(task) {
|
|
|
2984
3002
|
task.result = `[未完了の疑い: 変更ファイル0件かつ応答が途中で切れています] ${task.result}`.slice(0, RESULT_MAX);
|
|
2985
3003
|
sendProcessAct(task, 'Result looked incomplete (0 changed files, cut-off text) — marked interrupted instead of done for human review.');
|
|
2986
3004
|
}
|
|
3005
|
+
const completionIssue = completionContractIssue({
|
|
3006
|
+
request: goal?.text,
|
|
3007
|
+
followUp: task.reply ? task.detail : '',
|
|
3008
|
+
result: task.result,
|
|
3009
|
+
status: task.status,
|
|
3010
|
+
});
|
|
3011
|
+
if (completionIssue) {
|
|
3012
|
+
task.status = 'interrupted';
|
|
3013
|
+
task.result = `[未解決: ${completionIssue.reason}]\n\n${task.result}`.slice(0, RESULT_MAX);
|
|
3014
|
+
sendProcessAct(task, 'Completion contract rejected an unresolved refusal — kept out of Review for retry.');
|
|
3015
|
+
}
|
|
2987
3016
|
task.finishedAt = new Date().toISOString();
|
|
2988
3017
|
task.resultAttemptId = task.attemptId;
|
|
2989
3018
|
saveTask(task);
|
package/package.json
CHANGED