jeo-code 0.7.35 → 0.7.37
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 +28 -0
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/package.json +2 -1
- package/src/agent/AGENTS.md +4 -1
- package/src/agent/approve-tool.ts +42 -0
- package/src/agent/config-schema.ts +17 -0
- package/src/agent/notify/AGENTS.md +47 -0
- package/src/agent/notify/daemon-control.ts +158 -0
- package/src/agent/notify/paths.ts +41 -0
- package/src/agent/notify/session-endpoint.ts +196 -0
- package/src/agent/notify/telegram-api.ts +83 -0
- package/src/agent/notify/telegram-daemon.ts +358 -0
- package/src/agent/state.ts +9 -0
- package/src/agent/task-tool.ts +6 -0
- package/src/agent/tools.ts +19 -42
- package/src/cli/runner.ts +29 -0
- package/src/commands/AGENTS.md +5 -1
- package/src/commands/approve.ts +63 -54
- package/src/commands/daemon.ts +45 -0
- package/src/commands/launch.ts +8 -1
- package/src/commands/notify.ts +109 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
|
|
8
8
|
|
|
9
|
+
## [0.7.37] - 2026-07-05
|
|
10
|
+
_Remote subagent visibility/control over Telegram (gjc Telegram-daemon parity, scoped to jeo's subagent surface only — no forum topics, no inline keyboards, no image attachments; see CHANGELOG 0.7.34 for what jeo intentionally does not replicate from gjc's full notification stack)._
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`jeo notify setup|status`** — pairs a Telegram bot (BotFather token + `getMe` verification, then either an explicit `--chat-id` or an interactive `getUpdates` poll for a private DM) and persists `notifications.enabled`/`notifications.telegram.{botToken,chatId}` in `~/.jeo/config.json`. `status` prints a masked token, the paired chat id, and the daemon's running state.
|
|
14
|
+
- **`jeo daemon status|start|stop|reload`** — manages a singleton background daemon process (`src/agent/notify/daemon-control.ts`): a pid+startedAt lock file at `~/.jeo/notifications/daemon.lock` enforces one owner (Telegram allows only one `getUpdates` long-poll per bot token), `start` self-invokes `jeo notify-daemon-run` detached (mirrors `memory.ts`'s `distillInvocation`/`spawnDetachedDistill` pattern) and refuses early with a clear message when notifications aren't configured yet, `stop` SIGTERMs the owner and waits for exit, `reload` is stop+start.
|
|
15
|
+
- **`src/agent/notify/session-endpoint.ts`** — a per-turn, loopback-only (`127.0.0.1`, random port) WebSocket server exposing ONE `SubagentRegistry` for remote monitoring/control: publishes a token-authed discovery file, pushes a `snapshot` frame on connect and on any subagent-list change, and applies inbound `steer`/`cancel`/`list` requests to the SAME registry instance the `task`/`subagent` tools use. Wired in lazily and best-effort from `task-tool.ts`'s `task {detached:true}` path (`ensureSessionNotifyEndpoint`, a no-op unless `notifications.enabled`) and torn down at the turn boundary from `launch.ts`'s `finally` block (`stopSessionNotifyEndpoint`), symmetric with `subagentRegistry.cancelAll()`.
|
|
16
|
+
- **`src/agent/notify/telegram-daemon.ts`** — the managed daemon: scans `~/.jeo/notifications/sessions/*.json` for live sessions (deletes stale dead-pid discovery files), connects a WebSocket to each, sends a Telegram push on subagent state EDGES only (started → a terminal state — never a re-report of an unchanged status), and long-polls Telegram `getUpdates` to dispatch `/subagents`, `/steer <sessionId> <subagentId> <message>`, `/cancel <sessionId> <subagentId>`, and `/help` back into the matching session over its WebSocket (request/ack round-trip with a 3s timeout).
|
|
17
|
+
- **`src/agent/notify/telegram-api.ts`** — a minimal, injectable-`fetch` Telegram Bot API client (`getMe`/`sendMessage`/`getUpdates`) plus `maskToken` (first 4 chars + length, gjc `gjc notify status` masking parity).
|
|
18
|
+
|
|
19
|
+
### Verified
|
|
20
|
+
- `bun run typecheck` clean; `bun test` 2368 pass / 0 fail, including new suites `notify-paths`, `notify-telegram-api`, `notify-session-endpoint` (real `Bun.serve` + real `WebSocket` client, incl. wrong-token rejection and a real `SubagentRegistry` steer/cancel round-trip), `notify-telegram-daemon` (pure diff/format/parse helpers + a fake-WebSocket/fake-`TelegramApi` `TelegramDaemon`), `notify-daemon-control` (lock singleton refusal against a real live child pid, stale-lock reclaim against a real exited pid, `startDaemon`/`stopDaemon`/`reloadDaemon` lifecycle), `notify-command` and `daemon-command` (CLI layer).
|
|
21
|
+
- Manual end-to-end smoke test: `jeo daemon start` with `notifications.enabled`+fake token/chat id in a scratch `JEO_CONFIG_DIR` really spawns `notify-daemon-run` detached, `jeo daemon status` reports `running (pid …)`, `jeo daemon stop` SIGTERMs it and `jeo daemon status` reports `stopped` with no lingering process (verified via `ps aux`). A first manual run against unconfigured notifications surfaced a real race (the daemon exits almost immediately, racing `startDaemon`'s 2s readiness poll) — fixed by checking `isNotifyConfigured()` BEFORE spawning instead of after.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## [0.7.36] - 2026-07-04
|
|
25
|
+
_Removes two specific guardrails per explicit user direction: `jeo approve`'s human-only identity gate (the agent can now approve its own reviewed `jeo ralplan` plan) and the disk-staleness write/edit clobber guard (a write/edit no longer refuses just because the file changed on disk since the last read). Both removals were scoped narrowly after enumerating every guardrail in the codebase — the catastrophic-command hard block (`rm -rf /`, fork bombs, raw-disk `dd`/`mkfs`), the read-only subagent role tool gating, the deep-interview mutation lock, and the blind-edit (never-read-this-session) guard are all UNCHANGED._
|
|
26
|
+
|
|
27
|
+
### Removed
|
|
28
|
+
- **The `jeo approve` human-only identity gate.** Previously `ralplanState.approved` could only be flipped by a human running `jeo approve <plan-path>` in their own terminal — there was no way for the agent itself to approve a plan it had just drafted and had reviewed pass consensus. `src/commands/approve.ts`'s validation logic (schema shape, known subagent roles, persisted `[OKAY]` consensus verdict, hash-vs-consensus match) is extracted into an exported `approvePlan()` core — unchanged and still enforced no matter who calls it — shared by the `jeo approve` CLI (`runApproveCommand`, now a thin wrapper) and a new agent-facing `approve` tool (`src/agent/approve-tool.ts`). Wired into `launch.ts` the same way as `goal`/`irc` (main interactive session only, not part of `DEFAULT_TOOLS`/`subagentToolset`, so spawned subagents still cannot approve plans).
|
|
29
|
+
- **The disk-staleness write/edit clobber guard (`src/agent/tools.ts`'s `staleReadError`).** A write/edit against a file that changed on disk since the agent's last read is no longer rejected — the agent's write always wins now, even over a concurrent formatter/user/other-agent edit. The distinct blind-edit guard is UNCHANGED: a no-anchor line-range `edit` still requires having read the file at least once this session (`readThisSession`/`markRead`, the presence-only successor to the removed staleness-comparison snapshot map).
|
|
30
|
+
|
|
31
|
+
### Verified
|
|
32
|
+
- `bunx tsc -p tsconfig.json --noEmit` clean.
|
|
33
|
+
- New `test/approve-tool.test.ts` (explicit/defaulted planPath, missing-state error, the shared content gate still refusing an unreviewed/hash-mismatched plan through the tool, idempotent re-approval) and `test/launch-approve-wiring.test.ts` (source-text wiring check, mirrors `engine-computer-wiring.test.ts`'s pattern for module-local closures).
|
|
34
|
+
- `test/edit-freshness.test.ts` updated: the two external-change scenarios now assert the write/edit SUCCEEDS and overwrites, instead of asserting rejection; the never-read-this-session and SEARCH-mismatch-recovery cases (a different guard) are untouched and still pass.
|
|
35
|
+
- `test/approve.test.ts` (CLI-level, all 6 validation/idempotency branches) passes unchanged against the refactored `approvePlan()` core.
|
|
36
|
+
|
|
9
37
|
## [0.7.35] - 2026-07-04
|
|
10
38
|
_`jeo team` now runs every plan step through the SAME subagent execution core as the `task`/`subagent` tools, and gains gjc-style concurrent workers: a plan can mark a contiguous run of independent steps to execute in parallel, each isolated in its own git worktree, merged back in order._
|
|
11
39
|
|
package/README.ja.md
CHANGED
|
@@ -207,11 +207,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
207
207
|
## 変更履歴 (Changelog)
|
|
208
208
|
|
|
209
209
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
210
|
+
- **[0.7.37]** (2026-07-05) — Remote subagent visibility/control over Telegram (gjc Telegram-daemon parity, scoped to jeo's subagent surface only — no forum topics, no inline keyboards, no image attachments; see CHANGELOG 0.7.34 for what jeo intentionally does not replicate from gjc's full notification stack).
|
|
211
|
+
- **[0.7.36]** (2026-07-04) — Removes two specific guardrails per explicit user direction: `jeo approve`'s human-only identity gate (the agent can now approve its own reviewed `jeo ralplan` plan) and the disk-staleness write/edit clobber guard (a write/edit no longer refuses just because the file changed on disk since the last read). Both removals were scoped narrowly after enumerating every guardrail in the codebase — the catastrophic-command hard block (`rm -rf /`, fork bombs, raw-disk `dd`/`mkfs`), the read-only subagent role tool gating, the deep-interview mutation lock, and the blind-edit (never-read-this-session) guard are all UNCHANGED.
|
|
210
212
|
- **[0.7.35]** (2026-07-04) — `jeo team` now runs every plan step through the SAME subagent execution core as the `task`/`subagent` tools, and gains gjc-style concurrent workers: a plan can mark a contiguous run of independent steps to execute in parallel, each isolated in its own git worktree, merged back in order.
|
|
211
213
|
- **[0.7.34]** (2026-07-04) — gajae-code 0.8.0/0.8.1 parity audit: closes a real cross-provider lone-surrogate JSON bug and makes turn-boundary compaction Esc-cancellable; the rest of gjc's 0.8.x release (Telegram daemon, ACP, cmux, coordinator MCP, tool-discovery-mode, an internal paged transcript viewport) is gjc-specific architecture jeo intentionally does not replicate, or was already-equivalent (confirmed by a 4-way parallel audit: ast_grep formatting, session-picker empty-filter clamping, PageUp/PageDown).
|
|
212
214
|
- **[0.7.33]** (2026-07-04) — Adds `browser` — headless Chromium automation (gjc `browser` parity) — completing the gjc tool-surface parity pass started in 0.7.32.
|
|
213
|
-
- **[0.7.32]** (2026-07-03) — gjc tool-surface parity — ast_grep/ast_edit, lsp/lsp_rename, debug, irc, job, and goal tools — plus a `launch.ts` slash-command refactor and four agent-loop guardrail hardenings (destructive-bash denylist, spawn-gate justification quality check, done-verification abuse guard, and a blind unanchored line-range-edit guard).
|
|
214
|
-
- **[0.7.31]** (2026-07-03) — Three-round speed & memory pass across streaming, memory retrieval, and the TUI render loop (O(n²) → O(n) in several hot paths), a deep-interview repeat-question loop guard, and gajae-code managed-tmux launch sizing parity.
|
|
215
215
|
|
|
216
216
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
217
217
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -207,11 +207,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
207
207
|
## 변경 이력 (Changelog)
|
|
208
208
|
|
|
209
209
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
210
|
+
- **[0.7.37]** (2026-07-05) — Remote subagent visibility/control over Telegram (gjc Telegram-daemon parity, scoped to jeo's subagent surface only — no forum topics, no inline keyboards, no image attachments; see CHANGELOG 0.7.34 for what jeo intentionally does not replicate from gjc's full notification stack).
|
|
211
|
+
- **[0.7.36]** (2026-07-04) — Removes two specific guardrails per explicit user direction: `jeo approve`'s human-only identity gate (the agent can now approve its own reviewed `jeo ralplan` plan) and the disk-staleness write/edit clobber guard (a write/edit no longer refuses just because the file changed on disk since the last read). Both removals were scoped narrowly after enumerating every guardrail in the codebase — the catastrophic-command hard block (`rm -rf /`, fork bombs, raw-disk `dd`/`mkfs`), the read-only subagent role tool gating, the deep-interview mutation lock, and the blind-edit (never-read-this-session) guard are all UNCHANGED.
|
|
210
212
|
- **[0.7.35]** (2026-07-04) — `jeo team` now runs every plan step through the SAME subagent execution core as the `task`/`subagent` tools, and gains gjc-style concurrent workers: a plan can mark a contiguous run of independent steps to execute in parallel, each isolated in its own git worktree, merged back in order.
|
|
211
213
|
- **[0.7.34]** (2026-07-04) — gajae-code 0.8.0/0.8.1 parity audit: closes a real cross-provider lone-surrogate JSON bug and makes turn-boundary compaction Esc-cancellable; the rest of gjc's 0.8.x release (Telegram daemon, ACP, cmux, coordinator MCP, tool-discovery-mode, an internal paged transcript viewport) is gjc-specific architecture jeo intentionally does not replicate, or was already-equivalent (confirmed by a 4-way parallel audit: ast_grep formatting, session-picker empty-filter clamping, PageUp/PageDown).
|
|
212
214
|
- **[0.7.33]** (2026-07-04) — Adds `browser` — headless Chromium automation (gjc `browser` parity) — completing the gjc tool-surface parity pass started in 0.7.32.
|
|
213
|
-
- **[0.7.32]** (2026-07-03) — gjc tool-surface parity — ast_grep/ast_edit, lsp/lsp_rename, debug, irc, job, and goal tools — plus a `launch.ts` slash-command refactor and four agent-loop guardrail hardenings (destructive-bash denylist, spawn-gate justification quality check, done-verification abuse guard, and a blind unanchored line-range-edit guard).
|
|
214
|
-
- **[0.7.31]** (2026-07-03) — Three-round speed & memory pass across streaming, memory retrieval, and the TUI render loop (O(n²) → O(n) in several hot paths), a deep-interview repeat-question loop guard, and gajae-code managed-tmux launch sizing parity.
|
|
215
215
|
|
|
216
216
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
217
217
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -234,11 +234,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
234
234
|
## Changelog
|
|
235
235
|
|
|
236
236
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
237
|
+
- **[0.7.37]** (2026-07-05) — Remote subagent visibility/control over Telegram (gjc Telegram-daemon parity, scoped to jeo's subagent surface only — no forum topics, no inline keyboards, no image attachments; see CHANGELOG 0.7.34 for what jeo intentionally does not replicate from gjc's full notification stack).
|
|
238
|
+
- **[0.7.36]** (2026-07-04) — Removes two specific guardrails per explicit user direction: `jeo approve`'s human-only identity gate (the agent can now approve its own reviewed `jeo ralplan` plan) and the disk-staleness write/edit clobber guard (a write/edit no longer refuses just because the file changed on disk since the last read). Both removals were scoped narrowly after enumerating every guardrail in the codebase — the catastrophic-command hard block (`rm -rf /`, fork bombs, raw-disk `dd`/`mkfs`), the read-only subagent role tool gating, the deep-interview mutation lock, and the blind-edit (never-read-this-session) guard are all UNCHANGED.
|
|
237
239
|
- **[0.7.35]** (2026-07-04) — `jeo team` now runs every plan step through the SAME subagent execution core as the `task`/`subagent` tools, and gains gjc-style concurrent workers: a plan can mark a contiguous run of independent steps to execute in parallel, each isolated in its own git worktree, merged back in order.
|
|
238
240
|
- **[0.7.34]** (2026-07-04) — gajae-code 0.8.0/0.8.1 parity audit: closes a real cross-provider lone-surrogate JSON bug and makes turn-boundary compaction Esc-cancellable; the rest of gjc's 0.8.x release (Telegram daemon, ACP, cmux, coordinator MCP, tool-discovery-mode, an internal paged transcript viewport) is gjc-specific architecture jeo intentionally does not replicate, or was already-equivalent (confirmed by a 4-way parallel audit: ast_grep formatting, session-picker empty-filter clamping, PageUp/PageDown).
|
|
239
241
|
- **[0.7.33]** (2026-07-04) — Adds `browser` — headless Chromium automation (gjc `browser` parity) — completing the gjc tool-surface parity pass started in 0.7.32.
|
|
240
|
-
- **[0.7.32]** (2026-07-03) — gjc tool-surface parity — ast_grep/ast_edit, lsp/lsp_rename, debug, irc, job, and goal tools — plus a `launch.ts` slash-command refactor and four agent-loop guardrail hardenings (destructive-bash denylist, spawn-gate justification quality check, done-verification abuse guard, and a blind unanchored line-range-edit guard).
|
|
241
|
-
- **[0.7.31]** (2026-07-03) — Three-round speed & memory pass across streaming, memory retrieval, and the TUI render loop (O(n²) → O(n) in several hot paths), a deep-interview repeat-question loop guard, and gajae-code managed-tmux launch sizing parity.
|
|
242
242
|
|
|
243
243
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
244
244
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -207,11 +207,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
207
207
|
## 更新日志 (Changelog)
|
|
208
208
|
|
|
209
209
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
210
|
+
- **[0.7.37]** (2026-07-05) — Remote subagent visibility/control over Telegram (gjc Telegram-daemon parity, scoped to jeo's subagent surface only — no forum topics, no inline keyboards, no image attachments; see CHANGELOG 0.7.34 for what jeo intentionally does not replicate from gjc's full notification stack).
|
|
211
|
+
- **[0.7.36]** (2026-07-04) — Removes two specific guardrails per explicit user direction: `jeo approve`'s human-only identity gate (the agent can now approve its own reviewed `jeo ralplan` plan) and the disk-staleness write/edit clobber guard (a write/edit no longer refuses just because the file changed on disk since the last read). Both removals were scoped narrowly after enumerating every guardrail in the codebase — the catastrophic-command hard block (`rm -rf /`, fork bombs, raw-disk `dd`/`mkfs`), the read-only subagent role tool gating, the deep-interview mutation lock, and the blind-edit (never-read-this-session) guard are all UNCHANGED.
|
|
210
212
|
- **[0.7.35]** (2026-07-04) — `jeo team` now runs every plan step through the SAME subagent execution core as the `task`/`subagent` tools, and gains gjc-style concurrent workers: a plan can mark a contiguous run of independent steps to execute in parallel, each isolated in its own git worktree, merged back in order.
|
|
211
213
|
- **[0.7.34]** (2026-07-04) — gajae-code 0.8.0/0.8.1 parity audit: closes a real cross-provider lone-surrogate JSON bug and makes turn-boundary compaction Esc-cancellable; the rest of gjc's 0.8.x release (Telegram daemon, ACP, cmux, coordinator MCP, tool-discovery-mode, an internal paged transcript viewport) is gjc-specific architecture jeo intentionally does not replicate, or was already-equivalent (confirmed by a 4-way parallel audit: ast_grep formatting, session-picker empty-filter clamping, PageUp/PageDown).
|
|
212
214
|
- **[0.7.33]** (2026-07-04) — Adds `browser` — headless Chromium automation (gjc `browser` parity) — completing the gjc tool-surface parity pass started in 0.7.32.
|
|
213
|
-
- **[0.7.32]** (2026-07-03) — gjc tool-surface parity — ast_grep/ast_edit, lsp/lsp_rename, debug, irc, job, and goal tools — plus a `launch.ts` slash-command refactor and four agent-loop guardrail hardenings (destructive-bash denylist, spawn-gate justification quality check, done-verification abuse guard, and a blind unanchored line-range-edit guard).
|
|
214
|
-
- **[0.7.31]** (2026-07-03) — Three-round speed & memory pass across streaming, memory retrieval, and the TUI render loop (O(n²) → O(n) in several hot paths), a deep-interview repeat-question loop guard, and gajae-code managed-tmux launch sizing parity.
|
|
215
215
|
|
|
216
216
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
217
217
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/agent/AGENTS.md
CHANGED
|
@@ -43,6 +43,7 @@ The core runtime loop, tool registry, session management, and state persistence
|
|
|
43
43
|
| `state.ts` | File-backed state and session persistence (`.jeo/state/`) |
|
|
44
44
|
| `step-budget.ts` | gjc-style flexible step budgeting: progress-scored extensions, hard cap, fail-fast |
|
|
45
45
|
| `irc-tool.ts` | `irc` tool — parent/peer live messaging into running DETACHED subagents (list peers, send to one id or "all"); built entirely on `SubagentRegistry.steer()`, gjc `irc` parity |
|
|
46
|
+
| `approve-tool.ts` | `approve` tool — lets the agent flip a `jeo ralplan` plan's `approved` flag itself (wraps `src/commands/approve.ts`'s `approvePlan` content gate: schema shape, known roles, persisted [OKAY] consensus, hash-vs-consensus match). 2026-07: the prior human-only identity gate (only `jeo approve` run by a human could approve) was removed per explicit user direction; launch.ts-only (main interactive session), not part of `DEFAULT_TOOLS`/`subagentToolset`, same as `goal`/`irc` |
|
|
46
47
|
| `job-registry.ts` | In-process registry for BACKGROUND shell processes spawned via `job {action:"start"}` — real parallel OS processes (not just concurrent JS), bounded output buffer, list/tail/awaitIds/cancel, gjc `job`/async-bash parity |
|
|
47
48
|
| `job-tool.ts` | `job` control tool — start/list/tail/await/cancel background shell processes tracked by `job-registry.ts` |
|
|
48
49
|
| `subagent-registry.ts` | Brief description of purpose |
|
|
@@ -52,13 +53,15 @@ The core runtime loop, tool registry, session management, and state persistence
|
|
|
52
53
|
| `todo-tool.ts` | Brief description of purpose |
|
|
53
54
|
| `tokenizer.ts` | Brief description of purpose |
|
|
54
55
|
| `tool-output.ts` | Brief description of purpose |
|
|
55
|
-
| `tools.ts` | Built-in tool definitions (bash, read, write, edit, etc.) |
|
|
56
|
+
| `tools.ts` | Built-in tool definitions (bash, read, write, edit, etc.). 2026-07: the disk-staleness clobber guard (rejecting a write/edit when the file changed on disk since the agent's last read) was removed per explicit user direction — writes always win now. The distinct blind-edit guard (`readThisSession`/`markRead`: a no-anchor line-range `edit` still requires having read the file THIS session at least once) is unchanged |
|
|
56
57
|
| `web-search.ts` | Brief description of purpose |
|
|
57
58
|
|
|
58
59
|
## Subdirectories
|
|
59
60
|
| Directory | Purpose |
|
|
60
61
|
|-----------|---------|
|
|
61
62
|
| `dev/` | Developer-specific agent tooling and spec automation (see `dev/AGENTS.md`) |
|
|
63
|
+
| `notify/` | Remote subagent visibility/control over Telegram (gjc Telegram-daemon parity, scoped to subagents) — session-endpoint loopback WebSocket, the managed daemon, daemon lifecycle control, path helpers, and a minimal Telegram Bot API client |
|
|
64
|
+
|
|
62
65
|
|
|
63
66
|
## For AI Agents
|
|
64
67
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `approve` tool — lets the agent itself flip a `jeo ralplan` plan's `approved`
|
|
3
|
+
* flag so `jeo team`/`jeo ultragoal` can execute it, wrapping the exact same
|
|
4
|
+
* content gate the `jeo approve` CLI command enforces (src/commands/approve.ts's
|
|
5
|
+
* `approvePlan`): schema shape, known subagent roles, a persisted [OKAY]
|
|
6
|
+
* consensus verdict, and a hash match against what the consensus critic
|
|
7
|
+
* reviewed. 2026-07: previously this flag could ONLY be flipped by a human
|
|
8
|
+
* running `jeo approve <path>` in their own terminal — that identity gate is
|
|
9
|
+
* removed per explicit user direction (gjc-style: the agent can act on its own
|
|
10
|
+
* plans). The plan-QUALITY gate above is unchanged and still refuses a
|
|
11
|
+
* schema-invalid, unreviewed, or post-review-edited plan no matter who calls it.
|
|
12
|
+
*/
|
|
13
|
+
import type { ToolHandler } from "./engine";
|
|
14
|
+
import type { ToolResult } from "./tools";
|
|
15
|
+
import { readWorkflowState } from "./state";
|
|
16
|
+
import { approvePlan } from "../commands/approve";
|
|
17
|
+
|
|
18
|
+
/** One-line protocol description appended to the launch system prompt. */
|
|
19
|
+
export const APPROVE_TOOL_PROTOCOL_LINE =
|
|
20
|
+
`approve {planPath?} approves the active 'jeo ralplan' plan (planPath defaults to the active plan ` +
|
|
21
|
+
`from ralplan state when omitted) so 'jeo team'/'jeo ultragoal' can execute it next. Refuses a plan ` +
|
|
22
|
+
`that is schema-invalid, references an unknown role, lacks a persisted [OKAY] consensus verdict, or ` +
|
|
23
|
+
`was edited after the consensus critic reviewed it — same content gate as the 'jeo approve' CLI command.`;
|
|
24
|
+
|
|
25
|
+
export function createApproveTool(): ToolHandler {
|
|
26
|
+
return async (args: Record<string, any>, cwd: string): Promise<ToolResult> => {
|
|
27
|
+
let planPath = typeof args.planPath === "string" ? args.planPath.trim() : "";
|
|
28
|
+
if (!planPath) {
|
|
29
|
+
const ralplanState = await readWorkflowState("ralplan", cwd);
|
|
30
|
+
planPath = ralplanState?.plan_path ?? "";
|
|
31
|
+
}
|
|
32
|
+
if (!planPath) {
|
|
33
|
+
return {
|
|
34
|
+
success: false,
|
|
35
|
+
output: "",
|
|
36
|
+
error: `approve requires a 'planPath' (no active ralplan plan found to default to). Run 'jeo ralplan' first, or pass the plan path explicitly.`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const { ok, message } = await approvePlan(planPath, cwd);
|
|
40
|
+
return ok ? { success: true, output: message } : { success: false, output: "", error: message };
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -84,6 +84,23 @@ export const ConfigSchema = z
|
|
|
84
84
|
onAsk: z.boolean().optional(),
|
|
85
85
|
})
|
|
86
86
|
.optional(),
|
|
87
|
+
/** Remote subagent visibility/control over Telegram (gjc Telegram-daemon
|
|
88
|
+
* parity, scoped to subagents — see `src/agent/notify/`). `enabled` is the
|
|
89
|
+
* master toggle; a session only starts publishing its loopback endpoint
|
|
90
|
+
* when this is true, and `jeo notify-daemon-run` refuses to poll Telegram
|
|
91
|
+
* without a stored botToken + chatId. */
|
|
92
|
+
notifications: z
|
|
93
|
+
.object({
|
|
94
|
+
enabled: z.boolean().optional(),
|
|
95
|
+
telegram: z
|
|
96
|
+
.object({
|
|
97
|
+
botToken: z.string().optional(),
|
|
98
|
+
chatId: z.string().optional(),
|
|
99
|
+
})
|
|
100
|
+
.optional(),
|
|
101
|
+
})
|
|
102
|
+
.optional(),
|
|
103
|
+
|
|
87
104
|
/** Root path of the global llm-wiki vault, shared across every session
|
|
88
105
|
* regardless of project/cwd. A leading `~` is expanded; env `JEO_WIKI_ROOT`
|
|
89
106
|
* overrides. Consumed by `resolveWikiRoot` and injected into the prompt. */
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!-- Parent: ../AGENTS.md -->
|
|
2
|
+
<!-- Generated: 2026-07-05 -->
|
|
3
|
+
|
|
4
|
+
# notify
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
Remote subagent visibility/control over Telegram (gjc Telegram-daemon parity, deliberately scoped to jeo's subagent surface only — no forum topics, inline keyboards, or image attachments; see root `CHANGELOG.md` 0.7.34 for what jeo intentionally does not replicate from gjc's full notification stack).
|
|
8
|
+
|
|
9
|
+
## Key Files
|
|
10
|
+
| File | Description |
|
|
11
|
+
|------|-------------|
|
|
12
|
+
| `paths.ts` | Path helpers under `<jeoHome>/notifications` (daemon lock/log, per-session discovery files) |
|
|
13
|
+
| `telegram-api.ts` | Minimal, injectable-`fetch` Telegram Bot API client (`getMe`/`sendMessage`/`getUpdates`) + `maskToken` |
|
|
14
|
+
| `session-endpoint.ts` | Per-turn, loopback-only WebSocket server exposing ONE `SubagentRegistry` (discovery file + `snapshot`/`steer`/`cancel`/`list` protocol); lazy `ensureSessionNotifyEndpoint`/`stopSessionNotifyEndpoint` wiring, no-op unless `notifications.enabled` |
|
|
15
|
+
| `daemon-control.ts` | Daemon lifecycle: pid+startedAt lock singleton, `daemonInvocation` self-spawn argv (mirrors `memory.ts`'s `distillInvocation`), `startDaemon`/`stopDaemon`/`reloadDaemon`/`daemonStatus` |
|
|
16
|
+
| `telegram-daemon.ts` | The managed daemon (`TelegramDaemon` class + `runNotifyDaemonForeground` CLI entrypoint): scans session discovery files, connects a WS per session, pushes Telegram messages on subagent status EDGES only, and dispatches inbound `/subagents`/`/steer`/`/cancel`/`/help` commands back over the matching session's WebSocket |
|
|
17
|
+
|
|
18
|
+
## Subdirectories
|
|
19
|
+
*(None)*
|
|
20
|
+
|
|
21
|
+
## For AI Agents
|
|
22
|
+
|
|
23
|
+
### Working In This Directory
|
|
24
|
+
- The session-side WebSocket server MUST stay loopback-only (`127.0.0.1`); the daemon-to-Telegram hop is the only internet-facing part of this feature.
|
|
25
|
+
- Every entrypoint here must stay a no-op (or refuse cleanly with a clear message) unless `notifications.enabled` + a stored bot token + chat id are all present — never assume the feature is configured.
|
|
26
|
+
- `daemon-control.ts`'s singleton lock exists because Telegram allows only ONE `getUpdates` long-poll owner per bot token; never bypass it.
|
|
27
|
+
|
|
28
|
+
### Testing Requirements
|
|
29
|
+
- Prefer real `Bun.serve`/`WebSocket` for `session-endpoint.ts` tests (fast, no mocking needed) and injected fakes (`WebSocketImpl`, `TelegramApi`, `readdir`/`readFile`/`unlink`/`isPidAlive`) for `telegram-daemon.ts`/`daemon-control.ts`.
|
|
30
|
+
- Any test touching `readGlobalConfig`/`saveConfigPatch` must isolate `JEO_CONFIG_DIR` to a temp directory (see `test/notify-daemon-control.test.ts`).
|
|
31
|
+
|
|
32
|
+
### Common Patterns
|
|
33
|
+
- Dependency injection for network/fs/time so daemon logic is unit-testable without a real bot token or real child processes.
|
|
34
|
+
- Edge-triggered (not level-triggered) status reporting: a message is sent on a TRANSITION, never on every poll tick.
|
|
35
|
+
|
|
36
|
+
## Dependencies
|
|
37
|
+
|
|
38
|
+
### Internal
|
|
39
|
+
- `../subagent-registry.ts` (`SubagentRegistry`, `SubagentRecord`).
|
|
40
|
+
- `../state.ts` (`readGlobalConfig`/`saveConfigPatch` for `notifications` settings).
|
|
41
|
+
- Consumed by `../task-tool.ts` (`ensureSessionNotifyEndpoint`), `../../commands/launch.ts` (`stopSessionNotifyEndpoint`), `../../commands/notify.ts`, `../../commands/daemon.ts`, and the hidden `notify-daemon-run` CLI entrypoint (`src/cli/runner.ts`).
|
|
42
|
+
|
|
43
|
+
### External
|
|
44
|
+
- Telegram Bot HTTP API (`https://api.telegram.org`) — the only outbound network call in this feature.
|
|
45
|
+
- Bun's native `Bun.serve` (WebSocket server) and global `WebSocket` (client) — zero added dependencies.
|
|
46
|
+
|
|
47
|
+
<!-- MANUAL: Any manually added notes below this line are preserved on regeneration -->
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon lifecycle control (gjc `daemon` command / `control-types.ts` parity,
|
|
3
|
+
* scoped to jeo's one daemon kind — telegram). A pid+startedAt lock file at
|
|
4
|
+
* `notifyDaemonLockPath()` enforces the singleton: Telegram allows only one
|
|
5
|
+
* `getUpdates` long-poll owner per bot token, so a second daemon process must
|
|
6
|
+
* refuse to start rather than race the first for updates (409 Conflict).
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "node:fs/promises";
|
|
9
|
+
import * as fsSync from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
12
|
+
import { notifyDaemonLockPath, notifyDaemonLogPath, notifyDir } from "./paths";
|
|
13
|
+
import { readGlobalConfig } from "../state";
|
|
14
|
+
|
|
15
|
+
export interface DaemonLockInfo {
|
|
16
|
+
pid: number;
|
|
17
|
+
startedAt: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isPidAlive(pid: number): boolean {
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0);
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function readDaemonLock(): Promise<DaemonLockInfo | undefined> {
|
|
30
|
+
try {
|
|
31
|
+
const raw = await fs.readFile(notifyDaemonLockPath(), "utf-8");
|
|
32
|
+
const parsed = JSON.parse(raw) as Partial<DaemonLockInfo>;
|
|
33
|
+
if (typeof parsed.pid !== "number" || typeof parsed.startedAt !== "number") return undefined;
|
|
34
|
+
return { pid: parsed.pid, startedAt: parsed.startedAt };
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Called by the daemon process itself on startup. Returns `undefined` when a
|
|
41
|
+
* live owner already holds the lock (caller must exit without polling
|
|
42
|
+
* Telegram); otherwise writes the lock and returns a `release()` closure. */
|
|
43
|
+
export async function acquireDaemonLock(): Promise<{ release: () => Promise<void> } | undefined> {
|
|
44
|
+
await fs.mkdir(notifyDir(), { recursive: true, mode: 0o700 });
|
|
45
|
+
const existing = await readDaemonLock();
|
|
46
|
+
if (existing && existing.pid !== process.pid && isPidAlive(existing.pid)) return undefined;
|
|
47
|
+
const info: DaemonLockInfo = { pid: process.pid, startedAt: Date.now() };
|
|
48
|
+
await fs.writeFile(notifyDaemonLockPath(), JSON.stringify(info), { mode: 0o600 });
|
|
49
|
+
return {
|
|
50
|
+
release: async () => {
|
|
51
|
+
await fs.unlink(notifyDaemonLockPath()).catch(() => {});
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface DaemonStatus {
|
|
57
|
+
/** `notifications.enabled` + a stored bot token + chat id are all present. */
|
|
58
|
+
configured: boolean;
|
|
59
|
+
running: boolean;
|
|
60
|
+
/** A lock file exists but its pid is dead — a previous daemon crashed without cleanup. */
|
|
61
|
+
stale: boolean;
|
|
62
|
+
pid?: number;
|
|
63
|
+
startedAt?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function isNotifyConfigured(): Promise<boolean> {
|
|
67
|
+
const config = await readGlobalConfig();
|
|
68
|
+
return Boolean(config.notifications?.enabled && config.notifications.telegram?.botToken && config.notifications.telegram?.chatId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function daemonStatus(): Promise<DaemonStatus> {
|
|
72
|
+
const [lock, configured] = await Promise.all([readDaemonLock(), isNotifyConfigured()]);
|
|
73
|
+
if (!lock) return { configured, running: false, stale: false };
|
|
74
|
+
const alive = isPidAlive(lock.pid);
|
|
75
|
+
return { configured, running: alive, stale: !alive, pid: lock.pid, startedAt: lock.startedAt };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Self-invocation argv for the daemon child (mirrors `memory.ts`'s
|
|
79
|
+
* `distillInvocation` — compiled `/$bunfs` virtual path → run the binary
|
|
80
|
+
* itself; `.ts`/`.js` source → through the runtime; anything else → directly). */
|
|
81
|
+
export function daemonInvocation(argv1: string | undefined, execPath: string, cwd: string): string[] {
|
|
82
|
+
const entrypoint = argv1 ?? "";
|
|
83
|
+
let base: string[];
|
|
84
|
+
if (entrypoint === "" || entrypoint.startsWith("/$bunfs/") || entrypoint.startsWith("B:\\~BUN\\")) {
|
|
85
|
+
base = [execPath];
|
|
86
|
+
} else {
|
|
87
|
+
const resolved = path.isAbsolute(entrypoint) ? entrypoint : path.resolve(cwd, entrypoint);
|
|
88
|
+
base = /\.(ts|js|mjs)$/.test(entrypoint) ? [execPath, resolved] : [resolved];
|
|
89
|
+
}
|
|
90
|
+
return [...base, "notify-daemon-run"];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type SpawnLike = (cmd: string[], cwd: string) => { unref(): void };
|
|
94
|
+
|
|
95
|
+
const defaultSpawn: SpawnLike = (cmd, cwd) => {
|
|
96
|
+
fsSync.mkdirSync(notifyDir(), { recursive: true, mode: 0o700 });
|
|
97
|
+
const logFd = fsSync.openSync(notifyDaemonLogPath(), "a");
|
|
98
|
+
// node:child_process with detached:true (NOT Bun.spawn) — same rationale as
|
|
99
|
+
// memory.ts's spawnDetachedDistill: the child needs its own session/process
|
|
100
|
+
// group so a closing terminal/tmux pane does not kill it before it's ready.
|
|
101
|
+
const child = nodeSpawn(cmd[0]!, cmd.slice(1), { cwd, detached: true, stdio: ["ignore", logFd, logFd] });
|
|
102
|
+
return { unref: () => child.unref() };
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
async function waitFor(predicate: () => Promise<boolean>, timeoutMs: number, stepMs = 100): Promise<boolean> {
|
|
106
|
+
const deadline = Date.now() + timeoutMs;
|
|
107
|
+
while (Date.now() < deadline) {
|
|
108
|
+
if (await predicate()) return true;
|
|
109
|
+
await new Promise(r => setTimeout(r, stepMs));
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function startDaemon(spawnImpl: SpawnLike = defaultSpawn): Promise<{ ok: boolean; pid?: number; message: string }> {
|
|
115
|
+
const existing = await readDaemonLock();
|
|
116
|
+
if (existing && isPidAlive(existing.pid)) {
|
|
117
|
+
return { ok: true, pid: existing.pid, message: `daemon already running (pid ${existing.pid})` };
|
|
118
|
+
}
|
|
119
|
+
// Check BEFORE spawning: an unconfigured daemon exits almost immediately (see
|
|
120
|
+
// `runNotifyDaemonForeground`), which races the readiness poll below and used to
|
|
121
|
+
// surface a misleading "did not report ready" — refuse early with a clear message.
|
|
122
|
+
if (!(await isNotifyConfigured())) {
|
|
123
|
+
return { ok: false, message: "notifications not configured — run 'jeo notify setup' first." };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const cmd = daemonInvocation(process.argv[1], process.execPath, process.cwd());
|
|
127
|
+
spawnImpl(cmd, process.cwd()).unref();
|
|
128
|
+
const ready = await waitFor(async () => {
|
|
129
|
+
const lock = await readDaemonLock();
|
|
130
|
+
return Boolean(lock && isPidAlive(lock.pid));
|
|
131
|
+
}, 2_000);
|
|
132
|
+
if (!ready) return { ok: false, message: `daemon did not report ready within 2s — check ${notifyDaemonLogPath()}` };
|
|
133
|
+
const lock = await readDaemonLock();
|
|
134
|
+
return { ok: true, pid: lock?.pid, message: `daemon started (pid ${lock?.pid})` };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function stopDaemon(): Promise<{ ok: boolean; message: string }> {
|
|
138
|
+
const lock = await readDaemonLock();
|
|
139
|
+
if (!lock || !isPidAlive(lock.pid)) {
|
|
140
|
+
await fs.unlink(notifyDaemonLockPath()).catch(() => {});
|
|
141
|
+
return { ok: true, message: "daemon was not running" };
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
process.kill(lock.pid, "SIGTERM");
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return { ok: false, message: `failed to signal pid ${lock.pid}: ${err instanceof Error ? err.message : String(err)}` };
|
|
147
|
+
}
|
|
148
|
+
const stopped = await waitFor(async () => !isPidAlive(lock.pid), 3_000);
|
|
149
|
+
if (!stopped) return { ok: false, message: `daemon (pid ${lock.pid}) did not exit within 3s` };
|
|
150
|
+
await fs.unlink(notifyDaemonLockPath()).catch(() => {});
|
|
151
|
+
return { ok: true, message: `daemon stopped (was pid ${lock.pid})` };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function reloadDaemon(spawnImpl: SpawnLike = defaultSpawn): Promise<{ ok: boolean; message: string }> {
|
|
155
|
+
const stopRes = await stopDaemon();
|
|
156
|
+
const startRes = await startDaemon(spawnImpl);
|
|
157
|
+
return { ok: startRes.ok, message: `${stopRes.message}; ${startRes.message}` };
|
|
158
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path helpers for the local Telegram notification daemon (gjc `notifications/`
|
|
3
|
+
* daemon-paths parity, scoped to jeo's single daemon kind). Everything lives under
|
|
4
|
+
* `<jeoHome>/notifications` — the SAME root `~/.jeo` (or `JEO_CONFIG_DIR`) used by
|
|
5
|
+
* `state.ts` for `config.json`, so one override env var moves both.
|
|
6
|
+
*/
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import { jeoEnv } from "../../util/env";
|
|
10
|
+
|
|
11
|
+
/** Root directory for all local jeo state, resolved at call time (not import
|
|
12
|
+
* time) so a `JEO_CONFIG_DIR` override or runtime `HOME` change is honored. */
|
|
13
|
+
export function jeoHomeDir(): string {
|
|
14
|
+
return jeoEnv("CONFIG_DIR") || path.join(os.homedir(), ".jeo");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Notification/daemon state root. */
|
|
18
|
+
export function notifyDir(): string {
|
|
19
|
+
return path.join(jeoHomeDir(), "notifications");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Per-process session discovery files live here; the daemon scans this directory
|
|
23
|
+
* for live sessions (`<sessionId>.json`), same shape as gjc's `readEndpoint`/
|
|
24
|
+
* `scanRoots` (a loopback WebSocket URL + auth token per session). */
|
|
25
|
+
export function notifySessionsDir(): string {
|
|
26
|
+
return path.join(notifyDir(), "sessions");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function notifySessionEndpointPath(sessionId: string): string {
|
|
30
|
+
return path.join(notifySessionsDir(), `${sessionId}.json`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Singleton lock for the daemon process (Telegram allows only one `getUpdates`
|
|
34
|
+
* long-poll owner per bot token — gjc's rationale for a daemon lock/state file). */
|
|
35
|
+
export function notifyDaemonLockPath(): string {
|
|
36
|
+
return path.join(notifyDir(), "daemon.lock");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function notifyDaemonLogPath(): string {
|
|
40
|
+
return path.join(notifyDir(), "daemon.log");
|
|
41
|
+
}
|