@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,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();
|