goalbuddy 0.3.5 → 0.3.7

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 (83) hide show
  1. package/README.md +46 -12
  2. package/RELEASE-0.3.5.md +4 -4
  3. package/RELEASE-0.3.7.md +127 -0
  4. package/goalbuddy/SKILL.md +53 -23
  5. package/goalbuddy/agents/README.md +1 -1
  6. package/goalbuddy/agents/goal_judge.toml +8 -4
  7. package/goalbuddy/agents/goal_worker.toml +8 -5
  8. package/goalbuddy/scripts/check-goal-state.mjs +129 -0
  9. package/goalbuddy/scripts/render-task-prompt.mjs +83 -5
  10. package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/README.md +7 -9
  11. package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/examples/sample-goal/state.yaml +5 -5
  12. package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/examples/subgoal-parent/state.yaml +3 -3
  13. package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/state.yaml +3 -3
  14. package/goalbuddy/{extend → surfaces}/local-goal-board/scripts/lib/goal-board.mjs +250 -9
  15. package/goalbuddy/{extend → surfaces}/local-goal-board/scripts/local-goal-board.mjs +4 -4
  16. package/goalbuddy/{extend → surfaces}/local-goal-board/test/local-goal-board.test.mjs +67 -9
  17. package/goalbuddy/templates/agents.md +3 -2
  18. package/goalbuddy/templates/goal.md +27 -4
  19. package/goalbuddy/templates/state.yaml +13 -7
  20. package/internal/assets/goalbuddy-v0.3.7-release.png +0 -0
  21. package/internal/cli/goal-maker.mjs +112 -714
  22. package/package.json +4 -4
  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/agents/goal-judge.md +8 -4
  27. package/plugins/goalbuddy/agents/goal-worker.md +6 -4
  28. package/plugins/goalbuddy/skills/goalbuddy/SKILL.md +53 -23
  29. package/plugins/goalbuddy/skills/goalbuddy/agents/README.md +1 -1
  30. package/plugins/goalbuddy/skills/goalbuddy/agents/goal_judge.toml +8 -4
  31. package/plugins/goalbuddy/skills/goalbuddy/agents/goal_worker.toml +8 -5
  32. package/plugins/goalbuddy/skills/goalbuddy/scripts/check-goal-state.mjs +129 -0
  33. package/plugins/goalbuddy/skills/goalbuddy/scripts/render-task-prompt.mjs +83 -5
  34. package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/README.md +7 -9
  35. package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/examples/sample-goal/state.yaml +5 -5
  36. package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/examples/subgoal-parent/state.yaml +3 -3
  37. package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/state.yaml +3 -3
  38. package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/scripts/lib/goal-board.mjs +250 -9
  39. package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/scripts/local-goal-board.mjs +4 -4
  40. package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/test/local-goal-board.test.mjs +67 -9
  41. package/plugins/goalbuddy/skills/goalbuddy/templates/agents.md +3 -2
  42. package/plugins/goalbuddy/skills/goalbuddy/templates/goal.md +27 -4
  43. package/plugins/goalbuddy/skills/goalbuddy/templates/state.yaml +13 -7
  44. package/examples/extend-catalog-workflow/goal.md +0 -53
  45. package/examples/extend-catalog-workflow/notes/T001-extension-model-map.md +0 -47
  46. package/examples/extend-catalog-workflow/notes/T002-architecture-decision.md +0 -48
  47. package/examples/extend-catalog-workflow/notes/T003-implementation-summary.md +0 -43
  48. package/examples/extend-catalog-workflow/notes/T004-root-extend-folder.md +0 -24
  49. package/examples/extend-catalog-workflow/notes/T005-layout-cleanup.md +0 -46
  50. package/examples/extend-catalog-workflow/notes/T006-catalog-location.md +0 -50
  51. package/examples/extend-catalog-workflow/notes/T999-completion-audit.md +0 -36
  52. package/examples/extend-catalog-workflow/state.yaml +0 -327
  53. package/examples/github-pr-workflow-extension/pr-handoff.md +0 -46
  54. package/goalbuddy/extend/github-projects/README.md +0 -105
  55. package/goalbuddy/extend/github-projects/examples/goal-board-sync/state.yaml +0 -63
  56. package/goalbuddy/extend/github-projects/extension.yaml +0 -43
  57. package/goalbuddy/extend/github-projects/scripts/lib/github-projects.mjs +0 -728
  58. package/goalbuddy/extend/github-projects/scripts/lib/goal-state.mjs +0 -362
  59. package/goalbuddy/extend/github-projects/scripts/sync-github-project.mjs +0 -193
  60. package/goalbuddy/extend/github-projects/test/github-projects.test.mjs +0 -267
  61. package/goalbuddy/extend/local-goal-board/extension.yaml +0 -39
  62. package/internal/assets/extend-release.png +0 -0
  63. package/internal/assets/extend-release.svg +0 -83
  64. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/README.md +0 -105
  65. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/examples/goal-board-sync/state.yaml +0 -63
  66. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/extension.yaml +0 -43
  67. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/lib/github-projects.mjs +0 -728
  68. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/lib/goal-state.mjs +0 -362
  69. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/sync-github-project.mjs +0 -193
  70. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/test/github-projects.test.mjs +0 -267
  71. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/extension.yaml +0 -39
  72. /package/goalbuddy/{extend → surfaces}/local-goal-board/assets/goalbuddy-mark.png +0 -0
  73. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/sample-goal/notes/T001-scout.md +0 -0
  74. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/goal.md +0 -0
  75. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/notes/.gitkeep +0 -0
  76. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/goal.md +0 -0
  77. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/notes/.gitkeep +0 -0
  78. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/assets/goalbuddy-mark.png +0 -0
  79. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/sample-goal/notes/T001-scout.md +0 -0
  80. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/goal.md +0 -0
  81. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/notes/.gitkeep +0 -0
  82. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/goal.md +0 -0
  83. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/notes/.gitkeep +0 -0
@@ -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) {
@@ -110,7 +110,7 @@ export function normalizeTask(task, index) {
110
110
  }
111
111
 
112
112
  const id = cleanText(task.id);
113
- const status = cleanText(task.status);
113
+ const status = normalizeTaskStatus(task.status);
114
114
  if (!id) throw new GoalBoardError(`Task ${index + 1} is missing id.`);
115
115
  if (!VALID_STATUSES.has(status)) {
116
116
  throw new GoalBoardError(`Task ${id} has unsupported status "${status}".`);
@@ -332,14 +332,222 @@ function cleanText(value) {
332
332
  return String(value ?? "").trim();
333
333
  }
334
334
 
335
+ function normalizeTaskStatus(value) {
336
+ const status = cleanText(value);
337
+ if (status === "complete" || status === "completed") return "done";
338
+ return status;
339
+ }
340
+
335
341
  export function parseGoalStateText(text) {
336
- const lines = tokenizeYaml(text);
337
- if (!lines.length) throw new GoalBoardError("Goal state is empty.");
338
- const [value, nextIndex] = parseBlock(lines, 0, lines[0].indent);
339
- if (nextIndex < lines.length) {
340
- throw new GoalBoardError(`Could not parse line ${lines[nextIndex].number}.`);
342
+ try {
343
+ const lines = tokenizeYaml(text);
344
+ if (!lines.length) throw new GoalBoardError("Goal state is empty.");
345
+ const [value, nextIndex] = parseBlock(lines, 0, lines[0].indent);
346
+ if (nextIndex < lines.length) {
347
+ throw new GoalBoardError(`Could not parse line ${lines[nextIndex].number}.`);
348
+ }
349
+ return value;
350
+ } catch (error) {
351
+ if (error instanceof GoalBoardError && canRecoverBoardSubset(error)) {
352
+ return parseGoalBoardSubset(text);
353
+ }
354
+ throw error;
341
355
  }
342
- return value;
356
+ }
357
+
358
+ function canRecoverBoardSubset(error) {
359
+ return /Could not parse line|Expected key\/value pair|Expected mapping|Block scalar YAML/.test(error.message);
360
+ }
361
+
362
+ function parseGoalBoardSubset(text) {
363
+ const tasks = parseTaskSubsets(text);
364
+ if (!tasks.length) throw new GoalBoardError("Missing non-empty tasks list.");
365
+ return {
366
+ version: parseYamlScalar(findTopLevelScalar(text, "version") || "2"),
367
+ goal: {
368
+ title: parseYamlScalar(findNestedScalar(text, "goal", "title") || "Untitled goal"),
369
+ slug: parseYamlScalar(findNestedScalar(text, "goal", "slug") || "untitled-goal"),
370
+ kind: parseYamlScalar(findNestedScalar(text, "goal", "kind") || "open_ended"),
371
+ tranche: parseYamlScalar(findNestedScalar(text, "goal", "tranche") || ""),
372
+ status: parseYamlScalar(findNestedScalar(text, "goal", "status") || "active"),
373
+ },
374
+ active_task: parseYamlScalar(findTopLevelScalar(text, "active_task") || ""),
375
+ tasks,
376
+ };
377
+ }
378
+
379
+ function parseTaskSubsets(text) {
380
+ const tasksText = findTopLevelSection(text, "tasks");
381
+ if (!tasksText) return [];
382
+ const taskBlocks = [];
383
+ let current = [];
384
+ for (const line of tasksText.split("\n")) {
385
+ if (/^ - id:/.test(line)) {
386
+ if (current.length) taskBlocks.push(current.join("\n"));
387
+ current = [line];
388
+ } else if (current.length) {
389
+ current.push(line);
390
+ }
391
+ }
392
+ if (current.length) taskBlocks.push(current.join("\n"));
393
+ return taskBlocks.map((block) => ({
394
+ id: parseYamlScalar(findTaskScalar(block, "id") || ""),
395
+ type: parseYamlScalar(findTaskScalar(block, "type") || "pm"),
396
+ assignee: parseYamlScalar(findTaskScalar(block, "assignee") || ""),
397
+ status: parseYamlScalar(findTaskScalar(block, "status") || "queued"),
398
+ title: parseYamlScalar(findTaskScalar(block, "title") || ""),
399
+ objective: parseYamlScalar(findTaskScalar(block, "objective") || ""),
400
+ inputs: findTaskList(block, "inputs"),
401
+ constraints: findTaskList(block, "constraints"),
402
+ expected_output: findTaskList(block, "expected_output"),
403
+ allowed_files: findTaskList(block, "allowed_files"),
404
+ verify: findTaskList(block, "verify"),
405
+ stop_if: findTaskList(block, "stop_if"),
406
+ subgoal: findTaskSubgoal(block),
407
+ receipt: findTaskReceipt(block),
408
+ }));
409
+ }
410
+
411
+ function findTopLevelScalar(text, key) {
412
+ return findScalar(text, new RegExp(`^${escapeRegExp(key)}:\\s*(.*?)\\s*$`, "m"));
413
+ }
414
+
415
+ function findNestedScalar(text, section, key) {
416
+ return findScalar(findTopLevelSection(text, section), new RegExp(`^ ${escapeRegExp(key)}:\\s*(.*?)\\s*$`, "m"));
417
+ }
418
+
419
+ function findTaskScalar(text, key) {
420
+ if (key === "id") return findScalar(text, /^ - id:\s*(.*?)\s*$/m);
421
+ return findScalar(text, new RegExp(`^ ${escapeRegExp(key)}:\\s*(.*?)\\s*$`, "m"));
422
+ }
423
+
424
+ function findScalar(text, pattern) {
425
+ const match = String(text || "").match(pattern);
426
+ return match ? match[1] : "";
427
+ }
428
+
429
+ function findTopLevelSection(text, key) {
430
+ const lines = String(text || "").replace(/\r\n/g, "\n").split("\n");
431
+ const start = lines.findIndex((line) => line.trim() === `${key}:`);
432
+ if (start === -1) return "";
433
+ const section = [];
434
+ for (let index = start + 1; index < lines.length; index += 1) {
435
+ const line = lines[index];
436
+ if (/^\S/.test(line)) break;
437
+ section.push(line);
438
+ }
439
+ return section.join("\n");
440
+ }
441
+
442
+ function findIndentedSection(text, key, indent) {
443
+ const lines = String(text || "").replace(/\r\n/g, "\n").split("\n");
444
+ const prefix = " ".repeat(indent);
445
+ const start = lines.findIndex((line) => line.trim() === `${key}:` && line.startsWith(prefix));
446
+ if (start === -1) return "";
447
+ const section = [];
448
+ for (let index = start + 1; index < lines.length; index += 1) {
449
+ const line = lines[index];
450
+ if (line.trim() && !line.startsWith(`${prefix} `)) break;
451
+ section.push(line);
452
+ }
453
+ return section.join("\n");
454
+ }
455
+
456
+ function findTaskList(text, key) {
457
+ const inline = findTaskScalar(text, key);
458
+ if (inline) {
459
+ const parsed = parseYamlScalar(inline);
460
+ if (Array.isArray(parsed)) return parsed.map(cleanText).filter(Boolean);
461
+ return cleanText(parsed) ? [cleanText(parsed)] : [];
462
+ }
463
+ const section = findIndentedSection(text, key, 4);
464
+ return section
465
+ .split("\n")
466
+ .map((line) => line.match(/^ -\s*(.*?)\s*$/)?.[1] || "")
467
+ .map(parseYamlScalar)
468
+ .map(cleanText)
469
+ .filter(Boolean);
470
+ }
471
+
472
+ function findTaskSubgoal(text) {
473
+ const inline = findTaskScalar(text, "subgoal");
474
+ if (inline && parseYamlScalar(inline) === null) return null;
475
+ const section = findIndentedSection(text, "subgoal", 4);
476
+ if (!section) return null;
477
+ return {
478
+ status: parseYamlScalar(findScalar(section, /^ status:\s*(.*?)\s*$/m) || "active"),
479
+ path: parseYamlScalar(findScalar(section, /^ path:\s*(.*?)\s*$/m) || ""),
480
+ owner: parseYamlScalar(findScalar(section, /^ owner:\s*(.*?)\s*$/m) || ""),
481
+ created_from: parseYamlScalar(findScalar(section, /^ created_from:\s*(.*?)\s*$/m) || ""),
482
+ depth: parseYamlScalar(findScalar(section, /^ depth:\s*(.*?)\s*$/m) || "1"),
483
+ rollup_receipt: parseYamlScalar(findScalar(section, /^ rollup_receipt:\s*(.*?)\s*$/m) || "null"),
484
+ };
485
+ }
486
+
487
+ function findTaskReceipt(text) {
488
+ const inline = findTaskScalar(text, "receipt");
489
+ if (inline && parseYamlScalar(inline) === null) return null;
490
+ const section = findIndentedSection(text, "receipt", 4);
491
+ if (!section) return null;
492
+ return {
493
+ result: parseYamlScalar(findScalar(section, /^ result:\s*(.*?)\s*$/m) || ""),
494
+ summary: parseYamlScalar(findScalar(section, /^ summary:\s*(.*?)\s*$/m) || ""),
495
+ decision: parseYamlScalar(findScalar(section, /^ decision:\s*(.*?)\s*$/m) || ""),
496
+ note: parseYamlScalar(findScalar(section, /^ note:\s*(.*?)\s*$/m) || ""),
497
+ changed_files: findReceiptList(section, "changed_files"),
498
+ commands: findReceiptCommands(section),
499
+ evidence: [],
500
+ };
501
+ }
502
+
503
+ function findReceiptList(text, key) {
504
+ const section = findIndentedSection(text, key, 6);
505
+ return section
506
+ .split("\n")
507
+ .map((line) => line.match(/^ -\s*(.*?)\s*$/)?.[1] || "")
508
+ .map(parseYamlScalar)
509
+ .map(cleanText)
510
+ .filter(Boolean);
511
+ }
512
+
513
+ function findReceiptCommands(text) {
514
+ const section = findIndentedSection(text, "commands", 6);
515
+ const blocks = [];
516
+ let current = [];
517
+ for (const line of section.split("\n")) {
518
+ if (/^ - cmd:/.test(line)) {
519
+ if (current.length) blocks.push(current.join("\n"));
520
+ current = [line];
521
+ } else if (current.length) {
522
+ current.push(line);
523
+ }
524
+ }
525
+ if (current.length) blocks.push(current.join("\n"));
526
+ return blocks.map((block) => ({
527
+ cmd: parseYamlScalar(findScalar(block, /^ - cmd:\s*(.*?)\s*$/m) || ""),
528
+ status: parseYamlScalar(findScalar(block, /^ status:\s*(.*?)\s*$/m) || ""),
529
+ note: parseYamlScalar(findScalar(block, /^ note:\s*(.*?)\s*$/m) || ""),
530
+ }));
531
+ }
532
+
533
+ function parseYamlScalar(value) {
534
+ const text = stripComment(String(value ?? "")).trim();
535
+ if (!text) return "";
536
+ try {
537
+ return parseScalar(text);
538
+ } catch {
539
+ if (
540
+ (text.startsWith("\"") && text.endsWith("\"")) ||
541
+ (text.startsWith("'") && text.endsWith("'"))
542
+ ) {
543
+ return text.slice(1, -1);
544
+ }
545
+ return text;
546
+ }
547
+ }
548
+
549
+ function escapeRegExp(value) {
550
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
343
551
  }
344
552
 
345
553
  function tokenizeYaml(text) {
@@ -1326,6 +1534,25 @@ h1 {
1326
1534
  font-size: 14px;
1327
1535
  }
1328
1536
 
1537
+ .board-error {
1538
+ grid-column: 1 / -1;
1539
+ padding: 18px;
1540
+ border: 1px solid var(--red-border);
1541
+ border-radius: 8px;
1542
+ background: var(--red-bg);
1543
+ color: var(--text);
1544
+ }
1545
+
1546
+ .board-error h2 {
1547
+ margin: 0 0 8px;
1548
+ font-size: 16px;
1549
+ }
1550
+
1551
+ .board-error p {
1552
+ margin: 0;
1553
+ color: var(--muted);
1554
+ }
1555
+
1329
1556
  @media (prefers-reduced-motion: reduce) {
1330
1557
  .github-stars,
1331
1558
  .settings-button,
@@ -1825,6 +2052,11 @@ function renderBoard(board) {
1825
2052
  document.getElementById("goal-active").textContent = board.goal.activeTask || "None";
1826
2053
  document.getElementById("goal-updated").textContent = new Date(board.generatedAt).toLocaleTimeString();
1827
2054
 
2055
+ if (board.error) {
2056
+ boardEl.replaceChildren(renderBoardError(board.error));
2057
+ return;
2058
+ }
2059
+
1828
2060
  const delay = movingTaskIds.size ? 260 : 0;
1829
2061
  window.setTimeout(() => {
1830
2062
  boardEl.replaceChildren(...board.columns.map(renderColumn));
@@ -1832,6 +2064,15 @@ function renderBoard(board) {
1832
2064
  }, delay);
1833
2065
  }
1834
2066
 
2067
+ function renderBoardError(message) {
2068
+ const node = el("section", "board-error");
2069
+ node.append(
2070
+ el("h2", "", "GoalBuddy could not parse this board"),
2071
+ el("p", "", message),
2072
+ );
2073
+ return node;
2074
+ }
2075
+
1835
2076
  function renderBoardSwitcher(boards) {
1836
2077
  boardSwitcherEl.closest(".board-switcher").classList.toggle("is-empty", boards.length <= 1);
1837
2078
  const currentPath = normalizePath(window.location.pathname);
@@ -527,9 +527,9 @@ function serveStatic(appDir, pathname, response) {
527
527
  return;
528
528
  }
529
529
 
530
- const extension = cleanPath.match(/\.[^.]+$/)?.[0] || "";
530
+ const fileExtension = cleanPath.match(/\.[^.]+$/)?.[0] || "";
531
531
  response.writeHead(200, {
532
- "Content-Type": textTypes[extension] || "application/octet-stream",
532
+ "Content-Type": textTypes[fileExtension] || "application/octet-stream",
533
533
  "Cache-Control": "no-store",
534
534
  });
535
535
  response.end(readFileSync(file));
@@ -580,8 +580,8 @@ function usage() {
580
580
  console.log(`GoalBuddy Local Goal Board
581
581
 
582
582
  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
583
+ npx goalbuddy board docs/goals/<slug>
584
+ npx goalbuddy board docs/goals/<slug> --once --json
585
585
 
586
586
  Options:
587
587
  --goal <path> Goal directory containing state.yaml.
@@ -8,9 +8,9 @@ import { 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);
@@ -24,7 +24,7 @@ test("normalizes a dense goal into local board columns", () => {
24
24
  });
25
25
 
26
26
  test("loads depth-1 subgoal boards into parent task payloads", () => {
27
- const payload = createBoardPayload(resolve("goalbuddy/extend/local-goal-board/examples/subgoal-parent"));
27
+ const payload = createBoardPayload(resolve("goalbuddy/surfaces/local-goal-board/examples/subgoal-parent"));
28
28
  const parentTask = payload.tasks.find((task) => task.id === "T004");
29
29
 
30
30
  assert.equal(parentTask.subgoal.status, "active");
@@ -82,6 +82,62 @@ tasks:
82
82
  }
83
83
  });
84
84
 
85
+ test("keeps board rendering when deep receipt YAML is malformed", () => {
86
+ const root = mkdtempSync(join(tmpdir(), "goalbuddy-board-subset-parser-"));
87
+ try {
88
+ const goalDir = join(root, "subset-parser");
89
+ mkdirSync(join(goalDir, "notes"), { recursive: true });
90
+ writeFileSync(join(goalDir, "state.yaml"), `version: 2
91
+ goal:
92
+ title: "Subset parser"
93
+ slug: "subset-parser"
94
+ kind: specific
95
+ tranche: "Recover shallow board fields."
96
+ status: active
97
+ active_task: T003
98
+ checks:
99
+ last_verification:
100
+ status: pass
101
+ raw:
102
+ malformed nested checker output
103
+ tasks:
104
+ - id: T001
105
+ type: worker
106
+ assignee: Worker
107
+ status: completed
108
+ objective: "Ship a completed worker slice."
109
+ receipt:
110
+ result: done
111
+ summary: "Worker finished."
112
+ raw:
113
+ malformed nested receipt output
114
+ - id: T002
115
+ type: judge
116
+ assignee: Judge
117
+ status: complete
118
+ objective: "Approve the result."
119
+ receipt: null
120
+ - id: T003
121
+ type: scout
122
+ assignee: Scout
123
+ status: active
124
+ objective: "Inspect what is left."
125
+ receipt: null
126
+ `);
127
+
128
+ const payload = createBoardPayload(goalDir);
129
+ assert.equal(payload.goal.title, "Subset parser");
130
+ assert.equal(payload.goal.activeTask, "T003");
131
+ assert.equal(payload.counts.completed, 2);
132
+ assert.equal(payload.counts.inProgress, 1);
133
+ assert.equal(payload.tasks.find((task) => task.id === "T001").status, "done");
134
+ assert.equal(payload.tasks.find((task) => task.id === "T002").status, "done");
135
+ assert.equal(payload.tasks.find((task) => task.id === "T001").receipt.summary, "Worker finished.");
136
+ } finally {
137
+ rmSync(root, { recursive: true, force: true });
138
+ }
139
+ });
140
+
85
141
  test("fails loudly when a linked subgoal state file is missing", () => {
86
142
  const root = mkdtempSync(join(tmpdir(), "goalbuddy-missing-subgoal-"));
87
143
  try {
@@ -173,7 +229,7 @@ tasks:
173
229
  });
174
230
 
175
231
  test("writes a minimal GoalBuddy web app into the goal directory", () => {
176
- const appDir = writeBoardApp(resolve("extend/local-goal-board/examples/sample-goal"));
232
+ const appDir = writeBoardApp(resolve("goalbuddy/surfaces/local-goal-board/examples/sample-goal"));
177
233
  const html = readFileSync(join(appDir, "index.html"), "utf8");
178
234
  const css = readFileSync(join(appDir, "styles.css"), "utf8");
179
235
  const js = readFileSync(join(appDir, "app.js"), "utf8");
@@ -194,6 +250,7 @@ test("writes a minimal GoalBuddy web app into the goal directory", () => {
194
250
  assert.match(css, /:root\[data-density="compact"\] \.task-card/);
195
251
  assert.match(css, /:root\[data-completed-visibility="collapse"\]/);
196
252
  assert.match(css, /\.subgoal-board/);
253
+ assert.match(css, /\.board-error/);
197
254
  assert.match(js, /new EventSource\("\.\/events"\)/);
198
255
  assert.match(js, /fetch\("\.\.\/api\/boards"/);
199
256
  assert.match(js, /fetch\("\.\.\/api\/settings"/);
@@ -206,6 +263,7 @@ test("writes a minimal GoalBuddy web app into the goal directory", () => {
206
263
  assert.match(js, /card\.animate/);
207
264
  assert.match(js, /highlightMovingCards/);
208
265
  assert.match(js, /renderSubgoal/);
266
+ assert.match(js, /renderBoardError/);
209
267
  assert.match(js, /boardOptionLabel/);
210
268
  assert.match(js, /duration: changedColumn \? 980 : 520/);
211
269
  assert.equal(logo.subarray(1, 4).toString("ascii"), "PNG");
@@ -333,22 +391,22 @@ test("advertises goalbuddy.localhost while binding to loopback", async () => {
333
391
  });
334
392
 
335
393
  test("runs when installed under a symlinked temp path", () => {
336
- const root = mkdtempSync("/tmp/goalbuddy-local-board-direct-");
394
+ const root = mkdtempSync(join(tmpdir(), "goalbuddy-local-board-direct-"));
337
395
  try {
338
- cpSync("extend/local-goal-board/scripts", join(root, "scripts"), { recursive: true });
339
- cpSync("extend/local-goal-board/assets", join(root, "assets"), { recursive: true });
396
+ cpSync("goalbuddy/surfaces/local-goal-board/scripts", join(root, "scripts"), { recursive: true });
397
+ cpSync("goalbuddy/surfaces/local-goal-board/assets", join(root, "assets"), { recursive: true });
340
398
 
341
399
  const result = spawnSync(process.execPath, [
342
400
  join(root, "scripts", "local-goal-board.mjs"),
343
401
  "--goal",
344
- resolve("extend/local-goal-board/examples/sample-goal"),
402
+ resolve("goalbuddy/surfaces/local-goal-board/examples/sample-goal"),
345
403
  "--once",
346
404
  "--json",
347
405
  ], { encoding: "utf8" });
348
406
 
349
407
  assert.equal(result.status, 0, result.stderr || result.stdout);
350
408
  const report = JSON.parse(result.stdout);
351
- assert.equal(report.board.goal.title, "Local Kanban Board Extension");
409
+ assert.equal(report.board.goal.title, "Local Goal Board Surface");
352
410
  } finally {
353
411
  rmSync(root, { recursive: true, force: true });
354
412
  }
@@ -5,7 +5,7 @@ Use three generic agents. The main `/goal` thread remains PM and owns the board.
5
5
  | Agent | model_reasoning_effort | sandbox_mode | Purpose |
6
6
  |---|---:|---|---|
7
7
  | goal_scout | low | read-only | Targeted evidence mapping and candidate facts |
8
- | goal_worker | low | workspace-write | One bounded implementation/recovery task |
8
+ | goal_worker | medium | workspace-write | One coherent bounded implementation/recovery slice |
9
9
  | goal_judge | high | read-only | Strategic review, escalation, completion skepticism |
10
10
 
11
11
  ## PM Thinking Policy
@@ -44,5 +44,6 @@ Rules:
44
44
 
45
45
  - Only the PM loop chooses active tasks, marks tasks done, or completes the goal.
46
46
  - Keep at most one write-capable Worker active unless disjoint write scopes are explicit in `state.yaml`.
47
+ - Worker defaults to medium reasoning for implementation tasks and should complete the whole assigned slice.
47
48
  - Scout and Judge are read-only and safe to parallelize when their board inputs are clear.
48
- - Judge is high thinking.
49
+ - Judge is high thinking and should choose the largest safe useful slice, not the narrowest helper.
@@ -15,17 +15,26 @@
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`
25
34
 
26
35
  ## Current Tranche
27
36
 
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.>
37
+ <What is enough for the full owner outcome, and what is the current largest reversible local work package? For execution goals, the default is continuous: discover enough evidence, choose a coherent work package, implement it, verify it, review only at phase/risk/final boundaries, then immediately advance to the next work package until the full original outcome is complete. Plan-only or one-package-only stopping is valid only when explicitly requested.>
29
38
 
30
39
  ## Non-Negotiable Constraints
31
40
 
@@ -37,7 +46,21 @@ Stop only when a final audit proves the full original outcome is complete.
37
46
 
38
47
  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
48
 
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.
49
+ Do not stop after a single verified Worker package when the broader owner outcome still has safe local follow-up work. Advance the board to the next highest-leverage safe Worker package and continue unless a phase, risk, rejected-verification, ambiguity, or final-completion review is due.
50
+
51
+ Do not create one Worker/Judge pair per repeated file, table, route, or helper. Put repeated same-shape work into one Worker package and review the package as a whole.
52
+
53
+ ## Slice Sizing
54
+
55
+ Safe means bounded, explicit, verified, and reversible. It does not mean tiny.
56
+
57
+ A good task is the largest safe useful slice.
58
+
59
+ Small is not the goal. Useful is the goal.
60
+
61
+ A Worker should finish the whole assigned slice. A Judge should judge the whole assigned slice. A PM should reorient the board when tasks are safe but not moving the outcome.
62
+
63
+ Tiny tasks are allowed when the failure is isolated, the risk is high, the scope is unknown, or the tiny task unlocks a larger slice. Tiny tasks are bad when they keep happening, do not change behavior, only add wrappers/contracts/proof files, or avoid the real milestone.
41
64
 
42
65
  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
66
 
@@ -67,9 +90,9 @@ On every `/goal` continuation:
67
90
  6. Assign Scout, Judge, Worker, or PM according to the task.
68
91
  7. Write a compact task receipt.
69
92
  8. Update the board.
70
- 9. If Judge selected a safe Worker task with `allowed_files`, `verify`, and `stop_if`, activate it and continue unless blocked.
93
+ 9. If safe local work remains, choose the next largest reversible Worker package and continue unless blocked.
71
94
  10. If a problem, suggestion, or follow-up should become a repo artifact, create an approved issue/PR or ask the operator whether to create one.
72
- 11. Treat a slice audit as a checkpoint, not completion, unless it explicitly proves the full original outcome is complete.
95
+ 11. Review at phase, risk, rejected-verification, ambiguity, or final-completion boundaries; do not review every small Worker by habit.
73
96
  12. 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`.
74
97
 
75
98
  Issue and PR handoffs are supporting artifacts. `state.yaml` remains authoritative, and every external artifact decision must be recorded in a task receipt.
@@ -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,13 @@ 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
42
+ slice_policy:
43
+ max_consecutive_tiny_tasks: 2
44
+ prefer_vertical_slices: true
45
+ judge_picks_largest_safe_slice: true
46
+ worker_completes_whole_slice: true
36
47
 
37
48
  agents:
38
49
  # installed | bundled_not_installed | missing | unknown
@@ -42,17 +53,12 @@ agents:
42
53
  judge: unknown
43
54
 
44
55
  visual_board:
45
- # none | local | github_projects | both | unknown
56
+ # none | local | unknown
46
57
  selected: unknown
47
58
  local:
48
59
  status: not_requested # not_requested | starting | live | generated | blocked
49
60
  url: null
50
61
  command: "npx goalbuddy board docs/goals/<goal-slug>"
51
- github_projects:
52
- status: not_requested # not_requested | needs_approval | dry_run_ready | synced | blocked
53
- url: null
54
- command: "npx goalbuddy extend github-projects"
55
- missing: []
56
62
 
57
63
  active_task: T001
58
64
 
@@ -88,7 +94,7 @@ tasks:
88
94
  - "T001 receipt"
89
95
  constraints:
90
96
  - "Do not implement."
91
- - "Pick small reviewable work."
97
+ - "Pick the largest safe useful slice with clear allowed_files, verify commands, and stop conditions."
92
98
  expected_output:
93
99
  - "Decision"
94
100
  - "Exact Worker objective"
@@ -1,53 +0,0 @@
1
- # Extend Catalog Workflow
2
-
3
- ## Objective
4
-
5
- Design and implement Goal Maker's extension catalog workflow so optional features can move through GitHub-hosted extension metadata without requiring frequent npm package releases.
6
-
7
- ## Goal Kind
8
-
9
- `open_ended`
10
-
11
- ## Current Tranche
12
-
13
- Decide the product language and architecture for optional extensions, implement the first catalog-backed `extend` CLI workflow, add a visible root `extend/` surface, clean the skill/package layout, and leave a reviewable completion audit.
14
-
15
- ## Non-Negotiable Constraints
16
-
17
- - `state.yaml` remains the only board truth.
18
- - Extensions are optional support surfaces, not the control plane.
19
- - Use `extend` as the repo surface and `extensions` as the item name.
20
- - Avoid implying bidirectional sync with external services.
21
- - Keep npm as the stable core; catalog entries should be updateable from GitHub.
22
- - Keep package-only infrastructure out of the installable skill payload.
23
- - Do not require provider credentials for discovery or dry-run installation.
24
-
25
- ## Stop Rule
26
-
27
- Stop when the extension/catalog UX is implemented, the repo layout reflects the skill/package boundary, verification passes, and the final audit maps the shipped changes to receipts and commits.
28
-
29
- ## Canonical Board
30
-
31
- Machine truth lives at:
32
-
33
- `examples/extend-catalog-workflow/state.yaml`
34
-
35
- If this charter and `state.yaml` disagree, `state.yaml` wins for task status, active task, receipts, verification freshness, and completion truth.
36
-
37
- ## Run Command
38
-
39
- ```text
40
- /goal Follow examples/extend-catalog-workflow/goal.md
41
- ```
42
-
43
- ## PM Loop
44
-
45
- On every `/goal` continuation:
46
-
47
- 1. Read this charter.
48
- 2. Read `state.yaml`.
49
- 3. Work only on the active board task.
50
- 4. Assign Scout, Judge, Worker, or PM according to the task.
51
- 5. Write a compact task receipt.
52
- 6. Update the board.
53
- 7. Select the next active task or finish with a Judge/PM audit receipt.