@dmsdc-ai/aigentry-telepty 0.1.98 → 0.3.5

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 (44) hide show
  1. package/AGENTS.md +23 -0
  2. package/CHANGELOG.md +436 -0
  3. package/CLAUDE.md +5 -1
  4. package/README.md +70 -1
  5. package/cli.js +232 -53
  6. package/cross-machine.js +132 -0
  7. package/daemon.js +399 -39
  8. package/docs/reports/2026-05-05-issue-8-claude-review.md +194 -0
  9. package/docs/specs/2026-05-05-issue-8-telepty-init.md +477 -0
  10. package/docs/superpowers/specs/2026-04-26-inject-submit-enter-reliability.md +447 -0
  11. package/docs/superpowers/specs/2026-04-26-prompt-symbol-render-gate.md +571 -0
  12. package/docs/superpowers/specs/2026-04-26-submit-gate-fixes-v2.md +608 -0
  13. package/docs/superpowers/specs/2026-05-02-submit-force-and-retry.md +139 -0
  14. package/host-spec.js +60 -0
  15. package/mcp-server/index.mjs +24 -3
  16. package/package.json +6 -5
  17. package/scripts/regen-snippet-fixtures.js +42 -0
  18. package/skill-installer.js +42 -6
  19. package/skills/telepty/SKILL.md +1 -1
  20. package/skills/telepty-allow/SKILL.md +1 -1
  21. package/skills/telepty-attach/SKILL.md +1 -1
  22. package/skills/telepty-broadcast/SKILL.md +1 -1
  23. package/skills/telepty-daemon/SKILL.md +1 -1
  24. package/skills/telepty-inject/SKILL.md +76 -4
  25. package/skills/telepty-list/SKILL.md +1 -1
  26. package/skills/telepty-listen/SKILL.md +1 -1
  27. package/skills/telepty-rename/SKILL.md +1 -1
  28. package/skills/telepty-session/SKILL.md +1 -1
  29. package/specs/enforce-report-spec.md +237 -0
  30. package/src/init/print-snippet.js +114 -0
  31. package/src/init/snippets/agents.md +15 -0
  32. package/src/init/snippets/claude.md +15 -0
  33. package/src/init/snippets/gemini.md +15 -0
  34. package/src/prompt-symbol-registry.js +97 -0
  35. package/src/report-enforcement.js +86 -0
  36. package/src/submit-gate.js +269 -0
  37. package/tests/snippet-protocol/v1/golden-agents.json +1 -0
  38. package/tests/snippet-protocol/v1/golden-agents.md +17 -0
  39. package/tests/snippet-protocol/v1/golden-all.json +3 -0
  40. package/tests/snippet-protocol/v1/golden-all.md +53 -0
  41. package/tests/snippet-protocol/v1/golden-claude.json +1 -0
  42. package/tests/snippet-protocol/v1/golden-claude.md +17 -0
  43. package/tests/snippet-protocol/v1/golden-gemini.json +1 -0
  44. package/tests/snippet-protocol/v1/golden-gemini.md +17 -0
@@ -0,0 +1,139 @@
1
+ # 2026-05-02 — `inject --submit-force` + idempotent client retry
2
+
3
+ Closes task #347 (telepty 0.3.2 `--submit` prompt-symbol gate reliability —
4
+ context-ref inject arrived at orchestrator but Enter was skipped when the
5
+ input area had a transient render mismatch: autocomplete dropdown open,
6
+ cursor moved, mid-render race).
7
+
8
+ ## Problem
9
+
10
+ `telepty inject --submit` runs three layers of gating before pressing
11
+ Enter:
12
+
13
+ | Layer | File | Trigger | Skip behavior |
14
+ |---|---|---|---|
15
+ | 3. Prompt-symbol (0.3.2) | `src/submit-gate.js` `awaitPromptSymbol` | `cmux read-screen` does not show the per-CLI prompt symbol stably for ≥200 ms within 8 s | Falls through to Layer 1 (`no_prompt_symbol_seen`) |
16
+ | 1. State-gated (0.3.1) | `src/submit-gate.js` `awaitReplReady` | `sessionStateManager` is not in `idle`/`waiting` with conf ≥ 0.5 within 10 s | Best-effort dispatch on `timeout`; hard-fail short-circuits to 504 on `session_dead`/`error`/`restarting`/`no_state` |
17
+ | Verify | `src/submit-gate.js` `verifyBodyConsumed` | Injected body still visible in `outputRing` after dispatch | One bounded retry; if still visible, 504 with `reason: 'gated_dispatch_unconsumed'` |
18
+
19
+ In production this still produces a residual failure rate when the
20
+ orchestrator session has a transient render mismatch (autocomplete drop-down,
21
+ cursor outside input area, mid-paste). The body is injected, the gate times
22
+ out, the dispatch fires Enter into a "wrong" focus, and `verifyBodyConsumed`
23
+ correctly sees the body still in the input box → 504. Sub-sessions then
24
+ print `⚠️ Submit gated-timeout` and the human user has to press Enter
25
+ manually for the orchestrator to consume the inject.
26
+
27
+ ## Constraints
28
+
29
+ - **Article 1 (경량)**: minimum-touch fix. No new modules, no new daemon
30
+ endpoint, no new helper module.
31
+ - **Article 17 (무의존)**: no new runtime dependency.
32
+ - **Article 9 (독립)**: telepty must keep working standalone (no cmux/kitty
33
+ required for the new flags).
34
+ - **Backward compat**: existing `--submit` semantics unchanged. Default
35
+ `--submit-retry` value MUST be 0-effect on the happy path (which is the
36
+ vast majority of calls, currently shipping reliably).
37
+ - **Idempotency**: a retry must never double-press Enter.
38
+
39
+ ## Approach
40
+
41
+ Two opt-in CLI knobs on `telepty inject`, both implemented client-side
42
+ in `cli.js`. Daemon `/submit` endpoint is untouched — `force: true` is
43
+ already supported (introduced in 0.3.1 for `telepty send-key`); we just
44
+ plumb it through from the inject path.
45
+
46
+ ### `--submit-force`
47
+
48
+ Adds `force: true` to the `/submit` POST body. Daemon-side this skips
49
+ both Layer 3 (prompt-symbol) and Layer 1 (state-gate) and dispatches Enter
50
+ once via the existing `terminalLevelSubmit` chain (kitty → cmux → PTY).
51
+
52
+ Use case: caller is confident the target REPL is ready (e.g., orchestrator
53
+ visibly idle, or Phase-6 cascade where sub-session has just verified the
54
+ orchestrator's last bus event). Mirrors the existing `telepty send-key`
55
+ escape hatch but at the inject level so a single command does both.
56
+
57
+ ### `--submit-retry N` (default 1, clamp [0, 3])
58
+
59
+ After a 504 from `/submit` with a **retry-safe** reason, wait 300 ms and
60
+ re-issue the same `/submit` request up to N times. Retry-safe reasons:
61
+
62
+ | Reason | Source | Why retry is idempotent |
63
+ |---|---|---|
64
+ | `gated_dispatch_unconsumed` | `daemon.js:1680` | The verify path saw the body STILL in the input box after best-effort dispatch. Re-firing Enter when the body is visibly un-consumed cannot double-submit. |
65
+ | `gate_timeout` | `awaitReplReady` returning `timeout` (no longer reaches 504 directly in 0.3.1, but kept for forward-compat) | Same: body has not been consumed if we're still on the gated path. |
66
+ | `no_prompt_symbol_seen` | `awaitPromptSymbol` Layer 3 timeout (also not currently a 504 source, but kept for forward-compat) | Layer 3 alone never emits 504 today. Listed for completeness. |
67
+
68
+ Retry is **explicitly NOT** safe for hard-fail reasons — `session_dead`,
69
+ `session_error`, `session_restarting`, `no_state`, `no_state_manager`. Those
70
+ short-circuit the loop immediately because re-firing won't recover. Same
71
+ for any non-504 status (4xx) — no point retrying a malformed request.
72
+
73
+ The retry preserves the original flag set (`force` stays `force`, etc.).
74
+ The `attemptsMade` counter is rendered into the success line as
75
+ `[retry K/N]` so operators can see when the retry path actually fired.
76
+
77
+ ### Why client-side (not daemon-side)?
78
+
79
+ - Server-side already retries once internally inside `verifyBodyConsumed`
80
+ (`daemon.js:1663-1672`). Adding a second loop server-side conflates two
81
+ feedback signals (the inner verify retry vs. the outer client retry) in
82
+ one response shape.
83
+ - Per-call client control is more flexible — sub-sessions that have
84
+ cheap evidence of orchestrator readiness can pass `--submit-retry 0`
85
+ to avoid the extra round-trip; ones that don't can pass `--submit-retry 2`.
86
+ - Keeps the daemon stable. 0.3.0 cluster (memory:
87
+ `feedback_telepty_send_key_regression.md`) was a daemon-side change that
88
+ rippled into manual-override breakage. Client-side change has a strictly
89
+ smaller blast radius.
90
+
91
+ ## File map
92
+
93
+ | File | Change | LoC delta |
94
+ |---|---|---|
95
+ | `cli.js` (inject command) | Parse `--submit-force` + `--submit-retry`. Wrap existing `useSubmit` block in idempotent retry loop on 504-with-safe-reason. | +~55, -~25 |
96
+ | `test/cli.test.js` | Three new tests: --submit-force passes force=true; --submit-retry retries on safe-reason 504; --submit-retry does NOT retry on hard-fail 504. | +~120 |
97
+ | `CHANGELOG.md` | 0.3.3 entry. | +~30 |
98
+ | `package.json` | 0.3.2 → 0.3.3. | +1, -1 |
99
+ | `test/enforce-report.test.js:280` | Update stale version assertion 0.2.0 → 0.3.3. | +1, -1 |
100
+ | `README.md` | Mention new flags in inject summary. | +~6 |
101
+
102
+ No new files outside `test/` and `docs/`. No daemon changes. No new
103
+ dependencies. Total surface ≪ 200 LoC including tests.
104
+
105
+ ## Tests
106
+
107
+ ### Unit / integration (`test/cli.test.js`)
108
+
109
+ 1. **`--submit-force` passes `force: true` to /submit**
110
+ Spawn a session, intercept `/submit` (use existing harness method or
111
+ inspect bus event), invoke `telepty inject --submit --submit-force <id>
112
+ "x"`, assert daemon received `{ force: true }` in the request body.
113
+
114
+ 2. **`--submit-retry N` retries on safe-reason 504**
115
+ Mock the daemon to return 504 `{reason: 'gated_dispatch_unconsumed'}`
116
+ on the first call and 200 on the second. Assert the CLI made exactly
117
+ 2 POST /submit calls and exited 0. Assert `[retry 1/N]` is present
118
+ in stdout.
119
+
120
+ 3. **`--submit-retry N` does NOT retry on hard-fail 504**
121
+ Mock the daemon to return 504 `{reason: 'session_dead'}`. Assert the
122
+ CLI made exactly 1 POST /submit call (no retry).
123
+
124
+ ### Regression — full suite
125
+
126
+ `npm test` — 229 tests, all should pass after updating the stale
127
+ `enforce-report.test.js:280` version assertion.
128
+
129
+ ## Future-proofing notes
130
+
131
+ - If the daemon adds new 504 reasons, they are by default **NOT** retry-
132
+ safe (the safe set is an explicit allowlist). Adding a new safe reason
133
+ is a one-line `RETRY_SAFE_REASONS.add(...)` change in `cli.js`.
134
+ - The flag pair composes: `--submit-force --submit-retry 0` (force-once),
135
+ `--submit-force --submit-retry 2` (force, with idempotent retry on the
136
+ rare 503 — though force never returns 504 today).
137
+ - The 300 ms retry delay is a constant, not a flag, to keep the surface
138
+ small. Empirically chosen at the upper end of the architect's
139
+ 100–300 ms window for the autocomplete-dropdown-close case.
package/host-spec.js ADDED
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_PORT = 3848;
4
+
5
+ function parseHostSpec(value, defaultPort = DEFAULT_PORT) {
6
+ if (value === undefined || value === null || value === '') {
7
+ return { host: '127.0.0.1', port: defaultPort };
8
+ }
9
+
10
+ let raw = String(value).trim();
11
+ if (!raw) {
12
+ return { host: '127.0.0.1', port: defaultPort };
13
+ }
14
+
15
+ raw = raw.replace(/^https?:\/\//i, '');
16
+ raw = raw.replace(/\/.*$/, '');
17
+
18
+ const ipv6Bracketed = raw.match(/^\[([^\]]+)\](?::(\d+))?$/);
19
+ if (ipv6Bracketed) {
20
+ const port = ipv6Bracketed[2] ? Number(ipv6Bracketed[2]) : defaultPort;
21
+ return { host: ipv6Bracketed[1], port };
22
+ }
23
+
24
+ const colonCount = (raw.match(/:/g) || []).length;
25
+ if (colonCount > 1) {
26
+ return { host: raw, port: defaultPort };
27
+ }
28
+
29
+ const hostPort = raw.match(/^(.+):(\d+)$/);
30
+ if (hostPort) {
31
+ return { host: hostPort[1], port: Number(hostPort[2]) };
32
+ }
33
+
34
+ return { host: raw, port: defaultPort };
35
+ }
36
+
37
+ function formatHostForUrl(host) {
38
+ if (host && host.includes(':') && !host.startsWith('[')) {
39
+ return `[${host}]`;
40
+ }
41
+ return host;
42
+ }
43
+
44
+ function buildDaemonUrl(value, defaultPort = DEFAULT_PORT) {
45
+ const { host, port } = parseHostSpec(value, defaultPort);
46
+ return `http://${formatHostForUrl(host)}:${port}`;
47
+ }
48
+
49
+ function buildDaemonWsUrl(value, defaultPort = DEFAULT_PORT) {
50
+ const { host, port } = parseHostSpec(value, defaultPort);
51
+ return `ws://${formatHostForUrl(host)}:${port}`;
52
+ }
53
+
54
+ module.exports = {
55
+ DEFAULT_PORT,
56
+ parseHostSpec,
57
+ formatHostForUrl,
58
+ buildDaemonUrl,
59
+ buildDaemonWsUrl
60
+ };
@@ -34,9 +34,30 @@ function getAuthToken() {
34
34
  }
35
35
 
36
36
  function getDaemonUrl() {
37
- const port = process.env.TELEPTY_PORT || "3848";
38
- const host = process.env.TELEPTY_HOST || "127.0.0.1";
39
- return `http://${host}:${port}`;
37
+ // TELEPTY_HOST accepts: `host`, `host:port`, or `http://host:port`. Embedded
38
+ // port from TELEPTY_HOST is used unless TELEPTY_PORT is set explicitly.
39
+ const explicitPort = process.env.TELEPTY_PORT ? Number(process.env.TELEPTY_PORT) : null;
40
+ const raw = process.env.TELEPTY_HOST || "127.0.0.1";
41
+ let stripped = String(raw).trim().replace(/^https?:\/\//i, "").replace(/\/.*$/, "");
42
+ let host = stripped;
43
+ let embeddedPort = null;
44
+ const ipv6Bracketed = stripped.match(/^\[([^\]]+)\](?::(\d+))?$/);
45
+ if (ipv6Bracketed) {
46
+ host = ipv6Bracketed[1];
47
+ if (ipv6Bracketed[2]) embeddedPort = Number(ipv6Bracketed[2]);
48
+ } else {
49
+ const colonCount = (stripped.match(/:/g) || []).length;
50
+ if (colonCount === 1) {
51
+ const m = stripped.match(/^(.+):(\d+)$/);
52
+ if (m) {
53
+ host = m[1];
54
+ embeddedPort = Number(m[2]);
55
+ }
56
+ }
57
+ }
58
+ const port = explicitPort != null ? explicitPort : (embeddedPort != null ? embeddedPort : 3848);
59
+ const hostForUrl = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
60
+ return `http://${hostForUrl}:${port}`;
40
61
  }
41
62
 
42
63
  async function daemonFetch(endpoint, options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.1.98",
3
+ "version": "0.3.5",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",
@@ -9,9 +9,10 @@
9
9
  "telepty-mcp": "mcp-server/index.mjs"
10
10
  },
11
11
  "scripts": {
12
- "test": "node --test test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js",
13
- "test:watch": "node --test --watch test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js",
14
- "test:ci": "node --test --test-reporter=spec test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js"
12
+ "test": "node --test test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/init.test.js && git diff --exit-code tests/snippet-protocol/v1/",
13
+ "test:watch": "node --test --watch test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/init.test.js",
14
+ "test:ci": "node --test --test-reporter=spec test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/init.test.js && git diff --exit-code tests/snippet-protocol/v1/",
15
+ "regen-fixtures": "node scripts/regen-snippet-fixtures.js"
15
16
  },
16
17
  "keywords": [
17
18
  "pty",
@@ -48,7 +49,7 @@
48
49
  "node-pty": "^1.2.0-beta.11",
49
50
  "prompts": "^2.4.2",
50
51
  "update-notifier": "^5.1.0",
51
- "uuid": "^13.0.0",
52
+ "uuid": "^9.0.0",
52
53
  "ws": "^8.19.0",
53
54
  "zod": "^3.24.0"
54
55
  }
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { buildOutput } = require('../src/init/print-snippet');
6
+
7
+ const projectRoot = path.resolve(__dirname, '..');
8
+ const fixtureDir = path.join(projectRoot, 'tests', 'snippet-protocol', 'v1');
9
+ const targets = ['claude', 'agents', 'gemini', 'all'];
10
+ const formats = [
11
+ { name: 'markdown', ext: 'md' },
12
+ { name: 'json', ext: 'json' }
13
+ ];
14
+
15
+ function createCaptureStream() {
16
+ return {
17
+ value: '',
18
+ write(chunk) {
19
+ this.value += chunk;
20
+ }
21
+ };
22
+ }
23
+
24
+ fs.mkdirSync(fixtureDir, { recursive: true });
25
+
26
+ for (const target of targets) {
27
+ for (const format of formats) {
28
+ const stdout = createCaptureStream();
29
+ const stderr = createCaptureStream();
30
+ const code = buildOutput(['--print-snippet', '--target', target, '--format', format.name], {
31
+ stdout,
32
+ stderr
33
+ });
34
+
35
+ if (code !== 0) {
36
+ throw new Error(`failed to generate ${target} ${format.name}: ${stderr.value}`);
37
+ }
38
+
39
+ const fixturePath = path.join(fixtureDir, `golden-${target}.${format.ext}`);
40
+ fs.writeFileSync(fixturePath, stdout.value, 'utf8');
41
+ }
42
+ }
@@ -1,8 +1,10 @@
1
+ // LEGACY: grandfathered by ADR 2026-05-05-telepty-devkit-boundary §6.2.1. New installer behavior MUST land in @dmsdc-ai/aigentry-devkit. Bugfixes only.
1
2
  'use strict';
2
3
 
3
4
  const fs = require('fs');
4
5
  const os = require('os');
5
6
  const path = require('path');
7
+ const { execSync } = require('child_process');
6
8
  const prompts = require('prompts');
7
9
 
8
10
  const TARGET_CLIENTS = {
@@ -10,22 +12,44 @@ const TARGET_CLIENTS = {
10
12
  label: 'Claude Code',
11
13
  globalDir: () => path.join(os.homedir(), '.claude', 'skills'),
12
14
  projectDir: (cwd) => path.join(cwd, '.claude', 'skills'),
13
- defaultScope: 'global'
15
+ defaultScope: 'global',
16
+ homeDir: () => path.join(os.homedir(), '.claude'),
17
+ binary: 'claude'
14
18
  },
15
19
  codex: {
16
20
  label: 'Codex',
17
21
  globalDir: () => path.join(os.homedir(), '.codex', 'skills'),
18
22
  projectDir: (cwd) => path.join(cwd, '.codex', 'skills'),
19
- defaultScope: 'global'
23
+ defaultScope: 'global',
24
+ homeDir: () => path.join(os.homedir(), '.codex'),
25
+ binary: 'codex'
20
26
  },
21
27
  gemini: {
22
28
  label: 'Gemini',
23
29
  globalDir: () => path.join(os.homedir(), '.gemini', 'skills'),
24
30
  projectDir: (cwd) => path.join(cwd, '.gemini', 'skills'),
25
- defaultScope: 'project'
31
+ defaultScope: 'project',
32
+ homeDir: () => path.join(os.homedir(), '.gemini'),
33
+ binary: 'gemini'
26
34
  }
27
35
  };
28
36
 
37
+ function hasCliBinary(cmd) {
38
+ try {
39
+ execSync(`command -v ${cmd}`, { stdio: 'ignore', shell: '/bin/sh' });
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ function detectInstalledClients() {
47
+ return Object.keys(TARGET_CLIENTS).filter((key) => {
48
+ const client = TARGET_CLIENTS[key];
49
+ return fs.existsSync(client.homeDir()) || hasCliBinary(client.binary);
50
+ });
51
+ }
52
+
29
53
  function resolveSkillsSourceRoot(packageRoot = __dirname) {
30
54
  return path.join(packageRoot, 'skills');
31
55
  }
@@ -163,13 +187,23 @@ async function runInteractiveSkillInstaller(options = {}) {
163
187
  console.log(`Installing packaged skill: ${packagedSkills[0].name}`);
164
188
  }
165
189
 
190
+ const installedClients = options.detectClients
191
+ ? options.detectClients()
192
+ : detectInstalledClients();
193
+ const installedSet = new Set(installedClients);
194
+ const detectedLabel = installedClients.length
195
+ ? `Detected: ${installedClients.join(', ')}`
196
+ : 'No AI CLI detected — manual selection required';
197
+ console.log(detectedLabel);
198
+
166
199
  const selectedClientsAnswer = await promptImpl({
167
200
  type: 'multiselect',
168
201
  name: 'clients',
169
- message: 'Select target clients',
202
+ message: 'Select target clients (detected CLIs pre-selected)',
170
203
  choices: Object.entries(TARGET_CLIENTS).map(([key, value]) => ({
171
- title: value.label,
172
- value: key
204
+ title: `${value.label}${installedSet.has(key) ? ' ✓ detected' : ' (not detected)'}`,
205
+ value: key,
206
+ selected: installedSet.has(key)
173
207
  })),
174
208
  min: 1
175
209
  });
@@ -262,6 +296,8 @@ async function runInteractiveSkillInstaller(options = {}) {
262
296
  module.exports = {
263
297
  TARGET_CLIENTS,
264
298
  copySkillDirectory,
299
+ detectInstalledClients,
300
+ hasCliBinary,
265
301
  installSkillsWithPlan,
266
302
  listPackagedSkills,
267
303
  resolveTargetDirectory,
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: telepty
3
- description: Overview of telepty — PTY multiplexer for AI session orchestration. Use this when user asks "what is telepty" or needs a getting-started guide.
3
+ description: Overview of telepty — PTY multiplexer for AI session orchestration. Use this when user asks "what is telepty" or needs a getting-started guide. 키워드: 텔레프티, 텔레프티 개요, 시작하기, telepty 소개, 사용법, 가이드
4
4
  ---
5
5
 
6
6
  # telepty — Overview
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: telepty-allow
3
- description: Create telepty sessions by wrapping CLI processes. Covers the allow/enable/wrap command for session creation and PTY management.
3
+ description: Create telepty sessions by wrapping CLI processes. Covers the allow/enable/wrap command for session creation and PTY management. 키워드: 세션 생성, 세션 래핑, CLI 래핑, allow, 세션 만들기, PTY
4
4
  ---
5
5
 
6
6
  # telepty-allow — Create and Manage Sessions
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: telepty-attach
3
- description: Attach interactively to a telepty session to view output and send input in real-time.
3
+ description: Attach interactively to a telepty session to view output and send input in real-time. 키워드: 세션 접속, 세션 연결, 세션 들어가기, 어태치, attach, 실시간 보기
4
4
  ---
5
5
 
6
6
  # telepty-attach — Interactive Session Attachment
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: telepty-broadcast
3
- description: Send messages to multiple telepty sessions at once. Covers broadcast (all sessions) and multicast (selected targets).
3
+ description: Send messages to multiple telepty sessions at once. Covers broadcast (all sessions) and multicast (selected targets). 키워드: 전체 공지, 모든 세션에, 일괄 전송, 브로드캐스트, 멀티캐스트, 다중 주입
4
4
  ---
5
5
 
6
6
  # telepty-broadcast — Multi-Target Messaging
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: telepty-daemon
3
- description: Manage the telepty daemon — start, stop, repair, update, and TUI dashboard. Use when daemon is broken or needs maintenance.
3
+ description: Manage the telepty daemon — start, stop, repair, update, and TUI dashboard. Use when daemon is broken or needs maintenance. 키워드: 데몬 시작, 데몬 재시작, 데몬 종료, TUI, 대시보드, 데몬 상태, daemon
4
4
  ---
5
5
 
6
6
  # telepty-daemon — Daemon Management
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: telepty-inject
3
- description: Send messages, commands, and keystrokes to telepty sessions. Covers inject, enter, send-key, and reply commands.
3
+ description: Send messages, commands, and keystrokes to telepty sessions. Covers inject, enter, send-key, and reply commands. 키워드: 세션에 메시지, 메시지 보내기, 전달, 주입, inject, 응답, 답장, 키 입력
4
4
  ---
5
5
 
6
6
  # telepty-inject — Send Messages to Sessions
@@ -51,14 +51,34 @@ If you expect a reply, ALWAYS include `--from` so the target knows where to resp
51
51
  telepty inject <target> "your message" --from $(echo $TELEPTY_SESSION_ID)
52
52
  ```
53
53
 
54
- ### Cross-host inject
54
+ ### Cross-host inject — `<id>@<host>` syntax
55
55
 
56
- When the same session ID exists on multiple hosts:
56
+ To inject into a session running on a different machine, append `@<host>` to
57
+ the session ID. `<host>` can be a hostname, LAN IP, or Tailnet name.
57
58
 
58
59
  ```bash
59
- telepty inject session_id@remote-host "message"
60
+ # Hostname
61
+ telepty inject session_id@worker-01 "message"
62
+
63
+ # LAN IP (daemon must be reachable on port 3848)
64
+ telepty inject orchestrator-claude@172.28.4.165 "PING from build server"
65
+
66
+ # With return address (recommended for cross-host)
67
+ telepty inject orchestrator-claude@192.168.1.10 "task done" \
68
+ --from "$TELEPTY_SESSION_ID"
60
69
  ```
61
70
 
71
+ **Requirements**:
72
+ - The remote daemon must be reachable on port `3848` from the calling host
73
+ (firewall / LAN routing / Tailscale).
74
+ - No SSH or sshd is required on either side — the call hits the remote
75
+ daemon's HTTP API directly.
76
+ - Use the same `<id>@<host>` syntax for `attach`, `read-screen`, `enter`,
77
+ and `multicast` targets.
78
+
79
+ Use this when the same session ID exists on multiple hosts, or when the
80
+ calling host has no local daemon discovery (no Tailnet, mixed LAN, etc.).
81
+
62
82
  ## enter — Send Enter keystroke only
63
83
 
64
84
  ```bash
@@ -83,6 +103,58 @@ telepty reply "<message>"
83
103
 
84
104
  Automatically targets the session that last injected into yours (uses stored `lastInjectFrom`).
85
105
 
106
+ ## Report Convention (REPORT to orchestrator)
107
+
108
+ When a sub-session finishes a task or needs to escalate state, it reports to the
109
+ **orchestrator** session using the `REPORT:` prefix.
110
+
111
+ ### Hard rule: NEVER hardcode the orchestrator session ID
112
+
113
+ Session IDs are volatile runtime identifiers — they may change across hosts,
114
+ restarts, or topology shifts. Resolve the orchestrator ID at runtime from
115
+ `telepty list --json` instead.
116
+
117
+ ### Standard pattern (telepty 0.3.3+)
118
+
119
+ ```bash
120
+ # 1. Resolve orchestrator session ID at runtime (filter out role-suffixed peers)
121
+ ORCH_ID=$(telepty list --json | python3 -c "import json,sys; \
122
+ print(next(s['id'] for s in json.load(sys.stdin) \
123
+ if 'orchestrator' in s['id'] \
124
+ and not any(x in s['id'] for x in ('coder','reviewer','architect','runner','tester','analyst','builder'))))")
125
+
126
+ # 2. Inject the REPORT (retry-safe submit; --ref keeps payload short)
127
+ telepty inject --ref --submit --submit-retry 2 \
128
+ --from "$TELEPTY_SESSION_ID" "$ORCH_ID" \
129
+ "REPORT: <one-line summary> | evidence: <commit/test/etc> | next: <handoff>"
130
+ ```
131
+
132
+ ### Convention rules
133
+
134
+ - **Prefix**: messages MUST start with `REPORT:` so the orchestrator's event
135
+ classifier can route them.
136
+ - **Return address**: include `--from "$TELEPTY_SESSION_ID"` so the orchestrator
137
+ knows which sub-session reported.
138
+ - **Retry**: pass `--submit-retry 2` (telepty 0.3.3+). The retry is idempotent
139
+ on safe gate-timeout 504s (`gated_dispatch_unconsumed`, `gate_timeout`,
140
+ `no_prompt_symbol_seen`); hard-fail reasons (`session_dead`, `error`,
141
+ `restarting`, `no_state`) are not retried.
142
+ - **Long payloads**: store the full body in a file and use `--ref <file>` so the
143
+ inject prompt itself stays short.
144
+ - **Self-report (idempotent)**: a session reporting on its own behalf may use
145
+ `--submit-force` to bypass the render gate. Do NOT use `--submit-force` for
146
+ general inject — it can clobber in-flight user input.
147
+
148
+ ### Anti-patterns (DO NOT)
149
+
150
+ - `telepty inject orchestrator-claude "..."` — hardcoded session ID; breaks the
151
+ moment the orchestrator is renamed or runs under a different CLI (codex,
152
+ gemini).
153
+ - Embedding `aigentry-orchestrator-claude` in spec templates, scripts, or
154
+ docs as a literal target.
155
+ - Reporting without the `REPORT:` prefix — the orchestrator cannot distinguish
156
+ a status report from a peer-to-peer message.
157
+
86
158
  ## Common Errors
87
159
 
88
160
  | Error | Cause | Fix |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: telepty-list
3
- description: Discover telepty sessions, check status and health. Covers list, session info, and status commands.
3
+ description: Discover telepty sessions, check status and health. Covers list, session info, and status commands. 키워드: 세션 목록, 활성 세션, 세션 조회, 세션 상태, 세션 확인, 리스트
4
4
  ---
5
5
 
6
6
  # telepty-list — Discover Sessions and Check Status
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: telepty-listen
3
- description: Monitor telepty events and read session screen output. Covers listen (event bus) and read-screen commands.
3
+ description: Monitor telepty events and read session screen output. Covers listen (event bus) and read-screen commands. 키워드: 이벤트 모니터, 화면 확인, 화면 읽기, 이벤트 스트림, listen, read-screen, 모니터링
4
4
  ---
5
5
 
6
6
  # telepty-listen — Event Monitoring and Screen Reading
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: telepty-rename
3
- description: Rename, delete, and clean up telepty sessions. Session lifecycle management.
3
+ description: Rename, delete, and clean up telepty sessions. Session lifecycle management. 키워드: 세션 이름 변경, 세션 삭제, 세션 정리, 세션 청소, rename, 라이프사이클
4
4
  ---
5
5
 
6
6
  # telepty-rename — Session Lifecycle Management
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: telepty-session
3
- description: Multi-session orchestration — start multiple sessions at once and arrange terminal layouts. Covers session start and layout commands.
3
+ description: Multi-session orchestration — start multiple sessions at once and arrange terminal layouts. Covers session start and layout commands. 키워드: 멀티 세션 시작, 다중 세션, 세션 레이아웃, 세션 일괄 시작, 멀티 시작, layout
4
4
  ---
5
5
 
6
6
  # telepty-session — Multi-Session Orchestration