opensquid 0.5.441 → 0.5.449
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/README.md +1 -0
- package/dist/functions/arm_scope.d.ts +27 -0
- package/dist/functions/arm_scope.d.ts.map +1 -0
- package/dist/functions/arm_scope.js +52 -0
- package/dist/functions/arm_scope.js.map +1 -0
- package/dist/functions/index.d.ts +1 -0
- package/dist/functions/index.d.ts.map +1 -1
- package/dist/functions/index.js +1 -0
- package/dist/functions/index.js.map +1 -1
- package/dist/functions/recall_pre_inject.d.ts.map +1 -1
- package/dist/functions/recall_pre_inject.js +12 -0
- package/dist/functions/recall_pre_inject.js.map +1 -1
- package/dist/rag/store_git.d.ts +23 -0
- package/dist/rag/store_git.d.ts.map +1 -0
- package/dist/rag/store_git.js +57 -0
- package/dist/rag/store_git.js.map +1 -0
- package/dist/runtime/bootstrap.d.ts.map +1 -1
- package/dist/runtime/bootstrap.js +2 -0
- package/dist/runtime/bootstrap.js.map +1 -1
- package/dist/runtime/handoff/render.d.ts +5 -4
- package/dist/runtime/handoff/render.d.ts.map +1 -1
- package/dist/runtime/handoff/render.js +7 -7
- package/dist/runtime/handoff/render.js.map +1 -1
- package/dist/runtime/hooks/active_task_mirror.js +0 -0
- package/dist/runtime/hooks/apply_patch.js +0 -0
- package/dist/runtime/hooks/dispatch.js +0 -0
- package/dist/runtime/hooks/hook_output.js +0 -0
- package/dist/runtime/hooks/memory_reconcile.js +0 -0
- package/dist/runtime/hooks/new_project_detect.js +0 -0
- package/dist/runtime/hooks/profession_resolver.js +0 -0
- package/dist/runtime/hooks/scope_intent.js +0 -0
- package/dist/runtime/hooks/session-end.js +11 -0
- package/dist/runtime/hooks/session-end.js.map +1 -1
- package/dist/runtime/hooks/session_id.js +0 -0
- package/dist/runtime/hooks/session_liveness.js +0 -0
- package/dist/runtime/hooks/stop_drive.js +0 -0
- package/dist/runtime/hooks/stop_stream.js +0 -0
- package/dist/runtime/hooks/subagent_guard.js +0 -0
- package/dist/runtime/hooks/transcript.js +0 -0
- package/dist/runtime/hooks/transcript_tasks.js +0 -0
- package/dist/runtime/ralph/orchestrator.d.ts.map +1 -1
- package/dist/runtime/ralph/orchestrator.js +2 -1
- package/dist/runtime/ralph/orchestrator.js.map +1 -1
- package/dist/setup/cli/limits_state.d.ts.map +1 -1
- package/dist/setup/cli/limits_state.js +6 -40
- package/dist/setup/cli/limits_state.js.map +1 -1
- package/dist/setup/cli/pack_walk.d.ts +32 -0
- package/dist/setup/cli/pack_walk.d.ts.map +1 -0
- package/dist/setup/cli/pack_walk.js +76 -0
- package/dist/setup/cli/pack_walk.js.map +1 -0
- package/dist/setup/cli/permissions_state.d.ts.map +1 -1
- package/dist/setup/cli/permissions_state.js +6 -37
- package/dist/setup/cli/permissions_state.js.map +1 -1
- package/dist/setup/cli/triggers_state.d.ts.map +1 -1
- package/dist/setup/cli/triggers_state.js +3 -29
- package/dist/setup/cli/triggers_state.js.map +1 -1
- package/dist/workgraph/events.d.ts.map +1 -1
- package/dist/workgraph/events.js +10 -0
- package/dist/workgraph/events.js.map +1 -1
- package/dist/workgraph/store.d.ts.map +1 -1
- package/dist/workgraph/store.js +5 -0
- package/dist/workgraph/store.js.map +1 -1
- package/dist/workgraph/types.d.ts +2 -1
- package/dist/workgraph/types.d.ts.map +1 -1
- package/docs/ARCHITECTURE.md +268 -0
- package/docs/pack-runtime.md +15 -12
- package/docs/skill-grammar-guide.md +4 -4
- package/package.json +5 -3
- package/packs/builtin/coding-flow/skills/entry-and-handoffs/skill.yaml +13 -17
- package/dist/anti-drift/evaluator.d.ts +0 -88
- package/dist/anti-drift/evaluator.d.ts.map +0 -1
- package/dist/anti-drift/evaluator.js +0 -417
- package/dist/anti-drift/evaluator.js.map +0 -1
- package/dist/anti-drift/evaluator.test.js +0 -78
- package/dist/anti-drift/rules.d.ts +0 -80
- package/dist/anti-drift/rules.d.ts.map +0 -1
- package/dist/anti-drift/rules.js +0 -368
- package/dist/anti-drift/rules.js.map +0 -1
- package/dist/anti-drift/rules.test.js +0 -213
- package/dist/anti-drift/state.d.ts +0 -107
- package/dist/anti-drift/state.d.ts.map +0 -1
- package/dist/anti-drift/state.js +0 -177
- package/dist/anti-drift/state.js.map +0 -1
- package/dist/anti-drift/state.test.js +0 -120
- package/dist/chat/adapters/discord.d.ts +0 -41
- package/dist/chat/adapters/discord.d.ts.map +0 -1
- package/dist/chat/adapters/discord.js +0 -176
- package/dist/chat/adapters/discord.js.map +0 -1
- package/dist/chat/adapters/discord.test.js +0 -25
- package/dist/chat/adapters/slack.d.ts +0 -43
- package/dist/chat/adapters/slack.d.ts.map +0 -1
- package/dist/chat/adapters/slack.js +0 -172
- package/dist/chat/adapters/slack.js.map +0 -1
- package/dist/chat/adapters/slack.test.js +0 -30
- package/dist/chat/adapters/telegram.d.ts +0 -148
- package/dist/chat/adapters/telegram.d.ts.map +0 -1
- package/dist/chat/adapters/telegram.js +0 -498
- package/dist/chat/adapters/telegram.js.map +0 -1
- package/dist/chat/adapters/telegram.test.js +0 -94
- package/dist/chat/config.d.ts +0 -98
- package/dist/chat/config.d.ts.map +0 -1
- package/dist/chat/config.js +0 -185
- package/dist/chat/config.js.map +0 -1
- package/dist/chat/daemon/active-project.d.ts +0 -17
- package/dist/chat/daemon/active-project.d.ts.map +0 -1
- package/dist/chat/daemon/active-project.js +0 -23
- package/dist/chat/daemon/active-project.js.map +0 -1
- package/dist/chat/daemon/autospawn.d.ts +0 -40
- package/dist/chat/daemon/autospawn.d.ts.map +0 -1
- package/dist/chat/daemon/autospawn.js +0 -129
- package/dist/chat/daemon/autospawn.js.map +0 -1
- package/dist/chat/daemon/autospawn.test.js +0 -112
- package/dist/chat/daemon/cli.d.ts +0 -18
- package/dist/chat/daemon/cli.d.ts.map +0 -1
- package/dist/chat/daemon/cli.js +0 -71
- package/dist/chat/daemon/cli.js.map +0 -1
- package/dist/chat/daemon/collisions.js +0 -384
- package/dist/chat/daemon/health-check.d.ts +0 -69
- package/dist/chat/daemon/health-check.d.ts.map +0 -1
- package/dist/chat/daemon/health-check.js +0 -112
- package/dist/chat/daemon/health-check.js.map +0 -1
- package/dist/chat/daemon/inbox-read.d.ts +0 -35
- package/dist/chat/daemon/inbox-read.d.ts.map +0 -1
- package/dist/chat/daemon/inbox-read.js +0 -75
- package/dist/chat/daemon/inbox-read.js.map +0 -1
- package/dist/chat/daemon/inbox-read.test.js +0 -97
- package/dist/chat/daemon/inbox.d.ts +0 -63
- package/dist/chat/daemon/inbox.d.ts.map +0 -1
- package/dist/chat/daemon/inbox.js +0 -56
- package/dist/chat/daemon/inbox.js.map +0 -1
- package/dist/chat/daemon/inbox.test.js +0 -110
- package/dist/chat/daemon/lifecycle.d.ts +0 -71
- package/dist/chat/daemon/lifecycle.d.ts.map +0 -1
- package/dist/chat/daemon/lifecycle.js +0 -221
- package/dist/chat/daemon/lifecycle.js.map +0 -1
- package/dist/chat/daemon/lifecycle.test.js +0 -163
- package/dist/chat/daemon/protocol.d.ts +0 -107
- package/dist/chat/daemon/protocol.d.ts.map +0 -1
- package/dist/chat/daemon/protocol.js +0 -54
- package/dist/chat/daemon/protocol.js.map +0 -1
- package/dist/chat/daemon/routing.d.ts +0 -140
- package/dist/chat/daemon/routing.d.ts.map +0 -1
- package/dist/chat/daemon/routing.js +0 -198
- package/dist/chat/daemon/routing.js.map +0 -1
- package/dist/chat/daemon/routing.test.js +0 -259
- package/dist/chat/daemon/rpc-client.d.ts +0 -45
- package/dist/chat/daemon/rpc-client.d.ts.map +0 -1
- package/dist/chat/daemon/rpc-client.js +0 -133
- package/dist/chat/daemon/rpc-client.js.map +0 -1
- package/dist/chat/daemon/rpc-server.d.ts +0 -39
- package/dist/chat/daemon/rpc-server.d.ts.map +0 -1
- package/dist/chat/daemon/rpc-server.js +0 -385
- package/dist/chat/daemon/rpc-server.js.map +0 -1
- package/dist/chat/daemon/rpc.test.js +0 -177
- package/dist/chat/daemon/subscribers.js +0 -257
- package/dist/chat/daemon/worker.d.ts +0 -27
- package/dist/chat/daemon/worker.d.ts.map +0 -1
- package/dist/chat/daemon/worker.js +0 -313
- package/dist/chat/daemon/worker.js.map +0 -1
- package/dist/chat/daemon/workspace-topic.js +0 -324
- package/dist/chat/env-token.d.ts +0 -60
- package/dist/chat/env-token.d.ts.map +0 -1
- package/dist/chat/env-token.js +0 -137
- package/dist/chat/env-token.js.map +0 -1
- package/dist/chat/env-token.test.js +0 -160
- package/dist/chat/factory.d.ts +0 -30
- package/dist/chat/factory.d.ts.map +0 -1
- package/dist/chat/factory.js +0 -50
- package/dist/chat/factory.js.map +0 -1
- package/dist/chat/factory.test.js +0 -55
- package/dist/chat/gateway.d.ts +0 -176
- package/dist/chat/gateway.d.ts.map +0 -1
- package/dist/chat/gateway.js +0 -146
- package/dist/chat/gateway.js.map +0 -1
- package/dist/chat/gateway.test.js +0 -192
- package/dist/claude-md.d.ts +0 -39
- package/dist/claude-md.d.ts.map +0 -1
- package/dist/claude-md.js +0 -113
- package/dist/claude-md.js.map +0 -1
- package/dist/claude-md.test.js +0 -91
- package/dist/codex/activate.d.ts +0 -66
- package/dist/codex/activate.d.ts.map +0 -1
- package/dist/codex/activate.js +0 -329
- package/dist/codex/activate.js.map +0 -1
- package/dist/codex/activate.test.js +0 -229
- package/dist/codex/bundled-default/bundled-default.test.js +0 -161
- package/dist/codex/cli-publish.test.js +0 -133
- package/dist/codex/cli.d.ts +0 -35
- package/dist/codex/cli.d.ts.map +0 -1
- package/dist/codex/cli.js +0 -554
- package/dist/codex/cli.js.map +0 -1
- package/dist/codex/cli.test.js +0 -277
- package/dist/codex/import-skill-md.d.ts +0 -53
- package/dist/codex/import-skill-md.d.ts.map +0 -1
- package/dist/codex/import-skill-md.js +0 -236
- package/dist/codex/import-skill-md.js.map +0 -1
- package/dist/codex/import-skill-md.test.js +0 -225
- package/dist/codex/loader.d.ts +0 -27
- package/dist/codex/loader.d.ts.map +0 -1
- package/dist/codex/loader.js +0 -86
- package/dist/codex/loader.js.map +0 -1
- package/dist/codex/loader.test.js +0 -75
- package/dist/codex/parse.d.ts +0 -28
- package/dist/codex/parse.d.ts.map +0 -1
- package/dist/codex/parse.js +0 -309
- package/dist/codex/parse.js.map +0 -1
- package/dist/codex/parse.test.js +0 -241
- package/dist/codex/store.d.ts +0 -87
- package/dist/codex/store.d.ts.map +0 -1
- package/dist/codex/store.js +0 -205
- package/dist/codex/store.js.map +0 -1
- package/dist/codex/store.test.js +0 -242
- package/dist/codex/types.d.ts +0 -398
- package/dist/codex/types.d.ts.map +0 -1
- package/dist/codex/types.js +0 -21
- package/dist/codex/types.js.map +0 -1
- package/dist/config.d.ts +0 -53
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -202
- package/dist/config.js.map +0 -1
- package/dist/config.test.js +0 -117
- package/dist/engine/cli.d.ts +0 -14
- package/dist/engine/cli.d.ts.map +0 -1
- package/dist/engine/cli.js +0 -171
- package/dist/engine/cli.js.map +0 -1
- package/dist/engine/client.d.ts +0 -219
- package/dist/engine/client.d.ts.map +0 -1
- package/dist/engine/client.js +0 -312
- package/dist/engine/client.js.map +0 -1
- package/dist/engine/config.d.ts +0 -62
- package/dist/engine/config.d.ts.map +0 -1
- package/dist/engine/config.js +0 -223
- package/dist/engine/config.js.map +0 -1
- package/dist/engine/index.d.ts +0 -17
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js +0 -16
- package/dist/engine/index.js.map +0 -1
- package/dist/engine/resolver.d.ts +0 -62
- package/dist/engine/resolver.d.ts.map +0 -1
- package/dist/engine/resolver.js +0 -103
- package/dist/engine/resolver.js.map +0 -1
- package/dist/engine/singleton.d.ts +0 -95
- package/dist/engine/singleton.d.ts.map +0 -1
- package/dist/engine/singleton.js +0 -325
- package/dist/engine/singleton.js.map +0 -1
- package/dist/engine/types.d.ts +0 -402
- package/dist/engine/types.d.ts.map +0 -1
- package/dist/engine/types.js +0 -22
- package/dist/engine/types.js.map +0 -1
- package/dist/engine-binary-resolver.js +0 -110
- package/dist/engine-binary-resolver.test.js +0 -61
- package/dist/engine-cli.js +0 -60
- package/dist/engine-client.js +0 -301
- package/dist/engine-client.test.js +0 -118
- package/dist/functions/chain_state.d.ts +0 -51
- package/dist/functions/chain_state.d.ts.map +0 -1
- package/dist/functions/chain_state.js +0 -59
- package/dist/functions/chain_state.js.map +0 -1
- package/dist/hooks/drift-catalog.d.ts +0 -68
- package/dist/hooks/drift-catalog.d.ts.map +0 -1
- package/dist/hooks/drift-catalog.js +0 -184
- package/dist/hooks/drift-catalog.js.map +0 -1
- package/dist/hooks/drift-catalog.test.js +0 -154
- package/dist/hooks/drift-patterns.d.ts +0 -110
- package/dist/hooks/drift-patterns.d.ts.map +0 -1
- package/dist/hooks/drift-patterns.js +0 -289
- package/dist/hooks/drift-patterns.js.map +0 -1
- package/dist/hooks/drift-patterns.test.js +0 -325
- package/dist/hooks/engine-vocab-gate.d.ts +0 -108
- package/dist/hooks/engine-vocab-gate.d.ts.map +0 -1
- package/dist/hooks/engine-vocab-gate.js +0 -225
- package/dist/hooks/engine-vocab-gate.js.map +0 -1
- package/dist/hooks/engine-vocab-gate.test.js +0 -170
- package/dist/hooks/heartbeat.d.ts +0 -107
- package/dist/hooks/heartbeat.d.ts.map +0 -1
- package/dist/hooks/heartbeat.js +0 -316
- package/dist/hooks/heartbeat.js.map +0 -1
- package/dist/hooks/heartbeat.test.js +0 -393
- package/dist/hooks/honesty-ledger-session-scope.test.js +0 -100
- package/dist/hooks/honesty-ledger.d.ts +0 -123
- package/dist/hooks/honesty-ledger.d.ts.map +0 -1
- package/dist/hooks/honesty-ledger.js +0 -226
- package/dist/hooks/honesty-ledger.js.map +0 -1
- package/dist/hooks/honesty-ledger.test.js +0 -466
- package/dist/hooks/inline-report-check.d.ts +0 -63
- package/dist/hooks/inline-report-check.d.ts.map +0 -1
- package/dist/hooks/inline-report-check.js +0 -88
- package/dist/hooks/inline-report-check.js.map +0 -1
- package/dist/hooks/inline-report-check.test.js +0 -96
- package/dist/hooks/pre-tool-use.d.ts +0 -62
- package/dist/hooks/pre-tool-use.d.ts.map +0 -1
- package/dist/hooks/pre-tool-use.js +0 -342
- package/dist/hooks/pre-tool-use.js.map +0 -1
- package/dist/hooks/pre-tool-use.test.js +0 -134
- package/dist/hooks/session-end.d.ts +0 -15
- package/dist/hooks/session-end.d.ts.map +0 -1
- package/dist/hooks/session-end.js +0 -60
- package/dist/hooks/session-end.js.map +0 -1
- package/dist/hooks/session-end.test.js +0 -52
- package/dist/hooks/stop.d.ts +0 -35
- package/dist/hooks/stop.d.ts.map +0 -1
- package/dist/hooks/stop.js +0 -136
- package/dist/hooks/stop.js.map +0 -1
- package/dist/hooks/transcript-active-task.test.js +0 -342
- package/dist/hooks/transcript.d.ts +0 -26
- package/dist/hooks/transcript.d.ts.map +0 -1
- package/dist/hooks/transcript.js +0 -266
- package/dist/hooks/transcript.js.map +0 -1
- package/dist/hooks/transcript.test.js +0 -103
- package/dist/hooks/user-prompt-submit.d.ts +0 -74
- package/dist/hooks/user-prompt-submit.d.ts.map +0 -1
- package/dist/hooks/user-prompt-submit.js +0 -256
- package/dist/hooks/user-prompt-submit.js.map +0 -1
- package/dist/hooks/user-prompt-submit.test.js +0 -118
- package/dist/hooks/versioning-gate.d.ts +0 -101
- package/dist/hooks/versioning-gate.d.ts.map +0 -1
- package/dist/hooks/versioning-gate.js +0 -245
- package/dist/hooks/versioning-gate.js.map +0 -1
- package/dist/hooks/versioning-gate.test.js +0 -368
- package/dist/hooks/workflow-gate.d.ts +0 -64
- package/dist/hooks/workflow-gate.d.ts.map +0 -1
- package/dist/hooks/workflow-gate.js +0 -152
- package/dist/hooks/workflow-gate.js.map +0 -1
- package/dist/hooks/workflow-gate.test.js +0 -197
- package/dist/hooks-cli.d.ts +0 -25
- package/dist/hooks-cli.d.ts.map +0 -1
- package/dist/hooks-cli.js +0 -286
- package/dist/hooks-cli.js.map +0 -1
- package/dist/hooks-cli.test.js +0 -148
- package/dist/origin.d.ts +0 -16
- package/dist/origin.d.ts.map +0 -1
- package/dist/origin.js +0 -92
- package/dist/origin.js.map +0 -1
- package/dist/packs/seed_lessons_ingest.d.ts +0 -30
- package/dist/packs/seed_lessons_ingest.d.ts.map +0 -1
- package/dist/packs/seed_lessons_ingest.js +0 -107
- package/dist/packs/seed_lessons_ingest.js.map +0 -1
- package/dist/project-cli.d.ts +0 -7
- package/dist/project-cli.d.ts.map +0 -1
- package/dist/project-cli.js +0 -145
- package/dist/project-cli.js.map +0 -1
- package/dist/project.d.ts +0 -127
- package/dist/project.d.ts.map +0 -1
- package/dist/project.js +0 -281
- package/dist/project.js.map +0 -1
- package/dist/project.test.js +0 -287
- package/dist/rag/backends/loop_engine.d.ts +0 -61
- package/dist/rag/backends/loop_engine.d.ts.map +0 -1
- package/dist/rag/backends/loop_engine.js +0 -160
- package/dist/rag/backends/loop_engine.js.map +0 -1
- package/dist/recall.d.ts +0 -82
- package/dist/recall.d.ts.map +0 -1
- package/dist/recall.js +0 -81
- package/dist/recall.js.map +0 -1
- package/dist/runtime/agent_bridge/autospawn.d.ts +0 -131
- package/dist/runtime/agent_bridge/autospawn.d.ts.map +0 -1
- package/dist/runtime/agent_bridge/autospawn.js +0 -251
- package/dist/runtime/agent_bridge/autospawn.js.map +0 -1
- package/dist/runtime/chain_state.d.ts +0 -124
- package/dist/runtime/chain_state.d.ts.map +0 -1
- package/dist/runtime/chain_state.js +0 -189
- package/dist/runtime/chain_state.js.map +0 -1
- package/dist/runtime/hooks/permission_decision.d.ts +0 -34
- package/dist/runtime/hooks/permission_decision.d.ts.map +0 -1
- package/dist/runtime/hooks/permission_decision.js +0 -39
- package/dist/runtime/hooks/permission_decision.js.map +0 -1
- package/dist/runtime/workflow_fsm.d.ts +0 -21
- package/dist/runtime/workflow_fsm.d.ts.map +0 -1
- package/dist/runtime/workflow_fsm.js +0 -25
- package/dist/runtime/workflow_fsm.js.map +0 -1
- package/dist/runtime/workflow_map.d.ts +0 -26
- package/dist/runtime/workflow_map.d.ts.map +0 -1
- package/dist/runtime/workflow_map.js +0 -38
- package/dist/runtime/workflow_map.js.map +0 -1
- package/dist/scope.d.ts +0 -48
- package/dist/scope.d.ts.map +0 -1
- package/dist/scope.js +0 -111
- package/dist/scope.js.map +0 -1
- package/dist/setup/cli/topic_create_step.d.ts +0 -84
- package/dist/setup/cli/topic_create_step.d.ts.map +0 -1
- package/dist/setup/cli/topic_create_step.js +0 -213
- package/dist/setup/cli/topic_create_step.js.map +0 -1
- package/dist/system-export.d.ts +0 -65
- package/dist/system-export.d.ts.map +0 -1
- package/dist/system-export.js +0 -194
- package/dist/system-export.js.map +0 -1
- package/dist/utterance/classifier.d.ts +0 -53
- package/dist/utterance/classifier.d.ts.map +0 -1
- package/dist/utterance/classifier.js +0 -184
- package/dist/utterance/classifier.js.map +0 -1
- package/dist/utterance/classifier.test.js +0 -147
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"session-end.d.ts","sourceRoot":"","sources":["../../src.legacy/hooks/session-end.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAaH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA+CvD"}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `opensquid hook session-end` — Claude Code SessionEnd hook handler.
|
|
3
|
-
*
|
|
4
|
-
* Fires when a Claude Code session terminates. Wipes the session's
|
|
5
|
-
* honesty-ledger state (turn-ledger.jsonl + broken-promises.jsonl) so
|
|
6
|
-
* the ledger doesn't grow unbounded across all-time sessions.
|
|
7
|
-
*
|
|
8
|
-
* Without this hook, every Claude Code session creates a
|
|
9
|
-
* <data-root>/sessions/<id>/ directory with two JSONL files that
|
|
10
|
-
* never get cleaned up. Disk usage grows linearly with session count.
|
|
11
|
-
*
|
|
12
|
-
* Exit 0 always — SessionEnd is cleanup, not blocking.
|
|
13
|
-
*/
|
|
14
|
-
import { runDriftCatalogScan } from "./drift-catalog.js";
|
|
15
|
-
import { clearSession } from "./honesty-ledger.js";
|
|
16
|
-
export async function runSessionEndHook() {
|
|
17
|
-
let raw = "";
|
|
18
|
-
for await (const chunk of process.stdin) {
|
|
19
|
-
raw += chunk;
|
|
20
|
-
}
|
|
21
|
-
if (!raw.trim())
|
|
22
|
-
process.exit(0);
|
|
23
|
-
let payload;
|
|
24
|
-
try {
|
|
25
|
-
payload = JSON.parse(raw);
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
process.exit(0);
|
|
29
|
-
}
|
|
30
|
-
const sessionId = payload.session_id;
|
|
31
|
-
if (!sessionId)
|
|
32
|
-
process.exit(0);
|
|
33
|
-
// 0.7.22 / D10 — automated drift catalog. Scan the transcript for
|
|
34
|
-
// drift markers (user corrections, locked-rule citations, agent
|
|
35
|
-
// mea-culpas) and append to the project's drift-catalog.jsonl. Runs
|
|
36
|
-
// BEFORE clearSession so any session-scoped state used for context
|
|
37
|
-
// is still available.
|
|
38
|
-
try {
|
|
39
|
-
const count = await runDriftCatalogScan({
|
|
40
|
-
sessionId,
|
|
41
|
-
transcriptPath: payload.transcript_path,
|
|
42
|
-
cwd: payload.cwd,
|
|
43
|
-
});
|
|
44
|
-
if (count > 0) {
|
|
45
|
-
process.stderr.write(`🦑 [opensquid drift-catalog] recorded ${count} drift marker(s)\n`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
catch (err) {
|
|
49
|
-
process.stderr.write(`[opensquid hook session-end] drift-catalog scan failed (non-fatal): ${err instanceof Error ? err.message : err}\n`);
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
await clearSession(sessionId);
|
|
53
|
-
}
|
|
54
|
-
catch (err) {
|
|
55
|
-
// Cleanup failure is non-fatal — disk-space leak, not a correctness bug.
|
|
56
|
-
process.stderr.write(`[opensquid hook session-end] clearSession failed: ${err instanceof Error ? err.message : err}\n`);
|
|
57
|
-
}
|
|
58
|
-
process.exit(0);
|
|
59
|
-
}
|
|
60
|
-
//# sourceMappingURL=session-end.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"session-end.js","sourceRoot":"","sources":["../../src.legacy/hooks/session-end.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAUnD,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QACxC,GAAG,IAAI,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;QAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAEjC,IAAI,OAAwB,CAAC;IAC7B,IAAI,CAAC;QACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IACrC,IAAI,CAAC,SAAS;QAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAEhC,kEAAkE;IAClE,gEAAgE;IAChE,oEAAoE;IACpE,mEAAmE;IACnE,sBAAsB;IACtB,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,mBAAmB,CAAC;YACtC,SAAS;YACT,cAAc,EAAE,OAAO,CAAC,eAAe;YACvC,GAAG,EAAE,OAAO,CAAC,GAAG;SACjB,CAAC,CAAC;QACH,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACd,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,yCAAyC,KAAK,oBAAoB,CAAC,CAAC;QAC3F,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,uEAAuE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,CACpH,CAAC;IACJ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,YAAY,CAAC,SAAS,CAAC,CAAC;IAChC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,yEAAyE;QACzE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,qDAAqD,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,CAClG,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC"}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import * as crypto from "node:crypto";
|
|
2
|
-
import { promises as fs } from "node:fs";
|
|
3
|
-
import * as os from "node:os";
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
|
-
import { clearSession, readBrokenPromises, readTurnLedger, recordBrokenPromise, recordToolCall, } from "./honesty-ledger.js";
|
|
7
|
-
let tmpRoot;
|
|
8
|
-
const SESSION = "session-end-test";
|
|
9
|
-
beforeEach(async () => {
|
|
10
|
-
tmpRoot = path.join(os.tmpdir(), `oscli-session-end-${crypto.randomUUID()}`);
|
|
11
|
-
await fs.mkdir(tmpRoot, { recursive: true });
|
|
12
|
-
});
|
|
13
|
-
afterEach(async () => {
|
|
14
|
-
await fs.rm(tmpRoot, { recursive: true, force: true });
|
|
15
|
-
});
|
|
16
|
-
describe("SessionEnd cleanup (clearSession via the hook)", () => {
|
|
17
|
-
it("wipes both ledger and broken-promises after population", async () => {
|
|
18
|
-
await recordToolCall(SESSION, "Bash", "npm test", { dataRoot: tmpRoot });
|
|
19
|
-
await recordToolCall(SESSION, "Bash", "git commit -m foo", { dataRoot: tmpRoot });
|
|
20
|
-
await recordBrokenPromise(SESSION, {
|
|
21
|
-
ts: "2026-05-15T00:00:00Z",
|
|
22
|
-
claim_id: "fake",
|
|
23
|
-
claim_label: "x",
|
|
24
|
-
matched_text: "y",
|
|
25
|
-
reason: "z",
|
|
26
|
-
}, { dataRoot: tmpRoot });
|
|
27
|
-
// Sanity-check the precondition.
|
|
28
|
-
expect((await readTurnLedger(SESSION, { dataRoot: tmpRoot })).length).toBe(2);
|
|
29
|
-
expect((await readBrokenPromises(SESSION, { dataRoot: tmpRoot })).length).toBe(1);
|
|
30
|
-
// SessionEnd action.
|
|
31
|
-
await clearSession(SESSION, { dataRoot: tmpRoot });
|
|
32
|
-
expect(await readTurnLedger(SESSION, { dataRoot: tmpRoot })).toEqual([]);
|
|
33
|
-
expect(await readBrokenPromises(SESSION, { dataRoot: tmpRoot })).toEqual([]);
|
|
34
|
-
});
|
|
35
|
-
it("session directory remains, just empty of ledger files", async () => {
|
|
36
|
-
await recordToolCall(SESSION, "Bash", "ls", { dataRoot: tmpRoot });
|
|
37
|
-
await clearSession(SESSION, { dataRoot: tmpRoot });
|
|
38
|
-
// The sessions/<id>/ directory itself is preserved (cheap),
|
|
39
|
-
// only the JSONL files inside are removed.
|
|
40
|
-
const dir = path.join(tmpRoot, "sessions", SESSION);
|
|
41
|
-
await expect(fs.access(dir)).resolves.toBeUndefined();
|
|
42
|
-
const entries = await fs.readdir(dir);
|
|
43
|
-
expect(entries).toEqual([]);
|
|
44
|
-
});
|
|
45
|
-
it("does NOT touch other sessions' ledgers", async () => {
|
|
46
|
-
await recordToolCall(SESSION, "Bash", "ls", { dataRoot: tmpRoot });
|
|
47
|
-
await recordToolCall("other-session", "Bash", "ls", { dataRoot: tmpRoot });
|
|
48
|
-
await clearSession(SESSION, { dataRoot: tmpRoot });
|
|
49
|
-
expect((await readTurnLedger(SESSION, { dataRoot: tmpRoot })).length).toBe(0);
|
|
50
|
-
expect((await readTurnLedger("other-session", { dataRoot: tmpRoot })).length).toBe(1);
|
|
51
|
-
});
|
|
52
|
-
});
|
package/dist/hooks/stop.d.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `opensquid hook stop` — Claude Code Stop hook handler.
|
|
3
|
-
*
|
|
4
|
-
* Fires at the end of every assistant turn. Two responsibilities:
|
|
5
|
-
*
|
|
6
|
-
* 1. Honesty ledger reconciliation: cross-reference the assistant's
|
|
7
|
-
* final message against the session's accumulated tool-call ledger.
|
|
8
|
-
* Any unfulfilled claim is recorded as a broken promise that the
|
|
9
|
-
* next turn's UserPromptSubmit hook surfaces back to the agent.
|
|
10
|
-
*
|
|
11
|
-
* 2. Token-threshold heartbeat: estimate transcript token count, and
|
|
12
|
-
* if the conversation has grown past the configured threshold
|
|
13
|
-
* since the last checkpoint, arm a pending heartbeat marker so
|
|
14
|
-
* the next UserPromptSubmit hook injects a re-anchor nudge into
|
|
15
|
-
* the agent's context. The agent (already authenticated and in
|
|
16
|
-
* the loop) does the actual recall + classify work inline.
|
|
17
|
-
*
|
|
18
|
-
* Exit 0 always — Stop hook is observational, not blocking.
|
|
19
|
-
*
|
|
20
|
-
* Wired in ~/.claude/settings.json:
|
|
21
|
-
*
|
|
22
|
-
* "Stop": [
|
|
23
|
-
* { "hooks": [{
|
|
24
|
-
* "type": "command",
|
|
25
|
-
* "command": "node /path/to/opensquid/dist/index.js hook stop"
|
|
26
|
-
* }] }
|
|
27
|
-
* ]
|
|
28
|
-
*
|
|
29
|
-
* Pre-#124: this hook also spawned a detached LLM-classifier subprocess.
|
|
30
|
-
* Removed in favor of the heartbeat path — opensquid stays in-MCP-ecosystem
|
|
31
|
-
* (no external LLM dependency, no subprocess), and the agent does the
|
|
32
|
-
* classification work inline per CLAUDE.md classify-and-act rules.
|
|
33
|
-
*/
|
|
34
|
-
export declare function runStopHook(): Promise<void>;
|
|
35
|
-
//# sourceMappingURL=stop.d.ts.map
|
package/dist/hooks/stop.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"stop.d.ts","sourceRoot":"","sources":["../../src.legacy/hooks/stop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAmBH,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CA4GjD"}
|
package/dist/hooks/stop.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `opensquid hook stop` — Claude Code Stop hook handler.
|
|
3
|
-
*
|
|
4
|
-
* Fires at the end of every assistant turn. Two responsibilities:
|
|
5
|
-
*
|
|
6
|
-
* 1. Honesty ledger reconciliation: cross-reference the assistant's
|
|
7
|
-
* final message against the session's accumulated tool-call ledger.
|
|
8
|
-
* Any unfulfilled claim is recorded as a broken promise that the
|
|
9
|
-
* next turn's UserPromptSubmit hook surfaces back to the agent.
|
|
10
|
-
*
|
|
11
|
-
* 2. Token-threshold heartbeat: estimate transcript token count, and
|
|
12
|
-
* if the conversation has grown past the configured threshold
|
|
13
|
-
* since the last checkpoint, arm a pending heartbeat marker so
|
|
14
|
-
* the next UserPromptSubmit hook injects a re-anchor nudge into
|
|
15
|
-
* the agent's context. The agent (already authenticated and in
|
|
16
|
-
* the loop) does the actual recall + classify work inline.
|
|
17
|
-
*
|
|
18
|
-
* Exit 0 always — Stop hook is observational, not blocking.
|
|
19
|
-
*
|
|
20
|
-
* Wired in ~/.claude/settings.json:
|
|
21
|
-
*
|
|
22
|
-
* "Stop": [
|
|
23
|
-
* { "hooks": [{
|
|
24
|
-
* "type": "command",
|
|
25
|
-
* "command": "node /path/to/opensquid/dist/index.js hook stop"
|
|
26
|
-
* }] }
|
|
27
|
-
* ]
|
|
28
|
-
*
|
|
29
|
-
* Pre-#124: this hook also spawned a detached LLM-classifier subprocess.
|
|
30
|
-
* Removed in favor of the heartbeat path — opensquid stays in-MCP-ecosystem
|
|
31
|
-
* (no external LLM dependency, no subprocess), and the agent does the
|
|
32
|
-
* classification work inline per CLAUDE.md classify-and-act rules.
|
|
33
|
-
*/
|
|
34
|
-
import { clearTurnLedger, reconcile, readBrokenPromises, readTurnLedger, recordBrokenPromise, } from "./honesty-ledger.js";
|
|
35
|
-
import { checkAndMaybeArm } from "./heartbeat.js";
|
|
36
|
-
import { checkInlineReportFormat } from "./inline-report-check.js";
|
|
37
|
-
import { readLastAssistantText } from "./transcript.js";
|
|
38
|
-
export async function runStopHook() {
|
|
39
|
-
let raw = "";
|
|
40
|
-
for await (const chunk of process.stdin) {
|
|
41
|
-
raw += chunk;
|
|
42
|
-
}
|
|
43
|
-
if (!raw.trim()) {
|
|
44
|
-
process.exit(0);
|
|
45
|
-
}
|
|
46
|
-
let payload;
|
|
47
|
-
try {
|
|
48
|
-
payload = JSON.parse(raw);
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
process.stderr.write("[opensquid hook stop] malformed input — proceeding\n");
|
|
52
|
-
process.exit(0);
|
|
53
|
-
}
|
|
54
|
-
const sessionId = payload.session_id;
|
|
55
|
-
if (!sessionId)
|
|
56
|
-
process.exit(0);
|
|
57
|
-
// -- (1) Honesty-ledger reconcile ----------------------------------
|
|
58
|
-
const assistantText = payload.transcript_path
|
|
59
|
-
? await readLastAssistantText(payload.transcript_path)
|
|
60
|
-
: "";
|
|
61
|
-
const ledger = await readTurnLedger(sessionId);
|
|
62
|
-
const broken = reconcile(assistantText, ledger);
|
|
63
|
-
const existing = await readBrokenPromises(sessionId);
|
|
64
|
-
const existingKeys = new Set(existing.map((p) => `${p.claim_id}|${p.matched_text}`));
|
|
65
|
-
const fresh = [];
|
|
66
|
-
for (const promise of broken) {
|
|
67
|
-
const key = `${promise.claim_id}|${promise.matched_text}`;
|
|
68
|
-
if (existingKeys.has(key))
|
|
69
|
-
continue;
|
|
70
|
-
fresh.push(promise);
|
|
71
|
-
try {
|
|
72
|
-
await recordBrokenPromise(sessionId, promise);
|
|
73
|
-
}
|
|
74
|
-
catch (err) {
|
|
75
|
-
process.stderr.write(`[opensquid hook stop] failed to record promise: ${err instanceof Error ? err.message : err}\n`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
if (fresh.length > 0) {
|
|
79
|
-
for (const p of fresh) {
|
|
80
|
-
process.stderr.write(`🦑 [opensquid honesty] ${p.claim_id}: ${p.reason}\n`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
// 0.7.30 / D3 follow-up — when the agent writes a completion-report-
|
|
84
|
-
// shaped status update inline (vs. via mcp__opensquid__chat_send),
|
|
85
|
-
// D3's chat_send check doesn't fire. Catch the inline case here at
|
|
86
|
-
// Stop time and surface as a broken-promise next turn.
|
|
87
|
-
if (assistantText) {
|
|
88
|
-
const inlineViolation = checkInlineReportFormat(assistantText);
|
|
89
|
-
if (inlineViolation) {
|
|
90
|
-
const broken = {
|
|
91
|
-
ts: new Date().toISOString(),
|
|
92
|
-
claim_id: "inline-report-missing-phases",
|
|
93
|
-
claim_label: "PHASES block per [[feedback_telegram_reports]]",
|
|
94
|
-
matched_text: inlineViolation.matched_text,
|
|
95
|
-
reason: `inline message shape suggests a completion report ` +
|
|
96
|
-
`(version_refs=${inlineViolation.signals.version_refs}, ` +
|
|
97
|
-
`commit_hashes=${inlineViolation.signals.hash_refs}) but the ` +
|
|
98
|
-
`PHASES heading is missing. Catches D3 inline variant.`,
|
|
99
|
-
};
|
|
100
|
-
try {
|
|
101
|
-
await recordBrokenPromise(sessionId, broken);
|
|
102
|
-
process.stderr.write(`🦑 [opensquid honesty] ${broken.claim_id}: ${broken.reason}\n`);
|
|
103
|
-
}
|
|
104
|
-
catch (err) {
|
|
105
|
-
process.stderr.write(`[opensquid hook stop] inline-report check write failed: ${err instanceof Error ? err.message : err}\n`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
// 0.7.8 (#162): clear the turn-ledger AFTER reconciliation so the
|
|
110
|
-
// next turn's claims reconcile against ONLY that turn's tool calls.
|
|
111
|
-
// Previously the ledger only cleared at SessionEnd, which meant
|
|
112
|
-
// yesterday's git push satisfied today's "I'll push" claim on long
|
|
113
|
-
// resumed sessions — the load-bearing #160 finding for ledger drift.
|
|
114
|
-
try {
|
|
115
|
-
await clearTurnLedger(sessionId);
|
|
116
|
-
}
|
|
117
|
-
catch (err) {
|
|
118
|
-
process.stderr.write(`[opensquid hook stop] turn-ledger clear failed (non-fatal): ${err instanceof Error ? err.message : err}\n`);
|
|
119
|
-
}
|
|
120
|
-
// -- (2) Token-threshold heartbeat ---------------------------------
|
|
121
|
-
if (payload.transcript_path) {
|
|
122
|
-
try {
|
|
123
|
-
const armed = await checkAndMaybeArm(sessionId, payload.transcript_path);
|
|
124
|
-
if (armed) {
|
|
125
|
-
// Surface to stderr too so the user sees that opensquid noticed
|
|
126
|
-
// drift (in addition to the agent seeing it next turn via UPS).
|
|
127
|
-
process.stderr.write(`🦑 [opensquid heartbeat-armed] ${armed}\n`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
catch (err) {
|
|
131
|
-
process.stderr.write(`[opensquid hook stop] heartbeat check failed (non-fatal): ${err instanceof Error ? err.message : err}\n`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
process.exit(0);
|
|
135
|
-
}
|
|
136
|
-
//# sourceMappingURL=stop.js.map
|
package/dist/hooks/stop.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"stop.js","sourceRoot":"","sources":["../../src.legacy/hooks/stop.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for `readActiveTaskId` — the transcript-walking helper that
|
|
3
|
-
* finds the most-recent TodoWrite in_progress task id. Used by the
|
|
4
|
-
* workflow gate to figure out which task's phase ledger to query.
|
|
5
|
-
*/
|
|
6
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
7
|
-
import * as crypto from "node:crypto";
|
|
8
|
-
import { promises as fs } from "node:fs";
|
|
9
|
-
import * as os from "node:os";
|
|
10
|
-
import * as path from "node:path";
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
-
import { readActiveTaskId } from "./transcript.js";
|
|
13
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
-
let tmpDir;
|
|
15
|
-
let transcriptPath;
|
|
16
|
-
beforeEach(async () => {
|
|
17
|
-
tmpDir = path.join(os.tmpdir(), `opensquid-tx-${crypto.randomUUID()}`);
|
|
18
|
-
await fs.mkdir(tmpDir, { recursive: true });
|
|
19
|
-
transcriptPath = path.join(tmpDir, "transcript.jsonl");
|
|
20
|
-
});
|
|
21
|
-
afterEach(async () => {
|
|
22
|
-
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
23
|
-
});
|
|
24
|
-
async function writeEvents(events) {
|
|
25
|
-
const lines = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
26
|
-
await fs.writeFile(transcriptPath, lines, "utf8");
|
|
27
|
-
}
|
|
28
|
-
function todoWriteEvent(todos) {
|
|
29
|
-
return {
|
|
30
|
-
type: "assistant",
|
|
31
|
-
message: {
|
|
32
|
-
role: "assistant",
|
|
33
|
-
content: [
|
|
34
|
-
{
|
|
35
|
-
type: "tool_use",
|
|
36
|
-
name: "TodoWrite",
|
|
37
|
-
input: { todos },
|
|
38
|
-
},
|
|
39
|
-
],
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
describe("readActiveTaskId", () => {
|
|
44
|
-
it("returns null when transcript doesn't exist", async () => {
|
|
45
|
-
expect(await readActiveTaskId(path.join(tmpDir, "missing.jsonl"))).toBeNull();
|
|
46
|
-
});
|
|
47
|
-
it("returns null when transcript is empty", async () => {
|
|
48
|
-
await fs.writeFile(transcriptPath, "", "utf8");
|
|
49
|
-
expect(await readActiveTaskId(transcriptPath)).toBeNull();
|
|
50
|
-
});
|
|
51
|
-
it("returns null when no TodoWrite block exists", async () => {
|
|
52
|
-
await writeEvents([
|
|
53
|
-
{ type: "user", message: { role: "user", content: "hi" } },
|
|
54
|
-
{ type: "assistant", message: { role: "assistant", content: "hello" } },
|
|
55
|
-
]);
|
|
56
|
-
expect(await readActiveTaskId(transcriptPath)).toBeNull();
|
|
57
|
-
});
|
|
58
|
-
it("returns null when TodoWrite has no in_progress items", async () => {
|
|
59
|
-
await writeEvents([
|
|
60
|
-
todoWriteEvent([
|
|
61
|
-
{ id: "1", status: "completed" },
|
|
62
|
-
{ id: "2", status: "pending" },
|
|
63
|
-
]),
|
|
64
|
-
]);
|
|
65
|
-
expect(await readActiveTaskId(transcriptPath)).toBeNull();
|
|
66
|
-
});
|
|
67
|
-
it("returns the in_progress task id from a single TodoWrite", async () => {
|
|
68
|
-
await writeEvents([
|
|
69
|
-
todoWriteEvent([
|
|
70
|
-
{ id: "1", status: "completed" },
|
|
71
|
-
{ id: "2", status: "in_progress" },
|
|
72
|
-
{ id: "3", status: "pending" },
|
|
73
|
-
]),
|
|
74
|
-
]);
|
|
75
|
-
expect(await readActiveTaskId(transcriptPath)).toBe("2");
|
|
76
|
-
});
|
|
77
|
-
it("prefers the MOST RECENT TodoWrite when multiple exist", async () => {
|
|
78
|
-
await writeEvents([
|
|
79
|
-
todoWriteEvent([{ id: "old-task", status: "in_progress" }]),
|
|
80
|
-
todoWriteEvent([{ id: "newer-task", status: "in_progress" }]),
|
|
81
|
-
]);
|
|
82
|
-
expect(await readActiveTaskId(transcriptPath)).toBe("newer-task");
|
|
83
|
-
});
|
|
84
|
-
it("stops at the most-recent TodoWrite even if its in_progress is null", async () => {
|
|
85
|
-
// The MOST RECENT TodoWrite has no in_progress (all completed).
|
|
86
|
-
// We must NOT fall back to an OLDER TodoWrite's in_progress that
|
|
87
|
-
// may have been overwritten. Returns null.
|
|
88
|
-
await writeEvents([
|
|
89
|
-
todoWriteEvent([{ id: "stale-task", status: "in_progress" }]),
|
|
90
|
-
todoWriteEvent([
|
|
91
|
-
{ id: "stale-task", status: "completed" },
|
|
92
|
-
{ id: "all-done", status: "completed" },
|
|
93
|
-
]),
|
|
94
|
-
]);
|
|
95
|
-
expect(await readActiveTaskId(transcriptPath)).toBeNull();
|
|
96
|
-
});
|
|
97
|
-
it("ignores non-assistant events between TodoWrites", async () => {
|
|
98
|
-
await writeEvents([
|
|
99
|
-
{ type: "user", message: { role: "user", content: "do thing" } },
|
|
100
|
-
todoWriteEvent([{ id: "active", status: "in_progress" }]),
|
|
101
|
-
{ type: "user", message: { role: "user", content: "now go" } },
|
|
102
|
-
]);
|
|
103
|
-
expect(await readActiveTaskId(transcriptPath)).toBe("active");
|
|
104
|
-
});
|
|
105
|
-
it("ignores assistant text events without tool_use blocks", async () => {
|
|
106
|
-
await writeEvents([
|
|
107
|
-
todoWriteEvent([{ id: "real-active", status: "in_progress" }]),
|
|
108
|
-
{
|
|
109
|
-
type: "assistant",
|
|
110
|
-
message: {
|
|
111
|
-
role: "assistant",
|
|
112
|
-
content: [{ type: "text", text: "thinking..." }],
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
]);
|
|
116
|
-
expect(await readActiveTaskId(transcriptPath)).toBe("real-active");
|
|
117
|
-
});
|
|
118
|
-
it("ignores other tool_use events (Bash, Edit, etc.)", async () => {
|
|
119
|
-
await writeEvents([
|
|
120
|
-
todoWriteEvent([{ id: "active", status: "in_progress" }]),
|
|
121
|
-
{
|
|
122
|
-
type: "assistant",
|
|
123
|
-
message: {
|
|
124
|
-
role: "assistant",
|
|
125
|
-
content: [{ type: "tool_use", name: "Bash", input: { command: "ls" } }],
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
]);
|
|
129
|
-
expect(await readActiveTaskId(transcriptPath)).toBe("active");
|
|
130
|
-
});
|
|
131
|
-
it("coerces numeric ids to strings", async () => {
|
|
132
|
-
await writeEvents([
|
|
133
|
-
todoWriteEvent([
|
|
134
|
-
// Some serializations encode id as number.
|
|
135
|
-
{ id: 127, status: "in_progress" },
|
|
136
|
-
]),
|
|
137
|
-
]);
|
|
138
|
-
expect(await readActiveTaskId(transcriptPath)).toBe("127");
|
|
139
|
-
});
|
|
140
|
-
it("handles malformed JSON lines gracefully", async () => {
|
|
141
|
-
await fs.writeFile(transcriptPath, [
|
|
142
|
-
"{ malformed",
|
|
143
|
-
JSON.stringify(todoWriteEvent([{ id: "active", status: "in_progress" }])),
|
|
144
|
-
"still bad json",
|
|
145
|
-
].join("\n"), "utf8");
|
|
146
|
-
expect(await readActiveTaskId(transcriptPath)).toBe("active");
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
// =====================================================================
|
|
150
|
-
// v0.6.2 — TaskCreate + TaskUpdate recognition (the real-world Claude
|
|
151
|
-
// Code shape; TodoWrite was the v0.6.1 shape). My own dogfood session
|
|
152
|
-
// today used TaskCreate/TaskUpdate exclusively → workflow gate silent-
|
|
153
|
-
// allowed every commit because readActiveTaskId only recognized
|
|
154
|
-
// TodoWrite. This block is the regression coverage for the fix.
|
|
155
|
-
// =====================================================================
|
|
156
|
-
function assistantToolUse(name, blockId, input) {
|
|
157
|
-
return {
|
|
158
|
-
type: "assistant",
|
|
159
|
-
message: {
|
|
160
|
-
role: "assistant",
|
|
161
|
-
content: [
|
|
162
|
-
{
|
|
163
|
-
type: "tool_use",
|
|
164
|
-
id: blockId,
|
|
165
|
-
name,
|
|
166
|
-
input,
|
|
167
|
-
caller: { type: "direct" },
|
|
168
|
-
},
|
|
169
|
-
],
|
|
170
|
-
},
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
function toolResult(toolUseId, content) {
|
|
174
|
-
return {
|
|
175
|
-
type: "user",
|
|
176
|
-
message: {
|
|
177
|
-
role: "user",
|
|
178
|
-
content: [
|
|
179
|
-
{
|
|
180
|
-
type: "tool_result",
|
|
181
|
-
tool_use_id: toolUseId,
|
|
182
|
-
content,
|
|
183
|
-
},
|
|
184
|
-
],
|
|
185
|
-
},
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
describe("readActiveTaskId — TaskUpdate (v0.6.2 fix)", () => {
|
|
189
|
-
it("returns the taskId from TaskUpdate(status=in_progress)", async () => {
|
|
190
|
-
await writeEvents([
|
|
191
|
-
assistantToolUse("TaskUpdate", "tu-1", { taskId: "131", status: "in_progress" }),
|
|
192
|
-
]);
|
|
193
|
-
expect(await readActiveTaskId(transcriptPath)).toBe("131");
|
|
194
|
-
});
|
|
195
|
-
it("does not return tasks marked completed by a later TaskUpdate", async () => {
|
|
196
|
-
await writeEvents([
|
|
197
|
-
assistantToolUse("TaskUpdate", "tu-1", { taskId: "131", status: "in_progress" }),
|
|
198
|
-
assistantToolUse("TaskUpdate", "tu-2", { taskId: "131", status: "completed" }),
|
|
199
|
-
]);
|
|
200
|
-
expect(await readActiveTaskId(transcriptPath)).toBeNull();
|
|
201
|
-
});
|
|
202
|
-
it("picks the most-recently-touched in_progress task when multiple are active", async () => {
|
|
203
|
-
await writeEvents([
|
|
204
|
-
assistantToolUse("TaskUpdate", "tu-1", { taskId: "100", status: "in_progress" }),
|
|
205
|
-
assistantToolUse("TaskUpdate", "tu-2", { taskId: "200", status: "in_progress" }),
|
|
206
|
-
assistantToolUse("TaskUpdate", "tu-3", { taskId: "300", status: "in_progress" }),
|
|
207
|
-
]);
|
|
208
|
-
expect(await readActiveTaskId(transcriptPath)).toBe("300");
|
|
209
|
-
});
|
|
210
|
-
it("coerces numeric taskId to string", async () => {
|
|
211
|
-
await writeEvents([
|
|
212
|
-
assistantToolUse("TaskUpdate", "tu-1", { taskId: 131, status: "in_progress" }),
|
|
213
|
-
]);
|
|
214
|
-
expect(await readActiveTaskId(transcriptPath)).toBe("131");
|
|
215
|
-
});
|
|
216
|
-
it("ignores TaskUpdate with deleted status", async () => {
|
|
217
|
-
await writeEvents([
|
|
218
|
-
assistantToolUse("TaskUpdate", "tu-1", { taskId: "131", status: "in_progress" }),
|
|
219
|
-
assistantToolUse("TaskUpdate", "tu-2", { taskId: "131", status: "deleted" }),
|
|
220
|
-
]);
|
|
221
|
-
expect(await readActiveTaskId(transcriptPath)).toBeNull();
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
describe("readActiveTaskId — TaskCreate (v0.6.2 fix)", () => {
|
|
225
|
-
it("does NOT return TaskCreate'd tasks (default status = pending, not in_progress)", async () => {
|
|
226
|
-
// TaskCreate alone leaves the task as pending. Active-task detection
|
|
227
|
-
// requires an explicit TaskUpdate(in_progress) — otherwise no gate
|
|
228
|
-
// for tasks that were created but never started.
|
|
229
|
-
await writeEvents([
|
|
230
|
-
assistantToolUse("TaskCreate", "tc-1", {
|
|
231
|
-
subject: "Some task",
|
|
232
|
-
description: "...",
|
|
233
|
-
}),
|
|
234
|
-
toolResult("tc-1", "Task #131 created successfully: Some task"),
|
|
235
|
-
]);
|
|
236
|
-
expect(await readActiveTaskId(transcriptPath)).toBeNull();
|
|
237
|
-
});
|
|
238
|
-
it("returns the assigned id when TaskCreate is followed by TaskUpdate(in_progress)", async () => {
|
|
239
|
-
await writeEvents([
|
|
240
|
-
assistantToolUse("TaskCreate", "tc-1", { subject: "X", description: "..." }),
|
|
241
|
-
toolResult("tc-1", "Task #131 created successfully: X"),
|
|
242
|
-
assistantToolUse("TaskUpdate", "tu-1", { taskId: "131", status: "in_progress" }),
|
|
243
|
-
]);
|
|
244
|
-
expect(await readActiveTaskId(transcriptPath)).toBe("131");
|
|
245
|
-
});
|
|
246
|
-
it("handles TaskCreate without a matching tool_result (truncated transcript)", async () => {
|
|
247
|
-
await writeEvents([
|
|
248
|
-
assistantToolUse("TaskCreate", "tc-1", { subject: "X", description: "..." }),
|
|
249
|
-
// No tool_result follows
|
|
250
|
-
]);
|
|
251
|
-
// No id assigned, no in_progress → null.
|
|
252
|
-
expect(await readActiveTaskId(transcriptPath)).toBeNull();
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
// =====================================================================
|
|
256
|
-
// Real-world fixture — captured from an actual Claude Code session.
|
|
257
|
-
// The fixture lives at __fixtures__/real-task-shape.jsonl. If Claude
|
|
258
|
-
// Code ever changes the wire format for TaskCreate / TaskUpdate, this
|
|
259
|
-
// test fails BEFORE the workflow gate silently regresses in
|
|
260
|
-
// production. Earlier audit recommendation (v0.6.2 audit MED): synthesized
|
|
261
|
-
// tests passed in v0.6.1 but real-world shape didn't match — the same
|
|
262
|
-
// failure mode would have been caught here.
|
|
263
|
-
// =====================================================================
|
|
264
|
-
describe("readActiveTaskId — real Claude Code transcript fixture", () => {
|
|
265
|
-
it("recognizes TaskCreate + tool_result + TaskUpdate captured from a real session", async () => {
|
|
266
|
-
const fixturePath = path.resolve(__dirname, "__fixtures__", "real-task-shape.jsonl");
|
|
267
|
-
// The fixture is 3 events: TaskCreate "X" → tool_result "Task #1 created" →
|
|
268
|
-
// TaskUpdate(taskId=1, status=in_progress). Expected active task: "1".
|
|
269
|
-
const active = await readActiveTaskId(fixturePath);
|
|
270
|
-
expect(active).toBe("1");
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
// =====================================================================
|
|
274
|
-
// 0.7.9 (#163) — stale in_progress demotion
|
|
275
|
-
// =====================================================================
|
|
276
|
-
function assistantToolUseAt(name, blockId, input, timestamp) {
|
|
277
|
-
return {
|
|
278
|
-
type: "assistant",
|
|
279
|
-
timestamp,
|
|
280
|
-
message: {
|
|
281
|
-
role: "assistant",
|
|
282
|
-
content: [{ type: "tool_use", id: blockId, name, input, caller: { type: "direct" } }],
|
|
283
|
-
},
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
function userEventAt(timestamp, text = "hello") {
|
|
287
|
-
return { type: "user", timestamp, message: { role: "user", content: text } };
|
|
288
|
-
}
|
|
289
|
-
describe("readActiveTaskId — stale-task demotion (#163)", () => {
|
|
290
|
-
const oldDay = "2026-05-16T08:00:00Z"; // ~24h before latest
|
|
291
|
-
const today = "2026-05-17T08:00:00Z"; // latest activity
|
|
292
|
-
it("returns null when the only in_progress task is >1hr stale relative to latest activity", async () => {
|
|
293
|
-
await writeEvents([
|
|
294
|
-
assistantToolUseAt("TaskUpdate", "tu-1", { taskId: "999", status: "in_progress" }, oldDay),
|
|
295
|
-
// Many later events with newer timestamps — none touch task 999.
|
|
296
|
-
userEventAt(today, "new conversation today"),
|
|
297
|
-
]);
|
|
298
|
-
expect(await readActiveTaskId(transcriptPath)).toBeNull();
|
|
299
|
-
});
|
|
300
|
-
it("keeps the in_progress task when it was recently touched", async () => {
|
|
301
|
-
const recent = "2026-05-17T07:30:00Z"; // 30 min before latest
|
|
302
|
-
await writeEvents([
|
|
303
|
-
assistantToolUseAt("TaskUpdate", "tu-1", { taskId: "42", status: "in_progress" }, recent),
|
|
304
|
-
userEventAt(today),
|
|
305
|
-
]);
|
|
306
|
-
expect(await readActiveTaskId(transcriptPath)).toBe("42");
|
|
307
|
-
});
|
|
308
|
-
it("picks the more-recent in_progress when two exist (one stale, one fresh)", async () => {
|
|
309
|
-
const recent = "2026-05-17T07:45:00Z";
|
|
310
|
-
await writeEvents([
|
|
311
|
-
assistantToolUseAt("TaskUpdate", "tu-1", { taskId: "X", status: "in_progress" }, oldDay),
|
|
312
|
-
assistantToolUseAt("TaskUpdate", "tu-2", { taskId: "Y", status: "in_progress" }, recent),
|
|
313
|
-
userEventAt(today),
|
|
314
|
-
]);
|
|
315
|
-
expect(await readActiveTaskId(transcriptPath)).toBe("Y");
|
|
316
|
-
});
|
|
317
|
-
it("falls back to line-idx pick (no demotion) when events have no timestamps", async () => {
|
|
318
|
-
// Pre-existing behavior preserved when timestamps aren't available.
|
|
319
|
-
await writeEvents([
|
|
320
|
-
assistantToolUse("TaskUpdate", "tu-1", { taskId: "no-ts", status: "in_progress" }),
|
|
321
|
-
]);
|
|
322
|
-
expect(await readActiveTaskId(transcriptPath)).toBe("no-ts");
|
|
323
|
-
});
|
|
324
|
-
});
|
|
325
|
-
describe("readActiveTaskId — mixed TodoWrite + TaskUpdate", () => {
|
|
326
|
-
it("latest write wins per id, regardless of which tool", async () => {
|
|
327
|
-
// TodoWrite snapshot says id=5 is in_progress; later TaskUpdate
|
|
328
|
-
// marks id=5 completed. TaskUpdate is later → wins.
|
|
329
|
-
await writeEvents([
|
|
330
|
-
todoWriteEvent([{ id: "5", status: "in_progress" }]),
|
|
331
|
-
assistantToolUse("TaskUpdate", "tu-1", { taskId: "5", status: "completed" }),
|
|
332
|
-
]);
|
|
333
|
-
expect(await readActiveTaskId(transcriptPath)).toBeNull();
|
|
334
|
-
});
|
|
335
|
-
it("TodoWrite snapshot can revive an id that TaskUpdate marked completed if it comes later", async () => {
|
|
336
|
-
await writeEvents([
|
|
337
|
-
assistantToolUse("TaskUpdate", "tu-1", { taskId: "5", status: "completed" }),
|
|
338
|
-
todoWriteEvent([{ id: "5", status: "in_progress" }]),
|
|
339
|
-
]);
|
|
340
|
-
expect(await readActiveTaskId(transcriptPath)).toBe("5");
|
|
341
|
-
});
|
|
342
|
-
});
|