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.
Files changed (81) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/CONTRIBUTING.md +2 -2
  3. package/README.md +27 -10
  4. package/{RELEASE-0.3.5.md → docs/releases/0.3.5.md} +4 -4
  5. package/docs/releases/0.3.7.md +129 -0
  6. package/docs/releases/0.3.8.md +40 -0
  7. package/docs/releases/README.md +83 -0
  8. package/goalbuddy/SKILL.md +21 -10
  9. package/goalbuddy/scripts/check-goal-state.mjs +53 -0
  10. package/goalbuddy/scripts/render-task-prompt.mjs +39 -4
  11. package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/README.md +7 -9
  12. package/goalbuddy/{extend → surfaces}/local-goal-board/examples/sample-goal/state.yaml +5 -5
  13. package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/examples/subgoal-parent/state.yaml +3 -3
  14. package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/state.yaml +3 -3
  15. package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/scripts/lib/goal-board.mjs +17 -13
  16. package/goalbuddy/{extend → surfaces}/local-goal-board/scripts/local-goal-board.mjs +27 -6
  17. package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/test/local-goal-board.test.mjs +63 -12
  18. package/goalbuddy/templates/goal.md +9 -0
  19. package/goalbuddy/templates/state.yaml +7 -6
  20. package/internal/assets/goalbuddy-v0.3.7-release.png +0 -0
  21. package/internal/cli/goal-maker.mjs +177 -717
  22. package/package.json +7 -8
  23. package/plugins/goalbuddy/.claude-plugin/plugin.json +3 -4
  24. package/plugins/goalbuddy/.codex-plugin/plugin.json +5 -6
  25. package/plugins/goalbuddy/README.md +4 -3
  26. package/plugins/goalbuddy/skills/goalbuddy/SKILL.md +21 -10
  27. package/plugins/goalbuddy/skills/goalbuddy/scripts/check-goal-state.mjs +53 -0
  28. package/plugins/goalbuddy/skills/goalbuddy/scripts/render-task-prompt.mjs +39 -4
  29. package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/README.md +7 -9
  30. package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/sample-goal/state.yaml +5 -5
  31. package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/examples/subgoal-parent/state.yaml +3 -3
  32. package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/state.yaml +3 -3
  33. package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/scripts/lib/goal-board.mjs +2 -2
  34. package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/scripts/local-goal-board.mjs +27 -6
  35. package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/test/local-goal-board.test.mjs +35 -8
  36. package/plugins/goalbuddy/skills/goalbuddy/templates/goal.md +9 -0
  37. package/plugins/goalbuddy/skills/goalbuddy/templates/state.yaml +7 -6
  38. package/examples/extend-catalog-workflow/goal.md +0 -53
  39. package/examples/extend-catalog-workflow/notes/T001-extension-model-map.md +0 -47
  40. package/examples/extend-catalog-workflow/notes/T002-architecture-decision.md +0 -48
  41. package/examples/extend-catalog-workflow/notes/T003-implementation-summary.md +0 -43
  42. package/examples/extend-catalog-workflow/notes/T004-root-extend-folder.md +0 -24
  43. package/examples/extend-catalog-workflow/notes/T005-layout-cleanup.md +0 -46
  44. package/examples/extend-catalog-workflow/notes/T006-catalog-location.md +0 -50
  45. package/examples/extend-catalog-workflow/notes/T999-completion-audit.md +0 -36
  46. package/examples/extend-catalog-workflow/state.yaml +0 -327
  47. package/examples/github-pr-workflow-extension/pr-handoff.md +0 -46
  48. package/examples/improve-goal-maker/goal.md +0 -51
  49. package/examples/improve-goal-maker/notes/T001-repo-map.md +0 -59
  50. package/examples/improve-goal-maker/notes/T002-risk-map.md +0 -37
  51. package/examples/improve-goal-maker/state.yaml +0 -224
  52. package/goalbuddy/extend/github-projects/README.md +0 -105
  53. package/goalbuddy/extend/github-projects/examples/goal-board-sync/state.yaml +0 -63
  54. package/goalbuddy/extend/github-projects/extension.yaml +0 -43
  55. package/goalbuddy/extend/github-projects/scripts/lib/github-projects.mjs +0 -728
  56. package/goalbuddy/extend/github-projects/scripts/lib/goal-state.mjs +0 -362
  57. package/goalbuddy/extend/github-projects/scripts/sync-github-project.mjs +0 -193
  58. package/goalbuddy/extend/github-projects/test/github-projects.test.mjs +0 -267
  59. package/goalbuddy/extend/local-goal-board/extension.yaml +0 -39
  60. package/internal/assets/extend-release.png +0 -0
  61. package/internal/assets/extend-release.svg +0 -83
  62. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/README.md +0 -105
  63. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/examples/goal-board-sync/state.yaml +0 -63
  64. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/extension.yaml +0 -43
  65. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/lib/github-projects.mjs +0 -728
  66. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/lib/goal-state.mjs +0 -362
  67. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/sync-github-project.mjs +0 -193
  68. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/test/github-projects.test.mjs +0 -267
  69. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/extension.yaml +0 -39
  70. /package/goalbuddy/{extend → surfaces}/local-goal-board/assets/goalbuddy-mark.png +0 -0
  71. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/sample-goal/notes/T001-scout.md +0 -0
  72. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/goal.md +0 -0
  73. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/notes/.gitkeep +0 -0
  74. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/goal.md +0 -0
  75. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/notes/.gitkeep +0 -0
  76. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/assets/goalbuddy-mark.png +0 -0
  77. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/sample-goal/notes/T001-scout.md +0 -0
  78. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/goal.md +0 -0
  79. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/notes/.gitkeep +0 -0
  80. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/goal.md +0 -0
  81. /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 "../extend/local-goal-board/scripts/lib/goal-board.mjs";
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: [{ cmd: "<command>", status: "pass | fail | not_run" }],
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
- summary: "<=120 words",
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
  }
@@ -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 extension 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.
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
- node extend/local-goal-board/scripts/local-goal-board.mjs \
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 after the extension is installed or copied elsewhere.
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
- node extend/local-goal-board/scripts/local-goal-board.mjs \
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 extend/local-goal-board/test/*.test.mjs
67
- node extend/local-goal-board/scripts/local-goal-board.mjs \
68
- --goal extend/local-goal-board/examples/sample-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 Kanban Board Extension"
5
- slug: "local-kanban-board-extension"
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 extension."
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 extension launch paths."
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 extension packaging check."
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/extend/local-goal-board/scripts/lib/goal-board.mjs
35
- - goalbuddy/extend/local-goal-board/test/local-goal-board.test.mjs
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/extend/local-goal-board/test/local-goal-board.test.mjs
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/extend/local-goal-board/scripts/lib/goal-board.mjs
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/extend/local-goal-board/scripts/lib/goal-board.mjs
34
+ - goalbuddy/surfaces/local-goal-board/scripts/lib/goal-board.mjs
35
35
  verify:
36
- - node --test goalbuddy/extend/local-goal-board/test/local-goal-board.test.mjs
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 extensionRoot = resolve(__dirname, "../..");
10
- const logoAssetPath = join(extensionRoot, "assets", "goalbuddy-mark.png");
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.values()) {
146
- columnTasks.sort((left, right) => taskSortKey(left).localeCompare(taskSortKey(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 truncateTitle(`Implement ${routeMatch[1]} ${routeMatch[2]}`);
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 truncateTitle(firstClause || text);
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.writeHead(404);
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 extension = cleanPath.match(/\.[^.]+$/)?.[0] || "";
551
+ const fileExtension = cleanPath.match(/\.[^.]+$/)?.[0] || "";
531
552
  response.writeHead(200, {
532
- "Content-Type": textTypes[extension] || "application/octet-stream",
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
- node extend/local-goal-board/scripts/local-goal-board.mjs --goal docs/goals/<slug>
584
- node extend/local-goal-board/scripts/local-goal-board.mjs --goal docs/goals/<slug> --once --json
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("extend/local-goal-board/examples/sample-goal"));
11
+ const payload = createBoardPayload(resolve("goalbuddy/surfaces/local-goal-board/examples/sample-goal"));
12
12
 
13
- assert.equal(payload.goal.title, "Local Kanban Board Extension");
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/extend/local-goal-board/examples/subgoal-parent"));
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 compact card titles while preserving full objectives", () => {
41
- const root = mkdtempSync(join(tmpdir(), "goalbuddy-compact-titles-"));
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, "compact-titles");
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("extend/local-goal-board/examples/sample-goal"));
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("extend/local-goal-board/scripts", join(root, "scripts"), { recursive: true });
397
- cpSync("extend/local-goal-board/assets", join(root, "assets"), { recursive: true });
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("extend/local-goal-board/examples/sample-goal"),
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 Kanban Board Extension");
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 | github_projects | both | unknown
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