@galda/cli 0.10.72 → 0.10.74
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 +232 -196
- package/engine/lib.mjs +133 -8
- package/engine/server.mjs +85 -4
- package/package.json +1 -1
package/engine/lib.mjs
CHANGED
|
@@ -655,9 +655,25 @@ export function estimateTokenOptimization({ usage = null, weightedTokens = null,
|
|
|
655
655
|
// layer did to save tokens, and it is the opposite: the model is the user's
|
|
656
656
|
// own pick from the composer, and we deliberately never override it
|
|
657
657
|
// (Masa 2026-07-22). Every remaining line is an action this layer took.
|
|
658
|
+
|
|
659
|
+
// Masa 2026-07-26: a positive `baseline` percent is a comparison against past
|
|
660
|
+
// goals, not proof any technique was applied THIS run — a small goal that
|
|
661
|
+
// followed a run of large ones scores a big "saved" number purely from the
|
|
662
|
+
// size mix, with nothing the user actually did behind it. Only call it
|
|
663
|
+
// "saved" when a real technique signal fired this run (cache reuse, a fresh
|
|
664
|
+
// handoff, /compact, /clear); otherwise it is a true fact (this run was
|
|
665
|
+
// smaller/larger than usual) but not a causal claim, so we relabel it
|
|
666
|
+
// `baseline-untied` and the renderer must phrase it as a comparison, not a
|
|
667
|
+
// "you saved X% by doing Y".
|
|
668
|
+
const techniqueDriven = cacheRatio > 0 || usedHandoff || sessionMode === 'cold-handoff' || usedCompact || sessionMode === 'compact' || usedClear;
|
|
669
|
+
if (basis === 'baseline' && !techniqueDriven) basis = 'baseline-untied';
|
|
670
|
+
|
|
658
671
|
if (basis === 'baseline' && percent > 0) {
|
|
659
672
|
techniques.push(`${Math.round(perReq / 1000)}k tokens/requirement vs your ${Math.round(baseline / 1000)}k average`);
|
|
660
673
|
}
|
|
674
|
+
if (basis === 'baseline-untied') {
|
|
675
|
+
techniques.push(`${Math.round(perReq / 1000)}k tokens/requirement vs your ${Math.round(baseline / 1000)}k average`);
|
|
676
|
+
}
|
|
661
677
|
if (!techniques.length) techniques.push(`${Math.round(raw / 1000)}k tokens this run (no cache reuse yet)`);
|
|
662
678
|
|
|
663
679
|
return { rawTokens: raw, weightedTokens: weighted, savedTokens, percent, basis, techniques: [...new Set(techniques)] };
|
|
@@ -1261,6 +1277,38 @@ export function taskStatusAfterVerify({ verdict, code, timedOut }) {
|
|
|
1261
1277
|
return code === 0 ? 'done' : 'failed';
|
|
1262
1278
|
}
|
|
1263
1279
|
|
|
1280
|
+
// task 450 (goal #783, task 448): a worker can exit 0 with changedFiles: []
|
|
1281
|
+
// and a result that reads as a thought cut off mid-sentence (e.g. "I'll wait
|
|
1282
|
+
// for this background full-suite run to complete rather than poll.") — no
|
|
1283
|
+
// work happened, but taskStatusAfterVerify (decided from `code` alone, before
|
|
1284
|
+
// changedFiles even exists) already called it 'done'. This is the same
|
|
1285
|
+
// principle the product already applies to verification (don't trust
|
|
1286
|
+
// self-report, judge by evidence) applied to task completion itself.
|
|
1287
|
+
// Deliberately narrow, per the same 0-file-legitimate-report carve-out as
|
|
1288
|
+
// isNothingVerifiable above: a real investigation/report task also has zero
|
|
1289
|
+
// changed files, but its result reads as a normal finished sentence, so only
|
|
1290
|
+
// text that also LOOKS cut off trips this.
|
|
1291
|
+
export function looksIncomplete(text) {
|
|
1292
|
+
const t = String(text ?? '').trim();
|
|
1293
|
+
if (!t) return true;
|
|
1294
|
+
if (t.length >= 200) return false;
|
|
1295
|
+
// A short blurb ending mid-word/mid-clause (no sentence-final punctuation
|
|
1296
|
+
// in any of ja/en) reads as truncated, not as a deliberate one-liner.
|
|
1297
|
+
if (!/[.!?。!?」))]$/.test(t)) return true;
|
|
1298
|
+
// goal-783/task-448's actual fragment ("I'll wait for this background
|
|
1299
|
+
// full-suite run to complete rather than poll.") IS a grammatically
|
|
1300
|
+
// complete sentence, so the check above misses it — but it's a one-shot
|
|
1301
|
+
// batch worker narrating a future intent ("I'll…"/"let me…"), never a
|
|
1302
|
+
// report of something actually done. That opener is itself the signal.
|
|
1303
|
+
return /^(i'll|i will|i'm going to|i am going to|let me)\b/i.test(t);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
export function isSuspiciouslyIncompleteDone({ status, changedFiles = [], result = '', hasVerdict = false } = {}) {
|
|
1307
|
+
if (status !== 'done' || hasVerdict) return false;
|
|
1308
|
+
if ((changedFiles ?? []).length > 0) return false;
|
|
1309
|
+
return looksIncomplete(result);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1264
1312
|
// A task counts as complete for GOAL status when it cleanly finished
|
|
1265
1313
|
// (done/skipped) OR it was force-stopped (interrupted) but its change is STRONGLY
|
|
1266
1314
|
// verified — an independent passCondition verification PASSED (proof.verified &&
|
|
@@ -2254,6 +2302,21 @@ export function isNothingVerifiable({ changedFiles = [], hasPassCondition = fals
|
|
|
2254
2302
|
return hasPassCondition && changedFiles.length === 0 && !hasAnyProof;
|
|
2255
2303
|
}
|
|
2256
2304
|
|
|
2305
|
+
// goal-751 (2026-07-26): a wantsPR goal is an explicit ask for a PR, so a worker
|
|
2306
|
+
// that ends with zero changed files and no proof did not deliver what was asked —
|
|
2307
|
+
// most often a lane-conflict report ("空き次第自動再開します") that cannot actually
|
|
2308
|
+
// happen, since a `claude -p` worker is a one-shot batch run with no way to resume
|
|
2309
|
+
// itself later. taskStatusAfterVerify still reads a clean exit (code===0, no
|
|
2310
|
+
// verdict) as 'done', so this reached Review looking exactly like a normal
|
|
2311
|
+
// completed PR. Same shape as isNothingVerifiable above but keyed on wantsPR
|
|
2312
|
+
// instead of passCondition (a wantsPR goal has no passCondition of its own — the
|
|
2313
|
+
// PR ask itself is the promise). Advisory only, like proofNote/verificationInfra
|
|
2314
|
+
// Error below: the caller sets a note, it never blocks (Failedを作らない方針, see
|
|
2315
|
+
// docs/HANDOFF-2026-07-25-verify-gate-default-off.md).
|
|
2316
|
+
export function isNothingVerifiableForPR({ wantsPR = false, changedFiles = [], hasAnyProof = false } = {}) {
|
|
2317
|
+
return Boolean(wantsPR) && changedFiles.length === 0 && !hasAnyProof;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2257
2320
|
// チェックリストUIで要件が全件Approveされたら goal を review→done へ進める。
|
|
2258
2321
|
// 以前は「PRありgoalはマージが唯一の完了トリガー」として弾いていたが、それだと
|
|
2259
2322
|
// 承認しても done が永続化されず次のrefreshで review に戻る不具合になっていた
|
|
@@ -2542,10 +2605,10 @@ export function hasGitChanges(porcelainStatus) {
|
|
|
2542
2605
|
return Boolean(String(porcelainStatus ?? '').trim());
|
|
2543
2606
|
}
|
|
2544
2607
|
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2608
|
+
// Direct workspace is retired (2026-07-26): two accidents where a stray client-side
|
|
2609
|
+
// toggle edited a shared checkout directly. This always returns isolated-worktree
|
|
2610
|
+
// regardless of what a client sends, so a stale/tampered client can't force direct.
|
|
2611
|
+
export function resolveWorkspaceMode(_input) {
|
|
2549
2612
|
return 'isolated-worktree';
|
|
2550
2613
|
}
|
|
2551
2614
|
|
|
@@ -3151,6 +3214,13 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
|
|
|
3151
3214
|
// (research, summaries, answers) simply skip this — the worker decides,
|
|
3152
3215
|
// never us.
|
|
3153
3216
|
`- 人が実際に動かして結果を見られる依頼なら、最後にプロジェクト直下へ ${RUN_DECL_FILE} を書く: {"cmd": "<起動コマンド>", "url": "<開くURL>", "note": "<何が見えるか1行>", "check": "<人に何を見て判断してほしいか1行>", "title": "<この作業のチケット名。対象が分かる程度に説明的で動作を含む短い名前。例「看板モードの追加」「ヘッダのズレの修正」。依頼文をそのまま写さない>"}。Manager はこれを使って人に「動かして見る」と「何を見ればいいか」を出す。動かして見せるものが無い依頼(調査・要約・質問への回答など)では書かなくてよい。`,
|
|
3217
|
+
// `file` (goal #744):起動不要な静的成果物(比較HTML、レポート等)専用。この
|
|
3218
|
+
// クラスの依頼は cmd/url を使う理由がなく、worker が唯一持っている手(file://
|
|
3219
|
+
// URL)は http(s) しか許さないバリデーションに毎回弾かれ、しかも弾かれた理由が
|
|
3220
|
+
// 「JSONが壊れている」という誤診断メッセージだったため、同じ file:// を書き直
|
|
3221
|
+
// して再度失敗するループが実際に2回連続で起きた(goal #744)。ここで file を
|
|
3222
|
+
// 明示し、file:// URLを書く選択肢自体を塞ぐ。
|
|
3223
|
+
`- 起動不要な静的ファイル(HTML比較ページ・レポート等、npm run devのようなコマンドを必要としないもの)を見せたい場合は、cmd/url でなく "file": "<プロジェクト内の相対パス>" を使う。例: {"file": "docs/design/options.html", "title": "3案の比較"}。file:// URLは絶対に書かない(urlはhttp(s)以外だと宣言ごと弾かれる)。`,
|
|
3154
3224
|
// Root-cause prevention (goal #733, 2026-07-26): the worker Wrote this file,
|
|
3155
3225
|
// then Edit'd it, and the edit broke the JSON mid-edit — Manager silently
|
|
3156
3226
|
// dropped the whole declaration (fixed separately: it now surfaces an honest
|
|
@@ -3186,6 +3256,22 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
|
|
|
3186
3256
|
// the request is a change-something request, and most requests are not.
|
|
3187
3257
|
agentName === 'codex' ? '' : `- 同じ画面の「変える前」と「変えた後」を撮ったなら、${RUN_DECL_FILE} に \`"compare": ["<前の画像>.png", "<後の画像>.png"]\` と書いてよい(その2枚が同じ場所の前後だと分かるのは撮った本人だけ)。人には並べて、違う部分に印を付けて見せる。3案を並べた等、前後の関係が無い画像には書かない。`,
|
|
3188
3258
|
`- 最後に${replyLang}で2〜4行:何を変えたか・どのファイルか・テスト結果・人間が確認すべき点。`,
|
|
3259
|
+
// Root-cause fix (Masa 2026-07-26, goal #727 review): a worker fixed a real bug
|
|
3260
|
+
// (the To-Do lane's Doing ring had zero animation) but its report also claimed
|
|
3261
|
+
// the project-rail dot was "static" before — it actually already had a working
|
|
3262
|
+
// pulse animation (fsrundot), just not a spin. The worker never re-read the
|
|
3263
|
+
// pre-change code to check its own diagnosis; it narrated a plausible-sounding
|
|
3264
|
+
// "before" state instead of a verified one, and nothing downstream caught it.
|
|
3265
|
+
'- 「〜は動いていなかった/静止していた」のように直す前の状態を断定する場合は、その断定をする前に修正前の該当コード(該当CSS/関数)を実際に読んで確認すること。読まずに推測で「動いていない」と書かない。読んで確認した箇所は、報告に該当コードを短く引用すること。',
|
|
3266
|
+
// goal #751 (2026-07-26): you are a one-shot batch process — once this run exits,
|
|
3267
|
+
// nothing resumes you. If something outside your control blocks you (a lane/file
|
|
3268
|
+
// lock held by another agent, a missing permission, etc.), do NOT report that you
|
|
3269
|
+
// will "resume automatically once it frees up" or similar — that is not something
|
|
3270
|
+
// you or anything in this run can do. Say plainly that you made no code changes and
|
|
3271
|
+
// why, so the goal reads as unfinished/blocked rather than as a completed PR. A
|
|
3272
|
+
// human or the CTO will re-dispatch the work (e.g. via manager_reply) once the
|
|
3273
|
+
// blocker clears.
|
|
3274
|
+
'- あなたは1回きりのバッチ実行であり、このプロセスが終了した後に自動で再開することはない。レーン/ファイルの競合など自分では解けない理由で着手できない場合、「空き次第自動で再開します」のような、この実行の外では実現できない約束を書かないこと。何をどう変更できなかったか(コード変更0件であること)を正直に書き、ゴールが完了したPRではなく未完了/ブロックとして扱われるように終えること。再開は人間かCTOが改めて指示する。',
|
|
3189
3275
|
].filter(Boolean).join('\n');
|
|
3190
3276
|
}
|
|
3191
3277
|
|
|
@@ -3203,6 +3289,37 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
|
|
|
3203
3289
|
// button that opens the wrong thing is the fabricated-proof bug all over again.
|
|
3204
3290
|
export const RUN_DECL_FILE = '.manager-run.json';
|
|
3205
3291
|
|
|
3292
|
+
// A declaration that parsed as JSON but breaks a rule (bad url scheme, an
|
|
3293
|
+
// unsafe `file` path) is NOT the same failure as unparseable JSON — the first
|
|
3294
|
+
// is a worker mistake with a specific, sayable reason; the second is a broken
|
|
3295
|
+
// file. Conflating them (goal #744) sent the worker back a message that only
|
|
3296
|
+
// describes JSON corruption ("was not usable JSON"), so a worker whose actual
|
|
3297
|
+
// mistake was a file:// url read that as "my JSON broke" and rewrote the exact
|
|
3298
|
+
// same file:// url a second time. This return shape lets the caller tell them
|
|
3299
|
+
// apart: `null` stays "nothing usable to report" (unparseable / genuinely
|
|
3300
|
+
// empty), an `{ error, message }` object carries a reason a human — or the
|
|
3301
|
+
// worker reading its own feedback — can act on.
|
|
3302
|
+
function runDeclError(error, message) {
|
|
3303
|
+
return { error, message };
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
// `file` (goal #744, this PR): a static, already-built artifact — an HTML
|
|
3307
|
+
// comparison page, a report — that needs no process and has no http(s) URL of
|
|
3308
|
+
// its own. Declared as a path relative to the project so the Manager can copy
|
|
3309
|
+
// it out of the worktree (same pattern as SHOT_DIR) and serve it from a URL
|
|
3310
|
+
// that survives after the worktree is gone. Only a syntactic check lives here
|
|
3311
|
+
// (no absolute path, no ".." segment, no backslash/drive letter, no NUL) —
|
|
3312
|
+
// server.mjs still re-resolves against the worktree root before it reads
|
|
3313
|
+
// anything, because a syntax check alone doesn't rule out a symlink.
|
|
3314
|
+
export function isSafeRunDeclFilePath(p) {
|
|
3315
|
+
if (typeof p !== 'string' || !p) return false;
|
|
3316
|
+
if (p.includes('\0')) return false;
|
|
3317
|
+
if (p.startsWith('/') || p.startsWith('\\')) return false;
|
|
3318
|
+
if (/^[a-zA-Z]:[\\/]/.test(p)) return false; // Windows drive letter
|
|
3319
|
+
const norm = p.replace(/\\/g, '/');
|
|
3320
|
+
return !norm.split('/').some((seg) => seg === '..');
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3206
3323
|
export function parseRunDeclaration(text) {
|
|
3207
3324
|
let raw;
|
|
3208
3325
|
try { raw = JSON.parse(String(text ?? '')); } catch { return null; }
|
|
@@ -3210,6 +3327,7 @@ export function parseRunDeclaration(text) {
|
|
|
3210
3327
|
const str = (v, max) => (typeof v === 'string' && v.trim() ? v.trim().slice(0, max) : null);
|
|
3211
3328
|
const cmd = str(raw.cmd, 500);
|
|
3212
3329
|
const url = str(raw.url, 500);
|
|
3330
|
+
const file = str(raw.file, 500);
|
|
3213
3331
|
// `compare` (Masa 2026-07-22): which TWO of the worker's own screenshots are
|
|
3214
3332
|
// the same view before and after. Only the agent that did the work knows that
|
|
3215
3333
|
// two images relate — a request for three design options has no "after" at
|
|
@@ -3219,9 +3337,16 @@ export function parseRunDeclaration(text) {
|
|
|
3219
3337
|
const compare = parseComparePair(raw.compare);
|
|
3220
3338
|
const title = str(raw.title, 60);
|
|
3221
3339
|
// Nothing to start, nothing to open, nothing named, and no pair to show = nothing to look at.
|
|
3222
|
-
if (!cmd && !url && !compare && !title) return null;
|
|
3223
|
-
// Only http(s): the button opens this in the user's browser.
|
|
3224
|
-
|
|
3340
|
+
if (!cmd && !url && !file && !compare && !title) return null;
|
|
3341
|
+
// Only http(s): the button opens this in the user's browser. A file:// (or
|
|
3342
|
+
// any other scheme) is a worker mistake with a specific fix — declare `file`
|
|
3343
|
+
// instead — not a JSON parse failure, so it gets its own reason.
|
|
3344
|
+
if (url && !/^https?:\/\//i.test(url)) {
|
|
3345
|
+
return runDeclError('bad-url-scheme', "url must start with http:// or https:// — a file:// path can't be opened from a remote browser session; declare an artifact file instead of a file:// url");
|
|
3346
|
+
}
|
|
3347
|
+
if (file && !isSafeRunDeclFilePath(file)) {
|
|
3348
|
+
return runDeclError('bad-file-path', 'file must be a path inside the project — no leading /, no .. segments');
|
|
3349
|
+
}
|
|
3225
3350
|
// `check` (Masa 2026-07-22): what to look at, in the working AI's own words. It used
|
|
3226
3351
|
// to come from the intake AI's plan — this layer inventing a pass condition for a
|
|
3227
3352
|
// request it only read. The AI that did the work states it here instead, in the same
|
|
@@ -3232,7 +3357,7 @@ export function parseRunDeclaration(text) {
|
|
|
3232
3357
|
// the user's own sentence, repeated back at them in the one place that is
|
|
3233
3358
|
// supposed to say what the work WAS. The agent that just did it can name it in
|
|
3234
3359
|
// one line, and it is the only party that knows what actually got done.
|
|
3235
|
-
return { cmd, url, note: str(raw.note, 300), check: str(raw.check, 300), compare, title };
|
|
3360
|
+
return { cmd, url, file, note: str(raw.note, 300), check: str(raw.check, 300), compare, title };
|
|
3236
3361
|
}
|
|
3237
3362
|
|
|
3238
3363
|
// Exactly two shot file names, in [before, after] order. Anything else is
|
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, 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, 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, REPLY_INTENTS, buildReplyIntentPrompt, parseReplyIntent, buildGoalMessage, serializeGoalMessage, parseGoalMessages, computeInternalQualityMetrics, classifyInternalQualityTaskError, resolveWorkspaceMode, goalSessionFor, setGoalAgentSession, switchGoalAgentSession } from './lib.mjs';
|
|
24
|
+
import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, classifyWorkerFailure, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, 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, REPLY_INTENTS, buildReplyIntentPrompt, parseReplyIntent, buildGoalMessage, serializeGoalMessage, parseGoalMessages, computeInternalQualityMetrics, classifyInternalQualityTaskError, resolveWorkspaceMode, goalSessionFor, setGoalAgentSession, switchGoalAgentSession } from './lib.mjs';
|
|
25
25
|
import { createSerialQueue } from './lib.mjs';
|
|
26
26
|
import { isPrClosed } from './lib.mjs';
|
|
27
27
|
import { collectProjectRules } from './lib.mjs';
|
|
@@ -2406,6 +2406,17 @@ function readRunDeclaration(task, workDir) {
|
|
|
2406
2406
|
try { raw = readFileSync(file, 'utf8'); } catch { return; }
|
|
2407
2407
|
try { unlinkSync(file); } catch { /* best effort — never fail a task over cleanup */ }
|
|
2408
2408
|
const run = parseRunDeclaration(raw);
|
|
2409
|
+
// A declaration that parsed as JSON but broke a rule (bad url scheme, unsafe
|
|
2410
|
+
// file path) is a worker mistake with a specific, sayable fix — NOT the same
|
|
2411
|
+
// failure as unparseable JSON. Goal #744: conflating the two sent the worker
|
|
2412
|
+
// "was not usable JSON" for a file:// url, so it "fixed" the JSON (which was
|
|
2413
|
+
// never broken) and wrote the exact same file:// url a second time. `run` is
|
|
2414
|
+
// `{ error, message }` here, distinct from the `null` case below.
|
|
2415
|
+
if (run && run.error) {
|
|
2416
|
+
task.runDeclError = { reason: run.error, message: run.message, snippet: String(raw ?? '').slice(0, 200) };
|
|
2417
|
+
sendAct(task, `run declaration ignored — ${run.message}`);
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2409
2420
|
if (!run) {
|
|
2410
2421
|
// The worker DID try to declare something (the file exists) but it came out
|
|
2411
2422
|
// unusable — e.g. a Write followed by an Edit that broke the JSON mid-edit
|
|
@@ -2417,17 +2428,62 @@ function readRunDeclaration(task, workDir) {
|
|
|
2417
2428
|
// actually written (raw content is deleted above and never otherwise
|
|
2418
2429
|
// recoverable) so both the activity log and the Review card can say WHY,
|
|
2419
2430
|
// instead of the human having to guess or dig through file loss.
|
|
2420
|
-
task.runDeclError = { snippet: String(raw ?? '').slice(0, 200) };
|
|
2431
|
+
task.runDeclError = { reason: 'invalid-json', snippet: String(raw ?? '').slice(0, 200) };
|
|
2421
2432
|
sendAct(task, `run declaration ignored — ${RUN_DECL_FILE} was not usable JSON: ${task.runDeclError.snippet.slice(0, 80)}`);
|
|
2422
2433
|
return;
|
|
2423
2434
|
}
|
|
2424
2435
|
task.run = { ...run, dir: workDir };
|
|
2425
|
-
|
|
2436
|
+
collectRunDeclFileArtifact(task, workDir);
|
|
2437
|
+
const how = task.run.cmd ?? task.run.url;
|
|
2426
2438
|
if (how) sendProcessAct(task, `Worker declared how to see the result: ${how}`);
|
|
2427
2439
|
if (run.title) sendProcessAct(task, `Worker named the work: ${run.title}`);
|
|
2428
2440
|
if (run.compare) sendProcessAct(task, `Worker declared a before/after pair: ${run.compare.join(' → ')}`);
|
|
2429
2441
|
}
|
|
2430
2442
|
|
|
2443
|
+
// `file` (goal #744): a static, already-built artifact that needs no process
|
|
2444
|
+
// and has no http(s) URL of its own — an HTML comparison page, a report. Same
|
|
2445
|
+
// pattern as collectWorkerShots: copy it out of the worktree (which is about
|
|
2446
|
+
// to be diffed/discarded) into logDir/uploads and give it a URL that survives.
|
|
2447
|
+
// parseRunDeclaration already checked the path is syntactically inside the
|
|
2448
|
+
// project (no leading /, no ..); this re-resolves against the worktree root
|
|
2449
|
+
// before reading anything, because a syntax check alone doesn't rule out a
|
|
2450
|
+
// symlink pointing outside it.
|
|
2451
|
+
function collectRunDeclFileArtifact(task, workDir) {
|
|
2452
|
+
const rel = task.run?.file;
|
|
2453
|
+
if (!rel) return;
|
|
2454
|
+
const root = resolve(workDir) + '/';
|
|
2455
|
+
const abs = resolve(workDir, rel);
|
|
2456
|
+
if (!(abs + '/').startsWith(root) && abs !== root.slice(0, -1)) {
|
|
2457
|
+
task.runDeclError = { reason: 'file-outside-project', message: `file "${rel}" resolves outside the project`, snippet: rel };
|
|
2458
|
+
sendAct(task, `run declaration ignored — ${task.runDeclError.message}.`);
|
|
2459
|
+
task.run = { ...task.run, file: null };
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
2462
|
+
let data;
|
|
2463
|
+
try { data = readFileSync(abs); } catch (err) {
|
|
2464
|
+
task.runDeclError = { reason: 'file-unreadable', message: `file "${rel}" could not be read: ${String(err?.code ?? err).slice(0, 80)}`, snippet: rel };
|
|
2465
|
+
sendAct(task, `run declaration's file "${rel}" could not be read: ${String(err?.code ?? err).slice(0, 80)}`);
|
|
2466
|
+
task.run = { ...task.run, file: null };
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
const outDir = join(logDir, 'uploads');
|
|
2470
|
+
mkdirSync(outDir, { recursive: true });
|
|
2471
|
+
const safe = basename(rel).replace(/[^\w.-]/g, '_').slice(-80);
|
|
2472
|
+
const outFile = `${Date.now()}-artifact-${task.id}-${safe}`;
|
|
2473
|
+
try {
|
|
2474
|
+
writeFileSync(join(outDir, outFile), data);
|
|
2475
|
+
} catch (err) {
|
|
2476
|
+
task.runDeclError = { reason: 'file-attach-failed', message: `file "${rel}" could not be attached: ${String(err?.code ?? err).slice(0, 80)}`, snippet: rel };
|
|
2477
|
+
sendAct(task, `run declaration's file "${rel}" could not be attached: ${String(err?.code ?? err).slice(0, 80)}`);
|
|
2478
|
+
task.run = { ...task.run, file: null };
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
// No process to start for a static file — the Review card renders this as a
|
|
2482
|
+
// plain link (like a PR link), not the "Start it" button.
|
|
2483
|
+
task.run = { ...task.run, url: `/upload/${outFile}`, cmd: null };
|
|
2484
|
+
sendProcessAct(task, `Worker attached a file to open: ${rel}`);
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2431
2487
|
// A direct/shared workspace can still contain a declaration left by an older
|
|
2432
2488
|
// run (for example when that run was interrupted before readRunDeclaration).
|
|
2433
2489
|
// Remove it before this task starts so only a file created by THIS worker can
|
|
@@ -2865,6 +2921,19 @@ async function runTask(task) {
|
|
|
2865
2921
|
const after = await gitChanges(workDir);
|
|
2866
2922
|
task.changedFiles = after.filter((l) => !before.includes(l)).map((l) => l.slice(3));
|
|
2867
2923
|
sendProcessAct(task, `Detected ${task.changedFiles.length} changed file${task.changedFiles.length === 1 ? '' : 's'} from this task.`);
|
|
2924
|
+
// task 450: taskStatusAfterVerify decided 'done' from `code` alone, before
|
|
2925
|
+
// changedFiles existed to check against. Don't trust a clean exit code by
|
|
2926
|
+
// itself — a worker that changed nothing and left a cut-off-looking result
|
|
2927
|
+
// (goal #783 task 448: "I'll wait for this background full-suite run to
|
|
2928
|
+
// complete rather than poll.") did not actually finish. Downgrade to
|
|
2929
|
+
// 'interrupted' (retryable, surfaces in Attention) instead of silently
|
|
2930
|
+
// shipping it as done. Never touches a real 0-file report/investigation
|
|
2931
|
+
// task, whose result reads as a normal finished sentence.
|
|
2932
|
+
if (isSuspiciouslyIncompleteDone({ status: task.status, changedFiles: task.changedFiles, result: task.result, hasVerdict: Boolean(verdict) })) {
|
|
2933
|
+
task.status = 'interrupted';
|
|
2934
|
+
task.result = `[未完了の疑い: 変更ファイル0件かつ応答が途中で切れています] ${task.result}`.slice(0, RESULT_MAX);
|
|
2935
|
+
sendProcessAct(task, 'Result looked incomplete (0 changed files, cut-off text) — marked interrupted instead of done for human review.');
|
|
2936
|
+
}
|
|
2868
2937
|
task.finishedAt = new Date().toISOString();
|
|
2869
2938
|
task.resultAttemptId = task.attemptId;
|
|
2870
2939
|
saveTask(task);
|
|
@@ -3532,6 +3601,18 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
3532
3601
|
hasPassCondition: siblings.some((t) => t.passCondition),
|
|
3533
3602
|
hasAnyProof: siblings.some((t) => t.proof),
|
|
3534
3603
|
});
|
|
3604
|
+
// goal-751: wantsPR but the worker changed nothing and proved nothing (typically a
|
|
3605
|
+
// lane-conflict report promising an automatic resume that a one-shot `claude -p`
|
|
3606
|
+
// run can never deliver). Advisory only — see isNothingVerifiableForPR in lib.mjs.
|
|
3607
|
+
const prNothingVerifiable = isNothingVerifiableForPR({
|
|
3608
|
+
wantsPR: goal.wantsPR,
|
|
3609
|
+
changedFiles,
|
|
3610
|
+
hasAnyProof: siblings.some((t) => t.proof),
|
|
3611
|
+
});
|
|
3612
|
+
goal.prNote = prNothingVerifiable
|
|
3613
|
+
? 'workerはコード変更をせずに終了しました(PRを求めていたのに0ファイル変更)。実装は行われていない可能性があります。'
|
|
3614
|
+
: null;
|
|
3615
|
+
if (goal.prNote) goalAct(`PR advisory (not blocking): ${goal.prNote}`);
|
|
3535
3616
|
if (testsFailing) {
|
|
3536
3617
|
const failure = recordGoalFailure(goal, {
|
|
3537
3618
|
kind: 'test',
|
|
@@ -3608,7 +3689,7 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
3608
3689
|
const preNote = goal.testResult?.preExistingOnly
|
|
3609
3690
|
? ` (${goal.testResult.preExisting?.length ?? 0} pre-existing test failure(s) ignored as unrelated to this change)`
|
|
3610
3691
|
: '';
|
|
3611
|
-
goalAct(`Verification gate passed; moving goal to ${goal.status}.${preNote}${goal.proofNote ? ` [proof advisory: ${goal.proofNote}]` : ''}`);
|
|
3692
|
+
goalAct(`Verification gate passed; moving goal to ${goal.status}.${preNote}${goal.proofNote ? ` [proof advisory: ${goal.proofNote}]` : ''}${goal.prNote ? ` [PR advisory: ${goal.prNote}]` : ''}`);
|
|
3612
3693
|
}
|
|
3613
3694
|
}
|
|
3614
3695
|
|
package/package.json
CHANGED