@galda/cli 0.10.36 → 0.10.38
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 +717 -107
- package/engine/lib.mjs +87 -12
- package/engine/server.mjs +61 -16
- package/package.json +1 -1
package/engine/lib.mjs
CHANGED
|
@@ -877,6 +877,42 @@ export function buildGoalPrBody({ lang, goal, goalTasks, files, owner, branch, p
|
|
|
877
877
|
].join('\n');
|
|
878
878
|
}
|
|
879
879
|
|
|
880
|
+
// summarizeForReview's prompt + reply parser, kept pure/here so engine/test can
|
|
881
|
+
// assert (a) the JSON asks for a work-title, (b) the title guidance names
|
|
882
|
+
// 修正/追加/改善/調査 so an "追加したい" request never becomes "…の修正", and (c) a
|
|
883
|
+
// failed/missing reply falls back to goal.text (no regression) — all without an
|
|
884
|
+
// LLM call. The app's saReviewTitle reads reviewSummary.title first (PR-B).
|
|
885
|
+
export function buildReviewSummaryPrompt({ lang, goalText, changed, reports }) {
|
|
886
|
+
return [
|
|
887
|
+
`あるゴールの実装が完了しました。レビューする人が一目で判断できるよう、やさしい${lang === 'ja' ? '日本語' : lang}で書いてください。`,
|
|
888
|
+
'専門用語・長い説明は禁止。小学生でも分かる短い言い方。JSONのみ出力(前後に文章を書かない):',
|
|
889
|
+
'{"title":"依頼を短い作業タイトルに言い換え(体言止め可)","check":"何を確認すれば良いか(1行)","changed":"何を変えたか(1行)"}',
|
|
890
|
+
'titleは依頼文の意図を取り違えないこと。修正/追加/改善/調査を区別する。'
|
|
891
|
+
+ '例:「Xの不具合」→「Xの修正」/「Yを追加したい」→「Yの追加」/「Zを速くしたい」→「Zの改善」/「なぜWか調べて」→「Wの調査」。',
|
|
892
|
+
'',
|
|
893
|
+
`ゴール: ${(goalText ?? '').slice(0, 300)}`,
|
|
894
|
+
`変更ファイル: ${(changed ?? []).join(', ') || '(なし)'}`,
|
|
895
|
+
`ワーカー報告:\n${(reports ?? '').slice(0, 1500)}`,
|
|
896
|
+
].join('\n');
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Parse the model's JSON into {title, check, changed}, per-field fallback when a
|
|
900
|
+
// key is missing/blank. `fallback` carries the text-derived defaults
|
|
901
|
+
// (title/changed = goal.text) so a goal whose summary call failed still shows
|
|
902
|
+
// its own request text. Never throws — a broken/absent JSON yields the fallback.
|
|
903
|
+
export function parseReviewSummary(raw, fallback = {}) {
|
|
904
|
+
const clean = (v, n) => String(v ?? '').replace(/\s+/g, ' ').slice(0, n);
|
|
905
|
+
const m = String(raw ?? '').match(/\{[\s\S]*\}/);
|
|
906
|
+
if (!m) return { ...fallback };
|
|
907
|
+
let o;
|
|
908
|
+
try { o = JSON.parse(m[0]); } catch { return { ...fallback }; }
|
|
909
|
+
return {
|
|
910
|
+
title: clean(o.title, 60) || fallback.title,
|
|
911
|
+
check: clean(o.check, 120),
|
|
912
|
+
changed: clean(o.changed, 120) || fallback.changed,
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
880
916
|
// A finished task's changed files look like a UI edit: app/ (the Manager's
|
|
881
917
|
// own UI dir, or an equivalent frontend root) plus common frontend source
|
|
882
918
|
// extensions. Pure/deterministic — no LLM — so it can gate proof capture
|
|
@@ -1139,6 +1175,7 @@ export function goalGroupStatus(goalStatus, startedAt) {
|
|
|
1139
1175
|
if (goalStatus === 'folded') return 'review'; // PRD §6.5: rendered inside the Review section, not Done
|
|
1140
1176
|
if (goalStatus === 'blocked') return 'attn'; // §4E: blocked is Attention, not Done
|
|
1141
1177
|
if (goalStatus === 'needsInput') return 'todo'; // smart intake: awaiting an answer
|
|
1178
|
+
if (goalStatus === 'rejected') return 'rejected'; // 07-20c: Reject を溜める独立欄(Done ではない)
|
|
1142
1179
|
return 'done'; // includes 'reverted' (terminal)
|
|
1143
1180
|
}
|
|
1144
1181
|
|
|
@@ -1495,6 +1532,8 @@ export function isNothingVerifiable({ changedFiles = [], hasPassCondition = fals
|
|
|
1495
1532
|
// 人がアプリ側から閉じる経路。
|
|
1496
1533
|
export function advanceReviewGoal(goal) {
|
|
1497
1534
|
if (goal.status !== 'review') return { ok: false, error: 'goal is not in review' };
|
|
1535
|
+
// 07-20c: PR ありゴールは Approve だけで done にしない(merge=採用が唯一の完了トリガー)。
|
|
1536
|
+
if (goal && goal.pr) return { ok: true, needsMerge: true, status: 'review' };
|
|
1498
1537
|
return { ok: true, status: 'done' };
|
|
1499
1538
|
}
|
|
1500
1539
|
|
|
@@ -1508,20 +1547,39 @@ export function revertReviewGoal(goal) {
|
|
|
1508
1547
|
// レビュー詳細パネル(task 70)のApprove/Dismissボタンの判定。advanceReviewGoal(task 62,
|
|
1509
1548
|
// チェックリスト全件Approveの自動進行)とは別に、人間がレビュー画面から単発で下す判断
|
|
1510
1549
|
// を扱う——PRの有無を問わず、レビュー中のgoalだけが対象。
|
|
1511
|
-
// Approve
|
|
1512
|
-
//
|
|
1513
|
-
|
|
1550
|
+
// Approve は PR の有無で分岐する(07-20c)。PR 無しは従来どおり即 done。PR ありは
|
|
1551
|
+
// Approve だけで done にせず needsMerge を返す——呼び出し側が「PR をマージして完了」
|
|
1552
|
+
// ダイアログを出して PR へ飛ばし、マージ後に syncGoalMerges が review→done を自動確定
|
|
1553
|
+
// する。これで「アプリで Approve 済みだが PR は開いたまま」のゾンビを構造的に消す
|
|
1554
|
+
// (merge=採用は人の操作、の鉄則を守る)。
|
|
1555
|
+
export function approveGoal({ goalStatus, hasPr = false }) {
|
|
1514
1556
|
if (goalStatus !== 'review') return { ok: false, error: 'goal is not in review' };
|
|
1557
|
+
if (hasPr) return { ok: true, needsMerge: true, status: 'review' };
|
|
1515
1558
|
return { ok: true, status: 'done' };
|
|
1516
1559
|
}
|
|
1517
1560
|
|
|
1518
|
-
// Dismiss(差し戻し)は 'running'
|
|
1519
|
-
// startedAt済みなら'doing'
|
|
1561
|
+
// Dismiss(差し戻し)は 'running' に戻す——チャット(/reply や /dismiss にテキスト)での
|
|
1562
|
+
// 「作り直し」依頼の受け皿。goalGroupStatus は startedAt 済みなら 'doing' に読む。
|
|
1520
1563
|
export function dismissGoal({ goalStatus }) {
|
|
1521
1564
|
if (goalStatus !== 'review') return { ok: false, error: 'goal is not in review' };
|
|
1522
1565
|
return { ok: true, status: 'running' };
|
|
1523
1566
|
}
|
|
1524
1567
|
|
|
1568
|
+
// Reject(07-20c・Dismiss とは別物)は自動再作業をせず、Rejected 欄に溜める 'rejected'
|
|
1569
|
+
// へ park する。作り直しはチャット(/reply)で依頼する。Rejected からは reopenGoal で
|
|
1570
|
+
// review へ戻せ、Delete(=revert)で PR ごと破棄できる。/dismiss(rework)とは別の
|
|
1571
|
+
// /reject エンドポイントが担うので、既存のチャット rework 契約を壊さない。
|
|
1572
|
+
export function rejectGoal({ goalStatus }) {
|
|
1573
|
+
if (goalStatus !== 'review') return { ok: false, error: 'goal is not in review' };
|
|
1574
|
+
return { ok: true, status: 'rejected' };
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// Rejected 欄の『Reopen』——rejected を review に戻す(rejectGoal の逆操作)。
|
|
1578
|
+
export function reopenGoal({ goalStatus }) {
|
|
1579
|
+
if (goalStatus !== 'rejected') return { ok: false, error: 'goal is not rejected' };
|
|
1580
|
+
return { ok: true, status: 'review' };
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1525
1583
|
// ---- MODE (Auto / Plan) ----------------------------------------------------
|
|
1526
1584
|
// A goal runs in one of two modes. 'auto' = the default implement→verify→review
|
|
1527
1585
|
// flow (worker edits files with acceptEdits). 'plan' = the worker runs under
|
|
@@ -2461,6 +2519,8 @@ export function goalLogLine(goal, prevGoal) {
|
|
|
2461
2519
|
const label = String(goal?.plan?.[0] ?? goal?.text ?? '').replace(/\s+/g, ' ').trim().slice(0, 60);
|
|
2462
2520
|
if (!prevGoal) return goal?.status === 'pending' ? `Goal shelved to Pending: "${label}"` : null;
|
|
2463
2521
|
if (prevGoal.status === 'review' && goal.status === 'done') return `Approved: "${label}"`;
|
|
2522
|
+
if (prevGoal.status === 'review' && goal.status === 'rejected') return `Rejected: "${label}"`;
|
|
2523
|
+
if (prevGoal.status === 'rejected' && goal.status === 'review') return `Reopened: "${label}"`;
|
|
2464
2524
|
if (prevGoal.status === 'review' && goal.status === 'running') return `Dismissed: "${label}" — back to work`;
|
|
2465
2525
|
if (prevGoal.status !== 'running' && goal.status === 'running' && Array.isArray(goal.plan) && goal.plan.length) {
|
|
2466
2526
|
return `Goal decomposed into ${goal.plan.length} task(s) → To do: "${label}"`;
|
|
@@ -2564,8 +2624,10 @@ export function computeRetestOutcome(results = [], total = 3) {
|
|
|
2564
2624
|
// terminal 'reverted' status (distinct from 'skipped'/archived). Decision only —
|
|
2565
2625
|
// the destructive gh/git calls live behind the server's small wrapper.
|
|
2566
2626
|
export function revertGoal({ goalStatus }) {
|
|
2567
|
-
|
|
2568
|
-
|
|
2627
|
+
// 07-20c: 'rejected' も対象に含める——Rejected 欄の Delete は revert 経由で PR を閉じ
|
|
2628
|
+
// worktree を落とす(PR を残したまま消すと孤児 PR =別のゾンビになるため)。
|
|
2629
|
+
if (!['review', 'done', 'blocked', 'partial', 'rejected'].includes(goalStatus)) {
|
|
2630
|
+
return { ok: false, error: 'only a finished (review/done/blocked/partial/rejected) goal can be reverted' };
|
|
2569
2631
|
}
|
|
2570
2632
|
return { ok: true, status: 'reverted' };
|
|
2571
2633
|
}
|
|
@@ -2903,8 +2965,11 @@ export function nextResumeDelay(attempt, { baseMs = 5 * 60 * 1000, maxMs = 60 *
|
|
|
2903
2965
|
// tested — sharing a module across that boundary would couple two things
|
|
2904
2966
|
// that fail and deploy on different schedules. Keep the two copies in sync by
|
|
2905
2967
|
// hand; both have their own test coverage against the same free-tier numbers.
|
|
2968
|
+
// maxPendingGoals (the 5 concurrent "queued to-do" cap) was REMOVED 2026-07-20
|
|
2969
|
+
// (Masa): the free tier no longer caps how many to-dos run at once, and the UI
|
|
2970
|
+
// drops the "x / 5" counter. The lifetime (20) and project (1) caps remain the
|
|
2971
|
+
// monetization wall.
|
|
2906
2972
|
export const FREE_TIER_LIMITS = Object.freeze({
|
|
2907
|
-
maxPendingGoals: 5,
|
|
2908
2973
|
maxCumulativeGoals: 20,
|
|
2909
2974
|
maxProjects: 1,
|
|
2910
2975
|
});
|
|
@@ -2921,12 +2986,10 @@ export const QUEUED_GOAL_STATUSES = Object.freeze([
|
|
|
2921
2986
|
]);
|
|
2922
2987
|
|
|
2923
2988
|
export function checkFreeTierLimit(usage) {
|
|
2924
|
-
const pendingCount = Number(usage?.pendingCount) || 0;
|
|
2925
2989
|
const cumulativeCount = Number(usage?.cumulativeCount) || 0;
|
|
2926
2990
|
const projectCount = Number(usage?.projectCount) || 0;
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
}
|
|
2991
|
+
// Concurrent "pending" to-dos are no longer capped (Masa 2026-07-20) — only
|
|
2992
|
+
// the lifetime total and the project count gate the free tier.
|
|
2930
2993
|
if (cumulativeCount > FREE_TIER_LIMITS.maxCumulativeGoals) {
|
|
2931
2994
|
return { blocked: true, reason: 'cumulative-goal-limit', limit: FREE_TIER_LIMITS.maxCumulativeGoals, value: cumulativeCount };
|
|
2932
2995
|
}
|
|
@@ -3509,3 +3572,15 @@ export function parseClaimResponse(obj) {
|
|
|
3509
3572
|
if (obj.token) return { state: 'ready', token: String(obj.token), email: obj.email != null ? String(obj.email) : undefined };
|
|
3510
3573
|
return { state: 'pending' };
|
|
3511
3574
|
}
|
|
3575
|
+
|
|
3576
|
+
// --- connection status line (queue tray) -------------------------------------
|
|
3577
|
+
// Pure source of truth for the offline/reconnecting line that renders as the top
|
|
3578
|
+
// row of the queued tray above the composer. app/index.html is a browser static
|
|
3579
|
+
// file that can't import this module, so it MIRRORS these two tiny functions
|
|
3580
|
+
// inline (see renderQueue()); keep the logic identical on both sides.
|
|
3581
|
+
export function connectionBannerState({ online }) {
|
|
3582
|
+
return online ? { kind: 'ok' } : { kind: 'offline', reconnect: true };
|
|
3583
|
+
}
|
|
3584
|
+
export function unsentMetaLabel({ online }) {
|
|
3585
|
+
return online ? 'Unsent — tap to resend' : 'Waiting for connection';
|
|
3586
|
+
}
|
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, buildFolderQuestion, resolveFolderAnswer, classifyFolderTarget, buildFolderInitConfirm, isFolderInitConfirmed, needsReviewPreference, buildReviewPreferenceQuestion, resolveReviewPreferenceAnswer, routeQuestionAnswer, notUnderstoodNote, parseRelocalizedQuestion, classifyAuthProbe, authBannerText, makeSigninChallenge, parseClaimResponse } from './lib.mjs';
|
|
24
|
-
import { parseStreamEvents, parsePlan, refinePlanTasks, findOverlappingGoal, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildGoalProofMd, buildGoalPrBody, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, permissionModeFor, isPlanReview, nextAfterPlanApprove, GOAL_MODES, parseSkillFrontmatter, collectSkills, retestGoal, cancelRetestGoal, computeRetestOutcome, revertGoal, planRevertActions, archiveGoal, unarchiveGoal, undismissGoal, parseNumstat, truncateDiffText, sumUsage, resolveEntryUrl, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, isUiChange, shouldCaptureProof, shouldCaptureBaseline, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, classifyVerifyGate, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, QUEUED_GOAL_STATUSES, verifyLicenseToken, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, isNothingVerifiable, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot } from './lib.mjs';
|
|
24
|
+
import { parseStreamEvents, parsePlan, refinePlanTasks, findOverlappingGoal, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, 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, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, isUiChange, shouldCaptureProof, shouldCaptureBaseline, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, classifyVerifyGate, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, QUEUED_GOAL_STATUSES, verifyLicenseToken, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, isNothingVerifiable, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot } from './lib.mjs';
|
|
25
25
|
import { openPR } from './pr.mjs';
|
|
26
26
|
import { runVerification, exerciseUi } from './verify.mjs';
|
|
27
27
|
|
|
@@ -2604,21 +2604,19 @@ async function summarizeForReview(goal, siblings, dir, reviewDefinition) {
|
|
|
2604
2604
|
const lang = reviewDefinition.language === 'auto' ? detectRequestLanguage(goal.text) : (reviewDefinition.language || 'ja');
|
|
2605
2605
|
const changed = [...new Set(siblings.flatMap((t) => t.changedFiles ?? []))].slice(0, 20);
|
|
2606
2606
|
const reports = siblings.filter((t) => !t.reply).map((t) => `- ${t.title}: ${(t.result ?? '').replace(/\s+/g, ' ').slice(0, 300)}`).join('\n');
|
|
2607
|
-
|
|
2607
|
+
// fallback = current behavior: title/changed derived from the goal's own text,
|
|
2608
|
+
// so a goal with no summary (AI failed or nothing to summarize) still shows a
|
|
2609
|
+
// meaningful title/changed and never a blank or mis-phrased "…の修正".
|
|
2610
|
+
const fallback = {
|
|
2611
|
+
title: (goal.text ?? '').replace(/\s+/g, ' ').slice(0, 60),
|
|
2612
|
+
check: '',
|
|
2613
|
+
changed: (goal.plan?.[0] ?? goal.text ?? '').replace(/\s+/g, ' ').slice(0, 80),
|
|
2614
|
+
};
|
|
2608
2615
|
if (!changed.length && !reports) return fallback;
|
|
2609
|
-
const prompt =
|
|
2610
|
-
`あるゴールの実装が完了しました。レビューする人が一目で判断できるよう、やさしい${lang === 'ja' ? '日本語' : lang}で2行だけ書いてください。`,
|
|
2611
|
-
'専門用語・長い説明は禁止。小学生でも分かる短い言い方。JSONのみ出力(前後に文章を書かない):',
|
|
2612
|
-
'{"check":"何を確認すれば良いか(1行)","changed":"何を変えたか(1行)"}',
|
|
2613
|
-
'',
|
|
2614
|
-
`ゴール: ${(goal.text ?? '').slice(0, 300)}`,
|
|
2615
|
-
`変更ファイル: ${changed.join(', ') || '(なし)'}`,
|
|
2616
|
-
`ワーカー報告:\n${reports.slice(0, 1500)}`,
|
|
2617
|
-
].join('\n');
|
|
2616
|
+
const prompt = buildReviewSummaryPrompt({ lang, goalText: goal.text, changed, reports });
|
|
2618
2617
|
try {
|
|
2619
2618
|
const r = await runUtility({ prompt, cwd: dir, tools: 'Read' });
|
|
2620
|
-
|
|
2621
|
-
if (m) { const o = JSON.parse(m[0]); return { check: String(o.check ?? '').replace(/\s+/g, ' ').slice(0, 120), changed: String(o.changed ?? '').replace(/\s+/g, ' ').slice(0, 120) || fallback.changed }; }
|
|
2619
|
+
return parseReviewSummary(r.result, fallback);
|
|
2622
2620
|
} catch { /* fall through */ }
|
|
2623
2621
|
return fallback;
|
|
2624
2622
|
}
|
|
@@ -3177,8 +3175,8 @@ const server = createServer(async (req, res) => {
|
|
|
3177
3175
|
email: licenseState.email,
|
|
3178
3176
|
isPaying: Boolean(licenseState.isPaying),
|
|
3179
3177
|
plan: licenseState.isPaying ? 'paid' : 'free',
|
|
3180
|
-
usage: {
|
|
3181
|
-
limits: {
|
|
3178
|
+
usage: { cumulative: freeUsage.cumulativeCount },
|
|
3179
|
+
limits: { cumulative: FREE_TIER_LIMITS.maxCumulativeGoals },
|
|
3182
3180
|
price: await getBillingPrice(),
|
|
3183
3181
|
siteUrl: BILLING_API_URL || null,
|
|
3184
3182
|
};
|
|
@@ -3786,6 +3784,9 @@ const server = createServer(async (req, res) => {
|
|
|
3786
3784
|
if (!requireGoalOwnership(req, res, goal)) return;
|
|
3787
3785
|
const result = advanceReviewGoal(goal);
|
|
3788
3786
|
if (!result.ok) return json(res, 409, { error: result.error });
|
|
3787
|
+
// 07-20c: PR ありは done にせず「PR をマージして完了」を促す。status は review の
|
|
3788
|
+
// まま。マージ後に syncGoalMerges が done を確定する。
|
|
3789
|
+
if (result.needsMerge) return json(res, 200, { goal, needsMerge: true, pr: goal.pr });
|
|
3789
3790
|
goal.status = result.status;
|
|
3790
3791
|
saveGoal(goal);
|
|
3791
3792
|
return json(res, 200, goal);
|
|
@@ -4047,8 +4048,12 @@ const server = createServer(async (req, res) => {
|
|
|
4047
4048
|
pump(goal.projectId);
|
|
4048
4049
|
return json(res, 200, goal);
|
|
4049
4050
|
}
|
|
4050
|
-
const result = approveGoal({ goalStatus: goal.status });
|
|
4051
|
+
const result = approveGoal({ goalStatus: goal.status, hasPr: !!goal.pr });
|
|
4051
4052
|
if (!result.ok) return json(res, 409, { error: result.error });
|
|
4053
|
+
// 07-20c: PR ありは Approve だけで done にしない。status は review のまま返し、
|
|
4054
|
+
// クライアントは「PR をマージして完了してください」ダイアログ+ Open PR を出す。
|
|
4055
|
+
// マージされたら syncGoalMerges が review→done を自動確定する。
|
|
4056
|
+
if (result.needsMerge) return json(res, 200, { goal, needsMerge: true, pr: goal.pr });
|
|
4052
4057
|
goal.status = result.status;
|
|
4053
4058
|
saveGoal(goal);
|
|
4054
4059
|
return json(res, 200, goal);
|
|
@@ -4228,6 +4233,46 @@ const server = createServer(async (req, res) => {
|
|
|
4228
4233
|
return;
|
|
4229
4234
|
}
|
|
4230
4235
|
|
|
4236
|
+
// 07-20c: Reject — park a review goal in the Rejected column. No auto-rework
|
|
4237
|
+
// (rework is asked for via chat /reply or /dismiss). Kept as a SEPARATE
|
|
4238
|
+
// endpoint from /dismiss so the existing chat-rework contract is untouched.
|
|
4239
|
+
// An optional text is kept as a rejection note; no worker is spawned.
|
|
4240
|
+
const rejectMatch = url.pathname.match(/^\/api\/goals\/(\d+)\/reject$/);
|
|
4241
|
+
if (rejectMatch && req.method === 'POST') {
|
|
4242
|
+
const goal = goals.find((g) => g.id === Number(rejectMatch[1]));
|
|
4243
|
+
if (!goal) return json(res, 404, { error: 'goal not found' });
|
|
4244
|
+
if (!requireGoalOwnership(req, res, goal)) return;
|
|
4245
|
+
const result = rejectGoal({ goalStatus: goal.status });
|
|
4246
|
+
if (!result.ok) return json(res, 409, { error: result.error });
|
|
4247
|
+
let body = '';
|
|
4248
|
+
req.on('data', (d) => { body += d; });
|
|
4249
|
+
req.on('end', () => {
|
|
4250
|
+
let text = '';
|
|
4251
|
+
try { text = String(JSON.parse(body || '{}').text ?? '').trim(); } catch {}
|
|
4252
|
+
goal.status = result.status;
|
|
4253
|
+
if (text) goal.rejectNote = text;
|
|
4254
|
+
goal.rejectedAt = new Date().toISOString();
|
|
4255
|
+
saveGoal(goal);
|
|
4256
|
+
json(res, 200, { goal });
|
|
4257
|
+
});
|
|
4258
|
+
return;
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4261
|
+
// 07-20c: Rejected 欄の『Reopen』——rejected を review へ戻す(Reject の取り消し)。
|
|
4262
|
+
// Delete(破棄)は既存の /revert(PR を閉じ worktree を落とす)を使う。
|
|
4263
|
+
const reopenMatch = url.pathname.match(/^\/api\/goals\/(\d+)\/reopen$/);
|
|
4264
|
+
if (reopenMatch && req.method === 'POST') {
|
|
4265
|
+
const goal = goals.find((g) => g.id === Number(reopenMatch[1]));
|
|
4266
|
+
if (!goal) return json(res, 404, { error: 'goal not found' });
|
|
4267
|
+
if (!requireGoalOwnership(req, res, goal)) return;
|
|
4268
|
+
const result = reopenGoal({ goalStatus: goal.status });
|
|
4269
|
+
if (!result.ok) return json(res, 409, { error: result.error });
|
|
4270
|
+
goal.status = result.status;
|
|
4271
|
+
goal.rejectNote = null; goal.rejectedAt = null;
|
|
4272
|
+
saveGoal(goal);
|
|
4273
|
+
return json(res, 200, goal);
|
|
4274
|
+
}
|
|
4275
|
+
|
|
4231
4276
|
// image attachments: upload (base64 JSON) + serve back for thumbnails
|
|
4232
4277
|
if (url.pathname === '/api/upload' && req.method === 'POST') {
|
|
4233
4278
|
let body = '';
|
package/package.json
CHANGED