@dmsdc-ai/aigentry-telepty 0.1.97 → 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 +326 -0
- package/CLAUDE.md +5 -1
- package/README.md +3 -0
- package/cli.js +109 -16
- package/daemon.js +431 -42
- package/docs/superpowers/specs/2026-04-26-inject-submit-enter-reliability.md +447 -0
- package/docs/superpowers/specs/2026-04-26-prompt-symbol-render-gate.md +571 -0
- package/docs/superpowers/specs/2026-04-26-submit-gate-fixes-v2.md +608 -0
- package/docs/superpowers/specs/2026-05-02-submit-force-and-retry.md +139 -0
- package/package.json +4 -4
- package/specs/codex-inject-spec.md +201 -0
- package/specs/enforce-report-spec.md +237 -0
- package/src/prompt-symbol-registry.js +97 -0
- package/src/report-enforcement.js +86 -0
- package/src/submit-gate.js +269 -0
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.
|
|
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
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
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
|
-
|
|
1657
|
-
|
|
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
|
-
|
|
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})`);
|