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
@@ -53,6 +53,38 @@ function nestedScalar(section, key) {
53
53
  return null;
54
54
  }
55
55
 
56
+ function pathScalar(path, key) {
57
+ const lines = text.split(/\r?\n/);
58
+ let depth = 0;
59
+ for (const line of lines) {
60
+ if (!line.trim()) continue;
61
+ const indent = line.match(/^ */)[0].length;
62
+ if (indent < depth * 2) depth = Math.floor(indent / 2);
63
+
64
+ if (depth < path.length && indent === depth * 2 && new RegExp(`^\\s{${indent}}${path[depth]}:\\s*$`).test(line)) {
65
+ depth += 1;
66
+ continue;
67
+ }
68
+
69
+ if (depth === path.length && indent === depth * 2) {
70
+ const match = line.match(new RegExp(`^\\s{${indent}}${key}:\\s*(.*?)\\s*$`));
71
+ if (match) return clean(match[1]);
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+
77
+ function isWeakProof(value) {
78
+ if (value === null || value === undefined) return true;
79
+ const normalized = String(value).trim().toLowerCase();
80
+ return normalized === ""
81
+ || normalized === "unknown"
82
+ || normalized === "tbd"
83
+ || normalized === "todo"
84
+ || normalized === "none"
85
+ || /^<.*>$/.test(normalized);
86
+ }
87
+
56
88
  function sectionText(section) {
57
89
  const lines = text.split(/\r?\n/);
58
90
  const start = lines.findIndex((line) => new RegExp(`^${section}:\\s*$`).test(line));
@@ -225,6 +257,11 @@ const allowedAgentStatuses = new Set(["installed", "bundled_not_installed", "mis
225
257
  const continuousUntilFullOutcome = nestedScalar("rules", "continuous_until_full_outcome") === true;
226
258
  const missingInputOrCredentialsDoNotStopGoal =
227
259
  nestedScalar("rules", "missing_input_or_credentials_do_not_stop_goal") === true;
260
+ const goalPressureRequiresOracle = nestedScalar("rules", "goal_pressure_requires_oracle") !== false;
261
+ const noCompletionOnWeakProof = nestedScalar("rules", "no_completion_on_weak_proof") !== false;
262
+ const completionProof = pathScalar(["goal", "intake"], "completion_proof");
263
+ const oracleSignal = pathScalar(["goal", "oracle"], "signal");
264
+ const oracleFinalProof = pathScalar(["goal", "oracle"], "final_proof");
228
265
  const legacySignals = [
229
266
  /^gate:\s*$/m,
230
267
  /^artifact_policy:\s*$/m,
@@ -244,6 +281,19 @@ if (!["active", "blocked", "done"].includes(goalStatus)) {
244
281
  errors.push(`goal.status must be active, blocked, or done; got ${goalStatus || "<missing>"}`);
245
282
  }
246
283
 
284
+ if (goalPressureRequiresOracle) {
285
+ if (isWeakProof(oracleSignal)) {
286
+ warnings.push("goal.oracle.signal is missing or placeholder-like; weak oracles make /goal finish too early.");
287
+ }
288
+ if (isWeakProof(oracleFinalProof)) {
289
+ warnings.push("goal.oracle.final_proof is missing or placeholder-like; final completion needs receipt-backed proof.");
290
+ }
291
+ }
292
+
293
+ if (isWeakProof(completionProof)) {
294
+ warnings.push("goal.intake.completion_proof is missing or placeholder-like; record the observable signal that proves the full original outcome.");
295
+ }
296
+
247
297
  function agentStatusWarning(agent, status) {
248
298
  const agentLabel = agent[0].toUpperCase() + agent.slice(1);
249
299
  if (status === "bundled_not_installed") {
@@ -369,6 +419,9 @@ for (const task of tasks) {
369
419
  errors.push(`Worker receipt for ${task.id} has non-passing command status: ${status}`);
370
420
  }
371
421
  }
422
+ if (task.receipt.scalar("needs_judge") === true) {
423
+ warnings.push(`Worker receipt for ${task.id} requests legacy needs_judge; GoalBuddy now lets the PM continue by default and reviews only at phase, risk, ambiguity, rejected-verification, or final-completion boundaries`);
424
+ }
372
425
  }
373
426
  if (task.type === "scout" && task.status === "done" && hasReceipt) {
374
427
  if (!task.receipt.has("summary")) errors.push(`Scout receipt for ${task.id} missing summary`);
@@ -381,6 +434,8 @@ for (const task of tasks) {
381
434
  }
382
435
  }
383
436
 
437
+ warnings.push(...microSliceWarnings(tasks, activeTask, goalStatus));
438
+
384
439
  function validateSubgoal(task) {
385
440
  if (isChildCheck) {
386
441
  errors.push(`child task ${task.id} must not contain a nested subgoal`);
@@ -430,6 +485,77 @@ function validateSubgoal(task) {
430
485
  }
431
486
  }
432
487
 
488
+ function microSliceWarnings(tasks, activeTaskId, goalStatus) {
489
+ const found = [];
490
+ const guidance = "Board may be micro-slicing. Prefer the largest safe useful slice.";
491
+ const doneTasks = tasks.filter((task) => task.status === "done");
492
+ const workerTasks = tasks.filter((task) => task.type === "worker");
493
+ const recentTinyWorkers = workerTasks.slice(-5).filter((task) => isTinyTask(task));
494
+ const firstMilestoneComplete = nestedScalar("goal", "first_milestone_complete") === true;
495
+
496
+ if (recentTinyWorkers.length >= 3) {
497
+ found.push(`${guidance} Three recent Worker tasks look tiny.`);
498
+ }
499
+
500
+ for (const task of tasks) {
501
+ if (task.type === "judge" && /pick small reviewable work|select one narrow next task/i.test(task.raw)) {
502
+ found.push(`${guidance} Judge instructions still ask for small or narrow work.`);
503
+ break;
504
+ }
505
+ }
506
+
507
+ if (goalStatus !== "active" || !activeTaskId) return [...new Set(found)];
508
+ const activeIndex = tasks.findIndex((task) => task.id === activeTaskId);
509
+ if (activeIndex === -1) return [...new Set(found)];
510
+ const active = tasks[activeIndex];
511
+ if (active.type === "worker") {
512
+ if (doneTasks.length >= 10 && active.allowedFiles.length > 0 && active.allowedFiles.length <= 2) {
513
+ found.push(`${guidance} Active Worker ${active.id} has only ${active.allowedFiles.length} allowed_files after ${doneTasks.length} completed tasks.`);
514
+ }
515
+ if (firstMilestoneComplete && isTinyTask(active)) {
516
+ found.push(`${guidance} The first milestone is complete, so the active Worker should move toward the next real milestone.`);
517
+ }
518
+ if (isMicroWorkerTask(active)) {
519
+ found.push(`${guidance} Active Worker ${active.id} looks like another helper-sized slice.`);
520
+ }
521
+ }
522
+ if (active.type !== "judge") return [...new Set(found)];
523
+
524
+ let pairs = 0;
525
+ for (let index = activeIndex; index > 0; index -= 2) {
526
+ const judge = tasks[index];
527
+ const worker = tasks[index - 1];
528
+ if (!isMicroJudgeForWorker(judge, worker)) break;
529
+ pairs += 1;
530
+ }
531
+ if (pairs >= 2) {
532
+ found.push(`${guidance} Micro Worker/Judge loop detected ending at ${active.id}.`);
533
+ }
534
+ return [...new Set(found)];
535
+ }
536
+
537
+ function isMicroJudgeForWorker(judge, worker) {
538
+ if (!judge || !worker) return false;
539
+ if (judge.type !== "judge" || worker.type !== "worker") return false;
540
+ if (!["active", "queued", "done"].includes(judge.status) || worker.status !== "done") return false;
541
+ const objective = String(judge.objective || "").toLowerCase();
542
+ return objective.includes(worker.id.toLowerCase()) && /audit|review|approve/.test(objective) && isMicroWorkerTask(worker);
543
+ }
544
+
545
+ function isMicroWorkerTask(task) {
546
+ if (!task || task.type !== "worker") return false;
547
+ const objective = String(task.objective || "").toLowerCase();
548
+ if (/collapsed|batch|package|tranche/.test(objective)) return false;
549
+ return /one narrow|single helper|one helper|per[- ]helper|per[- ]table|projection helper/.test(objective);
550
+ }
551
+
552
+ function isTinyTask(task) {
553
+ if (!task) return false;
554
+ const text = [task.objective, task.raw, task.receipt?.raw].join(" ").toLowerCase();
555
+ if (/collapsed|batch|package|tranche|vertical slice|milestone/.test(text)) return false;
556
+ return /\b(tiny|narrow|single helper|one helper|projection helper|projection function|contract file|read-only proof|doc note|validator|validation wrapper|pure helper|caller-input)\b/.test(text);
557
+ }
558
+
433
559
  function matchesAllowedFile(file, allowedFiles) {
434
560
  return allowedFiles.some((pattern) => globMatch(pattern, file));
435
561
  }
@@ -456,6 +582,9 @@ function escapeRegExp(value) {
456
582
  }
457
583
 
458
584
  if (goalStatus === "done") {
585
+ if (noCompletionOnWeakProof && (isWeakProof(completionProof) || isWeakProof(oracleSignal) || isWeakProof(oracleFinalProof))) {
586
+ errors.push("done goals require concrete completion proof, goal.oracle.signal, and goal.oracle.final_proof; weak proof cannot close a goal");
587
+ }
459
588
  const finalAudit = tasks.some((task) => {
460
589
  if (!["judge", "pm"].includes(task.type) || task.status !== "done") return false;
461
590
  if (!task.receipt.present || task.receipt.value === null) return false;
@@ -2,12 +2,12 @@
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" },
9
9
  judge: { agent: "goal_judge", reasoning: "high", sandbox: "read-only" },
10
- worker: { agent: "goal_worker", reasoning: "low", sandbox: "workspace-write" },
10
+ worker: { agent: "goal_worker", reasoning: "medium", sandbox: "workspace-write" },
11
11
  pm: { agent: "PM", reasoning: "medium", sandbox: "workspace-write" },
12
12
  };
13
13
 
@@ -39,11 +39,14 @@ export function renderTaskPrompt(options) {
39
39
  payload: {
40
40
  metadata: {
41
41
  recommended_agent: defaults.agent,
42
+ required_spawn_agent_type: defaults.agent === "PM" ? null : defaults.agent,
42
43
  recommended_reasoning: reasoning,
43
44
  sandbox: defaults.sandbox,
44
45
  fork_context_allowed: role !== "worker",
45
46
  board_path: board.path,
46
47
  child_board_paths: childBoardPaths(board),
48
+ goal_oracle: board.goal.oracle || null,
49
+ slice_policy: board.document.rules?.slice_policy || null,
47
50
  warnings,
48
51
  },
49
52
  task: {
@@ -139,19 +142,63 @@ function promptWarnings(board, task) {
139
142
  const warnings = [];
140
143
  const role = normalizeRole(task.type);
141
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
+ }
142
151
  if (role === "worker") {
143
152
  if (stringList(task.allowed_files).length === 0) warnings.push(`Worker task ${task.id} has no allowed_files.`);
144
153
  if (stringList(task.verify).length === 0) warnings.push(`Worker task ${task.id} has no verify commands.`);
145
154
  if (stringList(task.stop_if).length === 0) warnings.push(`Worker task ${task.id} has no stop_if conditions.`);
155
+ if (isFalse(board.goal.full_outcome_complete)) {
156
+ warnings.push(`full_outcome_complete is false and ${task.id} is an active Worker; do not stop after rendering or repairing the board. Execute the Worker unless a stop_if condition applies.`);
157
+ }
146
158
  }
147
159
  for (const candidate of board.tasks) {
148
160
  if (candidate?.subgoal && Number(candidate.subgoal.depth) !== 1) {
149
161
  warnings.push(`Task ${candidate.id} has subgoal.depth ${candidate.subgoal.depth || "<missing>"}; only depth 1 is supported.`);
150
162
  }
151
163
  }
164
+ warnings.push(...microSliceWarnings(board, task));
152
165
  return warnings;
153
166
  }
154
167
 
168
+ function microSliceWarnings(board, task) {
169
+ const warnings = [];
170
+ const doneTasks = board.tasks.filter((candidate) => candidate?.status === "done");
171
+ const recentWorkers = board.tasks
172
+ .filter((candidate) => normalizeRole(candidate?.type) === "worker")
173
+ .slice(-5);
174
+ const recentTinyWorkers = recentWorkers.filter((candidate) => isTinyTask(candidate));
175
+ const activeRole = normalizeRole(task.type);
176
+ const activeAllowedFiles = stringList(task.allowed_files);
177
+ const firstMilestoneComplete = isTrue(board.goal.first_milestone_complete);
178
+ const microWarning = "Board may be micro-slicing. Prefer the largest safe useful slice.";
179
+
180
+ if (recentTinyWorkers.length >= 3) warnings.push(microWarning);
181
+ if (doneTasks.length >= 10 && activeRole === "worker" && activeAllowedFiles.length > 0 && activeAllowedFiles.length <= 2) {
182
+ warnings.push(`${microWarning} Active Worker ${task.id} has only ${activeAllowedFiles.length} allowed_files after ${doneTasks.length} completed tasks.`);
183
+ }
184
+ if (firstMilestoneComplete && activeRole === "worker" && isTinyTask(task)) {
185
+ warnings.push(`${microWarning} The first milestone is complete, so the active Worker should move toward the next real milestone.`);
186
+ }
187
+ if (activeRole === "judge" && /pick small reviewable work|select one narrow next task/i.test(String(task.objective || "") + "\n" + stringList(task.constraints).join("\n"))) {
188
+ warnings.push(`${microWarning} Judge instructions still ask for small or narrow work.`);
189
+ }
190
+ return [...new Set(warnings)];
191
+ }
192
+
193
+ function isTinyTask(task) {
194
+ const text = [
195
+ task?.objective,
196
+ stringList(task?.constraints).join(" "),
197
+ task?.receipt?.summary,
198
+ ].join(" ").toLowerCase();
199
+ return /\b(tiny|narrow|single helper|one helper|projection helper|projection function|contract file|read-only proof|doc note|validator|validation wrapper|pure helper|caller-input)\b/.test(text);
200
+ }
201
+
155
202
  function normalizeRole(value) {
156
203
  const role = String(value || "pm").toLowerCase();
157
204
  return ROLE_DEFAULTS[role] ? role : "pm";
@@ -163,6 +210,25 @@ function normalizeReasoning(value, fallback) {
163
210
  return fallback;
164
211
  }
165
212
 
213
+ function isFalse(value) {
214
+ return value === false || String(value).toLowerCase() === "false";
215
+ }
216
+
217
+ function isTrue(value) {
218
+ return value === true || String(value).toLowerCase() === "true";
219
+ }
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
+
166
232
  function stringList(value) {
167
233
  return Array.isArray(value) ? value.filter((item) => item !== null && item !== undefined).map(String) : [];
168
234
  }
@@ -175,15 +241,14 @@ function receiptSchema(role) {
175
241
  commands: [{ cmd: "<command>", status: "pass | fail | not_run" }],
176
242
  summary: "<=120 words",
177
243
  remaining_blockers: [],
178
- needs_judge: false,
179
244
  };
180
245
  }
181
246
  if (role === "judge") {
182
247
  return {
183
248
  result: "done | blocked",
184
- decision: "approve_next | reject_next | approve_subgoal | reject_subgoal | not_complete | complete",
249
+ decision: "approved | rejected | approve_subgoal | reject_subgoal | not_complete | complete",
250
+ full_outcome_complete: false,
185
251
  evidence: [],
186
- next_allowed_task: null,
187
252
  blocked_tasks: [],
188
253
  required_board_updates: [],
189
254
  };
@@ -204,6 +269,7 @@ function formatPrompt(payload) {
204
269
  "",
205
270
  "Metadata:",
206
271
  `- recommended_agent: ${payload.metadata.recommended_agent}`,
272
+ `- required_spawn_agent_type: ${payload.metadata.required_spawn_agent_type || "PM fallback"}`,
207
273
  `- recommended_reasoning: ${payload.metadata.recommended_reasoning}`,
208
274
  `- sandbox: ${payload.metadata.sandbox}`,
209
275
  `- fork_context_allowed: ${payload.metadata.fork_context_allowed}`,
@@ -213,12 +279,24 @@ function formatPrompt(payload) {
213
279
  lines.push("- child_board_paths:");
214
280
  for (const path of payload.metadata.child_board_paths) lines.push(` - ${path}`);
215
281
  }
282
+ if (payload.metadata.goal_oracle) {
283
+ lines.push(`- goal_oracle: ${JSON.stringify(payload.metadata.goal_oracle)}`);
284
+ }
285
+ if (payload.metadata.slice_policy) {
286
+ lines.push(`- slice_policy: ${JSON.stringify(payload.metadata.slice_policy)}`);
287
+ }
216
288
  if (payload.metadata.warnings.length) {
217
289
  lines.push("- warnings:");
218
290
  for (const warning of payload.metadata.warnings) lines.push(` - ${warning}`);
219
291
  }
220
292
 
221
293
  lines.push(
294
+ "",
295
+ "Spawn contract:",
296
+ `- Codex spawn_agent agent_type: ${payload.metadata.required_spawn_agent_type || "do not spawn; run as PM"}`,
297
+ "- Do not substitute generic scout, worker, or judge agents for GoalBuddy agents.",
298
+ "- If the required GoalBuddy agent is unavailable, stop spawning and continue as PM fallback or install agents.",
299
+ "- After one wait_agent timeout with no visible allowed-file changes, stop waiting and recover deterministically.",
222
300
  "",
223
301
  "Task:",
224
302
  `- id: ${payload.task.id}`,
@@ -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