@galda/cli 0.10.88 → 0.10.90
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 +52 -58
- package/engine/lib.mjs +71 -5
- package/engine/server.mjs +71 -13
- 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
|
@@ -294,17 +294,6 @@
|
|
|
294
294
|
.gearpop{}, so out here it was invalid at computed-value time and fell back to
|
|
295
295
|
transparent — measured rgba(0,0,0,0). The chips were never filled to begin with. */
|
|
296
296
|
.ctxbar{display:flex;align-items:center;gap:6px;padding:0 2px 7px;background:none;border:0}
|
|
297
|
-
/* Feature A: "Replying to #NNN ✕" chip — neutral hairline pill above the composer
|
|
298
|
-
(real tokens only → theme-adaptive; the ✕ hover moves glyph colour only, §5.5). */
|
|
299
|
-
.replybind{display:flex;align-items:center;gap:6px;max-width:var(--fscenterw,760px);width:100%;margin:0 auto 6px;padding:0 2px}
|
|
300
|
-
.replybind[hidden]{display:none}
|
|
301
|
-
.replybind .rb-seg{display:inline-flex;align-items:center;gap:6px;padding:5px 11px;border-radius:999px;
|
|
302
|
-
border:1px solid var(--hair);background:var(--panel);font:500 12px/1 var(--ui);color:var(--ink2)}
|
|
303
|
-
.replybind .rb-seg b{color:var(--ink);font-weight:600;font-family:var(--mono);font-variant-numeric:tabular-nums}
|
|
304
|
-
.replybind .rb-x{display:grid;place-items:center;width:24px;height:24px;border:0;border-radius:7px;background:transparent;
|
|
305
|
-
color:var(--ink3);cursor:pointer;transition:color .12s;flex:0 0 24px}
|
|
306
|
-
.replybind .rb-x:hover{color:var(--ink)}
|
|
307
|
-
.replybind .rb-x svg{width:13px;height:13px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
|
308
297
|
.ctxchip{display:inline-flex;align-items:center;height:26px;padding:0;gap:0;border-radius:8px;
|
|
309
298
|
border:1px solid var(--hair);background:transparent;position:relative;min-width:0}
|
|
310
299
|
.ctxsel{position:relative}
|
|
@@ -1558,6 +1547,14 @@
|
|
|
1558
1547
|
line-height:1.15;padding:2.5px 10px;border-radius:7px;box-shadow:inset 0 0 0 1px var(--hair2)}
|
|
1559
1548
|
#fsRoot .acts2 .errbtn:first-child{color:var(--ink2)}
|
|
1560
1549
|
#fsRoot .errbtn:hover{background:var(--hover);color:var(--ink)}
|
|
1550
|
+
/* The review/attention dialog is rendered in #seeall, outside #fsRoot.
|
|
1551
|
+
Keep its recovery actions on the same designed component instead of
|
|
1552
|
+
falling back to the browser's unstyled native button. */
|
|
1553
|
+
#seeall .acts2{display:flex;gap:6px;margin-top:3px;align-items:center}
|
|
1554
|
+
#seeall .errbtn{border:0;background:transparent;color:var(--ink3);font-family:var(--mono);font-size:10px;letter-spacing:.02em;cursor:pointer;
|
|
1555
|
+
line-height:1.15;padding:2.5px 10px;border-radius:7px;box-shadow:inset 0 0 0 1px var(--hair2)}
|
|
1556
|
+
#seeall .acts2 .errbtn:first-child{color:var(--ink2)}
|
|
1557
|
+
#seeall .errbtn:hover{background:var(--hover);color:var(--ink)}
|
|
1561
1558
|
/* AI question in the feed (q2 — §6.5: while current it lives in the chat feed).
|
|
1562
1559
|
H18 / CDO HANDOFF-question-ui-2026-07-13: QUESTION card = hairline frame +
|
|
1563
1560
|
2px --blue left rule + mono label + option chips + an always-on free-text
|
|
@@ -3029,9 +3026,6 @@
|
|
|
3029
3026
|
reply exists (progressive disclosure — DESIGN-RULES §1.5-3, "存在
|
|
3030
3027
|
しないものの器を先に見せない"). -->
|
|
3031
3028
|
<div class="peraanswer" id="chatReply" hidden></div>
|
|
3032
|
-
<!-- Feature A: when the composer is bound to the focused goal, this chip shows
|
|
3033
|
-
which conversation you're replying to; ✕ detaches back to new-goal mode. -->
|
|
3034
|
-
<div class="replybind" id="replyBind" hidden><span class="rb-seg"><span id="replyBindLabel">Replying to</span> <b id="replyBindNum"></b></span><button type="button" class="rb-x" id="replyBindX" title="New goal instead" aria-label="Detach"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button></div>
|
|
3035
3029
|
<div class="composer">
|
|
3036
3030
|
<div class="attachbar" id="attachbar" style="display:none"></div>
|
|
3037
3031
|
<!-- Slash-command palette (Masa決定 2026-07-07: chips→slash). Floats
|
|
@@ -4359,7 +4353,6 @@ function mergeGoalTimeline(goals, externalActivity, projectId){
|
|
|
4359
4353
|
function goToGoalInChat(goalId, bindComposer = true){
|
|
4360
4354
|
state.selectedGoal = goalId;
|
|
4361
4355
|
if (bindComposer) state.composerDetachedGoal = null;
|
|
4362
|
-
renderReplyBind(); // Feature A: the composer's "Replying to #NNN" chip follows the focused goal — renderStream() alone skips it (only render() calls it)
|
|
4363
4356
|
renderStream();
|
|
4364
4357
|
const goalEl = document.querySelector(`#stream [data-goalmsg="${goalId}"]`);
|
|
4365
4358
|
if (!goalEl) return;
|
|
@@ -5852,6 +5845,18 @@ function renderReviewAttach(){
|
|
|
5852
5845
|
// 関わらず全部「直して」で、「なんでこうしたの?」と聞いただけでゴールが Doing に戻り
|
|
5853
5846
|
// ワーカーが走っていた。人はモードを普段選ばない——AI が迷った時だけ選ぶ。
|
|
5854
5847
|
const REPLY_CHOICE_LABEL = { continue: 'Fix this', rescope: 'Rethink it', split: 'Separate ticket', answer: 'Just a question' };
|
|
5848
|
+
function replyChoiceLabel(choice){
|
|
5849
|
+
if (choice?.startsWith('project:')) {
|
|
5850
|
+
const id = choice.slice('project:'.length);
|
|
5851
|
+
return `Implement in ${state.projects?.find((project) => project.id === id)?.name || id}`;
|
|
5852
|
+
}
|
|
5853
|
+
return REPLY_CHOICE_LABEL[choice] ?? choice;
|
|
5854
|
+
}
|
|
5855
|
+
function replyChoices(body){
|
|
5856
|
+
const routes = body?.choices || REPLY_OUTCOMES_CLIENT;
|
|
5857
|
+
const targets = (body?.projectChoices || []).filter((project) => !project.current).map((project) => `project:${project.id}`);
|
|
5858
|
+
return [...routes, ...targets];
|
|
5859
|
+
}
|
|
5855
5860
|
// The one waiting line's two labels. Local to the reply note on purpose — STATUS_LABEL
|
|
5856
5861
|
// is the task/workflow vocabulary and this is neither (CDO §5).
|
|
5857
5862
|
const REPLY_WAIT_ROUTING = 'Thinking…';
|
|
@@ -5859,9 +5864,9 @@ const REPLY_WAIT_ANSWERING = 'Answering…';
|
|
|
5859
5864
|
function replyWorkerPayload(extra = {}){
|
|
5860
5865
|
return { ...extra, agent: state.connectedApp, model: state.stickyModel, effort: state.stickyEffort };
|
|
5861
5866
|
}
|
|
5862
|
-
async function postReply(goalId, text, outcome = 'auto'){
|
|
5867
|
+
async function postReply(goalId, text, outcome = 'auto', targetProjectId = null){
|
|
5863
5868
|
const r = await fetch(withKey(`/api/goals/${goalId}/dismiss`), { method: 'POST',
|
|
5864
|
-
headers: { 'content-type': 'application/json' }, body: JSON.stringify(replyWorkerPayload({ text, outcome })) });
|
|
5869
|
+
headers: { 'content-type': 'application/json' }, body: JSON.stringify(replyWorkerPayload({ text, outcome, targetProjectId })) });
|
|
5865
5870
|
if (!r.ok) throw new Error();
|
|
5866
5871
|
return r.json();
|
|
5867
5872
|
}
|
|
@@ -6001,7 +6006,7 @@ function saCardThreadHtml(it){
|
|
|
6001
6006
|
: it.rstatus === 'working' ? replyWorkingHtml()
|
|
6002
6007
|
: it.rstatus === 'unsure' ? (replyNoteRowHtml('Which one is this?')
|
|
6003
6008
|
+ `<div class="qchips">${(it.rchoices || []).map((c) =>
|
|
6004
|
-
`<button type="button" class="qchip" data-rc="${esc(c)}" data-rcgoal="${it.goalId}">${esc(
|
|
6009
|
+
`<button type="button" class="qchip" data-rc="${esc(c)}" data-rcgoal="${it.goalId}">${esc(replyChoiceLabel(c))}</button>`).join('')}</div>`)
|
|
6005
6010
|
: '';
|
|
6006
6011
|
return rows + trailer;
|
|
6007
6012
|
}
|
|
@@ -6046,7 +6051,7 @@ function askReplyDestination(anchorSel, choices){
|
|
|
6046
6051
|
// The You row stays above the chips so "this" has something to point at (CDO Q5).
|
|
6047
6052
|
setReplyNote(anchorSel, replyPastHtml() + said + replyNoteRowHtml('Which one is this?')
|
|
6048
6053
|
+ `<div class="qchips">${(choices || []).map((c) =>
|
|
6049
|
-
`<button type="button" class="qchip" data-rc="${esc(c)}">${esc(
|
|
6054
|
+
`<button type="button" class="qchip" data-rc="${esc(c)}">${esc(replyChoiceLabel(c))}</button>`).join('')}</div>`, resolve);
|
|
6050
6055
|
});
|
|
6051
6056
|
}
|
|
6052
6057
|
// answer (§1.3): nothing was built, so there is no task to watch — the reply comes
|
|
@@ -6077,9 +6082,11 @@ async function replyToGoal(goalId, text, anchorSel){
|
|
|
6077
6082
|
showReplySent(anchorSel, text, Number(goalId)); // before the round trip: the words stay, and it says it is thinking
|
|
6078
6083
|
let body = await postReply(goalId, text, 'auto');
|
|
6079
6084
|
if (body.outcome === 'unsure') {
|
|
6080
|
-
const picked = await askReplyDestination(anchorSel, body
|
|
6085
|
+
const picked = await askReplyDestination(anchorSel, replyChoices(body));
|
|
6081
6086
|
if (!picked) return null;
|
|
6082
|
-
body =
|
|
6087
|
+
body = picked.startsWith('project:')
|
|
6088
|
+
? await postReply(goalId, text, 'split', picked.slice('project:'.length))
|
|
6089
|
+
: await postReply(goalId, text, picked);
|
|
6083
6090
|
}
|
|
6084
6091
|
// seen + 1: the question itself was just recorded; the answer is the one after it.
|
|
6085
6092
|
if (body.outcome === 'answer') showAnswerWhenItArrives(anchorSel, goalId, seen + 1);
|
|
@@ -6414,7 +6421,6 @@ $('gdRevisionInput').addEventListener('keydown', (e) => {
|
|
|
6414
6421
|
function render(){
|
|
6415
6422
|
renderProjects(); renderStream(); renderQueue(); renderTasks(); renderActPanel(); wireWfEditButtons();
|
|
6416
6423
|
renderCtxBar(); // the folder above the composer follows the active project
|
|
6417
|
-
renderReplyBind(); // Feature A: show/hide the "Replying to #NNN" chip for the focused goal
|
|
6418
6424
|
renderFlagship();
|
|
6419
6425
|
if (state.reviewOpenGoal != null) renderReviewChecklist();
|
|
6420
6426
|
if (state.goalDetailOpen != null) renderGoalDetail();
|
|
@@ -7189,6 +7195,7 @@ const FAILURE_COPY = {
|
|
|
7189
7195
|
infra: {
|
|
7190
7196
|
title: 'Galda基盤エラー',
|
|
7191
7197
|
timeout: 'Galda側の実行上限や待ち時間で止まりました。変更内容の失敗とは限りません。',
|
|
7198
|
+
interrupted: 'ローカルの実行が中断されました。変更内容やAgentの失敗とは限らないため、同じ作業を再開できます。',
|
|
7192
7199
|
auth: '接続や認証確認で止まりました。接続を直してから再開できます。',
|
|
7193
7200
|
relay: 'relay接続で止まりました。接続状態を確認してから再試行できます。',
|
|
7194
7201
|
chrome: 'Chrome/ブラウザ確認で止まりました。画面確認環境の問題で、実装失敗とは限りません。',
|
|
@@ -7198,8 +7205,8 @@ const FAILURE_COPY = {
|
|
|
7198
7205
|
unknown: 'Galdaの実行基盤で止まりました。ログを確認し、原因に応じて再試行できます。',
|
|
7199
7206
|
},
|
|
7200
7207
|
actions: {
|
|
7201
|
-
retry: '
|
|
7202
|
-
other: '
|
|
7208
|
+
retry: 'Retry', resume: 'Resume', connect: 'Connect', check: 'Check connection',
|
|
7209
|
+
other: 'Try another Agent', log: 'View log', tests: 'Re-run tests', pr: 'Retry PR',
|
|
7203
7210
|
},
|
|
7204
7211
|
},
|
|
7205
7212
|
en: {
|
|
@@ -7207,6 +7214,7 @@ const FAILURE_COPY = {
|
|
|
7207
7214
|
infra: {
|
|
7208
7215
|
title: 'Galda system issue',
|
|
7209
7216
|
timeout: 'Galda stopped this at the run limit or a wait timed out. This is not necessarily a problem with the change.',
|
|
7217
|
+
interrupted: 'The local run was interrupted. This does not necessarily reflect a problem with the change or agent; resume the same work.',
|
|
7210
7218
|
auth: 'Connection or authentication failed. Reconnect, then continue.',
|
|
7211
7219
|
relay: 'The relay connection failed. Check the connection, then retry.',
|
|
7212
7220
|
chrome: 'Chrome/browser verification failed. The UI check environment stopped, not necessarily the implementation.',
|
|
@@ -7227,17 +7235,23 @@ function failureLang(){
|
|
|
7227
7235
|
function inferErrorClass(text, agent){
|
|
7228
7236
|
const low = String(text || '').toLowerCase();
|
|
7229
7237
|
const infra = (reason, actionIds) => ({ owner: 'galda-infra', reason, actionIds });
|
|
7230
|
-
if (/run limit|実行上限|timed out|timeout|タイムアウト/.test(low)) return infra('timeout', ['retry', '
|
|
7231
|
-
if (/
|
|
7232
|
-
if (/
|
|
7238
|
+
if (/run limit|実行上限|timed out|timeout|タイムアウト/.test(low)) return infra('timeout', ['retry', 'view-log']);
|
|
7239
|
+
if (/worker was interrupted|作業が途中で中断|exit 143|sigterm|sigint/.test(low)) return infra('interrupted', ['retry', 'view-log']);
|
|
7240
|
+
if (/spawn|enoent|eacces|command not found|worker tool exited early|作業ツールが途中で終了/.test(low)) return infra('worker-start', ['retry', 'view-log']);
|
|
7241
|
+
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']);
|
|
7233
7242
|
if (/relay|websocket|socket|econnreset|econnrefused|billing-api unreachable|cf access|cloudflare access/.test(low)) return infra('relay', ['retry', 'connect', 'view-log']);
|
|
7234
7243
|
if (/chrome|chromium|puppeteer|target closed|browser|navigation timeout|waiting failed|screenshot|headless/.test(low)) return infra('chrome', ['retry', 'view-log']);
|
|
7235
7244
|
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']);
|
|
7236
7245
|
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']);
|
|
7237
|
-
return { owner: 'agent', reason: agent === 'codex' ? 'codex' : 'claude-code', actionIds: ['retry', '
|
|
7246
|
+
return { owner: 'agent', reason: agent === 'codex' ? 'codex' : 'claude-code', actionIds: ['retry', 'view-log'] };
|
|
7238
7247
|
}
|
|
7239
7248
|
function taskErrorMeta(t, goal){
|
|
7240
|
-
|
|
7249
|
+
// Reclassify interrupted tasks from their concrete exit text instead of
|
|
7250
|
+
// trusting stale persisted metadata. Older records labelled an external
|
|
7251
|
+
// SIGTERM/restart as an Agent failure and permanently carried a Codex switch.
|
|
7252
|
+
const inferred = inferErrorClass(t.result, t.agent || goal?.agent);
|
|
7253
|
+
const raw = t.status === 'interrupted' ? inferred : (t.errorClass || inferred);
|
|
7254
|
+
const meta = { ...raw, actionIds: (raw.actionIds || []).filter((id) => !['run-with-codex', 'run-with-claude'].includes(id)) };
|
|
7241
7255
|
const lang = failureLang(), C = FAILURE_COPY[lang];
|
|
7242
7256
|
const isInfra = meta.owner === 'galda-infra';
|
|
7243
7257
|
return {
|
|
@@ -7251,20 +7265,16 @@ function goalBlockedErrorMeta(g){
|
|
|
7251
7265
|
const raw = g.blocked?.errorClass || (g.blocked?.kind === 'test'
|
|
7252
7266
|
? { owner: 'galda-infra', reason: 'test-runner', actionIds: ['retry-tests', 'view-log'] }
|
|
7253
7267
|
: inferErrorClass(g.blocked?.reason, g.agent));
|
|
7268
|
+
const meta = { ...raw, actionIds: (raw.actionIds || []).filter((id) => !['run-with-codex', 'run-with-claude'].includes(id)) };
|
|
7254
7269
|
const lang = failureLang(), C = FAILURE_COPY[lang];
|
|
7255
|
-
const isInfra =
|
|
7256
|
-
return { ...
|
|
7270
|
+
const isInfra = meta.owner === 'galda-infra';
|
|
7271
|
+
return { ...meta, title: isInfra ? C.infra.title : C.agent.title, body: isInfra ? (C.infra[meta.reason] || C.infra.unknown) : C.agent.body, copy: C };
|
|
7257
7272
|
}
|
|
7258
7273
|
function errorButtonsHtml(meta, { taskId = null, goalId = null, interrupted = false, agent = null, pr = false } = {}){
|
|
7259
7274
|
const C = meta.copy.actions;
|
|
7260
|
-
const canCodex = goalId != null && agent !== 'codex' && (state.agents || []).includes('codex');
|
|
7261
7275
|
const out = [];
|
|
7262
7276
|
for (const id of meta.actionIds || []) {
|
|
7263
7277
|
if (id === 'retry' && taskId != null) out.push(`<button class="errbtn" data-fsretry="${taskId}">${interrupted ? C.resume : C.retry}</button>`);
|
|
7264
|
-
// This keeps the existing agent-switch endpoint, but says what it really does
|
|
7265
|
-
// for an interrupted run: continue the saved goal with another agent rather
|
|
7266
|
-
// than imply a brand-new, contextless task will be created.
|
|
7267
|
-
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>`);
|
|
7268
7278
|
else if (id === 'connect') out.push(`<button class="errbtn" data-fsconnectworker="1">${C.connect}</button>`);
|
|
7269
7279
|
else if (id === 'check-connection') out.push(`<button class="errbtn" data-fscheckworker="1">${C.check}</button>`);
|
|
7270
7280
|
else if (id === 'retry-tests' && goalId != null) out.push(`<button class="errbtn" data-fsreverify="${goalId}">${C.tests}</button>`);
|
|
@@ -9001,22 +9011,6 @@ function replyBinding(){
|
|
|
9001
9011
|
if (REPLY_PLAIN.has(g.status)) return { goal: g, mode: 'plain' };
|
|
9002
9012
|
return null;
|
|
9003
9013
|
}
|
|
9004
|
-
// The visible, reversible "Replying to #NNN ✕" chip above the composer — never
|
|
9005
|
-
// silent (shown whenever bound), always detachable (✕ clears the binding).
|
|
9006
|
-
function renderReplyBind(){
|
|
9007
|
-
const el = $('replyBind'); if (!el) return;
|
|
9008
|
-
const b = replyBinding();
|
|
9009
|
-
el.hidden = !b;
|
|
9010
|
-
const inp = $('input');
|
|
9011
|
-
if (b) {
|
|
9012
|
-
const n = goalReviewNumber({ goal: b.goal, tasks: state.tasks }) ?? b.goal.id;
|
|
9013
|
-
const numEl = $('replyBindNum'); if (numEl) numEl.textContent = '#' + n;
|
|
9014
|
-
const lblEl = $('replyBindLabel'); if (lblEl) lblEl.textContent = b.mode === 'answer' ? 'Answering' : 'Replying to';
|
|
9015
|
-
if (inp) inp.placeholder = b.mode === 'answer' ? `Answering #${n}…` : `Reply to #${n}…`;
|
|
9016
|
-
} else if (inp) {
|
|
9017
|
-
inp.placeholder = 'Describe a goal… ( / for commands )';
|
|
9018
|
-
}
|
|
9019
|
-
}
|
|
9020
9014
|
async function send(){
|
|
9021
9015
|
const { text, model, effort, mode, skill, later, review } = resolveComposer($('input').value);
|
|
9022
9016
|
if (!text) return;
|
|
@@ -10407,8 +10401,8 @@ async function saCall(ids, path, body){
|
|
|
10407
10401
|
// (outcome:'auto', ONE-CONVERSATION §1.4) — this layer never guesses continue vs
|
|
10408
10402
|
// question. Unlike saAct it does NOT show its outcome first: "Feedback sent" would
|
|
10409
10403
|
// be a lie when the reply was a question.
|
|
10410
|
-
async function saReplyCall(it, text, outcome){
|
|
10411
|
-
const r = await saCall(it.ids, 'dismiss', { text, outcome });
|
|
10404
|
+
async function saReplyCall(it, text, outcome, targetProjectId = null){
|
|
10405
|
+
const r = await saCall(it.ids, 'dismiss', { text, outcome, targetProjectId });
|
|
10412
10406
|
if (!r?.ok) throw new Error('reply failed');
|
|
10413
10407
|
return r.json().catch(() => null);
|
|
10414
10408
|
}
|
|
@@ -10515,7 +10509,7 @@ async function saSendReply(it, i, text){
|
|
|
10515
10509
|
try {
|
|
10516
10510
|
let body = await saReplyCall(it, workerText, 'auto');
|
|
10517
10511
|
if (body?.outcome === 'unsure') {
|
|
10518
|
-
it.inflight = false; it.rstatus = 'unsure'; it.rchoices = body
|
|
10512
|
+
it.inflight = false; it.rstatus = 'unsure'; it.rchoices = replyChoices(body);
|
|
10519
10513
|
saPaintCard(goalId);
|
|
10520
10514
|
const picked = await new Promise((resolve) => { it._routeResolve = resolve; });
|
|
10521
10515
|
it._routeResolve = null;
|
|
@@ -10525,7 +10519,9 @@ async function saSendReply(it, i, text){
|
|
|
10525
10519
|
saPaintCard(goalId); return;
|
|
10526
10520
|
}
|
|
10527
10521
|
it.inflight = true; it.rstatus = 'thinking'; saPaintCard(goalId);
|
|
10528
|
-
body =
|
|
10522
|
+
body = picked.startsWith('project:')
|
|
10523
|
+
? await saReplyCall(it, workerText, 'split', picked.slice('project:'.length))
|
|
10524
|
+
: await saReplyCall(it, workerText, picked);
|
|
10529
10525
|
}
|
|
10530
10526
|
it.inflight = false;
|
|
10531
10527
|
if (body?.outcome === 'answer') { // a question: the AI answers, the goal never moves
|
|
@@ -11258,8 +11254,6 @@ $('filein').addEventListener('change', () => {
|
|
|
11258
11254
|
for (const f of $('filein').files) uploadFile(f);
|
|
11259
11255
|
$('filein').value = '';
|
|
11260
11256
|
});
|
|
11261
|
-
// Feature A: ✕ on the "Replying to #NNN" chip detaches → back to new-goal mode.
|
|
11262
|
-
$('replyBindX').onclick = () => { state.composerDetachedGoal = state.selectedGoal; render(); };
|
|
11263
11257
|
const IS_MOBILE = () => window.matchMedia('(max-width: 760px)').matches;
|
|
11264
11258
|
// Phones are chat-first: the Tasks sheet starts FOLDED so the first view is the
|
|
11265
11259
|
// conversation + composer (the sheet is an 88vw overlay — default-open buried
|
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
|
|
|
@@ -2451,6 +2458,55 @@ export function dismissGoal({ goalStatus, outcome = 'continue' } = {}) {
|
|
|
2451
2458
|
return { ok: true, outcome: 'continue', status: 'running', spawnsWorker: true };
|
|
2452
2459
|
}
|
|
2453
2460
|
|
|
2461
|
+
// `outcome:auto` is the contract used by every review reply box in the app.
|
|
2462
|
+
// Keep the routing decision in one place: the reply either asks a question,
|
|
2463
|
+
// requests a change, rescopes the ticket, starts separate work, or explicitly
|
|
2464
|
+
// remains unsure. Returning unsure is safe because the UI already presents
|
|
2465
|
+
// these exact choices without moving the goal.
|
|
2466
|
+
export const REPLY_INTENTS = [...REPLY_OUTCOMES, 'unsure'];
|
|
2467
|
+
export function buildReplyIntentPrompt({ goalText, reply, currentProject = null, projects = [] } = {}) {
|
|
2468
|
+
const projectLines = (projects ?? []).map((project) =>
|
|
2469
|
+
`- ${project.id}: ${project.name || project.id} | root=${project.dir || '(unknown)'} | scope=${project.note || '(not specified)'}`);
|
|
2470
|
+
return [
|
|
2471
|
+
'You route replies in Agent Manager: a person delegated a coding task, an AI did it, and it came back for review. The person has now replied.',
|
|
2472
|
+
'Decide what the reply ASKS FOR — exactly one of:',
|
|
2473
|
+
'- "continue": fix or change the work that was just done.',
|
|
2474
|
+
'- "rescope": stop and rethink; the request itself should now say something different.',
|
|
2475
|
+
'- "split": a separate piece of work that should not touch this one.',
|
|
2476
|
+
'- "answer": a question about the work. Nothing should be built or changed.',
|
|
2477
|
+
'- "unsure": you genuinely cannot tell. Prefer this over guessing.',
|
|
2478
|
+
'Judge the full meaning. A question followed by a requested change is "continue".',
|
|
2479
|
+
'Also decide which project must receive the requested work.',
|
|
2480
|
+
'- Keep targetProjectId equal to the current project for ordinary fixes to the reviewed work.',
|
|
2481
|
+
'- If the current project is design/docs/prototype-only and the reply asks to implement or launch the chosen design in the real product, select the matching production/code project.',
|
|
2482
|
+
'- Never infer a project that is not listed. If the target is ambiguous, use outcome "unsure" and targetProjectId null.',
|
|
2483
|
+
'Output ONLY one line of raw JSON: {"outcome":"continue|rescope|split|answer|unsure","targetProjectId":"listed-id-or-null"}.',
|
|
2484
|
+
'',
|
|
2485
|
+
`Current project: ${currentProject?.id || '(unknown)'}`,
|
|
2486
|
+
'Available projects:',
|
|
2487
|
+
...(projectLines.length ? projectLines : ['- (none)']),
|
|
2488
|
+
'',
|
|
2489
|
+
'The request that was worked on:',
|
|
2490
|
+
String(goalText ?? '').slice(0, 2000),
|
|
2491
|
+
'',
|
|
2492
|
+
'Their reply:',
|
|
2493
|
+
String(reply ?? '').slice(0, 4000),
|
|
2494
|
+
].join('\n');
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
export function parseReplyIntent(raw) {
|
|
2498
|
+
const unread = { outcome: 'unsure', read: false };
|
|
2499
|
+
if (!raw || typeof raw !== 'string') return unread;
|
|
2500
|
+
const m = raw.match(/\{[\s\S]*\}/);
|
|
2501
|
+
if (!m) return unread;
|
|
2502
|
+
let obj;
|
|
2503
|
+
try { obj = JSON.parse(m[0]); } catch { return unread; }
|
|
2504
|
+
if (!REPLY_INTENTS.includes(obj?.outcome)) return unread;
|
|
2505
|
+
const targetProjectId = typeof obj.targetProjectId === 'string' && obj.targetProjectId.trim()
|
|
2506
|
+
? obj.targetProjectId.trim() : null;
|
|
2507
|
+
return { outcome: obj.outcome, targetProjectId, read: true };
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2454
2510
|
// 宛先(docs/design/ONE-CONVERSATION.md §1.2)
|
|
2455
2511
|
//
|
|
2456
2512
|
// チャットで「#12 の件だけど」と書けたら、そのチケット宛て。**決定論で、我々の推測は
|
|
@@ -3180,6 +3236,14 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
|
|
|
3180
3236
|
return [
|
|
3181
3237
|
skill ? `FIRST, use the /${skill} skill via the Skill tool (invoke Skill with the "${skill}" skill) before doing anything else. Then carry out the task below.` : '',
|
|
3182
3238
|
`You are a ${displayName} worker managed by "Manager for AI". Do the request below inside this project directory.`,
|
|
3239
|
+
task?.projectContext ? [
|
|
3240
|
+
'PROJECT BOUNDARY (source of truth):',
|
|
3241
|
+
`- Project: ${task.projectContext.name || task.projectContext.id}`,
|
|
3242
|
+
`- Root: ${task.projectContext.dir}`,
|
|
3243
|
+
task.projectContext.note ? `- Scope/note: ${task.projectContext.note}` : '',
|
|
3244
|
+
'- Your edits and claims apply only to this project. Never call a design/docs/prototype change a production implementation.',
|
|
3245
|
+
'- If the request requires another project, do not pretend it was implemented here; report the exact project mismatch.',
|
|
3246
|
+
].filter(Boolean).join('\n') : '',
|
|
3183
3247
|
goal?.priorFailureMemory?.length ? `\nMANAGER FAILURE MEMORY:\n${latestFailurePolicy(goal.priorFailureMemory)?.text}\nApply this prevention before starting.` : '',
|
|
3184
3248
|
'',
|
|
3185
3249
|
// The user's own words, verbatim and in full, are the source of truth.
|
|
@@ -3397,7 +3461,9 @@ export function parseRunDeclaration(text) {
|
|
|
3397
3461
|
const compare = parseComparePair(raw.compare);
|
|
3398
3462
|
const title = str(raw.title, 60);
|
|
3399
3463
|
// Nothing to start, nothing to open, nothing named, and no pair to show = nothing to look at.
|
|
3400
|
-
if (!cmd && !url && !file && !compare && !title)
|
|
3464
|
+
if (!cmd && !url && !file && !compare && !title) {
|
|
3465
|
+
return runDeclError('no-open-target', 'declaration contains no command, URL, artifact file, comparison, or title');
|
|
3466
|
+
}
|
|
3401
3467
|
// Only http(s): the button opens this in the user's browser. A file:// (or
|
|
3402
3468
|
// any other scheme) is a worker mistake with a specific fix — declare `file`
|
|
3403
3469
|
// instead — not a JSON parse failure, so it gets its own reason.
|
package/engine/server.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { homedir } from 'node:os';
|
|
|
21
21
|
import { fileURLToPath } from 'node:url';
|
|
22
22
|
import { pathToFileURL } from 'node:url';
|
|
23
23
|
import { needsProjectFolder, folderLabel, chooseFolderScript, parseChosenFolder, buildFolderQuestion, resolveFolderAnswer, classifyFolderTarget, buildFolderInitConfirm, isFolderInitConfirmed, needsReviewPreference, buildReviewPreferenceQuestion, resolveReviewPreferenceAnswer, routeQuestionAnswer, notUnderstoodNote, parseRelocalizedQuestion, classifyAuthProbe, authBannerText, makeSigninChallenge, parseClaimResponse, isCommandOnPath } from './lib.mjs';
|
|
24
|
-
import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, classifyWorkerFailure, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildReviewAttemptLedger, latestGoalOutcomeTask, buildDirtyWorkspacePrBlock, buildGoalProofMd, buildGoalPrBody, buildReviewSummaryPrompt, parseReviewSummary, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, rejectGoal, reopenGoal, permissionModeFor, isPlanReview, nextAfterPlanApprove, GOAL_MODES, parseSkillFrontmatter, collectSkills, retestGoal, cancelRetestGoal, computeRetestOutcome, revertGoal, planRevertActions, archiveGoal, unarchiveGoal, undismissGoal, parseNumstat, truncateDiffText, sumUsage, resolveEntryUrl, rebasePreviewUrl, shouldCreateGoalPR, normalizePullRequestUrl, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, resolveTestGate, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, workerPrompt, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, autoFoldReviewsEnabled, parseGitUnifiedDiffLocations, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, classifyPrSafety, isInconclusiveTestRun, countInfraFlakes, classifyRunFailures, classifyVerifyGate, buildVerificationInfraError, verificationInfraErrorFrom, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, QUEUED_GOAL_STATUSES, verifyLicenseToken, licenseTokenPayload, shouldRefreshLicense, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, testFailureReason, manualTestRetryPrompt, isNothingVerifiable, isNothingVerifiableForPR, isSuspiciouslyIncompleteDone, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, shouldUsePlannerForGoal, RUN_DECL_FILE, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, buildGoalMessage, serializeGoalMessage, parseGoalMessages, computeInternalQualityMetrics, classifyInternalQualityTaskError, resolveWorkspaceMode, goalSessionFor, setGoalAgentSession, switchGoalAgentSession, parseApprovalRequest } from './lib.mjs';
|
|
24
|
+
import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, classifyWorkerFailure, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildReviewAttemptLedger, latestGoalOutcomeTask, buildDirtyWorkspacePrBlock, buildGoalProofMd, buildGoalPrBody, buildReviewSummaryPrompt, parseReviewSummary, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, rejectGoal, reopenGoal, permissionModeFor, isPlanReview, nextAfterPlanApprove, GOAL_MODES, parseSkillFrontmatter, collectSkills, retestGoal, cancelRetestGoal, computeRetestOutcome, revertGoal, planRevertActions, archiveGoal, unarchiveGoal, undismissGoal, parseNumstat, truncateDiffText, sumUsage, resolveEntryUrl, rebasePreviewUrl, shouldCreateGoalPR, normalizePullRequestUrl, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, resolveTestGate, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, workerPrompt, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, autoFoldReviewsEnabled, parseGitUnifiedDiffLocations, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, classifyPrSafety, isInconclusiveTestRun, countInfraFlakes, classifyRunFailures, classifyVerifyGate, buildVerificationInfraError, verificationInfraErrorFrom, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, QUEUED_GOAL_STATUSES, verifyLicenseToken, licenseTokenPayload, shouldRefreshLicense, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, testFailureReason, manualTestRetryPrompt, isNothingVerifiable, isNothingVerifiableForPR, isSuspiciouslyIncompleteDone, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, shouldUsePlannerForGoal, RUN_DECL_FILE, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, buildReplyIntentPrompt, parseReplyIntent, buildGoalMessage, serializeGoalMessage, parseGoalMessages, computeInternalQualityMetrics, classifyInternalQualityTaskError, resolveWorkspaceMode, goalSessionFor, setGoalAgentSession, switchGoalAgentSession, parseApprovalRequest } from './lib.mjs';
|
|
25
25
|
import { createSerialQueue } from './lib.mjs';
|
|
26
26
|
import { parseRequirementEvidence, unmappedChangedFiles } from './lib.mjs';
|
|
27
27
|
import { isPrClosed } from './lib.mjs';
|
|
@@ -2288,13 +2288,15 @@ function pauseGoalForApproval(goal, task, request) {
|
|
|
2288
2288
|
//
|
|
2289
2289
|
// 設定は親から引き継ぐ(プロジェクト・エージェント・モデル・effort)。別件とはいえ
|
|
2290
2290
|
// 同じ場所で同じ人が続けている作業なので、ここで選び直させる理由が無い。
|
|
2291
|
-
function createSplitGoal(parent, text, identity) {
|
|
2291
|
+
function createSplitGoal(parent, text, identity, targetProjectId = parent.projectId) {
|
|
2292
|
+
const targetProject = projects.find((project) => project.id === targetProjectId);
|
|
2293
|
+
if (!targetProject) throw new Error(`unknown target project: ${targetProjectId}`);
|
|
2292
2294
|
const goal = {
|
|
2293
|
-
id: nextId++, projectId:
|
|
2294
|
-
wantsPR: resolveWantsPR('auto', text, getReviewDefinition(
|
|
2295
|
+
id: nextId++, projectId: targetProjectId, text: text.slice(0, 8000), status: 'planning',
|
|
2296
|
+
wantsPR: resolveWantsPR('auto', text, getReviewDefinition(targetProjectId).defaultWantsPR),
|
|
2295
2297
|
model: parent.model, effort: parent.effort, agent: parent.agent, mode: 'auto',
|
|
2296
2298
|
skill: null, images: [], source: resolveGoalSource('app'), sourceRef: null,
|
|
2297
|
-
executionPlan: buildExecutionPlan({ projectId:
|
|
2299
|
+
executionPlan: buildExecutionPlan({ projectId: targetProjectId, text, previousSession: false }),
|
|
2298
2300
|
plan: null, pr: undefined, createdAt: new Date().toISOString(),
|
|
2299
2301
|
// 片方向の印。表示は app(新しいカード「#N の会話から生まれました」/元のカード
|
|
2300
2302
|
// 「→ #M を切り出しました」)。engine は数字を1つ持つだけ。
|
|
@@ -2444,6 +2446,13 @@ function readRunDeclaration(task, workDir) {
|
|
|
2444
2446
|
// never broken) and wrote the exact same file:// url a second time. `run` is
|
|
2445
2447
|
// `{ error, message }` here, distinct from the `null` case below.
|
|
2446
2448
|
if (run && run.error) {
|
|
2449
|
+
// Valid JSON with only note/check means there is simply nothing to open.
|
|
2450
|
+
// That is common for explanation-only follow-ups and must not manufacture
|
|
2451
|
+
// an "unreadable record" error in Review.
|
|
2452
|
+
if (run.error === 'no-open-target') {
|
|
2453
|
+
sendAct(task, 'run declaration ignored — this result has nothing to open.');
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2447
2456
|
task.runDeclError = { reason: run.error, message: run.message, snippet: String(raw ?? '').slice(0, 200) };
|
|
2448
2457
|
sendAct(task, `run declaration ignored — ${run.message}`);
|
|
2449
2458
|
return;
|
|
@@ -2673,6 +2682,7 @@ async function startPreview(task) {
|
|
|
2673
2682
|
async function runTask(task) {
|
|
2674
2683
|
const project = projects.find((p) => p.id === task.projectId);
|
|
2675
2684
|
const goal = goals.find((g) => g.id === task.goalId);
|
|
2685
|
+
task.projectContext = project ? { id: project.id, name: project.name, dir: project.dir, note: project.note } : null;
|
|
2676
2686
|
task.status = 'running'; task.startedAt = new Date().toISOString();
|
|
2677
2687
|
// The goal's "started" clock is set by the first task that actually runs — the
|
|
2678
2688
|
// signal the board uses to read a 'running' goal as DOING rather than QUEUED
|
|
@@ -5447,6 +5457,30 @@ const server = createServer(async (req, res) => {
|
|
|
5447
5457
|
// 「聞いてるだけ」への返事 (ONE-CONVERSATION.md §1.3 answer)。
|
|
5448
5458
|
//
|
|
5449
5459
|
// 答えるのは**作業した AI 本人**=その agent の session を resume する。状況一覧しか
|
|
5460
|
+
// Resolve the app's `outcome:auto` contract before changing goal state. The
|
|
5461
|
+
// utility call is classification-only; the goal's own agent/session still
|
|
5462
|
+
// writes an answer or performs the requested change. If routing is unavailable,
|
|
5463
|
+
// return unsure so a customer chooses explicitly instead of accidentally
|
|
5464
|
+
// starting a ten-minute implementation worker for a question.
|
|
5465
|
+
async function judgeReplyIntent(goal, reply) {
|
|
5466
|
+
try {
|
|
5467
|
+
const currentProject = projects.find((project) => project.id === goal.projectId) ?? null;
|
|
5468
|
+
const r = await runUtility({
|
|
5469
|
+
prompt: buildReplyIntentPrompt({ goalText: goal.text, reply, currentProject, projects }),
|
|
5470
|
+
cwd: ROOT,
|
|
5471
|
+
tools: 'Read',
|
|
5472
|
+
});
|
|
5473
|
+
const parsed = parseReplyIntent(r.result);
|
|
5474
|
+
if (!parsed.read) return { outcome: 'unsure', targetProjectId: null };
|
|
5475
|
+
if (parsed.targetProjectId && !projects.some((project) => project.id === parsed.targetProjectId)) {
|
|
5476
|
+
return { outcome: 'unsure', targetProjectId: null };
|
|
5477
|
+
}
|
|
5478
|
+
return { outcome: parsed.outcome, targetProjectId: parsed.targetProjectId };
|
|
5479
|
+
} catch {
|
|
5480
|
+
return { outcome: 'unsure', targetProjectId: null };
|
|
5481
|
+
}
|
|
5482
|
+
}
|
|
5483
|
+
|
|
5450
5484
|
// 知らない別AIでは「なぜそうしたか」に答えられない(Masa確定 2026-07-22)。
|
|
5451
5485
|
// コードは1文字も変わらない: ツールは Read だけ、permissionMode は 'plan'
|
|
5452
5486
|
// (Codex 側は read-only サンドボックスに落ちる)。タスクも作らないので看板も
|
|
@@ -5497,20 +5531,29 @@ async function answerGoalQuestion(goal, question) {
|
|
|
5497
5531
|
let body = '';
|
|
5498
5532
|
req.on('data', (d) => { body += d; });
|
|
5499
5533
|
req.on('end', async () => {
|
|
5500
|
-
let text = '', outcome = 'continue', requested = {};
|
|
5534
|
+
let text = '', outcome = 'continue', requested = {}, auto = false;
|
|
5501
5535
|
try {
|
|
5502
5536
|
requested = JSON.parse(body || '{}');
|
|
5503
5537
|
text = String(requested.text ?? '').trim();
|
|
5504
5538
|
if (REPLY_OUTCOMES.includes(requested.outcome)) outcome = requested.outcome;
|
|
5539
|
+
auto = requested.outcome === 'auto';
|
|
5505
5540
|
} catch {}
|
|
5506
5541
|
const worker = applyReplyWorker(goal, requested);
|
|
5507
5542
|
if (!worker.ok) return json(res, 409, { error: worker.error });
|
|
5508
5543
|
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5544
|
+
if (auto) {
|
|
5545
|
+
if (!text) return json(res, 400, { error: 'text required' });
|
|
5546
|
+
const routed = await judgeReplyIntent(goal, text);
|
|
5547
|
+
outcome = routed.outcome;
|
|
5548
|
+
requested.targetProjectId = routed.targetProjectId;
|
|
5549
|
+
if (outcome === 'unsure') return json(res, 200, {
|
|
5550
|
+
goal,
|
|
5551
|
+
outcome,
|
|
5552
|
+
choices: REPLY_OUTCOMES,
|
|
5553
|
+
projectChoices: projects.map((project) => ({ id: project.id, name: project.name, current: project.id === goal.projectId })),
|
|
5554
|
+
});
|
|
5555
|
+
if (requested.targetProjectId && requested.targetProjectId !== goal.projectId) outcome = 'split';
|
|
5556
|
+
}
|
|
5514
5557
|
// 返事の行き先4種 (ONE-CONVERSATION.md §1.3). 既定は今までどおり continue なので、
|
|
5515
5558
|
// outcome を送ってこない今の UI の挙動は1ミリも変わらない。
|
|
5516
5559
|
const result = dismissGoal({ goalStatus: goal.status, outcome });
|
|
@@ -5523,8 +5566,23 @@ async function answerGoalQuestion(goal, question) {
|
|
|
5523
5566
|
const gate = await requireEntitlementGate(identityFor(req));
|
|
5524
5567
|
if (!gate.allowed) return json(res, 402, { error: 'free-tier limit reached', blocked: gate.blocked });
|
|
5525
5568
|
addGoalMessage(goal, { from: 'you', via: 'review', text });
|
|
5526
|
-
const
|
|
5527
|
-
return json(res,
|
|
5569
|
+
const targetProjectId = requested.targetProjectId || goal.projectId;
|
|
5570
|
+
if (!projects.some((project) => project.id === targetProjectId)) return json(res, 400, { error: 'unknown target project' });
|
|
5571
|
+
const crossProject = targetProjectId !== goal.projectId;
|
|
5572
|
+
const latest = [...tasks].reverse().find((task) => task.goalId === goal.id && task.result);
|
|
5573
|
+
const handoffText = crossProject ? [
|
|
5574
|
+
`[Cross-project implementation handoff from #${goal.id}]`,
|
|
5575
|
+
`Original request: ${goal.text}`,
|
|
5576
|
+
goal.reviewSummary?.summary ? `Reviewed result: ${goal.reviewSummary.summary}` : '',
|
|
5577
|
+
latest?.result ? `Latest worker result: ${String(latest.result).slice(0, 3000)}` : '',
|
|
5578
|
+
`User's implementation request: ${text}`,
|
|
5579
|
+
].filter(Boolean).join('\n\n') : text;
|
|
5580
|
+
const born = createSplitGoal(goal, handoffText, identityFor(req), targetProjectId);
|
|
5581
|
+
if (crossProject) {
|
|
5582
|
+
const target = projects.find((project) => project.id === targetProjectId);
|
|
5583
|
+
addGoalMessage(goal, { from: 'manager', via: 'review', text: `Implementation was routed to ${target?.name || targetProjectId} (#${born.id}) instead of editing this project's review.` });
|
|
5584
|
+
}
|
|
5585
|
+
return json(res, 200, { goal, outcome, splitInto: born.id, born, targetProjectId });
|
|
5528
5586
|
}
|
|
5529
5587
|
|
|
5530
5588
|
// answer: 聞かれただけ。ゴールは review のまま、ワーカーも起こさない(§1.3)。
|
package/package.json
CHANGED