goalbuddy 0.3.1 → 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.
Files changed (52) hide show
  1. package/README.md +58 -180
  2. package/RELEASE-0.3.5.md +324 -0
  3. package/goalbuddy/SKILL.md +8 -2
  4. package/goalbuddy/agents/goal_judge.toml +29 -17
  5. package/goalbuddy/agents/goal_scout.toml +34 -14
  6. package/goalbuddy/agents/goal_worker.toml +32 -15
  7. package/goalbuddy/extend/local-goal-board/README.md +8 -4
  8. package/goalbuddy/extend/local-goal-board/examples/subgoal-parent/goal.md +3 -0
  9. package/goalbuddy/extend/local-goal-board/examples/subgoal-parent/notes/.gitkeep +1 -0
  10. package/goalbuddy/extend/local-goal-board/examples/subgoal-parent/state.yaml +60 -0
  11. package/goalbuddy/extend/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/goal.md +3 -0
  12. package/goalbuddy/extend/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/notes/.gitkeep +1 -0
  13. package/goalbuddy/extend/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/state.yaml +52 -0
  14. package/goalbuddy/extend/local-goal-board/extension.yaml +6 -4
  15. package/goalbuddy/extend/local-goal-board/scripts/lib/goal-board.mjs +940 -24
  16. package/goalbuddy/extend/local-goal-board/scripts/local-goal-board.mjs +389 -54
  17. package/goalbuddy/extend/local-goal-board/test/local-goal-board.test.mjs +420 -4
  18. package/goalbuddy/scripts/check-goal-state.mjs +116 -6
  19. package/goalbuddy/scripts/parallel-plan.mjs +191 -0
  20. package/goalbuddy/scripts/render-task-prompt.mjs +248 -0
  21. package/goalbuddy/templates/agents.md +2 -2
  22. package/goalbuddy/templates/state.yaml +8 -0
  23. package/internal/assets/goalbuddy-v0.3.0-release.png +0 -0
  24. package/internal/assets/goalbuddy-v0.3.5-release.png +0 -0
  25. package/internal/cli/goal-maker.mjs +70 -2
  26. package/package.json +3 -2
  27. package/plugins/goalbuddy/.claude-plugin/plugin.json +2 -2
  28. package/plugins/goalbuddy/.codex-plugin/plugin.json +4 -4
  29. package/plugins/goalbuddy/README.md +5 -3
  30. package/plugins/goalbuddy/agents/goal-judge.md +31 -16
  31. package/plugins/goalbuddy/agents/goal-scout.md +38 -13
  32. package/plugins/goalbuddy/agents/goal-worker.md +35 -14
  33. package/plugins/goalbuddy/skills/goalbuddy/SKILL.md +8 -2
  34. package/plugins/goalbuddy/skills/goalbuddy/agents/goal_judge.toml +29 -17
  35. package/plugins/goalbuddy/skills/goalbuddy/agents/goal_scout.toml +34 -14
  36. package/plugins/goalbuddy/skills/goalbuddy/agents/goal_worker.toml +32 -15
  37. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/README.md +8 -4
  38. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/subgoal-parent/goal.md +3 -0
  39. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/subgoal-parent/notes/.gitkeep +1 -0
  40. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/subgoal-parent/state.yaml +60 -0
  41. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/goal.md +3 -0
  42. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/notes/.gitkeep +1 -0
  43. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/state.yaml +52 -0
  44. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/extension.yaml +6 -4
  45. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/scripts/lib/goal-board.mjs +940 -24
  46. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/scripts/local-goal-board.mjs +389 -54
  47. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/test/local-goal-board.test.mjs +420 -4
  48. package/plugins/goalbuddy/skills/goalbuddy/scripts/check-goal-state.mjs +116 -6
  49. package/plugins/goalbuddy/skills/goalbuddy/scripts/parallel-plan.mjs +191 -0
  50. package/plugins/goalbuddy/skills/goalbuddy/scripts/render-task-prompt.mjs +248 -0
  51. package/plugins/goalbuddy/skills/goalbuddy/templates/agents.md +2 -2
  52. 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 | medium | read-only | Evidence mapping and candidate tasks |
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
@@ -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");
@@ -1240,6 +1303,7 @@ function installedPluginSkillRoot() {
1240
1303
  const versions = readdirSync(root, { withFileTypes: true })
1241
1304
  .filter((entry) => entry.isDirectory())
1242
1305
  .map((entry) => entry.name)
1306
+ .filter(isSupportedVersion)
1243
1307
  .sort(compareVersions)
1244
1308
  .reverse();
1245
1309
  for (const version of versions) {
@@ -1403,9 +1467,9 @@ function preserveInstalledExtensions(targets, { tempRoot = "" } = {}) {
1403
1467
  if (!target) continue;
1404
1468
  const source = join(target, "extend");
1405
1469
  if (!existsSync(source)) continue;
1406
- mkdirSync(tempPath, { recursive: true });
1407
1470
  for (const entry of readdirSync(source, { withFileTypes: true })) {
1408
1471
  if (bundledCoreExtensionIds.has(entry.name)) continue;
1472
+ mkdirSync(tempPath, { recursive: true });
1409
1473
  const from = join(source, entry.name);
1410
1474
  const to = join(tempPath, entry.name);
1411
1475
  cpSync(from, to, { recursive: true, force: true });
@@ -1639,6 +1703,10 @@ function normalizeVersion(value) {
1639
1703
  return `${Number(match[1])}.${Number(match[2])}.${Number(match[3])}`;
1640
1704
  }
1641
1705
 
1706
+ function isSupportedVersion(value) {
1707
+ return /^v?\d+\.\d+\.\d+(?:[-+].*)?$/.test(String(value).trim());
1708
+ }
1709
+
1642
1710
  function compareVersions(left, right) {
1643
1711
  const leftParts = normalizeVersion(left).split(".").map((part) => Number.parseInt(part, 10) || 0);
1644
1712
  const rightParts = normalizeVersion(right).split(".").map((part) => Number.parseInt(part, 10) || 0);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "goalbuddy",
3
- "version": "0.3.1",
4
- "description": "A /goal operating system for Codex and Claude Code: Scout/Judge/Worker boards with visual board surfaces, receipts, and verification.",
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.1",
4
- "description": "Turn broad Claude Code work into verified GoalBuddy boards with Scout, Judge, Worker, visual boards, and receipts.",
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.1",
4
- "description": "Turn broad Codex and Claude Code work into verified GoalBuddy boards with Scout, Judge, Worker, visual boards, and receipts.",
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": "Turn broad Codex or Claude Code work into verified Scout/Judge/Worker boards",
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": [