goalbuddy 0.2.10
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/CONTRIBUTING.md +45 -0
- package/LICENSE +21 -0
- package/README.md +215 -0
- package/examples/extend-catalog-workflow/goal.md +53 -0
- package/examples/extend-catalog-workflow/notes/T001-extension-model-map.md +47 -0
- package/examples/extend-catalog-workflow/notes/T002-architecture-decision.md +48 -0
- package/examples/extend-catalog-workflow/notes/T003-implementation-summary.md +43 -0
- package/examples/extend-catalog-workflow/notes/T004-root-extend-folder.md +24 -0
- package/examples/extend-catalog-workflow/notes/T005-layout-cleanup.md +46 -0
- package/examples/extend-catalog-workflow/notes/T006-catalog-location.md +50 -0
- package/examples/extend-catalog-workflow/notes/T999-completion-audit.md +36 -0
- package/examples/extend-catalog-workflow/state.yaml +327 -0
- package/examples/github-pr-workflow-extension/pr-handoff.md +46 -0
- package/examples/improve-goal-maker/goal.md +51 -0
- package/examples/improve-goal-maker/notes/T001-repo-map.md +59 -0
- package/examples/improve-goal-maker/notes/T002-risk-map.md +37 -0
- package/examples/improve-goal-maker/state.yaml +224 -0
- package/goal-maker/SKILL.md +18 -0
- package/goal-maker/agents/README.md +23 -0
- package/goal-maker/agents/config-snippet.toml +5 -0
- package/goal-maker/agents/goal_judge.toml +29 -0
- package/goal-maker/agents/goal_scout.toml +26 -0
- package/goal-maker/agents/goal_worker.toml +28 -0
- package/goal-maker/agents/openai.yaml +6 -0
- package/goal-maker/scripts/check-goal-state.mjs +370 -0
- package/goal-maker/scripts/install-agents.mjs +28 -0
- package/goal-maker/templates/agents.md +48 -0
- package/goal-maker/templates/goal-prompt.txt +1 -0
- package/goal-maker/templates/goal.md +71 -0
- package/goal-maker/templates/note.md +22 -0
- package/goal-maker/templates/state.yaml +125 -0
- package/goalbuddy/SKILL.md +484 -0
- package/goalbuddy/agents/README.md +23 -0
- package/goalbuddy/agents/config-snippet.toml +5 -0
- package/goalbuddy/agents/goal_judge.toml +29 -0
- package/goalbuddy/agents/goal_scout.toml +26 -0
- package/goalbuddy/agents/goal_worker.toml +28 -0
- package/goalbuddy/agents/openai.yaml +6 -0
- package/goalbuddy/scripts/check-goal-state.mjs +370 -0
- package/goalbuddy/scripts/install-agents.mjs +28 -0
- package/goalbuddy/templates/agents.md +48 -0
- package/goalbuddy/templates/goal-prompt.txt +1 -0
- package/goalbuddy/templates/goal.md +71 -0
- package/goalbuddy/templates/note.md +22 -0
- package/goalbuddy/templates/state.yaml +125 -0
- package/internal/assets/extend-release.png +0 -0
- package/internal/assets/extend-release.svg +83 -0
- package/internal/assets/goal-maker-flow.png +0 -0
- package/internal/cli/check-publish-version.mjs +86 -0
- package/internal/cli/goal-maker.mjs +1061 -0
- package/package.json +65 -0
- package/plugins/goalbuddy/.codex-plugin/plugin.json +48 -0
- package/plugins/goalbuddy/README.md +29 -0
- package/plugins/goalbuddy/assets/goalbuddy-icon.svg +8 -0
- package/plugins/goalbuddy/skills/goalbuddy/SKILL.md +484 -0
- package/plugins/goalbuddy/skills/goalbuddy/agents/README.md +23 -0
- package/plugins/goalbuddy/skills/goalbuddy/agents/config-snippet.toml +5 -0
- package/plugins/goalbuddy/skills/goalbuddy/agents/goal_judge.toml +29 -0
- package/plugins/goalbuddy/skills/goalbuddy/agents/goal_scout.toml +26 -0
- package/plugins/goalbuddy/skills/goalbuddy/agents/goal_worker.toml +28 -0
- package/plugins/goalbuddy/skills/goalbuddy/agents/openai.yaml +6 -0
- package/plugins/goalbuddy/skills/goalbuddy/scripts/check-goal-state.mjs +370 -0
- package/plugins/goalbuddy/skills/goalbuddy/scripts/install-agents.mjs +28 -0
- package/plugins/goalbuddy/skills/goalbuddy/templates/agents.md +48 -0
- package/plugins/goalbuddy/skills/goalbuddy/templates/goal-prompt.txt +1 -0
- package/plugins/goalbuddy/skills/goalbuddy/templates/goal.md +71 -0
- package/plugins/goalbuddy/skills/goalbuddy/templates/note.md +22 -0
- package/plugins/goalbuddy/skills/goalbuddy/templates/state.yaml +125 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name = "goal_judge"
|
|
2
|
+
description = "High-thinking strategic reviewer for GoalBuddy escalation: ambiguity, risky scope, source/product conflicts, safety/API/live decisions, and tranche completion."
|
|
3
|
+
model_reasoning_effort = "high"
|
|
4
|
+
sandbox_mode = "read-only"
|
|
5
|
+
nickname_candidates = ["Judge", "Reviewer", "Architect"]
|
|
6
|
+
|
|
7
|
+
developer_instructions = """
|
|
8
|
+
You are Judge for GoalBuddy.
|
|
9
|
+
|
|
10
|
+
Thinking level: high.
|
|
11
|
+
Mode: strategic reviewer and escalation authority.
|
|
12
|
+
|
|
13
|
+
Think as a skeptical staff engineer and project-management systems designer. You decide and constrain; you do not broadly implement.
|
|
14
|
+
|
|
15
|
+
Use when source, tests, product behavior, API/live strategy, dirty scope, giant-file risk, safety/auth/money/persistence semantics, task priority, or completion readiness is ambiguous.
|
|
16
|
+
|
|
17
|
+
Do not approve based on lots of docs or lots of tests. Require coherent receipts and current verification.
|
|
18
|
+
|
|
19
|
+
Return a compact Judge receipt for the PM to paste into state.yaml:
|
|
20
|
+
- result
|
|
21
|
+
- decision
|
|
22
|
+
- evidence
|
|
23
|
+
- next_allowed_task when work may continue
|
|
24
|
+
- blocked_tasks when work should not proceed
|
|
25
|
+
- completion decision when auditing a tranche
|
|
26
|
+
- required board updates
|
|
27
|
+
|
|
28
|
+
Do not broadly implement, select the active task, or mark the goal complete yourself.
|
|
29
|
+
"""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name = "goal_scout"
|
|
2
|
+
description = "Read-only evidence mapper for one GoalBuddy task. Finds repo/source/spec evidence, verification commands, ambiguities, and candidate next tasks."
|
|
3
|
+
model_reasoning_effort = "medium"
|
|
4
|
+
sandbox_mode = "read-only"
|
|
5
|
+
nickname_candidates = ["Scout", "Mapper", "Tracer"]
|
|
6
|
+
|
|
7
|
+
developer_instructions = """
|
|
8
|
+
You are Scout for GoalBuddy.
|
|
9
|
+
|
|
10
|
+
Thinking level: medium.
|
|
11
|
+
Mode: read-only evidence mapping.
|
|
12
|
+
|
|
13
|
+
You are read-only. Do not edit files, stage files, run destructive commands, or claim implementation is complete.
|
|
14
|
+
|
|
15
|
+
Given one active Scout task, map repo/source/spec evidence, verification commands, health signals, improvement candidates, target files, tests, and unresolved ambiguity.
|
|
16
|
+
|
|
17
|
+
Return a compact Scout receipt for the PM to paste into state.yaml:
|
|
18
|
+
- result
|
|
19
|
+
- summary
|
|
20
|
+
- evidence paths
|
|
21
|
+
- note path if findings are too large for the task card
|
|
22
|
+
- spawned_tasks when useful
|
|
23
|
+
- ambiguity requiring Judge
|
|
24
|
+
|
|
25
|
+
Do not select the active task or mark the goal complete.
|
|
26
|
+
"""
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name = "goal_worker"
|
|
2
|
+
description = "Low-thinking bounded implementer for exactly one GoalBuddy Worker task with allowed files, verification commands, and stop conditions."
|
|
3
|
+
model_reasoning_effort = "low"
|
|
4
|
+
sandbox_mode = "workspace-write"
|
|
5
|
+
nickname_candidates = ["Worker", "Patch", "Fixer"]
|
|
6
|
+
|
|
7
|
+
developer_instructions = """
|
|
8
|
+
You are Worker for GoalBuddy.
|
|
9
|
+
|
|
10
|
+
Thinking level: low.
|
|
11
|
+
Mode: one bounded writer.
|
|
12
|
+
|
|
13
|
+
Execute exactly one active Worker task. Do not broaden scope.
|
|
14
|
+
|
|
15
|
+
You may edit only the task's allowed_files. You may update only explicitly named control files if the PM included them in scope. Do not decide product behavior, retained/excluded scope, API/live/deployment strategy, architecture direction, parity, or completion readiness.
|
|
16
|
+
|
|
17
|
+
Stop immediately if required evidence is missing, files outside scope are needed, source/tests/product conflict, verification fails twice, or the diff exceeds the task budget.
|
|
18
|
+
|
|
19
|
+
Return a compact Worker receipt for the PM to paste into state.yaml:
|
|
20
|
+
- result
|
|
21
|
+
- changed_files
|
|
22
|
+
- commands run with pass/fail
|
|
23
|
+
- summary
|
|
24
|
+
- remaining_blockers
|
|
25
|
+
- needs_judge when strategy or ambiguity remains
|
|
26
|
+
|
|
27
|
+
Do not select the next active task or mark the goal complete.
|
|
28
|
+
"""
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "GoalBuddy"
|
|
3
|
+
short_description: "Diagnose intent before making reliable goal boards."
|
|
4
|
+
default_prompt: "Use goalbuddy to run diagnostic intake on my rough or detailed goal. If the goal is vague or improvement-oriented, ask one guided question at a time, surface likely blind spots, and do not create files until intent, proof, scope, and board handling are clear or explicitly defaulted. If it is clear or already planned, prepare or repair goal.md, state.yaml, and notes/, then print the /goal Follow command instead of starting /goal automatically."
|
|
5
|
+
policy:
|
|
6
|
+
allow_implicit_invocation: true
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const statePath = process.argv[2];
|
|
6
|
+
|
|
7
|
+
if (!statePath) {
|
|
8
|
+
console.error("Usage: node scripts/check-goal-state.mjs docs/goals/<slug>/state.yaml");
|
|
9
|
+
process.exit(2);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!existsSync(statePath)) {
|
|
13
|
+
console.error(JSON.stringify({ ok: false, errors: [`state file not found: ${statePath}`], warnings: [] }, null, 2));
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const root = dirname(statePath);
|
|
18
|
+
const text = readFileSync(statePath, "utf8");
|
|
19
|
+
const errors = [];
|
|
20
|
+
const warnings = [];
|
|
21
|
+
|
|
22
|
+
function clean(value) {
|
|
23
|
+
if (value === undefined || value === null) return null;
|
|
24
|
+
const cleaned = value.replace(/#.*/, "").trim().replace(/^[\'\"]|[\'\"]$/g, "");
|
|
25
|
+
if (cleaned === "" || cleaned === "null") return null;
|
|
26
|
+
if (cleaned === "true") return true;
|
|
27
|
+
if (cleaned === "false") return false;
|
|
28
|
+
if (/^\d+$/.test(cleaned)) return Number(cleaned);
|
|
29
|
+
return cleaned;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function topScalar(key) {
|
|
33
|
+
const match = text.match(new RegExp(`^${key}:\\s*(.*?)\\s*$`, "m"));
|
|
34
|
+
return match ? clean(match[1]) : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function nestedScalar(section, key) {
|
|
38
|
+
const lines = text.split(/\r?\n/);
|
|
39
|
+
let inSection = false;
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
if (new RegExp(`^${section}:\\s*$`).test(line)) {
|
|
42
|
+
inSection = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (inSection && /^\S/.test(line)) break;
|
|
46
|
+
if (inSection) {
|
|
47
|
+
const match = line.match(new RegExp(`^\\s{2}${key}:\\s*(.*?)\\s*$`));
|
|
48
|
+
if (match) return clean(match[1]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sectionText(section) {
|
|
55
|
+
const lines = text.split(/\r?\n/);
|
|
56
|
+
const start = lines.findIndex((line) => new RegExp(`^${section}:\\s*$`).test(line));
|
|
57
|
+
if (start === -1) return "";
|
|
58
|
+
const collected = [];
|
|
59
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
60
|
+
if (/^\S/.test(lines[i])) break;
|
|
61
|
+
collected.push(lines[i]);
|
|
62
|
+
}
|
|
63
|
+
return collected.join("\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseTasks() {
|
|
67
|
+
const body = sectionText("tasks");
|
|
68
|
+
if (!body) return [];
|
|
69
|
+
const lines = body.split(/\r?\n/);
|
|
70
|
+
const tasks = [];
|
|
71
|
+
let current = null;
|
|
72
|
+
let currentLines = [];
|
|
73
|
+
|
|
74
|
+
function finish() {
|
|
75
|
+
if (!current) return;
|
|
76
|
+
current.raw = currentLines.join("\n");
|
|
77
|
+
tasks.push(current);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
const idMatch = line.match(/^\s{2}-\s+id:\s*(.+?)\s*$/);
|
|
82
|
+
if (idMatch) {
|
|
83
|
+
finish();
|
|
84
|
+
current = { id: clean(idMatch[1]) };
|
|
85
|
+
currentLines = [line];
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (current) currentLines.push(line);
|
|
89
|
+
}
|
|
90
|
+
finish();
|
|
91
|
+
return tasks.map((task) => ({
|
|
92
|
+
...task,
|
|
93
|
+
type: taskScalar(task, "type"),
|
|
94
|
+
assignee: taskScalar(task, "assignee"),
|
|
95
|
+
status: taskScalar(task, "status"),
|
|
96
|
+
objective: taskScalar(task, "objective"),
|
|
97
|
+
allowedFiles: taskList(task, "allowed_files"),
|
|
98
|
+
verify: taskList(task, "verify"),
|
|
99
|
+
stopIf: taskList(task, "stop_if"),
|
|
100
|
+
receipt: taskReceipt(task),
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function taskScalar(task, key) {
|
|
105
|
+
const match = task.raw.match(new RegExp(`^\\s{4}${key}:\\s*(.*?)\\s*$`, "m"));
|
|
106
|
+
return match ? clean(match[1]) : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function taskList(task, key) {
|
|
110
|
+
const lines = task.raw.split(/\r?\n/);
|
|
111
|
+
const start = lines.findIndex((line) => new RegExp(`^\\s{4}${key}:\\s*$`).test(line));
|
|
112
|
+
if (start === -1) return [];
|
|
113
|
+
const values = [];
|
|
114
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
115
|
+
if (/^\s{4}\S/.test(lines[i])) break;
|
|
116
|
+
const item = lines[i].match(/^\s{6}-\s*(.+?)\s*$/);
|
|
117
|
+
if (item) values.push(clean(item[1]));
|
|
118
|
+
}
|
|
119
|
+
return values.filter((value) => value !== null);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function taskReceipt(task) {
|
|
123
|
+
const lines = task.raw.split(/\r?\n/);
|
|
124
|
+
const start = lines.findIndex((line) => /^\s{4}receipt:\s*/.test(line));
|
|
125
|
+
if (start === -1) return { present: false, value: null, raw: "" };
|
|
126
|
+
|
|
127
|
+
const inline = clean(lines[start].replace(/^\s{4}receipt:\s*/, ""));
|
|
128
|
+
if (inline === null && !/^(\s{6}|\s{8})/.test(lines[start + 1] || "")) {
|
|
129
|
+
return { present: true, value: null, raw: "" };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const receiptLines = [];
|
|
133
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
134
|
+
if (/^\s{4}\S/.test(lines[i])) break;
|
|
135
|
+
receiptLines.push(lines[i]);
|
|
136
|
+
}
|
|
137
|
+
const raw = receiptLines.join("\n");
|
|
138
|
+
return {
|
|
139
|
+
present: true,
|
|
140
|
+
value: inline || "object",
|
|
141
|
+
raw,
|
|
142
|
+
has: (key) => new RegExp(`^\\s{6}${key}:`, "m").test(raw),
|
|
143
|
+
list: (key) => receiptList(raw, key),
|
|
144
|
+
commandStatuses: () => receiptCommandStatuses(raw),
|
|
145
|
+
scalar: (key) => {
|
|
146
|
+
const match = raw.match(new RegExp(`^\\s{6}${key}:\\s*(.*?)\\s*$`, "m"));
|
|
147
|
+
return match ? clean(match[1]) : null;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function receiptList(raw, key) {
|
|
153
|
+
const lines = raw.split(/\r?\n/);
|
|
154
|
+
const start = lines.findIndex((line) => new RegExp(`^\\s{6}${key}:\\s*$`).test(line));
|
|
155
|
+
if (start === -1) return [];
|
|
156
|
+
const values = [];
|
|
157
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
158
|
+
if (/^\s{6}\S/.test(lines[i])) break;
|
|
159
|
+
const item = lines[i].match(/^\s{8}-\s*(.+?)\s*$/);
|
|
160
|
+
if (item) values.push(clean(item[1]));
|
|
161
|
+
}
|
|
162
|
+
return values.filter((value) => value !== null);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function receiptCommandStatuses(raw) {
|
|
166
|
+
return [...raw.matchAll(/^\s{10}status:\s*(.*?)\s*$/gm)]
|
|
167
|
+
.map((match) => clean(match[1]))
|
|
168
|
+
.filter((value) => value !== null);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function rootEntryErrors() {
|
|
172
|
+
const allowed = new Set(["goal.md", "state.yaml", "notes"]);
|
|
173
|
+
const unexpected = [];
|
|
174
|
+
for (const entry of readdirSync(root).filter((item) => item !== ".DS_Store")) {
|
|
175
|
+
const path = join(root, entry);
|
|
176
|
+
const stats = statSync(path);
|
|
177
|
+
if (!allowed.has(entry)) {
|
|
178
|
+
unexpected.push(entry);
|
|
179
|
+
} else if (entry === "notes" && !stats.isDirectory()) {
|
|
180
|
+
unexpected.push("notes (must be a directory)");
|
|
181
|
+
} else if (entry !== "notes" && !stats.isFile()) {
|
|
182
|
+
unexpected.push(`${entry} (must be a file)`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return unexpected;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const version = topScalar("version");
|
|
189
|
+
const goalStatus = nestedScalar("goal", "status");
|
|
190
|
+
const activeTask = topScalar("active_task");
|
|
191
|
+
const installedAgents = ["scout", "worker", "judge"].map((agent) => ({
|
|
192
|
+
agent,
|
|
193
|
+
status: nestedScalar("agents", agent),
|
|
194
|
+
}));
|
|
195
|
+
const continuousUntilFullOutcome = nestedScalar("rules", "continuous_until_full_outcome") === true;
|
|
196
|
+
const missingInputOrCredentialsDoNotStopGoal =
|
|
197
|
+
nestedScalar("rules", "missing_input_or_credentials_do_not_stop_goal") === true;
|
|
198
|
+
const legacySignals = [
|
|
199
|
+
/^gate:\s*$/m,
|
|
200
|
+
/^artifact_policy:\s*$/m,
|
|
201
|
+
/^active_unit:/m,
|
|
202
|
+
/^evidence\.jsonl/m,
|
|
203
|
+
].some((pattern) => pattern.test(text)) || ["units", "artifacts", "evidence.jsonl"].some((entry) => existsSync(join(root, entry)));
|
|
204
|
+
|
|
205
|
+
if (version !== 2) {
|
|
206
|
+
if (legacySignals) {
|
|
207
|
+
errors.push("legacy v1 goal state detected; GoalBuddy v2 requires version: 2 with a task board. Create a new v2 goal or migrate manually.");
|
|
208
|
+
} else {
|
|
209
|
+
errors.push("state.yaml must declare version: 2");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!["active", "blocked", "done"].includes(goalStatus)) {
|
|
214
|
+
errors.push(`goal.status must be active, blocked, or done; got ${goalStatus || "<missing>"}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const { agent, status } of installedAgents) {
|
|
218
|
+
if (status !== "installed") {
|
|
219
|
+
errors.push(`agents.${agent} must be installed; got ${status || "<missing>"}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!existsSync(join(root, "goal.md"))) errors.push("missing goal.md");
|
|
224
|
+
if (!existsSync(join(root, "notes")) || !statSync(join(root, "notes")).isDirectory()) {
|
|
225
|
+
errors.push("missing notes/ directory");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const unexpected = rootEntryErrors();
|
|
229
|
+
if (unexpected.length > 0) {
|
|
230
|
+
errors.push(`unexpected root entries; v2 goal roots may contain only goal.md, state.yaml, and notes/: ${unexpected.join(", ")}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const tasks = parseTasks();
|
|
234
|
+
const ids = new Set();
|
|
235
|
+
for (const task of tasks) {
|
|
236
|
+
if (!task.id || !/^T\d{3}$/.test(task.id)) errors.push(`task id must use T### format; got ${task.id || "<missing>"}`);
|
|
237
|
+
if (ids.has(task.id)) errors.push(`duplicate task id: ${task.id}`);
|
|
238
|
+
ids.add(task.id);
|
|
239
|
+
if (!["scout", "judge", "worker", "pm"].includes(task.type)) {
|
|
240
|
+
errors.push(`task ${task.id} type must be scout, judge, worker, or pm`);
|
|
241
|
+
}
|
|
242
|
+
if (!["Scout", "Judge", "Worker", "PM"].includes(task.assignee)) {
|
|
243
|
+
errors.push(`task ${task.id} assignee must be Scout, Judge, Worker, or PM`);
|
|
244
|
+
}
|
|
245
|
+
const expectedAssignee = {
|
|
246
|
+
scout: "Scout",
|
|
247
|
+
judge: "Judge",
|
|
248
|
+
worker: "Worker",
|
|
249
|
+
pm: "PM",
|
|
250
|
+
}[task.type];
|
|
251
|
+
if (expectedAssignee && task.assignee !== expectedAssignee) {
|
|
252
|
+
errors.push(`task ${task.id} assignee must be ${expectedAssignee} for type ${task.type}`);
|
|
253
|
+
}
|
|
254
|
+
if (!["queued", "active", "blocked", "done"].includes(task.status)) {
|
|
255
|
+
errors.push(`task ${task.id} status must be queued, active, blocked, or done`);
|
|
256
|
+
}
|
|
257
|
+
if (!task.objective) errors.push(`task ${task.id} missing objective`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (tasks.length === 0) errors.push("tasks must contain at least one task");
|
|
261
|
+
|
|
262
|
+
const activeTasks = tasks.filter((task) => task.status === "active");
|
|
263
|
+
if (goalStatus === "done") {
|
|
264
|
+
if (activeTasks.length !== 0) errors.push("done goals must not have an active task");
|
|
265
|
+
if (activeTask !== null) errors.push("done goals must set active_task: null");
|
|
266
|
+
const unfinishedWorkers = tasks
|
|
267
|
+
.filter((task) => task.type === "worker" && ["queued", "active"].includes(task.status))
|
|
268
|
+
.map((task) => task.id);
|
|
269
|
+
if (unfinishedWorkers.length > 0) {
|
|
270
|
+
errors.push(`done goals must not leave queued or active Worker tasks: ${unfinishedWorkers.join(", ")}`);
|
|
271
|
+
}
|
|
272
|
+
} else if (goalStatus === "blocked") {
|
|
273
|
+
if (activeTasks.length > 1) errors.push("blocked goals may have at most one active task");
|
|
274
|
+
if (continuousUntilFullOutcome && missingInputOrCredentialsDoNotStopGoal) {
|
|
275
|
+
errors.push("continuous goals must keep goal.status active; missing input or credentials should block specific tasks, not the whole goal");
|
|
276
|
+
}
|
|
277
|
+
} else if (activeTasks.length !== 1) {
|
|
278
|
+
errors.push(`exactly one active task is required while goal.status is active; found ${activeTasks.length}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (activeTasks.length === 1 && activeTask !== activeTasks[0].id) {
|
|
282
|
+
errors.push(`active_task must point to active task ${activeTasks[0].id}; got ${activeTask || "null"}`);
|
|
283
|
+
}
|
|
284
|
+
if (activeTask && !ids.has(activeTask)) errors.push(`active_task points to unknown task: ${activeTask}`);
|
|
285
|
+
|
|
286
|
+
for (const task of tasks) {
|
|
287
|
+
const hasReceipt = task.receipt.present && task.receipt.value !== null;
|
|
288
|
+
const receiptResult = hasReceipt ? task.receipt.scalar("result") : null;
|
|
289
|
+
if (task.status === "done" && !hasReceipt) {
|
|
290
|
+
errors.push(`done task ${task.id} missing receipt`);
|
|
291
|
+
}
|
|
292
|
+
if (task.status === "done" && hasReceipt && receiptResult !== "done") {
|
|
293
|
+
errors.push(`done task ${task.id} receipt must include result: done`);
|
|
294
|
+
}
|
|
295
|
+
if (task.status === "blocked" && !hasReceipt) {
|
|
296
|
+
errors.push(`blocked task ${task.id} missing receipt`);
|
|
297
|
+
}
|
|
298
|
+
if (task.type === "worker" && task.status === "active") {
|
|
299
|
+
if (task.allowedFiles.length === 0) errors.push(`active Worker task ${task.id} must include allowed_files`);
|
|
300
|
+
if (task.verify.length === 0) errors.push(`active Worker task ${task.id} must include verify`);
|
|
301
|
+
if (task.stopIf.length === 0) errors.push(`active Worker task ${task.id} must include stop_if`);
|
|
302
|
+
}
|
|
303
|
+
if (task.type === "worker" && task.status === "done" && hasReceipt) {
|
|
304
|
+
for (const key of ["changed_files", "commands", "summary"]) {
|
|
305
|
+
if (!task.receipt.has(key)) errors.push(`Worker receipt for ${task.id} missing ${key}`);
|
|
306
|
+
}
|
|
307
|
+
const changedFiles = task.receipt.list("changed_files");
|
|
308
|
+
for (const changedFile of changedFiles) {
|
|
309
|
+
if (!task.allowedFiles.includes(changedFile)) {
|
|
310
|
+
errors.push(`Worker receipt for ${task.id} changed file outside allowed_files: ${changedFile}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const commandStatuses = task.receipt.commandStatuses();
|
|
314
|
+
if (task.receipt.has("commands") && commandStatuses.length === 0) {
|
|
315
|
+
errors.push(`Worker receipt for ${task.id} commands must include status fields`);
|
|
316
|
+
}
|
|
317
|
+
for (const status of commandStatuses) {
|
|
318
|
+
if (status !== "pass") {
|
|
319
|
+
errors.push(`Worker receipt for ${task.id} has non-passing command status: ${status}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (task.type === "scout" && task.status === "done" && hasReceipt) {
|
|
324
|
+
if (!task.receipt.has("summary")) errors.push(`Scout receipt for ${task.id} missing summary`);
|
|
325
|
+
if (!task.receipt.has("evidence") && !task.receipt.has("note")) {
|
|
326
|
+
errors.push(`Scout receipt for ${task.id} must include evidence or note`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (task.type === "judge" && task.status === "done" && hasReceipt && !task.receipt.has("decision")) {
|
|
330
|
+
errors.push(`Judge receipt for ${task.id} missing decision`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (goalStatus === "done") {
|
|
335
|
+
const finalAudit = tasks.some((task) => {
|
|
336
|
+
if (!["judge", "pm"].includes(task.type) || task.status !== "done") return false;
|
|
337
|
+
if (!task.receipt.present || task.receipt.value === null) return false;
|
|
338
|
+
const decision = task.receipt.scalar("decision");
|
|
339
|
+
return decision === "complete" || decision === "done";
|
|
340
|
+
});
|
|
341
|
+
if (!finalAudit) {
|
|
342
|
+
errors.push("completion requires a final done Judge or PM audit receipt with decision: complete");
|
|
343
|
+
}
|
|
344
|
+
if (continuousUntilFullOutcome) {
|
|
345
|
+
const finalFullOutcomeAudit = tasks.some((task) => {
|
|
346
|
+
if (!["judge", "pm"].includes(task.type) || task.status !== "done") return false;
|
|
347
|
+
if (!task.receipt.present || task.receipt.value === null) return false;
|
|
348
|
+
const decision = task.receipt.scalar("decision");
|
|
349
|
+
const fullOutcomeComplete = task.receipt.scalar("full_outcome_complete");
|
|
350
|
+
return (decision === "complete" || decision === "done") && fullOutcomeComplete === true;
|
|
351
|
+
});
|
|
352
|
+
if (!finalFullOutcomeAudit) {
|
|
353
|
+
errors.push("continuous goals require a final done Judge or PM audit receipt with full_outcome_complete: true before goal.status: done");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const result = {
|
|
359
|
+
ok: errors.length === 0,
|
|
360
|
+
version,
|
|
361
|
+
state_path: statePath,
|
|
362
|
+
goal_status: goalStatus,
|
|
363
|
+
active_task: activeTask,
|
|
364
|
+
task_count: tasks.length,
|
|
365
|
+
errors,
|
|
366
|
+
warnings,
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
console.log(JSON.stringify(result, null, 2));
|
|
370
|
+
process.exit(result.ok ? 0 : 1);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const sourceDir = resolve(__dirname, "../agents");
|
|
8
|
+
const destDir = resolve(process.argv[2] || ".codex/agents");
|
|
9
|
+
const force = process.argv.includes("--force");
|
|
10
|
+
|
|
11
|
+
if (!existsSync(sourceDir)) {
|
|
12
|
+
console.error(`agent definitions not found: ${sourceDir}`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
mkdirSync(destDir, { recursive: true });
|
|
17
|
+
|
|
18
|
+
const files = readdirSync(sourceDir).filter((f) => f.endsWith(".toml") && !f.includes("config-snippet"));
|
|
19
|
+
for (const file of files) {
|
|
20
|
+
const src = join(sourceDir, file);
|
|
21
|
+
const dest = join(destDir, file);
|
|
22
|
+
if (existsSync(dest) && !force) {
|
|
23
|
+
console.log(`skip existing ${dest} (use --force to overwrite)`);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
copyFileSync(src, dest);
|
|
27
|
+
console.log(`installed ${dest}`);
|
|
28
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# GoalBuddy Agents
|
|
2
|
+
|
|
3
|
+
Use three generic agents. The main `/goal` thread remains PM and owns the board.
|
|
4
|
+
|
|
5
|
+
| Agent | model_reasoning_effort | sandbox_mode | Purpose |
|
|
6
|
+
|---|---:|---|---|
|
|
7
|
+
| goal_scout | medium | read-only | Evidence mapping and candidate tasks |
|
|
8
|
+
| goal_worker | low | workspace-write | One bounded implementation/recovery task |
|
|
9
|
+
| goal_judge | high | read-only | Strategic review, escalation, completion skepticism |
|
|
10
|
+
|
|
11
|
+
## PM Thinking Policy
|
|
12
|
+
|
|
13
|
+
The main `/goal` thread is the PM. It owns board truth, chooses active tasks, and decides when receipts are sufficient.
|
|
14
|
+
|
|
15
|
+
| Goal mode | PM thinking |
|
|
16
|
+
|---|---:|
|
|
17
|
+
| specific, bounded | medium |
|
|
18
|
+
| open-ended | high |
|
|
19
|
+
| recovery | high |
|
|
20
|
+
| audit | high |
|
|
21
|
+
| high-risk or multi-day final audit | xhigh optional |
|
|
22
|
+
|
|
23
|
+
Do not use `xhigh` by default. Use it only when a wrong board, scope, or completion decision would be materially more expensive than latency and cost.
|
|
24
|
+
|
|
25
|
+
Tasks may include optional `reasoning_hint: default | low | medium | high | xhigh`. Treat it as PM guidance, not permission to widen scope.
|
|
26
|
+
|
|
27
|
+
Recommended project config:
|
|
28
|
+
|
|
29
|
+
```toml
|
|
30
|
+
[agents]
|
|
31
|
+
max_threads = 4
|
|
32
|
+
max_depth = 1
|
|
33
|
+
job_max_runtime_seconds = 1800
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Install:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
mkdir -p .codex/agents
|
|
40
|
+
cp .codex/skills/goalbuddy/agents/goal_*.toml .codex/agents/
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Rules:
|
|
44
|
+
|
|
45
|
+
- Only the PM loop chooses active tasks, marks tasks done, or completes the goal.
|
|
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.
|
|
48
|
+
- Judge is high thinking.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/goal Follow docs/goals/<slug>/goal.md.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# <Goal Title>
|
|
2
|
+
|
|
3
|
+
## Objective
|
|
4
|
+
|
|
5
|
+
<User-editable objective. Keep this bounded to the current tranche, not an infinite mission.>
|
|
6
|
+
|
|
7
|
+
## Original Request
|
|
8
|
+
|
|
9
|
+
<Shortest faithful copy of what the user asked for. Preserve user-provided plan details here or summarize them under Intake Summary.>
|
|
10
|
+
|
|
11
|
+
## Intake Summary
|
|
12
|
+
|
|
13
|
+
- Input shape: `vague | specific | existing_plan | recovery | audit`
|
|
14
|
+
- Audience: <beneficiary or unknown>
|
|
15
|
+
- Authority: `requested | approved | inferred | needs_approval | blocked`
|
|
16
|
+
- Proof type: `test | demo | artifact | metric | review | source_backed_answer | decision`
|
|
17
|
+
- Completion proof: <observable signal that closes the full original outcome>
|
|
18
|
+
- Likely misfire: <how GoalBuddy could succeed at the wrong thing>
|
|
19
|
+
- Blind spots considered: <risks, unstated choices, or success dimensions surfaced during diagnostic intake>
|
|
20
|
+
- Existing plan facts: <user-provided steps/files/constraints/sequencing to preserve and validate, or none>
|
|
21
|
+
|
|
22
|
+
## Goal Kind
|
|
23
|
+
|
|
24
|
+
`specific | open_ended | existing_plan | recovery | audit`
|
|
25
|
+
|
|
26
|
+
## Current Tranche
|
|
27
|
+
|
|
28
|
+
<What is enough for the full owner outcome, and what is the current safe slice? For execution goals, the default is continuous: discover enough evidence, choose a safe implementation slice, implement it, verify it, audit it, then immediately advance to the next safe slice until the full original outcome is complete. Plan-only or one-slice-only stopping is valid only when explicitly requested.>
|
|
29
|
+
|
|
30
|
+
## Non-Negotiable Constraints
|
|
31
|
+
|
|
32
|
+
- <Constraint, safety rule, compatibility rule, or owner preference.>
|
|
33
|
+
|
|
34
|
+
## Stop Rule
|
|
35
|
+
|
|
36
|
+
Stop only when a final audit proves the full original outcome is complete.
|
|
37
|
+
|
|
38
|
+
Do not stop after planning, discovery, or Judge selection if the user asked for working software or automation and a safe Worker task can be activated.
|
|
39
|
+
|
|
40
|
+
Do not stop after a single verified Worker slice when the broader owner outcome still has safe local follow-up slices. After each slice audit, advance the board to the next highest-leverage safe Worker task and continue.
|
|
41
|
+
|
|
42
|
+
Do not stop because a slice needs owner input, credentials, production access, destructive operations, or policy decisions. Mark that exact slice blocked with a receipt, create the smallest safe follow-up or workaround task, and continue all local, non-destructive work that can still move the goal toward the full outcome.
|
|
43
|
+
|
|
44
|
+
## Canonical Board
|
|
45
|
+
|
|
46
|
+
Machine truth lives at:
|
|
47
|
+
|
|
48
|
+
`docs/goals/<slug>/state.yaml`
|
|
49
|
+
|
|
50
|
+
If this charter and `state.yaml` disagree, `state.yaml` wins for task status, active task, receipts, verification freshness, and completion truth.
|
|
51
|
+
|
|
52
|
+
## Run Command
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
/goal Follow docs/goals/<slug>/goal.md.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## PM Loop
|
|
59
|
+
|
|
60
|
+
On every `/goal` continuation:
|
|
61
|
+
|
|
62
|
+
1. Read this charter.
|
|
63
|
+
2. Read `state.yaml`.
|
|
64
|
+
3. Re-check the intake: original request, input shape, authority, proof, blind spots, existing plan facts, and likely misfire.
|
|
65
|
+
4. Work only on the active board task.
|
|
66
|
+
5. Assign Scout, Judge, Worker, or PM according to the task.
|
|
67
|
+
6. Write a compact task receipt.
|
|
68
|
+
7. Update the board.
|
|
69
|
+
8. If Judge selected a safe Worker task with `allowed_files`, `verify`, and `stop_if`, activate it and continue unless blocked.
|
|
70
|
+
9. Treat a slice audit as a checkpoint, not completion, unless it explicitly proves the full original outcome is complete.
|
|
71
|
+
10. Finish only with a Judge/PM audit receipt that maps receipts and verification back to the original user outcome and records `full_outcome_complete: true`.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# <Task ID>: <Note Title>
|
|
2
|
+
|
|
3
|
+
Task: `<T###>`
|
|
4
|
+
Kind: `scout | judge | pm`
|
|
5
|
+
Status: `current | superseded | blocked`
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
<One paragraph the board receipt can point to.>
|
|
10
|
+
|
|
11
|
+
## Details
|
|
12
|
+
|
|
13
|
+
- <Evidence, decision, or context too large for an inline receipt.>
|
|
14
|
+
|
|
15
|
+
## Board Receipt Snippet
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
receipt:
|
|
19
|
+
result: done
|
|
20
|
+
note: notes/<task-id>-<slug>.md
|
|
21
|
+
summary: "<compact result>"
|
|
22
|
+
```
|