@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/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はそのままdoneへ。goalGroupStatus/goalChipのdone分岐(review以外は全部doneと
1512
- // 読む)と揃えているので、doneに寄せて問題ない。
1513
- export function approveGoal({ goalStatus }) {
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' に戻す——goalGroupStatus/goalChipは '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
- if (!['review', 'done', 'blocked', 'partial'].includes(goalStatus)) {
2568
- return { ok: false, error: 'only a finished (review/done/blocked/partial) goal can be reverted' };
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
- if (pendingCount > FREE_TIER_LIMITS.maxPendingGoals) {
2928
- return { blocked: true, reason: 'pending-goal-limit', limit: FREE_TIER_LIMITS.maxPendingGoals, value: pendingCount };
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
- const fallback = { check: '', changed: (goal.plan?.[0] ?? goal.text ?? '').replace(/\s+/g, ' ').slice(0, 80) };
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
- const m = r.result.match(/\{[\s\S]*\}/);
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: { pending: freeUsage.pendingCount, cumulative: freeUsage.cumulativeCount },
3181
- limits: { pending: FREE_TIER_LIMITS.maxPendingGoals, cumulative: FREE_TIER_LIMITS.maxCumulativeGoals },
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.36",
3
+ "version": "0.10.38",
4
4
  "type": "module",
5
5
  "description": "Galda - hand off work to Claude Code or Codex, get proof back. Runs on your existing subscription, no extra API cost.",
6
6
  "scripts": {