@galda/cli 0.10.89 → 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/app/index.html +26 -44
- package/engine/lib.mjs +60 -1
- package/engine/server.mjs +71 -13
- package/package.json +1 -1
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}
|
|
@@ -3037,9 +3026,6 @@
|
|
|
3037
3026
|
reply exists (progressive disclosure — DESIGN-RULES §1.5-3, "存在
|
|
3038
3027
|
しないものの器を先に見せない"). -->
|
|
3039
3028
|
<div class="peraanswer" id="chatReply" hidden></div>
|
|
3040
|
-
<!-- Feature A: when the composer is bound to the focused goal, this chip shows
|
|
3041
|
-
which conversation you're replying to; ✕ detaches back to new-goal mode. -->
|
|
3042
|
-
<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>
|
|
3043
3029
|
<div class="composer">
|
|
3044
3030
|
<div class="attachbar" id="attachbar" style="display:none"></div>
|
|
3045
3031
|
<!-- Slash-command palette (Masa決定 2026-07-07: chips→slash). Floats
|
|
@@ -4367,7 +4353,6 @@ function mergeGoalTimeline(goals, externalActivity, projectId){
|
|
|
4367
4353
|
function goToGoalInChat(goalId, bindComposer = true){
|
|
4368
4354
|
state.selectedGoal = goalId;
|
|
4369
4355
|
if (bindComposer) state.composerDetachedGoal = null;
|
|
4370
|
-
renderReplyBind(); // Feature A: the composer's "Replying to #NNN" chip follows the focused goal — renderStream() alone skips it (only render() calls it)
|
|
4371
4356
|
renderStream();
|
|
4372
4357
|
const goalEl = document.querySelector(`#stream [data-goalmsg="${goalId}"]`);
|
|
4373
4358
|
if (!goalEl) return;
|
|
@@ -5860,6 +5845,18 @@ function renderReviewAttach(){
|
|
|
5860
5845
|
// 関わらず全部「直して」で、「なんでこうしたの?」と聞いただけでゴールが Doing に戻り
|
|
5861
5846
|
// ワーカーが走っていた。人はモードを普段選ばない——AI が迷った時だけ選ぶ。
|
|
5862
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
|
+
}
|
|
5863
5860
|
// The one waiting line's two labels. Local to the reply note on purpose — STATUS_LABEL
|
|
5864
5861
|
// is the task/workflow vocabulary and this is neither (CDO §5).
|
|
5865
5862
|
const REPLY_WAIT_ROUTING = 'Thinking…';
|
|
@@ -5867,9 +5864,9 @@ const REPLY_WAIT_ANSWERING = 'Answering…';
|
|
|
5867
5864
|
function replyWorkerPayload(extra = {}){
|
|
5868
5865
|
return { ...extra, agent: state.connectedApp, model: state.stickyModel, effort: state.stickyEffort };
|
|
5869
5866
|
}
|
|
5870
|
-
async function postReply(goalId, text, outcome = 'auto'){
|
|
5867
|
+
async function postReply(goalId, text, outcome = 'auto', targetProjectId = null){
|
|
5871
5868
|
const r = await fetch(withKey(`/api/goals/${goalId}/dismiss`), { method: 'POST',
|
|
5872
|
-
headers: { 'content-type': 'application/json' }, body: JSON.stringify(replyWorkerPayload({ text, outcome })) });
|
|
5869
|
+
headers: { 'content-type': 'application/json' }, body: JSON.stringify(replyWorkerPayload({ text, outcome, targetProjectId })) });
|
|
5873
5870
|
if (!r.ok) throw new Error();
|
|
5874
5871
|
return r.json();
|
|
5875
5872
|
}
|
|
@@ -6009,7 +6006,7 @@ function saCardThreadHtml(it){
|
|
|
6009
6006
|
: it.rstatus === 'working' ? replyWorkingHtml()
|
|
6010
6007
|
: it.rstatus === 'unsure' ? (replyNoteRowHtml('Which one is this?')
|
|
6011
6008
|
+ `<div class="qchips">${(it.rchoices || []).map((c) =>
|
|
6012
|
-
`<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>`)
|
|
6013
6010
|
: '';
|
|
6014
6011
|
return rows + trailer;
|
|
6015
6012
|
}
|
|
@@ -6054,7 +6051,7 @@ function askReplyDestination(anchorSel, choices){
|
|
|
6054
6051
|
// The You row stays above the chips so "this" has something to point at (CDO Q5).
|
|
6055
6052
|
setReplyNote(anchorSel, replyPastHtml() + said + replyNoteRowHtml('Which one is this?')
|
|
6056
6053
|
+ `<div class="qchips">${(choices || []).map((c) =>
|
|
6057
|
-
`<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);
|
|
6058
6055
|
});
|
|
6059
6056
|
}
|
|
6060
6057
|
// answer (§1.3): nothing was built, so there is no task to watch — the reply comes
|
|
@@ -6085,9 +6082,11 @@ async function replyToGoal(goalId, text, anchorSel){
|
|
|
6085
6082
|
showReplySent(anchorSel, text, Number(goalId)); // before the round trip: the words stay, and it says it is thinking
|
|
6086
6083
|
let body = await postReply(goalId, text, 'auto');
|
|
6087
6084
|
if (body.outcome === 'unsure') {
|
|
6088
|
-
const picked = await askReplyDestination(anchorSel, body
|
|
6085
|
+
const picked = await askReplyDestination(anchorSel, replyChoices(body));
|
|
6089
6086
|
if (!picked) return null;
|
|
6090
|
-
body =
|
|
6087
|
+
body = picked.startsWith('project:')
|
|
6088
|
+
? await postReply(goalId, text, 'split', picked.slice('project:'.length))
|
|
6089
|
+
: await postReply(goalId, text, picked);
|
|
6091
6090
|
}
|
|
6092
6091
|
// seen + 1: the question itself was just recorded; the answer is the one after it.
|
|
6093
6092
|
if (body.outcome === 'answer') showAnswerWhenItArrives(anchorSel, goalId, seen + 1);
|
|
@@ -6422,7 +6421,6 @@ $('gdRevisionInput').addEventListener('keydown', (e) => {
|
|
|
6422
6421
|
function render(){
|
|
6423
6422
|
renderProjects(); renderStream(); renderQueue(); renderTasks(); renderActPanel(); wireWfEditButtons();
|
|
6424
6423
|
renderCtxBar(); // the folder above the composer follows the active project
|
|
6425
|
-
renderReplyBind(); // Feature A: show/hide the "Replying to #NNN" chip for the focused goal
|
|
6426
6424
|
renderFlagship();
|
|
6427
6425
|
if (state.reviewOpenGoal != null) renderReviewChecklist();
|
|
6428
6426
|
if (state.goalDetailOpen != null) renderGoalDetail();
|
|
@@ -9013,22 +9011,6 @@ function replyBinding(){
|
|
|
9013
9011
|
if (REPLY_PLAIN.has(g.status)) return { goal: g, mode: 'plain' };
|
|
9014
9012
|
return null;
|
|
9015
9013
|
}
|
|
9016
|
-
// The visible, reversible "Replying to #NNN ✕" chip above the composer — never
|
|
9017
|
-
// silent (shown whenever bound), always detachable (✕ clears the binding).
|
|
9018
|
-
function renderReplyBind(){
|
|
9019
|
-
const el = $('replyBind'); if (!el) return;
|
|
9020
|
-
const b = replyBinding();
|
|
9021
|
-
el.hidden = !b;
|
|
9022
|
-
const inp = $('input');
|
|
9023
|
-
if (b) {
|
|
9024
|
-
const n = goalReviewNumber({ goal: b.goal, tasks: state.tasks }) ?? b.goal.id;
|
|
9025
|
-
const numEl = $('replyBindNum'); if (numEl) numEl.textContent = '#' + n;
|
|
9026
|
-
const lblEl = $('replyBindLabel'); if (lblEl) lblEl.textContent = b.mode === 'answer' ? 'Answering' : 'Replying to';
|
|
9027
|
-
if (inp) inp.placeholder = b.mode === 'answer' ? `Answering #${n}…` : `Reply to #${n}…`;
|
|
9028
|
-
} else if (inp) {
|
|
9029
|
-
inp.placeholder = 'Describe a goal… ( / for commands )';
|
|
9030
|
-
}
|
|
9031
|
-
}
|
|
9032
9014
|
async function send(){
|
|
9033
9015
|
const { text, model, effort, mode, skill, later, review } = resolveComposer($('input').value);
|
|
9034
9016
|
if (!text) return;
|
|
@@ -10419,8 +10401,8 @@ async function saCall(ids, path, body){
|
|
|
10419
10401
|
// (outcome:'auto', ONE-CONVERSATION §1.4) — this layer never guesses continue vs
|
|
10420
10402
|
// question. Unlike saAct it does NOT show its outcome first: "Feedback sent" would
|
|
10421
10403
|
// be a lie when the reply was a question.
|
|
10422
|
-
async function saReplyCall(it, text, outcome){
|
|
10423
|
-
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 });
|
|
10424
10406
|
if (!r?.ok) throw new Error('reply failed');
|
|
10425
10407
|
return r.json().catch(() => null);
|
|
10426
10408
|
}
|
|
@@ -10527,7 +10509,7 @@ async function saSendReply(it, i, text){
|
|
|
10527
10509
|
try {
|
|
10528
10510
|
let body = await saReplyCall(it, workerText, 'auto');
|
|
10529
10511
|
if (body?.outcome === 'unsure') {
|
|
10530
|
-
it.inflight = false; it.rstatus = 'unsure'; it.rchoices = body
|
|
10512
|
+
it.inflight = false; it.rstatus = 'unsure'; it.rchoices = replyChoices(body);
|
|
10531
10513
|
saPaintCard(goalId);
|
|
10532
10514
|
const picked = await new Promise((resolve) => { it._routeResolve = resolve; });
|
|
10533
10515
|
it._routeResolve = null;
|
|
@@ -10537,7 +10519,9 @@ async function saSendReply(it, i, text){
|
|
|
10537
10519
|
saPaintCard(goalId); return;
|
|
10538
10520
|
}
|
|
10539
10521
|
it.inflight = true; it.rstatus = 'thinking'; saPaintCard(goalId);
|
|
10540
|
-
body =
|
|
10522
|
+
body = picked.startsWith('project:')
|
|
10523
|
+
? await saReplyCall(it, workerText, 'split', picked.slice('project:'.length))
|
|
10524
|
+
: await saReplyCall(it, workerText, picked);
|
|
10541
10525
|
}
|
|
10542
10526
|
it.inflight = false;
|
|
10543
10527
|
if (body?.outcome === 'answer') { // a question: the AI answers, the goal never moves
|
|
@@ -11270,8 +11254,6 @@ $('filein').addEventListener('change', () => {
|
|
|
11270
11254
|
for (const f of $('filein').files) uploadFile(f);
|
|
11271
11255
|
$('filein').value = '';
|
|
11272
11256
|
});
|
|
11273
|
-
// Feature A: ✕ on the "Replying to #NNN" chip detaches → back to new-goal mode.
|
|
11274
|
-
$('replyBindX').onclick = () => { state.composerDetachedGoal = state.selectedGoal; render(); };
|
|
11275
11257
|
const IS_MOBILE = () => window.matchMedia('(max-width: 760px)').matches;
|
|
11276
11258
|
// Phones are chat-first: the Tasks sheet starts FOLDED so the first view is the
|
|
11277
11259
|
// conversation + composer (the sheet is an 88vw overlay — default-open buried
|
package/engine/lib.mjs
CHANGED
|
@@ -2458,6 +2458,55 @@ export function dismissGoal({ goalStatus, outcome = 'continue' } = {}) {
|
|
|
2458
2458
|
return { ok: true, outcome: 'continue', status: 'running', spawnsWorker: true };
|
|
2459
2459
|
}
|
|
2460
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
|
+
|
|
2461
2510
|
// 宛先(docs/design/ONE-CONVERSATION.md §1.2)
|
|
2462
2511
|
//
|
|
2463
2512
|
// チャットで「#12 の件だけど」と書けたら、そのチケット宛て。**決定論で、我々の推測は
|
|
@@ -3187,6 +3236,14 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
|
|
|
3187
3236
|
return [
|
|
3188
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.` : '',
|
|
3189
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') : '',
|
|
3190
3247
|
goal?.priorFailureMemory?.length ? `\nMANAGER FAILURE MEMORY:\n${latestFailurePolicy(goal.priorFailureMemory)?.text}\nApply this prevention before starting.` : '',
|
|
3191
3248
|
'',
|
|
3192
3249
|
// The user's own words, verbatim and in full, are the source of truth.
|
|
@@ -3404,7 +3461,9 @@ export function parseRunDeclaration(text) {
|
|
|
3404
3461
|
const compare = parseComparePair(raw.compare);
|
|
3405
3462
|
const title = str(raw.title, 60);
|
|
3406
3463
|
// Nothing to start, nothing to open, nothing named, and no pair to show = nothing to look at.
|
|
3407
|
-
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
|
+
}
|
|
3408
3467
|
// Only http(s): the button opens this in the user's browser. A file:// (or
|
|
3409
3468
|
// any other scheme) is a worker mistake with a specific fix — declare `file`
|
|
3410
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