@dmsdc-ai/aigentry-telepty 0.1.98 → 0.3.3

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 ADDED
@@ -0,0 +1,326 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@dmsdc-ai/aigentry-telepty` are documented here.
4
+
5
+ ## [0.3.3] — 2026-05-02
6
+
7
+ ### Added — `inject --submit-force` + idempotent client retry (spec: `docs/superpowers/specs/2026-05-02-submit-force-and-retry.md`)
8
+
9
+ Closes task #347. Two opt-in CLI knobs on `telepty inject` for cases where
10
+ the 0.3.2 prompt-symbol gate has a transient render mismatch (autocomplete
11
+ dropdown open, cursor moved, mid-paste race) and the 504 fall-through
12
+ forces the human user to press Enter manually.
13
+
14
+ - **`--submit-force`** — passes `force: true` to `POST /submit`. Skips
15
+ both Layer 3 (prompt-symbol) and Layer 1 (state-gate) and dispatches
16
+ Enter once via the existing `terminalLevelSubmit` chain (kitty → cmux
17
+ → PTY). Daemon-side `force` semantics already shipped in 0.3.1 for
18
+ `telepty send-key`; this just plumbs the flag through inject.
19
+ - **`--submit-retry N`** (default 1, clamp [0, 3]) — on a 504 response
20
+ with a retry-safe reason, wait 300 ms and retry the same `/submit`
21
+ request up to N times. Retry-safe reasons (idempotent re-fire is
22
+ guaranteed because the body is verifiably still in the input box):
23
+
24
+ | Reason | Source |
25
+ |---|---|
26
+ | `gated_dispatch_unconsumed` | `daemon.js:1680` (verify said body still visible after best-effort dispatch) |
27
+ | `gate_timeout` | reserved (Layer 1 plain timeout — falls through to dispatch in 0.3.1+, not currently a 504 source) |
28
+ | `no_prompt_symbol_seen` | reserved (Layer 3 timeout — currently never emits 504) |
29
+
30
+ Hard-fail reasons (`session_dead`, `session_error`, `session_restarting`,
31
+ `no_state`, `no_state_manager`) and any non-504 status (4xx) **never**
32
+ trigger client-side retry — re-firing won't recover.
33
+
34
+ - **Default behavior preserved**: a bare `telepty inject --submit ...`
35
+ call now retries once on a retry-safe 504. This is a strict improvement
36
+ over 0.3.2 (which surfaced a warning and required manual `send-key`)
37
+ and remains backward-compatible because retry only fires when the
38
+ server tells the client the dispatch demonstrably did not land.
39
+
40
+ ### Tests
41
+
42
+ - `test/inject-submit-flags.test.js` (NEW, 9 tests) — mock-daemon
43
+ coverage:
44
+ - `--submit-force` adds `force:true` to `/submit` body; success line
45
+ renders `[forced]` tag.
46
+ - bare `--submit` does NOT add `force` to body.
47
+ - default `--submit-retry 1` retries once on `gated_dispatch_unconsumed`
48
+ 504 then succeeds; output contains `[retry 1/1]`.
49
+ - `--submit-retry 2` exhausts to 3 calls then prints
50
+ `Submit gated-timeout … after 3 attempts`.
51
+ - `--submit-retry 0` makes exactly 1 call, no `[retry`.
52
+ - `session_dead` 504 → no retry even with `--submit-retry 3`.
53
+ - `no_state` 504 → no retry even with `--submit-retry 3`.
54
+ - `--submit-force --submit-retry 2` preserves `force:true` across retries.
55
+ - 500 error → no retry, prints to stderr.
56
+ - `test/enforce-report.test.js` — version assertion 0.2.0 → 0.3.3.
57
+ - All 174 existing tests pass unchanged.
58
+
59
+ ### Invariants preserved
60
+
61
+ - Daemon code unchanged. `force:true` and the gate layers behave exactly
62
+ as in 0.3.2.
63
+ - `telepty send-key` unchanged.
64
+ - `telepty enter` unchanged.
65
+ - `telepty inject --ref` (no `--submit`) unchanged.
66
+ - Cross-machine remote inject path unchanged (the SSH branch in `cli.js`
67
+ bypasses the new flags by design — remote daemons handle their own
68
+ submit semantics).
69
+ - Exit code on soft failure (504) remains 0; orchestrator scripts that
70
+ check for non-zero exits are unaffected.
71
+
72
+ ## [0.3.2] — 2026-04-26
73
+
74
+ ### Added — Layer 3 prompt-symbol render gate (spec: `docs/superpowers/specs/2026-04-26-prompt-symbol-render-gate.md`)
75
+
76
+ Strictly additive layer above the 0.3.1 `sessionStateManager` gate. Closes
77
+ the recurring "Enter not applied on freshly-spawned `claude`/`codex`" trap
78
+ by directly observing the rendered terminal screen for a per-CLI prompt
79
+ symbol — the only deterministic ready-signal these TUIs expose to external
80
+ automation (no OSC 133, no exit-on-prompt, no socket signal).
81
+
82
+ - **`src/prompt-symbol-registry.js`** (NEW) — per-CLI prompt-symbol catalog:
83
+
84
+ | CLI | Symbol | Codepoint | UTF-8 | Geometry sanity |
85
+ |---|---|---|---|---|
86
+ | `claude` | `❯` | U+276F | `E2 9D AF` | sandwiched between U+2500 (`─`) horizontal-rule borders |
87
+ | `codex` | `›` | U+203A | `E2 80 BA` | model footer (`gpt-N…`) within 2 lines below |
88
+ | `gemini` | `*` | U+002A | `2A` | bracketed by U+2580 (`▀`) above / U+2584 (`▄`) below |
89
+
90
+ `lookup(command)` normalizes path + args (`/usr/local/bin/claude --resume`
91
+ → claude entry; `codex resume` → codex entry). Unknown CLIs return `null`,
92
+ causing the gate to skip cleanly via `unknown_cli`.
93
+
94
+ - **`src/submit-gate.js` `awaitPromptSymbol(session, opts)`** (NEW) — polls
95
+ `cmux read-screen --workspace <id> --lines <n>` (default 30) every
96
+ `pollIntervalMs` (default 150 ms) and resolves only when the symbol has
97
+ been stably detected for ≥ `stabilityMs` (default 200 ms). Bounded by
98
+ `timeoutMs` (default 8000 ms; clamp [500, 30000]). Resolves cleanly with
99
+ one of:
100
+ - `{ ready: true, last_seen_at, waited_ms }`
101
+ - `{ ready: false, reason: 'no_screen_primitive', waited_ms: 0 }` (non-cmux backend)
102
+ - `{ ready: false, reason: 'unknown_cli', waited_ms: 0 }`
103
+ - `{ ready: false, reason: 'no_prompt_symbol_seen', waited_ms }` (timeout, fall through)
104
+ Pure helper: `now`/`sleep`/`readScreen`/`registry` are all injectable for
105
+ deterministic tests (fakeClock harness from `verifyBodyConsumed`).
106
+
107
+ - **`daemon.js` POST /submit** — Layer 3 runs immediately before Layer 1
108
+ on the gated path. Result threaded into success and 504 response bodies
109
+ as optional `prompt_symbol: { found, waited_ms, [reason], [last_seen_at] }`.
110
+ **Never emits its own 504** — best-effort fall-through to Layer 1, which
111
+ retains all existing 0.3.1 outcomes (success / `gated_dispatch_unconsumed`
112
+ / hard-fail). Per-request bypass via `{ "prompt_symbol_gate": false }`
113
+ (Layer 3 only); `force:true` and `TELEPTY_SUBMIT_GATE=off` continue to
114
+ bypass BOTH layers.
115
+
116
+ ### Tests
117
+
118
+ - `test/prompt-symbol-registry.test.js` (NEW) — registry coverage with
119
+ inline cmux read-screen fixtures: claude/codex/gemini detect on idle
120
+ screens, banner-stage rejection (no border geometry), history-echo
121
+ disambiguation (LAST occurrence anchored), `lookup()` path/args
122
+ normalization + case-insensitivity + unknown/null inputs, `byteSeq`
123
+ matches `Buffer.from(symbol, 'utf8')`.
124
+ - `test/submit-gate.test.js` (extended) — `awaitPromptSymbol` covers:
125
+ non-cmux → `no_screen_primitive`; missing workspace → same; unknown CLI
126
+ → `unknown_cli`; stable claude/codex screen → ready after `stabilityMs`;
127
+ empty `readScreen` returns → `no_prompt_symbol_seen` after `timeoutMs`;
128
+ symbol-then-disappear → stability streak resets; injected registry
129
+ override is honored; `readScreen` receives `(workspaceId, tailLines)`.
130
+
131
+ ### Invariants preserved
132
+
133
+ - All 32 existing `test/submit-gate.test.js` tests pass unchanged.
134
+ - `force: true` and `TELEPTY_SUBMIT_GATE=off` bypass BOTH layers.
135
+ - Layer 1 hard-fail short-circuits (`session_dead`/`error`/`restarting`/
136
+ `no_state`/`no_state_manager`) still emit 504; Layer 3 never adds a new
137
+ 504 source.
138
+ - `inject --ref` (no `--submit`) path unchanged.
139
+ - aterm / non-cmux backends skip Layer 3 cleanly via `no_screen_primitive`.
140
+ - Cross-machine remote inject unchanged: Layer 3 runs only on the daemon
141
+ with cmux access; remote daemons fall through.
142
+ - Response shape additive — `prompt_symbol` is an optional field; existing
143
+ callers ignore unknown JSON keys.
144
+
145
+ ## [0.3.1] — 2026-04-26
146
+
147
+ ### Fixed — submit-gate regression cluster (spec: `docs/superpowers/specs/2026-04-26-submit-gate-fixes-v2.md`)
148
+
149
+ Three regressions surfaced post-`0.3.0` against fresh-spawned `claude`/`codex`
150
+ sessions where the gate's strict thresholds and timeout-abandon path made the
151
+ new `/submit` endpoint less reliable than the pre-`0.3.0` blind retry on cold
152
+ REPLs. All three fixes ship in this single patch.
153
+
154
+ - **δ-fix-2 — `send-key` bypass (P0).** `POST /api/sessions/:id/submit` now
155
+ accepts `{ "force": true }` to skip the render-readiness gate and verify
156
+ step, dispatching once via the existing kitty/cmux/PTY chain. `cli.js`
157
+ `send-key` always sets `force:true`, restoring the manual Enter override.
158
+ Response shape additive (`forced:true`); existing callers unaffected.
159
+ - **δ-fix-3 — gate threshold relaxed 0.85 → 0.5 (P1).** `sessionStateManager`
160
+ emits IDLE `confidence=0.6` when neither OSC 133 nor a shell-prompt pattern
161
+ matches (`session-state.js:380`) — the dominant case for AI-CLI TUIs whose
162
+ Unicode-box input line bypasses `PROMPT_PATTERNS`. Default `minConfidence`
163
+ lowered to `0.5` (below the 0.6 silence-fallback with margin); per-request
164
+ override `min_confidence` body field accepted (clamped `[0, 1]`).
165
+ - **δ-fix-4 — timeout extension + best-effort dispatch on timeout (P1).**
166
+ Default `gate_timeout_ms` raised `5000 → 10000` (upper clamp `15000 →
167
+ 30000`) to cover empirical `claude` ready window (3-6 s on fresh spawn).
168
+ On a plain `timeout` reason, `/submit` now dispatches anyway and verifies
169
+ body consumption — the pre-`0.3.0` blind dispatch is restored as a fallback
170
+ while keeping the new honesty signal: 504 only fires when
171
+ `verifyBodyConsumed` confirms the body is still in the input box (new
172
+ `reason: 'gated_dispatch_unconsumed'`). Dispatch-on-timeout success path
173
+ adds `gated_dispatch_after_timeout: true` (additive).
174
+ Hard-fail reasons (`session_dead`/`error`/`restarting`/`no_state`) still
175
+ short-circuit to 504 immediately.
176
+
177
+ ### Invariants preserved
178
+
179
+ - `inject --submit` warm-session reliability ≥99% target (gate short-circuits
180
+ at conf≥0.85 still passes after default drops to 0.5).
181
+ - 504 still emitted in true-fail case (after best-effort dispatch + verify
182
+ reports `still_visible`).
183
+ - `TELEPTY_SUBMIT_GATE=off` daemon-wide escape hatch preserved.
184
+ - `inject --ref` (no `--submit`) path unchanged.
185
+ - 22/23 existing `test/submit-gate.test.js` tests pass unchanged; one test
186
+ (line 185-193) updated to preserve the below-threshold-rejection semantic
187
+ with literals shifted away from the new 0.5 default.
188
+
189
+ ## [0.3.0] — 2026-04-26
190
+
191
+ ### Added — render-gated submit (specs: `docs/superpowers/specs/2026-04-26-inject-submit-enter-reliability.md`)
192
+
193
+ - **`src/submit-gate.js`** — pure helpers exported for unit tests:
194
+ - `awaitReplReady(sessionId, stateManager, opts)` — waits for the target REPL
195
+ to reach an input-ready state (`idle` or `waiting`) with confidence ≥ 0.85
196
+ before Enter is fired. Bounded by `timeoutMs` (default 5000).
197
+ - `verifyBodyConsumed(session, bodyText, opts)` — polls the session's
198
+ `outputRing` for the inject body to disappear from the input box,
199
+ confirming Enter was actually consumed by the REPL (default 1500 ms,
200
+ 200 ms interval). Optimistic when body never visible (ANSI/wrap edge).
201
+ - `isReady`, `isFailed`, `READY_STATES`, `FAIL_STATES` — test surface.
202
+ - **POST `/api/sessions/:id/submit`** rewritten to use the gate by default.
203
+ Flow: gate on REPL readiness → dispatch via existing kitty/cmux/PTY chain →
204
+ verify consumption (when caller passes `injected_body`) → bounded retry.
205
+ Response now includes `gated`, `gate_wait_ms`, `verify` (when applicable).
206
+ - **HTTP `504 gate_timeout` response** on `/api/sessions/:id/submit` when the
207
+ REPL never readies for input within `gate_timeout_ms` (default 5000).
208
+ This is **why this is a minor bump** — consumers may need to handle the new
209
+ status code. 504 (Gateway Timeout) is the correct semantic versus 408 or
210
+ reused 503 — the daemon acted as a gateway to the upstream REPL and the
211
+ upstream did not respond in time.
212
+ - **CLI `inject --submit`** now passes `injected_body` to the daemon for
213
+ consumption verification, removed the legacy 500 ms blind sleep
214
+ (gate handles timing), and treats 504 as a soft failure (logs a clear
215
+ remediation hint, exits 0 — orchestrator scripts depend on exit 0 for
216
+ recoverable conditions).
217
+ - New body fields accepted by `/submit`: `injected_body`, `gate_timeout_ms`,
218
+ `verify_timeout_ms`. Existing `pre_delay_ms` / `retries` / `retry_delay_ms`
219
+ remain accepted for back-compat.
220
+ - **`TELEPTY_SUBMIT_GATE=off`** env var — escape hatch to revert to the 0.2.x
221
+ blind retry path for parity testing or rollback.
222
+
223
+ ### Changed
224
+
225
+ - POST `/api/sessions/:id/submit` is no longer open-loop. Default behavior
226
+ is gated; legacy blind retry preserved only behind `TELEPTY_SUBMIT_GATE=off`.
227
+ - CLI `✅ Submitted via <strategy>` line now optionally appends
228
+ `[gate <N>ms]` when the gate had to wait. Default-on; pre-existing
229
+ format preserved when gate fast-paths (warm sessions).
230
+ - `bus` event `submit` now carries optional fields `gated`, `gate_wait_ms`,
231
+ `verify` (additive — consumers ignore unknown fields).
232
+
233
+ ### Fixed
234
+
235
+ - Root cause: `/submit` previously fired Enter open-loop with a ~2.1 s
236
+ blind retry budget while a fresh `claude` REPL needed 3–6 s to render
237
+ (welcome banner, trust dialog, prompt setup). The legacy retry loop
238
+ also discarded `terminalLevelSubmit`'s return value, so the reported
239
+ `(N attempts)` count did not reflect verified dispatches. The new
240
+ gate observes the existing `sessionStateManager` (`idle` / `waiting`
241
+ with confidence ≥ 0.85) before dispatch, eliminating the race.
242
+ - Recurring orchestrator UX trap (parallel to #329 Track E27) where
243
+ every `inject --submit` required a manual `sleep N && telepty send-key
244
+ <id> enter` follow-up. Spec target: ≥ 99% on a 100× spawn-and-inject
245
+ E2E harness (current baseline ~0%); E2E harness execution is dispatched
246
+ to the builder (out of scope for this commit).
247
+
248
+ ### Tests
249
+
250
+ - `test/submit-gate.test.js` — 23 new unit tests (all pass) covering
251
+ `awaitReplReady` fast-paths, transition resolution, timeout, fail-state
252
+ short-circuits; `verifyBodyConsumed` happy-path / optimistic / timeout /
253
+ empty / no-ring / whitespace normalization / ANSI strip / injectable
254
+ clock for deterministic timing.
255
+ - Pre-existing test suite is unmodified; integration coverage of the new
256
+ endpoint behavior is delegated to the builder per SAWP scope.
257
+
258
+ ### Compatibility / migration
259
+
260
+ - **Default behavior changes** for callers of `/api/sessions/:id/submit`:
261
+ responses now succeed only when the REPL reaches readiness within
262
+ `gate_timeout_ms`. Most callers will see equivalent or better behavior;
263
+ callers that depended on "best effort fire-and-forget" can opt out via
264
+ `TELEPTY_SUBMIT_GATE=off`.
265
+ - `inject --ref` (without `--submit`), `telepty allow`, `telepty list`,
266
+ and `telepty send-key` semantics unchanged.
267
+ - Aterm sessions unaffected (gate is bypassed via existing
268
+ `session.type === 'aterm'` guards).
269
+ - No new external dependencies (Rule 17). No schema, persistence, or
270
+ state-machine changes (gate is read-only on `sessionStateManager`).
271
+
272
+ ## [0.2.0] — 2026-04-15
273
+
274
+ ### Added — REPORT enforcement (specs/enforce-report-spec.md)
275
+
276
+ - **New bus event types** for observable REPORT lifecycle:
277
+ - `TASK_IDLE_NO_REPORT` — fires once on idle transition for inject-driven sessions
278
+ - `TASK_COMPLETE_WITH_REPORT` — fires when matching REPORT inject detected via reverse-match
279
+ - `TASK_BLOCKED_WITH_REASON` — fires on `STATUS: blocked` reply inject
280
+ - `TASK_DISMISSED` — fires on `STATUS: dismissed` inject OR via DELETE endpoint
281
+ - `TASK_DEAD_NO_REPORT` — fires when session dies with pending report (attaches `auto_summary`)
282
+ - **New HTTP endpoints** on daemon:
283
+ - `GET /api/pendingReports/:id` — inspect pending report entry + optional auto_summary
284
+ - `DELETE /api/pendingReports/:id` — orchestrator-side dismissal; fires `TASK_DISMISSED`
285
+ - **New module** `src/report-enforcement.js` exports pure helpers:
286
+ - `classifyReportPrompt(prompt)` — classify inject prompt by prefix
287
+ - `buildAutoSummary(session, opts)` — scrape last N non-blank lines from outputRing with ANSI stripping and secret redaction
288
+ - **REPORT detection via reverse-match** in POST `/api/sessions/:id/inject`:
289
+ - An inject with `from=X` whose prompt starts with a REPORT prefix (`REPORT:`, `STATUS:`, `SPEC:`, `OWNER-DIAGNOSIS:`, `ENFORCE-SPEC:`, `ENFORCE-IMPLEMENTED:`, `LOG-FIX-SPEC:`, `LOG-FIX-IMPLEMENTED:`, `FIX-SPEC:`, `FIX-IMPLEMENTED:`, `SPEC-SYNC:`, `DIAGNOSIS:`) and whose recipient matches `pendingReports[X].source` fires the matching enforcement event.
290
+ - Prevents false positives: prefix alone is NOT enough; reverse-match to originating inject required.
291
+ - **Auto-summary with secret redaction**:
292
+ - Strips ANSI via shared regex
293
+ - Filters blank lines
294
+ - Caps at `DELIBERATION_REPORT_AUTO_SUMMARY_LINES` (default 40) + `DELIBERATION_REPORT_AUTO_SUMMARY_MAX_BYTES` (default 4096)
295
+ - Redacts `api_key`, `password`, `token`, `secret` assignment patterns → `[REDACTED]`
296
+ - Attached to `TASK_DEAD_NO_REPORT` events and GET query responses
297
+
298
+ ### Changed
299
+
300
+ - `sessionStateManager.onTransition` handler now fires the enforcement events above. Legacy `TASK_COMPLETE:` text-inject to source session is preserved during 0.2.x grandfather period.
301
+ - Legacy auto-report paths (health-poll idle threshold + ready-WS signal) now coordinate via `pendingReports[id].idleNotified` flag to prevent double-fire.
302
+ - `pendingReports[id]` schema extended with `awaitingReport: true`, `idleNotified: bool`, `idleAt: ISO8601`. Entry is now cleared only when REPORT arrives, session dies, or orchestrator dismisses.
303
+ - Duplicate pendingReports overwrite now emits `[AUTO-REPORT] overwritten pending` warning.
304
+
305
+ ### Configuration (new env vars)
306
+
307
+ - `DELIBERATION_REPORT_AUTO_SUMMARY_ON_QUERY` — bool, default `true`. Gates auto_summary on GET pendingReports.
308
+ - `DELIBERATION_REPORT_AUTO_SUMMARY_LINES` — int, default 40. Max lines in auto_summary.
309
+ - `DELIBERATION_REPORT_AUTO_SUMMARY_MAX_BYTES` — int, default 4096. Byte cap on auto_summary.
310
+
311
+ ### Deprecated
312
+
313
+ - `reportTimeoutSecs` env var — emits deprecation warning if set. Removed in 0.3.x. Evidence (7.5s–649s task range) showed a default timer is arbitrary and prone to false timeouts; replaced with event-driven detection (idle + dead + explicit query).
314
+
315
+ ### Tests
316
+
317
+ - `test/report-enforcement.test.js` — 28 new unit tests for `classifyReportPrompt`, `buildAutoSummary`, regex exports
318
+ - `test/enforce-report.test.js` — 11 new integration tests for bus events and endpoints
319
+ - Full suite: **170/170 passing** (131 pre-existing + 39 new)
320
+
321
+ ### Migration notes
322
+
323
+ - **No orchestrator-side changes required** to benefit. New bus events flow passively; legacy `TASK_COMPLETE:` text-inject still fires.
324
+ - Consumers that subscribe to the bus now see richer event types — optional to consume.
325
+ - Orchestrators wanting to dismiss a pending report can use `DELETE /api/pendingReports/{id}`.
326
+ - Orchestrators wanting on-demand summary can use `GET /api/pendingReports/{id}` (honors `DELIBERATION_REPORT_AUTO_SUMMARY_ON_QUERY`).
package/CLAUDE.md CHANGED
@@ -65,10 +65,14 @@ npm version patch --no-git-tag-version && npm publish --access public
65
65
  - inject 시 발신자 session ID (`--from`)를 항상 포함
66
66
  - PTY `\r` 직접 의존 금지
67
67
 
68
- ## 최근 주요 변경 (v0.1.58–0.1.62)
68
+ ## 최근 주요 변경 (v0.1.58–0.3.3)
69
69
 
70
70
  | 버전 | 변경 |
71
71
  |------|------|
72
+ | 0.3.3 | inject `--submit-force` (gate bypass) + idempotent `--submit-retry` (default 1, retry-safe 504만). 클라이언트 측 변경, daemon 무수정. task #347. |
73
+ | 0.3.2 | Layer 3 prompt-symbol 렌더 게이트 — claude/codex/gemini 별 prompt symbol을 cmux read-screen으로 polling. |
74
+ | 0.3.1 | 게이트 임계값 0.85 → 0.5 완화 + dispatch-on-timeout best-effort + send-key force=true 우회 추가. |
75
+ | 0.3.0 | Render-gated submit (sessionStateManager 기반). Enter 송신 전 REPL ready 검증. |
72
76
  | 0.1.62 | TUI 태스크 추적 — bus 이벤트에서 [태그] 자동 파싱, 세션별 상태 표시 |
73
77
  | 0.1.61 | reconnect 시 resize/\x0c 제거 (멀티터미널 깜빡임 수정) |
74
78
  | 0.1.60 | TUI P1 — s=start, k=kill, p=purge stale |
package/README.md CHANGED
@@ -53,6 +53,9 @@ telepty broadcast "status report"
53
53
  | `telepty list [--json]` | List sessions across all discovered hosts |
54
54
  | `telepty attach [id[@host]]` | Attach to a session (interactive picker if no ID) |
55
55
  | `telepty inject <id[@host]> "text"` | Inject text into a session |
56
+ | `telepty inject --submit <id> "text"` | Inject text and press Enter (render-gated, retries once on safe gate-timeout) |
57
+ | `telepty inject --submit --submit-force <id> "text"` | As above, but bypass the gate (skip Layer 1/3 detection — opt-in escape hatch) |
58
+ | `telepty inject --submit --submit-retry N <id> "text"` | Override retry count [0–3] on safe 504 (default 1) |
56
59
  | `telepty enter <id[@host]>` | Send Enter/Return to a session |
57
60
  | `telepty multicast <id1,id2> "text"` | Inject into multiple sessions |
58
61
  | `telepty broadcast "text"` | Inject into ALL sessions |
package/cli.js CHANGED
@@ -1550,6 +1550,32 @@ async function main() {
1550
1550
  const useSubmit = submitIndex !== -1;
1551
1551
  if (useSubmit) args.splice(submitIndex, 1);
1552
1552
 
1553
+ // Extract --submit-force flag (gate bypass; opt-in escape hatch).
1554
+ // Mirrors `telepty send-key`'s force semantics: skip both Layer 3 and
1555
+ // Layer 1 gates and dispatch Enter immediately. Safe only when the
1556
+ // caller is confident the target REPL is ready (e.g., orchestrator is
1557
+ // visibly idle). See specs/2026-05-02-submit-force-and-retry.md
1558
+ const submitForceIndex = args.indexOf('--submit-force');
1559
+ const submitForce = submitForceIndex !== -1;
1560
+ if (submitForce) args.splice(submitForceIndex, 1);
1561
+
1562
+ // Extract --submit-retry N flag (default 1, clamp [0, 3]). On a 504
1563
+ // gated-failure with a retry-safe reason (gate timed out and body is
1564
+ // still in the input box → idempotent), wait 300ms and retry. Hard-fail
1565
+ // reasons (session_dead/error/restarting/no_state) do NOT retry —
1566
+ // re-firing won't recover and would be a wasted round-trip.
1567
+ let submitRetries = 1;
1568
+ const submitRetryIndex = args.indexOf('--submit-retry');
1569
+ if (submitRetryIndex !== -1) {
1570
+ const raw = Number(args[submitRetryIndex + 1]);
1571
+ if (Number.isFinite(raw)) {
1572
+ submitRetries = Math.min(Math.max(Math.floor(raw), 0), 3);
1573
+ args.splice(submitRetryIndex, 2);
1574
+ } else {
1575
+ args.splice(submitRetryIndex, 1);
1576
+ }
1577
+ }
1578
+
1553
1579
  // Extract --from flag
1554
1580
  let fromId;
1555
1581
  const fromIndex = args.indexOf('--from');
@@ -1638,23 +1664,84 @@ async function main() {
1638
1664
  const refSuffix = referencePath ? ` (ref: ${referencePath})` : '';
1639
1665
  console.log(`✅ Context injected successfully into '\x1b[36m${target.id}\x1b[0m'.${refSuffix}`);
1640
1666
 
1641
- // Terminal-level submit: POST /submit after text injection
1667
+ // Terminal-level submit: POST /submit after text injection.
1668
+ // Daemon-side render-gate handles timing (waits for REPL readiness),
1669
+ // so the CLI no longer needs the legacy 500ms blind sleep. Pass the
1670
+ // injected body so the daemon can verify it was consumed by the input
1671
+ // box and bounded-retry once if not.
1672
+ //
1673
+ // 0.3.3: opt-in --submit-force (gate bypass) and idempotent client-side
1674
+ // retry on retry-safe 504s. The retry guard is gate timeout + body
1675
+ // still visible in the input box (verify.consumed=false) — re-firing
1676
+ // an Enter that genuinely never landed cannot double-submit.
1677
+ // See docs/superpowers/specs/2026-04-26-inject-submit-enter-reliability.md
1642
1678
  if (useSubmit) {
1643
- await new Promise(resolve => setTimeout(resolve, 500));
1644
- try {
1645
- const submitRes = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/submit`, {
1646
- method: 'POST',
1647
- headers: { 'Content-Type': 'application/json' },
1648
- body: JSON.stringify({ pre_delay_ms: 600, retries: 2, retry_delay_ms: 500 })
1649
- });
1650
- const submitData = await submitRes.json();
1651
- if (submitRes.ok) {
1652
- console.log(`✅ Submitted via ${submitData.strategy}${submitData.attempts > 1 ? ` (${submitData.attempts} attempts)` : ''}.`);
1653
- } else {
1654
- console.error(`⚠️ Submit failed: ${formatApiError(submitData)}`);
1679
+ const submitBody = {
1680
+ injected_body: injectPrompt || '',
1681
+ retries: 1,
1682
+ retry_delay_ms: 500,
1683
+ ...(submitForce ? { force: true } : {}),
1684
+ };
1685
+ const RETRY_DELAY_MS = 300;
1686
+ const RETRY_SAFE_REASONS = new Set([
1687
+ 'gated_dispatch_unconsumed',
1688
+ 'gate_timeout',
1689
+ 'no_prompt_symbol_seen',
1690
+ ]);
1691
+ const maxAttempts = 1 + submitRetries;
1692
+ let submitRes = null;
1693
+ let submitData = null;
1694
+ let attemptsMade = 0;
1695
+ let lastError = null;
1696
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1697
+ if (attempt > 0) {
1698
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
1699
+ }
1700
+ attemptsMade = attempt + 1;
1701
+ try {
1702
+ submitRes = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/submit`, {
1703
+ method: 'POST',
1704
+ headers: { 'Content-Type': 'application/json' },
1705
+ body: JSON.stringify(submitBody),
1706
+ });
1707
+ submitData = await submitRes.json();
1708
+ } catch (submitErr) {
1709
+ lastError = submitErr;
1710
+ submitRes = null;
1711
+ submitData = null;
1712
+ break;
1655
1713
  }
1656
- } catch (submitErr) {
1657
- console.error(`⚠️ Submit failed: ${submitErr.message}`);
1714
+ if (submitRes.ok) break;
1715
+ if (submitRes.status !== 504) break;
1716
+ const retryReason = submitData && typeof submitData.reason === 'string' ? submitData.reason : null;
1717
+ if (!RETRY_SAFE_REASONS.has(retryReason)) break;
1718
+ }
1719
+ if (lastError) {
1720
+ console.error(`⚠️ Submit failed: ${lastError.message}`);
1721
+ } else if (submitRes && submitRes.ok) {
1722
+ const gateNote = submitData.gated && submitData.gate_wait_ms > 0
1723
+ ? ` [gate ${submitData.gate_wait_ms}ms]`
1724
+ : '';
1725
+ const lateNote = submitData.gated_dispatch_after_timeout
1726
+ ? ' (dispatched-after-gate-timeout)'
1727
+ : '';
1728
+ const attemptsNote = submitData.attempts > 1 ? ` (${submitData.attempts} attempts)` : '';
1729
+ const retryNote = attemptsMade > 1 ? ` [retry ${attemptsMade - 1}/${submitRetries}]` : '';
1730
+ const forcedNote = submitData.forced ? ' [forced]' : '';
1731
+ console.log(`✅ Submitted via ${submitData.strategy}${attemptsNote}${gateNote}${lateNote}${retryNote}${forcedNote}.`);
1732
+ } else if (submitRes && submitRes.status === 504) {
1733
+ // Soft failure: REPL never readied. Orchestrator scripts depend on
1734
+ // exit 0 here — surface a clear remediation hint but do not exit
1735
+ // non-zero.
1736
+ const reason = (submitData && submitData.reason) || 'gate_timeout';
1737
+ const lastState = (submitData && submitData.last_state) || 'unknown';
1738
+ const retriesNote = attemptsMade > 1 ? ` after ${attemptsMade} attempts` : '';
1739
+ const hint = submitForce
1740
+ ? ''
1741
+ : ` Try \`telepty inject --submit --submit-force ${target.id} ...\` or manual \`telepty send-key ${target.id} enter\`.`;
1742
+ console.log(`⚠️ Submit gated-timeout (${reason}, last_state=${lastState})${retriesNote}.${hint}`);
1743
+ } else {
1744
+ console.error(`⚠️ Submit failed: ${formatApiError(submitData)}`);
1658
1745
  }
1659
1746
  }
1660
1747
  } catch (e) { console.error(`❌ ${e.message || 'Failed to connect to the target daemon.'}`); }
@@ -1719,7 +1806,13 @@ async function main() {
1719
1806
  process.exit(1);
1720
1807
  }
1721
1808
 
1722
- const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/submit`, { method: 'POST' });
1809
+ // send-key is a manual override bypass the render gate via force=true.
1810
+ // See: docs/superpowers/specs/2026-04-26-submit-gate-fixes-v2.md §3.1
1811
+ const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/submit`, {
1812
+ method: 'POST',
1813
+ headers: { 'Content-Type': 'application/json' },
1814
+ body: JSON.stringify({ force: true }),
1815
+ });
1723
1816
  const data = await res.json();
1724
1817
  if (!res.ok) { console.error(`❌ ${formatApiError(data)}`); return; }
1725
1818
  console.log(`✅ Key '${key}' sent to '\x1b[36m${target.id}\x1b[0m'. (strategy: ${data.strategy})`);