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.
- package/README.md +46 -12
- package/RELEASE-0.3.5.md +4 -4
- package/RELEASE-0.3.7.md +127 -0
- package/goalbuddy/SKILL.md +53 -23
- package/goalbuddy/agents/README.md +1 -1
- package/goalbuddy/agents/goal_judge.toml +8 -4
- package/goalbuddy/agents/goal_worker.toml +8 -5
- package/goalbuddy/scripts/check-goal-state.mjs +129 -0
- package/goalbuddy/scripts/render-task-prompt.mjs +83 -5
- package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/README.md +7 -9
- package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/examples/sample-goal/state.yaml +5 -5
- package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/examples/subgoal-parent/state.yaml +3 -3
- package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/state.yaml +3 -3
- package/goalbuddy/{extend → surfaces}/local-goal-board/scripts/lib/goal-board.mjs +250 -9
- package/goalbuddy/{extend → surfaces}/local-goal-board/scripts/local-goal-board.mjs +4 -4
- package/goalbuddy/{extend → surfaces}/local-goal-board/test/local-goal-board.test.mjs +67 -9
- package/goalbuddy/templates/agents.md +3 -2
- package/goalbuddy/templates/goal.md +27 -4
- package/goalbuddy/templates/state.yaml +13 -7
- package/internal/assets/goalbuddy-v0.3.7-release.png +0 -0
- package/internal/cli/goal-maker.mjs +112 -714
- package/package.json +4 -4
- package/plugins/goalbuddy/.claude-plugin/plugin.json +3 -4
- package/plugins/goalbuddy/.codex-plugin/plugin.json +5 -6
- package/plugins/goalbuddy/README.md +4 -3
- package/plugins/goalbuddy/agents/goal-judge.md +8 -4
- package/plugins/goalbuddy/agents/goal-worker.md +6 -4
- package/plugins/goalbuddy/skills/goalbuddy/SKILL.md +53 -23
- package/plugins/goalbuddy/skills/goalbuddy/agents/README.md +1 -1
- package/plugins/goalbuddy/skills/goalbuddy/agents/goal_judge.toml +8 -4
- package/plugins/goalbuddy/skills/goalbuddy/agents/goal_worker.toml +8 -5
- package/plugins/goalbuddy/skills/goalbuddy/scripts/check-goal-state.mjs +129 -0
- package/plugins/goalbuddy/skills/goalbuddy/scripts/render-task-prompt.mjs +83 -5
- package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/README.md +7 -9
- package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/examples/sample-goal/state.yaml +5 -5
- package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/examples/subgoal-parent/state.yaml +3 -3
- package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/state.yaml +3 -3
- package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/scripts/lib/goal-board.mjs +250 -9
- package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/scripts/local-goal-board.mjs +4 -4
- package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/test/local-goal-board.test.mjs +67 -9
- package/plugins/goalbuddy/skills/goalbuddy/templates/agents.md +3 -2
- package/plugins/goalbuddy/skills/goalbuddy/templates/goal.md +27 -4
- package/plugins/goalbuddy/skills/goalbuddy/templates/state.yaml +13 -7
- package/examples/extend-catalog-workflow/goal.md +0 -53
- package/examples/extend-catalog-workflow/notes/T001-extension-model-map.md +0 -47
- package/examples/extend-catalog-workflow/notes/T002-architecture-decision.md +0 -48
- package/examples/extend-catalog-workflow/notes/T003-implementation-summary.md +0 -43
- package/examples/extend-catalog-workflow/notes/T004-root-extend-folder.md +0 -24
- package/examples/extend-catalog-workflow/notes/T005-layout-cleanup.md +0 -46
- package/examples/extend-catalog-workflow/notes/T006-catalog-location.md +0 -50
- package/examples/extend-catalog-workflow/notes/T999-completion-audit.md +0 -36
- package/examples/extend-catalog-workflow/state.yaml +0 -327
- package/examples/github-pr-workflow-extension/pr-handoff.md +0 -46
- package/goalbuddy/extend/github-projects/README.md +0 -105
- package/goalbuddy/extend/github-projects/examples/goal-board-sync/state.yaml +0 -63
- package/goalbuddy/extend/github-projects/extension.yaml +0 -43
- package/goalbuddy/extend/github-projects/scripts/lib/github-projects.mjs +0 -728
- package/goalbuddy/extend/github-projects/scripts/lib/goal-state.mjs +0 -362
- package/goalbuddy/extend/github-projects/scripts/sync-github-project.mjs +0 -193
- package/goalbuddy/extend/github-projects/test/github-projects.test.mjs +0 -267
- package/goalbuddy/extend/local-goal-board/extension.yaml +0 -39
- package/internal/assets/extend-release.png +0 -0
- package/internal/assets/extend-release.svg +0 -83
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/README.md +0 -105
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/examples/goal-board-sync/state.yaml +0 -63
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/extension.yaml +0 -43
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/lib/github-projects.mjs +0 -728
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/lib/goal-state.mjs +0 -362
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/sync-github-project.mjs +0 -193
- package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/test/github-projects.test.mjs +0 -267
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/extension.yaml +0 -39
- /package/goalbuddy/{extend → surfaces}/local-goal-board/assets/goalbuddy-mark.png +0 -0
- /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/sample-goal/notes/T001-scout.md +0 -0
- /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/goal.md +0 -0
- /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/notes/.gitkeep +0 -0
- /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/goal.md +0 -0
- /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/notes/.gitkeep +0 -0
- /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/assets/goalbuddy-mark.png +0 -0
- /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/sample-goal/notes/T001-scout.md +0 -0
- /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/goal.md +0 -0
- /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/notes/.gitkeep +0 -0
- /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/goal.md +0 -0
- /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/notes/.gitkeep +0 -0
|
@@ -6,8 +6,8 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
const VALID_STATUSES = new Set(["queued", "active", "blocked", "done"]);
|
|
7
7
|
const COLUMN_ORDER = ["todo", "in-progress", "blocked", "completed"];
|
|
8
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const
|
|
10
|
-
const logoAssetPath = join(
|
|
9
|
+
const surfaceRoot = resolve(__dirname, "../..");
|
|
10
|
+
const logoAssetPath = join(surfaceRoot, "assets", "goalbuddy-mark.png");
|
|
11
11
|
|
|
12
12
|
export class GoalBoardError extends Error {
|
|
13
13
|
constructor(message) {
|
|
@@ -110,7 +110,7 @@ export function normalizeTask(task, index) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
const id = cleanText(task.id);
|
|
113
|
-
const 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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
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
|
|
530
|
+
const fileExtension = cleanPath.match(/\.[^.]+$/)?.[0] || "";
|
|
531
531
|
response.writeHead(200, {
|
|
532
|
-
"Content-Type": textTypes[
|
|
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
|
-
|
|
584
|
-
|
|
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("
|
|
11
|
+
const payload = createBoardPayload(resolve("goalbuddy/surfaces/local-goal-board/examples/sample-goal"));
|
|
12
12
|
|
|
13
|
-
assert.equal(payload.goal.title, "Local
|
|
13
|
+
assert.equal(payload.goal.title, "Local Goal Board Surface");
|
|
14
14
|
assert.equal(payload.goal.activeTask, "");
|
|
15
15
|
assert.equal(payload.counts.total, 14);
|
|
16
16
|
assert.equal(payload.counts.todo, 0);
|
|
@@ -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/
|
|
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("
|
|
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("
|
|
394
|
+
const root = mkdtempSync(join(tmpdir(), "goalbuddy-local-board-direct-"));
|
|
337
395
|
try {
|
|
338
|
-
cpSync("
|
|
339
|
-
cpSync("
|
|
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("
|
|
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
|
|
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 |
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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 |
|
|
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
|
|
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.
|