goalbuddy 0.3.2 → 0.3.5
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/README.md +28 -3
- package/RELEASE-0.3.5.md +324 -0
- package/goalbuddy/SKILL.md +8 -2
- package/goalbuddy/agents/goal_judge.toml +29 -17
- package/goalbuddy/agents/goal_scout.toml +34 -14
- package/goalbuddy/agents/goal_worker.toml +32 -15
- package/goalbuddy/extend/local-goal-board/README.md +8 -4
- package/goalbuddy/extend/local-goal-board/examples/subgoal-parent/goal.md +3 -0
- package/goalbuddy/extend/local-goal-board/examples/subgoal-parent/notes/.gitkeep +1 -0
- package/goalbuddy/extend/local-goal-board/examples/subgoal-parent/state.yaml +60 -0
- package/goalbuddy/extend/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/goal.md +3 -0
- package/goalbuddy/extend/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/notes/.gitkeep +1 -0
- package/goalbuddy/extend/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/state.yaml +52 -0
- package/goalbuddy/extend/local-goal-board/extension.yaml +6 -4
- package/goalbuddy/extend/local-goal-board/scripts/lib/goal-board.mjs +940 -24
- package/goalbuddy/extend/local-goal-board/scripts/local-goal-board.mjs +389 -54
- package/goalbuddy/extend/local-goal-board/test/local-goal-board.test.mjs +420 -4
- package/goalbuddy/scripts/check-goal-state.mjs +116 -6
- package/goalbuddy/scripts/parallel-plan.mjs +191 -0
- package/goalbuddy/scripts/render-task-prompt.mjs +248 -0
- package/goalbuddy/templates/agents.md +2 -2
- package/goalbuddy/templates/state.yaml +8 -0
- package/internal/assets/goalbuddy-v0.3.5-release.png +0 -0
- package/internal/cli/goal-maker.mjs +64 -1
- package/package.json +3 -2
- package/plugins/goalbuddy/.claude-plugin/plugin.json +2 -2
- package/plugins/goalbuddy/.codex-plugin/plugin.json +4 -4
- package/plugins/goalbuddy/README.md +5 -3
- package/plugins/goalbuddy/agents/goal-judge.md +31 -16
- package/plugins/goalbuddy/agents/goal-scout.md +38 -13
- package/plugins/goalbuddy/agents/goal-worker.md +35 -14
- package/plugins/goalbuddy/skills/goalbuddy/SKILL.md +8 -2
- package/plugins/goalbuddy/skills/goalbuddy/agents/goal_judge.toml +29 -17
- package/plugins/goalbuddy/skills/goalbuddy/agents/goal_scout.toml +34 -14
- package/plugins/goalbuddy/skills/goalbuddy/agents/goal_worker.toml +32 -15
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/README.md +8 -4
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/subgoal-parent/goal.md +3 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/subgoal-parent/notes/.gitkeep +1 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/subgoal-parent/state.yaml +60 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/goal.md +3 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/notes/.gitkeep +1 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/state.yaml +52 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/extension.yaml +6 -4
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/scripts/lib/goal-board.mjs +940 -24
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/scripts/local-goal-board.mjs +389 -54
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/test/local-goal-board.test.mjs +420 -4
- package/plugins/goalbuddy/skills/goalbuddy/scripts/check-goal-state.mjs +116 -6
- package/plugins/goalbuddy/skills/goalbuddy/scripts/parallel-plan.mjs +191 -0
- package/plugins/goalbuddy/skills/goalbuddy/scripts/render-task-prompt.mjs +248 -0
- package/plugins/goalbuddy/skills/goalbuddy/templates/agents.md +2 -2
- package/plugins/goalbuddy/skills/goalbuddy/templates/state.yaml +8 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { relative, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { childBoardPaths, loadBoard, parseArgs, resolveBoardPath, selectTask } from "./render-task-prompt.mjs";
|
|
6
|
+
|
|
7
|
+
if (isDirectRun()) {
|
|
8
|
+
try {
|
|
9
|
+
const options = parseArgs(process.argv.slice(2));
|
|
10
|
+
const plan = createParallelPlan(options);
|
|
11
|
+
if (options.json) {
|
|
12
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
13
|
+
} else {
|
|
14
|
+
console.log(formatPlan(plan));
|
|
15
|
+
}
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error(error.message);
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createParallelPlan(options) {
|
|
23
|
+
const rootBoardPath = resolveBoardPath(options);
|
|
24
|
+
const boards = [loadBoard(rootBoardPath)];
|
|
25
|
+
for (const childPath of childBoardPaths(boards[0])) {
|
|
26
|
+
if (existsSync(childPath)) boards.push(loadBoard(childPath));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const candidates = boards.map((board) => candidateForBoard(board));
|
|
30
|
+
const workerCandidates = candidates.filter((candidate) => candidate.role === "worker");
|
|
31
|
+
return {
|
|
32
|
+
root_board_path: rootBoardPath,
|
|
33
|
+
mutated: false,
|
|
34
|
+
spawned_agents: false,
|
|
35
|
+
candidates: candidates.map((candidate) => ({
|
|
36
|
+
...candidate,
|
|
37
|
+
safe_to_parallelize: isSafeCandidate(candidate, workerCandidates),
|
|
38
|
+
reason: safetyReason(candidate, workerCandidates),
|
|
39
|
+
render_prompt_command: promptCommand(candidate),
|
|
40
|
+
})),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function candidateForBoard(board) {
|
|
45
|
+
const task = selectTask(board);
|
|
46
|
+
const role = normalizeRole(task.type);
|
|
47
|
+
return {
|
|
48
|
+
board_path: board.path,
|
|
49
|
+
task_id: task.id,
|
|
50
|
+
role,
|
|
51
|
+
recommended_agent: role === "scout" ? "goal_scout" : role === "judge" ? "goal_judge" : role === "worker" ? "goal_worker" : "PM",
|
|
52
|
+
reasoning_hint: reasoningHint(task, role),
|
|
53
|
+
allowed_files: Array.isArray(task.allowed_files) ? task.allowed_files.map(String) : [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isSafeCandidate(candidate, workers) {
|
|
58
|
+
if (candidate.role === "scout" || candidate.role === "judge") return true;
|
|
59
|
+
if (candidate.role !== "worker") return false;
|
|
60
|
+
if (workers.length < 2) return false;
|
|
61
|
+
if (candidate.allowed_files.length === 0) return false;
|
|
62
|
+
return workers
|
|
63
|
+
.filter((worker) => worker !== candidate)
|
|
64
|
+
.every((worker) => worker.allowed_files.length > 0 && areDisjoint(candidate.allowed_files, worker.allowed_files));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function safetyReason(candidate, workers) {
|
|
68
|
+
if (candidate.role === "scout") return "Scout is read-only.";
|
|
69
|
+
if (candidate.role === "judge") return "Judge is read-only.";
|
|
70
|
+
if (candidate.role !== "worker") return "PM tasks mutate board truth and should stay serial.";
|
|
71
|
+
if (candidate.allowed_files.length === 0) return "Worker has no allowed_files, so write scope is unknown.";
|
|
72
|
+
const overlapping = workers
|
|
73
|
+
.filter((worker) => worker !== candidate)
|
|
74
|
+
.filter((worker) => worker.allowed_files.length === 0 || !areDisjoint(candidate.allowed_files, worker.allowed_files));
|
|
75
|
+
if (overlapping.length === 0) return workers.length > 1 ? "Worker write scope is disjoint from other active Workers." : "Only one active Worker candidate; parallel Worker safety needs a disjoint peer.";
|
|
76
|
+
return `Worker write scope overlaps or cannot be compared with ${overlapping.map((worker) => `${relative(process.cwd(), worker.board_path)}:${worker.task_id}`).join(", ")}.`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function promptCommand(candidate) {
|
|
80
|
+
return `goalbuddy prompt --board ${quote(candidate.board_path)} --task ${candidate.task_id}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function areDisjoint(left, right) {
|
|
84
|
+
return left.every((leftPattern) => right.every((rightPattern) => !patternsOverlap(leftPattern, rightPattern)));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function patternsOverlap(left, right) {
|
|
88
|
+
const a = normalizePattern(left);
|
|
89
|
+
const b = normalizePattern(right);
|
|
90
|
+
const aHasGlob = hasGlob(a);
|
|
91
|
+
const bHasGlob = hasGlob(b);
|
|
92
|
+
if (a === b) return true;
|
|
93
|
+
if (a.endsWith("/**") && b.startsWith(a.slice(0, -3))) return true;
|
|
94
|
+
if (b.endsWith("/**") && a.startsWith(b.slice(0, -3))) return true;
|
|
95
|
+
if (!aHasGlob && !bHasGlob) return false;
|
|
96
|
+
if (!aHasGlob) return globToRegExp(b).test(a);
|
|
97
|
+
if (!bHasGlob) return globToRegExp(a).test(b);
|
|
98
|
+
if (hasUnsupportedGlob(a) || hasUnsupportedGlob(b)) return literalPrefixesMayOverlap(a, b);
|
|
99
|
+
return literalPrefixesMayOverlap(a, b);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function literalPrefixesMayOverlap(left, right) {
|
|
103
|
+
const a = literalPrefix(left);
|
|
104
|
+
const b = literalPrefix(right);
|
|
105
|
+
if (!a || !b) return true;
|
|
106
|
+
return a.startsWith(b) || b.startsWith(a);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function literalPrefix(pattern) {
|
|
110
|
+
const match = /[*?[\]]/.exec(pattern);
|
|
111
|
+
return match ? pattern.slice(0, match.index) : pattern;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function hasUnsupportedGlob(pattern) {
|
|
115
|
+
return /[\[\]]/.test(pattern);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function globToRegExp(pattern) {
|
|
119
|
+
let source = "";
|
|
120
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
121
|
+
const char = pattern[index];
|
|
122
|
+
const next = pattern[index + 1];
|
|
123
|
+
if (char === "*" && next === "*") {
|
|
124
|
+
source += ".*";
|
|
125
|
+
index += 1;
|
|
126
|
+
} else if (char === "*") {
|
|
127
|
+
source += "[^/]*";
|
|
128
|
+
} else if (char === "?") {
|
|
129
|
+
source += "[^/]";
|
|
130
|
+
} else {
|
|
131
|
+
source += escapeRegExp(char);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return new RegExp(`^${source}$`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function escapeRegExp(value) {
|
|
138
|
+
return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function hasGlob(pattern) {
|
|
142
|
+
return /[*?[\]]/.test(pattern);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizePattern(pattern) {
|
|
146
|
+
return String(pattern || "").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeRole(value) {
|
|
150
|
+
const role = String(value || "pm").toLowerCase();
|
|
151
|
+
return ["scout", "judge", "worker", "pm"].includes(role) ? role : "pm";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function reasoningHint(task, role) {
|
|
155
|
+
const hint = String(task.reasoning_hint || "").toLowerCase();
|
|
156
|
+
if (["low", "medium", "high", "xhigh"].includes(hint)) return hint;
|
|
157
|
+
if (role === "judge") return "high";
|
|
158
|
+
return "low";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function quote(value) {
|
|
162
|
+
return JSON.stringify(resolve(value));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function formatPlan(plan) {
|
|
166
|
+
const lines = [
|
|
167
|
+
"GoalBuddy parallel plan",
|
|
168
|
+
"",
|
|
169
|
+
`Root board: ${plan.root_board_path}`,
|
|
170
|
+
"Mutates state: no",
|
|
171
|
+
"Spawns agents: no",
|
|
172
|
+
"",
|
|
173
|
+
];
|
|
174
|
+
for (const candidate of plan.candidates) {
|
|
175
|
+
lines.push(
|
|
176
|
+
`${candidate.board_path}:${candidate.task_id}`,
|
|
177
|
+
`- role: ${candidate.role}`,
|
|
178
|
+
`- recommended_agent: ${candidate.recommended_agent}`,
|
|
179
|
+
`- reasoning_hint: ${candidate.reasoning_hint}`,
|
|
180
|
+
`- safe_to_parallelize: ${candidate.safe_to_parallelize}`,
|
|
181
|
+
`- reason: ${candidate.reason}`,
|
|
182
|
+
`- render_prompt_command: ${candidate.render_prompt_command}`,
|
|
183
|
+
"",
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return lines.join("\n").trimEnd();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isDirectRun() {
|
|
190
|
+
return process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
191
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { parseGoalStateText } from "../extend/local-goal-board/scripts/lib/goal-board.mjs";
|
|
6
|
+
|
|
7
|
+
const ROLE_DEFAULTS = {
|
|
8
|
+
scout: { agent: "goal_scout", reasoning: "low", sandbox: "read-only" },
|
|
9
|
+
judge: { agent: "goal_judge", reasoning: "high", sandbox: "read-only" },
|
|
10
|
+
worker: { agent: "goal_worker", reasoning: "low", sandbox: "workspace-write" },
|
|
11
|
+
pm: { agent: "PM", reasoning: "medium", sandbox: "workspace-write" },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
if (isDirectRun()) {
|
|
15
|
+
try {
|
|
16
|
+
const result = renderTaskPrompt(parseArgs(process.argv.slice(2)));
|
|
17
|
+
if (result.json) {
|
|
18
|
+
console.log(JSON.stringify(result.payload, null, 2));
|
|
19
|
+
} else {
|
|
20
|
+
console.log(formatPrompt(result.payload));
|
|
21
|
+
}
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error(error.message);
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function renderTaskPrompt(options) {
|
|
29
|
+
const boardPath = resolveBoardPath(options);
|
|
30
|
+
const board = loadBoard(boardPath);
|
|
31
|
+
const task = selectTask(board, options.taskId);
|
|
32
|
+
const role = normalizeRole(task.type);
|
|
33
|
+
const defaults = ROLE_DEFAULTS[role] || ROLE_DEFAULTS.pm;
|
|
34
|
+
const reasoning = normalizeReasoning(task.reasoning_hint, defaults.reasoning);
|
|
35
|
+
const warnings = promptWarnings(board, task);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
json: options.json,
|
|
39
|
+
payload: {
|
|
40
|
+
metadata: {
|
|
41
|
+
recommended_agent: defaults.agent,
|
|
42
|
+
recommended_reasoning: reasoning,
|
|
43
|
+
sandbox: defaults.sandbox,
|
|
44
|
+
fork_context_allowed: role !== "worker",
|
|
45
|
+
board_path: board.path,
|
|
46
|
+
child_board_paths: childBoardPaths(board),
|
|
47
|
+
warnings,
|
|
48
|
+
},
|
|
49
|
+
task: {
|
|
50
|
+
id: task.id,
|
|
51
|
+
type: role,
|
|
52
|
+
assignee: task.assignee || defaults.agent,
|
|
53
|
+
status: task.status,
|
|
54
|
+
objective: task.objective || "",
|
|
55
|
+
inputs: stringList(task.inputs),
|
|
56
|
+
constraints: stringList(task.constraints),
|
|
57
|
+
allowed_files: stringList(task.allowed_files),
|
|
58
|
+
verify: stringList(task.verify),
|
|
59
|
+
stop_if: stringList(task.stop_if),
|
|
60
|
+
reasoning_hint: task.reasoning_hint || null,
|
|
61
|
+
expected_output: stringList(task.expected_output),
|
|
62
|
+
},
|
|
63
|
+
receipt_schema: receiptSchema(role),
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function parseArgs(args) {
|
|
69
|
+
const options = { goalRoot: "", boardPath: "", taskId: "", json: false };
|
|
70
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
71
|
+
const arg = args[index];
|
|
72
|
+
if (arg === "--json") {
|
|
73
|
+
options.json = true;
|
|
74
|
+
} else if (arg === "--task") {
|
|
75
|
+
options.taskId = args[++index] || "";
|
|
76
|
+
} else if (arg.startsWith("--task=")) {
|
|
77
|
+
options.taskId = arg.slice("--task=".length);
|
|
78
|
+
} else if (arg === "--board") {
|
|
79
|
+
options.boardPath = args[++index] || "";
|
|
80
|
+
} else if (arg.startsWith("--board=")) {
|
|
81
|
+
options.boardPath = arg.slice("--board=".length);
|
|
82
|
+
} else if (arg === "--parallel-plan") {
|
|
83
|
+
options.parallelPlan = true;
|
|
84
|
+
} else if (arg.startsWith("-")) {
|
|
85
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
86
|
+
} else if (!options.goalRoot) {
|
|
87
|
+
options.goalRoot = arg;
|
|
88
|
+
} else {
|
|
89
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!options.goalRoot && !options.boardPath) {
|
|
93
|
+
throw new Error("Usage: goalbuddy prompt <goal-root> [--task T###] [--board path/to/state.yaml]");
|
|
94
|
+
}
|
|
95
|
+
return options;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function loadBoard(boardPath) {
|
|
99
|
+
if (!existsSync(boardPath)) throw new Error(`state file not found: ${boardPath}`);
|
|
100
|
+
const document = parseGoalStateText(readFileSync(boardPath, "utf8"));
|
|
101
|
+
if (!document || Number(document.version) !== 2) {
|
|
102
|
+
throw new Error(`unsupported GoalBuddy state version in ${boardPath}`);
|
|
103
|
+
}
|
|
104
|
+
if (!Array.isArray(document.tasks)) throw new Error(`state file has no tasks: ${boardPath}`);
|
|
105
|
+
return {
|
|
106
|
+
path: boardPath,
|
|
107
|
+
root: dirname(boardPath),
|
|
108
|
+
document,
|
|
109
|
+
tasks: document.tasks,
|
|
110
|
+
goal: document.goal || {},
|
|
111
|
+
activeTask: document.active_task || "",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function resolveBoardPath(options) {
|
|
116
|
+
const candidate = options.boardPath || options.goalRoot;
|
|
117
|
+
if (!candidate) throw new Error("Missing goal root or board path.");
|
|
118
|
+
const resolved = resolve(candidate);
|
|
119
|
+
if (basename(resolved) === "state.yaml") return resolved;
|
|
120
|
+
return resolve(resolved, "state.yaml");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function selectTask(board, taskId = "") {
|
|
124
|
+
const id = taskId || board.activeTask;
|
|
125
|
+
if (!id) throw new Error(`No task selected and active_task is empty in ${board.path}`);
|
|
126
|
+
const task = board.tasks.find((candidate) => candidate?.id === id);
|
|
127
|
+
if (!task) throw new Error(`Task ${id} not found in ${board.path}`);
|
|
128
|
+
return task;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function childBoardPaths(board) {
|
|
132
|
+
return board.tasks
|
|
133
|
+
.map((task) => task?.subgoal?.path)
|
|
134
|
+
.filter(Boolean)
|
|
135
|
+
.map((childPath) => resolve(board.root, childPath));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function promptWarnings(board, task) {
|
|
139
|
+
const warnings = [];
|
|
140
|
+
const role = normalizeRole(task.type);
|
|
141
|
+
if (task.id !== board.activeTask) warnings.push(`Task ${task.id} is not the active task on this board.`);
|
|
142
|
+
if (role === "worker") {
|
|
143
|
+
if (stringList(task.allowed_files).length === 0) warnings.push(`Worker task ${task.id} has no allowed_files.`);
|
|
144
|
+
if (stringList(task.verify).length === 0) warnings.push(`Worker task ${task.id} has no verify commands.`);
|
|
145
|
+
if (stringList(task.stop_if).length === 0) warnings.push(`Worker task ${task.id} has no stop_if conditions.`);
|
|
146
|
+
}
|
|
147
|
+
for (const candidate of board.tasks) {
|
|
148
|
+
if (candidate?.subgoal && Number(candidate.subgoal.depth) !== 1) {
|
|
149
|
+
warnings.push(`Task ${candidate.id} has subgoal.depth ${candidate.subgoal.depth || "<missing>"}; only depth 1 is supported.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return warnings;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeRole(value) {
|
|
156
|
+
const role = String(value || "pm").toLowerCase();
|
|
157
|
+
return ROLE_DEFAULTS[role] ? role : "pm";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizeReasoning(value, fallback) {
|
|
161
|
+
const hint = String(value || "").toLowerCase();
|
|
162
|
+
if (["low", "medium", "high", "xhigh"].includes(hint)) return hint;
|
|
163
|
+
return fallback;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function stringList(value) {
|
|
167
|
+
return Array.isArray(value) ? value.filter((item) => item !== null && item !== undefined).map(String) : [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function receiptSchema(role) {
|
|
171
|
+
if (role === "worker") {
|
|
172
|
+
return {
|
|
173
|
+
result: "done | blocked",
|
|
174
|
+
changed_files: [],
|
|
175
|
+
commands: [{ cmd: "<command>", status: "pass | fail | not_run" }],
|
|
176
|
+
summary: "<=120 words",
|
|
177
|
+
remaining_blockers: [],
|
|
178
|
+
needs_judge: false,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (role === "judge") {
|
|
182
|
+
return {
|
|
183
|
+
result: "done | blocked",
|
|
184
|
+
decision: "approve_next | reject_next | approve_subgoal | reject_subgoal | not_complete | complete",
|
|
185
|
+
evidence: [],
|
|
186
|
+
next_allowed_task: null,
|
|
187
|
+
blocked_tasks: [],
|
|
188
|
+
required_board_updates: [],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
result: "done | blocked",
|
|
193
|
+
summary: "<=120 words",
|
|
194
|
+
evidence: [],
|
|
195
|
+
facts: [],
|
|
196
|
+
contradictions: [],
|
|
197
|
+
ambiguity_requiring_judge: [],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function formatPrompt(payload) {
|
|
202
|
+
const lines = [
|
|
203
|
+
"GoalBuddy task prompt",
|
|
204
|
+
"",
|
|
205
|
+
"Metadata:",
|
|
206
|
+
`- recommended_agent: ${payload.metadata.recommended_agent}`,
|
|
207
|
+
`- recommended_reasoning: ${payload.metadata.recommended_reasoning}`,
|
|
208
|
+
`- sandbox: ${payload.metadata.sandbox}`,
|
|
209
|
+
`- fork_context_allowed: ${payload.metadata.fork_context_allowed}`,
|
|
210
|
+
`- board_path: ${payload.metadata.board_path}`,
|
|
211
|
+
];
|
|
212
|
+
if (payload.metadata.child_board_paths.length) {
|
|
213
|
+
lines.push("- child_board_paths:");
|
|
214
|
+
for (const path of payload.metadata.child_board_paths) lines.push(` - ${path}`);
|
|
215
|
+
}
|
|
216
|
+
if (payload.metadata.warnings.length) {
|
|
217
|
+
lines.push("- warnings:");
|
|
218
|
+
for (const warning of payload.metadata.warnings) lines.push(` - ${warning}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
lines.push(
|
|
222
|
+
"",
|
|
223
|
+
"Task:",
|
|
224
|
+
`- id: ${payload.task.id}`,
|
|
225
|
+
`- type: ${payload.task.type}`,
|
|
226
|
+
`- assignee: ${payload.task.assignee}`,
|
|
227
|
+
`- status: ${payload.task.status}`,
|
|
228
|
+
`- objective: ${payload.task.objective}`,
|
|
229
|
+
);
|
|
230
|
+
addList(lines, "inputs", payload.task.inputs);
|
|
231
|
+
addList(lines, "constraints", payload.task.constraints);
|
|
232
|
+
addList(lines, "allowed_files", payload.task.allowed_files);
|
|
233
|
+
addList(lines, "verify", payload.task.verify);
|
|
234
|
+
addList(lines, "stop_if", payload.task.stop_if);
|
|
235
|
+
addList(lines, "expected_output", payload.task.expected_output);
|
|
236
|
+
lines.push("", "Expected receipt JSON shape:", JSON.stringify(payload.receipt_schema, null, 2));
|
|
237
|
+
return lines.join("\n");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function addList(lines, label, values) {
|
|
241
|
+
if (!values.length) return;
|
|
242
|
+
lines.push(`- ${label}:`);
|
|
243
|
+
for (const value of values) lines.push(` - ${value}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isDirectRun() {
|
|
247
|
+
return process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
248
|
+
}
|
|
@@ -4,7 +4,7 @@ Use three generic agents. The main `/goal` thread remains PM and owns the board.
|
|
|
4
4
|
|
|
5
5
|
| Agent | model_reasoning_effort | sandbox_mode | Purpose |
|
|
6
6
|
|---|---:|---|---|
|
|
7
|
-
| goal_scout |
|
|
7
|
+
| goal_scout | low | read-only | Targeted evidence mapping and candidate facts |
|
|
8
8
|
| goal_worker | low | workspace-write | One bounded implementation/recovery task |
|
|
9
9
|
| goal_judge | high | read-only | Strategic review, escalation, completion skepticism |
|
|
10
10
|
|
|
@@ -44,5 +44,5 @@ Rules:
|
|
|
44
44
|
|
|
45
45
|
- Only the PM loop chooses active tasks, marks tasks done, or completes the goal.
|
|
46
46
|
- Keep at most one write-capable Worker active unless disjoint write scopes are explicit in `state.yaml`.
|
|
47
|
-
- Scout and Judge are read-only.
|
|
47
|
+
- Scout and Judge are read-only and safe to parallelize when their board inputs are clear.
|
|
48
48
|
- Judge is high thinking.
|
|
@@ -109,6 +109,14 @@ tasks:
|
|
|
109
109
|
- "Need files outside allowed_files."
|
|
110
110
|
- "Behavior is ambiguous."
|
|
111
111
|
- "Verification fails twice."
|
|
112
|
+
# Optional depth-1 child board promoted by PM when a task needs bounded branching work:
|
|
113
|
+
# subgoal:
|
|
114
|
+
# status: active # active | blocked | done
|
|
115
|
+
# path: subgoals/T003-child/state.yaml
|
|
116
|
+
# owner: Worker
|
|
117
|
+
# created_from: T003
|
|
118
|
+
# depth: 1
|
|
119
|
+
# rollup_receipt: null
|
|
112
120
|
receipt: null
|
|
113
121
|
- id: T999
|
|
114
122
|
type: judge
|