@galda/cli 0.10.95 → 0.10.96
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/completion-contract.mjs +38 -0
- package/engine/server.mjs +79 -1
- package/package.json +1 -1
|
@@ -40,8 +40,46 @@ export const COMPLETION_CONTRACT_PROMPT = [
|
|
|
40
40
|
'- The latest explicit user instruction can revise an older product decision or code comment. Treat old comments as context, not proof that the requested change is impossible. If the change has consequences, explain them and implement the latest request; only a real safety, permission, legal, or external-system constraint is a blocker.',
|
|
41
41
|
'- A test must assert the observable outcome the user requested. Never replace the request with a tooltip, explanation, or a test that permanently preserves the opposite behavior.',
|
|
42
42
|
'- Do not report a change request as complete with “cannot fix”, “直せません”, or equivalent. If a real external blocker remains, state the concrete blocker and leave the work explicitly unfinished instead of presenting it for Review as completed.',
|
|
43
|
+
'- You own the routing decision. When the run is not complete, append exactly one single-line receipt: MANAGER_OUTCOME: {"outcome":"needs-input|blocked|rebind-workspace","summary":"short reason","question":"question for the user when needed","options":["optional choice"],"workingDirectory":"absolute path for rebind-workspace"}. Do not emit this receipt for completed work. Galda transports this declaration verbatim; it does not reinterpret your prose.',
|
|
43
44
|
].join('\n');
|
|
44
45
|
|
|
46
|
+
const MANAGER_OUTCOMES = new Set(['needs-input', 'blocked', 'rebind-workspace']);
|
|
47
|
+
|
|
48
|
+
// A transport receipt written by the worker, not a Manager inference. Keep the
|
|
49
|
+
// schema deliberately tiny: the agent decides what happened; Galda only checks
|
|
50
|
+
// that the envelope is safe to route.
|
|
51
|
+
export function parseManagerOutcome(raw) {
|
|
52
|
+
const m = String(raw ?? '').match(/MANAGER_OUTCOME:\s*(\{[^\n]+\})/);
|
|
53
|
+
if (!m) return null;
|
|
54
|
+
let value;
|
|
55
|
+
try { value = JSON.parse(m[1]); } catch { return null; }
|
|
56
|
+
if (!MANAGER_OUTCOMES.has(value?.outcome)) return null;
|
|
57
|
+
const summary = typeof value.summary === 'string' ? value.summary.trim().slice(0, 500) : '';
|
|
58
|
+
const question = typeof value.question === 'string' ? value.question.trim().slice(0, 300) : '';
|
|
59
|
+
const options = Array.isArray(value.options)
|
|
60
|
+
? value.options.map((x) => typeof x === 'string' ? x.trim().slice(0, 60) : '').filter(Boolean).slice(0, 4)
|
|
61
|
+
: [];
|
|
62
|
+
const workingDirectory = typeof value.workingDirectory === 'string' ? value.workingDirectory.trim().slice(0, 1000) : '';
|
|
63
|
+
if (value.outcome === 'needs-input' && !question) return null;
|
|
64
|
+
if (value.outcome === 'blocked' && !summary) return null;
|
|
65
|
+
if (value.outcome === 'rebind-workspace' && !workingDirectory.startsWith('/')) return null;
|
|
66
|
+
return { outcome: value.outcome, summary, question, options, workingDirectory };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Existing requirement evidence is also an explicit worker declaration. A
|
|
70
|
+
// worker cannot say `partial`/`not-addressed` and simultaneously be routed to
|
|
71
|
+
// Review as complete. This is schema consistency, not semantic judgment.
|
|
72
|
+
export function incompleteEvidenceOutcome(evidence) {
|
|
73
|
+
const incomplete = (evidence?.requirements ?? []).filter((r) => ['partial', 'not-addressed'].includes(r?.status));
|
|
74
|
+
if (!incomplete.length) return null;
|
|
75
|
+
return {
|
|
76
|
+
outcome: 'blocked',
|
|
77
|
+
summary: incomplete.map((r) => r.evidence || `${r.id}: ${r.status}`).filter(Boolean).join('; ').slice(0, 500)
|
|
78
|
+
|| 'The worker declared one or more requirements incomplete.',
|
|
79
|
+
question: '', options: [], workingDirectory: '',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
45
83
|
export function completionContractIssue({ request = '', followUp = '', result = '', status = '', changedFiles = [] } = {}) {
|
|
46
84
|
if (status !== 'done') return null;
|
|
47
85
|
const requested = `${request}\n${followUp}`;
|
package/engine/server.mjs
CHANGED
|
@@ -26,7 +26,7 @@ import { createSerialQueue } from './lib.mjs';
|
|
|
26
26
|
import { parseRequirementEvidence, unmappedChangedFiles } from './lib.mjs';
|
|
27
27
|
import { isPrClosed } from './lib.mjs';
|
|
28
28
|
import { collectProjectRules } from './lib.mjs';
|
|
29
|
-
import { COMPLETION_CONTRACT_PROMPT, completionContractIssue } from './completion-contract.mjs';
|
|
29
|
+
import { COMPLETION_CONTRACT_PROMPT, completionContractIssue, parseManagerOutcome, incompleteEvidenceOutcome } from './completion-contract.mjs';
|
|
30
30
|
import { FALLBACK_CODEX_MODELS, loadCodexModels } from './codex-models.mjs';
|
|
31
31
|
import { CLAUDE_MODELS } from './claude-models.mjs';
|
|
32
32
|
import { classifyWorkerPreflight, shouldProbeClaudeAuth, workerCanStart } from './worker-availability.mjs';
|
|
@@ -2310,6 +2310,66 @@ function pauseGoalForApproval(goal, task, request) {
|
|
|
2310
2310
|
return true;
|
|
2311
2311
|
}
|
|
2312
2312
|
|
|
2313
|
+
// The worker decides where its result belongs. Galda does not classify prose;
|
|
2314
|
+
// it only validates and transports the explicit receipt (or the worker's
|
|
2315
|
+
// existing partial/not-addressed evidence) into the corresponding board state.
|
|
2316
|
+
function routeWorkerOutcome(goal, task, project) {
|
|
2317
|
+
if (!goal || !task || task.status !== 'done') return false;
|
|
2318
|
+
const outcome = parseManagerOutcome(task.result) || incompleteEvidenceOutcome(task.requirementEvidence);
|
|
2319
|
+
if (!outcome) return false; // backwards-compatible completed run
|
|
2320
|
+
task.managerOutcome = outcome;
|
|
2321
|
+
task.finishedAt = new Date().toISOString();
|
|
2322
|
+
|
|
2323
|
+
if (outcome.outcome === 'rebind-workspace') {
|
|
2324
|
+
const rebound = rebindGoalWorkspace(goal, project, outcome.workingDirectory);
|
|
2325
|
+
if (rebound.ok) {
|
|
2326
|
+
task.status = 'skipped';
|
|
2327
|
+
task.result = `${task.result}\n\n[Galda: continuing in ${rebound.dir} with a fresh session]`.slice(0, RESULT_MAX);
|
|
2328
|
+
saveTask(task);
|
|
2329
|
+
goal.status = 'running';
|
|
2330
|
+
goal.startedAt = null;
|
|
2331
|
+
goal.question = null;
|
|
2332
|
+
goal.blocked = null;
|
|
2333
|
+
saveGoal(goal);
|
|
2334
|
+
const continuation = queueReplyTask(goal, [
|
|
2335
|
+
`Continue the same request in the newly bound working directory: ${rebound.dir}`,
|
|
2336
|
+
outcome.summary,
|
|
2337
|
+
'Inspect the files already present there and complete the original request. Do not repeat the old sandbox diagnosis.',
|
|
2338
|
+
].filter(Boolean).join('\n'), { from: 'manager', via: 'system' });
|
|
2339
|
+
continuation.workspaceRebindContinuation = true;
|
|
2340
|
+
saveTask(continuation);
|
|
2341
|
+
sendProcessAct(task, `Worker requested a workspace rebind; continuing in ${rebound.dir} with a fresh session.`);
|
|
2342
|
+
return true;
|
|
2343
|
+
}
|
|
2344
|
+
// An unsafe/unavailable path is not silently retried in the old sandbox.
|
|
2345
|
+
outcome.outcome = 'needs-input';
|
|
2346
|
+
outcome.question = rebound.error;
|
|
2347
|
+
outcome.options = [];
|
|
2348
|
+
outcome.summary = rebound.error;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
// This turn ended successfully as a routing hand-off, not as a failed unit
|
|
2352
|
+
// of implementation. Keeping it `interrupted` would poison the eventual
|
|
2353
|
+
// closeout even after the user answers and the continuation completes.
|
|
2354
|
+
task.status = 'skipped';
|
|
2355
|
+
saveTask(task);
|
|
2356
|
+
goal.startedAt = null;
|
|
2357
|
+
if (outcome.outcome === 'needs-input') {
|
|
2358
|
+
goal.status = 'needsInput';
|
|
2359
|
+
goal.blocked = null;
|
|
2360
|
+
goal.question = { kind: 'worker', text: outcome.question, options: outcome.options };
|
|
2361
|
+
sendProcessAct(task, `Worker asked for input: ${outcome.question}`);
|
|
2362
|
+
} else {
|
|
2363
|
+
goal.status = 'blocked';
|
|
2364
|
+
goal.question = null;
|
|
2365
|
+
goal.blocked = { kind: 'worker-outcome', reason: outcome.summary };
|
|
2366
|
+
sendProcessAct(task, `Worker declared the work blocked: ${outcome.summary}`);
|
|
2367
|
+
}
|
|
2368
|
+
saveGoal(goal);
|
|
2369
|
+
startNextStacked(goal.projectId);
|
|
2370
|
+
return true;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2313
2373
|
// split (docs/design/ONE-CONVERSATION.md §1.3): 「それは別件」と返された時の受け皿。
|
|
2314
2374
|
// 元のゴールには触らない——新しいチケットを1枚起こし、そこに「どこから生まれたか」だけ
|
|
2315
2375
|
// 片方向の印(bornFrom)を残す。
|
|
@@ -2821,6 +2881,9 @@ async function runTask(task) {
|
|
|
2821
2881
|
task.unmappedChangedFiles = unmappedChangedFiles(task.changedFiles, task.requirementEvidence);
|
|
2822
2882
|
task.requirementVerification = { proof: task.proof ?? null, run: task.run ?? null, changedFiles: task.changedFiles };
|
|
2823
2883
|
sendProcessAct(task, `Detected ${task.changedFiles.length} changed file${task.changedFiles.length === 1 ? '' : 's'} from this follow-up.`);
|
|
2884
|
+
// An explicit worker receipt takes precedence over Galda's legacy prose
|
|
2885
|
+
// guard. The agent owns this decision; Manager only transports it.
|
|
2886
|
+
if (routeWorkerOutcome(goal, task, project)) return;
|
|
2824
2887
|
const completionIssue = completionContractIssue({
|
|
2825
2888
|
request: goal?.text,
|
|
2826
2889
|
followUp: task.detail,
|
|
@@ -3043,6 +3106,9 @@ async function runTask(task) {
|
|
|
3043
3106
|
task.unmappedChangedFiles = unmappedChangedFiles(task.changedFiles, task.requirementEvidence);
|
|
3044
3107
|
task.requirementVerification = { proof: task.proof ?? null, run: task.run ?? null, changedFiles: task.changedFiles };
|
|
3045
3108
|
sendProcessAct(task, `Detected ${task.changedFiles.length} changed file${task.changedFiles.length === 1 ? '' : 's'} from this task.`);
|
|
3109
|
+
// An explicit worker receipt takes precedence over Galda's legacy prose
|
|
3110
|
+
// guard. The agent owns this decision; Manager only transports it.
|
|
3111
|
+
if (routeWorkerOutcome(goal, task, project)) return;
|
|
3046
3112
|
// task 450: taskStatusAfterVerify decided 'done' from `code` alone, before
|
|
3047
3113
|
// changedFiles existed to check against. Don't trust a clean exit code by
|
|
3048
3114
|
// itself — a worker that changed nothing and left a cut-off-looking result
|
|
@@ -5143,6 +5209,18 @@ const server = createServer(async (req, res) => {
|
|
|
5143
5209
|
let answer = '';
|
|
5144
5210
|
try { answer = String(JSON.parse(body || '{}').answer ?? '').trim(); } catch {}
|
|
5145
5211
|
if (!answer) return json(res, 400, { error: 'answer required' });
|
|
5212
|
+
// A worker-authored question is a transport pause, not a new planning
|
|
5213
|
+
// cycle. Preserve the worker session and pass the user's answer straight
|
|
5214
|
+
// back to it; Galda must not reinterpret or rewrite the answer.
|
|
5215
|
+
if (goal.question?.kind === 'worker') {
|
|
5216
|
+
goal.question = null;
|
|
5217
|
+
goal.blocked = null;
|
|
5218
|
+
goal.status = 'running';
|
|
5219
|
+
goal.startedAt = null;
|
|
5220
|
+
saveGoal(goal);
|
|
5221
|
+
const replyTask = queueReplyTask(goal, answer, { from: 'you', via: 'question' });
|
|
5222
|
+
return json(res, 200, { ...goal, replyTaskId: replyTask.id });
|
|
5223
|
+
}
|
|
5146
5224
|
// Bind this goal to a project rooted at `dir` and start planning. Shared by
|
|
5147
5225
|
// the "picked a repo" and "git-init'd a new folder" paths below.
|
|
5148
5226
|
const bindAndPlan = (dir) => {
|
package/package.json
CHANGED