@galda/cli 0.10.87 → 0.10.88
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 +25 -15
- package/engine/completion-contract.mjs +19 -0
- package/engine/server.mjs +32 -3
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -3633,10 +3633,7 @@ function reviewCardConfigFor(projectId){ return { ...DEFAULT_REVIEW_DEFINITION.r
|
|
|
3633
3633
|
// project to one language regardless of what a given goal was typed in.
|
|
3634
3634
|
function reviewLangFor(projectId){ const l = reviewDefinitionFor(projectId).language; return (l === 'en' || l === 'ja') ? l : 'auto'; }
|
|
3635
3635
|
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' };
|
|
3636
|
+
return { infra: 'Verification infrastructure error', retry: 'Retry verification', retrying: 'Retrying…', failed: 'Verification retry failed' };
|
|
3640
3637
|
}
|
|
3641
3638
|
|
|
3642
3639
|
function esc(s){ const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
|
@@ -4891,7 +4888,8 @@ function renderTasks(){
|
|
|
4891
4888
|
for (const b of $('tasklist').querySelectorAll('[data-goalreverify]')) {
|
|
4892
4889
|
b.onclick = async (e) => { e.stopPropagation(); const copy = retryCopy(state.active); b.disabled = true; b.textContent = copy.retrying;
|
|
4893
4890
|
let r; try { r = await fetch(withKey(`/api/goals/${b.dataset.goalreverify}/reverify`), { method: 'POST' }); }
|
|
4894
|
-
catch { showErr(copy.failed); return; }
|
|
4891
|
+
catch { showErr(copy.failed); b.disabled = false; b.textContent = copy.retry; return; }
|
|
4892
|
+
if (!r.ok) { showErr(copy.failed); b.disabled = false; b.textContent = copy.retry; return; }
|
|
4895
4893
|
qrefresh(r, copy.failed); };
|
|
4896
4894
|
}
|
|
4897
4895
|
for (const b of $('tasklist').querySelectorAll('[data-qreply]')) {
|
|
@@ -5557,8 +5555,8 @@ function renderReviewChecklist(){
|
|
|
5557
5555
|
b.disabled = true;
|
|
5558
5556
|
b.textContent = retryCopy(goal.projectId).retrying;
|
|
5559
5557
|
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);
|
|
5558
|
+
catch { showErr(retryCopy(goal.projectId).failed); b.disabled = false; b.textContent = retryCopy(goal.projectId).retry; return; }
|
|
5559
|
+
if (r.ok) await refresh(); else { showErr(retryCopy(goal.projectId).failed); b.disabled = false; b.textContent = retryCopy(goal.projectId).retry; }
|
|
5562
5560
|
};
|
|
5563
5561
|
}
|
|
5564
5562
|
for (const b of $('reviewList').querySelectorAll('.reqsend')) {
|
|
@@ -8530,7 +8528,7 @@ let sseUp = false; // live engine link — the honest signal behind the MCP tab'
|
|
|
8530
8528
|
// Connection status line (queue tray): grace-debounce the offline state ~800ms so
|
|
8531
8529
|
// sub-second SSE blips don't flash a red line. connOffline is the debounced flag
|
|
8532
8530
|
// renderQueue() reads; connSetOnline/Offline flip it around real sseUp changes.
|
|
8533
|
-
let connOffline = false, _connGraceT = null, _sseReconnectT = null, _es = null, _healthT = null;
|
|
8531
|
+
let connOffline = false, _connGraceT = null, _sseReconnectT = null, _es = null, _healthT = null, _healthFailStreak = 0;
|
|
8534
8532
|
function connSetOnline(){ if (_connGraceT) { clearTimeout(_connGraceT); _connGraceT = null; } if (connOffline) { connOffline = false; renderQueue(); } }
|
|
8535
8533
|
function connSetOffline(){ if (_connGraceT || connOffline) return; _connGraceT = setTimeout(() => { _connGraceT = null; connOffline = true; renderQueue(); }, 800); }
|
|
8536
8534
|
// Reconnect button: drop any pending backoff timer and reopen the EventSource now.
|
|
@@ -8545,15 +8543,23 @@ async function checkHealth(){
|
|
|
8545
8543
|
if (document.hidden) return;
|
|
8546
8544
|
let ok = false;
|
|
8547
8545
|
try {
|
|
8548
|
-
|
|
8546
|
+
// Mobile browsers and DevTools device emulation can briefly starve this low-priority
|
|
8547
|
+
// probe even while the already-open SSE and POST path are healthy. Do not turn one
|
|
8548
|
+
// transient probe into a user-visible outage.
|
|
8549
|
+
const c = new AbortController(); const to = setTimeout(() => c.abort(), 5000);
|
|
8549
8550
|
const r = await fetch(withKey('/'), { method: 'HEAD', cache: 'no-store', signal: c.signal });
|
|
8550
8551
|
clearTimeout(to); ok = r.ok;
|
|
8551
8552
|
} catch { ok = false; }
|
|
8552
8553
|
if (!ok) {
|
|
8554
|
+
_healthFailStreak++;
|
|
8555
|
+
if (_healthFailStreak < 2) return;
|
|
8553
8556
|
connSetOffline(); // grace-debounced ~800ms
|
|
8554
8557
|
if (!_sseReconnectT) { try { _es && _es.close(); } catch {} sseUp = false; esRetryMs = Math.min(esRetryMs, 4000); _sseReconnectT = setTimeout(connectSSE, esRetryMs); }
|
|
8555
|
-
} else
|
|
8558
|
+
} else {
|
|
8559
|
+
_healthFailStreak = 0;
|
|
8560
|
+
if (connOffline && !_sseReconnectT) {
|
|
8556
8561
|
forceReconnect(); // server reachable again but the line is still up → reconnect to clear + flush
|
|
8562
|
+
}
|
|
8557
8563
|
}
|
|
8558
8564
|
}
|
|
8559
8565
|
function startHealthPoll(){ clearInterval(_healthT); _healthT = setInterval(checkHealth, 4000); }
|
|
@@ -8773,6 +8779,10 @@ async function boot(){
|
|
|
8773
8779
|
flushOutbox();
|
|
8774
8780
|
connectSSE();
|
|
8775
8781
|
startHealthPoll();
|
|
8782
|
+
// A Wi-Fi ⇄ cellular switch can leave EventSource waiting for its normal retry
|
|
8783
|
+
// budget. The browser has already confirmed connectivity here, so reconnect now
|
|
8784
|
+
// instead of making a mobile send wait for the next health poll/backoff cycle.
|
|
8785
|
+
window.addEventListener('online', () => { _healthFailStreak = 0; forceReconnect(); });
|
|
8776
8786
|
document.addEventListener('visibilitychange', () => { if (!document.hidden) { refresh(); flushOutbox(); checkHealth(); } });
|
|
8777
8787
|
// belt and suspenders for the never-lose guarantee: even if every event
|
|
8778
8788
|
// path misfires, unsent memos are retried on a slow heartbeat
|
|
@@ -10130,13 +10140,12 @@ function saItemHtml(it, i){
|
|
|
10130
10140
|
${coverageSec}
|
|
10131
10141
|
${evidenceSec}
|
|
10132
10142
|
${reqVerifySec}
|
|
10133
|
-
${S('Open it', openBody)}
|
|
10143
|
+
${S('Open it', openBody + infraBody)}
|
|
10134
10144
|
${S('Run state', interruptedBody)}
|
|
10135
10145
|
${S('PR', prRepairBody)}
|
|
10136
10146
|
${S('Also carries', carriesBody)}
|
|
10137
10147
|
${S('What to check', checkBody)}
|
|
10138
10148
|
${S('Result', reportBody)}
|
|
10139
|
-
${S(infra?.title || '検証基盤エラー', infraBody)}
|
|
10140
10149
|
${S('Screenshots', shotsBody)}
|
|
10141
10150
|
${S('Steps', stepsBody)}
|
|
10142
10151
|
${S('Proof', proofInner)}
|
|
@@ -10629,11 +10638,12 @@ $('seeall').addEventListener('click', async (e) => {
|
|
|
10629
10638
|
// meta-row toggle (H23): tap the token chip to reveal the technique breakdown.
|
|
10630
10639
|
if (k === 'tok') { b.classList.toggle('open'); $('seeall').querySelector(`#tokdetail${i}`)?.classList.toggle('open'); return; }
|
|
10631
10640
|
if (k === 'reverify') {
|
|
10641
|
+
const copy = retryCopy(it.goal?.projectId || state.active);
|
|
10632
10642
|
b.disabled = true;
|
|
10633
|
-
b.textContent =
|
|
10643
|
+
b.textContent = copy.retrying;
|
|
10634
10644
|
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(
|
|
10645
|
+
catch { showErr(copy.failed); b.disabled = false; b.textContent = copy.retry; return; }
|
|
10646
|
+
if (r.ok) await refresh(); else { showErr(copy.failed); b.disabled = false; b.textContent = copy.retry; }
|
|
10637
10647
|
return;
|
|
10638
10648
|
}
|
|
10639
10649
|
// 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/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