@galda/cli 0.10.71 → 0.10.73
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 +245 -191
- package/engine/lib.mjs +115 -9
- package/engine/server.mjs +83 -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)] };
|
|
@@ -2254,6 +2270,21 @@ export function isNothingVerifiable({ changedFiles = [], hasPassCondition = fals
|
|
|
2254
2270
|
return hasPassCondition && changedFiles.length === 0 && !hasAnyProof;
|
|
2255
2271
|
}
|
|
2256
2272
|
|
|
2273
|
+
// goal-751 (2026-07-26): a wantsPR goal is an explicit ask for a PR, so a worker
|
|
2274
|
+
// that ends with zero changed files and no proof did not deliver what was asked —
|
|
2275
|
+
// most often a lane-conflict report ("空き次第自動再開します") that cannot actually
|
|
2276
|
+
// happen, since a `claude -p` worker is a one-shot batch run with no way to resume
|
|
2277
|
+
// itself later. taskStatusAfterVerify still reads a clean exit (code===0, no
|
|
2278
|
+
// verdict) as 'done', so this reached Review looking exactly like a normal
|
|
2279
|
+
// completed PR. Same shape as isNothingVerifiable above but keyed on wantsPR
|
|
2280
|
+
// instead of passCondition (a wantsPR goal has no passCondition of its own — the
|
|
2281
|
+
// PR ask itself is the promise). Advisory only, like proofNote/verificationInfra
|
|
2282
|
+
// Error below: the caller sets a note, it never blocks (Failedを作らない方針, see
|
|
2283
|
+
// docs/HANDOFF-2026-07-25-verify-gate-default-off.md).
|
|
2284
|
+
export function isNothingVerifiableForPR({ wantsPR = false, changedFiles = [], hasAnyProof = false } = {}) {
|
|
2285
|
+
return Boolean(wantsPR) && changedFiles.length === 0 && !hasAnyProof;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2257
2288
|
// チェックリストUIで要件が全件Approveされたら goal を review→done へ進める。
|
|
2258
2289
|
// 以前は「PRありgoalはマージが唯一の完了トリガー」として弾いていたが、それだと
|
|
2259
2290
|
// 承認しても done が永続化されず次のrefreshで review に戻る不具合になっていた
|
|
@@ -2542,10 +2573,10 @@ export function hasGitChanges(porcelainStatus) {
|
|
|
2542
2573
|
return Boolean(String(porcelainStatus ?? '').trim());
|
|
2543
2574
|
}
|
|
2544
2575
|
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2576
|
+
// Direct workspace is retired (2026-07-26): two accidents where a stray client-side
|
|
2577
|
+
// toggle edited a shared checkout directly. This always returns isolated-worktree
|
|
2578
|
+
// regardless of what a client sends, so a stale/tampered client can't force direct.
|
|
2579
|
+
export function resolveWorkspaceMode(_input) {
|
|
2549
2580
|
return 'isolated-worktree';
|
|
2550
2581
|
}
|
|
2551
2582
|
|
|
@@ -3151,8 +3182,28 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
|
|
|
3151
3182
|
// (research, summaries, answers) simply skip this — the worker decides,
|
|
3152
3183
|
// never us.
|
|
3153
3184
|
`- 人が実際に動かして結果を見られる依頼なら、最後にプロジェクト直下へ ${RUN_DECL_FILE} を書く: {"cmd": "<起動コマンド>", "url": "<開くURL>", "note": "<何が見えるか1行>", "check": "<人に何を見て判断してほしいか1行>", "title": "<この作業のチケット名。対象が分かる程度に説明的で動作を含む短い名前。例「看板モードの追加」「ヘッダのズレの修正」。依頼文をそのまま写さない>"}。Manager はこれを使って人に「動かして見る」と「何を見ればいいか」を出す。動かして見せるものが無い依頼(調査・要約・質問への回答など)では書かなくてよい。`,
|
|
3185
|
+
// `file` (goal #744):起動不要な静的成果物(比較HTML、レポート等)専用。この
|
|
3186
|
+
// クラスの依頼は cmd/url を使う理由がなく、worker が唯一持っている手(file://
|
|
3187
|
+
// URL)は http(s) しか許さないバリデーションに毎回弾かれ、しかも弾かれた理由が
|
|
3188
|
+
// 「JSONが壊れている」という誤診断メッセージだったため、同じ file:// を書き直
|
|
3189
|
+
// して再度失敗するループが実際に2回連続で起きた(goal #744)。ここで file を
|
|
3190
|
+
// 明示し、file:// URLを書く選択肢自体を塞ぐ。
|
|
3191
|
+
`- 起動不要な静的ファイル(HTML比較ページ・レポート等、npm run devのようなコマンドを必要としないもの)を見せたい場合は、cmd/url でなく "file": "<プロジェクト内の相対パス>" を使う。例: {"file": "docs/design/options.html", "title": "3案の比較"}。file:// URLは絶対に書かない(urlはhttp(s)以外だと宣言ごと弾かれる)。`,
|
|
3192
|
+
// Root-cause prevention (goal #733, 2026-07-26): the worker Wrote this file,
|
|
3193
|
+
// then Edit'd it, and the edit broke the JSON mid-edit — Manager silently
|
|
3194
|
+
// dropped the whole declaration (fixed separately: it now surfaces an honest
|
|
3195
|
+
// note instead of vanishing). But the honest note is a recovery, not a fix —
|
|
3196
|
+
// the real fix is stopping the corruption before it happens. Edit requires an
|
|
3197
|
+
// exact byte-for-byte match against the file's current content; a small slip
|
|
3198
|
+
// (a stray character, a stale in-memory copy) breaks the JSON with no error
|
|
3199
|
+
// the worker would notice, because it happens in the tool call, not in
|
|
3200
|
+
// anything it reads back. Write overwrites the whole file atomically — there
|
|
3201
|
+
// is no "current content" to mismatch against — so telling the worker to
|
|
3202
|
+
// always fully overwrite rather than partially edit removes the failure mode
|
|
3203
|
+
// structurally, not just its symptom.
|
|
3204
|
+
`- ${RUN_DECL_FILE} は毎回 Write で全文を書き直すこと。Edit で一部だけ直さない(既存内容と一致しないと壊れたJSONになり、Managerに握りつぶされて人に何も表示されない)。`,
|
|
3154
3205
|
`- Markdown・設計文書・調査メモなど文章そのものが成果物なら、${RUN_DECL_FILE} に別のHTMLや過去のURLを書かない。最終回答に成果物の本文も貼り、保存先のMarkdownリンクを添えること。「ファイルに書きました」だけで終わらない。`,
|
|
3155
|
-
`- 「ローカルで見れない」というフィードバックには、ターミナル操作や npx の再実行を案内して終わらないこと。${RUN_DECL_FILE}
|
|
3206
|
+
`- 「ローカルで見れない」というフィードバックには、ターミナル操作や npx の再実行を案内して終わらないこと。${RUN_DECL_FILE} を Write で全文書き直して実際に開ける内容へ更新し、可能なら自分でURLへ接続確認してから完了すること。`,
|
|
3156
3207
|
// Links are rendered as links. The product cannot tell a real one from a
|
|
3157
3208
|
// decorative one, so the only place this can be held is here.
|
|
3158
3209
|
`- 自分が作っていない場所へのリンクを報告に書かない(存在しないURLや「詳しくはここ」のような飾りのリンクは、そのままリンクとして人に表示される)。`,
|
|
@@ -3173,6 +3224,22 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
|
|
|
3173
3224
|
// the request is a change-something request, and most requests are not.
|
|
3174
3225
|
agentName === 'codex' ? '' : `- 同じ画面の「変える前」と「変えた後」を撮ったなら、${RUN_DECL_FILE} に \`"compare": ["<前の画像>.png", "<後の画像>.png"]\` と書いてよい(その2枚が同じ場所の前後だと分かるのは撮った本人だけ)。人には並べて、違う部分に印を付けて見せる。3案を並べた等、前後の関係が無い画像には書かない。`,
|
|
3175
3226
|
`- 最後に${replyLang}で2〜4行:何を変えたか・どのファイルか・テスト結果・人間が確認すべき点。`,
|
|
3227
|
+
// Root-cause fix (Masa 2026-07-26, goal #727 review): a worker fixed a real bug
|
|
3228
|
+
// (the To-Do lane's Doing ring had zero animation) but its report also claimed
|
|
3229
|
+
// the project-rail dot was "static" before — it actually already had a working
|
|
3230
|
+
// pulse animation (fsrundot), just not a spin. The worker never re-read the
|
|
3231
|
+
// pre-change code to check its own diagnosis; it narrated a plausible-sounding
|
|
3232
|
+
// "before" state instead of a verified one, and nothing downstream caught it.
|
|
3233
|
+
'- 「〜は動いていなかった/静止していた」のように直す前の状態を断定する場合は、その断定をする前に修正前の該当コード(該当CSS/関数)を実際に読んで確認すること。読まずに推測で「動いていない」と書かない。読んで確認した箇所は、報告に該当コードを短く引用すること。',
|
|
3234
|
+
// goal #751 (2026-07-26): you are a one-shot batch process — once this run exits,
|
|
3235
|
+
// nothing resumes you. If something outside your control blocks you (a lane/file
|
|
3236
|
+
// lock held by another agent, a missing permission, etc.), do NOT report that you
|
|
3237
|
+
// will "resume automatically once it frees up" or similar — that is not something
|
|
3238
|
+
// you or anything in this run can do. Say plainly that you made no code changes and
|
|
3239
|
+
// why, so the goal reads as unfinished/blocked rather than as a completed PR. A
|
|
3240
|
+
// human or the CTO will re-dispatch the work (e.g. via manager_reply) once the
|
|
3241
|
+
// blocker clears.
|
|
3242
|
+
'- あなたは1回きりのバッチ実行であり、このプロセスが終了した後に自動で再開することはない。レーン/ファイルの競合など自分では解けない理由で着手できない場合、「空き次第自動で再開します」のような、この実行の外では実現できない約束を書かないこと。何をどう変更できなかったか(コード変更0件であること)を正直に書き、ゴールが完了したPRではなく未完了/ブロックとして扱われるように終えること。再開は人間かCTOが改めて指示する。',
|
|
3176
3243
|
].filter(Boolean).join('\n');
|
|
3177
3244
|
}
|
|
3178
3245
|
|
|
@@ -3190,6 +3257,37 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
|
|
|
3190
3257
|
// button that opens the wrong thing is the fabricated-proof bug all over again.
|
|
3191
3258
|
export const RUN_DECL_FILE = '.manager-run.json';
|
|
3192
3259
|
|
|
3260
|
+
// A declaration that parsed as JSON but breaks a rule (bad url scheme, an
|
|
3261
|
+
// unsafe `file` path) is NOT the same failure as unparseable JSON — the first
|
|
3262
|
+
// is a worker mistake with a specific, sayable reason; the second is a broken
|
|
3263
|
+
// file. Conflating them (goal #744) sent the worker back a message that only
|
|
3264
|
+
// describes JSON corruption ("was not usable JSON"), so a worker whose actual
|
|
3265
|
+
// mistake was a file:// url read that as "my JSON broke" and rewrote the exact
|
|
3266
|
+
// same file:// url a second time. This return shape lets the caller tell them
|
|
3267
|
+
// apart: `null` stays "nothing usable to report" (unparseable / genuinely
|
|
3268
|
+
// empty), an `{ error, message }` object carries a reason a human — or the
|
|
3269
|
+
// worker reading its own feedback — can act on.
|
|
3270
|
+
function runDeclError(error, message) {
|
|
3271
|
+
return { error, message };
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
// `file` (goal #744, this PR): a static, already-built artifact — an HTML
|
|
3275
|
+
// comparison page, a report — that needs no process and has no http(s) URL of
|
|
3276
|
+
// its own. Declared as a path relative to the project so the Manager can copy
|
|
3277
|
+
// it out of the worktree (same pattern as SHOT_DIR) and serve it from a URL
|
|
3278
|
+
// that survives after the worktree is gone. Only a syntactic check lives here
|
|
3279
|
+
// (no absolute path, no ".." segment, no backslash/drive letter, no NUL) —
|
|
3280
|
+
// server.mjs still re-resolves against the worktree root before it reads
|
|
3281
|
+
// anything, because a syntax check alone doesn't rule out a symlink.
|
|
3282
|
+
export function isSafeRunDeclFilePath(p) {
|
|
3283
|
+
if (typeof p !== 'string' || !p) return false;
|
|
3284
|
+
if (p.includes('\0')) return false;
|
|
3285
|
+
if (p.startsWith('/') || p.startsWith('\\')) return false;
|
|
3286
|
+
if (/^[a-zA-Z]:[\\/]/.test(p)) return false; // Windows drive letter
|
|
3287
|
+
const norm = p.replace(/\\/g, '/');
|
|
3288
|
+
return !norm.split('/').some((seg) => seg === '..');
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3193
3291
|
export function parseRunDeclaration(text) {
|
|
3194
3292
|
let raw;
|
|
3195
3293
|
try { raw = JSON.parse(String(text ?? '')); } catch { return null; }
|
|
@@ -3197,6 +3295,7 @@ export function parseRunDeclaration(text) {
|
|
|
3197
3295
|
const str = (v, max) => (typeof v === 'string' && v.trim() ? v.trim().slice(0, max) : null);
|
|
3198
3296
|
const cmd = str(raw.cmd, 500);
|
|
3199
3297
|
const url = str(raw.url, 500);
|
|
3298
|
+
const file = str(raw.file, 500);
|
|
3200
3299
|
// `compare` (Masa 2026-07-22): which TWO of the worker's own screenshots are
|
|
3201
3300
|
// the same view before and after. Only the agent that did the work knows that
|
|
3202
3301
|
// two images relate — a request for three design options has no "after" at
|
|
@@ -3206,9 +3305,16 @@ export function parseRunDeclaration(text) {
|
|
|
3206
3305
|
const compare = parseComparePair(raw.compare);
|
|
3207
3306
|
const title = str(raw.title, 60);
|
|
3208
3307
|
// Nothing to start, nothing to open, nothing named, and no pair to show = nothing to look at.
|
|
3209
|
-
if (!cmd && !url && !compare && !title) return null;
|
|
3210
|
-
// Only http(s): the button opens this in the user's browser.
|
|
3211
|
-
|
|
3308
|
+
if (!cmd && !url && !file && !compare && !title) return null;
|
|
3309
|
+
// Only http(s): the button opens this in the user's browser. A file:// (or
|
|
3310
|
+
// any other scheme) is a worker mistake with a specific fix — declare `file`
|
|
3311
|
+
// instead — not a JSON parse failure, so it gets its own reason.
|
|
3312
|
+
if (url && !/^https?:\/\//i.test(url)) {
|
|
3313
|
+
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");
|
|
3314
|
+
}
|
|
3315
|
+
if (file && !isSafeRunDeclFilePath(file)) {
|
|
3316
|
+
return runDeclError('bad-file-path', 'file must be a path inside the project — no leading /, no .. segments');
|
|
3317
|
+
}
|
|
3212
3318
|
// `check` (Masa 2026-07-22): what to look at, in the working AI's own words. It used
|
|
3213
3319
|
// to come from the intake AI's plan — this layer inventing a pass condition for a
|
|
3214
3320
|
// request it only read. The AI that did the work states it here instead, in the same
|
|
@@ -3219,7 +3325,7 @@ export function parseRunDeclaration(text) {
|
|
|
3219
3325
|
// the user's own sentence, repeated back at them in the one place that is
|
|
3220
3326
|
// supposed to say what the work WAS. The agent that just did it can name it in
|
|
3221
3327
|
// one line, and it is the only party that knows what actually got done.
|
|
3222
|
-
return { cmd, url, note: str(raw.note, 300), check: str(raw.check, 300), compare, title };
|
|
3328
|
+
return { cmd, url, file, note: str(raw.note, 300), check: str(raw.check, 300), compare, title };
|
|
3223
3329
|
}
|
|
3224
3330
|
|
|
3225
3331
|
// 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, 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,17 +2406,84 @@ 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
|
|
2422
|
+
// unusable — e.g. a Write followed by an Edit that broke the JSON mid-edit
|
|
2423
|
+
// (goal #733, 2026-07-26: Masa saw a working artifact with no "Open it" and
|
|
2424
|
+
// no explanation at all — the Review card just silently had no section,
|
|
2425
|
+
// indistinguishable from "nothing to run"). Silence was already the honest
|
|
2426
|
+
// answer for a request with nothing to run; it stops being honest the moment
|
|
2427
|
+
// a declaration was attempted and swallowed. Keep a short snippet of what was
|
|
2428
|
+
// actually written (raw content is deleted above and never otherwise
|
|
2429
|
+
// recoverable) so both the activity log and the Review card can say WHY,
|
|
2430
|
+
// instead of the human having to guess or dig through file loss.
|
|
2431
|
+
task.runDeclError = { reason: 'invalid-json', snippet: String(raw ?? '').slice(0, 200) };
|
|
2432
|
+
sendAct(task, `run declaration ignored — ${RUN_DECL_FILE} was not usable JSON: ${task.runDeclError.snippet.slice(0, 80)}`);
|
|
2411
2433
|
return;
|
|
2412
2434
|
}
|
|
2413
2435
|
task.run = { ...run, dir: workDir };
|
|
2414
|
-
|
|
2436
|
+
collectRunDeclFileArtifact(task, workDir);
|
|
2437
|
+
const how = task.run.cmd ?? task.run.url;
|
|
2415
2438
|
if (how) sendProcessAct(task, `Worker declared how to see the result: ${how}`);
|
|
2416
2439
|
if (run.title) sendProcessAct(task, `Worker named the work: ${run.title}`);
|
|
2417
2440
|
if (run.compare) sendProcessAct(task, `Worker declared a before/after pair: ${run.compare.join(' → ')}`);
|
|
2418
2441
|
}
|
|
2419
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
|
+
|
|
2420
2487
|
// A direct/shared workspace can still contain a declaration left by an older
|
|
2421
2488
|
// run (for example when that run was interrupted before readRunDeclaration).
|
|
2422
2489
|
// Remove it before this task starts so only a file created by THIS worker can
|
|
@@ -3521,6 +3588,18 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
3521
3588
|
hasPassCondition: siblings.some((t) => t.passCondition),
|
|
3522
3589
|
hasAnyProof: siblings.some((t) => t.proof),
|
|
3523
3590
|
});
|
|
3591
|
+
// goal-751: wantsPR but the worker changed nothing and proved nothing (typically a
|
|
3592
|
+
// lane-conflict report promising an automatic resume that a one-shot `claude -p`
|
|
3593
|
+
// run can never deliver). Advisory only — see isNothingVerifiableForPR in lib.mjs.
|
|
3594
|
+
const prNothingVerifiable = isNothingVerifiableForPR({
|
|
3595
|
+
wantsPR: goal.wantsPR,
|
|
3596
|
+
changedFiles,
|
|
3597
|
+
hasAnyProof: siblings.some((t) => t.proof),
|
|
3598
|
+
});
|
|
3599
|
+
goal.prNote = prNothingVerifiable
|
|
3600
|
+
? 'workerはコード変更をせずに終了しました(PRを求めていたのに0ファイル変更)。実装は行われていない可能性があります。'
|
|
3601
|
+
: null;
|
|
3602
|
+
if (goal.prNote) goalAct(`PR advisory (not blocking): ${goal.prNote}`);
|
|
3524
3603
|
if (testsFailing) {
|
|
3525
3604
|
const failure = recordGoalFailure(goal, {
|
|
3526
3605
|
kind: 'test',
|
|
@@ -3597,7 +3676,7 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
3597
3676
|
const preNote = goal.testResult?.preExistingOnly
|
|
3598
3677
|
? ` (${goal.testResult.preExisting?.length ?? 0} pre-existing test failure(s) ignored as unrelated to this change)`
|
|
3599
3678
|
: '';
|
|
3600
|
-
goalAct(`Verification gate passed; moving goal to ${goal.status}.${preNote}${goal.proofNote ? ` [proof advisory: ${goal.proofNote}]` : ''}`);
|
|
3679
|
+
goalAct(`Verification gate passed; moving goal to ${goal.status}.${preNote}${goal.proofNote ? ` [proof advisory: ${goal.proofNote}]` : ''}${goal.prNote ? ` [PR advisory: ${goal.prNote}]` : ''}`);
|
|
3601
3680
|
}
|
|
3602
3681
|
}
|
|
3603
3682
|
|
package/package.json
CHANGED