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.
- package/README.md +58 -180
- 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.0-release.png +0 -0
- package/internal/assets/goalbuddy-v0.3.5-release.png +0 -0
- package/internal/cli/goal-maker.mjs +70 -2
- 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
|
|
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");
|
|
@@ -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.
|
|
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": [
|