@deltafleet/goalkeeper 0.2.1 → 0.3.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 CHANGED
@@ -4,6 +4,12 @@ All notable changes to Goalkeeper are documented here.
4
4
 
5
5
  This project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.3.0] - 2026-05-18
8
+
9
+ - Added `goalkeeper-close.mjs` so agents can shut down Goalkeeper sessions when a goal completes.
10
+ - Added the `close` event type and `closed` event status.
11
+ - Added shutdown instructions so Goalkeeper stops applying checkpoint-first recovery to unrelated questions after completion.
12
+
7
13
  ## [0.2.1] - 2026-05-18
8
14
 
9
15
  - Simplified public invocation copy to `Use goalkeeper for this goal.`
package/CONTRIBUTING.md CHANGED
@@ -18,7 +18,7 @@ The project has one strong bias: keep the core small enough that agents will act
18
18
  - Background processes.
19
19
  - Runtime hooks into private host-agent internals.
20
20
  - Global databases or cross-project indexing.
21
- - Large abstractions around the five core helper scripts.
21
+ - Large abstractions around the six core helper scripts.
22
22
 
23
23
  These may be useful later, but they should not enter the project without a clear real-world failure case.
24
24
 
package/README.ja.md CHANGED
@@ -119,10 +119,14 @@ Goalkeeper は長いエージェント作業を単純なループにします。
119
119
  -> resume、handoff、compaction が疑われる後、agent は checkpoint.md を最初に読む
120
120
  -> checkpoint が薄ければ context-pack.md を読む
121
121
  -> 正確な証拠が必要なら events.jsonl または source file を確認する
122
+ -> goal が完了したら、agent が Goalkeeper セッションを閉じる
123
+ -> その後の無関係な質問には Goalkeeper recovery を適用しない
122
124
  ```
123
125
 
124
126
  これは会話 transcript の保存ではありません。作業状態の保存です。
125
127
 
128
+ Goalkeeper は永遠に張り付くべきではありません。管理していた goal が完了したら、agent は最終結果を記録し、checkpoint を closed 状態にし、active session pointer を削除してから完了報告をします。
129
+
126
130
  ## あえて小さくしています
127
131
 
128
132
  このプロジェクトを大きくするのは簡単です。
package/README.ko.md CHANGED
@@ -119,10 +119,14 @@ Goalkeeper는 긴 에이전트 작업을 단순한 루프로 바꿉니다.
119
119
  -> resume, handoff, compact 의심 이후 agent는 checkpoint.md를 먼저 읽는다
120
120
  -> checkpoint가 얇으면 context-pack.md를 읽는다
121
121
  -> 정확한 증거가 필요하면 events.jsonl이나 source file을 확인한다
122
+ -> goal이 끝나면 agent가 Goalkeeper 세션을 닫는다
123
+ -> 이후 관련 없는 일반 질문에는 Goalkeeper recovery가 붙지 않는다
122
124
  ```
123
125
 
124
126
  이것은 대화 transcript 저장이 아닙니다. 작업 상태 보존입니다.
125
127
 
128
+ Goalkeeper가 영원히 붙어 있으면 안 됩니다. 관리하던 goal이 끝나면 agent가 최종 결과를 기록하고, checkpoint를 closed 상태로 표시하고, active session pointer를 제거한 뒤 완료 보고를 합니다.
129
+
126
130
  ## 일부러 작게 만들었습니다
127
131
 
128
132
  이 프로젝트를 크게 만드는 방법은 쉽습니다.
package/README.md CHANGED
@@ -119,10 +119,14 @@ Long /goal begins
119
119
  -> after resume, handoff, or suspected compaction, the agent reads checkpoint.md first
120
120
  -> if the checkpoint is too thin, the agent reads context-pack.md
121
121
  -> if exact proof is needed, the agent checks events.jsonl or source files
122
+ -> when the goal is done, the agent closes the Goalkeeper session
123
+ -> later unrelated questions do not trigger Goalkeeper recovery
122
124
  ```
123
125
 
124
126
  This is not transcript storage. It is working-state preservation.
125
127
 
128
+ Goalkeeper should not stay attached forever. A managed goal has a shutdown step: the agent records the final outcome, marks the checkpoint closed, and removes the active session pointer before giving the completion response.
129
+
126
130
  ## Why It Is Small On Purpose
127
131
 
128
132
  The obvious version of this project is too big:
package/README.zh-CN.md CHANGED
@@ -119,10 +119,14 @@ Goalkeeper 把长时间 agent 工作变成一个简单循环:
119
119
  -> resume、handoff 或怀疑 compaction 后,agent 先读 checkpoint.md
120
120
  -> 如果 checkpoint 太薄,agent 再读 context-pack.md
121
121
  -> 如果需要精确证据,agent 检查 events.jsonl 或 source files
122
+ -> goal 完成后,agent 关闭 Goalkeeper session
123
+ -> 之后无关的一般问题不会触发 Goalkeeper recovery
122
124
  ```
123
125
 
124
126
  这不是保存对话 transcript。它保存的是工作状态。
125
127
 
128
+ Goalkeeper 不应该一直附着在会话上。被管理的 goal 完成后,agent 会记录最终结果,把 checkpoint 标记为 closed,移除 active session pointer,然后再汇报完成。
129
+
126
130
  ## 为什么故意做得很小
127
131
 
128
132
  把这个项目做大很容易:
package/docs/ROADMAP.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  Long-running agent goals need a small continuity layer outside the model context.
6
6
 
7
- Goalkeeper should stay boring: a short checkpoint, a medium-density context pack, an append-only event log, a turn-start helper, and a doctor check. It should not become a substitute context engine or a promise of perfect post-compact recovery.
7
+ Goalkeeper should stay boring: a short checkpoint, a medium-density context pack, an append-only event log, a turn-start helper, a close helper, and a doctor check. It should not become a substitute context engine or a promise of perfect post-compact recovery.
8
8
 
9
9
  ## MVP
10
10
 
@@ -27,6 +27,7 @@ Core behavior:
27
27
  - read the context pack when the checkpoint is too thin to recover pre-compaction reasoning
28
28
  - append meaningful decisions, failures, verification, and handoff events
29
29
  - refresh the checkpoint when recoverable working state changes
30
+ - close the active session when the managed goal completes so unrelated questions do not trigger recovery
30
31
  - run a read-only doctor before trusting a workspace for long work
31
32
 
32
33
  ## User-Facing Scope
@@ -37,6 +38,7 @@ Keep these scripts central:
37
38
  - `goalkeeper-turn-start.mjs`
38
39
  - `goalkeeper-append-event.mjs`
39
40
  - `goalkeeper-update-checkpoint.mjs`
41
+ - `goalkeeper-close.mjs`
40
42
  - `goalkeeper-doctor.mjs`
41
43
 
42
44
  Keep this optional and maintainer-oriented:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deltafleet/goalkeeper",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "A small Agent Skill for keeping long-running goals oriented across compaction, resumes, and handoffs.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,11 +29,13 @@
29
29
  "goalkeeper-append-event": "src/goalkeeper/scripts/goalkeeper-append-event.mjs",
30
30
  "goalkeeper-update-checkpoint": "src/goalkeeper/scripts/goalkeeper-update-checkpoint.mjs",
31
31
  "goalkeeper-doctor": "src/goalkeeper/scripts/goalkeeper-doctor.mjs",
32
+ "goalkeeper-close": "src/goalkeeper/scripts/goalkeeper-close.mjs",
32
33
  "codex-goalkeeper-init": "src/goalkeeper/scripts/goalkeeper-init.mjs",
33
34
  "codex-goalkeeper-turn-start": "src/goalkeeper/scripts/goalkeeper-turn-start.mjs",
34
35
  "codex-goalkeeper-append-event": "src/goalkeeper/scripts/goalkeeper-append-event.mjs",
35
36
  "codex-goalkeeper-update-checkpoint": "src/goalkeeper/scripts/goalkeeper-update-checkpoint.mjs",
36
- "codex-goalkeeper-doctor": "src/goalkeeper/scripts/goalkeeper-doctor.mjs"
37
+ "codex-goalkeeper-doctor": "src/goalkeeper/scripts/goalkeeper-doctor.mjs",
38
+ "codex-goalkeeper-close": "src/goalkeeper/scripts/goalkeeper-close.mjs"
37
39
  },
38
40
  "files": [
39
41
  "src/goalkeeper",
@@ -17,6 +17,8 @@ When a user starts or continues a `/goal` in Claude Code, Codex, or another skil
17
17
 
18
18
  When a Goalkeeper-managed goal is already active, the first project-state action in a new assistant turn should be reading the active checkpoint, unless you have already read it in the same turn.
19
19
 
20
+ Only treat Goalkeeper as active when `<workspace>/.goalkeeper/active-session` points to a live session or the user explicitly asks to resume a specific Goalkeeper session. If no active session pointer exists and the user asks an unrelated question, do not apply checkpoint-first recovery.
21
+
20
22
  This is stricter than waiting until you notice compaction. A compacted turn may not reliably expose the compaction marker to the model, so checkpoint-first is the practical recovery rule for long-running goals.
21
23
 
22
24
  Allowed before the checkpoint read:
@@ -113,6 +115,31 @@ Update Goalkeeper state when any of these change:
113
115
  Append the event first, then update the session's `checkpoint.md` when the event changes the current working state.
114
116
  Use `scripts/goalkeeper-update-checkpoint.mjs` when you want a bounded canonical checkpoint instead of manual Markdown edits.
115
117
 
118
+ ## Shutdown Rule
119
+
120
+ When a Goalkeeper-managed goal is complete, shut down the active Goalkeeper session before sending the final completion response.
121
+
122
+ Do this when:
123
+
124
+ - the done criteria are satisfied
125
+ - the user explicitly ends the goal
126
+ - the work is abandoned, superseded, or intentionally paused without needing checkpoint-first recovery on unrelated questions
127
+
128
+ Shutdown steps:
129
+
130
+ 1. Append a final `close` event.
131
+ 2. Mark `checkpoint.md` as closed with the final outcome and residual risks.
132
+ 3. Remove `.goalkeeper/active-session` when it points to the closed session.
133
+ 4. Do not apply checkpoint-first recovery to later unrelated user questions.
134
+
135
+ Use the close helper:
136
+
137
+ ```bash
138
+ node <skill-path>/scripts/goalkeeper-close.mjs --workspace <workspace> --outcome "<final outcome>"
139
+ ```
140
+
141
+ After shutdown, read the closed session again only if the user explicitly resumes that goal or asks about its history.
142
+
116
143
  ## Keep It Short
117
144
 
118
145
  The checkpoint is a recovery artifact, not a transcript.
@@ -157,6 +184,7 @@ Read it when checkpoint recovery is not enough. Keep raw transcripts and long co
157
184
  - Run `scripts/goalkeeper-turn-start.mjs --context` when checkpoint recovery needs the larger context pack too.
158
185
  - Run `scripts/goalkeeper-append-event.mjs` instead of hand-writing JSONL when recording decisions, verification, failures, risks, or handoffs; it can use `.goalkeeper/active-session` when `--session` is omitted.
159
186
  - Run `scripts/goalkeeper-update-checkpoint.mjs` after appending a meaningful event when checkpoint state should be refreshed in a short canonical shape.
187
+ - Run `scripts/goalkeeper-close.mjs` before the final completion response when the managed goal is done, abandoned, or superseded.
160
188
  - Run `scripts/goalkeeper-doctor.mjs` after creating or changing Goalkeeper state to verify the target workspace is ready.
161
189
 
162
190
  ## Safety Boundary
@@ -33,7 +33,7 @@ When `<workspace>/.goalkeeper/active-session` points to the current session, `--
33
33
  - `evidence`: short supporting detail.
34
34
  - `files`: array of file paths.
35
35
  - `commands`: array of commands.
36
- - `status`: `open`, `done`, `failed`, `blocked`, or `superseded`.
36
+ - `status`: `open`, `done`, `failed`, `blocked`, `superseded`, or `closed`.
37
37
  - `supersedes`: event id or short reference.
38
38
 
39
39
  ## Initial Types
@@ -51,6 +51,7 @@ When `<workspace>/.goalkeeper/active-session` points to the current session, `--
51
51
  - `next_action`: explicit next step.
52
52
  - `compact_observed`: a real agent compaction boundary was observed.
53
53
  - `recovery_violation`: the agent continued after compaction or resume before reading the Goalkeeper checkpoint.
54
+ - `close`: the managed goal was completed, abandoned, or superseded and the active session should stop applying to unrelated questions.
54
55
 
55
56
  ## Writing Rules
56
57
 
@@ -30,6 +30,9 @@ For an active Goalkeeper-managed goal:
30
30
  2. Use `context-pack.md` when the checkpoint is too thin to recover the reasoning chain.
31
31
  3. Use `events.jsonl` only when exact evidence is needed.
32
32
  4. Append `recovery_violation` if the agent continued after compaction or resume before reading the checkpoint.
33
+ 5. When the goal is complete, run `goalkeeper-close.mjs` before the final completion response.
34
+
35
+ If `.goalkeeper/active-session` is absent and the user asks an unrelated question, do not read closed Goalkeeper sessions first.
33
36
 
34
37
  If `scripts/goalkeeper-turn-start.mjs` is present, it can be used as the first recovery action:
35
38
 
@@ -61,4 +64,10 @@ Before starting a high-stakes long run, use the read-only doctor to verify the t
61
64
  node <skill-path>/scripts/goalkeeper-doctor.mjs --workspace <workspace> --session <goal-session-id> --strict
62
65
  ```
63
66
 
67
+ When the managed goal is done, close the session:
68
+
69
+ ```bash
70
+ node <skill-path>/scripts/goalkeeper-close.mjs --workspace <workspace> --outcome "<final outcome>"
71
+ ```
72
+
64
73
  Parallel calls are still subject to checkpoint-first ordering. It is acceptable to batch `pwd`, `.goalkeeper/sessions` discovery, and `goalkeeper-turn-start.mjs`; it is not acceptable to include normal project files or verification in that same first post-compact parallel call.
@@ -73,6 +73,8 @@ Use `context-pack.md` for medium-density reasoning that should survive compactio
73
73
 
74
74
  For an already active Goalkeeper-managed task, begin each new assistant turn with a checkpoint-first recovery read before touching normal project files.
75
75
 
76
+ Only apply this rule when `.goalkeeper/active-session` exists or the user explicitly resumes a known Goalkeeper session. If the active pointer is absent and the user asks an unrelated question, do not read closed sessions first.
77
+
76
78
  Recommended sequence:
77
79
 
78
80
  ```bash
@@ -166,6 +168,23 @@ Before ending a long working segment:
166
168
  2. Update `checkpoint.md` with the current state and exact next action.
167
169
  3. Include unresolved risks and verification gaps.
168
170
 
171
+ ## Shutdown
172
+
173
+ Before sending the final completion response for a Goalkeeper-managed goal:
174
+
175
+ 1. Confirm the done criteria are satisfied, or that the goal was explicitly abandoned or superseded.
176
+ 2. Run the close helper:
177
+
178
+ ```bash
179
+ node <skill-path>/scripts/goalkeeper-close.mjs --workspace <workspace> --outcome "<final outcome>"
180
+ ```
181
+
182
+ 3. Include repeated `--risk "<text>"` and `--evidence "<text>"` fields when residual risks or final proof should remain recoverable.
183
+ 4. Verify `.goalkeeper/active-session` was removed when it pointed to the closed session.
184
+ 5. Send the final completion response.
185
+
186
+ After shutdown, do not apply checkpoint-first recovery to unrelated user questions. Read the closed session only if the user explicitly resumes that goal or asks about its history.
187
+
169
188
  ## Checkpoint Update Guidance
170
189
 
171
190
  Update the checkpoint after a meaningful state transition, not after every minor tool call.
@@ -17,9 +17,10 @@ const EVENT_TYPES = new Set([
17
17
  "next_action",
18
18
  "compact_observed",
19
19
  "recovery_violation",
20
+ "close",
20
21
  ]);
21
22
 
22
- const STATUSES = new Set(["open", "done", "failed", "blocked", "superseded"]);
23
+ const STATUSES = new Set(["open", "done", "failed", "blocked", "superseded", "closed"]);
23
24
 
24
25
  const USAGE = `Usage:
25
26
  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]
@@ -0,0 +1,285 @@
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
+ "close",
21
+ ]);
22
+
23
+ const STATUSES = new Set(["open", "done", "failed", "blocked", "superseded", "closed"]);
24
+
25
+ const USAGE = `Usage:
26
+ node scripts/goalkeeper-close.mjs --outcome <text> [--session <goal-session-id>] [--workspace <path>] [--risk <text> ...] [--evidence <text> ...] [--json]
27
+
28
+ Closes the active Goalkeeper session after a goal is complete, abandoned, or superseded.
29
+ This appends a close event, marks checkpoint.md closed, and removes <workspace>/.goalkeeper/active-session when it points to the closed session.
30
+ `;
31
+
32
+ function parseArgs(argv) {
33
+ const options = {
34
+ sessionId: null,
35
+ workspace: ".",
36
+ outcome: null,
37
+ risks: [],
38
+ evidence: [],
39
+ json: false,
40
+ };
41
+
42
+ for (let i = 0; i < argv.length; i += 1) {
43
+ const arg = argv[i];
44
+ if (arg === "--session") {
45
+ options.sessionId = argv[i + 1];
46
+ i += 1;
47
+ } else if (arg === "--workspace") {
48
+ options.workspace = argv[i + 1];
49
+ i += 1;
50
+ } else if (arg === "--outcome") {
51
+ options.outcome = argv[i + 1];
52
+ i += 1;
53
+ } else if (arg === "--risk") {
54
+ options.risks.push(argv[i + 1]);
55
+ i += 1;
56
+ } else if (arg === "--evidence") {
57
+ options.evidence.push(argv[i + 1]);
58
+ i += 1;
59
+ } else if (arg === "--json") {
60
+ options.json = true;
61
+ } else {
62
+ throw new Error(`Unknown argument: ${arg}`);
63
+ }
64
+ }
65
+
66
+ if (!options.workspace || !options.outcome) {
67
+ throw new Error("Missing required argument.");
68
+ }
69
+
70
+ options.outcome = normalizeText(options.outcome, "outcome");
71
+ options.risks = options.risks.map((item) => normalizeText(item, "risk"));
72
+ options.evidence = options.evidence.map((item) => normalizeText(item, "evidence"));
73
+
74
+ if (options.sessionId && !isValidSessionId(options.sessionId)) {
75
+ throw new Error("Session id must be a single path segment.");
76
+ }
77
+
78
+ return options;
79
+ }
80
+
81
+ function normalizeText(value, field) {
82
+ if (typeof value !== "string") {
83
+ throw new Error(`${field} must be a string.`);
84
+ }
85
+ const normalized = value.replace(/\s+/g, " ").trim();
86
+ if (!normalized) {
87
+ throw new Error(`${field} must not be empty.`);
88
+ }
89
+ return normalized;
90
+ }
91
+
92
+ function isValidSessionId(sessionId) {
93
+ return typeof sessionId === "string" && sessionId.trim().length > 0 && !sessionId.includes("/") && !sessionId.includes("..");
94
+ }
95
+
96
+ function resolveSessionId(workspace, explicitSessionId) {
97
+ const activeSessionPath = path.join(workspace, ".goalkeeper", "active-session");
98
+ if (explicitSessionId) {
99
+ return { sessionId: explicitSessionId, activeSessionPath, resolvedFromActive: false };
100
+ }
101
+
102
+ if (!fs.existsSync(activeSessionPath)) {
103
+ throw new Error(`Missing --session and active session pointer: ${activeSessionPath}`);
104
+ }
105
+
106
+ const sessionId = fs.readFileSync(activeSessionPath, "utf8").trim();
107
+ if (!isValidSessionId(sessionId)) {
108
+ throw new Error(`Invalid active session id in ${activeSessionPath}`);
109
+ }
110
+
111
+ return { sessionId, activeSessionPath, resolvedFromActive: true };
112
+ }
113
+
114
+ function validateExistingJsonl(eventsPath) {
115
+ if (!fs.existsSync(eventsPath)) return 0;
116
+ const lines = fs.readFileSync(eventsPath, "utf8").split(/\r?\n/);
117
+ let records = 0;
118
+ for (let i = 0; i < lines.length; i += 1) {
119
+ const line = lines[i];
120
+ if (!line.trim()) continue;
121
+ records += 1;
122
+ let parsed;
123
+ try {
124
+ parsed = JSON.parse(line);
125
+ } catch (error) {
126
+ throw new Error(`Refusing to append to invalid JSONL at ${eventsPath}:${i + 1}: ${error.message}`);
127
+ }
128
+ validateEventRecord(parsed, eventsPath, i + 1);
129
+ }
130
+ return records;
131
+ }
132
+
133
+ function validateEventRecord(parsed, eventsPath, lineNumber) {
134
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
135
+ throw new Error(`Invalid event at ${eventsPath}:${lineNumber}: event must be a JSON object`);
136
+ }
137
+ if (typeof parsed.ts !== "string" || Number.isNaN(Date.parse(parsed.ts))) {
138
+ throw new Error(`Invalid event at ${eventsPath}:${lineNumber}: event ts must be a valid ISO timestamp string`);
139
+ }
140
+ if (typeof parsed.type !== "string" || !EVENT_TYPES.has(parsed.type)) {
141
+ throw new Error(`Invalid event at ${eventsPath}:${lineNumber}: event type is missing or unknown: ${parsed.type}`);
142
+ }
143
+ if (typeof parsed.text !== "string" || parsed.text.trim().length === 0) {
144
+ throw new Error(`Invalid event at ${eventsPath}:${lineNumber}: event text must be a non-empty string`);
145
+ }
146
+ if (parsed.status !== undefined && (typeof parsed.status !== "string" || !STATUSES.has(parsed.status))) {
147
+ throw new Error(`Invalid event at ${eventsPath}:${lineNumber}: event status is unknown: ${parsed.status}`);
148
+ }
149
+ }
150
+
151
+ function bulletList(items, fallback = "None recorded.") {
152
+ const values = items.length > 0 ? items : [fallback];
153
+ return values.map((item) => `- ${item}`).join("\n");
154
+ }
155
+
156
+ function renderClosedCheckpoint(existingCheckpoint, options, closedAt) {
157
+ const withoutOldClosedSection = existingCheckpoint.replace(/\n## Closed\n[\s\S]*?(?=\n## |\s*$)/g, "").trimEnd();
158
+ const withClosedStatus = withoutOldClosedSection.includes("- Current status:")
159
+ ? withoutOldClosedSection.replace(/^- Current status: .*$/m, "- Current status: Closed.")
160
+ : `${withoutOldClosedSection}\n\n## Status\n\n- Current status: Closed.`;
161
+
162
+ return `${withClosedStatus}
163
+
164
+ ## Closed
165
+
166
+ - Outcome: ${options.outcome}
167
+ - Closed at: ${closedAt}
168
+
169
+ ## Residual Risks
170
+
171
+ ${bulletList(options.risks)}
172
+
173
+ ## Final Evidence
174
+
175
+ ${bulletList(options.evidence)}
176
+ `;
177
+ }
178
+
179
+ function maybeRemoveActiveSession(activeSessionPath, sessionId) {
180
+ if (!fs.existsSync(activeSessionPath)) {
181
+ return { removed: false, reason: "active-session was already missing." };
182
+ }
183
+
184
+ const activeSessionId = fs.readFileSync(activeSessionPath, "utf8").trim();
185
+ if (activeSessionId !== sessionId) {
186
+ return {
187
+ removed: false,
188
+ reason: `active-session points to ${activeSessionId || "(empty)"}, not ${sessionId}.`,
189
+ };
190
+ }
191
+
192
+ fs.rmSync(activeSessionPath);
193
+ return { removed: true, reason: "active-session pointed to the closed session." };
194
+ }
195
+
196
+ function main() {
197
+ let options;
198
+ try {
199
+ options = parseArgs(process.argv.slice(2));
200
+ } catch (error) {
201
+ console.error(error.message);
202
+ console.error(USAGE);
203
+ process.exit(2);
204
+ }
205
+
206
+ const workspace = path.resolve(options.workspace);
207
+ if (!fs.existsSync(workspace) || !fs.statSync(workspace).isDirectory()) {
208
+ console.error(`Workspace does not exist or is not a directory: ${workspace}`);
209
+ process.exit(1);
210
+ }
211
+
212
+ let resolvedSession;
213
+ try {
214
+ resolvedSession = resolveSessionId(workspace, options.sessionId);
215
+ } catch (error) {
216
+ console.error(error.message);
217
+ process.exit(1);
218
+ }
219
+
220
+ const sessionDir = path.join(workspace, ".goalkeeper", "sessions", resolvedSession.sessionId);
221
+ const checkpointPath = path.join(sessionDir, "checkpoint.md");
222
+ const eventsPath = path.join(sessionDir, "events.jsonl");
223
+
224
+ if (!fs.existsSync(sessionDir) || !fs.statSync(sessionDir).isDirectory()) {
225
+ console.error(`Goalkeeper session directory is missing: ${sessionDir}`);
226
+ process.exit(1);
227
+ }
228
+
229
+ if (!fs.existsSync(checkpointPath)) {
230
+ console.error(`Checkpoint is missing: ${checkpointPath}`);
231
+ process.exit(1);
232
+ }
233
+
234
+ let existingRecords;
235
+ try {
236
+ existingRecords = validateExistingJsonl(eventsPath);
237
+ } catch (error) {
238
+ console.error(error.message);
239
+ process.exit(1);
240
+ }
241
+
242
+ const closedAt = new Date().toISOString();
243
+ const event = {
244
+ ts: closedAt,
245
+ type: "close",
246
+ text: options.outcome,
247
+ status: "closed",
248
+ };
249
+ if (options.risks.length > 0) event.reason = `Residual risks: ${options.risks.join("; ")}`;
250
+ if (options.evidence.length > 0) event.evidence = options.evidence.join("; ");
251
+
252
+ fs.appendFileSync(eventsPath, `${JSON.stringify(event)}\n`);
253
+
254
+ const existingCheckpoint = fs.readFileSync(checkpointPath, "utf8");
255
+ const checkpoint = renderClosedCheckpoint(existingCheckpoint, options, closedAt);
256
+ fs.writeFileSync(checkpointPath, checkpoint);
257
+
258
+ const activeSession = maybeRemoveActiveSession(resolvedSession.activeSessionPath, resolvedSession.sessionId);
259
+
260
+ const result = {
261
+ ok: true,
262
+ workspace,
263
+ sessionId: resolvedSession.sessionId,
264
+ sessionDir,
265
+ checkpointPath,
266
+ eventsPath,
267
+ lineNumber: existingRecords + 1,
268
+ activeSessionPath: resolvedSession.activeSessionPath,
269
+ activeSession,
270
+ event,
271
+ };
272
+
273
+ if (options.json) {
274
+ console.log(JSON.stringify(result, null, 2));
275
+ return;
276
+ }
277
+
278
+ console.log("Goalkeeper close: PASS");
279
+ console.log(`Session: ${resolvedSession.sessionId}`);
280
+ console.log(`Checkpoint: ${checkpointPath}`);
281
+ console.log(`Events: ${eventsPath}`);
282
+ console.log(`Active session removed: ${activeSession.removed ? "yes" : "no"}`);
283
+ }
284
+
285
+ main();
@@ -19,9 +19,10 @@ const EVENT_TYPES = new Set([
19
19
  "next_action",
20
20
  "compact_observed",
21
21
  "recovery_violation",
22
+ "close",
22
23
  ]);
23
24
 
24
- const STATUSES = new Set(["open", "done", "failed", "blocked", "superseded"]);
25
+ const STATUSES = new Set(["open", "done", "failed", "blocked", "superseded", "closed"]);
25
26
  const CHECKPOINT_TARGET_BYTES = 8_000;
26
27
  const CHECKPOINT_MAX_BYTES = 16_000;
27
28
  const CONTEXT_PACK_TARGET_BYTES = 30_000;
@@ -1,6 +1,8 @@
1
1
  # Goalkeeper Guardrail
2
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.
3
+ When this repository has an active `.goalkeeper/active-session` pointer, treat the referenced `.goalkeeper/sessions/<goal-session-id>/` directory as the continuity source for long-running agent work.
4
+
5
+ If `.goalkeeper/active-session` is absent and the user asks an unrelated question, do not read closed Goalkeeper sessions first.
4
6
 
5
7
  At the start of each new assistant turn, before reading normal project files or making edits:
6
8
 
@@ -45,4 +47,6 @@ Do not read project docs, source files, examples, tests, or make edits before th
45
47
 
46
48
  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
49
 
50
+ When the managed goal is complete, run `goalkeeper-close.mjs` before sending the final completion response. This records the final outcome and removes `.goalkeeper/active-session` so later unrelated questions are not treated as goal recovery.
51
+
48
52
  Do not claim Goalkeeper reduces compaction frequency. Its purpose is direction recovery after compaction, resume, or handoff.
@@ -1,6 +1,8 @@
1
1
  # Goalkeeper Guardrail
2
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.
3
+ When this repository has an active `.goalkeeper/active-session` pointer, treat the referenced `.goalkeeper/sessions/<goal-session-id>/` directory as the continuity source for long-running Claude Code or agent work.
4
+
5
+ If `.goalkeeper/active-session` is absent and the user asks an unrelated question, do not read closed Goalkeeper sessions first.
4
6
 
5
7
  At the start of each new assistant turn, before reading normal project files or making edits:
6
8
 
@@ -45,4 +47,6 @@ Do not read project docs, source files, examples, tests, or make edits before th
45
47
 
46
48
  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
49
 
50
+ When the managed goal is complete, run `goalkeeper-close.mjs` before sending the final completion response. This records the final outcome and removes `.goalkeeper/active-session` so later unrelated questions are not treated as goal recovery.
51
+
48
52
  Do not claim Goalkeeper reduces compaction frequency. Its purpose is direction recovery after compaction, resume, or handoff.