goalbuddy 0.3.6 → 0.3.8
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/CHANGELOG.md +61 -0
- package/CONTRIBUTING.md +2 -2
- package/README.md +27 -10
- package/{RELEASE-0.3.5.md → docs/releases/0.3.5.md} +4 -4
- package/docs/releases/0.3.7.md +129 -0
- package/docs/releases/0.3.8.md +40 -0
- package/docs/releases/README.md +83 -0
- package/goalbuddy/SKILL.md +21 -10
- package/goalbuddy/scripts/check-goal-state.mjs +53 -0
- package/goalbuddy/scripts/render-task-prompt.mjs +39 -4
- package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/README.md +7 -9
- package/goalbuddy/{extend → surfaces}/local-goal-board/examples/sample-goal/state.yaml +5 -5
- package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/examples/subgoal-parent/state.yaml +3 -3
- package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/state.yaml +3 -3
- package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/scripts/lib/goal-board.mjs +17 -13
- package/goalbuddy/{extend → surfaces}/local-goal-board/scripts/local-goal-board.mjs +27 -6
- package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/test/local-goal-board.test.mjs +63 -12
- package/goalbuddy/templates/goal.md +9 -0
- package/goalbuddy/templates/state.yaml +7 -6
- package/internal/assets/goalbuddy-v0.3.7-release.png +0 -0
- package/internal/cli/goal-maker.mjs +177 -717
- package/package.json +7 -8
- package/plugins/goalbuddy/.claude-plugin/plugin.json +3 -4
- package/plugins/goalbuddy/.codex-plugin/plugin.json +5 -6
- package/plugins/goalbuddy/README.md +4 -3
- package/plugins/goalbuddy/skills/goalbuddy/SKILL.md +21 -10
- package/plugins/goalbuddy/skills/goalbuddy/scripts/check-goal-state.mjs +53 -0
- package/plugins/goalbuddy/skills/goalbuddy/scripts/render-task-prompt.mjs +39 -4
- package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/README.md +7 -9
- package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/sample-goal/state.yaml +5 -5
- package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/examples/subgoal-parent/state.yaml +3 -3
- package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/state.yaml +3 -3
- package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/scripts/lib/goal-board.mjs +2 -2
- package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/scripts/local-goal-board.mjs +27 -6
- package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/test/local-goal-board.test.mjs +35 -8
- package/plugins/goalbuddy/skills/goalbuddy/templates/goal.md +9 -0
- package/plugins/goalbuddy/skills/goalbuddy/templates/state.yaml +7 -6
- package/examples/extend-catalog-workflow/goal.md +0 -53
- package/examples/extend-catalog-workflow/notes/T001-extension-model-map.md +0 -47
- package/examples/extend-catalog-workflow/notes/T002-architecture-decision.md +0 -48
- package/examples/extend-catalog-workflow/notes/T003-implementation-summary.md +0 -43
- package/examples/extend-catalog-workflow/notes/T004-root-extend-folder.md +0 -24
- package/examples/extend-catalog-workflow/notes/T005-layout-cleanup.md +0 -46
- package/examples/extend-catalog-workflow/notes/T006-catalog-location.md +0 -50
- package/examples/extend-catalog-workflow/notes/T999-completion-audit.md +0 -36
- package/examples/extend-catalog-workflow/state.yaml +0 -327
- package/examples/github-pr-workflow-extension/pr-handoff.md +0 -46
- package/examples/improve-goal-maker/goal.md +0 -51
- package/examples/improve-goal-maker/notes/T001-repo-map.md +0 -59
- package/examples/improve-goal-maker/notes/T002-risk-map.md +0 -37
- package/examples/improve-goal-maker/state.yaml +0 -224
- package/goalbuddy/extend/github-projects/README.md +0 -105
- package/goalbuddy/extend/github-projects/examples/goal-board-sync/state.yaml +0 -63
- package/goalbuddy/extend/github-projects/extension.yaml +0 -43
- package/goalbuddy/extend/github-projects/scripts/lib/github-projects.mjs +0 -728
- package/goalbuddy/extend/github-projects/scripts/lib/goal-state.mjs +0 -362
- package/goalbuddy/extend/github-projects/scripts/sync-github-project.mjs +0 -193
- package/goalbuddy/extend/github-projects/test/github-projects.test.mjs +0 -267
- package/goalbuddy/extend/local-goal-board/extension.yaml +0 -39
- package/internal/assets/extend-release.png +0 -0
- package/internal/assets/extend-release.svg +0 -83
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/README.md +0 -105
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/examples/goal-board-sync/state.yaml +0 -63
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/extension.yaml +0 -43
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/lib/github-projects.mjs +0 -728
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/lib/goal-state.mjs +0 -362
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/sync-github-project.mjs +0 -193
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/test/github-projects.test.mjs +0 -267
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/extension.yaml +0 -39
- /package/goalbuddy/{extend → surfaces}/local-goal-board/assets/goalbuddy-mark.png +0 -0
- /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/sample-goal/notes/T001-scout.md +0 -0
- /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/goal.md +0 -0
- /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/notes/.gitkeep +0 -0
- /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/goal.md +0 -0
- /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/notes/.gitkeep +0 -0
- /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/assets/goalbuddy-mark.png +0 -0
- /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/sample-goal/notes/T001-scout.md +0 -0
- /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/goal.md +0 -0
- /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/notes/.gitkeep +0 -0
- /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/goal.md +0 -0
- /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/notes/.gitkeep +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { parseGoalStateText } from "../
|
|
5
|
+
import { parseGoalStateText } from "../surfaces/local-goal-board/scripts/lib/goal-board.mjs";
|
|
6
6
|
|
|
7
7
|
const ROLE_DEFAULTS = {
|
|
8
8
|
scout: { agent: "goal_scout", reasoning: "low", sandbox: "read-only" },
|
|
@@ -45,6 +45,7 @@ export function renderTaskPrompt(options) {
|
|
|
45
45
|
fork_context_allowed: role !== "worker",
|
|
46
46
|
board_path: board.path,
|
|
47
47
|
child_board_paths: childBoardPaths(board),
|
|
48
|
+
goal_oracle: board.goal.oracle || null,
|
|
48
49
|
slice_policy: board.document.rules?.slice_policy || null,
|
|
49
50
|
warnings,
|
|
50
51
|
},
|
|
@@ -141,6 +142,12 @@ function promptWarnings(board, task) {
|
|
|
141
142
|
const warnings = [];
|
|
142
143
|
const role = normalizeRole(task.type);
|
|
143
144
|
if (task.id !== board.activeTask) warnings.push(`Task ${task.id} is not the active task on this board.`);
|
|
145
|
+
if (isWeakProof(board.goal.oracle?.signal)) {
|
|
146
|
+
warnings.push("goal.oracle.signal is missing or placeholder-like; keep the goal pressured by a concrete completion oracle.");
|
|
147
|
+
}
|
|
148
|
+
if (isWeakProof(board.goal.oracle?.final_proof)) {
|
|
149
|
+
warnings.push("goal.oracle.final_proof is missing or placeholder-like; do not mark the goal complete without receipt-backed proof.");
|
|
150
|
+
}
|
|
144
151
|
if (role === "worker") {
|
|
145
152
|
if (stringList(task.allowed_files).length === 0) warnings.push(`Worker task ${task.id} has no allowed_files.`);
|
|
146
153
|
if (stringList(task.verify).length === 0) warnings.push(`Worker task ${task.id} has no verify commands.`);
|
|
@@ -211,6 +218,17 @@ function isTrue(value) {
|
|
|
211
218
|
return value === true || String(value).toLowerCase() === "true";
|
|
212
219
|
}
|
|
213
220
|
|
|
221
|
+
function isWeakProof(value) {
|
|
222
|
+
if (value === null || value === undefined) return true;
|
|
223
|
+
const normalized = String(value).trim().toLowerCase();
|
|
224
|
+
return normalized === ""
|
|
225
|
+
|| normalized === "unknown"
|
|
226
|
+
|| normalized === "tbd"
|
|
227
|
+
|| normalized === "todo"
|
|
228
|
+
|| normalized === "none"
|
|
229
|
+
|| /^<.*>$/.test(normalized);
|
|
230
|
+
}
|
|
231
|
+
|
|
214
232
|
function stringList(value) {
|
|
215
233
|
return Array.isArray(value) ? value.filter((item) => item !== null && item !== undefined).map(String) : [];
|
|
216
234
|
}
|
|
@@ -219,29 +237,43 @@ function receiptSchema(role) {
|
|
|
219
237
|
if (role === "worker") {
|
|
220
238
|
return {
|
|
221
239
|
result: "done | blocked",
|
|
240
|
+
task_id: "<T###>",
|
|
241
|
+
board_path: "<path to state.yaml>",
|
|
222
242
|
changed_files: [],
|
|
223
|
-
commands: [
|
|
224
|
-
summary: "<=120 words",
|
|
243
|
+
commands: [],
|
|
244
|
+
summary: "<=120 words>",
|
|
225
245
|
remaining_blockers: [],
|
|
246
|
+
verification_attempts: 1,
|
|
247
|
+
stopped_because: null,
|
|
226
248
|
};
|
|
227
249
|
}
|
|
228
250
|
if (role === "judge") {
|
|
229
251
|
return {
|
|
230
252
|
result: "done | blocked",
|
|
253
|
+
task_id: "<T###>",
|
|
254
|
+
board_path: "<path to state.yaml>",
|
|
231
255
|
decision: "approved | rejected | approve_subgoal | reject_subgoal | not_complete | complete",
|
|
232
256
|
full_outcome_complete: false,
|
|
257
|
+
rationale: "<=120 words>",
|
|
233
258
|
evidence: [],
|
|
259
|
+
subgoal_contract: null,
|
|
260
|
+
parallel_safety: null,
|
|
234
261
|
blocked_tasks: [],
|
|
262
|
+
missing_evidence: [],
|
|
235
263
|
required_board_updates: [],
|
|
236
264
|
};
|
|
237
265
|
}
|
|
238
266
|
return {
|
|
239
267
|
result: "done | blocked",
|
|
240
|
-
|
|
268
|
+
task_id: "<T###>",
|
|
269
|
+
board_path: "<path to state.yaml>",
|
|
270
|
+
summary: "<=120 words>",
|
|
241
271
|
evidence: [],
|
|
242
272
|
facts: [],
|
|
243
273
|
contradictions: [],
|
|
244
274
|
ambiguity_requiring_judge: [],
|
|
275
|
+
commands: [],
|
|
276
|
+
note_needed: false,
|
|
245
277
|
};
|
|
246
278
|
}
|
|
247
279
|
|
|
@@ -261,6 +293,9 @@ function formatPrompt(payload) {
|
|
|
261
293
|
lines.push("- child_board_paths:");
|
|
262
294
|
for (const path of payload.metadata.child_board_paths) lines.push(` - ${path}`);
|
|
263
295
|
}
|
|
296
|
+
if (payload.metadata.goal_oracle) {
|
|
297
|
+
lines.push(`- goal_oracle: ${JSON.stringify(payload.metadata.goal_oracle)}`);
|
|
298
|
+
}
|
|
264
299
|
if (payload.metadata.slice_policy) {
|
|
265
300
|
lines.push(`- slice_policy: ${JSON.stringify(payload.metadata.slice_policy)}`);
|
|
266
301
|
}
|
package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/README.md
RENAMED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Generate a small local GoalBuddy board for a goal directory and watch it update live while agents work.
|
|
4
4
|
|
|
5
|
-
The
|
|
5
|
+
The surface keeps `state.yaml` authoritative. It writes static web app files into the goal directory and serves them from a local-only Node server. The browser subscribes to Server-Sent Events, so cards update as `state.yaml`, `notes/`, or linked depth-1 sub-goal state changes without a manual reload.
|
|
6
6
|
|
|
7
7
|
## Use When
|
|
8
8
|
|
|
@@ -14,11 +14,10 @@ The extension keeps `state.yaml` authoritative. It writes static web app files i
|
|
|
14
14
|
## Generate And Serve
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
|
|
18
|
-
--goal docs/goals/<slug>
|
|
17
|
+
npx goalbuddy board docs/goals/<slug>
|
|
19
18
|
```
|
|
20
19
|
|
|
21
|
-
The generated app includes the bundled `assets/goalbuddy-mark.png`, so the board keeps the GoalBuddy mark
|
|
20
|
+
The generated app includes the bundled `assets/goalbuddy-mark.png`, so the board keeps the GoalBuddy mark anywhere the package is installed.
|
|
22
21
|
|
|
23
22
|
The command writes:
|
|
24
23
|
|
|
@@ -34,8 +33,7 @@ Then it starts or reuses the shared local board hub at `http://goalbuddy.localho
|
|
|
34
33
|
## Check Without A Long-Running Server
|
|
35
34
|
|
|
36
35
|
```bash
|
|
37
|
-
|
|
38
|
-
--goal docs/goals/<slug> \
|
|
36
|
+
npx goalbuddy board docs/goals/<slug> \
|
|
39
37
|
--once \
|
|
40
38
|
--json
|
|
41
39
|
```
|
|
@@ -63,9 +61,9 @@ Clicking a card opens a detail modal with the task objective, status, assignee,
|
|
|
63
61
|
## Verification
|
|
64
62
|
|
|
65
63
|
```bash
|
|
66
|
-
node --test
|
|
67
|
-
node
|
|
68
|
-
--goal
|
|
64
|
+
node --test goalbuddy/surfaces/local-goal-board/test/*.test.mjs
|
|
65
|
+
node goalbuddy/surfaces/local-goal-board/scripts/local-goal-board.mjs \
|
|
66
|
+
--goal goalbuddy/surfaces/local-goal-board/examples/sample-goal \
|
|
69
67
|
--once \
|
|
70
68
|
--json
|
|
71
69
|
```
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
version: 2
|
|
2
2
|
|
|
3
3
|
goal:
|
|
4
|
-
title: "Local
|
|
5
|
-
slug: "local-
|
|
4
|
+
title: "Local Goal Board Surface"
|
|
5
|
+
slug: "local-goal-board-surface"
|
|
6
6
|
kind: specific
|
|
7
7
|
tranche: "Demonstrate local GoalBuddy board rendering."
|
|
8
8
|
status: active
|
|
@@ -30,7 +30,7 @@ tasks:
|
|
|
30
30
|
type: worker
|
|
31
31
|
assignee: Worker
|
|
32
32
|
status: blocked
|
|
33
|
-
objective: "Catalog and document the local board
|
|
33
|
+
objective: "Catalog and document the local board surface."
|
|
34
34
|
receipt:
|
|
35
35
|
result: blocked
|
|
36
36
|
summary: "T003 is blocked during the progressive board motion demo."
|
|
@@ -78,7 +78,7 @@ tasks:
|
|
|
78
78
|
type: scout
|
|
79
79
|
assignee: Scout
|
|
80
80
|
status: done
|
|
81
|
-
objective: "List the
|
|
81
|
+
objective: "List the board launch paths."
|
|
82
82
|
receipt:
|
|
83
83
|
result: done
|
|
84
84
|
summary: "T009 completed during the progressive board motion demo."
|
|
@@ -118,7 +118,7 @@ tasks:
|
|
|
118
118
|
type: worker
|
|
119
119
|
assignee: Worker
|
|
120
120
|
status: done
|
|
121
|
-
objective: "Prepare final
|
|
121
|
+
objective: "Prepare final board packaging check."
|
|
122
122
|
receipt:
|
|
123
123
|
result: done
|
|
124
124
|
summary: "T014 completed during the progressive board motion demo."
|
|
@@ -31,10 +31,10 @@ tasks:
|
|
|
31
31
|
status: active
|
|
32
32
|
objective: "Build the sub-goal board view."
|
|
33
33
|
allowed_files:
|
|
34
|
-
- goalbuddy/
|
|
35
|
-
- goalbuddy/
|
|
34
|
+
- goalbuddy/surfaces/local-goal-board/scripts/lib/goal-board.mjs
|
|
35
|
+
- goalbuddy/surfaces/local-goal-board/test/local-goal-board.test.mjs
|
|
36
36
|
verify:
|
|
37
|
-
- node --test goalbuddy/
|
|
37
|
+
- node --test goalbuddy/surfaces/local-goal-board/test/local-goal-board.test.mjs
|
|
38
38
|
stop_if:
|
|
39
39
|
- "Need files outside allowed_files."
|
|
40
40
|
subgoal:
|
|
@@ -24,16 +24,16 @@ tasks:
|
|
|
24
24
|
result: done
|
|
25
25
|
summary: "Child board payload needs normal columns and task details."
|
|
26
26
|
evidence:
|
|
27
|
-
- goalbuddy/
|
|
27
|
+
- goalbuddy/surfaces/local-goal-board/scripts/lib/goal-board.mjs
|
|
28
28
|
- id: T002
|
|
29
29
|
type: worker
|
|
30
30
|
assignee: Worker
|
|
31
31
|
status: active
|
|
32
32
|
objective: "Render the read-only embedded child board."
|
|
33
33
|
allowed_files:
|
|
34
|
-
- goalbuddy/
|
|
34
|
+
- goalbuddy/surfaces/local-goal-board/scripts/lib/goal-board.mjs
|
|
35
35
|
verify:
|
|
36
|
-
- node --test goalbuddy/
|
|
36
|
+
- node --test goalbuddy/surfaces/local-goal-board/test/local-goal-board.test.mjs
|
|
37
37
|
stop_if:
|
|
38
38
|
- "Need files outside allowed_files."
|
|
39
39
|
receipt: null
|
|
@@ -6,8 +6,8 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
const VALID_STATUSES = new Set(["queued", "active", "blocked", "done"]);
|
|
7
7
|
const COLUMN_ORDER = ["todo", "in-progress", "blocked", "completed"];
|
|
8
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const
|
|
10
|
-
const logoAssetPath = join(
|
|
9
|
+
const surfaceRoot = resolve(__dirname, "../..");
|
|
10
|
+
const logoAssetPath = join(surfaceRoot, "assets", "goalbuddy-mark.png");
|
|
11
11
|
|
|
12
12
|
export class GoalBoardError extends Error {
|
|
13
13
|
constructor(message) {
|
|
@@ -142,8 +142,8 @@ export function buildColumns(tasks) {
|
|
|
142
142
|
byColumn.get(task.column).push(task);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
for (const columnTasks of byColumn.
|
|
146
|
-
columnTasks.sort((left, right) =>
|
|
145
|
+
for (const [columnId, columnTasks] of byColumn.entries()) {
|
|
146
|
+
columnTasks.sort((left, right) => compareColumnTasks(columnId, left, right));
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
return [
|
|
@@ -289,7 +289,7 @@ function titleForTask(task) {
|
|
|
289
289
|
function compactTaskTitle(value) {
|
|
290
290
|
const text = cleanText(value).replace(/\.$/, "");
|
|
291
291
|
const routeMatch = text.match(/^Implement\b.*?\s(\/[A-Za-z0-9_./:-]+)\s+(route|queue slice|slice)\b/i);
|
|
292
|
-
if (routeMatch) return
|
|
292
|
+
if (routeMatch) return `Implement ${routeMatch[1]} ${routeMatch[2]}`;
|
|
293
293
|
|
|
294
294
|
const firstClause = text
|
|
295
295
|
.split(/(?<=[.!?])\s+|\s+(?:Use only|Add|Match|Render|Clearly label|Do not)\b/i)[0]
|
|
@@ -300,14 +300,7 @@ function compactTaskTitle(value) {
|
|
|
300
300
|
.replace(/[.;:,]\s*$/, "")
|
|
301
301
|
.trim();
|
|
302
302
|
|
|
303
|
-
return
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function truncateTitle(value, maxLength = 82) {
|
|
307
|
-
const text = cleanText(value).replace(/\.$/, "");
|
|
308
|
-
if (text.length <= maxLength) return text;
|
|
309
|
-
const shortened = text.slice(0, maxLength + 1).replace(/\s+\S*$/, "").trim();
|
|
310
|
-
return `${shortened || text.slice(0, maxLength).trim()}...`;
|
|
303
|
+
return firstClause || text;
|
|
311
304
|
}
|
|
312
305
|
|
|
313
306
|
function columnForStatus(status) {
|
|
@@ -322,6 +315,12 @@ function taskSortKey(task) {
|
|
|
322
315
|
return `${rank}:${task.id}`;
|
|
323
316
|
}
|
|
324
317
|
|
|
318
|
+
function compareColumnTasks(columnId, left, right) {
|
|
319
|
+
const order = taskSortKey(left).localeCompare(taskSortKey(right));
|
|
320
|
+
if (columnId === "completed") return -order;
|
|
321
|
+
return order;
|
|
322
|
+
}
|
|
323
|
+
|
|
325
324
|
function normalizeStringList(value) {
|
|
326
325
|
if (!value) return [];
|
|
327
326
|
if (Array.isArray(value)) return value.map(cleanText).filter(Boolean);
|
|
@@ -1503,8 +1502,13 @@ h1 {
|
|
|
1503
1502
|
.task-title {
|
|
1504
1503
|
margin: 0;
|
|
1505
1504
|
color: #2f3437;
|
|
1505
|
+
display: -webkit-box;
|
|
1506
1506
|
font-size: 15px;
|
|
1507
1507
|
line-height: 1.35;
|
|
1508
|
+
overflow: hidden;
|
|
1509
|
+
overflow-wrap: anywhere;
|
|
1510
|
+
-webkit-box-orient: vertical;
|
|
1511
|
+
-webkit-line-clamp: 5;
|
|
1508
1512
|
}
|
|
1509
1513
|
|
|
1510
1514
|
.card-footer {
|
|
@@ -228,8 +228,7 @@ export async function startBoardServer(options = {}) {
|
|
|
228
228
|
|
|
229
229
|
const route = routeBoardRequest(url.pathname, boards, initialBoard);
|
|
230
230
|
if (!route.board) {
|
|
231
|
-
response.
|
|
232
|
-
response.end("Not found");
|
|
231
|
+
sendUnregisteredBoardPath(response, url.pathname, boards, baseUrl);
|
|
233
232
|
return;
|
|
234
233
|
}
|
|
235
234
|
if (route.pathname === "/api/board") {
|
|
@@ -400,6 +399,28 @@ function routeBoardRequest(pathname, boards, initialBoard) {
|
|
|
400
399
|
return matches[0] || { board: null, pathname };
|
|
401
400
|
}
|
|
402
401
|
|
|
402
|
+
function sendUnregisteredBoardPath(response, pathname, boards, baseUrl) {
|
|
403
|
+
response.writeHead(404, {
|
|
404
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
405
|
+
"Cache-Control": "no-store",
|
|
406
|
+
});
|
|
407
|
+
const registeredBoards = [...boards.values()].map((board) => {
|
|
408
|
+
const summary = boardSummary(board, baseUrl);
|
|
409
|
+
return `- ${summary.title}: ${summary.url}`;
|
|
410
|
+
});
|
|
411
|
+
response.end([
|
|
412
|
+
`GoalBuddy board path is not registered in this local hub: ${pathname}`,
|
|
413
|
+
"",
|
|
414
|
+
"This server is the GoalBuddy multi-board hub. Do not stop it just because a /<slug>/ board URL returned 404.",
|
|
415
|
+
"Start or rerun `npx goalbuddy board <goal-dir>` to register that goal on this same port, then open the printed /<slug>/ URL.",
|
|
416
|
+
"",
|
|
417
|
+
"Registered boards:",
|
|
418
|
+
registeredBoards.length ? registeredBoards.join("\n") : "- none",
|
|
419
|
+
"",
|
|
420
|
+
`Hub API: ${baseUrl}/api/boards`,
|
|
421
|
+
].join("\n"));
|
|
422
|
+
}
|
|
423
|
+
|
|
403
424
|
function stripBoardPathPrefix(pathname, boardPath) {
|
|
404
425
|
const prefix = boardPath.endsWith("/") ? boardPath.slice(0, -1) : boardPath;
|
|
405
426
|
if (pathname === prefix) return "/";
|
|
@@ -527,9 +548,9 @@ function serveStatic(appDir, pathname, response) {
|
|
|
527
548
|
return;
|
|
528
549
|
}
|
|
529
550
|
|
|
530
|
-
const
|
|
551
|
+
const fileExtension = cleanPath.match(/\.[^.]+$/)?.[0] || "";
|
|
531
552
|
response.writeHead(200, {
|
|
532
|
-
"Content-Type": textTypes[
|
|
553
|
+
"Content-Type": textTypes[fileExtension] || "application/octet-stream",
|
|
533
554
|
"Cache-Control": "no-store",
|
|
534
555
|
});
|
|
535
556
|
response.end(readFileSync(file));
|
|
@@ -580,8 +601,8 @@ function usage() {
|
|
|
580
601
|
console.log(`GoalBuddy Local Goal Board
|
|
581
602
|
|
|
582
603
|
Usage:
|
|
583
|
-
|
|
584
|
-
|
|
604
|
+
npx goalbuddy board docs/goals/<slug>
|
|
605
|
+
npx goalbuddy board docs/goals/<slug> --once --json
|
|
585
606
|
|
|
586
607
|
Options:
|
|
587
608
|
--goal <path> Goal directory containing state.yaml.
|
|
@@ -4,13 +4,13 @@ import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } f
|
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join, resolve } from "node:path";
|
|
7
|
-
import { createBoardPayload, writeBoardApp } from "../scripts/lib/goal-board.mjs";
|
|
7
|
+
import { buildColumns, createBoardPayload, writeBoardApp } from "../scripts/lib/goal-board.mjs";
|
|
8
8
|
import { parseArgs, startBoardServer } from "../scripts/local-goal-board.mjs";
|
|
9
9
|
|
|
10
10
|
test("normalizes a dense goal into local board columns", () => {
|
|
11
|
-
const payload = createBoardPayload(resolve("
|
|
11
|
+
const payload = createBoardPayload(resolve("goalbuddy/surfaces/local-goal-board/examples/sample-goal"));
|
|
12
12
|
|
|
13
|
-
assert.equal(payload.goal.title, "Local
|
|
13
|
+
assert.equal(payload.goal.title, "Local Goal Board Surface");
|
|
14
14
|
assert.equal(payload.goal.activeTask, "");
|
|
15
15
|
assert.equal(payload.counts.total, 14);
|
|
16
16
|
assert.equal(payload.counts.todo, 0);
|
|
@@ -23,8 +23,20 @@ test("normalizes a dense goal into local board columns", () => {
|
|
|
23
23
|
assert.equal(scout.receipt.summary, "T001 completed during the progressive board motion demo.");
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
test("orders completed cards newest first while preserving queued order", () => {
|
|
27
|
+
const columns = buildColumns([
|
|
28
|
+
{ id: "T001", column: "completed", status: "done" },
|
|
29
|
+
{ id: "T002", column: "todo", status: "queued" },
|
|
30
|
+
{ id: "T003", column: "completed", status: "done" },
|
|
31
|
+
{ id: "T004", column: "todo", status: "queued" },
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
assert.deepEqual(columns.find((column) => column.id === "todo").tasks.map((task) => task.id), ["T002", "T004"]);
|
|
35
|
+
assert.deepEqual(columns.find((column) => column.id === "completed").tasks.map((task) => task.id), ["T003", "T001"]);
|
|
36
|
+
});
|
|
37
|
+
|
|
26
38
|
test("loads depth-1 subgoal boards into parent task payloads", () => {
|
|
27
|
-
const payload = createBoardPayload(resolve("goalbuddy/
|
|
39
|
+
const payload = createBoardPayload(resolve("goalbuddy/surfaces/local-goal-board/examples/subgoal-parent"));
|
|
28
40
|
const parentTask = payload.tasks.find((task) => task.id === "T004");
|
|
29
41
|
|
|
30
42
|
assert.equal(parentTask.subgoal.status, "active");
|
|
@@ -37,10 +49,10 @@ test("loads depth-1 subgoal boards into parent task payloads", () => {
|
|
|
37
49
|
assert.equal(parentTask.subgoal.board.tasks.find((task) => task.id === "T002").subgoal, null);
|
|
38
50
|
});
|
|
39
51
|
|
|
40
|
-
test("uses
|
|
41
|
-
const root = mkdtempSync(join(tmpdir(), "goalbuddy-
|
|
52
|
+
test("uses readable card titles while preserving full objectives", () => {
|
|
53
|
+
const root = mkdtempSync(join(tmpdir(), "goalbuddy-readable-titles-"));
|
|
42
54
|
try {
|
|
43
|
-
const goalDir = join(root, "
|
|
55
|
+
const goalDir = join(root, "readable-titles");
|
|
44
56
|
mkdirSync(join(goalDir, "notes"), { recursive: true });
|
|
45
57
|
writeFileSync(join(goalDir, "state.yaml"), `version: 2
|
|
46
58
|
goal:
|
|
@@ -70,6 +82,13 @@ tasks:
|
|
|
70
82
|
status: queued
|
|
71
83
|
objective: "This objective can stay much more detailed because it belongs in the modal, not on the card face."
|
|
72
84
|
receipt: null
|
|
85
|
+
- id: T004
|
|
86
|
+
title: "Run installed-Cursor runtime proof for a named model request through the local BYOK bridge"
|
|
87
|
+
type: worker
|
|
88
|
+
assignee: Worker
|
|
89
|
+
status: queued
|
|
90
|
+
objective: "Run installed-Cursor runtime proof for a named model request through the local BYOK bridge."
|
|
91
|
+
receipt: null
|
|
73
92
|
`);
|
|
74
93
|
|
|
75
94
|
const payload = createBoardPayload(goalDir);
|
|
@@ -77,6 +96,10 @@ tasks:
|
|
|
77
96
|
assert.equal(payload.tasks.find((task) => task.id === "T001").objective.includes("admin_seed_metrics.enrichment_qa"), true);
|
|
78
97
|
assert.equal(payload.tasks.find((task) => task.id === "T002").title, "Implement /contacts/con_aaron_keller route");
|
|
79
98
|
assert.equal(payload.tasks.find((task) => task.id === "T003").title, "Human-friendly release title");
|
|
99
|
+
assert.equal(
|
|
100
|
+
payload.tasks.find((task) => task.id === "T004").title,
|
|
101
|
+
"Run installed-Cursor runtime proof for a named model request through the local BYOK bridge",
|
|
102
|
+
);
|
|
80
103
|
} finally {
|
|
81
104
|
rmSync(root, { recursive: true, force: true });
|
|
82
105
|
}
|
|
@@ -229,7 +252,7 @@ tasks:
|
|
|
229
252
|
});
|
|
230
253
|
|
|
231
254
|
test("writes a minimal GoalBuddy web app into the goal directory", () => {
|
|
232
|
-
const appDir = writeBoardApp(resolve("
|
|
255
|
+
const appDir = writeBoardApp(resolve("goalbuddy/surfaces/local-goal-board/examples/sample-goal"));
|
|
233
256
|
const html = readFileSync(join(appDir, "index.html"), "utf8");
|
|
234
257
|
const css = readFileSync(join(appDir, "styles.css"), "utf8");
|
|
235
258
|
const js = readFileSync(join(appDir, "app.js"), "utf8");
|
|
@@ -249,6 +272,7 @@ test("writes a minimal GoalBuddy web app into the goal directory", () => {
|
|
|
249
272
|
assert.match(css, /:root\[data-theme="dark"\]/);
|
|
250
273
|
assert.match(css, /:root\[data-density="compact"\] \.task-card/);
|
|
251
274
|
assert.match(css, /:root\[data-completed-visibility="collapse"\]/);
|
|
275
|
+
assert.match(css, /-webkit-line-clamp: 5/);
|
|
252
276
|
assert.match(css, /\.subgoal-board/);
|
|
253
277
|
assert.match(css, /\.board-error/);
|
|
254
278
|
assert.match(js, /new EventSource\("\.\/events"\)/);
|
|
@@ -393,20 +417,20 @@ test("advertises goalbuddy.localhost while binding to loopback", async () => {
|
|
|
393
417
|
test("runs when installed under a symlinked temp path", () => {
|
|
394
418
|
const root = mkdtempSync(join(tmpdir(), "goalbuddy-local-board-direct-"));
|
|
395
419
|
try {
|
|
396
|
-
cpSync("
|
|
397
|
-
cpSync("
|
|
420
|
+
cpSync("goalbuddy/surfaces/local-goal-board/scripts", join(root, "scripts"), { recursive: true });
|
|
421
|
+
cpSync("goalbuddy/surfaces/local-goal-board/assets", join(root, "assets"), { recursive: true });
|
|
398
422
|
|
|
399
423
|
const result = spawnSync(process.execPath, [
|
|
400
424
|
join(root, "scripts", "local-goal-board.mjs"),
|
|
401
425
|
"--goal",
|
|
402
|
-
resolve("
|
|
426
|
+
resolve("goalbuddy/surfaces/local-goal-board/examples/sample-goal"),
|
|
403
427
|
"--once",
|
|
404
428
|
"--json",
|
|
405
429
|
], { encoding: "utf8" });
|
|
406
430
|
|
|
407
431
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
408
432
|
const report = JSON.parse(result.stdout);
|
|
409
|
-
assert.equal(report.board.goal.title, "Local
|
|
433
|
+
assert.equal(report.board.goal.title, "Local Goal Board Surface");
|
|
410
434
|
} finally {
|
|
411
435
|
rmSync(root, { recursive: true, force: true });
|
|
412
436
|
}
|
|
@@ -558,6 +582,33 @@ test("serves multiple local boards from one shared hub URL", async () => {
|
|
|
558
582
|
}
|
|
559
583
|
});
|
|
560
584
|
|
|
585
|
+
test("unregistered board paths explain hub reuse instead of stale-port cleanup", async () => {
|
|
586
|
+
const root = mkdtempSync(join(tmpdir(), "goalbuddy-local-board-unregistered-"));
|
|
587
|
+
const goalDir = join(root, "first-goal");
|
|
588
|
+
try {
|
|
589
|
+
mkdirSync(join(goalDir, "notes"), { recursive: true });
|
|
590
|
+
writeFileSync(join(goalDir, "state.yaml"), stateYaml("active", { title: "First Goal", slug: "first-goal" }));
|
|
591
|
+
|
|
592
|
+
const server = await startBoardServer({ goalDir, host: "127.0.0.1", port: 0 });
|
|
593
|
+
try {
|
|
594
|
+
const baseUrl = new URL(server.url).origin;
|
|
595
|
+
const missingResponse = await fetch(`${baseUrl}/rinova-client-revision-redesign/`);
|
|
596
|
+
assert.equal(missingResponse.status, 404);
|
|
597
|
+
const message = await missingResponse.text();
|
|
598
|
+
assert.match(message, /board path is not registered/i);
|
|
599
|
+
assert.match(message, /multi-board hub/i);
|
|
600
|
+
assert.match(message, /Do not stop it just because a \/<slug>\/ board URL returned 404/);
|
|
601
|
+
assert.match(message, /npx goalbuddy board <goal-dir>/);
|
|
602
|
+
assert.match(message, /First Goal/);
|
|
603
|
+
assert.match(message, /\/api\/boards/);
|
|
604
|
+
} finally {
|
|
605
|
+
await server.close();
|
|
606
|
+
}
|
|
607
|
+
} finally {
|
|
608
|
+
rmSync(root, { recursive: true, force: true });
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
|
|
561
612
|
async function readUntil(reader, pattern) {
|
|
562
613
|
const decoder = new TextDecoder();
|
|
563
614
|
let text = "";
|
|
@@ -15,10 +15,19 @@
|
|
|
15
15
|
- Authority: `requested | approved | inferred | needs_approval | blocked`
|
|
16
16
|
- Proof type: `test | demo | artifact | metric | review | source_backed_answer | decision`
|
|
17
17
|
- Completion proof: <observable signal that closes the full original outcome>
|
|
18
|
+
- Goal oracle: <live check, walkthrough, artifact, metric, source-backed answer, or decision that keeps pressure on the goal>
|
|
18
19
|
- Likely misfire: <how GoalBuddy could succeed at the wrong thing>
|
|
19
20
|
- Blind spots considered: <risks, unstated choices, or success dimensions surfaced during diagnostic intake>
|
|
20
21
|
- Existing plan facts: <user-provided steps/files/constraints/sequencing to preserve and validate, or none>
|
|
21
22
|
|
|
23
|
+
## Goal Oracle
|
|
24
|
+
|
|
25
|
+
The oracle for this goal is:
|
|
26
|
+
|
|
27
|
+
`<specific observable signal>`
|
|
28
|
+
|
|
29
|
+
The PM must keep comparing task receipts to this oracle. Planning, discovery, a passing tiny slice, or a clean-looking board is not enough. The goal finishes only when a final Judge/PM audit maps receipts and verification back to this oracle and records `full_outcome_complete: true`.
|
|
30
|
+
|
|
22
31
|
## Goal Kind
|
|
23
32
|
|
|
24
33
|
`specific | open_ended | existing_plan | recovery | audit`
|
|
@@ -9,6 +9,10 @@ goal:
|
|
|
9
9
|
kind: open_ended # specific | open_ended | existing_plan | recovery | audit
|
|
10
10
|
tranche: "<continuous execution: complete successive safe verified slices until the full original outcome is complete>"
|
|
11
11
|
status: active # active | blocked | done
|
|
12
|
+
oracle:
|
|
13
|
+
signal: "<live check, walkthrough, artifact, metric, source-backed answer, or decision that proves the owner outcome>"
|
|
14
|
+
cadence: "after each Worker package and at final audit"
|
|
15
|
+
final_proof: "<receipt-backed evidence required before full_outcome_complete: true>"
|
|
12
16
|
intake:
|
|
13
17
|
original_request: "<shortest faithful user request>"
|
|
14
18
|
interpreted_outcome: "<one sentence>"
|
|
@@ -33,6 +37,8 @@ rules:
|
|
|
33
37
|
missing_input_or_credentials_do_not_stop_goal: true
|
|
34
38
|
preserve_and_validate_existing_plan: true
|
|
35
39
|
intake_misfire_must_be_audited: true
|
|
40
|
+
goal_pressure_requires_oracle: true
|
|
41
|
+
no_completion_on_weak_proof: true
|
|
36
42
|
slice_policy:
|
|
37
43
|
max_consecutive_tiny_tasks: 2
|
|
38
44
|
prefer_vertical_slices: true
|
|
@@ -47,17 +53,12 @@ agents:
|
|
|
47
53
|
judge: unknown
|
|
48
54
|
|
|
49
55
|
visual_board:
|
|
50
|
-
# none | local |
|
|
56
|
+
# none | local | unknown
|
|
51
57
|
selected: unknown
|
|
52
58
|
local:
|
|
53
59
|
status: not_requested # not_requested | starting | live | generated | blocked
|
|
54
60
|
url: null
|
|
55
61
|
command: "npx goalbuddy board docs/goals/<goal-slug>"
|
|
56
|
-
github_projects:
|
|
57
|
-
status: not_requested # not_requested | needs_approval | dry_run_ready | synced | blocked
|
|
58
|
-
url: null
|
|
59
|
-
command: "npx goalbuddy extend github-projects"
|
|
60
|
-
missing: []
|
|
61
62
|
|
|
62
63
|
active_task: T001
|
|
63
64
|
|
|
Binary file
|