@galda/cli 0.10.84 → 0.10.86

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/theme.css CHANGED
@@ -21,9 +21,9 @@
21
21
  --tint:20,23,31;
22
22
  --hair:rgba(var(--tint),.10); --hair2:rgba(var(--tint),.16);
23
23
  --glass:rgb(255 255 255 / .72);
24
- --green-rgb:10,154,106; --blue-rgb:56,116,214; --amber-rgb:170,128,40; --danger-rgb:200,44,44;
24
+ --green-rgb:10,154,106; --blue-rgb:56,116,214; --review-rgb:92,196,255; --amber-rgb:170,128,40; --danger-rgb:200,44,44;
25
25
  --purple-rgb:150,90,220; --glow-rgb:120,150,230;
26
- --green:rgb(var(--green-rgb)); --amber:rgb(var(--amber-rgb)); --blue:rgb(var(--blue-rgb)); --danger:rgb(var(--danger-rgb));
26
+ --green:rgb(var(--green-rgb)); --amber:rgb(var(--amber-rgb)); --blue:rgb(var(--blue-rgb)); --review:rgb(var(--review-rgb)); --danger:rgb(var(--danger-rgb));
27
27
  --iri:linear-gradient(112deg,#b794f6 0%,#7cc4ff 38%,#5ce0c0 72%,#ffd27a 100%);
28
28
  --on-accent:#0a0a0b; --invink:#ffffff;
29
29
  --send-bg:#1d1d1f; --send-ink:#ffffff;
@@ -44,7 +44,7 @@ body[data-theme="banff"]{
44
44
  --tint:255,255,255;
45
45
  --hair:rgba(var(--tint),.09); --hair2:rgba(var(--tint),.16);
46
46
  --glass:rgba(12,13,17,.6);
47
- --green-rgb:79,215,156; --blue-rgb:124,196,255; --amber-rgb:228,198,120; --danger-rgb:240,115,111;
47
+ --green-rgb:79,215,156; --blue-rgb:3,155,229; --amber-rgb:228,198,120; --danger-rgb:240,115,111;
48
48
  --purple-rgb:183,148,246; --glow-rgb:146,180,255;
49
49
  --green:rgb(var(--green-rgb)); --amber:rgb(var(--amber-rgb)); --blue:rgb(var(--blue-rgb)); --danger:rgb(var(--danger-rgb));
50
50
  --on-accent:#0a0a0b; --invink:#0a0a0b;
@@ -62,7 +62,7 @@ body[data-theme="dark"],body[data-theme="midnight"],body[data-theme="cinema"]{
62
62
  --tint:255,255,255;
63
63
  --hair:rgba(var(--tint),.07); --hair2:rgba(var(--tint),.12);
64
64
  --glass:rgba(22,22,27,.55);
65
- --green-rgb:52,211,153; --blue-rgb:124,196,255; --amber-rgb:228,198,120; --danger-rgb:255,120,120;
65
+ --green-rgb:52,211,153; --blue-rgb:3,155,229; --amber-rgb:228,198,120; --danger-rgb:255,120,120;
66
66
  --purple-rgb:183,148,246; --glow-rgb:146,180,255;
67
67
  --on-accent:#0a0a0b; --invink:#0a0a0b;
68
68
  --send-bg:var(--iri); --send-ink:#0a0a0b;
@@ -92,7 +92,7 @@ body[data-theme="glass"]{
92
92
  --tint:255,255,255;
93
93
  --hair:rgba(255,255,255,.16); --hair2:rgba(255,255,255,.24);
94
94
  --glass:rgba(12,13,17,.6);
95
- --green-rgb:102,230,198; --blue-rgb:143,208,255; --amber-rgb:232,206,134; --danger-rgb:255,138,134;
95
+ --green-rgb:102,230,198; --blue-rgb:3,155,229; --amber-rgb:232,206,134; --danger-rgb:255,138,134;
96
96
  --purple-rgb:183,148,246; --glow-rgb:146,180,255;
97
97
  --green:rgb(var(--green-rgb)); --amber:rgb(var(--amber-rgb)); --blue:rgb(var(--blue-rgb)); --danger:rgb(var(--danger-rgb));
98
98
  --on-accent:#0a0a0b; --invink:#0a0a0b;
@@ -117,7 +117,7 @@ body[data-theme="gradient"]{
117
117
  --tint:20,23,31;
118
118
  --hair:rgba(var(--tint),.10); --hair2:rgba(var(--tint),.16);
119
119
  --glass:rgb(255 255 255 / .72);
120
- --green-rgb:79,215,156; --blue-rgb:37,99,235; --amber-rgb:178,140,40; --danger-rgb:240,115,111;
120
+ --green-rgb:79,215,156; --blue-rgb:3,155,229; --amber-rgb:178,140,40; --danger-rgb:240,115,111;
121
121
  --purple-rgb:150,90,220; --glow-rgb:120,150,230;
122
122
  --green:rgb(var(--green-rgb)); --amber:rgb(var(--amber-rgb)); --blue:rgb(var(--blue-rgb)); --danger:rgb(var(--danger-rgb));
123
123
  --on-accent:#0a0a0b; --invink:#ffffff;
package/engine/lib.mjs CHANGED
@@ -215,7 +215,7 @@ export const INTERNAL_QUALITY_EVENT_DEFINITIONS = Object.freeze([
215
215
 
216
216
  const INTERNAL_TERMINAL_TASK_STATUSES = new Set(['done', 'failed', 'interrupted', 'skipped']);
217
217
  const INTERNAL_FAILURE_GOAL_STATUSES = new Set(['failed', 'blocked', 'interrupted', 'partial']);
218
- const INTERNAL_OPEN_GOAL_STATUSES = new Set(['stacked', 'planning', 'running', 'needsInput', 'blocked', 'partial', 'retesting']);
218
+ const INTERNAL_OPEN_GOAL_STATUSES = new Set(['stacked', 'planning', 'running', 'needsInput', 'needsApproval', 'blocked', 'partial', 'retesting']);
219
219
 
220
220
  function timeMs(value) {
221
221
  if (!value) return null;
@@ -1151,6 +1151,34 @@ export function parseGoalMessages(raw) {
1151
1151
  }).filter(Boolean);
1152
1152
  }
1153
1153
 
1154
+ // A Review describes one implementation attempt. Preserve the user's words
1155
+ // and measured worker output together, without deciding whether the request
1156
+ // was satisfied — that judgment remains with the reviewer.
1157
+ export function buildReviewAttemptLedger({ goal = null, tasks = [] } = {}) {
1158
+ const time = (task) => Date.parse(task?.finishedAt || task?.startedAt || task?.createdAt || 0) || 0;
1159
+ const own = (tasks ?? []).filter((task) => task?.goalId === goal?.id);
1160
+ const settled = own.filter((task) => ['done', 'failed', 'interrupted', 'skipped'].includes(task?.status));
1161
+ const outcome = [...settled].sort((a, b) => time(b) - time(a))[0] ?? null;
1162
+ const requirements = [
1163
+ goal?.text ? { id: `goal-${goal.id}-initial`, kind: 'initial', text: String(goal.text), at: goal.createdAt ?? null } : null,
1164
+ ...own.filter((task) => task?.reply && task?.detail).map((task) => ({
1165
+ id: task.attemptId || `task-${task.id}`,
1166
+ kind: 'reply', text: String(task.detail), at: task.createdAt ?? null,
1167
+ })),
1168
+ ].filter(Boolean);
1169
+ return {
1170
+ attemptId: outcome?.attemptId || (outcome?.id != null ? `task-${outcome.id}` : null),
1171
+ taskId: outcome?.id ?? null,
1172
+ completedAt: outcome?.finishedAt ?? null,
1173
+ requirements,
1174
+ activeRequirement: requirements.at(-1) ?? null,
1175
+ result: String(outcome?.result ?? '').slice(0, 8000),
1176
+ changedFiles: [...new Set((outcome?.changedFiles ?? []).filter(Boolean))],
1177
+ verification: { proof: outcome?.proof ?? null, tests: goal?.testResult ?? null },
1178
+ run: outcome?.run ?? null,
1179
+ };
1180
+ }
1181
+
1154
1182
  // ---- which goal states allow EDIT / DELETE from the board ------------------
1155
1183
  // Masa (dogfood): 「急に入ったやつ(ゴール)が編集したり削除したりできない。
1156
1184
  // できるようにして」。Expanded beyond the original stacked/pending.
@@ -1176,7 +1204,7 @@ export function canEditGoal(status, startedAt) {
1176
1204
  // and dropping its queued tasks (server side). A goal already in review or
1177
1205
  // finished (done) is NOT deleted here — that's the Ledger's Archive/Revert
1178
1206
  // territory, and 'done' keeps its 409 as before.
1179
- export const DELETABLE_GOAL_STATUSES = ['stacked', 'sending', 'pending', 'planning', 'needsInput', 'running', 'partial', 'failed', 'interrupted', 'blocked'];
1207
+ export const DELETABLE_GOAL_STATUSES = ['stacked', 'sending', 'pending', 'planning', 'needsInput', 'needsApproval', 'running', 'partial', 'failed', 'interrupted', 'blocked'];
1180
1208
  export function canDeleteGoal(status) {
1181
1209
  return DELETABLE_GOAL_STATUSES.includes(status);
1182
1210
  }
@@ -1909,6 +1937,7 @@ export function goalGroupStatus(goalStatus, startedAt) {
1909
1937
  if (goalStatus === 'folded') return 'review'; // PRD §6.5: rendered inside the Review section, not Done
1910
1938
  if (goalStatus === 'blocked') return 'attn'; // §4E: blocked is Attention, not Done
1911
1939
  if (goalStatus === 'needsInput') return 'todo'; // smart intake: awaiting an answer
1940
+ if (goalStatus === 'needsApproval') return 'needsYou'; // paused for a human go/no-go, never Review
1912
1941
  if (goalStatus === 'rejected') return 'rejected'; // 07-20c: Reject を溜める独立欄(Done ではない)
1913
1942
  return 'done'; // includes 'reverted' (terminal)
1914
1943
  }
@@ -1957,9 +1986,32 @@ export function goalChip(goalStatus, columns, startedAt) {
1957
1986
  if (goalStatus === 'folded') return ['FOLDED', '']; // PRD §6.5: settled, not failed — no red/badge class, matches 'done'/'reverted'
1958
1987
  if (goalStatus === 'blocked') return ['BLOCKED', 'blocked']; // §4E: distinct state (reason + reverify), not FAIL/Done
1959
1988
  if (goalStatus === 'needsInput') return ['確認待ち', 'blocked']; // smart intake: awaiting an answer
1989
+ if (goalStatus === 'needsApproval') return ['NEEDS YOU', 'blocked'];
1960
1990
  return ['FAIL', ''];
1961
1991
  }
1962
1992
 
1993
+ // Workers can pause without pretending their unfinished work is reviewable.
1994
+ // The marker is deliberately explicit and machine-readable; ordinary prose
1995
+ // containing words such as "confirm" must never stop a goal by accident.
1996
+ // Invalid/missing fields fail open as a normal worker result.
1997
+ export function parseApprovalRequest(text) {
1998
+ const source = String(text ?? '');
1999
+ const match = source.match(/<manager-approval>\s*([\s\S]*?)\s*<\/manager-approval>/i);
2000
+ if (!match) return null;
2001
+ try {
2002
+ const raw = JSON.parse(match[1]);
2003
+ const question = String(raw.question ?? '').trim().slice(0, 500);
2004
+ const action = String(raw.action ?? '').trim().slice(0, 120);
2005
+ if (!question || !action) return null;
2006
+ const scope = Array.isArray(raw.scope)
2007
+ ? raw.scope.map((x) => String(x).trim().slice(0, 300)).filter(Boolean).slice(0, 20)
2008
+ : [];
2009
+ const resumeInstruction = String(raw.resumeInstruction ?? '').trim().slice(0, 1000)
2010
+ || `承認されたため「${action}」を実行し、元のタスクを最後まで完了してください。`;
2011
+ return { type: 'approval_required', action, question, scope, resumeInstruction };
2012
+ } catch { return null; }
2013
+ }
2014
+
1963
2015
  // Group a flat task list under their parent goal for the Tasks sidebar, so
1964
2016
  // a goal like "この体験を実現したい" shows as one collapsible unit instead of
1965
2017
  // its tasks scattered flat in the list. Each group carries a done/total
@@ -2038,6 +2090,9 @@ export function buildReviewDigest({ goals, tasks } = {}) {
2038
2090
  testResult: g.testResult ?? null,
2039
2091
  verificationInfraError: g.verificationInfraError ?? null,
2040
2092
  proof: goalTasks.find((t) => t.proof)?.proof ?? null,
2093
+ // Worker-declared partial/not-addressed requirements are not a failed
2094
+ // verdict. Count them so a human does not miss them in the compact list.
2095
+ incompleteRequirementCount: goalTasks.reduce((n, t) => n + (t.requirementEvidence?.requirements ?? []).filter((r) => ['partial', 'not-addressed'].includes(r.status)).length, 0),
2041
2096
  plan,
2042
2097
  planText: plan ? (g.planText ?? '') : null,
2043
2098
  };
@@ -2050,6 +2105,7 @@ export function buildReviewDigest({ goals, tasks } = {}) {
2050
2105
  if (existing.proof == null) existing.proof = row.proof;
2051
2106
  if (existing.testResult == null) existing.testResult = row.testResult;
2052
2107
  if (existing.verificationInfraError == null) existing.verificationInfraError = row.verificationInfraError;
2108
+ existing.incompleteRequirementCount += row.incompleteRequirementCount;
2053
2109
  continue;
2054
2110
  }
2055
2111
  if (row.pr) byPr.set(row.pr, row);
@@ -2807,6 +2863,13 @@ export function buildEphemeralSeedLines(at = '2026-01-01T00:00:00.000Z') {
2807
2863
  ...reqTasks,
2808
2864
  { kind: 'goal', id: 16, projectId: 'default', text: '[verify fixture] 失敗したTo Do行', status: 'partial', wantsPR: false, model: 'sonnet', images: [], plan: ['失敗行のアクション確認'], pr: undefined, createdAt: at },
2809
2865
  { kind: 'task', id: 17, num: 9, goalId: 16, projectId: 'default', title: '失敗行のアクション確認', detail: '', passCondition: null, model: 'sonnet', status: 'failed', createdAt: at, result: 'テスト失敗 4 件', changedFiles: ['app/index.html'], secs: 1, proof: null, usage: null, finishedAt: at },
2866
+ // タスク461: 「レビューの青丸を確認できるようなデータ」— 'default' プロジェクトは
2867
+ // MANAGER_SEED_RUNNING_FIXTURE の実行中ゴールも持つため、レールの丸は常にスピン
2868
+ // (running優先)になり、青いレビュー丸を単体では確認できない。'default-2' には
2869
+ // running なゴールが無いので、ここにレビュー待ちゴールを置けば、そのプロジェクトの
2870
+ // レール丸は spin せず素直にReview Bになる。
2871
+ { kind: 'goal', id: 18, projectId: 'default-2', text: '[verify fixture] レビュー待ちゴール(running無し・レール青丸確認用)', status: 'review', wantsPR: true, model: 'sonnet', images: [], plan: ['fixture task'], pr: 'https://github.com/example/verify-fixture/pull/3', createdAt: at },
2872
+ { kind: 'task', id: 19, num: 1, goalId: 18, projectId: 'default-2', title: 'fixture task', detail: '', passCondition: '', model: 'sonnet', status: 'done', createdAt: at, result: 'ok', changedFiles: ['fixture.txt'], secs: 1, proof: { pass: true, detail: 'ok', gif: null, png: null, mp4: null }, usage: null, finishedAt: at },
2810
2873
  ];
2811
2874
  }
2812
2875
 
@@ -3129,9 +3192,14 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
3129
3192
  // Put it next to the original request and make the precedence explicit:
3130
3193
  // the interface transports the user's words, it does not reinterpret them.
3131
3194
  task?.reply && task?.detail ? `CURRENT USER FOLLOW-UP (verbatim — these are the acceptance conditions for THIS re-review):\n${task.detail}` : '',
3195
+ task?.reply && task?.userRequirementHistory?.length ? `EARLIER USER REQUIREMENTS (verbatim task records; Manager-internal retry messages are excluded):\n${task.userRequirementHistory.map((r, i) => `${i + 1}. [${r.status}] ${r.text}`).join('\n')}` : '',
3196
+ // Falls back to the unfiltered attempt log only when the user-authored list
3197
+ // is absent, so the worker never sees the same items twice.
3198
+ task?.reply && !task?.userRequirementHistory?.length && task?.requirementHistory?.length ? `REQUIREMENT HISTORY (facts from earlier attempts; do not silently drop an item):\n${task.requirementHistory.map((r, i) => `${i + 1}. [${r.status}] ${r.text}`).join('\n')}` : '',
3132
3199
  runContext ? (typeof runContext === 'string' ? runContext : buildWorkerContextBlock(runContext)) : '',
3133
3200
  '',
3134
3201
  `TASK ${task.num}: ${task.title}`,
3202
+ `REQUIREMENT ID FOR THIS RUN: task-${task.id ?? task.num}.`,
3135
3203
  task.detail ? `DETAIL: ${task.detail}` : '',
3136
3204
  `WORKSPACE MODE: ${workspaceMode}.`,
3137
3205
  goal?.images?.length ? `ATTACHED IMAGES (absolute paths, use the Read tool to view them): ${goal.images.join(' ')}` : '',
@@ -3153,7 +3221,9 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
3153
3221
  // instead of expanding scope"): the request, not our fear of scope, sets
3154
3222
  // the size of the work.
3155
3223
  '- Deliver what the request actually asks for. If it asks for multiple options, N variations, a visual, an artifact, or a full exploration, produce ALL of them — do not reduce it to one, and do not cut it short to finish faster.',
3224
+ '- At the end of your final report, add exactly one line: MANAGER_REQUIREMENT_EVIDENCE: {"requirements":[{"id":"task-<this run id>","status":"addressed|partial|not-addressed","evidence":"file, test, or Open it evidence","files":["changed/path"]}]}. This labels evidence for Review; it is not self-approval. Report partial or not-addressed honestly.',
3156
3225
  '- For a review follow-up, implement and report against CURRENT USER FOLLOW-UP above. Do not claim the first attempt as this attempt\'s result. Do not change shared theme tokens, seed/demo data, or unrelated indicators merely to make a manual check easier unless the user explicitly requested those changes; put test fixtures in test files only.',
3226
+ '- Requirement history is context, not a Manager priority decision. Keep prior items visible; if an item remains unaddressed, say so plainly rather than omitting it.',
3157
3227
  '- デザイン案・UI案・レイアウト案を求められた場合は、文章だけで完了にしない。画像を使わない案でも、比較して確認できるHTML等のartifactをプロジェクト内に作り、実際に開ける .manager-run.json を残す。',
3158
3228
  '- Keep the work bounded to the request: inspect only files needed; avoid broad repository scans unless they are necessary.',
3159
3229
  "- You may use the user's installed skills (Skill tool, e.g. /galda-doc) when one clearly fits the request.",
@@ -3167,6 +3237,9 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
3167
3237
  // stop re-checking; but the deliverables above must still be produced in full.
3168
3238
  '- 動くと確認できたら十分(無駄な再検証でログを漁り続けない)。ただし依頼された成果物は削らず出し切ること。',
3169
3239
  '- Do not commit (git is handled by the Manager).',
3240
+ '- 原則は合理的な仮定で最後まで進める。どうしても人の明示許可なしに実行できない操作が残った場合だけ、完成物のReviewとして提出せず、最終出力に次の機械可読マーカーを1個含めて停止する:',
3241
+ '<manager-approval>{"action":"短い操作ID","question":"人に確認する1問","scope":["対象"],"resumeInstruction":"承認後に同じセッションで行う具体的な続き"}</manager-approval>',
3242
+ '- 上のマーカーは、作業完了後の見た目確認・通常のレビュー・自分で安全に仮定できる質問には使わない。Manager が承認後に同じゴールを再開する。',
3170
3243
  // How the human actually looks at the result. We never guess the run
3171
3244
  // command ourselves — guessing means branching on project type (Next.js vs
3172
3245
  // Python vs …), and that is the "smart PM" behaviour this layer must not
@@ -3232,7 +3305,7 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
3232
3305
  // why, so the goal reads as unfinished/blocked rather than as a completed PR. A
3233
3306
  // human or the CTO will re-dispatch the work (e.g. via manager_reply) once the
3234
3307
  // blocker clears.
3235
- '- あなたは1回きりのバッチ実行であり、このプロセスが終了した後に自動で再開することはない。レーン/ファイルの競合など自分では解けない理由で着手できない場合、「空き次第自動で再開します」のような、この実行の外では実現できない約束を書かないこと。何をどう変更できなかったか(コード変更0件であること)を正直に書き、ゴールが完了したPRではなく未完了/ブロックとして扱われるように終えること。再開は人間かCTOが改めて指示する。',
3308
+ '- この実行は終了後に勝手には再開しない。レーン/ファイルの競合など承認では解けないブロッカーに対して「空き次第自動で再開します」と約束しないこと。人の明示許可で解ける場合だけ上記 manager-approval を使い、それ以外は何を変更できなかったかを正直に報告する。',
3236
3309
  ].filter(Boolean).join('\n');
3237
3310
  }
3238
3311
 
@@ -3250,6 +3323,32 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
3250
3323
  // button that opens the wrong thing is the fabricated-proof bug all over again.
3251
3324
  export const RUN_DECL_FILE = '.manager-run.json';
3252
3325
 
3326
+ // Structured evidence is a transport record, not a Manager verdict. A worker
3327
+ // may append it to its normal final report so each requirement remains visible
3328
+ // with the attempt that addressed it.
3329
+ export function parseRequirementEvidence(raw) {
3330
+ const m = String(raw ?? '').match(/MANAGER_REQUIREMENT_EVIDENCE:\s*(\{[^\n]+\})/);
3331
+ if (!m) return null;
3332
+ let value;
3333
+ try { value = JSON.parse(m[1]); } catch { return null; }
3334
+ if (!Array.isArray(value?.requirements)) return null;
3335
+ const requirements = value.requirements.slice(0, 20).map((entry) => {
3336
+ const id = typeof entry?.id === 'string' ? entry.id.trim().slice(0, 120) : '';
3337
+ const status = ['addressed', 'partial', 'not-addressed'].includes(entry?.status) ? entry.status : '';
3338
+ const evidence = typeof entry?.evidence === 'string' ? entry.evidence.trim().slice(0, 500) : '';
3339
+ // `files` is optional: a worker that names no files gets no key, so the
3340
+ // record stays exactly what was declared.
3341
+ const files = Array.isArray(entry?.files) ? [...new Set(entry.files.map((f) => typeof f === 'string' ? f.trim().slice(0, 300) : '').filter(Boolean))].slice(0, 30) : [];
3342
+ return id && status ? { id, status, evidence, ...(files.length ? { files } : {}) } : null;
3343
+ }).filter(Boolean);
3344
+ return requirements.length ? { requirements } : null;
3345
+ }
3346
+
3347
+ export function unmappedChangedFiles(changedFiles = [], evidence = null) {
3348
+ const claimed = new Set((evidence?.requirements ?? []).flatMap((r) => r.files ?? []));
3349
+ return [...new Set((changedFiles ?? []).filter(Boolean))].filter((file) => !claimed.has(file));
3350
+ }
3351
+
3253
3352
  // A declaration that parsed as JSON but breaks a rule (bad url scheme, an
3254
3353
  // unsafe `file` path) is NOT the same failure as unparseable JSON — the first
3255
3354
  // is a worker mistake with a specific, sayable reason; the second is a broken
@@ -3752,6 +3851,7 @@ export function inboxKind(goal) {
3752
3851
  if (isPlanReview(goal)) return 'plan';
3753
3852
  if (goal.status === 'review') return goal.reviewOnly ? 'decision' : 'review';
3754
3853
  if (goal.status === 'needsInput') return 'question';
3854
+ if (goal.status === 'needsApproval') return 'approval';
3755
3855
  if (goal.status === 'blocked') return 'blocked';
3756
3856
  return null;
3757
3857
  }
@@ -3764,6 +3864,7 @@ export function inboxKind(goal) {
3764
3864
  // 質問し続ける(running に戻す分岐にも入らない)。弾かれるより悪い=黙って違うことを
3765
3865
  // するので、ここで行き先を固定する。
3766
3866
  export const INBOX_ACTIONS = {
3867
+ approval: 'decide in the Manager UI, or call manager_decide_approval(decision). Proceed/edit resumes only the approved action; cancel stops it. This is NOT Review.',
3767
3868
  plan: 'manager_approve → runs the approved plan (does NOT file to Done) / manager_dismiss → sends it back',
3768
3869
  decision: 'manager_approve → files it to Done / manager_dismiss → drops it (no worker runs either way)',
3769
3870
  review: 'manager_approve → files it to Done / manager_dismiss(text) → sends it back to the worker with your fix note',
@@ -3794,6 +3895,11 @@ export function buildReviewInbox(goals = [], { project } = {}) {
3794
3895
  options: Array.isArray(opts) ? opts.map((o) => String(o).replace(/\s+/g, ' ').trim()).slice(0, 8) : [],
3795
3896
  }
3796
3897
  : null,
3898
+ approval: kind === 'approval' ? {
3899
+ action: String(g.approvalRequest?.action ?? '').slice(0, 120),
3900
+ question: String(g.approvalRequest?.question ?? g.text ?? '').replace(/\s+/g, ' ').trim().slice(0, 200),
3901
+ scope: Array.isArray(g.approvalRequest?.scope) ? g.approvalRequest.scope.slice(0, 20) : [],
3902
+ } : null,
3797
3903
  blockedReason: kind === 'blocked'
3798
3904
  ? String(g.blocked?.reason ?? 'blocked').replace(/\s+/g, ' ').trim().slice(0, 120)
3799
3905
  : null,
@@ -3802,6 +3908,7 @@ export function buildReviewInbox(goals = [], { project } = {}) {
3802
3908
  }
3803
3909
 
3804
3910
  const INBOX_KIND_HEADING = {
3911
+ approval: 'NEEDS YOUR APPROVAL',
3805
3912
  question: 'NEEDS YOUR ANSWER',
3806
3913
  plan: 'PLAN — approve to execute',
3807
3914
  decision: 'DECISION — no worker ran',
@@ -3809,7 +3916,7 @@ const INBOX_KIND_HEADING = {
3809
3916
  blocked: 'BLOCKED',
3810
3917
  };
3811
3918
  // 止まっているものから先に。question/plan は答えるまで前に進まないので上。
3812
- const INBOX_KIND_ORDER = ['question', 'plan', 'decision', 'review', 'blocked'];
3919
+ const INBOX_KIND_ORDER = ['approval', 'question', 'plan', 'decision', 'review', 'blocked'];
3813
3920
 
3814
3921
  // kind ごとにまとめ、「次の一手」は1グループ1回だけ書く。これを item ごとに繰り返すと、
3815
3922
  // レビューが20件溜まった時に同じ一文を20回読ませることになる——読み手は Claude なので
@@ -3832,6 +3939,11 @@ export function formatReviewInbox(items = []) {
3832
3939
  lines.push(` Q: ${it.question.text}`);
3833
3940
  if (it.question.options.length) lines.push(` options: ${it.question.options.join(' | ')}`);
3834
3941
  }
3942
+ if (it.approval) {
3943
+ lines.push(` Q: ${it.approval.question}`);
3944
+ lines.push(` action: ${it.approval.action || 'continue'}`);
3945
+ if (it.approval.scope.length) lines.push(` scope: ${it.approval.scope.join(' | ')}`);
3946
+ }
3835
3947
  }
3836
3948
  return lines.join('\n');
3837
3949
  });
package/engine/mcp.mjs CHANGED
@@ -37,6 +37,7 @@ const fmtGoal = (g, tasks) => {
37
37
  const gt = tasks.filter((t) => t.goalId === g.id);
38
38
  const lines = gt.map((t) => ` - [${t.status}] ${t.num} ${t.title}${t.secs ? ` (${t.secs}s)` : ''}${t.proof ? ` proof:${t.proof.pass ? 'PASS' : 'FAIL'}` : ''}`);
39
39
  return [`goal ${g.id} [${g.status}] (${g.projectId}) ${g.text.replace(/\s+/g, ' ').slice(0, 120)}`,
40
+ g.approvalDecision ? ` decision: ${g.approvalDecision.decision}${g.approvalDecision.instruction ? ` — ${g.approvalDecision.instruction}` : ''}` : '',
40
41
  g.pr ? ` PR: ${g.pr}` : '', ...lines].filter(Boolean).join('\n');
41
42
  };
42
43
 
@@ -58,7 +59,7 @@ const TOOLS = [
58
59
  },
59
60
  {
60
61
  name: 'manager_request_review',
61
- description: 'Put an item in the Manager Review lane for a human to look at / approve — WITHOUT running any worker. Use for "please confirm / decide / eyeball this" that needs human judgment, not implementation (a design call, a go/no-go, a thing you already did and want signed off). It appears in the Review column; the human taps Approve (files it to Done) or Dismiss (drops it). No Claude worker is spawned, no PR is opened. Prefer this over manager_create_goal whenever you only need a decision, not work.',
62
+ description: 'Put a FINISHED deliverable in Review for acceptance. Use only after the work/result exists and can be evaluated. Do not use for permission to continue an operation; use manager_request_approval for that.',
62
63
  inputSchema: { type: 'object', properties: {
63
64
  project: { type: 'string', description: 'project id (e.g. galda1, galda2)' },
64
65
  text: { type: 'string', description: 'the one-line ask / what to decide (shown as the requirement)' },
@@ -70,6 +71,23 @@ const TOOLS = [
70
71
  return `Filed for review: goal ${g.id} [${g.status}] in ${g.projectId}. No worker started — waiting on a human Approve/Dismiss.`;
71
72
  },
72
73
  },
74
+ {
75
+ name: 'manager_request_approval',
76
+ description: 'Pause for a human go/no-go before continuing an operation. Creates a Needs you card, never a Review item. Poll manager_status for proceed/edit/cancel, then continue only within the approved scope.',
77
+ inputSchema: { type: 'object', properties: {
78
+ project: { type: 'string', description: 'project id (e.g. galda1, galda2)' },
79
+ question: { type: 'string', description: 'one short approval question shown to the human' },
80
+ action: { type: 'string', description: 'stable short action id, e.g. git_commit_rebase_create_pr' },
81
+ scope: { type: 'array', items: { type: 'string' }, description: 'exact files, branch, systems, or effects covered by the approval' },
82
+ resumeInstruction: { type: 'string', description: 'what the requesting agent will do after proceed' },
83
+ }, required: ['project', 'question', 'action'] },
84
+ run: async (a) => {
85
+ const g = await api('/api/tasks', { method: 'POST', headers: { 'content-type': 'application/json' },
86
+ body: JSON.stringify({ projectId: a.project, text: a.question, approval: true, action: a.action,
87
+ scope: a.scope, resumeInstruction: a.resumeInstruction, source: 'mcp' }) });
88
+ return `Approval requested: goal ${g.id} [${g.status}] in ${g.projectId}. Do not continue yet; poll manager_status until decision is proceed, edit, or cancel.`;
89
+ },
90
+ },
73
91
  {
74
92
  name: 'manager_status',
75
93
  description: 'List goals and their tasks (status, proof, PR links). Optionally filter by project id.',
@@ -145,6 +163,20 @@ const TOOLS = [
145
163
  return `Answered: goal ${a.goalId} is now [${g.status}] in ${g.projectId}.`;
146
164
  },
147
165
  },
166
+ {
167
+ name: 'manager_decide_approval',
168
+ description: 'Resolve a Needs you approval request. proceed resumes the exact requested action, edit replaces its allowed scope/instruction and resumes, cancel stops it. Never use this for a finished Review item.',
169
+ inputSchema: { type: 'object', properties: {
170
+ goalId: { type: 'number' },
171
+ decision: { type: 'string', enum: ['proceed', 'edit', 'cancel'] },
172
+ instruction: { type: 'string', description: 'required for edit: the revised allowed scope/instruction' },
173
+ }, required: ['goalId', 'decision'] },
174
+ run: async (a) => {
175
+ const g = await api(`/api/goals/${a.goalId}/approval`, { method: 'POST', headers: { 'content-type': 'application/json' },
176
+ body: JSON.stringify({ decision: a.decision, instruction: a.instruction }) });
177
+ return `Approval ${a.decision}: goal ${a.goalId} is now [${g.status}]${g.replyTaskId ? `; continuation task ${g.replyTaskId} queued` : ''}.`;
178
+ },
179
+ },
148
180
  ];
149
181
 
150
182
  function reply(id, result) { process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n'); }