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
|
|
Binary file
|
|
@@ -52,6 +52,8 @@ const optionsWithValues = new Set([
|
|
|
52
52
|
"--port",
|
|
53
53
|
"--source",
|
|
54
54
|
"--target",
|
|
55
|
+
"--task",
|
|
56
|
+
"--board",
|
|
55
57
|
]);
|
|
56
58
|
|
|
57
59
|
const args = process.argv.slice(2);
|
|
@@ -116,6 +118,12 @@ async function main() {
|
|
|
116
118
|
case "board":
|
|
117
119
|
await board();
|
|
118
120
|
break;
|
|
121
|
+
case "prompt":
|
|
122
|
+
await prompt();
|
|
123
|
+
break;
|
|
124
|
+
case "parallel-plan":
|
|
125
|
+
await parallelPlan();
|
|
126
|
+
break;
|
|
119
127
|
case "help":
|
|
120
128
|
case "--help":
|
|
121
129
|
case "-h":
|
|
@@ -191,6 +199,8 @@ Usage:
|
|
|
191
199
|
${canonicalCliName} extend install --all [--catalog-url <url-or-path>] [--dry-run] [--force] [--json]
|
|
192
200
|
${canonicalCliName} extend doctor [<id>] [--codex-home <path>] [--json]
|
|
193
201
|
${canonicalCliName} board <docs/goals/slug> [--catalog-url <url-or-path>] [--host <host>] [--port <port>] [--once] [--json]
|
|
202
|
+
${canonicalCliName} prompt <docs/goals/slug> [--task T###] [--board <path/to/state.yaml>] [--json]
|
|
203
|
+
${canonicalCliName} parallel-plan <docs/goals/slug> [--json]
|
|
194
204
|
|
|
195
205
|
Targets: by default, install/update prepares both Codex (~/.codex) and Claude Code (~/.claude). Use --target codex or --target claude to limit the command.
|
|
196
206
|
|
|
@@ -713,13 +723,15 @@ function installPlugin({ quiet = false } = {}) {
|
|
|
713
723
|
throw new Error(`Failed to add Codex plugin marketplace: ${firstLine(marketplace.stderr || marketplace.stdout)}`);
|
|
714
724
|
}
|
|
715
725
|
|
|
726
|
+
const legacySkillPaths = legacyCodexSkillRoots();
|
|
716
727
|
const existingPluginSkillPath = installedPluginSkillRoot();
|
|
717
|
-
const preservedExtensions = preserveInstalledExtensions([existingPluginSkillPath], { tempRoot: dirname(pluginCachePath) });
|
|
728
|
+
const preservedExtensions = preserveInstalledExtensions([existingPluginSkillPath, ...legacySkillPaths], { tempRoot: dirname(pluginCachePath) });
|
|
718
729
|
mkdirSync(dirname(pluginCachePath), { recursive: true });
|
|
719
730
|
rmSync(pluginCachePath, { recursive: true, force: true });
|
|
720
731
|
cpSync(pluginSource, pluginCachePath, { recursive: true });
|
|
721
732
|
restoreInstalledExtensions(pluginSkillPath, preservedExtensions.tempPath);
|
|
722
733
|
cleanupPreservedExtensions([preservedExtensions.tempPath]);
|
|
734
|
+
const removedLegacySkillPaths = cleanupLegacyCodexSkills();
|
|
723
735
|
const configPath = enablePluginConfig();
|
|
724
736
|
|
|
725
737
|
const report = {
|
|
@@ -732,6 +744,7 @@ function installPlugin({ quiet = false } = {}) {
|
|
|
732
744
|
cache_path: pluginCachePath,
|
|
733
745
|
config_path: configPath,
|
|
734
746
|
preserved_extensions: preservedExtensions.ids,
|
|
747
|
+
removed_legacy_skill_paths: removedLegacySkillPaths,
|
|
735
748
|
};
|
|
736
749
|
|
|
737
750
|
if (hasFlag("--json") && !quiet) {
|
|
@@ -748,6 +761,9 @@ function installPlugin({ quiet = false } = {}) {
|
|
|
748
761
|
if (report.preserved_extensions.length) {
|
|
749
762
|
console.log(`Preserved extensions: ${report.preserved_extensions.join(", ")}`);
|
|
750
763
|
}
|
|
764
|
+
if (report.removed_legacy_skill_paths.length) {
|
|
765
|
+
console.log(`Removed legacy personal skills: ${report.removed_legacy_skill_paths.join(", ")}`);
|
|
766
|
+
}
|
|
751
767
|
console.log("");
|
|
752
768
|
console.log("Restart Codex, then use:");
|
|
753
769
|
console.log(` $${canonicalSkillName}`);
|
|
@@ -758,6 +774,20 @@ function installPlugin({ quiet = false } = {}) {
|
|
|
758
774
|
return report;
|
|
759
775
|
}
|
|
760
776
|
|
|
777
|
+
function legacyCodexSkillRoots() {
|
|
778
|
+
return [installedSkillRoot(), legacyInstalledSkillRoot()];
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function cleanupLegacyCodexSkills() {
|
|
782
|
+
const removed = [];
|
|
783
|
+
for (const path of legacyCodexSkillRoots()) {
|
|
784
|
+
if (!existsSync(path)) continue;
|
|
785
|
+
rmSync(path, { recursive: true, force: true });
|
|
786
|
+
removed.push(path);
|
|
787
|
+
}
|
|
788
|
+
return removed;
|
|
789
|
+
}
|
|
790
|
+
|
|
761
791
|
function pluginCacheRoot(version) {
|
|
762
792
|
return join(codexHome(), "plugins", "cache", pluginName, pluginName, version);
|
|
763
793
|
}
|
|
@@ -941,6 +971,39 @@ async function board() {
|
|
|
941
971
|
process.exit(result.status ?? 1);
|
|
942
972
|
}
|
|
943
973
|
|
|
974
|
+
async function prompt() {
|
|
975
|
+
if (hasFlag("--parallel-plan")) {
|
|
976
|
+
await parallelPlan();
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const script = join(skillSource, "scripts", "render-task-prompt.mjs");
|
|
981
|
+
const scriptArgs = [script, ...args.slice(1)];
|
|
982
|
+
const result = spawnSync(process.execPath, scriptArgs, {
|
|
983
|
+
cwd: packageRoot,
|
|
984
|
+
encoding: "utf8",
|
|
985
|
+
env: process.env,
|
|
986
|
+
});
|
|
987
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
988
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
989
|
+
if (result.error) throw result.error;
|
|
990
|
+
process.exit(result.status ?? 1);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
async function parallelPlan() {
|
|
994
|
+
const script = join(skillSource, "scripts", "parallel-plan.mjs");
|
|
995
|
+
const scriptArgs = [script, ...args.slice(1).filter((arg) => arg !== "--parallel-plan")];
|
|
996
|
+
const result = spawnSync(process.execPath, scriptArgs, {
|
|
997
|
+
cwd: packageRoot,
|
|
998
|
+
encoding: "utf8",
|
|
999
|
+
env: process.env,
|
|
1000
|
+
});
|
|
1001
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
1002
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
1003
|
+
if (result.error) throw result.error;
|
|
1004
|
+
process.exit(result.status ?? 1);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
944
1007
|
async function ensureLocalBoardExtension() {
|
|
945
1008
|
const id = "local-goal-board";
|
|
946
1009
|
const script = join(extensionTarget(id), "scripts", "local-goal-board.mjs");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "goalbuddy",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "A /goal operating system for Codex and Claude Code:
|
|
3
|
+
"version": "0.3.5",
|
|
4
|
+
"description": "A /goal operating system for Codex and Claude Code: subgoals, parallel-agent-ready boards, dark mode, receipts, and verification.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"goalbuddy": "internal/cli/goal-maker.mjs",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
".agents/plugins/marketplace.json",
|
|
12
12
|
"README.md",
|
|
13
|
+
"RELEASE-0.3.5.md",
|
|
13
14
|
"CONTRIBUTING.md",
|
|
14
15
|
"examples",
|
|
15
16
|
"plugins/goalbuddy",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "goalbuddy",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Turn broad Claude Code work into verified GoalBuddy boards with
|
|
3
|
+
"version": "0.3.5",
|
|
4
|
+
"description": "Turn broad Claude Code work into verified GoalBuddy boards with subgoals, parallel-agent-ready handoffs, dark mode, and receipts.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tolibear",
|
|
7
7
|
"email": "support@tolibear.com",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "goalbuddy",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Turn broad Codex and Claude Code work into verified GoalBuddy boards with
|
|
3
|
+
"version": "0.3.5",
|
|
4
|
+
"description": "Turn broad Codex and Claude Code work into verified GoalBuddy boards with subgoals, parallel-agent-ready handoffs, dark mode, and receipts.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tolibear",
|
|
7
7
|
"email": "support@tolibear.com",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"skills": "./skills/",
|
|
25
25
|
"interface": {
|
|
26
26
|
"displayName": "GoalBuddy",
|
|
27
|
-
"shortDescription": "
|
|
28
|
-
"longDescription": "GoalBuddy packages a structured goal workflow for broad, long-running, or ambiguous engineering work in Codex or Claude Code. It creates durable goal charters, task boards, visual board surfaces, receipts, verification gates, extension handoffs, and compatibility guidance for teams moving from goal-maker.",
|
|
27
|
+
"shortDescription": "Verified goal boards with subgoals, parallel-agent-ready handoffs, and dark mode",
|
|
28
|
+
"longDescription": "GoalBuddy packages a structured goal workflow for broad, long-running, or ambiguous engineering work in Codex or Claude Code. It creates durable goal charters, task boards, optional depth-1 subgoals, visual board surfaces, parallel-agent-ready handoffs, receipts, verification gates, extension handoffs, and compatibility guidance for teams moving from goal-maker.",
|
|
29
29
|
"developerName": "tolibear",
|
|
30
30
|
"category": "Coding",
|
|
31
31
|
"capabilities": [
|
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
GoalBuddy packages the canonical `goal-prep` skill as a plugin so teams can install the reusable workflow in **Codex** and **Claude Code**, while keeping the npm CLI for local setup, doctor checks, and extension management.
|
|
4
4
|
|
|
5
|
+
Version 0.3.5 is the Subgoals, Parallel Agents, and Dark Mode release: depth-1 child boards, a shared multi-board local hub, readable dark-mode boards, stricter agent contracts, deterministic prompt rendering, and conservative parallel planning for long-running work.
|
|
6
|
+
|
|
5
7
|
## What It Contains
|
|
6
8
|
|
|
7
9
|
- `.codex-plugin/plugin.json`: Codex plugin manifest and Codex UI copy.
|
|
8
10
|
- `.claude-plugin/plugin.json`: Claude Code plugin manifest.
|
|
9
11
|
- `skills/goalbuddy/`: the installable GoalBuddy skill payload (shared by both platforms).
|
|
10
12
|
- `agents/`: Claude Code subagent definitions (`goal-scout.md`, `goal-judge.md`, `goal-worker.md`).
|
|
11
|
-
- `
|
|
13
|
+
- `skills/goalbuddy/SKILL.md`: canonical `$goal-prep` / `/goal-prep` entry point.
|
|
12
14
|
- `assets/goalbuddy-icon.svg`: lightweight plugin icon.
|
|
13
15
|
|
|
14
16
|
## Local Testing
|
|
@@ -27,7 +29,7 @@ npx goalbuddy check-update
|
|
|
27
29
|
npx goalbuddy
|
|
28
30
|
```
|
|
29
31
|
|
|
30
|
-
This installs and enables the native Codex plugin in `~/.codex/`, then installs the GoalBuddy skill
|
|
32
|
+
This installs and enables the native Codex plugin in `~/.codex/`, then installs the GoalBuddy skill and Scout/Judge/Worker subagents into `~/.claude/`. The skill surfaces `/goal-prep` in Claude Code.
|
|
31
33
|
|
|
32
34
|
## Install One Target
|
|
33
35
|
|
|
@@ -36,7 +38,7 @@ npx goalbuddy --target codex
|
|
|
36
38
|
npx goalbuddy --target claude
|
|
37
39
|
```
|
|
38
40
|
|
|
39
|
-
This installs the GoalBuddy skill
|
|
41
|
+
This installs the GoalBuddy skill and the three Scout/Judge/Worker subagents into `~/.claude/`. Restart Claude Code, then run:
|
|
40
42
|
|
|
41
43
|
```text
|
|
42
44
|
/goal-prep
|