@songsid/agend 2.1.0-beta.4 → 2.1.0-beta.41

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.
Files changed (131) hide show
  1. package/README.md +1 -0
  2. package/README.zh-TW.md +2 -1
  3. package/dist/access-path.js +3 -3
  4. package/dist/access-path.js.map +1 -1
  5. package/dist/agent-cli.js +1 -1
  6. package/dist/agent-cli.js.map +1 -1
  7. package/dist/agent-endpoint.d.ts +8 -0
  8. package/dist/agent-endpoint.js +36 -8
  9. package/dist/agent-endpoint.js.map +1 -1
  10. package/dist/backend/antigravity.d.ts +6 -0
  11. package/dist/backend/antigravity.js +34 -3
  12. package/dist/backend/antigravity.js.map +1 -1
  13. package/dist/backend/claude-code.d.ts +6 -0
  14. package/dist/backend/claude-code.js +20 -4
  15. package/dist/backend/claude-code.js.map +1 -1
  16. package/dist/backend/codex.d.ts +4 -0
  17. package/dist/backend/codex.js +5 -0
  18. package/dist/backend/codex.js.map +1 -1
  19. package/dist/backend/factory.js +4 -1
  20. package/dist/backend/factory.js.map +1 -1
  21. package/dist/backend/grok.d.ts +57 -0
  22. package/dist/backend/grok.js +306 -0
  23. package/dist/backend/grok.js.map +1 -0
  24. package/dist/backend/kiro.d.ts +6 -0
  25. package/dist/backend/kiro.js +36 -3
  26. package/dist/backend/kiro.js.map +1 -1
  27. package/dist/backend/opencode.d.ts +4 -0
  28. package/dist/backend/opencode.js +5 -0
  29. package/dist/backend/opencode.js.map +1 -1
  30. package/dist/backend/types.d.ts +59 -3
  31. package/dist/backend/types.js +11 -0
  32. package/dist/backend/types.js.map +1 -1
  33. package/dist/channel/adapters/discord.d.ts +20 -0
  34. package/dist/channel/adapters/discord.js +89 -11
  35. package/dist/channel/adapters/discord.js.map +1 -1
  36. package/dist/channel/adapters/telegram.js +4 -1
  37. package/dist/channel/adapters/telegram.js.map +1 -1
  38. package/dist/channel/mcp-tools.js +18 -2
  39. package/dist/channel/mcp-tools.js.map +1 -1
  40. package/dist/chat-export.js +5 -2
  41. package/dist/chat-export.js.map +1 -1
  42. package/dist/classic-channel-manager.d.ts +16 -1
  43. package/dist/classic-channel-manager.js +76 -4
  44. package/dist/classic-channel-manager.js.map +1 -1
  45. package/dist/cli.js +116 -19
  46. package/dist/cli.js.map +1 -1
  47. package/dist/config-validator.js +70 -1
  48. package/dist/config-validator.js.map +1 -1
  49. package/dist/config.d.ts +3 -1
  50. package/dist/config.js +14 -1
  51. package/dist/config.js.map +1 -1
  52. package/dist/context-guardian.d.ts +2 -0
  53. package/dist/context-guardian.js +10 -2
  54. package/dist/context-guardian.js.map +1 -1
  55. package/dist/daemon-entry.js +2 -1
  56. package/dist/daemon-entry.js.map +1 -1
  57. package/dist/daemon.d.ts +91 -6
  58. package/dist/daemon.js +602 -186
  59. package/dist/daemon.js.map +1 -1
  60. package/dist/daily-summary.js +7 -2
  61. package/dist/daily-summary.js.map +1 -1
  62. package/dist/fleet-context.d.ts +6 -0
  63. package/dist/fleet-manager.d.ts +120 -8
  64. package/dist/fleet-manager.js +1018 -142
  65. package/dist/fleet-manager.js.map +1 -1
  66. package/dist/fleet-system-prompt.js +1 -1
  67. package/dist/general-knowledge/skills/fleet-config/SKILL.md +19 -13
  68. package/dist/general-knowledge/skills/fleet-health/SKILL.md +30 -51
  69. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
  70. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +19 -13
  71. package/dist/general-knowledge/skills/model-discovery/SKILL.md +1 -5
  72. package/dist/general-knowledge/skills/scheduling/SKILL.md +41 -0
  73. package/dist/general-knowledge/skills/session-management/SKILL.md +29 -116
  74. package/dist/instance-lifecycle.d.ts +15 -1
  75. package/dist/instance-lifecycle.js +71 -22
  76. package/dist/instance-lifecycle.js.map +1 -1
  77. package/dist/locale.js +28 -2
  78. package/dist/locale.js.map +1 -1
  79. package/dist/logger.d.ts +6 -0
  80. package/dist/logger.js +28 -9
  81. package/dist/logger.js.map +1 -1
  82. package/dist/outbound-handlers.d.ts +3 -0
  83. package/dist/outbound-handlers.js +224 -11
  84. package/dist/outbound-handlers.js.map +1 -1
  85. package/dist/outbound-schemas.d.ts +44 -2
  86. package/dist/outbound-schemas.js +50 -3
  87. package/dist/outbound-schemas.js.map +1 -1
  88. package/dist/pause-marker.d.ts +5 -0
  89. package/dist/pause-marker.js +36 -0
  90. package/dist/pause-marker.js.map +1 -0
  91. package/dist/scheduler/db.d.ts +8 -0
  92. package/dist/scheduler/db.js +74 -5
  93. package/dist/scheduler/db.js.map +1 -1
  94. package/dist/scheduler/db.test.js +50 -0
  95. package/dist/scheduler/db.test.js.map +1 -1
  96. package/dist/scheduler/scheduler.d.ts +6 -0
  97. package/dist/scheduler/scheduler.js +99 -16
  98. package/dist/scheduler/scheduler.js.map +1 -1
  99. package/dist/scheduler/scheduler.test.js +131 -1
  100. package/dist/scheduler/scheduler.test.js.map +1 -1
  101. package/dist/scheduler/types.d.ts +9 -3
  102. package/dist/scheduler/types.js.map +1 -1
  103. package/dist/settings-api.d.ts +20 -2
  104. package/dist/settings-api.js +213 -9
  105. package/dist/settings-api.js.map +1 -1
  106. package/dist/setup-wizard.js +8 -0
  107. package/dist/setup-wizard.js.map +1 -1
  108. package/dist/statusline-watcher.d.ts +1 -1
  109. package/dist/statusline-watcher.js +3 -2
  110. package/dist/statusline-watcher.js.map +1 -1
  111. package/dist/tmux-control.d.ts +7 -0
  112. package/dist/tmux-control.js +22 -1
  113. package/dist/tmux-control.js.map +1 -1
  114. package/dist/tmux-manager.d.ts +1 -1
  115. package/dist/tmux-manager.js.map +1 -1
  116. package/dist/topic-commands.d.ts +19 -1
  117. package/dist/topic-commands.js +148 -23
  118. package/dist/topic-commands.js.map +1 -1
  119. package/dist/transcript-monitor.js +2 -0
  120. package/dist/transcript-monitor.js.map +1 -1
  121. package/dist/types.d.ts +29 -1
  122. package/dist/tz-utils.d.ts +26 -0
  123. package/dist/tz-utils.js +46 -0
  124. package/dist/tz-utils.js.map +1 -0
  125. package/dist/ui/dashboard.html +3 -3
  126. package/dist/ui/settings.html +453 -74
  127. package/dist/web-api.d.ts +1 -0
  128. package/dist/web-api.js +24 -15
  129. package/dist/web-api.js.map +1 -1
  130. package/dist/workflow-templates/default.md +0 -4
  131. package/package.json +2 -1
@@ -43,7 +43,7 @@ User messages arrive as text in your prompt with a prefix:
43
43
  ### Scheduling
44
44
  | Tool | Purpose |
45
45
  |------|---------|
46
- | \`create_schedule\` | Create a cron-based scheduled task |
46
+ | \`create_schedule\` | Create a recurring cron or one-shot scheduled task |
47
47
  | \`list_schedules\` | List all schedules |
48
48
  | \`update_schedule\` | Update a schedule |
49
49
  | \`delete_schedule\` | Delete a schedule |
@@ -30,29 +30,35 @@ templates: # Reusable fleet deployment templates
30
30
  - Instance logs: `~/.agend/instances/<name>/output.log`
31
31
  - Fleet log: `~/.agend/fleet.log`
32
32
 
33
+ ## kiro-cli UI mode (kiro_ui)
34
+
35
+ For `backend: kiro-cli` instances, `kiro_ui` selects the CLI's UI/agent profile:
36
+ ```yaml
37
+ instances:
38
+ my-kiro:
39
+ backend: kiro-cli
40
+ kiro_ui: legacy # default — current stable behavior
41
+ # kiro_ui: tui # try the newer TUI
42
+ # kiro_ui: v3 # try the v3 profile
43
+ ```
44
+ Leave unset (or `legacy`) unless testing a newer profile. Ignored by other backends.
45
+
33
46
  ## Config Validation
34
47
 
35
- **Before editing fleet.yaml or classicBot.yaml, always validate after:**
48
+ **After editing fleet.yaml or classicBot.yaml, validate before reloading:**
49
+ - `validate_config` MCP tool (preferred) — checks channels, instances, backends,
50
+ access, and both yaml files. Reports errors **and** warnings.
51
+ - CLI equivalent: `agend validate`
36
52
 
37
- ```bash
38
- # Validate fleet.yaml syntax
39
- agend fleet start --dry-run 2>&1 | head -5
40
- # Or simply:
41
- node -e "const yaml = require('js-yaml'); const fs = require('fs'); yaml.load(fs.readFileSync('$HOME/.agend/fleet.yaml', 'utf-8')); console.log('✓ valid YAML')"
42
- ```
53
+ Fix all errors before `agend reload`; warnings are advisory.
43
54
 
44
55
  **Common fleet.yaml mistakes:**
45
56
  - Missing `channel.mode` field → error on start
46
57
  - Wrong indentation (YAML is indent-sensitive)
47
58
  - `topic_id` as string vs number (both work, but be consistent)
48
- - `backend` typo (valid: `claude-code`, `gemini-cli`, `codex`, `opencode`, `kiro-cli`, `antigravity`)
59
+ - `backend` typo (valid: `claude-code`, `gemini-cli`, `codex`, `opencode`, `kiro-cli`, `antigravity`, `grok`)
49
60
  - `model` using wrong format for the backend
50
61
 
51
- **classicBot.yaml validation:**
52
- ```bash
53
- node -e "const yaml = require('js-yaml'); const fs = require('fs'); yaml.load(fs.readFileSync('$HOME/.agend/classicBot.yaml', 'utf-8')); console.log('✓ valid YAML')"
54
- ```
55
-
56
62
  **Common classicBot.yaml mistakes:**
57
63
  - `allowed_guilds` values must be strings (Discord IDs are too large for YAML integers)
58
64
  - Channel IDs as keys must be quoted strings
@@ -1,55 +1,34 @@
1
1
  ---
2
2
  name: fleet-health
3
- description: Check instance health via tmux, detect stuck agents, fleet-wide health scan
3
+ description: Check instance health and what an agent is doing; recover a stuck instance
4
4
  ---
5
5
 
6
- ## Instance Health Check via tmux
7
-
8
- When user asks to check an instance's status or what it's doing:
9
- - Use `execute_bash` to run: `tmux capture-pane -t agend:<instance-name> -p | tail -20`
10
- - This shows the actual CLI screen (what the agent sees right now)
11
- - More useful than just "running/stopped" status
12
- - If the instance appears stuck, suggest `/raw /compact` or restart
13
-
14
- ## Fleet Health Check
15
-
16
- Check all instances for stuck/error state:
17
-
18
- ```bash
19
- for win in $(tmux list-windows -t agend -F '#{window_name}' | grep -v bash); do
20
- last=$(tmux capture-pane -t "agend:$win" -p | tail -3 | tr '\n' ' ')
21
- if echo "$last" | grep -q "!>"; then
22
- echo "✅ $win idle"
23
- elif echo "$last" | grep -q "error:"; then
24
- echo "❌ $win ERROR"
25
- else
26
- echo "⏳ $winbusy"
27
- fi
28
- done
29
- ```
30
-
31
- States:
32
- - idle prompt visible (X% !>), ready for input
33
- - busy processing a task, wait for it to finish
34
- - error check tmux pane for details, may need restart
35
-
36
- If an instance is stuck (busy for >10 minutes with no output), restart it:
37
- - `restart_instance("<instance-name>")`
38
-
39
- ## Unsticking a Frozen Instance via tmux
40
-
41
- If an instance is frozen (not responding, no output, no prompt):
42
- 1. Send Ctrl+C via tmux to interrupt the current operation:
43
- ```bash
44
- tmux send-keys -t agend:<instance-name> C-c
45
- ```
46
- 2. Wait a few seconds, then check if it returned to idle:
47
- ```bash
48
- tmux capture-pane -t agend:<instance-name> -p | tail -5
49
- ```
50
- 3. If it shows the prompt (`X% !>` or `(To exit the CLI...)`) — it's unstuck. Resend the task.
51
- 4. If still frozen after Ctrl+C, use `restart_instance("<instance-name>")`
52
-
53
- **When to use Ctrl+C vs restart:**
54
- - Ctrl+C: instance is alive but stuck on a long operation (API timeout, large file read, infinite loop)
55
- - restart: instance is completely dead (no tmux pane, crash loop, or Ctrl+C doesn't help)
6
+ ## Check health
7
+
8
+ Prefer the fleet tools they already know each instance's state:
9
+ - `get_fleet_status` status + execution state (idle / working / stuck) for every instance.
10
+ - `describe_instance("<name>")` one instance's status, last activity, description.
11
+ - `list_instances` quick roster.
12
+
13
+ The daemon derives idle/working/stuck itself (per-backend), so you do **not** need to
14
+ scrape prompts. Don't hand-write pane-text checks like `X% !>` — that prompt is
15
+ kiro-specific and wrong for claude-code / codex / grok / antigravity.
16
+
17
+ ## See what an agent is actually doing
18
+
19
+ When the user wants the live screen (not just a status word):
20
+ - `get_instance_logs("<name>")` recent output.
21
+ - Raw terminal (last resort, no tool for the live pane):
22
+ `tmux capture-pane -t agend:<name> -p | tail -20`
23
+
24
+ ## Recover a stuck instance
25
+
26
+ Do this when the user asks, or confirm first don't silently restart others' work.
27
+ - `restart_instance("<name>")` — reloads config, keeps the session. First choice for a
28
+ dead/looping instance.
29
+ - `replace_instance("<name>")` — fresh instance with handover context, when the session
30
+ itself is the problem (see instance-lifecycle skill).
31
+
32
+ A genuinely wedged CLI can sometimes be freed without a restart by interrupting it —
33
+ that is the cancel key, exposed to users as the Cancel button / `/cancel`. Only drop to
34
+ `tmux send-keys -t agend:<name> C-c` (kiro) / `Escape` (others) if the tools aren't enough.
@@ -26,7 +26,7 @@ description: Fleet restart types, recovery from tmux crash, rate limit handling,
26
26
  **Update AgEnD to latest version:**
27
27
  ```bash
28
28
  agend update # update to latest
29
- agend update --version 0.0.6 # pin specific version
29
+ agend update --version 2.1.0 # pin a specific version
30
30
  ```
31
31
 
32
32
  The `agend update` command automatically:
@@ -1,20 +1,26 @@
1
1
  ---
2
2
  name: instance-lifecycle
3
- description: Replace vs restart instances, monitoring state, when to use each
3
+ description: restart vs replace vs pause/wake; when to use each
4
4
  ---
5
5
 
6
- ## Instance Lifecycle Management
6
+ ## Restart vs Replace
7
7
 
8
- **Replace vs Restart:**
9
- - `restart_instance` — keeps session, reloads config. Use when config changed.
10
- - `replace_instance` kills old, creates fresh with handover context. Use when context is polluted or instance is stuck in a loop.
8
+ - `restart_instance("<name>")` — keeps the session, reloads config. Use when config changed.
9
+ - `replace_instance("<name>")` — kills old, creates a fresh instance with handover context.
10
+ Use when the session is the problem: hallucinating / referencing stale info, stuck in a
11
+ tool-call loop, or context is degrading (only backends that report context % surface that).
11
12
 
12
- **When to replace (not restart):**
13
- - Instance keeps hallucinating or referencing stale information
14
- - Instance is stuck in a tool-call loop
15
- - Context is reported >80% full and responses are degrading (only applicable to backends that report context usage)
13
+ ## Pause / Wake (resource management)
16
14
 
17
- **Monitoring instance state:**
18
- - `describe_instance("<name>")` — shows status, last activity, description
19
- - `tmux capture-pane -t agend:<name> -p | tail -20` see actual CLI screen
20
- - Look for `X% !>` prompt = idle, `Thinking...` = busy, `error` = needs attention
15
+ - `pause_instance("<name>")` — stop the resident CLI but keep the instance; frees resources.
16
+ - `wake_instance("<name>")` — bring a paused instance back.
17
+ - Instances also pause automatically: `auto_pause_after` (idle minutes) and `warm_cap`
18
+ (fleet-wide cap the least-recently-active idle instance is paused when over the cap).
19
+ The **general** instance is never auto-paused.
20
+ - You don't need to wake before sending work: delivery wakes a paused instance first.
21
+
22
+ ## Monitoring state
23
+
24
+ - `get_fleet_status` / `describe_instance("<name>")` — status + idle/working/stuck + last
25
+ activity (the daemon derives the state; don't scrape pane prompts).
26
+ - For the raw screen, see the fleet-health skill (`get_instance_logs` / tmux capture).
@@ -14,11 +14,7 @@ Models are specified in fleet.yaml `defaults.model` or per-instance `model` fiel
14
14
  | **antigravity** | Run `agy models` to see available models | Gemini 3.5 Flash (Medium) |
15
15
  | **codex** | `gpt-4o`, `o3`, `o4-mini` | gpt-4o |
16
16
  | **opencode** | `opencode models` | depends on provider |
17
-
18
- **To discover available models for a backend, run the CLI's model listing command:**
19
- - `agy models` — lists all available models for antigravity
20
- - `opencode models` — lists all available models for opencode
21
- - `codex` — check config.toml
17
+ | **grok** | `grok-4.5`, `grok-4.3`, `grok-code`, `grok-build-0.1` | grok default |
22
18
 
23
19
  **Important for antigravity (agy):**
24
20
  - `agy models` shows names like `Gemini 3.5 Flash (Medium)` — the parenthetical suffix (Medium/High/Low/Thinking) is the **effort level**, NOT part of the model name.
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: scheduling
3
+ description: Cron, one-shot, and silent schedules via the schedule MCP tools
4
+ ---
5
+
6
+ ## Tools
7
+
8
+ - `create_schedule` — make a schedule (recurring or one-shot).
9
+ - `list_schedules` — list all (optionally filter by target instance).
10
+ - `update_schedule` — change cron/at/message/timezone/enabled of an existing one.
11
+ - `delete_schedule` — remove one.
12
+
13
+ A schedule injects `message` into the `target` instance when it fires
14
+ (`target` defaults to the current instance).
15
+
16
+ ## Three ways to schedule
17
+
18
+ **1. Recurring (cron)** — `cron` + optional `timezone` (IANA, default `Asia/Taipei`):
19
+ ```
20
+ create_schedule({ cron: "0 9 * * *", message: "Daily standup summary", timezone: "Asia/Taipei" })
21
+ ```
22
+ Cron is 5-field (min hour dom mon dow). `0 9 * * *` = 09:00 daily in the given tz.
23
+
24
+ **2. One-shot (at)** — `at` = ISO-8601 datetime **with offset**; runs once then auto-deletes:
25
+ ```
26
+ create_schedule({ at: "2026-07-26T14:00:00+08:00", message: "Ship the release", target: "dev1" })
27
+ ```
28
+ Exactly one of `cron` / `at` is required (mutually exclusive).
29
+
30
+ **3. Silent** — add `silent: true` to any of the above. The message is pasted straight
31
+ into the target's tmux pane instead of posting to the channel — use for heartbeats /
32
+ background nudges the user shouldn't see:
33
+ ```
34
+ create_schedule({ cron: "*/30 * * * *", message: "check CI and self-report if red", silent: true })
35
+ ```
36
+
37
+ ## Notes
38
+
39
+ - `timezone` is per-schedule; the fire time is fixed to that zone (DST-safe).
40
+ - Give recurring schedules a `label` so `list_schedules` / `delete_schedule` are easy.
41
+ - One-shot (`at`) needs no cleanup — it deletes itself after firing.
@@ -1,133 +1,46 @@
1
1
  ---
2
2
  name: session-management
3
- description: Save/load/fork sessions, batch backup, reviewer session setup, kiro-cli and claude-code session paths
3
+ description: Where kiro-cli and claude-code store sessions, and how to fork one to a new instance
4
4
  ---
5
5
 
6
- ## kiro-cli Session Storage
6
+ ## Where sessions live
7
7
 
8
- kiro-cli automatically stores conversation sessions at:
9
- - **Path:** `~/.kiro/sessions/cli/<uuid>.json`
10
- - Each session is a JSON file with the full conversation history
11
- - Sessions persist across restarts kiro-cli auto-resumes the latest session for each working directory
12
-
13
- **Useful for:**
14
- - Manual backup: `cp ~/.kiro/sessions/cli/*.json ~/backup/`
15
- - Finding a specific session: `ls -lt ~/.kiro/sessions/cli/ | head -5`
16
- - Loading a session into a new instance via `pre_task_command: "/chat load <path>"`
17
-
18
- ## Reviewer Session Management
19
-
20
- For reviewer instances using kiro-cli:
21
- - Recommend setting `pre_task_command: "/chat load reviewer-base.json"` in fleet.yaml
22
- - This loads a base session with review guidelines on every restart
23
- - Help user create the base session:
24
- 1. Attach to reviewer: `agend attach <reviewer-instance>`
25
- 2. Set up review context and guidelines
26
- 3. Save: `/chat save reviewer-base.json -f`
27
- 4. Add to fleet.yaml under the reviewer instance config:
28
- ```yaml
29
- instances:
30
- reviewer-xxx:
31
- pre_task_command: "/chat load reviewer-base.json"
32
- ```
33
-
34
- ## Fork Instance (Session Cloning)
35
-
36
- When user wants to fork/clone an instance's session to a new instance:
37
-
38
- Steps:
39
- 1. Wait for source instance to be idle (check with tmux capture-pane, look for "X% !>" prompt)
40
-
41
- 2. Save current session on source instance via tmux:
42
- - `execute_bash`: `tmux send-keys -t agend:<source-instance> '/chat save YYYYMMDD.json -f' Enter`
43
- - Wait a few seconds for save to complete
44
-
45
- 3. Create new instance:
46
- - `create_instance` with same backend and working_directory (or new one)
47
-
48
- 4. Copy session file to new instance workspace:
49
- - `execute_bash`: `cp ~/.agend/workspaces/<source>/YYYYMMDD.json ~/.agend/workspaces/<target>/`
50
-
51
- 5. Wait for new instance to be idle, then load session via tmux:
52
- - `execute_bash`: `tmux send-keys -t agend:<new-instance-name> '/chat load YYYYMMDD.json' Enter`
53
- - Or configure `pre_task_command: "/chat load YYYYMMDD.json"` for auto-load on restart
54
-
55
- ## Claude Code Session Storage & Fork
56
-
57
- claude-code stores conversation sessions as JSONL, **keyed by the project (working) directory**:
58
- - **Path:** `~/.claude/projects/<project-path-encoded>/*.jsonl`
59
- - `<project-path-encoded>` is the absolute working_directory with `/` replaced by `-`
60
- (e.g. `/home/han/Projects/AgEnD` → `-home-han-Projects-AgEnD`)
61
- - Each `.jsonl` is one session (full message history). Latest = most recently modified.
62
-
63
- **Key difference from kiro-cli:** claude-code has **no `/chat save` / `/chat load`**. You resume
64
- only via `--continue` (latest session for this dir) or `--resume <id>`. `/export` produces
65
- plain text only — it **cannot** be reloaded as a session. So forking is done by **copying the
66
- `.jsonl` file**, not by save/load commands.
8
+ | | kiro-cli | claude-code |
9
+ |---|---|---|
10
+ | Store | `~/.kiro/sessions/cli/<uuid>.json` | `~/.claude/projects/<path-encoded>/*.jsonl` |
11
+ | Keyed by | session uuid | project (working) directory |
12
+ | Reload | `/chat load <file>` | none — `--continue` (latest for the dir) / `--resume <id>` |
13
+ | Text export | — | `/export` (plain text, **not** reloadable) |
67
14
 
68
- ### Fork a claude-code session to a new instance
15
+ `<path-encoded>` = the absolute working_directory with `/` `-`
16
+ (e.g. `/home/han/Projects/AgEnD` → `-home-han-Projects-AgEnD`).
69
17
 
70
- 1. **Confirm source instance is idle** `tmux capture-pane -t agend:<source> -p | tail -5`
71
- (look for the ready prompt, e.g. `❯`). Don't fork mid-task.
18
+ ## Fork a session to a new instance
72
19
 
73
- 2. **Find the source session file** (newest first):
74
- ```bash
75
- ls -lt ~/.claude/projects/<source-path-encoded>/*.jsonl | head -5
76
- ```
20
+ Confirm the source is **idle** first (`describe_instance` / `get_fleet_status`) — don't fork mid-task.
77
21
 
78
- 3. **Create the new instance** with `create_instance` (backend: `claude-code`). Note its
79
- `working_directory` it determines the target project path.
22
+ **kiro-cli** save, copy, load:
23
+ 1. On the source, save: `/chat save <name>.json -f` (paste via tmux if needed).
24
+ 2. `create_instance` (same backend).
25
+ 3. `cp ~/.agend/workspaces/<source>/<name>.json ~/.agend/workspaces/<target>/`
26
+ 4. Load on the target: `/chat load <name>.json`, or set `pre_task_command: "/chat load <name>.json"`.
80
27
 
81
- 4. **Copy the `.jsonl` into the new instance's encoded project dir:**
28
+ **claude-code** — copy the `.jsonl` (no save/load command):
29
+ 1. Newest source session: `ls -lt ~/.claude/projects/<source-encoded>/*.jsonl | head`
30
+ 2. `create_instance` (backend `claude-code`); note its working_directory.
31
+ 3. Copy into the target's encoded project dir:
82
32
  ```bash
83
33
  TARGET_ENC="$(echo '<target-working-dir>' | sed 's#/#-#g')"
84
34
  mkdir -p ~/.claude/projects/$TARGET_ENC
85
- cp ~/.claude/projects/<source-path-encoded>/<session>.jsonl \
86
- ~/.claude/projects/$TARGET_ENC/
35
+ cp ~/.claude/projects/<source-encoded>/<session>.jsonl ~/.claude/projects/$TARGET_ENC/
87
36
  ```
37
+ 4. Start the target — claude-code resumes the newest `.jsonl` via `--continue`.
88
38
 
89
- 5. **Start/restart the new instance** the claude-code backend resumes via `--continue`
90
- (it auto-picks the latest session for that project dir), so the copied session is continued.
91
-
92
- ### Caveats
93
- - **Same project path required:** a session can only resume under the working_directory it
94
- was recorded in. If the target dir differs, claude-code still loads it via `--continue`
95
- (it reads the newest `.jsonl` in the target's encoded dir), but file paths/context inside
96
- the transcript will refer to the original dir.
97
- - `/export` = text only, not reloadable. Use the raw `.jsonl`.
98
- - Pick the **right** `.jsonl` if multiple exist (sort by mtime; each branch/compaction can
99
- create new files).
100
-
101
- ### kiro-cli vs claude-code fork (summary)
102
- | | kiro-cli | claude-code |
103
- |---|---|---|
104
- | Store | `~/.kiro/sessions/cli/<uuid>.json` | `~/.claude/projects/<path-encoded>/*.jsonl` |
105
- | Keyed by | session uuid | project (working) directory |
106
- | Fork method | `/chat save` → copy → `/chat load` | copy `.jsonl` → `--continue` |
107
- | Reload command | `/chat load <file>` | none — `--continue` / `--resume` only |
108
- | Text export | — | `/export` (not reloadable) |
109
-
110
- ## Batch Session Backup
111
-
112
- Save all instances' sessions to a dated backup directory:
113
-
114
- ```bash
115
- DATE=$(date +%Y%m%d)
116
- BACKUP_DIR="$HOME/.agend/session-backups/$DATE"
117
- mkdir -p "$BACKUP_DIR"
118
- MY_NAME="<your-own-instance-name>" # skip yourself to avoid paste collision
119
- for win in $(tmux list-windows -t agend -F '#{window_name}' | grep -v bash); do
120
- if [ "$win" = "$MY_NAME" ]; then continue; fi
121
- tmux send-keys -t "agend:$win" "/chat save $BACKUP_DIR/${win}.json -f" Enter
122
- sleep 3
123
- done
124
- ```
39
+ **Caveats:** a claude-code session only truly makes sense under its original working_directory
40
+ (paths inside the transcript refer to it). Pick the right `.jsonl` if several exist (newest by
41
+ mtime; compaction/branches create new files). `/export` is text only, not reloadable.
125
42
 
126
- Important:
127
- - Skip your own instance (the one executing this) to avoid paste collision
128
- - Use `sleep 3` between saves
129
- - Run fleet health check first — only backup idle instances
130
- - Do NOT backup while instances are busy
43
+ ## Backup
131
44
 
132
- Restore a single instance:
133
- - `tmux send-keys -t agend:<instance> '/chat load /path/to/backup.json' Enter`
45
+ Sessions are plain files — back them up by copying the store paths above
46
+ (e.g. `cp ~/.kiro/sessions/cli/*.json <dest>/`). Only copy while the instance is idle.
@@ -4,6 +4,14 @@ import type { Logger } from "./logger.js";
4
4
  import type { IpcClient } from "./channel/ipc-bridge.js";
5
5
  import type { EventLog } from "./event-log.js";
6
6
  import type { TmuxControlClient } from "./tmux-control.js";
7
+ export interface BackendInstallationInfo {
8
+ binary: string;
9
+ install: string;
10
+ }
11
+ /** Shared CLI metadata used by startup validation and ClassicBot onboarding. */
12
+ export declare const BACKEND_INSTALLATION_INFO: Readonly<Record<string, BackendInstallationInfo>>;
13
+ /** Check one executable using the same PATH visible to the fleet process. */
14
+ export declare function checkBinaryInstalled(binary: string): boolean;
7
15
  /**
8
16
  * Context interface for instance lifecycle operations.
9
17
  * FleetManager implements this.
@@ -20,6 +28,10 @@ export interface LifecycleContext {
20
28
  readonly controlClient: TmuxControlClient | null;
21
29
  getInstanceDir(name: string): string;
22
30
  saveFleetConfig(): void;
31
+ /** Full stop+start. freshStart forces the respawn to skip session resume. */
32
+ restartSingleInstance(name: string, opts?: {
33
+ freshStart?: boolean;
34
+ }): Promise<void>;
23
35
  connectIpcToInstance(name: string): Promise<void>;
24
36
  createForumTopic(topicName: string, adapterId?: string): Promise<number | string>;
25
37
  deleteForumTopic(topicId: number | string): Promise<void>;
@@ -27,7 +39,7 @@ export interface LifecycleContext {
27
39
  /** Remove instance with full cleanup (scheduler, IPC, routing, config). */
28
40
  removeInstance(name: string): Promise<void>;
29
41
  touchActivity(name: string): void;
30
- sendHangNotification(name: string): Promise<void>;
42
+ sendHangNotification(name: string, unchangedForMs?: number): Promise<void>;
31
43
  notifyInstanceTopic(name: string, text: string): void;
32
44
  /** List claimed tasks for an instance (from task board). Returns empty array if unavailable. */
33
45
  listClaimedTasks(assignee: string): Array<{
@@ -39,7 +51,9 @@ export interface LifecycleContext {
39
51
  /** Retire (delete) any pending Cancel button for an instance. No-op if none. */
40
52
  clearCancelButton(name: string): void;
41
53
  startStatuslineWatcher(name: string): void;
54
+ stopStatuslineWatcher(name: string): void;
42
55
  reactMessageStatus(instanceName: string, chatId: string, messageId: string, emoji: string): void;
56
+ startPersistedPausedInstance(name: string): Promise<void>;
43
57
  }
44
58
  /** Arguments accepted by handleCreate — mirrors CreateInstanceArgs in outbound-schemas.ts
45
59
  * plus internal-only fields forwarded by deploy_template (profile-derived). */
@@ -8,6 +8,27 @@ import { sanitizeInstanceName } from "./topic-commands.js";
8
8
  import { isModelCompatible } from "./backend/types.js";
9
9
  import { safeHandler } from "./safe-async.js";
10
10
  import { t } from "./locale.js";
11
+ import { clearPausedMarker, hasPausedMarker, readPausedAt, writePausedMarker } from "./pause-marker.js";
12
+ /** Shared CLI metadata used by startup validation and ClassicBot onboarding. */
13
+ export const BACKEND_INSTALLATION_INFO = {
14
+ "claude-code": { binary: "claude", install: "curl -fsSL https://claude.ai/install.sh | bash" },
15
+ "gemini-cli": { binary: "gemini", install: "npm i -g @google/gemini-cli" },
16
+ "kiro-cli": { binary: "kiro-cli", install: "brew install --cask kiro-cli" },
17
+ codex: { binary: "codex", install: "npm i -g @openai/codex" },
18
+ opencode: { binary: "opencode", install: "curl -fsSL https://opencode.ai/install | bash" },
19
+ antigravity: { binary: "agy", install: "curl -fsSL https://antigravity.google/cli/install.sh | bash" },
20
+ grok: { binary: "grok", install: "curl -fsSL https://x.ai/cli/install.sh | bash" },
21
+ };
22
+ /** Check one executable using the same PATH visible to the fleet process. */
23
+ export function checkBinaryInstalled(binary) {
24
+ try {
25
+ execFileSync("which", [binary], { stdio: "pipe", timeout: 2000 });
26
+ return true;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
11
32
  export class InstanceLifecycle {
12
33
  ctx;
13
34
  /** Active daemon processes: instanceName → Daemon */
@@ -35,18 +56,9 @@ export class InstanceLifecycle {
35
56
  const { createBackend } = await import("./backend/factory.js");
36
57
  const backendName = config.backend ?? this.ctx.fleetConfig?.defaults?.backend ?? "claude-code";
37
58
  // Verify backend binary is in PATH before spawning
38
- const binaryMap = {
39
- "claude-code": "claude", "codex": "codex", "gemini-cli": "gemini",
40
- "opencode": "opencode", "kiro-cli": "kiro-cli", "antigravity": "agy",
41
- };
42
- const bin = binaryMap[backendName];
43
- if (bin) {
44
- try {
45
- execFileSync("which", [bin], { stdio: "pipe", timeout: 2000 });
46
- }
47
- catch {
48
- this.ctx.logger.error({ binary: bin, backend: backendName, instance: name }, `Backend binary "${bin}" not found in PATH`);
49
- }
59
+ const installation = BACKEND_INSTALLATION_INFO[backendName];
60
+ if (installation && !checkBinaryInstalled(installation.binary)) {
61
+ this.ctx.logger.error({ binary: installation.binary, backend: backendName, instance: name }, `Backend binary "${installation.binary}" not found in PATH`);
50
62
  }
51
63
  // claude-code refuses --dangerously-skip-permissions as root — fail fast
52
64
  // Unless IS_SANDBOX=1 is set (recognized sandbox environment)
@@ -57,7 +69,7 @@ export class InstanceLifecycle {
57
69
  return;
58
70
  }
59
71
  const backend = createBackend(backendName, instanceDir);
60
- const daemon = new Daemon(name, config, instanceDir, topicMode, backend, this.ctx.controlClient ?? undefined);
72
+ const daemon = new Daemon(name, config, instanceDir, topicMode, backend, this.ctx.controlClient ?? undefined, this.ctx.logger);
61
73
  // Catch errors from daemon internals (e.g. IPC server) to prevent crashing the fleet process
62
74
  daemon.on("error", (err) => {
63
75
  this.ctx.logger.error({ err, name }, "Daemon emitted error — instance isolated");
@@ -66,7 +78,7 @@ export class InstanceLifecycle {
66
78
  this.daemons.set(name, daemon);
67
79
  const hangDetector = daemon.getHangDetector();
68
80
  if (hangDetector) {
69
- hangDetector.on("hang", safeHandler(async () => {
81
+ hangDetector.on("hang", safeHandler(async (data) => {
70
82
  this.ctx.eventLog?.insert(name, "hang_detected", {});
71
83
  this.ctx.logger.warn({ name }, "Instance appears hung");
72
84
  // Check if instance has claimed tasks — nudge it to continue
@@ -84,7 +96,7 @@ export class InstanceLifecycle {
84
96
  });
85
97
  }
86
98
  }
87
- await this.ctx.sendHangNotification(name);
99
+ await this.ctx.sendHangNotification(name, data?.unchangedForMs);
88
100
  this.ctx.webhookEmit("hang", name);
89
101
  }, this.ctx.logger, `hangDetector[${name}]`));
90
102
  }
@@ -135,6 +147,13 @@ export class InstanceLifecycle {
135
147
  if (data.action === "failover") {
136
148
  this.ctx.checkModelFailover(name, 100); // Force failover trigger
137
149
  }
150
+ else if (data.action === "restart") {
151
+ // A broken *resumed* session (e.g. agy pinned to a dead model) can't
152
+ // self-recover; a plain restart would --continue back into it. freshStart
153
+ // makes the respawn skip resume so the CLI starts a clean session on its
154
+ // default (valid) model.
155
+ this.ctx.restartSingleInstance(name, { freshStart: true }).catch(err => this.ctx.logger.error({ err, name }, "pty_error restart failed"));
156
+ }
138
157
  }, this.ctx.logger, `daemon.pty_error[${name}]`));
139
158
  daemon.on("pty_recovered", safeHandler((data) => {
140
159
  const mins = Math.floor(data.downtime_s / 60);
@@ -164,22 +183,49 @@ export class InstanceLifecycle {
164
183
  this.ctx.touchActivity(name);
165
184
  }
166
185
  isPaused(name) {
167
- return this.daemons.get(name)?.isPaused ?? false;
186
+ return this.daemons.get(name)?.isPaused ?? hasPausedMarker(this.ctx.getInstanceDir(name));
168
187
  }
169
188
  getLastPausedAt(name) {
170
- return this.daemons.get(name)?.lastPausedAt ?? null;
189
+ return this.daemons.get(name)?.lastPausedAt ?? readPausedAt(this.ctx.getInstanceDir(name));
171
190
  }
172
191
  async pause(name) {
173
192
  const daemon = this.daemons.get(name);
174
- if (!daemon)
193
+ if (!daemon) {
194
+ if (hasPausedMarker(this.ctx.getInstanceDir(name)))
195
+ return;
175
196
  throw new Error(`Cannot pause stopped instance '${name}'`);
176
- await daemon.pause();
197
+ }
198
+ this.ctx.stopStatuslineWatcher(name);
199
+ try {
200
+ await daemon.pause();
201
+ }
202
+ finally {
203
+ // A rejected/no-op pause leaves the instance active and must not strand
204
+ // its fleet-level statusline watcher in the frozen state.
205
+ if (!daemon.isPaused)
206
+ this.ctx.startStatuslineWatcher(name);
207
+ }
177
208
  }
178
209
  async wake(name, timeoutMs = 30_000) {
179
210
  const daemon = this.daemons.get(name);
180
- if (!daemon)
181
- throw new Error(`Cannot wake stopped instance '${name}'`);
182
- await daemon.wake(timeoutMs);
211
+ if (daemon) {
212
+ await daemon.wake(timeoutMs);
213
+ }
214
+ else {
215
+ const instanceDir = this.ctx.getInstanceDir(name);
216
+ if (!hasPausedMarker(instanceDir))
217
+ throw new Error(`Cannot wake stopped instance '${name}'`);
218
+ const pausedAt = readPausedAt(instanceDir) ?? Date.now();
219
+ clearPausedMarker(instanceDir);
220
+ try {
221
+ await this.ctx.startPersistedPausedInstance(name);
222
+ }
223
+ catch (err) {
224
+ writePausedMarker(instanceDir, pausedAt);
225
+ throw err;
226
+ }
227
+ }
228
+ this.ctx.startStatuslineWatcher(name);
183
229
  }
184
230
  async stop(name) {
185
231
  this.ctx.setTopicIcon(name, "remove");
@@ -326,6 +372,7 @@ export class InstanceLifecycle {
326
372
  // Remove from fleet config and save
327
373
  delete this.ctx.fleetConfig.instances[name];
328
374
  this.ctx.saveFleetConfig();
375
+ clearPausedMarker(this.ctx.getInstanceDir(name));
329
376
  this.ctx.logger.info({ name }, "Instance removed");
330
377
  }
331
378
  /** Handle create_instance tool call from a daemon. */
@@ -474,6 +521,8 @@ export class InstanceLifecycle {
474
521
  const explicitTopicName = args.topic_name;
475
522
  const nameBase = explicitTopicName ?? (worktreePath ? topicName : (directory ? basename(workDir) : topicName));
476
523
  newInstanceName = `${sanitizeInstanceName(nameBase)}-t${createdTopicId}`;
524
+ // A recycled name must never inherit a stale pause marker.
525
+ clearPausedMarker(this.ctx.getInstanceDir(newInstanceName));
477
526
  // If no directory was provided, auto-create default workspace
478
527
  if (!directory) {
479
528
  workDir = join(getAgendHome(), "workspaces", newInstanceName);