@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.
- package/CHANGELOG.md +29 -0
- package/CODE_OF_CONDUCT.md +13 -0
- package/CONTRIBUTING.md +57 -0
- package/LICENSE +22 -0
- package/README.ja.md +220 -0
- package/README.ko.md +220 -0
- package/README.md +220 -0
- package/README.zh-CN.md +220 -0
- package/SECURITY.md +32 -0
- package/docs/RELEASE.md +79 -0
- package/docs/ROADMAP.md +77 -0
- package/examples/goalkeeper-session/checkpoint.md +58 -0
- package/examples/goalkeeper-session/context-pack.md +39 -0
- package/examples/goalkeeper-session/events.jsonl +5 -0
- package/package.json +65 -0
- package/src/goalkeeper/SKILL.md +166 -0
- package/src/goalkeeper/agents/openai.yaml +5 -0
- package/src/goalkeeper/references/event-schema.md +63 -0
- package/src/goalkeeper/references/guardrail.md +64 -0
- package/src/goalkeeper/references/workflow.md +187 -0
- package/src/goalkeeper/scripts/goalkeeper-append-event.mjs +263 -0
- package/src/goalkeeper/scripts/goalkeeper-doctor.mjs +490 -0
- package/src/goalkeeper/scripts/goalkeeper-init.mjs +271 -0
- package/src/goalkeeper/scripts/goalkeeper-turn-start.mjs +166 -0
- package/src/goalkeeper/scripts/goalkeeper-update-checkpoint.mjs +339 -0
- package/src/goalkeeper/templates/AGENTS.goalkeeper.md +48 -0
- package/src/goalkeeper/templates/CLAUDE.goalkeeper.md +48 -0
- package/src/goalkeeper/templates/active-session +1 -0
- package/src/goalkeeper/templates/checkpoint.md +54 -0
- package/src/goalkeeper/templates/context-pack.md +45 -0
- package/src/goalkeeper/templates/event.jsonl +3 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MAX_BYTES = 8_000;
|
|
7
|
+
const HARD_MAX_BYTES = 16_000;
|
|
8
|
+
|
|
9
|
+
const USAGE = `Usage:
|
|
10
|
+
node scripts/goalkeeper-update-checkpoint.mjs --goal <text> --next <text> [--session <goal-session-id>] [--workspace <path>] [--done <text>] [--status <text>] [--throughline <text>] [--why <text>] [--constraint <text> ...] [--forbidden <text> ...] [--decision <text> ...] [--attempt <text> ...] [--file <path> ...] [--verified <text> ...] [--unverified <text> ...] [--risk <text> ...] [--evidence <text> ...] [--max-bytes <n>] [--dry-run] [--json]
|
|
11
|
+
|
|
12
|
+
Replaces checkpoint.md with a bounded, canonical recovery checkpoint.
|
|
13
|
+
Append the corresponding event first with goalkeeper-append-event.mjs; this script writes only checkpoint.md.
|
|
14
|
+
If --session is omitted, <workspace>/.goalkeeper/active-session is used.
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
function parseArgs(argv) {
|
|
18
|
+
const options = {
|
|
19
|
+
sessionId: null,
|
|
20
|
+
workspace: ".",
|
|
21
|
+
title: null,
|
|
22
|
+
goal: null,
|
|
23
|
+
doneCriteria: null,
|
|
24
|
+
status: null,
|
|
25
|
+
throughline: null,
|
|
26
|
+
why: null,
|
|
27
|
+
constraints: [],
|
|
28
|
+
forbidden: [],
|
|
29
|
+
decisions: [],
|
|
30
|
+
attempts: [],
|
|
31
|
+
files: [],
|
|
32
|
+
verified: [],
|
|
33
|
+
unverified: [],
|
|
34
|
+
risks: [],
|
|
35
|
+
evidence: [],
|
|
36
|
+
next: null,
|
|
37
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
38
|
+
dryRun: false,
|
|
39
|
+
json: false,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const repeated = new Map([
|
|
43
|
+
["--constraint", "constraints"],
|
|
44
|
+
["--forbidden", "forbidden"],
|
|
45
|
+
["--decision", "decisions"],
|
|
46
|
+
["--attempt", "attempts"],
|
|
47
|
+
["--file", "files"],
|
|
48
|
+
["--verified", "verified"],
|
|
49
|
+
["--unverified", "unverified"],
|
|
50
|
+
["--risk", "risks"],
|
|
51
|
+
["--evidence", "evidence"],
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
55
|
+
const arg = argv[i];
|
|
56
|
+
if (arg === "--session") {
|
|
57
|
+
options.sessionId = argv[i + 1];
|
|
58
|
+
i += 1;
|
|
59
|
+
} else if (arg === "--workspace") {
|
|
60
|
+
options.workspace = argv[i + 1];
|
|
61
|
+
i += 1;
|
|
62
|
+
} else if (arg === "--title") {
|
|
63
|
+
options.title = argv[i + 1];
|
|
64
|
+
i += 1;
|
|
65
|
+
} else if (arg === "--goal") {
|
|
66
|
+
options.goal = argv[i + 1];
|
|
67
|
+
i += 1;
|
|
68
|
+
} else if (arg === "--done") {
|
|
69
|
+
options.doneCriteria = argv[i + 1];
|
|
70
|
+
i += 1;
|
|
71
|
+
} else if (arg === "--status") {
|
|
72
|
+
options.status = argv[i + 1];
|
|
73
|
+
i += 1;
|
|
74
|
+
} else if (arg === "--throughline") {
|
|
75
|
+
options.throughline = argv[i + 1];
|
|
76
|
+
i += 1;
|
|
77
|
+
} else if (arg === "--why") {
|
|
78
|
+
options.why = argv[i + 1];
|
|
79
|
+
i += 1;
|
|
80
|
+
} else if (arg === "--next") {
|
|
81
|
+
options.next = argv[i + 1];
|
|
82
|
+
i += 1;
|
|
83
|
+
} else if (arg === "--max-bytes") {
|
|
84
|
+
options.maxBytes = Number(argv[i + 1]);
|
|
85
|
+
i += 1;
|
|
86
|
+
} else if (arg === "--dry-run") {
|
|
87
|
+
options.dryRun = true;
|
|
88
|
+
} else if (arg === "--json") {
|
|
89
|
+
options.json = true;
|
|
90
|
+
} else if (repeated.has(arg)) {
|
|
91
|
+
options[repeated.get(arg)].push(argv[i + 1]);
|
|
92
|
+
i += 1;
|
|
93
|
+
} else {
|
|
94
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!options.workspace || !options.goal || !options.next) {
|
|
99
|
+
throw new Error("Missing required argument.");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!Number.isInteger(options.maxBytes) || options.maxBytes < 1 || options.maxBytes > HARD_MAX_BYTES) {
|
|
103
|
+
throw new Error(`--max-bytes must be an integer between 1 and ${HARD_MAX_BYTES}.`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const stringFields = [
|
|
107
|
+
"sessionId",
|
|
108
|
+
"title",
|
|
109
|
+
"goal",
|
|
110
|
+
"doneCriteria",
|
|
111
|
+
"status",
|
|
112
|
+
"throughline",
|
|
113
|
+
"why",
|
|
114
|
+
"next",
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
for (const field of stringFields) {
|
|
118
|
+
if (options[field] !== null && options[field] !== undefined) {
|
|
119
|
+
options[field] = normalizeText(options[field], field);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const key of repeated.values()) {
|
|
124
|
+
const normalize = key === "files" ? normalizePathText : normalizeText;
|
|
125
|
+
options[key] = options[key].map((value) => normalize(value, key));
|
|
126
|
+
if (options[key].some((value) => !value)) {
|
|
127
|
+
throw new Error(`Values for ${key} must not be empty.`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return options;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizeText(value, field) {
|
|
135
|
+
if (typeof value !== "string") {
|
|
136
|
+
throw new Error(`${field} must be a string.`);
|
|
137
|
+
}
|
|
138
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
139
|
+
if (!normalized) {
|
|
140
|
+
throw new Error(`${field} must not be empty.`);
|
|
141
|
+
}
|
|
142
|
+
return normalized;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizePathText(value, field) {
|
|
146
|
+
if (typeof value !== "string") {
|
|
147
|
+
throw new Error(`${field} must be a string.`);
|
|
148
|
+
}
|
|
149
|
+
const normalized = value.trim();
|
|
150
|
+
if (!normalized) {
|
|
151
|
+
throw new Error(`${field} must not be empty.`);
|
|
152
|
+
}
|
|
153
|
+
if (/[\r\n]/.test(normalized)) {
|
|
154
|
+
throw new Error(`${field} must not contain newlines.`);
|
|
155
|
+
}
|
|
156
|
+
return normalized;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isSingleSegmentSessionId(value) {
|
|
160
|
+
return typeof value === "string" && value.trim() && !value.includes("/") && !value.includes("..");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readActiveSession(workspace) {
|
|
164
|
+
const activeSessionPath = path.join(workspace, ".goalkeeper", "active-session");
|
|
165
|
+
if (!fs.existsSync(activeSessionPath)) {
|
|
166
|
+
throw new Error(`--session was omitted and active-session is missing: ${activeSessionPath}`);
|
|
167
|
+
}
|
|
168
|
+
const sessionId = fs.readFileSync(activeSessionPath, "utf8").trim();
|
|
169
|
+
if (!isSingleSegmentSessionId(sessionId)) {
|
|
170
|
+
throw new Error(`active-session must contain a single session id path segment: ${activeSessionPath}`);
|
|
171
|
+
}
|
|
172
|
+
return { sessionId, activeSessionPath };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function bulletList(items, fallback = "None recorded.") {
|
|
176
|
+
if (items.length === 0) return `- ${fallback}`;
|
|
177
|
+
return items.map((item) => `- ${item}`).join("\n");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function renderCheckpoint(options, context) {
|
|
181
|
+
const title = options.title || context.sessionId;
|
|
182
|
+
const contextPack = fs.existsSync(context.contextPackPath)
|
|
183
|
+
? `.goalkeeper/sessions/${context.sessionId}/context-pack.md`
|
|
184
|
+
: "None recorded.";
|
|
185
|
+
|
|
186
|
+
return `# Checkpoint: ${title}
|
|
187
|
+
|
|
188
|
+
## Active Goal
|
|
189
|
+
|
|
190
|
+
- Objective: ${options.goal}
|
|
191
|
+
- Done criteria: ${options.doneCriteria || "Not explicitly recorded."}
|
|
192
|
+
- Current status: ${options.status || "Open."}
|
|
193
|
+
|
|
194
|
+
## Throughline
|
|
195
|
+
|
|
196
|
+
- Current direction: ${options.throughline || "Continue from the active goal and latest verified state."}
|
|
197
|
+
- Why this direction: ${options.why || "Preserve direction across compaction with project-local state."}
|
|
198
|
+
|
|
199
|
+
## Constraints
|
|
200
|
+
|
|
201
|
+
- Non-negotiable:
|
|
202
|
+
${indentBullets(options.constraints, "None recorded.")}
|
|
203
|
+
- Forbidden approaches:
|
|
204
|
+
${indentBullets(options.forbidden, "None recorded.")}
|
|
205
|
+
|
|
206
|
+
## Decisions
|
|
207
|
+
|
|
208
|
+
${bulletList(options.decisions)}
|
|
209
|
+
|
|
210
|
+
## Attempts And Failures
|
|
211
|
+
|
|
212
|
+
${bulletList(options.attempts)}
|
|
213
|
+
|
|
214
|
+
## Important Files
|
|
215
|
+
|
|
216
|
+
${bulletList(options.files)}
|
|
217
|
+
|
|
218
|
+
## Evidence
|
|
219
|
+
|
|
220
|
+
${bulletList(options.evidence)}
|
|
221
|
+
|
|
222
|
+
## Context Pack
|
|
223
|
+
|
|
224
|
+
- ${contextPack}
|
|
225
|
+
|
|
226
|
+
## Verification
|
|
227
|
+
|
|
228
|
+
- Verified:
|
|
229
|
+
${indentBullets(options.verified, "None recorded.")}
|
|
230
|
+
- Not yet verified:
|
|
231
|
+
${indentBullets(options.unverified, "None recorded.")}
|
|
232
|
+
|
|
233
|
+
## Open Risks
|
|
234
|
+
|
|
235
|
+
${bulletList(options.risks)}
|
|
236
|
+
|
|
237
|
+
## Next Action
|
|
238
|
+
|
|
239
|
+
- ${options.next}
|
|
240
|
+
|
|
241
|
+
## Last Updated
|
|
242
|
+
|
|
243
|
+
- ${context.updatedAt}
|
|
244
|
+
`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function indentBullets(items, fallback) {
|
|
248
|
+
return bulletList(items, fallback)
|
|
249
|
+
.split("\n")
|
|
250
|
+
.map((line) => ` ${line}`)
|
|
251
|
+
.join("\n");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function main() {
|
|
255
|
+
let options;
|
|
256
|
+
try {
|
|
257
|
+
options = parseArgs(process.argv.slice(2));
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error(error.message);
|
|
260
|
+
console.error(USAGE);
|
|
261
|
+
process.exit(2);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const workspace = path.resolve(options.workspace);
|
|
265
|
+
if (!fs.existsSync(workspace) || !fs.statSync(workspace).isDirectory()) {
|
|
266
|
+
console.error(`Workspace does not exist or is not a directory: ${workspace}`);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let activeSessionPath = null;
|
|
271
|
+
if (!options.sessionId) {
|
|
272
|
+
try {
|
|
273
|
+
const active = readActiveSession(workspace);
|
|
274
|
+
options.sessionId = active.sessionId;
|
|
275
|
+
activeSessionPath = active.activeSessionPath;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error(error.message);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!isSingleSegmentSessionId(options.sessionId)) {
|
|
283
|
+
console.error("Session id must be a single path segment.");
|
|
284
|
+
process.exit(2);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const sessionDir = path.join(workspace, ".goalkeeper", "sessions", options.sessionId);
|
|
288
|
+
const checkpointPath = path.join(sessionDir, "checkpoint.md");
|
|
289
|
+
const contextPackPath = path.join(sessionDir, "context-pack.md");
|
|
290
|
+
|
|
291
|
+
if (!fs.existsSync(sessionDir) || !fs.statSync(sessionDir).isDirectory()) {
|
|
292
|
+
console.error(`Goalkeeper session directory is missing: ${sessionDir}`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const updatedAt = new Date().toISOString();
|
|
297
|
+
const checkpoint = renderCheckpoint(options, {
|
|
298
|
+
sessionId: options.sessionId,
|
|
299
|
+
contextPackPath,
|
|
300
|
+
updatedAt,
|
|
301
|
+
});
|
|
302
|
+
const bytes = Buffer.byteLength(checkpoint);
|
|
303
|
+
|
|
304
|
+
if (bytes > options.maxBytes) {
|
|
305
|
+
console.error(`Rendered checkpoint is ${bytes} bytes, over --max-bytes ${options.maxBytes}.`);
|
|
306
|
+
console.error("Shorten fields or raise --max-bytes up to the 16000 hard limit only when recovery cost is acceptable.");
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!options.dryRun) {
|
|
311
|
+
fs.writeFileSync(checkpointPath, checkpoint);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const result = {
|
|
315
|
+
ok: true,
|
|
316
|
+
dryRun: options.dryRun,
|
|
317
|
+
workspace,
|
|
318
|
+
sessionId: options.sessionId,
|
|
319
|
+
sessionDir,
|
|
320
|
+
checkpointPath,
|
|
321
|
+
activeSessionPath,
|
|
322
|
+
bytes,
|
|
323
|
+
maxBytes: options.maxBytes,
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
if (options.json) {
|
|
327
|
+
console.log(JSON.stringify(result, null, 2));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
console.log("Goalkeeper update-checkpoint: PASS");
|
|
332
|
+
console.log(`Workspace: ${workspace}`);
|
|
333
|
+
console.log(`Session: ${options.sessionId}`);
|
|
334
|
+
console.log(`Checkpoint: ${checkpointPath}`);
|
|
335
|
+
console.log(`Bytes: ${bytes}/${options.maxBytes}`);
|
|
336
|
+
if (options.dryRun) console.log("Dry run: yes");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
main();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Goalkeeper Guardrail
|
|
2
|
+
|
|
3
|
+
When this repository has an active `.goalkeeper/sessions/<goal-session-id>/` directory, treat it as the continuity source for long-running agent work.
|
|
4
|
+
|
|
5
|
+
At the start of each new assistant turn, before reading normal project files or making edits:
|
|
6
|
+
|
|
7
|
+
1. Run `pwd` if the workspace is unclear.
|
|
8
|
+
2. Locate the active checkpoint under `.goalkeeper/sessions/`.
|
|
9
|
+
3. Read `.goalkeeper/sessions/<goal-session-id>/checkpoint.md`.
|
|
10
|
+
4. If the checkpoint is unclear or too thin, read `.goalkeeper/sessions/<goal-session-id>/context-pack.md`.
|
|
11
|
+
5. If exact evidence is needed, inspect recent `.goalkeeper/sessions/<goal-session-id>/events.jsonl` entries.
|
|
12
|
+
|
|
13
|
+
If this repository includes `scripts/goalkeeper-turn-start.mjs`, you may use:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
node scripts/goalkeeper-turn-start.mjs --session <goal-session-id>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
If `.goalkeeper/active-session` contains the current session id, this shorter form is also valid:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
node scripts/goalkeeper-turn-start.mjs
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
If the helper comes from an installed skill path instead of this repository, pass the target workspace:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
node <skill-path>/scripts/goalkeeper-turn-start.mjs --workspace <workspace> --session <goal-session-id>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
If checkpoint recovery needs the larger context pack too, add `--context`.
|
|
32
|
+
|
|
33
|
+
Allowed before reading the checkpoint:
|
|
34
|
+
|
|
35
|
+
- `pwd`
|
|
36
|
+
- listing `.goalkeeper/sessions/`
|
|
37
|
+
- reading `.goalkeeper/active-session`
|
|
38
|
+
- minimal filename inspection needed to choose the active session
|
|
39
|
+
- running `node scripts/goalkeeper-turn-start.mjs --session <goal-session-id>`
|
|
40
|
+
- running `node scripts/goalkeeper-turn-start.mjs`
|
|
41
|
+
- running `node <skill-path>/scripts/goalkeeper-turn-start.mjs --workspace <workspace> --session <goal-session-id>`
|
|
42
|
+
- adding `--context` to the turn-start command when the checkpoint is too thin
|
|
43
|
+
|
|
44
|
+
Do not read project docs, source files, examples, tests, or make edits before the checkpoint read.
|
|
45
|
+
|
|
46
|
+
If you notice that you continued after compaction or resume without reading the checkpoint, stop, read it immediately, append a `recovery_violation` event, then continue from the recovered state.
|
|
47
|
+
|
|
48
|
+
Do not claim Goalkeeper reduces compaction frequency. Its purpose is direction recovery after compaction, resume, or handoff.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Goalkeeper Guardrail
|
|
2
|
+
|
|
3
|
+
When this repository has an active `.goalkeeper/sessions/<goal-session-id>/` directory, treat it as the continuity source for long-running Claude Code or agent work.
|
|
4
|
+
|
|
5
|
+
At the start of each new assistant turn, before reading normal project files or making edits:
|
|
6
|
+
|
|
7
|
+
1. Run `pwd` if the workspace is unclear.
|
|
8
|
+
2. Locate the active checkpoint under `.goalkeeper/sessions/`.
|
|
9
|
+
3. Read `.goalkeeper/sessions/<goal-session-id>/checkpoint.md`.
|
|
10
|
+
4. If the checkpoint is unclear or too thin, read `.goalkeeper/sessions/<goal-session-id>/context-pack.md`.
|
|
11
|
+
5. If exact evidence is needed, inspect recent `.goalkeeper/sessions/<goal-session-id>/events.jsonl` entries.
|
|
12
|
+
|
|
13
|
+
If this repository includes `scripts/goalkeeper-turn-start.mjs`, you may use:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
node scripts/goalkeeper-turn-start.mjs --session <goal-session-id>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
If `.goalkeeper/active-session` contains the current session id, this shorter form is also valid:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
node scripts/goalkeeper-turn-start.mjs
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
If the helper comes from an installed skill path instead of this repository, pass the target workspace:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
node <skill-path>/scripts/goalkeeper-turn-start.mjs --workspace <workspace> --session <goal-session-id>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
If checkpoint recovery needs the larger context pack too, add `--context`.
|
|
32
|
+
|
|
33
|
+
Allowed before reading the checkpoint:
|
|
34
|
+
|
|
35
|
+
- `pwd`
|
|
36
|
+
- listing `.goalkeeper/sessions/`
|
|
37
|
+
- reading `.goalkeeper/active-session`
|
|
38
|
+
- minimal filename inspection needed to choose the active session
|
|
39
|
+
- running `node scripts/goalkeeper-turn-start.mjs --session <goal-session-id>`
|
|
40
|
+
- running `node scripts/goalkeeper-turn-start.mjs`
|
|
41
|
+
- running `node <skill-path>/scripts/goalkeeper-turn-start.mjs --workspace <workspace> --session <goal-session-id>`
|
|
42
|
+
- adding `--context` to the turn-start command when the checkpoint is too thin
|
|
43
|
+
|
|
44
|
+
Do not read project docs, source files, examples, tests, or make edits before the checkpoint read.
|
|
45
|
+
|
|
46
|
+
If you notice that you continued after compaction or resume without reading the checkpoint, stop, read it immediately, append a `recovery_violation` event, then continue from the recovered state.
|
|
47
|
+
|
|
48
|
+
Do not claim Goalkeeper reduces compaction frequency. Its purpose is direction recovery after compaction, resume, or handoff.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<goal-session-id>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Goalkeeper Checkpoint
|
|
2
|
+
|
|
3
|
+
## Active Goal
|
|
4
|
+
|
|
5
|
+
- Objective:
|
|
6
|
+
- Done criteria:
|
|
7
|
+
- Current status:
|
|
8
|
+
|
|
9
|
+
## Throughline
|
|
10
|
+
|
|
11
|
+
- Current direction:
|
|
12
|
+
- Why this direction:
|
|
13
|
+
|
|
14
|
+
## Constraints
|
|
15
|
+
|
|
16
|
+
- Non-negotiable:
|
|
17
|
+
- Forbidden approaches:
|
|
18
|
+
|
|
19
|
+
## Decisions
|
|
20
|
+
|
|
21
|
+
-
|
|
22
|
+
|
|
23
|
+
## Attempts And Failures
|
|
24
|
+
|
|
25
|
+
-
|
|
26
|
+
|
|
27
|
+
## Important Files
|
|
28
|
+
|
|
29
|
+
-
|
|
30
|
+
|
|
31
|
+
## Evidence
|
|
32
|
+
|
|
33
|
+
-
|
|
34
|
+
|
|
35
|
+
## Context Pack
|
|
36
|
+
|
|
37
|
+
-
|
|
38
|
+
|
|
39
|
+
## Verification
|
|
40
|
+
|
|
41
|
+
- Verified:
|
|
42
|
+
- Not yet verified:
|
|
43
|
+
|
|
44
|
+
## Open Risks
|
|
45
|
+
|
|
46
|
+
-
|
|
47
|
+
|
|
48
|
+
## Next Action
|
|
49
|
+
|
|
50
|
+
-
|
|
51
|
+
|
|
52
|
+
## Last Updated
|
|
53
|
+
|
|
54
|
+
-
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Goalkeeper Context Pack
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Preserve medium-density context that is too detailed for `checkpoint.md` but too important to leave only in compacted conversation history.
|
|
6
|
+
|
|
7
|
+
Read this when:
|
|
8
|
+
|
|
9
|
+
- `checkpoint.md` is too thin to explain why the current direction exists
|
|
10
|
+
- a long-running goal resumes after many turns or a long gap
|
|
11
|
+
- the agent is about to change direction and needs the decision chain
|
|
12
|
+
|
|
13
|
+
## Active Goal
|
|
14
|
+
|
|
15
|
+
-
|
|
16
|
+
|
|
17
|
+
## Durable Constraints
|
|
18
|
+
|
|
19
|
+
-
|
|
20
|
+
|
|
21
|
+
## Working Model
|
|
22
|
+
|
|
23
|
+
-
|
|
24
|
+
|
|
25
|
+
## Decision Chain
|
|
26
|
+
|
|
27
|
+
-
|
|
28
|
+
|
|
29
|
+
## Rejected Alternatives
|
|
30
|
+
|
|
31
|
+
-
|
|
32
|
+
|
|
33
|
+
## Open Threads
|
|
34
|
+
|
|
35
|
+
-
|
|
36
|
+
|
|
37
|
+
## Evidence Index
|
|
38
|
+
|
|
39
|
+
- Atomic events: `.goalkeeper/sessions/<goal-session-id>/events.jsonl`
|
|
40
|
+
|
|
41
|
+
## Maintenance Notes
|
|
42
|
+
|
|
43
|
+
- Keep `checkpoint.md` short enough to read every turn.
|
|
44
|
+
- Use this file for the larger explanation that helps reconstruct pre-compaction reasoning.
|
|
45
|
+
- Do not paste raw transcripts or long command output here.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"ts":"2026-05-17T00:00:00Z","type":"goal","text":"Define the active long-running agent goal.","status":"open"}
|
|
2
|
+
{"ts":"2026-05-17T00:00:00Z","type":"user_constraint","text":"Record durable user constraints that must survive compaction."}
|
|
3
|
+
{"ts":"2026-05-17T00:00:00Z","type":"decision","text":"Record the current direction and why it was chosen."}
|