@deltafleet/goalkeeper 0.2.0 → 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,18 @@ 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
+
13
+ ## [0.2.1] - 2026-05-18
14
+
15
+ - Simplified public invocation copy to `Use goalkeeper for this goal.`
16
+ - Rewrote the README problem example around a clearer long-session payment bug scenario.
17
+ - Shortened the UI default prompt so users are not asked to spell out Goalkeeper internals.
18
+
7
19
  ## [0.2.0] - 2026-05-18
8
20
 
9
21
  - Renamed the public project to `goalkeeper`.
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
@@ -39,15 +39,15 @@ Skill-compatible agent は、リクエストが metadata と強く一致する i
39
39
 
40
40
  そのため、次のような goal だけで有効になることがあります。
41
41
 
42
- > `/goal` Harden this release over a long-running session. Keep the goal, constraints, rejected paths, failed attempts, verification state, and next action recoverable after compact/resume.
42
+ > `/goal` Harden this release over a long-running session. Use goalkeeper.
43
43
 
44
44
  ただし skill activation は routing decision であり、private agent runtime hook ではありません。Goalkeeper がすべての goal に強制的に自分を適用することはできません。
45
45
 
46
46
  重要な長期作業では、goal を作る時点、または goal 作成直後で本格作業に入る前に、明示的に呼び出すのがもっとも安全です。
47
47
 
48
- > Use goalkeeper for this `/goal`. Keep the goal, constraints, decisions, verification state, failed attempts, and next action recoverable across compaction.
48
+ > Use goalkeeper for this goal.
49
49
 
50
- その後、ユーザーが Goalkeeper helper script を手で実行する必要はありません。agent skill workflow の一部として実行します。
50
+ ユーザーが覚える必要がある文はここまでです。checkpoint、context pack、event log、失敗した試行、検証状態、helper script は、Goalkeeper workflow の中で agent が扱います。
51
51
 
52
52
  ## 問題
53
53
 
@@ -57,15 +57,15 @@ Skill-compatible agent は、リクエストが metadata と強く一致する i
57
57
 
58
58
  実際のセッションを想像してください。
59
59
 
60
- 1. agent にリリース hardening を任せます。
61
- 2. いちばん分かりやすい patch は目の前の bug を直しますが、rollback compatibility を壊す可能性があります。
62
- 3. ユーザーは強い制約を置きます。database schema は変更せず、backward compatibility を維持する。
63
- 4. 2 回目の試行は unit test を通りますが、integration edge case で失敗します。
64
- 5. agent compatibility shim targeted regression test の組み合わせに決めます。
65
- 6. regression test が通ります。このルートが安全なルートになります。
60
+ 1. agent に決済バグの修正を任せます。
61
+ 2. 早い段階で、`refunds` 周りのコードは legacy なので触るべきではないと分かります。
62
+ 3. 最速の patch `refunds` を直接編集する方法なので、ユーザーはその方針を拒否します。
63
+ 4. agent webhook handler 側に移そうとしますが、duplicate event のケースで失敗します。
64
+ 5. 最終的に service layer idempotency guard を置き、regression test で検証するルートが通ります。
65
+ 6. テストが通ります。このルートが維持すべき安全なルートになります。
66
66
  7. コンテキストが compact されます。
67
- 8. 後でエージェントは「release hardening はほぼ完了」というきれいな要約で戻ってきます。
68
- 9. goal は残っています。しかし、なぜ schema shortcut を禁止し続ける必要があるのか、なぜ前の patch が失敗したのか、なぜその regression test が重要なのかは薄れているかもしれません。
67
+ 8. 後でエージェントは「決済バグはほぼ修正済み」というきれいな要約で戻ってきます。
68
+ 9. goal は残っています。しかし、`refunds` が触ってはいけない場所だったこと、webhook の試行が失敗したこと、service-layer test が安全なルートを証明したことは薄れているかもしれません。
69
69
 
70
70
  ここから drift が始まります。
71
71
 
@@ -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
@@ -39,15 +39,15 @@ Skill-compatible agent는 설치된 skill 중 요청과 관련성이 높은 skil
39
39
 
40
40
  그래서 아래처럼 goal 자체가 충분히 분명하면 자동으로 붙을 수 있습니다.
41
41
 
42
- > `/goal` 이번 릴리스를 장기 세션으로 안정화해줘. compact/resume 이후에도 목표, 제약, 거부한 경로, 실패한 시도, 검증 상태, 다음 액션이 복구 가능하게 관리해줘.
42
+ > `/goal` 이번 릴리스를 장기 세션으로 안정화해줘. goalkeeper를 사용해줘.
43
43
 
44
44
  하지만 skill 활성화는 agent runtime hook이 아니라 routing 판단입니다. Goalkeeper가 모든 goal에 자신을 강제로 붙일 수는 없습니다.
45
45
 
46
46
  중요한 장기 작업이라면 goal을 만들 때, 또는 goal을 만든 직후 본격 작업 전에 명시적으로 호출하는 편이 가장 안전합니다.
47
47
 
48
- > 이 `/goal`에는 goalkeeper를 사용해줘. 목표, 제약, 결정, 검증 상태, 실패한 시도, 다음 액션이 compact 이후에도 복구 가능하게 관리해줘.
48
+ > 이 goal에는 goalkeeper를 사용해줘.
49
49
 
50
- 다음부터 사용자가 Goalkeeper의 helper script 직접 실행할 필요는 없습니다. agent가 skill workflow의 일부로 실행합니다.
50
+ 사용자가 기억할 문장은 여기까지입니다. checkpoint, context pack, event log, 실패한 시도, 검증 상태, helper script 같은 것은 Goalkeeper workflow 안에서 agent가 관리합니다.
51
51
 
52
52
  ## 문제
53
53
 
@@ -57,15 +57,15 @@ Skill-compatible agent는 설치된 skill 중 요청과 관련성이 높은 skil
57
57
 
58
58
  실제 세션을 상상해보면 이렇습니다.
59
59
 
60
- 1. agent에게 릴리스 hardening을 맡깁니다.
61
- 2. 가장 쉬운 patch는 눈앞의 bug는 고치지만 rollback compatibility를 깨뜨릴 있습니다.
62
- 3. 사용자는 강한 제약을 둡니다. database schema는 바꾸지 말고, backward compatibility를 유지해야 합니다.
63
- 4. 번째 시도는 unit test를 통과하지만 integration edge case에서 실패합니다.
64
- 5. agent는 compatibility shim과 targeted regression test 조합으로 방향을 정합니다.
65
- 6. regression test가 통과합니다. 이제 이 경로가 안전한 경로입니다.
60
+ 1. agent에게 결제 버그 수정을 맡깁니다.
61
+ 2. 초반 조사에서 `refunds` 코드는 레거시라 건드리면 된다는 사실이 드러납니다.
62
+ 3. 가장 빠른 patch는 `refunds`를 직접 고치는 방식이라, 사용자가 경로를 명시적으로 거부합니다.
63
+ 4. agent는 webhook handler 쪽으로 옮겨보지만, duplicate event 케이스에서 실패합니다.
64
+ 5. 결국 service layer에 idempotency guard를 두고 regression test 막는 경로를 검증합니다.
65
+ 6. 테스트가 통과합니다. 이제 이 경로가 유지되어야 하는 안전한 경로입니다.
66
66
  7. 컨텍스트가 compact됩니다.
67
- 8. 나중에 에이전트는 “release hardening은 거의 됨” 같은 깔끔한 요약으로 돌아옵니다.
68
- 9. goal은 기억하지만, schema shortcut이 계속 금지되어야 하는지, 앞선 patch들이 실패했는지, 그 regression test가 중요한지는 희미해질 수 있습니다.
67
+ 8. 나중에 에이전트는 “결제 버그는 거의 해결됨” 같은 깔끔한 요약으로 돌아옵니다.
68
+ 9. goal은 기억하지만, `refunds`를 건드리면 된다는 점, webhook 시도가 실패했다는 점, service-layer test가 안전한 경로를 증명했다는 점은 희미해질 수 있습니다.
69
69
 
70
70
  여기서 drift가 시작됩니다.
71
71
 
@@ -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
@@ -39,15 +39,15 @@ Skill-compatible agents can automatically load installed skills when a request s
39
39
 
40
40
  So this can be enough:
41
41
 
42
- > `/goal` Harden this release over a long-running session. Keep the goal, constraints, rejected paths, failed attempts, verification state, and next action recoverable after compact/resume.
42
+ > `/goal` Harden this release over a long-running session. Use goalkeeper.
43
43
 
44
44
  But skill activation is still a routing decision, not a private runtime hook. Goalkeeper cannot force itself onto every goal.
45
45
 
46
46
  For important long-running work, the safest path is to be explicit when you create the goal, or immediately after creating it:
47
47
 
48
- > Use goalkeeper for this `/goal`. Keep the goal, constraints, decisions, verification state, failed attempts, and next action recoverable across compaction.
48
+ > Use goalkeeper for this goal.
49
49
 
50
- After that, you should not have to run Goalkeeper's helper scripts yourself. The agent runs them as part of the skill workflow.
50
+ That is the whole user-facing instruction. After that, you should not have to name the checkpoint, context pack, event log, failed attempts, verification state, or helper scripts yourself. The agent runs Goalkeeper as part of the skill workflow.
51
51
 
52
52
  ## The Problem
53
53
 
@@ -57,15 +57,15 @@ But long goals are different.
57
57
 
58
58
  Imagine a real session:
59
59
 
60
- 1. You ask an agent to harden a release.
61
- 2. The obvious patch fixes the visible bug, but would break rollback compatibility.
62
- 3. You set a hard constraint: no database schema change, keep backward compatibility.
63
- 4. A second attempt passes unit tests, but fails an integration edge case.
64
- 5. The agent settles on a compatibility shim plus a targeted regression test.
65
- 6. The regression test passes. That path is now the safe one.
60
+ 1. You ask an agent to fix a payment bug.
61
+ 2. Early in the work, it discovers `refunds` is legacy code and should not be touched.
62
+ 3. The quickest patch would edit `refunds`, so you reject that path.
63
+ 4. The agent tries moving the fix into a webhook handler, but duplicate events break it.
64
+ 5. It finally proves the safe route: put an idempotency guard in the service layer and cover it with a regression test.
65
+ 6. The test passes. That route is now the one you want preserved.
66
66
  7. The context compacts.
67
- 8. Later, the agent resumes from a clean summary: "release hardening mostly done."
68
- 9. It still knows the goal, but may no longer feel why the schema shortcut stayed forbidden, why the first patches failed, or why that regression test mattered.
67
+ 8. Later, the agent resumes from a clean summary: "payment bug mostly fixed."
68
+ 9. It still knows the goal, but may no longer remember that `refunds` was off-limits, that the webhook attempt failed, or that the service-layer test is what made the route safe.
69
69
 
70
70
  That is where drift starts.
71
71
 
@@ -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
@@ -39,15 +39,15 @@ npx skills add deltafleet/goalkeeper --agent claude-code codex
39
39
 
40
40
  所以像下面这样的 goal 可能已经足够触发它:
41
41
 
42
- > `/goal` Harden this release over a long-running session. Keep the goal, constraints, rejected paths, failed attempts, verification state, and next action recoverable after compact/resume.
42
+ > `/goal` Harden this release over a long-running session. Use goalkeeper.
43
43
 
44
44
  但 skill activation 仍然是 routing decision,不是私有的 agent runtime hook。Goalkeeper 不能强制自己附着到每一个 goal。
45
45
 
46
46
  对于重要的长期任务,最稳妥的做法是在创建 goal 时,或创建 goal 后正式开始工作前,明确调用它:
47
47
 
48
- > Use goalkeeper for this `/goal`. Keep the goal, constraints, decisions, verification state, failed attempts, and next action recoverable across compaction.
48
+ > Use goalkeeper for this goal.
49
49
 
50
- 之后用户不需要手动执行 Goalkeeper helper scriptsagent 会把它们作为 skill workflow 的一部分来运行。
50
+ 用户需要记住的句子到这里就够了。checkpoint、context pack、event log、失败尝试、验证状态和 helper scripts,都由 agent Goalkeeper workflow 中处理。
51
51
 
52
52
  ## 问题
53
53
 
@@ -57,15 +57,15 @@ npx skills add deltafleet/goalkeeper --agent claude-code codex
57
57
 
58
58
  想象一个真实会话:
59
59
 
60
- 1. 你让 agent release hardening
61
- 2. 最显眼的 patch 能修掉眼前的 bug,但会破坏 rollback compatibility。
62
- 3. 你设下硬约束:不能改 database schema,必须保持 backward compatibility。
63
- 4. 第二次尝试通过了 unit tests,但在 integration edge case 上失败。
64
- 5. agent 选择 compatibility shim targeted regression test
65
- 6. regression test 通过了。这条路线现在是安全路线。
60
+ 1. 你让 agent 修一个支付 bug
61
+ 2. 早期调查发现,`refunds` 相关代码是 legacy,不能轻易动。
62
+ 3. 最快的 patch 是直接改 `refunds`,所以你明确拒绝这条路。
63
+ 4. agent 尝试把修复放到 webhook handler,但 duplicate event 场景会失败。
64
+ 5. 最后它验证了安全路线:在 service layeridempotency guard,并用 regression test 覆盖。
65
+ 6. 测试通过了。这条路线现在是需要保留下来的安全路线。
66
66
  7. 上下文被 compact。
67
- 8. 后来 agent 带着整洁摘要回来:“release hardening 基本完成。”
68
- 9. 它还记得 goal,但可能不再清楚为什么 schema shortcut 必须继续禁止,为什么前几个 patch 失败,以及为什么那个 regression test 很关键。
67
+ 8. 后来 agent 带着整洁摘要回来:“支付 bug 基本修好了。”
68
+ 9. 它还记得 goal,但可能不再清楚 `refunds` 为什么不能动,webhook 尝试为什么失败,以及 service-layer test 为什么证明了正确路线。
69
69
 
70
70
  drift 就从这里开始。
71
71
 
@@ -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.0",
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
@@ -1,5 +1,5 @@
1
1
  interface:
2
2
  display_name: "Goalkeeper"
3
3
  short_description: "Keep long goals oriented"
4
- default_prompt: "Use $goalkeeper for this long-running goal so checkpoint, context pack, and event log preserve direction across compaction."
4
+ default_prompt: "Use $goalkeeper for this long-running goal."
5
5
  brand_color: "#2563EB"
@@ -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.