@galda/cli 0.10.83 → 0.10.85

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
  }
@@ -1474,7 +1502,7 @@ export function buildGoalPrBody({ lang, goal, goalTasks, files, owner, branch, p
1474
1502
  // 修正/追加/改善/調査 so an "追加したい" request never becomes "…の修正", and (c) a
1475
1503
  // failed/missing reply falls back to goal.text (no regression) — all without an
1476
1504
  // LLM call. The app's saReviewTitle reads reviewSummary.title first (PR-B).
1477
- export function buildReviewSummaryPrompt({ lang, goalText, changed, reports }) {
1505
+ export function buildReviewSummaryPrompt({ lang, goalText, changed, reports, latestFeedback = '' }) {
1478
1506
  return [
1479
1507
  `あるゴールの実装が完了しました。レビューする人が一目で判断できるよう、やさしい${lang === 'ja' ? '日本語' : lang}で書いてください。`,
1480
1508
  '専門用語・長い説明は禁止。小学生でも分かる短い言い方。JSONのみ出力(前後に文章を書かない):',
@@ -1483,6 +1511,7 @@ export function buildReviewSummaryPrompt({ lang, goalText, changed, reports }) {
1483
1511
  + '例:「Xの不具合」→「Xの修正」/「Yを追加したい」→「Yの追加」/「Zを速くしたい」→「Zの改善」/「なぜWか調べて」→「Wの調査」。',
1484
1512
  '',
1485
1513
  `ゴール: ${(goalText ?? '').slice(0, 300)}`,
1514
+ latestFeedback ? `最新のユーザー返信(この再レビューで満たすべき条件。初回の結果ではなく、これへの対応をchanged/checkに書く):\n${String(latestFeedback).slice(0, 1000)}` : null,
1486
1515
  `変更ファイル: ${(changed ?? []).join(', ') || '(なし)'}`,
1487
1516
  `ワーカー報告:\n${(reports ?? '').slice(0, 1500)}`,
1488
1517
  ].join('\n');
@@ -1650,6 +1679,8 @@ export function countInfraFlakes(text = '') {
1650
1679
  /Navigation timeout of \d+\s*ms exceeded/g, // puppeteer page.goto
1651
1680
  /test timed out after \d+\s*ms/g, // node --test per-test timeout
1652
1681
  /\bTarget closed\b/g, // puppeteer: browser died mid-test
1682
+ /puppeteer-core is not installed next to the Manager/g, // worktree/node_modules missing
1683
+ /Cannot find module ['"]puppeteer-core['"]/g, // same dependency failure, alternate Node wording
1653
1684
  ];
1654
1685
  return pats.reduce((n, re) => n + (String(text ?? '').match(re) || []).length, 0);
1655
1686
  }
@@ -1906,6 +1937,7 @@ export function goalGroupStatus(goalStatus, startedAt) {
1906
1937
  if (goalStatus === 'folded') return 'review'; // PRD §6.5: rendered inside the Review section, not Done
1907
1938
  if (goalStatus === 'blocked') return 'attn'; // §4E: blocked is Attention, not Done
1908
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
1909
1941
  if (goalStatus === 'rejected') return 'rejected'; // 07-20c: Reject を溜める独立欄(Done ではない)
1910
1942
  return 'done'; // includes 'reverted' (terminal)
1911
1943
  }
@@ -1954,9 +1986,32 @@ export function goalChip(goalStatus, columns, startedAt) {
1954
1986
  if (goalStatus === 'folded') return ['FOLDED', '']; // PRD §6.5: settled, not failed — no red/badge class, matches 'done'/'reverted'
1955
1987
  if (goalStatus === 'blocked') return ['BLOCKED', 'blocked']; // §4E: distinct state (reason + reverify), not FAIL/Done
1956
1988
  if (goalStatus === 'needsInput') return ['確認待ち', 'blocked']; // smart intake: awaiting an answer
1989
+ if (goalStatus === 'needsApproval') return ['NEEDS YOU', 'blocked'];
1957
1990
  return ['FAIL', ''];
1958
1991
  }
1959
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
+
1960
2015
  // Group a flat task list under their parent goal for the Tasks sidebar, so
1961
2016
  // a goal like "この体験を実現したい" shows as one collapsible unit instead of
1962
2017
  // its tasks scattered flat in the list. Each group carries a done/total
@@ -2035,6 +2090,9 @@ export function buildReviewDigest({ goals, tasks } = {}) {
2035
2090
  testResult: g.testResult ?? null,
2036
2091
  verificationInfraError: g.verificationInfraError ?? null,
2037
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),
2038
2096
  plan,
2039
2097
  planText: plan ? (g.planText ?? '') : null,
2040
2098
  };
@@ -2047,6 +2105,7 @@ export function buildReviewDigest({ goals, tasks } = {}) {
2047
2105
  if (existing.proof == null) existing.proof = row.proof;
2048
2106
  if (existing.testResult == null) existing.testResult = row.testResult;
2049
2107
  if (existing.verificationInfraError == null) existing.verificationInfraError = row.verificationInfraError;
2108
+ existing.incompleteRequirementCount += row.incompleteRequirementCount;
2050
2109
  continue;
2051
2110
  }
2052
2111
  if (row.pr) byPr.set(row.pr, row);
@@ -2804,6 +2863,13 @@ export function buildEphemeralSeedLines(at = '2026-01-01T00:00:00.000Z') {
2804
2863
  ...reqTasks,
2805
2864
  { kind: 'goal', id: 16, projectId: 'default', text: '[verify fixture] 失敗したTo Do行', status: 'partial', wantsPR: false, model: 'sonnet', images: [], plan: ['失敗行のアクション確認'], pr: undefined, createdAt: at },
2806
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 },
2807
2873
  ];
2808
2874
  }
2809
2875
 
@@ -3120,9 +3186,20 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
3120
3186
  // TASK/DETAIL below is a paraphrase from the planner — when they differ,
3121
3187
  // follow the user's words.
3122
3188
  goal?.text ? `USER'S REQUEST (verbatim — the source of truth; deliver exactly this):\n${goal.text}` : '',
3189
+ // A review reply is a new, explicit acceptance condition. It used to be
3190
+ // present only as TASK DETAIL below the inherited session context, which
3191
+ // made it easy for a resumed agent to keep describing the first attempt.
3192
+ // Put it next to the original request and make the precedence explicit:
3193
+ // the interface transports the user's words, it does not reinterpret them.
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')}` : '',
3123
3199
  runContext ? (typeof runContext === 'string' ? runContext : buildWorkerContextBlock(runContext)) : '',
3124
3200
  '',
3125
3201
  `TASK ${task.num}: ${task.title}`,
3202
+ `REQUIREMENT ID FOR THIS RUN: task-${task.id ?? task.num}.`,
3126
3203
  task.detail ? `DETAIL: ${task.detail}` : '',
3127
3204
  `WORKSPACE MODE: ${workspaceMode}.`,
3128
3205
  goal?.images?.length ? `ATTACHED IMAGES (absolute paths, use the Read tool to view them): ${goal.images.join(' ')}` : '',
@@ -3144,6 +3221,9 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
3144
3221
  // instead of expanding scope"): the request, not our fear of scope, sets
3145
3222
  // the size of the work.
3146
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.',
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.',
3147
3227
  '- デザイン案・UI案・レイアウト案を求められた場合は、文章だけで完了にしない。画像を使わない案でも、比較して確認できるHTML等のartifactをプロジェクト内に作り、実際に開ける .manager-run.json を残す。',
3148
3228
  '- Keep the work bounded to the request: inspect only files needed; avoid broad repository scans unless they are necessary.',
3149
3229
  "- You may use the user's installed skills (Skill tool, e.g. /galda-doc) when one clearly fits the request.",
@@ -3157,6 +3237,9 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
3157
3237
  // stop re-checking; but the deliverables above must still be produced in full.
3158
3238
  '- 動くと確認できたら十分(無駄な再検証でログを漁り続けない)。ただし依頼された成果物は削らず出し切ること。',
3159
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 が承認後に同じゴールを再開する。',
3160
3243
  // How the human actually looks at the result. We never guess the run
3161
3244
  // command ourselves — guessing means branching on project type (Next.js vs
3162
3245
  // Python vs …), and that is the "smart PM" behaviour this layer must not
@@ -3222,7 +3305,7 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
3222
3305
  // why, so the goal reads as unfinished/blocked rather than as a completed PR. A
3223
3306
  // human or the CTO will re-dispatch the work (e.g. via manager_reply) once the
3224
3307
  // blocker clears.
3225
- '- あなたは1回きりのバッチ実行であり、このプロセスが終了した後に自動で再開することはない。レーン/ファイルの競合など自分では解けない理由で着手できない場合、「空き次第自動で再開します」のような、この実行の外では実現できない約束を書かないこと。何をどう変更できなかったか(コード変更0件であること)を正直に書き、ゴールが完了したPRではなく未完了/ブロックとして扱われるように終えること。再開は人間かCTOが改めて指示する。',
3308
+ '- この実行は終了後に勝手には再開しない。レーン/ファイルの競合など承認では解けないブロッカーに対して「空き次第自動で再開します」と約束しないこと。人の明示許可で解ける場合だけ上記 manager-approval を使い、それ以外は何を変更できなかったかを正直に報告する。',
3226
3309
  ].filter(Boolean).join('\n');
3227
3310
  }
3228
3311
 
@@ -3240,6 +3323,32 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
3240
3323
  // button that opens the wrong thing is the fabricated-proof bug all over again.
3241
3324
  export const RUN_DECL_FILE = '.manager-run.json';
3242
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
+
3243
3352
  // A declaration that parsed as JSON but breaks a rule (bad url scheme, an
3244
3353
  // unsafe `file` path) is NOT the same failure as unparseable JSON — the first
3245
3354
  // is a worker mistake with a specific, sayable reason; the second is a broken
@@ -3742,6 +3851,7 @@ export function inboxKind(goal) {
3742
3851
  if (isPlanReview(goal)) return 'plan';
3743
3852
  if (goal.status === 'review') return goal.reviewOnly ? 'decision' : 'review';
3744
3853
  if (goal.status === 'needsInput') return 'question';
3854
+ if (goal.status === 'needsApproval') return 'approval';
3745
3855
  if (goal.status === 'blocked') return 'blocked';
3746
3856
  return null;
3747
3857
  }
@@ -3754,6 +3864,7 @@ export function inboxKind(goal) {
3754
3864
  // 質問し続ける(running に戻す分岐にも入らない)。弾かれるより悪い=黙って違うことを
3755
3865
  // するので、ここで行き先を固定する。
3756
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.',
3757
3868
  plan: 'manager_approve → runs the approved plan (does NOT file to Done) / manager_dismiss → sends it back',
3758
3869
  decision: 'manager_approve → files it to Done / manager_dismiss → drops it (no worker runs either way)',
3759
3870
  review: 'manager_approve → files it to Done / manager_dismiss(text) → sends it back to the worker with your fix note',
@@ -3784,6 +3895,11 @@ export function buildReviewInbox(goals = [], { project } = {}) {
3784
3895
  options: Array.isArray(opts) ? opts.map((o) => String(o).replace(/\s+/g, ' ').trim()).slice(0, 8) : [],
3785
3896
  }
3786
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,
3787
3903
  blockedReason: kind === 'blocked'
3788
3904
  ? String(g.blocked?.reason ?? 'blocked').replace(/\s+/g, ' ').trim().slice(0, 120)
3789
3905
  : null,
@@ -3792,6 +3908,7 @@ export function buildReviewInbox(goals = [], { project } = {}) {
3792
3908
  }
3793
3909
 
3794
3910
  const INBOX_KIND_HEADING = {
3911
+ approval: 'NEEDS YOUR APPROVAL',
3795
3912
  question: 'NEEDS YOUR ANSWER',
3796
3913
  plan: 'PLAN — approve to execute',
3797
3914
  decision: 'DECISION — no worker ran',
@@ -3799,7 +3916,7 @@ const INBOX_KIND_HEADING = {
3799
3916
  blocked: 'BLOCKED',
3800
3917
  };
3801
3918
  // 止まっているものから先に。question/plan は答えるまで前に進まないので上。
3802
- const INBOX_KIND_ORDER = ['question', 'plan', 'decision', 'review', 'blocked'];
3919
+ const INBOX_KIND_ORDER = ['approval', 'question', 'plan', 'decision', 'review', 'blocked'];
3803
3920
 
3804
3921
  // kind ごとにまとめ、「次の一手」は1グループ1回だけ書く。これを item ごとに繰り返すと、
3805
3922
  // レビューが20件溜まった時に同じ一文を20回読ませることになる——読み手は Claude なので
@@ -3822,6 +3939,11 @@ export function formatReviewInbox(items = []) {
3822
3939
  lines.push(` Q: ${it.question.text}`);
3823
3940
  if (it.question.options.length) lines.push(` options: ${it.question.options.join(' | ')}`);
3824
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
+ }
3825
3947
  }
3826
3948
  return lines.join('\n');
3827
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'); }