@dmsdc-ai/aigentry-telepty 0.3.3 → 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 (34) hide show
  1. package/AGENTS.md +23 -0
  2. package/CHANGELOG.md +110 -0
  3. package/README.md +67 -1
  4. package/cli.js +125 -39
  5. package/cross-machine.js +132 -0
  6. package/docs/reports/2026-05-05-issue-8-claude-review.md +194 -0
  7. package/docs/specs/2026-05-05-issue-8-telepty-init.md +477 -0
  8. package/host-spec.js +60 -0
  9. package/mcp-server/index.mjs +24 -3
  10. package/package.json +6 -5
  11. package/scripts/regen-snippet-fixtures.js +42 -0
  12. package/skill-installer.js +42 -6
  13. package/skills/telepty/SKILL.md +1 -1
  14. package/skills/telepty-allow/SKILL.md +1 -1
  15. package/skills/telepty-attach/SKILL.md +1 -1
  16. package/skills/telepty-broadcast/SKILL.md +1 -1
  17. package/skills/telepty-daemon/SKILL.md +1 -1
  18. package/skills/telepty-inject/SKILL.md +76 -4
  19. package/skills/telepty-list/SKILL.md +1 -1
  20. package/skills/telepty-listen/SKILL.md +1 -1
  21. package/skills/telepty-rename/SKILL.md +1 -1
  22. package/skills/telepty-session/SKILL.md +1 -1
  23. package/src/init/print-snippet.js +114 -0
  24. package/src/init/snippets/agents.md +15 -0
  25. package/src/init/snippets/claude.md +15 -0
  26. package/src/init/snippets/gemini.md +15 -0
  27. package/tests/snippet-protocol/v1/golden-agents.json +1 -0
  28. package/tests/snippet-protocol/v1/golden-agents.md +17 -0
  29. package/tests/snippet-protocol/v1/golden-all.json +3 -0
  30. package/tests/snippet-protocol/v1/golden-all.md +53 -0
  31. package/tests/snippet-protocol/v1/golden-claude.json +1 -0
  32. package/tests/snippet-protocol/v1/golden-claude.md +17 -0
  33. package/tests/snippet-protocol/v1/golden-gemini.json +1 -0
  34. package/tests/snippet-protocol/v1/golden-gemini.md +17 -0
@@ -0,0 +1,477 @@
1
+ # Spec: Telepty Issue #8 — `telepty init --print-snippet`
2
+
3
+ | Field | Value |
4
+ |---|---|
5
+ | **Status** | DRAFT — awaiting user approval before implementation |
6
+ | **Issue** | #8 (`telepty init` for AGENTS.md/CLAUDE.md/GEMINI.md graceful integration) |
7
+ | **Authoring date** | 2026-05-05 |
8
+ | **Authoring session** | aigentry-telepty (issue #8 dispatch) |
9
+ | **Dispatch envelope** | `~/.telepty/shared/bc208165370fee6bcce1d5cb28b0e32b188636f5b0bb74b1bd14dc0a34dde6e9.md` (also `/tmp/aigentry-dispatch/issue-8-telepty-init.md`) |
10
+ | **Binding ADR** | `~/projects/aigentry-orchestrator/docs/adr/2026-05-05-telepty-devkit-boundary.md` (commit `e4b072b`, status ACCEPTED) |
11
+ | **Protocol surface** | `telepty-snippet/v1` |
12
+ | **SSOT registry** | `~/projects/aigentry-ssot/contracts/telepty-snippet-v1.md` (created by this PR — Option B atomic delivery) |
13
+ | **Telepty version target** | `@dmsdc-ai/aigentry-telepty` ≥ 0.3.5 (next minor, additive) |
14
+ | **Workflow gate** | SAWP Rule 24 — SPEC FIRST. NO implementation before user approval. |
15
+
16
+ ---
17
+
18
+ ## §1 Background & dispatch context
19
+
20
+ Issue #8 introduces `telepty init` so users can graft a telepty-baseline reference into their per-CLI agent files (`~/CLAUDE.md`, `~/AGENTS.md`, `~/GEMINI.md`). The Boundary ADR (commit `e4b072b`) splits this across two repos:
21
+
22
+ - **Telepty owns the snippet content + emit mechanism** — `telepty init --print-snippet` writes versioned, sha256-hashable canonical text to stdout. **No file I/O.**
23
+ - **Devkit owns all per-AI-CLI file editing** — `aigentry scaffold --integrate-telepty` consumes the stdout and performs the idempotent sentinel-bracketed insertion.
24
+
25
+ This spec covers the telepty side only. Devkit's `--integrate-telepty` is a separate dispatch (Phase 3 follow-up under aigentry-devkit ownership; tracked at ADR §3.1.1.3).
26
+
27
+ **Pre-flight gate audit (M0, §6.5.1):** at spec-author time, this session observed:
28
+ - G1 (`telepty-snippet/v1` SSOT stub) — ABSENT before this PR. **This PR creates it** (Option B atomic delivery, confirmed by user).
29
+ - G7 (telepty README receiver-side cleanup) — failing (line 155 still names `telepty install hooks`). **Out of scope here**; needs a separate doc-only PR per ADR §3.1.2.5.
30
+
31
+ The orchestrator must either accept G1 closure as part of this PR's merge or close G7/G8/G9 in parallel. This spec ships G1 only; the other M0 doc gates are flagged for the orchestrator's separate dispatches.
32
+
33
+ ---
34
+
35
+ ## §2 Decision (locked)
36
+
37
+ `telepty init` ships a single subcommand in v1: `--print-snippet`. The CLI surface is:
38
+
39
+ ```
40
+ telepty init --print-snippet [--target {claude|agents|gemini|all}] [--format {markdown|json}]
41
+ ```
42
+
43
+ with stdin never consumed, stderr reserved for warnings only, and exit codes per ADR §3.1.1.2 (`0` success / `2` unsupported target / `3` legacy "command not found" / `4` internal failure).
44
+
45
+ The canonical body content is locked in this spec's normative appendix (§A). Body content is **identical across all three targets except the section header line** (`## telepty-snippet:<target>`). Per §3.4 row 1 + Article 3, telepty does not encode CLI-specific placement guidance ("paste this into CLAUDE.md vs AGENTS.md") — that is devkit's territory.
46
+
47
+ ---
48
+
49
+ ## §3 UX & CLI surface
50
+
51
+ ### §3.1 Invocation matrix
52
+
53
+ | Invocation | Output |
54
+ |---|---|
55
+ | `telepty init` (no flags) | One-line help: `usage: telepty init --print-snippet [--target ...] [--format ...]`; exit 0. |
56
+ | `telepty init --print-snippet` | All three sentinel-bracketed markdown sections concatenated in order claude → agents → gemini; exit 0. |
57
+ | `telepty init --print-snippet --target claude` | Single sentinel envelope for `target=claude`; exit 0. |
58
+ | `telepty init --print-snippet --target agents` | Same shape, `target=agents`. |
59
+ | `telepty init --print-snippet --target gemini` | Same shape, `target=gemini`. |
60
+ | `telepty init --print-snippet --target all --format json` | 3 NDJSON lines, one per target, schema `{version,target,sha256,body}`. |
61
+ | `telepty init --print-snippet --target zsh` | Exit 2; stderr: `error: --target must be one of claude, agents, gemini, all`. |
62
+ | `telepty init --help` | Same help line as `telepty init`; exit 0. |
63
+ | Stdin piped to any invocation | Stdin is closed/ignored; output unchanged. |
64
+
65
+ ### §3.2 stdout sentinel envelope (markdown)
66
+
67
+ Per ADR §3.1.1.1 row "stdout — envelope":
68
+
69
+ ```
70
+ <!-- telepty-snippet/v1 BEGIN target=<name> sha256=<hex8> -->
71
+ <body — UTF-8, LF-only, no CRLF>
72
+ <!-- telepty-snippet/v1 END target=<name> -->
73
+ ```
74
+
75
+ Empty newline between consecutive envelopes when `--target=all`. The 8-character `sha256=<hex8>` is the first 8 hex chars of the sha256 of the body bytes (between BEGIN and END sentinels, *excluding* the sentinel lines themselves and the surrounding newlines).
76
+
77
+ ### §3.3 stdout NDJSON form (--format json)
78
+
79
+ ```
80
+ {"version":"telepty-snippet/v1","target":"claude","sha256":"<full-64-hex>","body":"<markdown text with literal \n>"}
81
+ ```
82
+
83
+ One line per target. `sha256` here is the **full** 64-char digest (NOT truncated 8-char) — devkit consumers can compare with `--format markdown`'s 8-char prefix via `digest.slice(0,8)`.
84
+
85
+ ### §3.4 stderr policy
86
+
87
+ - Warnings only. Never errors. Never status messages.
88
+ - Allowed warnings (v1 enumeration):
89
+ - `warn: telepty version <X.Y.Z> emits telepty-snippet/v1 forward-compat lines; consumers expecting strict v1 line set may see additive content.` (when telepty version exceeds the version range it was last fixture-pinned to.)
90
+ - Tee-safe: devkit consumers may redirect stderr to a log file without affecting stdout pipeline integrity.
91
+
92
+ ### §3.5 Exit codes (per ADR §3.1.1.2 — paraphrased)
93
+
94
+ | Code | Meaning |
95
+ |---|---|
96
+ | 0 | Success — snippet emitted to stdout |
97
+ | 2 | Unsupported `--target` value |
98
+ | 3 | Telepty version older than `--print-snippet` introduction (legacy telepty, command not found); consumers detect shell exit 127 OR exit 3 |
99
+ | 4 | Internal failure (snippet generation error, e.g., template file unreadable) |
100
+
101
+ `v1` MAY add new non-zero codes; consumers MUST treat any non-zero as fail-closed (refuse to write into user files).
102
+
103
+ ---
104
+
105
+ ## §4 Boundary respect — verbatim ADR cites
106
+
107
+ ### §4.1 Files telepty TOUCHES
108
+
109
+ | Path | Why telepty owns it | ADR rule |
110
+ |---|---|---|
111
+ | `src/init/print-snippet.js` (new) | Transport+protocol primitive (stdout contract). | §3.1 rule 1 — "Telepty owns transport/runtime primitives and normative protocol semantics." |
112
+ | `src/init/snippets/{claude,agents,gemini}.md` (new) | Telepty-self reference content describing its own CLI/protocol surface. | §3.1 rule 2 — "Telepty may own reference content only when it documents telepty's own CLI/protocol surface." |
113
+ | `cli.js` (modified — `init` dispatch block) | Telepty's CLI surface. | §3.2 row 1 (transport primitives) |
114
+ | `tests/snippet-protocol/v1/golden-{claude,agents,gemini,all}.md` and `.json` (new) | Conformance fixtures (telepty-side). | §3.1.1.4 — "Telepty repo: `tests/snippet-protocol/v1/golden-*` fixed-output snapshot tests." |
115
+ | `package.json` `scripts.regen-fixtures` (new) | Fixture maintenance pattern (OC-2=B). | implementation choice; not ADR-mandated |
116
+ | `~/projects/aigentry-ssot/contracts/telepty-snippet-v1.md` (new, cross-repo) | SSOT registry stub for `telepty-snippet/v1` (G1 gate artifact). | §6.5/§6.5.1 G1 |
117
+
118
+ ### §4.2 Files telepty HANDS OFF (devkit-owned)
119
+
120
+ | Path | Why devkit owns it | ADR rule |
121
+ |---|---|---|
122
+ | `~/CLAUDE.md`, `~/AGENTS.md`, `~/GEMINI.md` editing | "All mutation of user/project files." | §3.1 rule 3 |
123
+ | Sentinel `<!-- BEGIN telepty setup v1 -->`…`<!-- END -->` insertion | Devkit-owned consumer-side sentinel labels. | §3.1.1.3 row "Sentinel labels (file edit)" |
124
+ | `--dry-run`, `--backup`, `--uninstall` of file edits | Devkit-side flags on `aigentry scaffold --integrate-telepty`. | §3.1.1.3 |
125
+ | Idempotency-via-sha256 detection on disk | Devkit consumer logic. | §3.1.1.3 row "Idempotency" |
126
+
127
+ ### §4.3 Verbatim ADR §3.5 / §3.4 / §3.3.1.5 citations
128
+
129
+ Reproduced for the spec record (and to satisfy the dispatch envelope's "verbatim cite" requirement):
130
+
131
+ > **§3.4 row 1 (issue #8 placement):** "telepty exposes stable stdout contract (versioned snippet); devkit consumes it. **No file editing in telepty.**"
132
+
133
+ > **§3.5 codex-conditions table, row 1:** "Contract spec gate: before #8/#10.2/#3 implementation, publish SSOT entries and conformance fixtures for `telepty-snippet/v1`, `[context-ref/v1]`, `telepty list --json`, and `--scaffold`." (RESOLVED at §3.1.1, §3.1.2, §3.3.1, §6.5.)
134
+
135
+ > **§3.3.1.5 (what telepty MUST NOT do):** "Telepty CI MUST pass on a clean machine without devkit installed (Article 9 / §8 M3). Telepty's core test suite MUST NOT invoke `aigentry scaffold` for any non-`--scaffold` codepath."
136
+
137
+ ### §4.4 §3.1 4-rule self-test (first match wins)
138
+
139
+ Apply rules 1→2→3→4 to each new artifact:
140
+
141
+ - `src/init/print-snippet.js` → **rule 1 match** (transport primitive: stdout contract). STOP.
142
+ - `src/init/snippets/*.md` → rule 1 no (not transport itself); **rule 2 match** (telepty-self reference content). STOP.
143
+ - `tests/snippet-protocol/v1/*` → rule 1 match (conformance fixtures for protocol semantics). STOP.
144
+ - `aigentry-ssot/contracts/telepty-snippet-v1.md` → registry stub; placement fixed by §6.5 (cross-repo to ssot, not telepty proper); not subject to §3.1 rules.
145
+
146
+ No artifact triggers two rules; no artifact requires decomposition.
147
+
148
+ ---
149
+
150
+ ## §5 Graceful integration — explicitly NOT telepty's concern
151
+
152
+ The dispatch envelope's question (c) ("Graceful integration: merge strategy when AGENTS.md/CLAUDE.md/GEMINI.md already exist. NEVER overwrite.") is answered by the boundary itself: **telepty performs zero file I/O in this issue.** The merge strategy is owned by devkit's `aigentry scaffold --integrate-telepty`, fully specified at ADR §3.1.1.3:
153
+
154
+ | Aspect | Devkit specification (reference) |
155
+ |---|---|
156
+ | Sentinel format | `<!-- BEGIN telepty setup v1 sha256=<hex8> -->` … `<!-- END telepty setup v1 -->` |
157
+ | First-time write | Append section at EOF; create file if absent (mode 0644). |
158
+ | Re-run, identical body sha256 | No-op. |
159
+ | Re-run, different body sha256 | In-place replacement (with `--backup` writes `.bak.<ISO8601>`). |
160
+ | `--dry-run` | Print intended diff; modify nothing; exit 0 if would-change, exit 1 if no-change. |
161
+ | `--uninstall` | Remove sentinel-bracketed section; backup always ON. |
162
+ | Failure if telepty missing/broken | Refuse to write; print actionable error; exit 4. |
163
+
164
+ **Telepty's contribution** to making the merge possible: a stable, sha256-hashable byte sequence per target. That's the entirety of the mechanism contribution. Invariant I1 (boundary direction LOCKED) and I2 (NEVER overwrite existing files) are upheld by **not touching files at all**.
165
+
166
+ ---
167
+
168
+ ## §6 Mechanism contract — handoff to devkit
169
+
170
+ ### §6.1 Subprocess invocation (devkit side, reference)
171
+
172
+ Per ADR §3.1.1.3 row "Subprocess":
173
+
174
+ ```js
175
+ const { spawn } = require('node:child_process');
176
+ const child = spawn(
177
+ 'telepty',
178
+ ['init', '--print-snippet', '--target', target, '--format', 'markdown'],
179
+ { stdio: ['ignore', 'pipe', 'pipe'] } // stdin ignored — matches §3.1.1.1 row "stdin"
180
+ );
181
+ ```
182
+
183
+ - **Timeout:** 10 seconds (devkit responsibility; not enforced telepty-side).
184
+ - **Stdin:** ignored. Telepty is contractually required NEVER to read stdin.
185
+ - **Exit:** non-zero → devkit fail-closed (refuse to write user file).
186
+
187
+ ### §6.2 No env vars, no IPC, no file marker
188
+
189
+ - Telepty does NOT read environment variables to alter snippet content.
190
+ - No `~/.telepty/init.json` config consumed at print-snippet time.
191
+ - No daemon connection required (`telepty init` is daemon-free; runs on a clean machine).
192
+ - The full handoff channel is: argv in, stdout out, exit code out, stderr warnings out. POSIX-portable.
193
+
194
+ ### §6.3 Article 17 / Article 9 compliance
195
+
196
+ - **Article 9 (independence):** `telepty init --print-snippet` runs on a fresh machine without devkit installed (it's a self-describing emitter; it does not call `aigentry`). M3 smoke test will exercise this.
197
+ - **Article 17 (zero external dep):** no new runtime npm dependency. Implementation uses node built-ins (`fs`, `crypto`, `process`).
198
+
199
+ ---
200
+
201
+ ## §7 Implementation plan
202
+
203
+ ### §7.1 File layout (new)
204
+
205
+ ```
206
+ src/init/
207
+ print-snippet.js # main implementation
208
+ snippets/
209
+ claude.md # canonical body — claude target
210
+ agents.md # canonical body — agents target
211
+ gemini.md # canonical body — gemini target
212
+ tests/snippet-protocol/v1/
213
+ golden-claude.md # snapshot of --target claude --format markdown
214
+ golden-agents.md # snapshot of --target agents --format markdown
215
+ golden-gemini.md # snapshot of --target gemini --format markdown
216
+ golden-all.md # snapshot of --target all --format markdown
217
+ golden-claude.json # snapshot of --target claude --format json
218
+ golden-agents.json # snapshot of --target agents --format json
219
+ golden-gemini.json # snapshot of --target gemini --format json
220
+ golden-all.json # snapshot of --target all --format json
221
+ test/
222
+ init.test.js # node:test suite, 14 tests (§8)
223
+ docs/specs/
224
+ 2026-05-05-issue-8-telepty-init.md # this spec
225
+ scripts/
226
+ regen-snippet-fixtures.js # OC-2=B: writes runtime stdout to fixture paths
227
+ ```
228
+
229
+ ### §7.2 cli.js dispatch (modified)
230
+
231
+ A new `if (cmd === 'init')` block at the same nesting level as existing `daemon`/`list`/`allow` dispatches (insertion point near `cli.js:861` group). Block delegates to `require('./src/init/print-snippet').main(args)` and propagates the returned exit code.
232
+
233
+ ### §7.3 print-snippet.js skeleton (illustrative — not binding)
234
+
235
+ ```js
236
+ 'use strict';
237
+ const fs = require('node:fs');
238
+ const path = require('node:path');
239
+ const { createHash } = require('node:crypto');
240
+
241
+ const TARGETS = ['claude', 'agents', 'gemini'];
242
+ const SNIPPET_DIR = path.join(__dirname, 'snippets');
243
+
244
+ function loadBody(target) {
245
+ return fs.readFileSync(path.join(SNIPPET_DIR, `${target}.md`), 'utf8');
246
+ }
247
+
248
+ function sha256Hex(bytes) {
249
+ return createHash('sha256').update(bytes, 'utf8').digest('hex');
250
+ }
251
+
252
+ function emitMarkdown(target, body) {
253
+ const sha8 = sha256Hex(body).slice(0, 8);
254
+ return `<!-- telepty-snippet/v1 BEGIN target=${target} sha256=${sha8} -->\n${body}<!-- telepty-snippet/v1 END target=${target} -->\n`;
255
+ }
256
+
257
+ function emitJson(target, body) {
258
+ const sha = sha256Hex(body);
259
+ return JSON.stringify({ version: 'telepty-snippet/v1', target, sha256: sha, body }) + '\n';
260
+ }
261
+
262
+ exports.main = function main(args) {
263
+ // argv parser: --print-snippet (required for v1), --target <X>, --format <Y>
264
+ // returns 0 on success, 2 on bad target, 4 on internal failure
265
+ };
266
+ ```
267
+
268
+ ### §7.4 Snippet body source-of-truth
269
+
270
+ Per OC-1=B, canonical body lives in `src/init/snippets/{claude,agents,gemini}.md`. The bodies are byte-equal *except* for the first line section header (`## telepty-snippet:<target>`). The locked content text appears in this spec's §A normative appendix. Implementer MUST byte-equal the §A text (including line endings) when authoring the three template files.
271
+
272
+ ### §7.5 Conformance fixtures (OC-2=B)
273
+
274
+ `scripts/regen-snippet-fixtures.js` invokes the runtime emitter for each (target × format) combination and writes byte-equal output to `tests/snippet-protocol/v1/golden-*.{md,json}`. CI runs `npm test` which includes a step `git diff --exit-code tests/snippet-protocol/v1/` — any drift between runtime and fixture is a CI failure. Intentional snippet body changes flow through: edit `src/init/snippets/<target>.md` → run `npm run regen-fixtures` → commit both the source change and the fixture diff.
275
+
276
+ ### §7.6 SSOT stub (G1 — cross-repo)
277
+
278
+ File: `~/projects/aigentry-ssot/contracts/telepty-snippet-v1.md`
279
+
280
+ Body shape:
281
+
282
+ ```markdown
283
+ # Contract: telepty-snippet/v1
284
+
285
+ | Field | Value |
286
+ |---|---|
287
+ | Tag | `telepty-snippet/v1` |
288
+ | Owning repo | `aigentry-telepty` |
289
+ | Consuming repo | `aigentry-devkit` (via `aigentry scaffold --integrate-telepty`) |
290
+ | Spec doc | `aigentry-telepty/docs/specs/2026-05-05-issue-8-telepty-init.md` |
291
+ | ADR ref | `aigentry-orchestrator/docs/adr/2026-05-05-telepty-devkit-boundary.md` §3.1.1, §3.4 row 1 |
292
+ | Telepty fixtures | `aigentry-telepty/tests/snippet-protocol/v1/golden-{claude,agents,gemini,all}.{md,json}` |
293
+ | Devkit fixtures | `aigentry-devkit/tests/scaffold-integrate-telepty/v1/` (separate dispatch) |
294
+ | Deprecation policy | 14-day pre-announce + dual-emit during overlap (per §3.1.1.5) |
295
+ | Semver | additive within v1; breaking → v2 + 14-day announce |
296
+
297
+ (Section bodies inlined or link to spec doc as appropriate.)
298
+ ```
299
+
300
+ Cross-repo PR coordination: a sibling PR to `aigentry-ssot` lands G1 stub; this telepty PR will not merge until G1 is referenced from the merged stub.
301
+
302
+ ---
303
+
304
+ ## §8 Test plan (TDD per superpowers)
305
+
306
+ All tests live in `test/init.test.js` (node:test, matching existing 17-file convention). Each item is a failing-test-first checkpoint per `superpowers:test-driven-development`.
307
+
308
+ ### §8.1 Envelope/format (5)
309
+
310
+ 1. `--target=claude --format=markdown` emits BEGIN line containing literal `target=claude` and matches `/sha256=[0-9a-f]{8}/`.
311
+ 2. `--target=agents --format=markdown` ditto with `target=agents`.
312
+ 3. `--target=gemini --format=markdown` ditto with `target=gemini`.
313
+ 4. `--target=all --format=markdown` emits exactly 3 envelopes in order claude→agents→gemini, separated by an empty line.
314
+ 5. `--format=json --target=all` emits exactly 3 NDJSON lines, each parses to `{version: "telepty-snippet/v1", target, sha256, body}`; the four keys exist and types are (string, string, hex string of length 64, string).
315
+
316
+ ### §8.2 Body invariants (3)
317
+
318
+ 6. For each target, body bytes contain none of: `$HOME`, `$(`, backtick (`` ` ``) outside fenced code blocks, literal `~` anywhere in body. (Defends §3.1.1.1 line 161 "no shell substitution.")
319
+ 7. For each target, body is UTF-8 LF-only — `body.includes('\r')` is false.
320
+ 8. For each target, two sequential `--print-snippet --target <X>` invocations produce byte-identical stdout. (§3.1.1.1 idempotency row.)
321
+
322
+ ### §8.3 Exit codes (3)
323
+
324
+ 9. `--print-snippet` (no other args) exits 0.
325
+ 10. `--print-snippet --target zsh` exits 2; stderr matches `/--target must be one of claude, agents, gemini, all/`; stdout is empty.
326
+ 11. Internal failure path (mock `fs.readFileSync` throws via stub or non-existent template path injection) exits 4; stderr non-empty; stdout empty.
327
+
328
+ ### §8.4 stdin/stderr discipline (2)
329
+
330
+ 12. Stdin closed/ignored at spawn — node:test spawns telepty as child with `stdio: ['pipe', 'pipe', 'pipe']`, immediately closes child stdin, child still exits 0 with full stdout. Verifies §3.1.1.1 stdin row.
331
+ 13. No warnings on the happy path → stderr is empty for `--print-snippet --target claude`.
332
+
333
+ ### §8.5 Golden snapshot (1)
334
+
335
+ 14. `tests/snippet-protocol/v1/golden-{claude,agents,gemini,all}.{md,json}` (8 files) byte-equal the runtime emitter's stdout for the matching invocation. Test uses `fs.readFileSync` and `assert.strictEqual`. Test will fail if `git diff --exit-code tests/snippet-protocol/v1/` is non-empty after running `npm run regen-fixtures`.
336
+
337
+ ### §8.6 Devkit-free path enforcement (1, M3 in-suite)
338
+
339
+ 15. Run `telepty init --print-snippet --target all` in a child process with `PATH` filtered to remove any directory containing an `aigentry` executable; assert exit 0 and stdout matches the golden fixture. Defends Article 9 / §8 M3 inside the test suite (not just narratively).
340
+
341
+ **Total: 15 tests.** All run under `npm test` (node:test runner, devkit-free per Article 9 / M3).
342
+
343
+ ### §8.7 Cross-cutting verification
344
+
345
+ - `npm test` runs to green on a clean checkout without `aigentry` on PATH (M3) — enforced by test 15 (§8.6) plus narrative external smoke.
346
+ - Telepty CI does not invoke `aigentry scaffold` (§3.3.1.5).
347
+
348
+ ---
349
+
350
+ ## §9 G-gate contribution (§6.5.1 audit)
351
+
352
+ | Gate | Contribution | Verification (post-merge) |
353
+ |---|---|---|
354
+ | **G1** | ✅ DIRECT — this PR's cross-repo sibling lands `aigentry-ssot/contracts/telepty-snippet-v1.md`. | `test -f ~/projects/aigentry-ssot/contracts/telepty-snippet-v1.md && grep -q 'telepty-snippet/v1' ~/projects/aigentry-ssot/contracts/telepty-snippet-v1.md` |
355
+ | **G2** | ❌ none — `[context-ref/v1]` is #10.2 scope. |
356
+ | **G3** | ❌ none — `scaffold/v1` is devkit (#3) scope. |
357
+ | **G4** | ❌ none — `scaffold-shim/v1` is `--scaffold` flag work, separate. |
358
+ | **G5** | ❌ none — `telepty-list-json/v1` is a separate surface. |
359
+ | **G6** | ❌ none — `posix-command-v-aigentry` is `--scaffold` shim concern. (Dispatch envelope's "likely G1/G6" prediction is corrected here: G6 is not contributed by this PR.) |
360
+ | **G7** | ❌ none — telepty README cleanup is a separate doc-only PR per §3.1.2.5. |
361
+ | **G8** | ❌ none — telepty AGENTS.md legacy-exception subsection is separate per §6.2.1. |
362
+ | **G9** | ❌ none — `skill-installer.js` legacy header is separate per §6.2.1.3. |
363
+ | **M3** | ✅ INDIRECT — `telepty init --print-snippet` is exercised on devkit-free CI. |
364
+ | **M6** | ✅ DIRECT — ships `tests/snippet-protocol/v1/golden-*` (8 fixture files), unblocking the §3.1.1.4 conformance set. |
365
+
366
+ ---
367
+
368
+ ## §10 Out-of-scope (explicit)
369
+
370
+ The following are NOT in this issue, NOT in this PR, and any drift toward them constitutes scope creep that must be split:
371
+
372
+ 1. **No file I/O on user files** (`~/CLAUDE.md`, `~/AGENTS.md`, `~/GEMINI.md`). Devkit owns this (#3 / `--integrate-telepty`).
373
+ 2. **No interactive UX** (no readline, no tty prompt, no `--yes`/`--no` confirmation flow).
374
+ 3. **No `[context-ref]` hook installation** — that is issue #10.2, separate dispatch (devkit-side `aigentry scaffold install-hooks <cli>`).
375
+ 4. **No project-scope `CLAUDE.md` / `.claude/settings.json` scaffolding** — that is issue #3, separate dispatch (devkit `aigentry scaffold --project`).
376
+ 5. **No `telepty install hooks` resurrection** — explicitly rejected by ADR §3.1.2 / §3.4 row 2.
377
+ 6. **No `--scaffold` shim work** (`scaffold-shim/v1`) — that is the §3.3.1.2 follow-up, separate.
378
+ 7. **No `telepty list --json` schema work** — that is its own surface (§3.6.1, §6.5.1 G5).
379
+ 8. **No README §"Integration scope" cleanup (G7)**, **no AGENTS.md legacy exception subsection (G8)**, **no `skill-installer.js` header (G9)** — these are M0 doc-only PRs separate from this implementation work, per ADR §3.1.2.5 / §6.2.1.
380
+ 9. **No new external runtime dependency** (Article 17 / M4).
381
+ 10. **No content for unfamiliar CLIs** (e.g., `roo`, `cursor`) — v1 ships exactly the three documented targets. New CLI = MINOR additive within v1, but not in this PR.
382
+
383
+ Lessons honored:
384
+ - **F1** (past article-3 violations on session-scaffold logic in telepty): No telepty session-scaffold logic introduced; only stdout emitter.
385
+ - **F2** (aggressive merge/rewrite of dotfiles burns trust): No file I/O at all in telepty.
386
+ - **F3** (sub-issue creep): Item 3, 4, 6, 7, 8 above explicitly fence creep into #3 / #10.2 / G7-G9.
387
+
388
+ ---
389
+
390
+ ## §11 Risks & open questions
391
+
392
+ ### §11.1 Risks
393
+
394
+ | ID | Risk | Mitigation |
395
+ |---|---|---|
396
+ | R1 | Snippet body content drifts as telepty evolves; sha256 churn breaks devkit's idempotency check on user files. | OC-2=B regen-fixtures gate + 14-day pre-announce policy (§3.1.1.1 versioning row). Body changes are deliberate, reviewed, and visible in PR diff. |
397
+ | R2 | G1 SSOT stub PR lands separately and lags telepty PR — cross-repo merge order race. | Ship G1 stub PR FIRST and require its merge SHA to be cited in this telepty PR description. CI gate on existence of stub at merge time. |
398
+ | R3 | Dispatch envelope's "G1/G6" prediction misled. Spec corrects to G1 + M6. | Surface deviation explicitly to orchestrator in §9 + REPORT envelope. |
399
+ | R4 | Devkit `aigentry scaffold --integrate-telepty` PR not yet authored — telepty users may run `--print-snippet` and have no consumer. | Acceptable: telepty's stdout is self-documenting and a user can hand-paste between sentinels. Coordinate devkit dispatch as Phase 3 follow-up (separate). |
400
+ | R5 | M0 gate composite still failing (G7/G8/G9) at merge time. | Out of scope here, but flag in the REPORT so orchestrator can dispatch their closures in parallel before the 7-day window expires (2026-05-12). |
401
+
402
+ ### §11.2 Open questions (for user / orchestrator review)
403
+
404
+ - **OQ-A** — Should the G1 SSOT stub PR be co-authored from this session (single agent ships both repos) or dispatched as a sibling to a different session? **Author lean:** single agent ships both for atomic correctness; user has confirmed Option B which implies single-agent atomic.
405
+ - ~~**OQ-B**~~ — *Resolved by §3.1 invocation matrix row 1: `telepty init` with no flags prints help to stdout, exit 0. Help is a documented happy-path output, not an error.*
406
+ - **OQ-C** — Is the 8-char sha256 prefix in the markdown sentinel sufficient for devkit's idempotency check, or should the markdown form also carry the full 64-char digest in a comment? **Author lean:** 8-char is sufficient for tag-line collision detection (2^32 namespace, deterministic input set ≤ 3 targets); full digest is available via `--format json` if devkit needs it.
407
+
408
+ User/orchestrator: signal preferences on OQ-A/C in the approval round, or accept author-leans.
409
+
410
+ ---
411
+
412
+ ## §12 Self-review against ADR §3.1 4-rule sharpening
413
+
414
+ Per dispatch step 2, this section runs the 4-rule test on every artifact this PR introduces, in rule order, first match wins.
415
+
416
+ | Artifact | Rule 1 (transport) | Rule 2 (telepty-self ref) | Rule 3 (devkit territory) | Rule 4 (provisioning) | Verdict |
417
+ |---|---|---|---|---|---|
418
+ | `src/init/print-snippet.js` | ✅ stdout contract | — | — | — | **Telepty (rule 1).** |
419
+ | `src/init/snippets/{claude,agents,gemini}.md` | — (not transport itself) | ✅ documents telepty's own CLI/protocol | — | — | **Telepty (rule 2).** |
420
+ | `cli.js` `init` block | ✅ CLI surface | — | — | — | **Telepty (rule 1).** |
421
+ | `tests/snippet-protocol/v1/*` | ✅ protocol conformance fixtures | — | — | — | **Telepty (rule 1).** |
422
+ | `package.json regen-fixtures` script | ✅ test-tooling for protocol fixtures | — | — | — | **Telepty (rule 1).** |
423
+ | `aigentry-ssot/contracts/telepty-snippet-v1.md` | — (not in telepty repo) | — | — | — | **SSOT registry (separate repo); placement fixed by §6.5.** |
424
+
425
+ No artifact triggers rule 3 (devkit territory). No artifact triggers two rules → no decomposition needed. Boundary direction respected.
426
+
427
+ ---
428
+
429
+ ## §A Normative appendix — canonical snippet body (LOCKED)
430
+
431
+ The three target template files MUST byte-equal the corresponding text below, UTF-8 LF-only, including the trailing blank line. Body-byte sha256 (full 64 hex) is recorded for idempotency audit.
432
+
433
+ ### §A.1 `src/init/snippets/claude.md`
434
+
435
+ ```
436
+ ## telepty-snippet:claude
437
+
438
+ **telepty** is the aigentry ecosystem's PTY multiplexer and session orchestrator. It allows wrapping AI CLI sessions under stable IDs and addressing them across local and cross-machine boundaries via a daemon-mediated transport.
439
+
440
+ Quick-start (5 commands):
441
+
442
+ telepty daemon
443
+ telepty allow --id <name> claude
444
+ telepty list
445
+ telepty inject <name> "<prompt>"
446
+ telepty attach <name>
447
+
448
+ `telepty allow` wraps a CLI under the chosen `<name>`; `telepty list` enumerates known sessions; `telepty inject` sends a prompt to a wrapped session; `telepty attach` interactively connects to one.
449
+
450
+ Run `telepty --help` for the full command list. Run `telepty <command> --help` for per-command flags.
451
+ ```
452
+
453
+ ### §A.2 `src/init/snippets/agents.md`
454
+
455
+ Identical to §A.1 except line 1 reads `## telepty-snippet:agents`.
456
+
457
+ ### §A.3 `src/init/snippets/gemini.md`
458
+
459
+ Identical to §A.1 except line 1 reads `## telepty-snippet:gemini`.
460
+
461
+ ### §A.4 Per-target body sha256 (computed at first commit)
462
+
463
+ To be filled by implementation PR. Spec acceptance does not require these values; they are recorded in the conformance fixture filenames at test time and in the SSOT stub at merge time.
464
+
465
+ ---
466
+
467
+ ## §13 Implementation gate — explicit user-approval requirement
468
+
469
+ Per SAWP Rule 24 + dispatch step 3:
470
+
471
+ > Step 3. Commit spec to `~/projects/aigentry-telepty` + report to orchestrator. WAIT for user approval before implementation.
472
+
473
+ This spec is the deliverable for the SPEC FIRST gate. **No code lands until the user (via orchestrator) approves this spec.** After approval, implementation proceeds per `superpowers:test-driven-development` + `superpowers:writing-plans` with frequent commits, fixture regeneration via `npm run regen-fixtures`, and a final REPORT confirming all 15 tests green.
474
+
475
+ ---
476
+
477
+ *End of spec.*
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.3.3",
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 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",
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",
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"
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
+ }