goalbuddy 0.3.7 → 0.3.9
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/.claude-plugin/marketplace.json +16 -0
- package/CHANGELOG.md +70 -0
- package/CONTRIBUTING.md +2 -2
- package/README.md +11 -3
- package/{RELEASE-0.3.7.md → docs/releases/0.3.7.md} +2 -0
- package/docs/releases/0.3.8.md +40 -0
- package/docs/releases/0.3.9.md +46 -0
- package/docs/releases/README.md +84 -0
- package/goalbuddy/SKILL.md +26 -8
- package/goalbuddy/scripts/check-goal-state.mjs +22 -4
- package/goalbuddy/scripts/check-update.mjs +18 -1
- package/goalbuddy/scripts/render-task-prompt.mjs +17 -3
- package/goalbuddy/surfaces/local-goal-board/scripts/lib/goal-board.mjs +16 -15
- package/goalbuddy/surfaces/local-goal-board/scripts/local-goal-board.mjs +25 -3
- package/goalbuddy/surfaces/local-goal-board/test/local-goal-board.test.mjs +189 -4
- package/goalbuddy/templates/goal.md +12 -0
- package/goalbuddy/templates/state.yaml +2 -1
- package/internal/cli/goal-maker.mjs +186 -7
- package/package.json +6 -6
- package/plugins/goalbuddy/.claude-plugin/plugin.json +1 -1
- package/plugins/goalbuddy/.codex-plugin/plugin.json +1 -1
- package/plugins/goalbuddy/README.md +1 -1
- package/plugins/goalbuddy/skills/goalbuddy/SKILL.md +26 -8
- package/plugins/goalbuddy/skills/goalbuddy/scripts/check-goal-state.mjs +22 -4
- package/plugins/goalbuddy/skills/goalbuddy/scripts/check-update.mjs +18 -1
- package/plugins/goalbuddy/skills/goalbuddy/scripts/render-task-prompt.mjs +17 -3
- package/plugins/goalbuddy/skills/goalbuddy/surfaces/local-goal-board/scripts/lib/goal-board.mjs +1 -4
- package/plugins/goalbuddy/skills/goalbuddy/surfaces/local-goal-board/scripts/local-goal-board.mjs +25 -3
- package/plugins/goalbuddy/skills/goalbuddy/surfaces/local-goal-board/test/local-goal-board.test.mjs +27 -0
- package/plugins/goalbuddy/skills/goalbuddy/templates/goal.md +12 -0
- package/plugins/goalbuddy/skills/goalbuddy/templates/state.yaml +2 -1
- package/examples/improve-goal-maker/goal.md +0 -51
- package/examples/improve-goal-maker/notes/T001-repo-map.md +0 -59
- package/examples/improve-goal-maker/notes/T002-risk-map.md +0 -37
- package/examples/improve-goal-maker/state.yaml +0 -224
- /package/{RELEASE-0.3.5.md → docs/releases/0.3.5.md} +0 -0
|
@@ -33,6 +33,7 @@ const SETTINGS_OPTIONS = {
|
|
|
33
33
|
const DEFAULT_BIND_HOST = "127.0.0.1";
|
|
34
34
|
const DEFAULT_PUBLIC_HOST = "goalbuddy.localhost";
|
|
35
35
|
const DEFAULT_PORT = 41737;
|
|
36
|
+
const STATE_CHANGE_SETTLE_MS = 300;
|
|
36
37
|
|
|
37
38
|
if (isDirectRun()) {
|
|
38
39
|
main().catch((error) => {
|
|
@@ -228,8 +229,7 @@ export async function startBoardServer(options = {}) {
|
|
|
228
229
|
|
|
229
230
|
const route = routeBoardRequest(url.pathname, boards, initialBoard);
|
|
230
231
|
if (!route.board) {
|
|
231
|
-
response.
|
|
232
|
-
response.end("Not found");
|
|
232
|
+
sendUnregisteredBoardPath(response, url.pathname, boards, baseUrl);
|
|
233
233
|
return;
|
|
234
234
|
}
|
|
235
235
|
if (route.pathname === "/api/board") {
|
|
@@ -400,6 +400,28 @@ function routeBoardRequest(pathname, boards, initialBoard) {
|
|
|
400
400
|
return matches[0] || { board: null, pathname };
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
+
function sendUnregisteredBoardPath(response, pathname, boards, baseUrl) {
|
|
404
|
+
response.writeHead(404, {
|
|
405
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
406
|
+
"Cache-Control": "no-store",
|
|
407
|
+
});
|
|
408
|
+
const registeredBoards = [...boards.values()].map((board) => {
|
|
409
|
+
const summary = boardSummary(board, baseUrl);
|
|
410
|
+
return `- ${summary.title}: ${summary.url}`;
|
|
411
|
+
});
|
|
412
|
+
response.end([
|
|
413
|
+
`GoalBuddy board path is not registered in this local hub: ${pathname}`,
|
|
414
|
+
"",
|
|
415
|
+
"This server is the GoalBuddy multi-board hub. Do not stop it just because a /<slug>/ board URL returned 404.",
|
|
416
|
+
"Start or rerun `npx goalbuddy board <goal-dir>` to register that goal on this same port, then open the printed /<slug>/ URL.",
|
|
417
|
+
"",
|
|
418
|
+
"Registered boards:",
|
|
419
|
+
registeredBoards.length ? registeredBoards.join("\n") : "- none",
|
|
420
|
+
"",
|
|
421
|
+
`Hub API: ${baseUrl}/api/boards`,
|
|
422
|
+
].join("\n"));
|
|
423
|
+
}
|
|
424
|
+
|
|
403
425
|
function stripBoardPathPrefix(pathname, boardPath) {
|
|
404
426
|
const prefix = boardPath.endsWith("/") ? boardPath.slice(0, -1) : boardPath;
|
|
405
427
|
if (pathname === prefix) return "/";
|
|
@@ -420,7 +442,7 @@ async function readJsonRequest(request) {
|
|
|
420
442
|
|
|
421
443
|
function watchGoal(goalDir, onChange) {
|
|
422
444
|
const watchers = [];
|
|
423
|
-
const schedule = debounce(onChange,
|
|
445
|
+
const schedule = debounce(onChange, STATE_CHANGE_SETTLE_MS);
|
|
424
446
|
let watchedDirs = new Set();
|
|
425
447
|
|
|
426
448
|
const rebuild = () => {
|
|
@@ -4,7 +4,7 @@ import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } f
|
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join, resolve } from "node:path";
|
|
7
|
-
import { createBoardPayload, writeBoardApp } from "../scripts/lib/goal-board.mjs";
|
|
7
|
+
import { buildColumns, createBoardPayload, writeBoardApp } from "../scripts/lib/goal-board.mjs";
|
|
8
8
|
import { parseArgs, startBoardServer } from "../scripts/local-goal-board.mjs";
|
|
9
9
|
|
|
10
10
|
test("normalizes a dense goal into local board columns", () => {
|
|
@@ -23,6 +23,71 @@ test("normalizes a dense goal into local board columns", () => {
|
|
|
23
23
|
assert.equal(scout.receipt.summary, "T001 completed during the progressive board motion demo.");
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
test("orders completed cards newest first while preserving queued order", () => {
|
|
27
|
+
const columns = buildColumns([
|
|
28
|
+
{ id: "T001", column: "completed", status: "done" },
|
|
29
|
+
{ id: "T002", column: "todo", status: "queued" },
|
|
30
|
+
{ id: "T003", column: "completed", status: "done" },
|
|
31
|
+
{ id: "T004", column: "todo", status: "queued" },
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
assert.deepEqual(columns.find((column) => column.id === "todo").tasks.map((task) => task.id), ["T002", "T004"]);
|
|
35
|
+
assert.deepEqual(columns.find((column) => column.id === "completed").tasks.map((task) => task.id), ["T003", "T001"]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("renders multiple active tasks in the in-progress column", () => {
|
|
39
|
+
const root = mkdtempSync(join(tmpdir(), "goalbuddy-multiple-active-"));
|
|
40
|
+
try {
|
|
41
|
+
const goalDir = join(root, "parallel-workers");
|
|
42
|
+
mkdirSync(join(goalDir, "notes"), { recursive: true });
|
|
43
|
+
writeFileSync(join(goalDir, "state.yaml"), `version: 2
|
|
44
|
+
goal:
|
|
45
|
+
title: "Parallel workers"
|
|
46
|
+
slug: "parallel-workers"
|
|
47
|
+
kind: specific
|
|
48
|
+
tranche: "Render disjoint active workers."
|
|
49
|
+
status: active
|
|
50
|
+
active_task: T001
|
|
51
|
+
tasks:
|
|
52
|
+
- id: T001
|
|
53
|
+
type: worker
|
|
54
|
+
assignee: Worker A
|
|
55
|
+
status: active
|
|
56
|
+
objective: "Patch the board parser."
|
|
57
|
+
allowed_files:
|
|
58
|
+
- goalbuddy/surfaces/local-goal-board/scripts/lib/goal-board.mjs
|
|
59
|
+
verify:
|
|
60
|
+
- node --test goalbuddy/surfaces/local-goal-board/test/local-goal-board.test.mjs
|
|
61
|
+
stop_if:
|
|
62
|
+
- "Need files outside allowed_files."
|
|
63
|
+
receipt: null
|
|
64
|
+
- id: T002
|
|
65
|
+
type: worker
|
|
66
|
+
assignee: Worker B
|
|
67
|
+
status: active
|
|
68
|
+
objective: "Patch the board tests."
|
|
69
|
+
allowed_files:
|
|
70
|
+
- goalbuddy/surfaces/local-goal-board/test/local-goal-board.test.mjs
|
|
71
|
+
verify:
|
|
72
|
+
- node --test goalbuddy/surfaces/local-goal-board/test/local-goal-board.test.mjs
|
|
73
|
+
stop_if:
|
|
74
|
+
- "Need files outside allowed_files."
|
|
75
|
+
receipt: null
|
|
76
|
+
`);
|
|
77
|
+
|
|
78
|
+
const payload = createBoardPayload(goalDir);
|
|
79
|
+
assert.equal(payload.goal.activeTask, "T001");
|
|
80
|
+
assert.equal(payload.counts.inProgress, 2);
|
|
81
|
+
assert.deepEqual(
|
|
82
|
+
payload.columns.find((column) => column.id === "in-progress").tasks.map((task) => task.id),
|
|
83
|
+
["T001", "T002"],
|
|
84
|
+
);
|
|
85
|
+
assert.deepEqual(payload.tasks.filter((task) => task.active).map((task) => task.id), ["T001", "T002"]);
|
|
86
|
+
} finally {
|
|
87
|
+
rmSync(root, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
26
91
|
test("loads depth-1 subgoal boards into parent task payloads", () => {
|
|
27
92
|
const payload = createBoardPayload(resolve("goalbuddy/surfaces/local-goal-board/examples/subgoal-parent"));
|
|
28
93
|
const parentTask = payload.tasks.find((task) => task.id === "T004");
|
|
@@ -37,10 +102,10 @@ test("loads depth-1 subgoal boards into parent task payloads", () => {
|
|
|
37
102
|
assert.equal(parentTask.subgoal.board.tasks.find((task) => task.id === "T002").subgoal, null);
|
|
38
103
|
});
|
|
39
104
|
|
|
40
|
-
test("uses
|
|
41
|
-
const root = mkdtempSync(join(tmpdir(), "goalbuddy-
|
|
105
|
+
test("uses readable card titles while preserving full objectives", () => {
|
|
106
|
+
const root = mkdtempSync(join(tmpdir(), "goalbuddy-readable-titles-"));
|
|
42
107
|
try {
|
|
43
|
-
const goalDir = join(root, "
|
|
108
|
+
const goalDir = join(root, "readable-titles");
|
|
44
109
|
mkdirSync(join(goalDir, "notes"), { recursive: true });
|
|
45
110
|
writeFileSync(join(goalDir, "state.yaml"), `version: 2
|
|
46
111
|
goal:
|
|
@@ -70,6 +135,13 @@ tasks:
|
|
|
70
135
|
status: queued
|
|
71
136
|
objective: "This objective can stay much more detailed because it belongs in the modal, not on the card face."
|
|
72
137
|
receipt: null
|
|
138
|
+
- id: T004
|
|
139
|
+
title: "Run installed-Cursor runtime proof for a named model request through the local BYOK bridge"
|
|
140
|
+
type: worker
|
|
141
|
+
assignee: Worker
|
|
142
|
+
status: queued
|
|
143
|
+
objective: "Run installed-Cursor runtime proof for a named model request through the local BYOK bridge."
|
|
144
|
+
receipt: null
|
|
73
145
|
`);
|
|
74
146
|
|
|
75
147
|
const payload = createBoardPayload(goalDir);
|
|
@@ -77,6 +149,10 @@ tasks:
|
|
|
77
149
|
assert.equal(payload.tasks.find((task) => task.id === "T001").objective.includes("admin_seed_metrics.enrichment_qa"), true);
|
|
78
150
|
assert.equal(payload.tasks.find((task) => task.id === "T002").title, "Implement /contacts/con_aaron_keller route");
|
|
79
151
|
assert.equal(payload.tasks.find((task) => task.id === "T003").title, "Human-friendly release title");
|
|
152
|
+
assert.equal(
|
|
153
|
+
payload.tasks.find((task) => task.id === "T004").title,
|
|
154
|
+
"Run installed-Cursor runtime proof for a named model request through the local BYOK bridge",
|
|
155
|
+
);
|
|
80
156
|
} finally {
|
|
81
157
|
rmSync(root, { recursive: true, force: true });
|
|
82
158
|
}
|
|
@@ -249,6 +325,7 @@ test("writes a minimal GoalBuddy web app into the goal directory", () => {
|
|
|
249
325
|
assert.match(css, /:root\[data-theme="dark"\]/);
|
|
250
326
|
assert.match(css, /:root\[data-density="compact"\] \.task-card/);
|
|
251
327
|
assert.match(css, /:root\[data-completed-visibility="collapse"\]/);
|
|
328
|
+
assert.match(css, /-webkit-line-clamp: 5/);
|
|
252
329
|
assert.match(css, /\.subgoal-board/);
|
|
253
330
|
assert.match(css, /\.board-error/);
|
|
254
331
|
assert.match(js, /new EventSource\("\.\/events"\)/);
|
|
@@ -448,6 +525,50 @@ test("serves board JSON and streams live state changes over SSE", async () => {
|
|
|
448
525
|
}
|
|
449
526
|
});
|
|
450
527
|
|
|
528
|
+
test("coalesces transient active-task violations during multi-write transitions", async () => {
|
|
529
|
+
const root = mkdtempSync(join(tmpdir(), "goalbuddy-local-board-transition-"));
|
|
530
|
+
const goalDir = join(root, "transition-goal");
|
|
531
|
+
try {
|
|
532
|
+
mkdirSync(join(goalDir, "notes"), { recursive: true });
|
|
533
|
+
writeFileSync(join(goalDir, "state.yaml"), transitionStateYaml({
|
|
534
|
+
activeTask: "T001",
|
|
535
|
+
firstStatus: "active",
|
|
536
|
+
secondStatus: "queued",
|
|
537
|
+
}));
|
|
538
|
+
|
|
539
|
+
const server = await startBoardServer({ goalDir, host: "127.0.0.1", port: 0 });
|
|
540
|
+
try {
|
|
541
|
+
const controller = new AbortController();
|
|
542
|
+
const events = await fetch(`${server.url}events`, { signal: controller.signal });
|
|
543
|
+
assert.equal(events.status, 200);
|
|
544
|
+
const reader = events.body.getReader();
|
|
545
|
+
|
|
546
|
+
await readUntil(reader, /"activeTask":"T001"/);
|
|
547
|
+
writeFileSync(join(goalDir, "state.yaml"), transitionStateYaml({
|
|
548
|
+
activeTask: "T002",
|
|
549
|
+
firstStatus: "active",
|
|
550
|
+
secondStatus: "active",
|
|
551
|
+
}));
|
|
552
|
+
await delay(120);
|
|
553
|
+
writeFileSync(join(goalDir, "state.yaml"), transitionStateYaml({
|
|
554
|
+
activeTask: "T002",
|
|
555
|
+
firstStatus: "done",
|
|
556
|
+
secondStatus: "active",
|
|
557
|
+
}));
|
|
558
|
+
|
|
559
|
+
const update = await readUntil(reader, /"activeTask":"T002"/);
|
|
560
|
+
assert.doesNotMatch(update, /more than one active task/i);
|
|
561
|
+
|
|
562
|
+
controller.abort();
|
|
563
|
+
await reader.cancel().catch(() => {});
|
|
564
|
+
} finally {
|
|
565
|
+
await server.close();
|
|
566
|
+
}
|
|
567
|
+
} finally {
|
|
568
|
+
rmSync(root, { recursive: true, force: true });
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
|
|
451
572
|
test("streams parent board updates when linked child subgoal state changes", async () => {
|
|
452
573
|
const root = mkdtempSync(join(tmpdir(), "goalbuddy-local-board-subgoal-live-"));
|
|
453
574
|
const goalDir = join(root, "parent-goal");
|
|
@@ -558,6 +679,33 @@ test("serves multiple local boards from one shared hub URL", async () => {
|
|
|
558
679
|
}
|
|
559
680
|
});
|
|
560
681
|
|
|
682
|
+
test("unregistered board paths explain hub reuse instead of stale-port cleanup", async () => {
|
|
683
|
+
const root = mkdtempSync(join(tmpdir(), "goalbuddy-local-board-unregistered-"));
|
|
684
|
+
const goalDir = join(root, "first-goal");
|
|
685
|
+
try {
|
|
686
|
+
mkdirSync(join(goalDir, "notes"), { recursive: true });
|
|
687
|
+
writeFileSync(join(goalDir, "state.yaml"), stateYaml("active", { title: "First Goal", slug: "first-goal" }));
|
|
688
|
+
|
|
689
|
+
const server = await startBoardServer({ goalDir, host: "127.0.0.1", port: 0 });
|
|
690
|
+
try {
|
|
691
|
+
const baseUrl = new URL(server.url).origin;
|
|
692
|
+
const missingResponse = await fetch(`${baseUrl}/rinova-client-revision-redesign/`);
|
|
693
|
+
assert.equal(missingResponse.status, 404);
|
|
694
|
+
const message = await missingResponse.text();
|
|
695
|
+
assert.match(message, /board path is not registered/i);
|
|
696
|
+
assert.match(message, /multi-board hub/i);
|
|
697
|
+
assert.match(message, /Do not stop it just because a \/<slug>\/ board URL returned 404/);
|
|
698
|
+
assert.match(message, /npx goalbuddy board <goal-dir>/);
|
|
699
|
+
assert.match(message, /First Goal/);
|
|
700
|
+
assert.match(message, /\/api\/boards/);
|
|
701
|
+
} finally {
|
|
702
|
+
await server.close();
|
|
703
|
+
}
|
|
704
|
+
} finally {
|
|
705
|
+
rmSync(root, { recursive: true, force: true });
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
|
|
561
709
|
async function readUntil(reader, pattern) {
|
|
562
710
|
const decoder = new TextDecoder();
|
|
563
711
|
let text = "";
|
|
@@ -573,6 +721,10 @@ async function readUntil(reader, pattern) {
|
|
|
573
721
|
assert.fail(`Timed out waiting for ${pattern}. Received:\n${text}`);
|
|
574
722
|
}
|
|
575
723
|
|
|
724
|
+
function delay(ms) {
|
|
725
|
+
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
|
|
726
|
+
}
|
|
727
|
+
|
|
576
728
|
function parentWithSubgoalYaml() {
|
|
577
729
|
return `version: 2
|
|
578
730
|
goal:
|
|
@@ -597,6 +749,39 @@ tasks:
|
|
|
597
749
|
`;
|
|
598
750
|
}
|
|
599
751
|
|
|
752
|
+
function transitionStateYaml({ activeTask, firstStatus, secondStatus }) {
|
|
753
|
+
return `version: 2
|
|
754
|
+
goal:
|
|
755
|
+
title: "Transition Goal"
|
|
756
|
+
slug: "transition-goal"
|
|
757
|
+
kind: specific
|
|
758
|
+
tranche: "Verify multi-write task transition."
|
|
759
|
+
status: active
|
|
760
|
+
active_task: ${activeTask}
|
|
761
|
+
tasks:
|
|
762
|
+
- id: T001
|
|
763
|
+
type: scout
|
|
764
|
+
assignee: Scout
|
|
765
|
+
status: ${firstStatus}
|
|
766
|
+
objective: "Map transition."
|
|
767
|
+
receipt:
|
|
768
|
+
result: done
|
|
769
|
+
summary: "Mapped transition."
|
|
770
|
+
- id: T002
|
|
771
|
+
type: worker
|
|
772
|
+
assignee: Worker
|
|
773
|
+
status: ${secondStatus}
|
|
774
|
+
objective: "Implement transition."
|
|
775
|
+
allowed_files:
|
|
776
|
+
- goalbuddy/surfaces/local-goal-board/**
|
|
777
|
+
verify:
|
|
778
|
+
- npm run check
|
|
779
|
+
stop_if:
|
|
780
|
+
- "Need files outside allowed_files."
|
|
781
|
+
receipt: null
|
|
782
|
+
`;
|
|
783
|
+
}
|
|
784
|
+
|
|
600
785
|
function stateYaml(status, { title = "Live board", slug = "live-board" } = {}) {
|
|
601
786
|
return `version: 2
|
|
602
787
|
goal:
|
|
@@ -64,6 +64,18 @@ Tiny tasks are allowed when the failure is isolated, the risk is high, the scope
|
|
|
64
64
|
|
|
65
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.
|
|
66
66
|
|
|
67
|
+
If an exact human approval phrase is the only remaining blocker and no safe local work remains, ask once and stop. Preserve the exact phrase in the blocked receipt as `required_reply`, set `waiting_for_user_approval: true`, set `goal.status: blocked`, and set `active_task: null`. Do not keep posting approval prompts until the user replies.
|
|
68
|
+
|
|
69
|
+
## Board Health
|
|
70
|
+
|
|
71
|
+
The PM owns board health. If the board looks stale, misleading, offline, or inconsistent, run the bundled checker:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
node <skill-path>/scripts/check-goal-state.mjs docs/goals/<slug>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
If the local board is running, compare `state.yaml` to the live board API. Repair only GoalBuddy control files unless an active Worker or PM task explicitly allows product-file edits.
|
|
78
|
+
|
|
67
79
|
## Canonical Board
|
|
68
80
|
|
|
69
81
|
Machine truth lives at:
|
|
@@ -35,6 +35,7 @@ rules:
|
|
|
35
35
|
queued_required_worker_blocks_completion: true
|
|
36
36
|
continuous_until_full_outcome: true
|
|
37
37
|
missing_input_or_credentials_do_not_stop_goal: true
|
|
38
|
+
exact_human_approval_can_terminal_wait: true
|
|
38
39
|
preserve_and_validate_existing_plan: true
|
|
39
40
|
intake_misfire_must_be_audited: true
|
|
40
41
|
goal_pressure_requires_oracle: true
|
|
@@ -58,7 +59,7 @@ visual_board:
|
|
|
58
59
|
local:
|
|
59
60
|
status: not_requested # not_requested | starting | live | generated | blocked
|
|
60
61
|
url: null
|
|
61
|
-
command: "
|
|
62
|
+
command: "node <skill-path>/surfaces/local-goal-board/scripts/local-goal-board.mjs --goal docs/goals/<goal-slug>"
|
|
62
63
|
|
|
63
64
|
active_task: T001
|
|
64
65
|
|
|
@@ -49,6 +49,7 @@ const optionsWithValues = new Set([
|
|
|
49
49
|
"--task",
|
|
50
50
|
"--board",
|
|
51
51
|
]);
|
|
52
|
+
const pathOptions = new Set(["--board", "--goal"]);
|
|
52
53
|
|
|
53
54
|
const args = process.argv.slice(2);
|
|
54
55
|
const command = args[0] === "--help" || args[0] === "-h"
|
|
@@ -111,6 +112,17 @@ async function main() {
|
|
|
111
112
|
doctorClaude();
|
|
112
113
|
}
|
|
113
114
|
break;
|
|
115
|
+
case "reset":
|
|
116
|
+
if (wantsHelp()) {
|
|
117
|
+
usage();
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if (targetMode() !== "codex") {
|
|
121
|
+
console.error("Reset currently supports --target codex only.");
|
|
122
|
+
process.exit(2);
|
|
123
|
+
}
|
|
124
|
+
resetCodex();
|
|
125
|
+
break;
|
|
114
126
|
case "check-update":
|
|
115
127
|
case "update-check":
|
|
116
128
|
checkUpdate();
|
|
@@ -193,6 +205,35 @@ function positionalArgs() {
|
|
|
193
205
|
return values;
|
|
194
206
|
}
|
|
195
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Resolve goal-related paths in raw args to absolute paths.
|
|
210
|
+
* Child processes spawned with cwd=packageRoot cannot resolve
|
|
211
|
+
* relative goal paths from the user's working directory.
|
|
212
|
+
*/
|
|
213
|
+
function resolveChildGoalArgs(rawArgs) {
|
|
214
|
+
const out = [];
|
|
215
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
216
|
+
const arg = rawArgs[index];
|
|
217
|
+
const joinedMatch = [...pathOptions].find((opt) => arg.startsWith(opt + "="));
|
|
218
|
+
if (joinedMatch) {
|
|
219
|
+
const value = arg.slice(joinedMatch.length + 1);
|
|
220
|
+
out.push(`${joinedMatch}=${value ? resolve(value) : value}`);
|
|
221
|
+
} else if (pathOptions.has(arg)) {
|
|
222
|
+
out.push(arg);
|
|
223
|
+
const value = rawArgs[++index] || "";
|
|
224
|
+
out.push(value ? resolve(value) : value);
|
|
225
|
+
} else if (optionsWithValues.has(arg)) {
|
|
226
|
+
out.push(arg);
|
|
227
|
+
out.push(rawArgs[++index] || "");
|
|
228
|
+
} else if (!arg.startsWith("-")) {
|
|
229
|
+
out.push(resolve(arg));
|
|
230
|
+
} else {
|
|
231
|
+
out.push(arg);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
196
237
|
function usage() {
|
|
197
238
|
console.log(`${canonicalProductName} for Claude Code and Codex
|
|
198
239
|
|
|
@@ -203,6 +244,7 @@ Usage:
|
|
|
203
244
|
${canonicalCliName} update [--target claude|codex] [--claude-home <path>] [--codex-home <path>] [--json]
|
|
204
245
|
${canonicalCliName} agents [--target claude|codex] [--claude-home <path>] [--codex-home <path>] [--force]
|
|
205
246
|
${canonicalCliName} doctor [--target claude|codex] [--claude-home <path>] [--codex-home <path>] [--goal-ready]
|
|
247
|
+
${canonicalCliName} reset --target codex [--codex-home <path>] [--json]
|
|
206
248
|
${canonicalCliName} check-update [--json]
|
|
207
249
|
${canonicalCliName} board <docs/goals/slug> [--host <host>] [--port <port>] [--once] [--json]
|
|
208
250
|
${canonicalCliName} prompt <docs/goals/slug> [--task T###] [--board <path/to/state.yaml>] [--json]
|
|
@@ -576,20 +618,36 @@ function doctor() {
|
|
|
576
618
|
const agents = existsSync(agentsPath)
|
|
577
619
|
? readdirSync(agentsPath).filter((file) => file.startsWith("goal_") && file.endsWith(".toml"))
|
|
578
620
|
: [];
|
|
579
|
-
const
|
|
621
|
+
const installSurfacePresent = plugin.skill_installed || installed || legacyInstalled;
|
|
622
|
+
const residualAgents = installSurfacePresent ? [] : agents.filter((file) => requiredAgentFiles.includes(file));
|
|
623
|
+
const missingAgents = installSurfacePresent || residualAgents.length > 0
|
|
624
|
+
? requiredAgentFiles.filter((file) => !agents.includes(file))
|
|
625
|
+
: [];
|
|
580
626
|
const staleAgents = requiredAgentFiles.filter((file) => {
|
|
581
627
|
const installedAgent = join(agentsPath, file);
|
|
582
628
|
const bundledAgent = join(skillSource, "agents", file);
|
|
583
629
|
if (!existsSync(installedAgent) || !existsSync(bundledAgent)) return false;
|
|
584
630
|
return sha256(readFileSync(installedAgent)) !== sha256(readFileSync(bundledAgent));
|
|
585
631
|
});
|
|
632
|
+
const runtimeState = codexInstallState({
|
|
633
|
+
plugin,
|
|
634
|
+
installed,
|
|
635
|
+
legacyInstalled,
|
|
636
|
+
residualAgents,
|
|
637
|
+
missingAgents,
|
|
638
|
+
staleAgents,
|
|
639
|
+
});
|
|
586
640
|
const goalRuntime = codexGoalRuntimeStatus();
|
|
587
641
|
const warnings = [];
|
|
588
642
|
const errors = [];
|
|
589
643
|
if (!goalRuntime.ready) {
|
|
590
644
|
warnings.push("native Codex /goal runtime is not ready; run `codex login` and `codex features enable goals` before using /goal.");
|
|
591
645
|
}
|
|
592
|
-
if (
|
|
646
|
+
if (runtimeState === "fully-removed") {
|
|
647
|
+
errors.push("Codex GoalBuddy is fully removed; run `npx goalbuddy --target codex` to install.");
|
|
648
|
+
} else if (runtimeState === "residual-agents-only") {
|
|
649
|
+
errors.push(`Residual GoalBuddy Codex agents remain without plugin cache/config: ${residualAgents.join(", ")}; run a GoalBuddy reset/cleanup before treating it as removed.`);
|
|
650
|
+
} else if (!plugin.skill_installed && !installed) {
|
|
593
651
|
errors.push("Codex GoalBuddy plugin is not installed; run `npx goalbuddy --target codex`.");
|
|
594
652
|
}
|
|
595
653
|
if (plugin.skill_installed && !plugin.enabled) {
|
|
@@ -621,7 +679,9 @@ function doctor() {
|
|
|
621
679
|
skill_path: skillPath,
|
|
622
680
|
compatibility_skill_installed: legacyInstalled,
|
|
623
681
|
compatibility_skill_path: legacySkillPath,
|
|
682
|
+
runtime_state: runtimeState,
|
|
624
683
|
installed_agents: agents,
|
|
684
|
+
residual_agents: residualAgents,
|
|
625
685
|
missing_agents: missingAgents,
|
|
626
686
|
stale_agents: staleAgents,
|
|
627
687
|
goal_runtime: goalRuntime,
|
|
@@ -636,6 +696,20 @@ function doctor() {
|
|
|
636
696
|
process.exit(installOk && goalReadyOk && errors.length === 0 ? 0 : 1);
|
|
637
697
|
}
|
|
638
698
|
|
|
699
|
+
function codexInstallState({ plugin, installed, legacyInstalled, residualAgents, missingAgents, staleAgents }) {
|
|
700
|
+
if (residualAgents.length > 0 && !plugin.skill_installed && !installed && !legacyInstalled) {
|
|
701
|
+
return "residual-agents-only";
|
|
702
|
+
}
|
|
703
|
+
if (!plugin.skill_installed && !installed && !legacyInstalled) {
|
|
704
|
+
return "fully-removed";
|
|
705
|
+
}
|
|
706
|
+
if (staleAgents.length > 0) return "stale-agents";
|
|
707
|
+
if (missingAgents.length > 0) return "incomplete";
|
|
708
|
+
if (plugin.skill_installed && !plugin.enabled) return "disabled";
|
|
709
|
+
if ((plugin.skill_installed && plugin.enabled) || installed) return "installed";
|
|
710
|
+
return "incomplete";
|
|
711
|
+
}
|
|
712
|
+
|
|
639
713
|
function checkUpdate() {
|
|
640
714
|
const report = updateReport();
|
|
641
715
|
|
|
@@ -661,7 +735,7 @@ function updateReport() {
|
|
|
661
735
|
latest_version: null,
|
|
662
736
|
update_available: false,
|
|
663
737
|
check_status: "unknown",
|
|
664
|
-
update_command:
|
|
738
|
+
update_command: detectUpdateCommand(),
|
|
665
739
|
};
|
|
666
740
|
|
|
667
741
|
try {
|
|
@@ -676,6 +750,23 @@ function updateReport() {
|
|
|
676
750
|
return report;
|
|
677
751
|
}
|
|
678
752
|
|
|
753
|
+
function detectUpdateCommand() {
|
|
754
|
+
if (process.env.GOALBUDDY_TEST_UPDATE_COMMAND) return process.env.GOALBUDDY_TEST_UPDATE_COMMAND;
|
|
755
|
+
if (process.env.CLAUDE_PLUGIN_ROOT || normalizedPath(__dirname).includes("/.claude/")) return `/plugin update ${pluginName}@${pluginName}`;
|
|
756
|
+
|
|
757
|
+
const userAgent = process.env.npm_config_user_agent || "";
|
|
758
|
+
if (/^pnpm\//.test(userAgent)) return `pnpm update -g ${canonicalCliName}`;
|
|
759
|
+
if (/^bun\//.test(userAgent)) return `bun update -g ${canonicalCliName}`;
|
|
760
|
+
if (process.env.MISE_EXE || process.env.MISE_SHELL || process.env.MISE_PROJECT_ROOT) return `mise upgrade npm:${canonicalCliName}`;
|
|
761
|
+
if (/^npm\//.test(userAgent)) return `npx ${canonicalCliName}@latest`;
|
|
762
|
+
|
|
763
|
+
return `use the install channel that installed ${canonicalProductName}`;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function normalizedPath(path) {
|
|
767
|
+
return String(path).replace(/\\/g, "/");
|
|
768
|
+
}
|
|
769
|
+
|
|
679
770
|
function plugin() {
|
|
680
771
|
const subcommand = positional(1) || "";
|
|
681
772
|
if (wantsHelp()) {
|
|
@@ -782,8 +873,95 @@ function cleanupLegacyCodexSkills() {
|
|
|
782
873
|
return removed;
|
|
783
874
|
}
|
|
784
875
|
|
|
876
|
+
function resetCodex() {
|
|
877
|
+
const configPath = join(codexHome(), "config.toml");
|
|
878
|
+
const removedConfigSections = [];
|
|
879
|
+
if (existsSync(configPath)) {
|
|
880
|
+
const existing = readFileSync(configPath, "utf8");
|
|
881
|
+
let updated = existing;
|
|
882
|
+
for (const header of [`[plugins."${pluginName}@${pluginName}"]`, `[marketplaces.${pluginName}]`]) {
|
|
883
|
+
const next = removeTomlTable(updated, header);
|
|
884
|
+
if (next !== updated) {
|
|
885
|
+
removedConfigSections.push(header);
|
|
886
|
+
updated = next;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
if (updated !== existing) writeFileSync(configPath, updated);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const removedPluginCachePaths = [];
|
|
893
|
+
const cacheRoot = pluginCacheOwnerRoot();
|
|
894
|
+
if (existsSync(cacheRoot)) {
|
|
895
|
+
rmSync(cacheRoot, { recursive: true, force: true });
|
|
896
|
+
removedPluginCachePaths.push(cacheRoot);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const removedAgents = [];
|
|
900
|
+
const agentsRoot = join(codexHome(), "agents");
|
|
901
|
+
for (const file of requiredAgentFiles) {
|
|
902
|
+
const path = join(agentsRoot, file);
|
|
903
|
+
if (!existsSync(path)) continue;
|
|
904
|
+
rmSync(path, { recursive: true, force: true });
|
|
905
|
+
removedAgents.push(path);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const removedLegacySkillPaths = cleanupLegacyCodexSkills();
|
|
909
|
+
const report = {
|
|
910
|
+
reset: true,
|
|
911
|
+
target: "codex",
|
|
912
|
+
codex_home: codexHome(),
|
|
913
|
+
config_path: configPath,
|
|
914
|
+
removed_config_sections: removedConfigSections,
|
|
915
|
+
removed_plugin_cache_paths: removedPluginCachePaths,
|
|
916
|
+
removed_agents: removedAgents,
|
|
917
|
+
removed_legacy_skill_paths: removedLegacySkillPaths,
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
if (hasFlag("--json")) {
|
|
921
|
+
printJson(report);
|
|
922
|
+
return report;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
console.log(`Reset ${canonicalProductName} Codex-owned runtime files`);
|
|
926
|
+
console.log(`Config sections: ${removedConfigSections.length ? removedConfigSections.join(", ") : "none"}`);
|
|
927
|
+
console.log(`Plugin cache: ${removedPluginCachePaths.length ? removedPluginCachePaths.join(", ") : "none"}`);
|
|
928
|
+
console.log(`Agents: ${removedAgents.length ? removedAgents.join(", ") : "none"}`);
|
|
929
|
+
console.log(`Legacy personal skills: ${removedLegacySkillPaths.length ? removedLegacySkillPaths.join(", ") : "none"}`);
|
|
930
|
+
return report;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function removeTomlTable(text, header) {
|
|
934
|
+
const normalized = text.endsWith("\n") || text.length === 0 ? text : `${text}\n`;
|
|
935
|
+
const lines = normalized.split("\n");
|
|
936
|
+
const output = [];
|
|
937
|
+
let skipping = false;
|
|
938
|
+
let removed = false;
|
|
939
|
+
const descendantPrefix = `${header.slice(0, -1)}.`;
|
|
940
|
+
|
|
941
|
+
for (const line of lines) {
|
|
942
|
+
const trimmed = line.trim();
|
|
943
|
+
if (trimmed === header || trimmed.startsWith(descendantPrefix)) {
|
|
944
|
+
skipping = true;
|
|
945
|
+
removed = true;
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (skipping && /^\s*\[/.test(line)) {
|
|
949
|
+
skipping = trimmed.startsWith(descendantPrefix);
|
|
950
|
+
if (skipping) continue;
|
|
951
|
+
}
|
|
952
|
+
if (!skipping) output.push(line);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (!removed) return text;
|
|
956
|
+
return output.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n*$/, "\n");
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function pluginCacheOwnerRoot() {
|
|
960
|
+
return join(codexHome(), "plugins", "cache", pluginName);
|
|
961
|
+
}
|
|
962
|
+
|
|
785
963
|
function pluginCacheRoot(version) {
|
|
786
|
-
return join(
|
|
964
|
+
return join(pluginCacheOwnerRoot(), pluginName, version);
|
|
787
965
|
}
|
|
788
966
|
|
|
789
967
|
function enablePluginConfig() {
|
|
@@ -913,8 +1091,9 @@ async function board() {
|
|
|
913
1091
|
process.exit(2);
|
|
914
1092
|
}
|
|
915
1093
|
|
|
1094
|
+
const absoluteGoal = resolve(goal);
|
|
916
1095
|
const script = ensureLocalBoardSurface();
|
|
917
|
-
const scriptArgs = [script, "--goal",
|
|
1096
|
+
const scriptArgs = [script, "--goal", absoluteGoal];
|
|
918
1097
|
for (const option of ["--host", "--port"]) {
|
|
919
1098
|
const value = optionValue(option);
|
|
920
1099
|
if (value) scriptArgs.push(option, value);
|
|
@@ -945,7 +1124,7 @@ async function prompt() {
|
|
|
945
1124
|
}
|
|
946
1125
|
|
|
947
1126
|
const script = join(skillSource, "scripts", "render-task-prompt.mjs");
|
|
948
|
-
const scriptArgs = [script, ...args.slice(1)];
|
|
1127
|
+
const scriptArgs = [script, ...resolveChildGoalArgs(args.slice(1))];
|
|
949
1128
|
const result = spawnSync(process.execPath, scriptArgs, {
|
|
950
1129
|
cwd: packageRoot,
|
|
951
1130
|
encoding: "utf8",
|
|
@@ -959,7 +1138,7 @@ async function prompt() {
|
|
|
959
1138
|
|
|
960
1139
|
async function parallelPlan() {
|
|
961
1140
|
const script = join(skillSource, "scripts", "parallel-plan.mjs");
|
|
962
|
-
const scriptArgs = [script, ...args.slice(1).filter((arg) => arg !== "--parallel-plan")];
|
|
1141
|
+
const scriptArgs = [script, ...resolveChildGoalArgs(args.slice(1).filter((arg) => arg !== "--parallel-plan"))];
|
|
963
1142
|
const result = spawnSync(process.execPath, scriptArgs, {
|
|
964
1143
|
cwd: packageRoot,
|
|
965
1144
|
encoding: "utf8",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "goalbuddy",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "A /goal operating loop for Codex and Claude Code: goal oracles, local boards, receipts, and verification.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
".agents/plugins/marketplace.json",
|
|
12
|
+
".claude-plugin/marketplace.json",
|
|
13
|
+
"CHANGELOG.md",
|
|
12
14
|
"README.md",
|
|
13
|
-
"RELEASE-0.3.5.md",
|
|
14
|
-
"RELEASE-0.3.7.md",
|
|
15
15
|
"CONTRIBUTING.md",
|
|
16
|
-
"
|
|
16
|
+
"docs/releases",
|
|
17
17
|
"plugins/goalbuddy",
|
|
18
18
|
"internal/assets",
|
|
19
19
|
"internal/cli",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"goalbuddy/templates"
|
|
25
25
|
],
|
|
26
26
|
"scripts": {
|
|
27
|
-
"check": "node --check internal/cli/*.mjs goalbuddy/scripts/*.mjs && node --test internal/test/*.test.mjs",
|
|
28
|
-
"test": "node --test internal/test/*.test.mjs",
|
|
27
|
+
"check": "node --check internal/cli/*.mjs goalbuddy/scripts/*.mjs goalbuddy/surfaces/local-goal-board/scripts/*.mjs goalbuddy/surfaces/local-goal-board/scripts/lib/*.mjs && node --test internal/test/*.test.mjs goalbuddy/surfaces/local-goal-board/test/*.test.mjs",
|
|
28
|
+
"test": "node --test internal/test/*.test.mjs goalbuddy/surfaces/local-goal-board/test/*.test.mjs",
|
|
29
29
|
"pack:dry-run": "npm pack --dry-run",
|
|
30
30
|
"postinstall": "node internal/cli/postinstall.mjs",
|
|
31
31
|
"publish:check": "node internal/cli/check-publish-version.mjs && npm run pack:dry-run",
|