@galda/cli 0.10.86 → 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 +39 -22
- package/engine/completion-contract.mjs +19 -0
- package/engine/server.mjs +38 -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
|
|
@@ -9859,6 +9869,11 @@ function saItemHtml(it, i){
|
|
|
9859
9869
|
// verification visible together while prior attempts stay in Conversation.
|
|
9860
9870
|
const attempt = it.attempt;
|
|
9861
9871
|
const activeText = String(attempt?.activeRequirement?.text || '').replace(/\s+/g, ' ').trim();
|
|
9872
|
+
const requestText = String(it.goalText || '').replace(/\s+/g, ' ').trim();
|
|
9873
|
+
// A single requirement is commonly recorded verbatim from the Request. Do
|
|
9874
|
+
// not make the review read it back as a second section; keep this section for
|
|
9875
|
+
// a narrowed retry requirement or concrete run evidence instead.
|
|
9876
|
+
const reviewText = activeText && activeText !== requestText ? activeText : '';
|
|
9862
9877
|
const attemptFiles = Array.isArray(attempt?.changedFiles) ? attempt.changedFiles.filter(Boolean) : [];
|
|
9863
9878
|
const attemptTests = attempt?.verification?.tests;
|
|
9864
9879
|
const attemptProof = attempt?.verification?.proof;
|
|
@@ -9867,8 +9882,8 @@ function saItemHtml(it, i){
|
|
|
9867
9882
|
attemptTests?.ran ? `Tests: ${attemptTests.passed ?? 0} passed${attemptTests.failed ? ` / ${attemptTests.failed} failed` : ''}` : '',
|
|
9868
9883
|
attemptProof?.pass === true ? 'Proof recorded' : '',
|
|
9869
9884
|
].filter(Boolean).join(' · ');
|
|
9870
|
-
const attemptSec =
|
|
9871
|
-
? `<section class="rsec"><div class="rsec-h">This review</div>${
|
|
9885
|
+
const attemptSec = reviewText || attemptEvidence
|
|
9886
|
+
? `<section class="rsec"><div class="rsec-h">This review</div>${reviewText ? `<p class="rbody rask">${esc(reviewText)}</p>` : ''}${attemptEvidence ? `<p class="rbody v">${esc(attemptEvidence)}</p>` : ''}</section>`
|
|
9872
9887
|
: '';
|
|
9873
9888
|
const reqVerify = it.requirementVerification;
|
|
9874
9889
|
const reqVerifyParts = [
|
|
@@ -9877,10 +9892,15 @@ function saItemHtml(it, i){
|
|
|
9877
9892
|
Array.isArray(reqVerify?.changedFiles) && reqVerify.changedFiles.length ? `Files: ${reqVerify.changedFiles.join(' · ')}` : '',
|
|
9878
9893
|
].filter(Boolean);
|
|
9879
9894
|
const reqVerifySec = reqVerifyParts.length ? `<section class="rsec"><div class="rsec-h">Requirement verification</div><p class="rbody v">${esc(reqVerifyParts.join(' · '))}</p></section>` : '';
|
|
9880
|
-
const
|
|
9895
|
+
const coverage = (it.requirementCoverage || []).filter((r) => r.text);
|
|
9896
|
+
// For one requirement copied from Request, its status is useful but the full
|
|
9897
|
+
// sentence is not. Multiple or narrowed requirements retain their text.
|
|
9898
|
+
const coverageIsSingleRequest = coverage.length === 1
|
|
9899
|
+
&& String(coverage[0].text).replace(/\s+/g, ' ').trim() === requestText;
|
|
9900
|
+
const coverageRows = coverage.map((r) => {
|
|
9881
9901
|
const status = r.evidenceStatus || r.taskStatus;
|
|
9882
9902
|
const tone = status === 'addressed' || status === 'done' ? 'ok' : status === 'partial' ? '' : 'dim';
|
|
9883
|
-
return `<p class="rbody v"><b class="${tone}">${esc(status)}</b
|
|
9903
|
+
return `<p class="rbody v"><b class="${tone}">${esc(status)}</b>${coverageIsSingleRequest ? '' : ` · ${esc(r.text)}`}</p>`;
|
|
9884
9904
|
}).join('');
|
|
9885
9905
|
const coverageSec = coverageRows ? `<section class="rsec"><div class="rsec-h">Requirement coverage</div>${coverageRows}</section>` : '';
|
|
9886
9906
|
// Worker-declared evidence is displayed verbatim as an auditable label, not
|
|
@@ -9890,8 +9910,6 @@ function saItemHtml(it, i){
|
|
|
9890
9910
|
return `<p class="rbody v"><b class="${tone}">${esc(entry.status)}</b> · ${esc(entry.evidence || entry.id)}</p>`;
|
|
9891
9911
|
}).join('');
|
|
9892
9912
|
const evidenceSec = evidenceRows ? `<section class="rsec"><div class="rsec-h">Requirement evidence</div>${evidenceRows}</section>` : '';
|
|
9893
|
-
const userCoverageRows = (it.requirementCoverage || []).filter((r) => r.text).map((r) => `<p class="rbody v"><b class="${r.status === 'done' ? 'ok' : 'dim'}">${esc(r.status)}</b> · ${esc(r.text)}</p>`).join('');
|
|
9894
|
-
const userCoverageSec = userCoverageRows ? `<section class="rsec"><div class="rsec-h">User requirements</div>${userCoverageRows}</section>` : '';
|
|
9895
9913
|
// ── Open it (Masa 2026-07-22): a plain section in the card's own typography — the same
|
|
9896
9914
|
// heading + body shape as Request and Result, not a chip in another vocabulary. The
|
|
9897
9915
|
// address the worker declared IS the control (press it → the process starts on demand →
|
|
@@ -10122,14 +10140,12 @@ function saItemHtml(it, i){
|
|
|
10122
10140
|
${coverageSec}
|
|
10123
10141
|
${evidenceSec}
|
|
10124
10142
|
${reqVerifySec}
|
|
10125
|
-
${
|
|
10126
|
-
${S('Open it', openBody)}
|
|
10143
|
+
${S('Open it', openBody + infraBody)}
|
|
10127
10144
|
${S('Run state', interruptedBody)}
|
|
10128
10145
|
${S('PR', prRepairBody)}
|
|
10129
10146
|
${S('Also carries', carriesBody)}
|
|
10130
10147
|
${S('What to check', checkBody)}
|
|
10131
10148
|
${S('Result', reportBody)}
|
|
10132
|
-
${S(infra?.title || '検証基盤エラー', infraBody)}
|
|
10133
10149
|
${S('Screenshots', shotsBody)}
|
|
10134
10150
|
${S('Steps', stepsBody)}
|
|
10135
10151
|
${S('Proof', proofInner)}
|
|
@@ -10622,11 +10638,12 @@ $('seeall').addEventListener('click', async (e) => {
|
|
|
10622
10638
|
// meta-row toggle (H23): tap the token chip to reveal the technique breakdown.
|
|
10623
10639
|
if (k === 'tok') { b.classList.toggle('open'); $('seeall').querySelector(`#tokdetail${i}`)?.classList.toggle('open'); return; }
|
|
10624
10640
|
if (k === 'reverify') {
|
|
10641
|
+
const copy = retryCopy(it.goal?.projectId || state.active);
|
|
10625
10642
|
b.disabled = true;
|
|
10626
|
-
b.textContent =
|
|
10643
|
+
b.textContent = copy.retrying;
|
|
10627
10644
|
let r; try { r = await fetch(withKey(`/api/goals/${it.goalId}/reverify`), { method: 'POST' }); }
|
|
10628
|
-
catch { showErr(
|
|
10629
|
-
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; }
|
|
10630
10647
|
return;
|
|
10631
10648
|
}
|
|
10632
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);
|
|
@@ -5520,6 +5549,12 @@ async function answerGoalQuestion(goal, question) {
|
|
|
5520
5549
|
addGoalMessage(goal, { from: 'you', via: 'review', text });
|
|
5521
5550
|
goal.text = text.slice(0, 8000);
|
|
5522
5551
|
}
|
|
5552
|
+
// A PR request added during Review changes the deliverable, not the
|
|
5553
|
+
// worker's permissions. Workers deliberately cannot git add/commit; the
|
|
5554
|
+
// Manager close-out below owns commit/push/PR creation. Keep this
|
|
5555
|
+
// monotonic: an explicit later PR request upgrades a No-PR/auto goal,
|
|
5556
|
+
// while ordinary feedback never silently downgrades an existing PR goal.
|
|
5557
|
+
if (result.spawnsWorker && resolveWantsPR('auto', text, false)) goal.wantsPR = true;
|
|
5523
5558
|
goal.status = result.status;
|
|
5524
5559
|
saveGoal(goal);
|
|
5525
5560
|
// rescope はワーカーを起こさない——「いったん止めて考え直す」なので、走り出したら
|
package/package.json
CHANGED