@deltafleet/goalkeeper 0.2.0

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.
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ const USAGE = `Usage:
7
+ node scripts/goalkeeper-init.mjs --session <goal-session-id> --goal <text> [--workspace <path>] [--constraint <text> ...] [--no-activate] [--force] [--json]
8
+
9
+ Creates a project-local Goalkeeper session with checkpoint.md, context-pack.md, and events.jsonl.
10
+ This script writes only under <workspace>/.goalkeeper/sessions/<goal-session-id>/.
11
+ `;
12
+
13
+ function parseArgs(argv) {
14
+ const options = {
15
+ sessionId: null,
16
+ goal: null,
17
+ workspace: ".",
18
+ constraints: [],
19
+ activate: true,
20
+ force: false,
21
+ json: false,
22
+ };
23
+
24
+ for (let i = 0; i < argv.length; i += 1) {
25
+ const arg = argv[i];
26
+ if (arg === "--session") {
27
+ options.sessionId = argv[i + 1];
28
+ i += 1;
29
+ } else if (arg === "--goal") {
30
+ options.goal = argv[i + 1];
31
+ i += 1;
32
+ } else if (arg === "--workspace") {
33
+ options.workspace = argv[i + 1];
34
+ i += 1;
35
+ } else if (arg === "--constraint") {
36
+ options.constraints.push(argv[i + 1]);
37
+ i += 1;
38
+ } else if (arg === "--no-activate") {
39
+ options.activate = false;
40
+ } else if (arg === "--force") {
41
+ options.force = true;
42
+ } else if (arg === "--json") {
43
+ options.json = true;
44
+ } else {
45
+ throw new Error(`Unknown argument: ${arg}`);
46
+ }
47
+ }
48
+
49
+ if (!options.sessionId || !options.goal || !options.workspace) {
50
+ throw new Error("Missing required argument.");
51
+ }
52
+
53
+ if (options.sessionId.includes("/") || options.sessionId.includes("..")) {
54
+ throw new Error("Session id must be a single path segment.");
55
+ }
56
+
57
+ if (options.constraints.some((constraint) => !constraint)) {
58
+ throw new Error("Constraint text must not be empty.");
59
+ }
60
+
61
+ return options;
62
+ }
63
+
64
+ function eventLine(record) {
65
+ return `${JSON.stringify(record)}\n`;
66
+ }
67
+
68
+ function renderCheckpoint({ sessionId, goal, constraints, createdAt }) {
69
+ const constraintLines =
70
+ constraints.length > 0
71
+ ? constraints.map((constraint) => `- ${constraint}`).join("\n")
72
+ : "- None recorded yet.";
73
+
74
+ return `# Checkpoint: ${sessionId}
75
+
76
+ ## Active Goal
77
+
78
+ ${goal}
79
+
80
+ ## Current Throughline
81
+
82
+ Initial Goalkeeper session created. Replace this section with the actual working direction after the first meaningful decision or investigation result.
83
+
84
+ ## Constraints
85
+
86
+ ${constraintLines}
87
+
88
+ ## Evidence
89
+
90
+ - Initialized at ${createdAt}.
91
+ - Runtime state is project-local under \`.goalkeeper/sessions/${sessionId}/\`.
92
+
93
+ ## Open Risks
94
+
95
+ - This is a seed checkpoint. It is not yet proof that recovery works for the project.
96
+ - Add failed attempts, verification results, and exact next actions as the session develops.
97
+
98
+ ## Next Action
99
+
100
+ Run Goalkeeper doctor for this workspace, then continue the goal and update this checkpoint after the first meaningful state change.
101
+ `;
102
+ }
103
+
104
+ function renderContextPack({ sessionId, goal, constraints, createdAt }) {
105
+ const constraintLines =
106
+ constraints.length > 0
107
+ ? constraints.map((constraint) => `- ${constraint}`).join("\n")
108
+ : "- None recorded yet.";
109
+
110
+ return `# Context Pack: ${sessionId}
111
+
112
+ ## Purpose
113
+
114
+ This file preserves medium-density context that is too detailed for checkpoint.md but too important to rely on compacted conversation memory.
115
+
116
+ Read this file when checkpoint.md is too thin, when resuming after a long gap, or before changing direction on a long-running goal.
117
+
118
+ ## Active Goal
119
+
120
+ ${goal}
121
+
122
+ ## Durable Constraints
123
+
124
+ ${constraintLines}
125
+
126
+ ## Working Model
127
+
128
+ - Not recorded yet.
129
+
130
+ ## Decision Chain
131
+
132
+ - Not recorded yet.
133
+
134
+ ## Rejected Alternatives
135
+
136
+ - Not recorded yet.
137
+
138
+ ## Open Threads
139
+
140
+ - Not recorded yet.
141
+
142
+ ## Evidence Index
143
+
144
+ - Initialized at ${createdAt}.
145
+ - Atomic events live in \`.goalkeeper/sessions/${sessionId}/events.jsonl\`.
146
+
147
+ ## Maintenance Notes
148
+
149
+ - Keep checkpoint.md short enough to read every turn.
150
+ - Use this file for the larger explanation that helps reconstruct pre-compaction reasoning.
151
+ - Do not paste raw transcripts or long command output here.
152
+ `;
153
+ }
154
+
155
+ function buildEvents({ goal, constraints, createdAt }) {
156
+ const events = [
157
+ {
158
+ ts: createdAt,
159
+ type: "goal",
160
+ text: goal,
161
+ status: "open",
162
+ },
163
+ ];
164
+
165
+ for (const constraint of constraints) {
166
+ events.push({
167
+ ts: createdAt,
168
+ type: "user_constraint",
169
+ text: constraint,
170
+ });
171
+ }
172
+
173
+ events.push({
174
+ ts: createdAt,
175
+ type: "next_action",
176
+ text: "Run Goalkeeper doctor, then update checkpoint.md after the first meaningful state change.",
177
+ status: "open",
178
+ });
179
+
180
+ return events.map(eventLine).join("");
181
+ }
182
+
183
+ function main() {
184
+ let options;
185
+ try {
186
+ options = parseArgs(process.argv.slice(2));
187
+ } catch (error) {
188
+ console.error(error.message);
189
+ console.error(USAGE);
190
+ process.exit(2);
191
+ }
192
+
193
+ const workspace = path.resolve(options.workspace);
194
+ const goalkeeperDir = path.join(workspace, ".goalkeeper");
195
+ const sessionDir = path.join(workspace, ".goalkeeper", "sessions", options.sessionId);
196
+ const checkpointPath = path.join(sessionDir, "checkpoint.md");
197
+ const contextPackPath = path.join(sessionDir, "context-pack.md");
198
+ const eventsPath = path.join(sessionDir, "events.jsonl");
199
+ const activeSessionPath = path.join(goalkeeperDir, "active-session");
200
+
201
+ if (!fs.existsSync(workspace) || !fs.statSync(workspace).isDirectory()) {
202
+ console.error(`Workspace does not exist or is not a directory: ${workspace}`);
203
+ process.exit(1);
204
+ }
205
+
206
+ if (!options.force && (fs.existsSync(checkpointPath) || fs.existsSync(contextPackPath) || fs.existsSync(eventsPath))) {
207
+ console.error(`Goalkeeper session already exists: ${sessionDir}`);
208
+ console.error("Use --force only if you intentionally want to overwrite checkpoint.md, context-pack.md, and events.jsonl.");
209
+ process.exit(1);
210
+ }
211
+
212
+ const createdAt = new Date().toISOString();
213
+ fs.mkdirSync(sessionDir, { recursive: true });
214
+ fs.writeFileSync(
215
+ checkpointPath,
216
+ renderCheckpoint({
217
+ sessionId: options.sessionId,
218
+ goal: options.goal,
219
+ constraints: options.constraints,
220
+ createdAt,
221
+ }),
222
+ );
223
+ fs.writeFileSync(
224
+ contextPackPath,
225
+ renderContextPack({
226
+ sessionId: options.sessionId,
227
+ goal: options.goal,
228
+ constraints: options.constraints,
229
+ createdAt,
230
+ }),
231
+ );
232
+ fs.writeFileSync(
233
+ eventsPath,
234
+ buildEvents({
235
+ goal: options.goal,
236
+ constraints: options.constraints,
237
+ createdAt,
238
+ }),
239
+ );
240
+ if (options.activate) {
241
+ fs.writeFileSync(activeSessionPath, `${options.sessionId}\n`);
242
+ }
243
+
244
+ const result = {
245
+ ok: true,
246
+ workspace,
247
+ sessionId: options.sessionId,
248
+ sessionDir,
249
+ checkpointPath,
250
+ contextPackPath,
251
+ eventsPath,
252
+ activeSessionPath: options.activate ? activeSessionPath : null,
253
+ };
254
+
255
+ if (options.json) {
256
+ console.log(JSON.stringify(result, null, 2));
257
+ return;
258
+ }
259
+
260
+ console.log("Goalkeeper init: PASS");
261
+ console.log(`Workspace: ${workspace}`);
262
+ console.log(`Session: ${options.sessionId}`);
263
+ console.log(`Checkpoint: ${checkpointPath}`);
264
+ console.log(`Context pack: ${contextPackPath}`);
265
+ console.log(`Events: ${eventsPath}`);
266
+ if (options.activate) {
267
+ console.log(`Active session: ${activeSessionPath}`);
268
+ }
269
+ }
270
+
271
+ main();
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ const USAGE = `Usage:
7
+ node scripts/goalkeeper-turn-start.mjs [--session <goal-session-id>] [--workspace <path>] [--events <n>] [--context] [--json]
8
+
9
+ Reads the active Goalkeeper checkpoint at the start of an agent turn.
10
+ This script reads only .goalkeeper state.
11
+ `;
12
+
13
+ function parseArgs(argv) {
14
+ const options = {
15
+ sessionId: null,
16
+ workspace: ".",
17
+ events: 0,
18
+ context: false,
19
+ json: false,
20
+ };
21
+
22
+ for (let i = 0; i < argv.length; i += 1) {
23
+ const arg = argv[i];
24
+ if (arg === "--session") {
25
+ options.sessionId = argv[i + 1];
26
+ i += 1;
27
+ } else if (arg === "--workspace") {
28
+ options.workspace = argv[i + 1];
29
+ i += 1;
30
+ } else if (arg === "--events") {
31
+ options.events = Number.parseInt(argv[i + 1], 10);
32
+ i += 1;
33
+ } else if (arg === "--context") {
34
+ options.context = true;
35
+ } else if (arg === "--json") {
36
+ options.json = true;
37
+ } else {
38
+ throw new Error(`Unknown argument: ${arg}`);
39
+ }
40
+ }
41
+
42
+ if (!options.workspace || !Number.isInteger(options.events) || options.events < 0) {
43
+ throw new Error("Missing or invalid required argument.");
44
+ }
45
+
46
+ if (options.sessionId && !isValidSessionId(options.sessionId)) {
47
+ throw new Error("Session id must be a single path segment.");
48
+ }
49
+
50
+ return options;
51
+ }
52
+
53
+ function isValidSessionId(sessionId) {
54
+ return typeof sessionId === "string" && sessionId.trim().length > 0 && !sessionId.includes("/") && !sessionId.includes("..");
55
+ }
56
+
57
+ function resolveSessionId(workspace, explicitSessionId) {
58
+ if (explicitSessionId) {
59
+ return { sessionId: explicitSessionId, activeSessionPath: null };
60
+ }
61
+
62
+ const activeSessionPath = path.join(workspace, ".goalkeeper", "active-session");
63
+ if (!fs.existsSync(activeSessionPath)) {
64
+ throw new Error(`Missing --session and active session pointer: ${activeSessionPath}`);
65
+ }
66
+
67
+ const sessionId = fs.readFileSync(activeSessionPath, "utf8").trim();
68
+ if (!isValidSessionId(sessionId)) {
69
+ throw new Error(`Invalid active session id in ${activeSessionPath}`);
70
+ }
71
+
72
+ return { sessionId, activeSessionPath };
73
+ }
74
+
75
+ function readRecentEvents(eventsPath, limit) {
76
+ if (limit <= 0 || !fs.existsSync(eventsPath)) return [];
77
+ const lines = fs
78
+ .readFileSync(eventsPath, "utf8")
79
+ .split(/\r?\n/)
80
+ .filter((line) => line.trim().length > 0);
81
+ return lines.slice(-limit);
82
+ }
83
+
84
+ function main() {
85
+ let options;
86
+ try {
87
+ options = parseArgs(process.argv.slice(2));
88
+ } catch (error) {
89
+ console.error(error.message);
90
+ console.error(USAGE);
91
+ process.exit(2);
92
+ }
93
+
94
+ const workspace = path.resolve(options.workspace);
95
+ let resolvedSession;
96
+ try {
97
+ resolvedSession = resolveSessionId(workspace, options.sessionId);
98
+ } catch (error) {
99
+ console.error(error.message);
100
+ process.exit(1);
101
+ }
102
+
103
+ const sessionDir = path.join(workspace, ".goalkeeper", "sessions", resolvedSession.sessionId);
104
+ const checkpointPath = path.join(sessionDir, "checkpoint.md");
105
+ const contextPackPath = path.join(sessionDir, "context-pack.md");
106
+ const eventsPath = path.join(sessionDir, "events.jsonl");
107
+
108
+ if (!fs.existsSync(checkpointPath)) {
109
+ console.error(`Missing checkpoint: ${checkpointPath}`);
110
+ process.exit(1);
111
+ }
112
+
113
+ const checkpoint = fs.readFileSync(checkpointPath, "utf8");
114
+ const contextPack = options.context && fs.existsSync(contextPackPath) ? fs.readFileSync(contextPackPath, "utf8") : null;
115
+ const recentEvents = readRecentEvents(eventsPath, options.events);
116
+
117
+ if (options.json) {
118
+ console.log(
119
+ JSON.stringify(
120
+ {
121
+ sessionId: resolvedSession.sessionId,
122
+ workspace,
123
+ activeSessionPath: resolvedSession.activeSessionPath,
124
+ checkpointPath,
125
+ contextPackPath: fs.existsSync(contextPackPath) ? contextPackPath : null,
126
+ eventsPath,
127
+ checkpoint,
128
+ contextPack,
129
+ recentEvents,
130
+ },
131
+ null,
132
+ 2,
133
+ ),
134
+ );
135
+ return;
136
+ }
137
+
138
+ console.log(`# Goalkeeper Turn Start`);
139
+ console.log("");
140
+ console.log(`Session: ${resolvedSession.sessionId}`);
141
+ console.log(`Workspace: ${workspace}`);
142
+ if (resolvedSession.activeSessionPath) {
143
+ console.log(`Active session pointer: ${resolvedSession.activeSessionPath}`);
144
+ }
145
+ console.log(`Checkpoint: ${checkpointPath}`);
146
+ if (fs.existsSync(contextPackPath)) {
147
+ console.log(`Context pack: ${contextPackPath}${options.context ? "" : " (use --context when checkpoint is too thin)"}`);
148
+ }
149
+ console.log("");
150
+ console.log(checkpoint.trimEnd());
151
+
152
+ if (contextPack) {
153
+ console.log("");
154
+ console.log(contextPack.trimEnd());
155
+ }
156
+
157
+ if (recentEvents.length > 0) {
158
+ console.log("");
159
+ console.log(`## Recent Events`);
160
+ for (const line of recentEvents) {
161
+ console.log(line);
162
+ }
163
+ }
164
+ }
165
+
166
+ main();