@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,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const EVENT_TYPES = new Set([
|
|
7
|
+
"goal",
|
|
8
|
+
"user_constraint",
|
|
9
|
+
"decision",
|
|
10
|
+
"attempt",
|
|
11
|
+
"failure",
|
|
12
|
+
"edit",
|
|
13
|
+
"command",
|
|
14
|
+
"verification",
|
|
15
|
+
"risk",
|
|
16
|
+
"handoff",
|
|
17
|
+
"next_action",
|
|
18
|
+
"compact_observed",
|
|
19
|
+
"recovery_violation",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const STATUSES = new Set(["open", "done", "failed", "blocked", "superseded"]);
|
|
23
|
+
|
|
24
|
+
const USAGE = `Usage:
|
|
25
|
+
node scripts/goalkeeper-append-event.mjs --type <event-type> --text <summary> [--session <goal-session-id>] [--workspace <path>] [--goal <text>] [--reason <text>] [--evidence <text>] [--status <status>] [--file <path> ...] [--command <cmd> ...] [--ts <iso>] [--json]
|
|
26
|
+
|
|
27
|
+
Appends one validated JSONL event to <workspace>/.goalkeeper/sessions/<goal-session-id>/events.jsonl.
|
|
28
|
+
This script writes only to the target events.jsonl file.
|
|
29
|
+
If --session is omitted, <workspace>/.goalkeeper/active-session is used.
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
function parseArgs(argv) {
|
|
33
|
+
const options = {
|
|
34
|
+
sessionId: null,
|
|
35
|
+
workspace: ".",
|
|
36
|
+
type: null,
|
|
37
|
+
text: null,
|
|
38
|
+
goal: null,
|
|
39
|
+
reason: null,
|
|
40
|
+
evidence: null,
|
|
41
|
+
status: null,
|
|
42
|
+
files: [],
|
|
43
|
+
commands: [],
|
|
44
|
+
ts: null,
|
|
45
|
+
json: false,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
49
|
+
const arg = argv[i];
|
|
50
|
+
if (arg === "--session") {
|
|
51
|
+
options.sessionId = argv[i + 1];
|
|
52
|
+
i += 1;
|
|
53
|
+
} else if (arg === "--workspace") {
|
|
54
|
+
options.workspace = argv[i + 1];
|
|
55
|
+
i += 1;
|
|
56
|
+
} else if (arg === "--type") {
|
|
57
|
+
options.type = argv[i + 1];
|
|
58
|
+
i += 1;
|
|
59
|
+
} else if (arg === "--text") {
|
|
60
|
+
options.text = argv[i + 1];
|
|
61
|
+
i += 1;
|
|
62
|
+
} else if (arg === "--goal") {
|
|
63
|
+
options.goal = argv[i + 1];
|
|
64
|
+
i += 1;
|
|
65
|
+
} else if (arg === "--reason") {
|
|
66
|
+
options.reason = argv[i + 1];
|
|
67
|
+
i += 1;
|
|
68
|
+
} else if (arg === "--evidence") {
|
|
69
|
+
options.evidence = argv[i + 1];
|
|
70
|
+
i += 1;
|
|
71
|
+
} else if (arg === "--status") {
|
|
72
|
+
options.status = argv[i + 1];
|
|
73
|
+
i += 1;
|
|
74
|
+
} else if (arg === "--file") {
|
|
75
|
+
options.files.push(argv[i + 1]);
|
|
76
|
+
i += 1;
|
|
77
|
+
} else if (arg === "--command") {
|
|
78
|
+
options.commands.push(argv[i + 1]);
|
|
79
|
+
i += 1;
|
|
80
|
+
} else if (arg === "--ts") {
|
|
81
|
+
options.ts = argv[i + 1];
|
|
82
|
+
i += 1;
|
|
83
|
+
} else if (arg === "--json") {
|
|
84
|
+
options.json = true;
|
|
85
|
+
} else {
|
|
86
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!options.workspace || !options.type || !options.text) {
|
|
91
|
+
throw new Error("Missing required argument.");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (options.sessionId && !isValidSessionId(options.sessionId)) {
|
|
95
|
+
throw new Error("Session id must be a single path segment.");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!EVENT_TYPES.has(options.type)) {
|
|
99
|
+
throw new Error(`Unknown event type: ${options.type}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (options.status && !STATUSES.has(options.status)) {
|
|
103
|
+
throw new Error(`Unknown status: ${options.status}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (options.ts && Number.isNaN(Date.parse(options.ts))) {
|
|
107
|
+
throw new Error(`Invalid timestamp: ${options.ts}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const value of [...options.files, ...options.commands]) {
|
|
111
|
+
if (!value) throw new Error("Repeated --file and --command values must not be empty.");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return options;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function validateExistingJsonl(eventsPath) {
|
|
118
|
+
if (!fs.existsSync(eventsPath)) return 0;
|
|
119
|
+
const lines = fs.readFileSync(eventsPath, "utf8").split(/\r?\n/);
|
|
120
|
+
let records = 0;
|
|
121
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
122
|
+
const line = lines[i];
|
|
123
|
+
if (!line.trim()) continue;
|
|
124
|
+
records += 1;
|
|
125
|
+
try {
|
|
126
|
+
const parsed = JSON.parse(line);
|
|
127
|
+
validateEventRecord(parsed, eventsPath, i + 1);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw new Error(`Refusing to append to invalid JSONL at ${eventsPath}:${i + 1}: ${error.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return records;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validateEventRecord(parsed, eventsPath, lineNumber) {
|
|
136
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
137
|
+
throw new Error("event must be a JSON object");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (typeof parsed.ts !== "string" || Number.isNaN(Date.parse(parsed.ts))) {
|
|
141
|
+
throw new Error("event ts must be a valid ISO timestamp string");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (typeof parsed.type !== "string" || !EVENT_TYPES.has(parsed.type)) {
|
|
145
|
+
throw new Error(`event type is missing or unknown: ${parsed.type}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (typeof parsed.text !== "string" || parsed.text.trim().length === 0) {
|
|
149
|
+
throw new Error("event text must be a non-empty string");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (parsed.status !== undefined && (typeof parsed.status !== "string" || !STATUSES.has(parsed.status))) {
|
|
153
|
+
throw new Error(`event status is unknown: ${parsed.status}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (parsed.files !== undefined && (!Array.isArray(parsed.files) || parsed.files.some((item) => typeof item !== "string" || !item))) {
|
|
157
|
+
throw new Error("event files must be an array of non-empty strings");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (
|
|
161
|
+
parsed.commands !== undefined &&
|
|
162
|
+
(!Array.isArray(parsed.commands) || parsed.commands.some((item) => typeof item !== "string" || !item))
|
|
163
|
+
) {
|
|
164
|
+
throw new Error("event commands must be an array of non-empty strings");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { eventsPath, lineNumber };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function buildEvent(options) {
|
|
171
|
+
const event = {
|
|
172
|
+
ts: options.ts || new Date().toISOString(),
|
|
173
|
+
type: options.type,
|
|
174
|
+
text: options.text,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (options.goal) event.goal = options.goal;
|
|
178
|
+
if (options.reason) event.reason = options.reason;
|
|
179
|
+
if (options.evidence) event.evidence = options.evidence;
|
|
180
|
+
if (options.files.length > 0) event.files = options.files;
|
|
181
|
+
if (options.commands.length > 0) event.commands = options.commands;
|
|
182
|
+
if (options.status) event.status = options.status;
|
|
183
|
+
|
|
184
|
+
return event;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isValidSessionId(sessionId) {
|
|
188
|
+
return typeof sessionId === "string" && sessionId.trim().length > 0 && !sessionId.includes("/") && !sessionId.includes("..");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function resolveSessionId(workspace, explicitSessionId) {
|
|
192
|
+
if (explicitSessionId) {
|
|
193
|
+
return { sessionId: explicitSessionId, activeSessionPath: null };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const activeSessionPath = path.join(workspace, ".goalkeeper", "active-session");
|
|
197
|
+
if (!fs.existsSync(activeSessionPath)) {
|
|
198
|
+
throw new Error(`Missing --session and active session pointer: ${activeSessionPath}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const sessionId = fs.readFileSync(activeSessionPath, "utf8").trim();
|
|
202
|
+
if (!isValidSessionId(sessionId)) {
|
|
203
|
+
throw new Error(`Invalid active session id in ${activeSessionPath}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { sessionId, activeSessionPath };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function main() {
|
|
210
|
+
let options;
|
|
211
|
+
try {
|
|
212
|
+
options = parseArgs(process.argv.slice(2));
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error(error.message);
|
|
215
|
+
console.error(USAGE);
|
|
216
|
+
process.exit(2);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const workspace = path.resolve(options.workspace);
|
|
220
|
+
let resolvedSession;
|
|
221
|
+
try {
|
|
222
|
+
resolvedSession = resolveSessionId(workspace, options.sessionId);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error(error.message);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const sessionDir = path.join(workspace, ".goalkeeper", "sessions", resolvedSession.sessionId);
|
|
229
|
+
const eventsPath = path.join(sessionDir, "events.jsonl");
|
|
230
|
+
|
|
231
|
+
if (!fs.existsSync(sessionDir) || !fs.statSync(sessionDir).isDirectory()) {
|
|
232
|
+
console.error(`Goalkeeper session directory is missing: ${sessionDir}`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const existingRecords = validateExistingJsonl(eventsPath);
|
|
237
|
+
|
|
238
|
+
const event = buildEvent(options);
|
|
239
|
+
fs.appendFileSync(eventsPath, `${JSON.stringify(event)}\n`);
|
|
240
|
+
|
|
241
|
+
const result = {
|
|
242
|
+
ok: true,
|
|
243
|
+
workspace,
|
|
244
|
+
sessionId: resolvedSession.sessionId,
|
|
245
|
+
activeSessionPath: resolvedSession.activeSessionPath,
|
|
246
|
+
eventsPath,
|
|
247
|
+
lineNumber: existingRecords + 1,
|
|
248
|
+
event,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
if (options.json) {
|
|
252
|
+
console.log(JSON.stringify(result, null, 2));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
console.log("Goalkeeper append-event: PASS");
|
|
257
|
+
console.log(`Events: ${eventsPath}`);
|
|
258
|
+
console.log(`Line: ${result.lineNumber}`);
|
|
259
|
+
console.log(`Type: ${event.type}`);
|
|
260
|
+
console.log(`Text: ${event.text}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
main();
|