ninja-terminals 2.2.5 → 2.2.6

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/CLAUDE.md CHANGED
@@ -38,6 +38,17 @@ These status lines are CRITICAL — the orchestrator parses them to know your st
38
38
  - If a build breaks, fix it yourself — don't wait for the orchestrator
39
39
  - If you hit a permissions wall, report BLOCKED with specifics
40
40
 
41
+ ### Stuck Terminal Recovery
42
+ If you notice your terminal is stuck (no response to input, commands hang), or if you see another terminal is stuck:
43
+ 1. **Report it**: `STATUS: ERROR:STUCK — terminal unresponsive after [what happened]`
44
+ 2. **The orchestrator will**: Refresh the page or restart the terminal
45
+ 3. **After restart**: You will lose context. Wait for re-orientation from the orchestrator.
46
+
47
+ Common causes of stuck terminals:
48
+ - Permission errors (chmod, file access)
49
+ - Tool failures that leave PTY in bad state
50
+ - Context window overflow
51
+
41
52
  ### Completeness
42
53
  - Don't report DONE for partial work. If you were asked to "build and test", both must be done.
43
54
  - Include evidence with DONE: test output, screenshot path, API response, URL, file path
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ninja-terminals",
3
- "version": "2.2.5",
3
+ "version": "2.2.6",
4
4
  "description": "MCP server for multi-terminal Claude Code orchestration with DAG task management, parallel execution, and self-improvement",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -110,8 +110,22 @@ When done: STATUS: DONE — [template name and test result]
110
110
  | `done` | Read output. Verify the claim. Assign next task. |
111
111
  | `blocked` | Read what it needs. Provide it, or reassign. |
112
112
  | `error` | Read the error. Send fix instructions or restart. |
113
+ | `stuck` | Terminal is unresponsive. **Refresh the page** or `POST /api/terminals/:id/restart`. |
113
114
  | `compacting` | Wait, then re-orient fully with context summary. |
114
115
 
116
+ ### Stuck Terminal Recovery
117
+ Terminals can get stuck after tool errors (permission denied, failed commands, etc.). Signs of a stuck terminal:
118
+ - No output for 2+ minutes while status shows "working"
119
+ - Input commands have no effect
120
+ - Status shows `stuck`
121
+
122
+ **Recovery steps:**
123
+ 1. **First try**: Refresh the Ninja Terminals page (Cmd+R / Ctrl+R)
124
+ 2. **If that fails**: `POST /api/terminals/:id/restart` to restart just that terminal
125
+ 3. **Last resort**: Kill and respawn: `DELETE /api/terminals/:id` then `POST /api/terminals/spawn`
126
+
127
+ After recovery, re-dispatch the task with full context — the terminal lost its memory.
128
+
115
129
  ### Context Preservation
116
130
  - Terminals WILL compact during long tasks and lose memory
117
131
  - You MUST re-orient them: what they were doing, what's completed, what's next, critical context
package/server.js CHANGED
@@ -219,14 +219,41 @@ function spawnTerminal(label, scope = [], cwd = null, tier = 'pro') {
219
219
 
220
220
  // ── Status Detection Loop (2s) ──────────────────────────────
221
221
 
222
+ // Track last output time for stuck detection
223
+ const lastOutputTime = new Map();
224
+ const STUCK_THRESHOLD_MS = 120000; // 2 minutes with no output = stuck
225
+
222
226
  setInterval(() => {
223
- for (const [, terminal] of terminals) {
227
+ for (const [id, terminal] of terminals) {
224
228
  if (terminal.status === 'exited') continue;
225
229
  const prev = terminal.status;
226
230
  const recentLines = terminal.lineBuffer.last(50);
227
231
  const newStatus = detectStatus(recentLines);
228
232
 
229
- if (newStatus !== prev) {
233
+ // Track output activity for stuck detection
234
+ const currentOutput = terminal.lineBuffer.last(5).join('');
235
+ const lastOutput = lastOutputTime.get(id);
236
+ if (!lastOutput || lastOutput.output !== currentOutput) {
237
+ lastOutputTime.set(id, { output: currentOutput, time: Date.now() });
238
+ }
239
+
240
+ // Check for stuck terminal (no output for 2+ minutes while "working")
241
+ const lastActivity = lastOutputTime.get(id);
242
+ if (lastActivity && terminal.status === 'working') {
243
+ const stuckTime = Date.now() - lastActivity.time;
244
+ if (stuckTime > STUCK_THRESHOLD_MS) {
245
+ terminal.status = 'stuck';
246
+ sse.broadcast('terminal_stuck', {
247
+ terminal: terminal.label,
248
+ id: terminal.id,
249
+ stuckFor: Math.round(stuckTime / 1000),
250
+ suggestion: 'Refresh page or POST /api/terminals/:id/restart',
251
+ });
252
+ console.log(`Terminal ${id} (${terminal.label}) appears stuck - no output for ${Math.round(stuckTime / 1000)}s`);
253
+ }
254
+ }
255
+
256
+ if (newStatus !== prev && newStatus !== 'stuck') {
230
257
  terminal.status = newStatus;
231
258
  sse.broadcast('status_change', {
232
259
  terminal: terminal.label,