@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/CLAUDE.md +16 -0
- package/app/index.html +281 -96
- package/app/theme.css +6 -6
- package/engine/lib.mjs +116 -4
- package/engine/mcp.mjs +33 -1
- package/engine/server.mjs +127 -12
- package/package.json +1 -1
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
'-
|
|
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
|
|
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'); }
|