@bastani/atomic 0.6.5 → 0.6.6-0

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 (125) hide show
  1. package/.agents/skills/ado-commit/SKILL.md +2 -0
  2. package/.agents/skills/ado-create-pr/SKILL.md +2 -0
  3. package/.agents/skills/advanced-evaluation/SKILL.md +2 -0
  4. package/.agents/skills/ast-grep/SKILL.md +2 -0
  5. package/.agents/skills/bdi-mental-states/SKILL.md +2 -0
  6. package/.agents/skills/bun/SKILL.md +156 -122
  7. package/.agents/skills/context-compression/SKILL.md +2 -0
  8. package/.agents/skills/context-degradation/SKILL.md +2 -0
  9. package/.agents/skills/context-fundamentals/SKILL.md +2 -0
  10. package/.agents/skills/context-optimization/SKILL.md +2 -0
  11. package/.agents/skills/create-spec/SKILL.md +2 -0
  12. package/.agents/skills/docx/SKILL.md +2 -0
  13. package/.agents/skills/evaluation/SKILL.md +2 -0
  14. package/.agents/skills/explain-code/SKILL.md +2 -0
  15. package/.agents/skills/filesystem-context/SKILL.md +2 -0
  16. package/.agents/skills/find-skills/SKILL.md +2 -0
  17. package/.agents/skills/gh-commit/SKILL.md +2 -0
  18. package/.agents/skills/gh-create-pr/SKILL.md +2 -0
  19. package/.agents/skills/hosted-agents/SKILL.md +2 -0
  20. package/.agents/skills/impeccable/SKILL.md +117 -304
  21. package/.agents/skills/impeccable/agents/openai.yaml +4 -0
  22. package/.agents/skills/{adapt/SKILL.md → impeccable/reference/adapt.md} +2 -11
  23. package/.agents/skills/{animate/SKILL.md → impeccable/reference/animate.md} +15 -15
  24. package/.agents/skills/{audit/SKILL.md → impeccable/reference/audit.md} +8 -22
  25. package/.agents/skills/{bolder/SKILL.md → impeccable/reference/bolder.md} +9 -13
  26. package/.agents/skills/impeccable/reference/brand.md +114 -0
  27. package/.agents/skills/{clarify/SKILL.md → impeccable/reference/clarify.md} +2 -11
  28. package/.agents/skills/{colorize/SKILL.md → impeccable/reference/colorize.md} +23 -12
  29. package/.agents/skills/impeccable/reference/craft.md +152 -29
  30. package/.agents/skills/{critique/SKILL.md → impeccable/reference/critique.md} +25 -37
  31. package/.agents/skills/{delight/SKILL.md → impeccable/reference/delight.md} +9 -11
  32. package/.agents/skills/{distill/SKILL.md → impeccable/reference/distill.md} +2 -13
  33. package/.agents/skills/impeccable/reference/document.md +427 -0
  34. package/.agents/skills/impeccable/reference/extract.md +1 -1
  35. package/.agents/skills/{harden/SKILL.md → impeccable/reference/harden.md} +1 -43
  36. package/.agents/skills/{layout/SKILL.md → impeccable/reference/layout.md} +27 -11
  37. package/.agents/skills/impeccable/reference/live.md +594 -0
  38. package/.agents/skills/impeccable/reference/motion-design.md +12 -2
  39. package/.agents/skills/impeccable/reference/onboard.md +234 -0
  40. package/.agents/skills/{optimize/SKILL.md → impeccable/reference/optimize.md} +4 -12
  41. package/.agents/skills/{overdrive/SKILL.md → impeccable/reference/overdrive.md} +9 -21
  42. package/.agents/skills/{critique → impeccable}/reference/personas.md +1 -1
  43. package/.agents/skills/{polish/SKILL.md → impeccable/reference/polish.md} +31 -23
  44. package/.agents/skills/impeccable/reference/product.md +62 -0
  45. package/.agents/skills/{quieter/SKILL.md → impeccable/reference/quieter.md} +7 -11
  46. package/.agents/skills/impeccable/reference/shape.md +151 -0
  47. package/.agents/skills/impeccable/reference/teach.md +156 -0
  48. package/.agents/skills/{typeset/SKILL.md → impeccable/reference/typeset.md} +19 -11
  49. package/.agents/skills/impeccable/reference/typography.md +31 -14
  50. package/.agents/skills/impeccable/scripts/cleanup-deprecated.mjs +87 -17
  51. package/.agents/skills/impeccable/scripts/command-metadata.json +94 -0
  52. package/.agents/skills/impeccable/scripts/design-parser.mjs +820 -0
  53. package/.agents/skills/impeccable/scripts/detect-csp.mjs +198 -0
  54. package/.agents/skills/impeccable/scripts/is-generated.mjs +69 -0
  55. package/.agents/skills/impeccable/scripts/live-accept.mjs +595 -0
  56. package/.agents/skills/impeccable/scripts/live-browser.js +4781 -0
  57. package/.agents/skills/impeccable/scripts/live-inject.mjs +445 -0
  58. package/.agents/skills/impeccable/scripts/live-poll.mjs +186 -0
  59. package/.agents/skills/impeccable/scripts/live-server.mjs +694 -0
  60. package/.agents/skills/impeccable/scripts/live-wrap.mjs +571 -0
  61. package/.agents/skills/impeccable/scripts/live.mjs +247 -0
  62. package/.agents/skills/impeccable/scripts/load-context.mjs +141 -0
  63. package/.agents/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
  64. package/.agents/skills/impeccable/scripts/pin.mjs +214 -0
  65. package/.agents/skills/init/SKILL.md +2 -0
  66. package/.agents/skills/liteparse/SKILL.md +1 -0
  67. package/.agents/skills/memory-systems/SKILL.md +2 -0
  68. package/.agents/skills/multi-agent-patterns/SKILL.md +2 -0
  69. package/.agents/skills/opentui/SKILL.md +1 -0
  70. package/.agents/skills/pdf/SKILL.md +2 -0
  71. package/.agents/skills/playwright-cli/SKILL.md +51 -5
  72. package/.agents/skills/playwright-cli/references/playwright-tests.md +1 -1
  73. package/.agents/skills/playwright-cli/references/running-code.md +10 -0
  74. package/.agents/skills/playwright-cli/references/session-management.md +56 -0
  75. package/.agents/skills/playwright-cli/references/spec-driven-testing.md +305 -0
  76. package/.agents/skills/playwright-cli/references/test-generation.md +49 -3
  77. package/.agents/skills/pptx/SKILL.md +2 -0
  78. package/.agents/skills/project-development/SKILL.md +2 -0
  79. package/.agents/skills/prompt-engineer/SKILL.md +2 -0
  80. package/.agents/skills/research-codebase/SKILL.md +2 -0
  81. package/.agents/skills/ripgrep/SKILL.md +2 -0
  82. package/.agents/skills/skill-creator/LICENSE.txt +1 -1
  83. package/.agents/skills/skill-creator/SKILL.md +2 -0
  84. package/.agents/skills/sl-commit/SKILL.md +2 -0
  85. package/.agents/skills/sl-submit-diff/SKILL.md +2 -0
  86. package/.agents/skills/tdd/SKILL.md +4 -0
  87. package/.agents/skills/tool-design/SKILL.md +2 -0
  88. package/.agents/skills/typescript-advanced-types/SKILL.md +2 -1
  89. package/.agents/skills/typescript-expert/SKILL.md +7 -1
  90. package/.agents/skills/typescript-react-reviewer/SKILL.md +2 -1
  91. package/.agents/skills/workflow-creator/SKILL.md +75 -72
  92. package/.agents/skills/workflow-creator/references/session-config.md +48 -1
  93. package/.agents/skills/xlsx/SKILL.md +2 -0
  94. package/.opencode/opencode.json +4 -2
  95. package/dist/sdk/runtime/executor.d.ts +8 -0
  96. package/dist/sdk/runtime/executor.d.ts.map +1 -1
  97. package/dist/sdk/runtime/port-discovery.d.ts +71 -0
  98. package/dist/sdk/runtime/port-discovery.d.ts.map +1 -0
  99. package/dist/sdk/runtime/tmux.d.ts +10 -0
  100. package/dist/sdk/runtime/tmux.d.ts.map +1 -1
  101. package/dist/sdk/types.d.ts +1 -0
  102. package/dist/sdk/types.d.ts.map +1 -1
  103. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
  104. package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts.map +1 -1
  105. package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +1 -1
  106. package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +1 -1
  107. package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts +15 -0
  108. package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts.map +1 -1
  109. package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts.map +1 -1
  110. package/package.json +1 -1
  111. package/src/sdk/runtime/executor.test.ts +254 -1
  112. package/src/sdk/runtime/executor.ts +135 -89
  113. package/src/sdk/runtime/port-discovery.test.ts +573 -0
  114. package/src/sdk/runtime/port-discovery.ts +496 -0
  115. package/src/sdk/runtime/tmux.ts +16 -0
  116. package/src/sdk/types.ts +1 -0
  117. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +24 -6
  118. package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +52 -13
  119. package/src/sdk/workflows/builtin/ralph/claude/index.ts +31 -3
  120. package/src/sdk/workflows/builtin/ralph/copilot/index.ts +16 -0
  121. package/src/sdk/workflows/builtin/ralph/helpers/prompts.ts +70 -3
  122. package/src/sdk/workflows/builtin/ralph/opencode/index.ts +50 -6
  123. package/.agents/skills/shape/SKILL.md +0 -96
  124. /package/.agents/skills/{critique → impeccable}/reference/cognitive-load.md +0 -0
  125. /package/.agents/skills/{critique → impeccable}/reference/heuristics-scoring.md +0 -0
@@ -0,0 +1,496 @@
1
+ /**
2
+ * Cross-platform TCP port discovery for child processes.
3
+ *
4
+ * Polls the kernel's per-process socket table until a listening TCP port
5
+ * is found for the given PID, or until the timeout elapses.
6
+ *
7
+ * Platform implementations:
8
+ * - Linux: /proc/<pid>/net/tcp + /proc/<pid>/fd/* (no external binary)
9
+ * - macOS: lsof -nP -iTCP -sTCP:LISTEN -a -p <pid>
10
+ * - Windows: Get-NetTCPConnection PowerShell; falls back to netstat -ano
11
+ */
12
+
13
+ import {
14
+ existsSync,
15
+ readdirSync,
16
+ readlinkSync,
17
+ readFileSync,
18
+ } from "node:fs";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Public API
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export const PORT_DISCOVERY_TIMEOUT_MS = 15_000;
25
+
26
+ export interface GetListeningPortOptions {
27
+ timeoutMs?: number;
28
+ pollIntervalMs?: number;
29
+ }
30
+
31
+ type PortDiscoverySpawnOptions = {
32
+ cmd: string[];
33
+ stdout: "pipe";
34
+ stderr: "pipe";
35
+ };
36
+
37
+ type PortDiscoverySpawnResult = {
38
+ stdout: { toString(): string };
39
+ stderr: { toString(): string };
40
+ success: boolean;
41
+ };
42
+
43
+ type PortDiscoverySpawnSync = (options: PortDiscoverySpawnOptions) => PortDiscoverySpawnResult;
44
+
45
+ let spawnSync: PortDiscoverySpawnSync = (options) => Bun.spawnSync(options);
46
+
47
+ export function _setPortDiscoverySpawnSyncForTest(nextSpawnSync: PortDiscoverySpawnSync): () => void {
48
+ const previous = spawnSync;
49
+ spawnSync = nextSpawnSync;
50
+ return () => {
51
+ spawnSync = previous;
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Discover the TCP port that a given process is listening on.
57
+ *
58
+ * Polls the kernel's per-process socket table at ~500ms intervals until
59
+ * a listening port is found or the timeout elapses. Returns null on
60
+ * timeout (caller is responsible for throwing if that's an error).
61
+ */
62
+ export async function getListeningPortForPid(
63
+ pid: number,
64
+ options?: GetListeningPortOptions,
65
+ ): Promise<number | null> {
66
+ const timeoutMs = options?.timeoutMs ?? PORT_DISCOVERY_TIMEOUT_MS;
67
+ const pollIntervalMs = options?.pollIntervalMs ?? 500;
68
+ const deadline = Date.now() + timeoutMs;
69
+
70
+ while (Date.now() < deadline) {
71
+ const port = _readListeningPortForPid(pid);
72
+ if (port !== null) return port;
73
+ if (!_isProcessAlive(pid)) return null;
74
+ await Bun.sleep(pollIntervalMs);
75
+ }
76
+ return null;
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Platform dispatch
81
+ // ---------------------------------------------------------------------------
82
+
83
+ export function _readListeningPortForPid(pid: number): number | null {
84
+ const platform = process.platform;
85
+ if (platform === "linux") {
86
+ return linuxReadListeningPort(pid, 0);
87
+ } else if (platform === "darwin") {
88
+ return _macosReadListeningPort(pid, 0);
89
+ } else {
90
+ return _windowsReadListeningPort(pid, 0);
91
+ }
92
+ }
93
+
94
+ export function _isProcessAlive(pid: number): boolean {
95
+ if (process.platform === "linux") {
96
+ return existsSync(`/proc/${pid}`);
97
+ }
98
+ try {
99
+ process.kill(pid, 0);
100
+ return true;
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Linux implementation
108
+ // ---------------------------------------------------------------------------
109
+
110
+ const LINUX_MAX_CHILD_DEPTH = 3;
111
+
112
+ /**
113
+ * Parse a single /proc/net/tcp or /proc/net/tcp6 line.
114
+ * Returns {inode, port} for LISTEN sockets (state 0A), or null otherwise.
115
+ */
116
+ export function _parseLinuxTcpLine(line: string): { inode: number; port: number } | null {
117
+ // Format: sl local_address rem_address st tx:rx tr:when retrans uid timeout inode
118
+ const trimmed = line.trim();
119
+ if (!trimmed || trimmed.startsWith("sl")) return null;
120
+
121
+ const cols = trimmed.split(/\s+/);
122
+ if (cols.length < 10) return null;
123
+
124
+ const localAddr: string = cols[1] ?? "";
125
+ const stateHex: string = cols[3] ?? "";
126
+ const inodeStr: string = cols[9] ?? "";
127
+
128
+ // Only LISTEN state (0x0A)
129
+ if (stateHex.toUpperCase() !== "0A") return null;
130
+ if (!localAddr) return null;
131
+
132
+ const colonIdx = localAddr.indexOf(":");
133
+ if (colonIdx === -1) return null;
134
+
135
+ // Port is big-endian hex after the colon
136
+ const portHex = localAddr.slice(colonIdx + 1);
137
+ const port = parseInt(portHex, 16);
138
+ if (isNaN(port) || port <= 0) return null;
139
+
140
+ const inode = parseInt(inodeStr, 10);
141
+ if (isNaN(inode)) return null;
142
+
143
+ return { inode, port };
144
+ }
145
+
146
+ /** Parse /proc/net/tcp or /proc/net/tcp6 content. Returns map of inode -> port for LISTEN sockets. */
147
+ export function _parseLinuxTcpTable(content: string): Map<number, number> {
148
+ const result = new Map<number, number>();
149
+ for (const line of content.split("\n")) {
150
+ const entry = _parseLinuxTcpLine(line);
151
+ if (entry !== null) {
152
+ result.set(entry.inode, entry.port);
153
+ }
154
+ }
155
+ return result;
156
+ }
157
+
158
+ /** Get socket inodes owned by a PID via /proc/<pid>/fd/* symlinks. */
159
+ export function _getLinuxPidSocketInodes(pid: number): Set<number> {
160
+ const inodes = new Set<number>();
161
+ const fdDir = `/proc/${pid}/fd`;
162
+ if (!existsSync(fdDir)) return inodes;
163
+
164
+ let fds: string[];
165
+ try {
166
+ fds = readdirSync(fdDir);
167
+ } catch {
168
+ return inodes;
169
+ }
170
+
171
+ for (const fd of fds) {
172
+ try {
173
+ const target = readlinkSync(`${fdDir}/${fd}`);
174
+ // Socket symlinks look like: socket:[<inode>]
175
+ const match = target.match(/^socket:\[(\d+)\]$/);
176
+ if (match && match[1]) {
177
+ inodes.add(parseInt(match[1], 10));
178
+ }
179
+ } catch {
180
+ // Permission denied or fd disappeared — skip
181
+ }
182
+ }
183
+ return inodes;
184
+ }
185
+
186
+ function readProcFile(path: string): string {
187
+ try {
188
+ return readFileSync(path, "utf8");
189
+ } catch {
190
+ return "";
191
+ }
192
+ }
193
+
194
+ export function _linuxGetListeningPort(
195
+ tcpContent: string,
196
+ tcp6Content: string,
197
+ socketInodes: Set<number>,
198
+ ): number | null {
199
+ const table4 = _parseLinuxTcpTable(tcpContent);
200
+ const table6 = _parseLinuxTcpTable(tcp6Content);
201
+
202
+ for (const inode of socketInodes) {
203
+ const port4 = table4.get(inode);
204
+ if (port4 !== undefined) return port4;
205
+ const port6 = table6.get(inode);
206
+ if (port6 !== undefined) return port6;
207
+ }
208
+ return null;
209
+ }
210
+
211
+ function linuxReadListeningPort(pid: number, depth: number): number | null {
212
+ if (depth > LINUX_MAX_CHILD_DEPTH) return null;
213
+
214
+ const tcpContent = readProcFile(`/proc/${pid}/net/tcp`);
215
+ const tcp6Content = readProcFile(`/proc/${pid}/net/tcp6`);
216
+ const socketInodes = _getLinuxPidSocketInodes(pid);
217
+
218
+ const port = _linuxGetListeningPort(tcpContent, tcp6Content, socketInodes);
219
+ if (port !== null) return port;
220
+
221
+ // Walk children if no listening port found
222
+ const children = _linuxGetChildren(pid);
223
+ for (const childPid of children) {
224
+ const childPort = linuxReadListeningPort(childPid, depth + 1);
225
+ if (childPort !== null) return childPort;
226
+ }
227
+ return null;
228
+ }
229
+
230
+ export function _linuxGetChildren(pid: number): number[] {
231
+ // /proc/<pid>/task/<pid>/children lists direct child PIDs (space-separated)
232
+ const content = readProcFile(`/proc/${pid}/task/${pid}/children`).trim();
233
+ if (!content) return [];
234
+ return content
235
+ .split(/\s+/)
236
+ .map((s) => parseInt(s, 10))
237
+ .filter((n) => !isNaN(n) && n > 0);
238
+ }
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // macOS implementation
242
+ // ---------------------------------------------------------------------------
243
+
244
+ const MACOS_MAX_CHILD_DEPTH = 3;
245
+
246
+ /**
247
+ * Parse lsof tabular output.
248
+ * Returns the first listening port, preferring loopback/any addresses.
249
+ */
250
+ export function _parseMacosLsofOutput(output: string): number | null {
251
+ const lines = output.split("\n");
252
+ const fallbackCandidates: number[] = [];
253
+
254
+ for (const line of lines) {
255
+ const trimmed = line.trim();
256
+ if (!trimmed || trimmed.startsWith("COMMAND")) continue;
257
+
258
+ const cols = trimmed.split(/\s+/);
259
+ // lsof NAME column: "host:port (LISTEN)" — may appear as two separate tokens
260
+ // when split by whitespace. Find the token that looks like "host:port".
261
+ // Strategy: reconstruct the trailing portion after NODE column (index 8),
262
+ // then strip "(LISTEN)".
263
+ // Simpler: join the last 1-2 cols and strip the LISTEN annotation.
264
+ const rawName = cols.slice(8).join(" ").replace(/\s*\(LISTEN\)\s*$/, "");
265
+ // rawName is now "NODE host:port" — take the last token which is "host:port"
266
+ const nameParts = rawName.trim().split(/\s+/);
267
+ const name: string = nameParts[nameParts.length - 1] ?? "";
268
+ if (!name) continue;
269
+ const nameWithoutListen = name;
270
+ const lastColon = nameWithoutListen.lastIndexOf(":");
271
+ if (lastColon === -1) continue;
272
+
273
+ const portStr = nameWithoutListen.slice(lastColon + 1);
274
+ const port = parseInt(portStr, 10);
275
+ if (isNaN(port) || port <= 0) continue;
276
+
277
+ const host = nameWithoutListen.slice(0, lastColon);
278
+ // Prefer loopback / wildcard
279
+ if (
280
+ host === "127.0.0.1" ||
281
+ host === "::1" ||
282
+ host === "*" ||
283
+ host === "0.0.0.0" ||
284
+ host === "::"
285
+ ) {
286
+ return port;
287
+ }
288
+ fallbackCandidates.push(port);
289
+ }
290
+
291
+ return fallbackCandidates.length > 0 ? (fallbackCandidates[0] ?? null) : null;
292
+ }
293
+
294
+ export function _macosReadListeningPort(pid: number, depth: number): number | null {
295
+ if (depth > MACOS_MAX_CHILD_DEPTH) return null;
296
+
297
+ const result = spawnSync({
298
+ cmd: ["lsof", "-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", String(pid)],
299
+ stdout: "pipe",
300
+ stderr: "pipe",
301
+ });
302
+
303
+ const output = result.stdout.toString();
304
+ const port = _parseMacosLsofOutput(output);
305
+ if (port !== null) return port;
306
+
307
+ // Walk children via pgrep
308
+ const children = _macosGetChildren(pid);
309
+ for (const childPid of children) {
310
+ const childPort = _macosReadListeningPort(childPid, depth + 1);
311
+ if (childPort !== null) return childPort;
312
+ }
313
+ return null;
314
+ }
315
+
316
+ export function _macosGetChildren(pid: number): number[] {
317
+ const result = spawnSync({
318
+ cmd: ["pgrep", "-P", String(pid)],
319
+ stdout: "pipe",
320
+ stderr: "pipe",
321
+ });
322
+ const output = result.stdout.toString().trim();
323
+ if (!output) return [];
324
+ return output
325
+ .split("\n")
326
+ .map((s) => parseInt(s.trim(), 10))
327
+ .filter((n) => !isNaN(n) && n > 0);
328
+ }
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // Windows implementation
332
+ // ---------------------------------------------------------------------------
333
+
334
+ const WINDOWS_MAX_CHILD_DEPTH = 3;
335
+
336
+ /** Parse PowerShell Get-NetTCPConnection JSON output. */
337
+ export function _parseWindowsPowerShellOutput(json: string): number | null {
338
+ const trimmed = json.trim();
339
+ if (!trimmed || trimmed === "null") return null;
340
+
341
+ type PortObject = { LocalPort: unknown };
342
+
343
+ try {
344
+ const parsed: unknown = JSON.parse(trimmed);
345
+ if (parsed === null || parsed === undefined) return null;
346
+
347
+ if (Array.isArray(parsed)) {
348
+ for (const item of parsed as unknown[]) {
349
+ if (item !== null && typeof item === "object" && "LocalPort" in (item as object)) {
350
+ const port = Number((item as PortObject).LocalPort);
351
+ if (!isNaN(port) && port > 0) return port;
352
+ }
353
+ }
354
+ return null;
355
+ }
356
+
357
+ if (typeof parsed === "object" && parsed !== null && "LocalPort" in parsed) {
358
+ const port = Number((parsed as PortObject).LocalPort);
359
+ return !isNaN(port) && port > 0 ? port : null;
360
+ }
361
+
362
+ return null;
363
+ } catch {
364
+ return null;
365
+ }
366
+ }
367
+
368
+ /** Parse netstat -ano tabular output for a given PID. */
369
+ export function _parseWindowsNetstatOutput(output: string, pid: number): number | null {
370
+ // Columns: Proto LocalAddress ForeignAddress State PID
371
+ // e.g.: TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 1234
372
+ const pidStr = String(pid);
373
+ for (const line of output.split("\n")) {
374
+ const trimmed = line.trim();
375
+ if (!trimmed) continue;
376
+
377
+ const cols = trimmed.split(/\s+/);
378
+ if (cols.length < 5) continue;
379
+
380
+ const proto: string = (cols[0] ?? "").toUpperCase();
381
+ const localAddr: string = cols[1] ?? "";
382
+ const state: string = (cols[3] ?? "").toUpperCase();
383
+ const linePid: string = cols[4] ?? "";
384
+
385
+ if (proto !== "TCP" && proto !== "TCP6") continue;
386
+ if (state !== "LISTENING") continue;
387
+ if (linePid !== pidStr) continue;
388
+
389
+ const lastColon = localAddr.lastIndexOf(":");
390
+ if (lastColon === -1) continue;
391
+
392
+ const port = parseInt(localAddr.slice(lastColon + 1), 10);
393
+ if (!isNaN(port) && port > 0) return port;
394
+ }
395
+ return null;
396
+ }
397
+
398
+ export function _windowsReadListeningPort(pid: number, depth: number): number | null {
399
+ if (depth > WINDOWS_MAX_CHILD_DEPTH) return null;
400
+
401
+ const psResult = spawnSync({
402
+ cmd: [
403
+ "powershell",
404
+ "-NoProfile",
405
+ "-Command",
406
+ `Get-NetTCPConnection -OwningProcess ${pid} -State Listen -ErrorAction SilentlyContinue | Select-Object LocalPort | ConvertTo-Json`,
407
+ ],
408
+ stdout: "pipe",
409
+ stderr: "pipe",
410
+ });
411
+
412
+ const stderr = psResult.stderr.toString();
413
+ const useFallback =
414
+ !psResult.success ||
415
+ stderr.includes("is not recognized") ||
416
+ stderr.includes("CommandNotFoundException");
417
+
418
+ if (!useFallback) {
419
+ const port = _parseWindowsPowerShellOutput(psResult.stdout.toString());
420
+ if (port !== null) return port;
421
+ } else {
422
+ // Fallback: netstat -ano | findstr <pid>
423
+ const nsResult = spawnSync({
424
+ cmd: ["cmd", "/c", `netstat -ano | findstr ${pid}`],
425
+ stdout: "pipe",
426
+ stderr: "pipe",
427
+ });
428
+ const port = _parseWindowsNetstatOutput(nsResult.stdout.toString(), pid);
429
+ if (port !== null) return port;
430
+ }
431
+
432
+ // Walk children
433
+ const children = _windowsGetChildren(pid);
434
+ for (const childPid of children) {
435
+ const childPort = _windowsReadListeningPort(childPid, depth + 1);
436
+ if (childPort !== null) return childPort;
437
+ }
438
+ return null;
439
+ }
440
+
441
+ export function _windowsGetChildren(pid: number): number[] {
442
+ const psResult = spawnSync({
443
+ cmd: [
444
+ "powershell",
445
+ "-NoProfile",
446
+ "-Command",
447
+ `Get-CimInstance Win32_Process -Filter "ParentProcessId=${pid}" | Select-Object ProcessId | ConvertTo-Json`,
448
+ ],
449
+ stdout: "pipe",
450
+ stderr: "pipe",
451
+ });
452
+
453
+ const stderr = psResult.stderr.toString();
454
+ const useFallback =
455
+ !psResult.success ||
456
+ stderr.includes("is not recognized") ||
457
+ stderr.includes("CommandNotFoundException");
458
+
459
+ if (!useFallback) {
460
+ const output = psResult.stdout.toString().trim();
461
+ if (!output || output === "null") return [];
462
+ try {
463
+ const parsed: unknown = JSON.parse(output);
464
+ type PidObject = { ProcessId: unknown };
465
+ if (Array.isArray(parsed)) {
466
+ return (parsed as unknown[])
467
+ .filter(
468
+ (item): item is PidObject =>
469
+ item !== null && typeof item === "object" && "ProcessId" in (item as object),
470
+ )
471
+ .map((item) => Number(item.ProcessId))
472
+ .filter((n) => !isNaN(n) && n > 0);
473
+ }
474
+ if (typeof parsed === "object" && parsed !== null && "ProcessId" in parsed) {
475
+ const id = Number((parsed as PidObject).ProcessId);
476
+ return !isNaN(id) && id > 0 ? [id] : [];
477
+ }
478
+ } catch {
479
+ // ignore
480
+ }
481
+ return [];
482
+ }
483
+
484
+ // Fallback: wmic
485
+ const wmicResult = spawnSync({
486
+ cmd: ["wmic", "process", "where", `(ParentProcessId=${pid})`, "get", "ProcessId"],
487
+ stdout: "pipe",
488
+ stderr: "pipe",
489
+ });
490
+ return wmicResult
491
+ .stdout.toString()
492
+ .split("\n")
493
+ .slice(1) // skip header
494
+ .map((s) => parseInt(s.trim(), 10))
495
+ .filter((n) => !isNaN(n) && n > 0);
496
+ }
@@ -471,6 +471,22 @@ export function parseSessionEnvValue(stdout: string, key: string): string | null
471
471
  return line ? line.slice(prefix.length) : null;
472
472
  }
473
473
 
474
+ /**
475
+ * Get the PID of the foreground process in a tmux pane.
476
+ * Returns null if the pane no longer exists or the query fails.
477
+ *
478
+ * Note: this is the pane's "current" process — typically the agent
479
+ * itself when the pane was created with the agent as the initial command.
480
+ * If tmux exec'd a wrapper shell that then exec'd the agent, the PID
481
+ * will refer to the same process (exec replaces in-place).
482
+ */
483
+ export function getPanePid(paneId: string): number | null {
484
+ const result = tmuxRun(["display-message", "-t", paneId, "-p", "#{pane_pid}"]);
485
+ if (!result.ok) return null;
486
+ const pid = Number(result.stdout.trim());
487
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
488
+ }
489
+
474
490
  /**
475
491
  * Read a session-level environment variable.
476
492
  * Returns `null` when the session doesn't exist or the variable isn't set.
package/src/sdk/types.ts CHANGED
@@ -50,6 +50,7 @@ type SessionOptionsMap = {
50
50
  parentID?: string;
51
51
  title?: string;
52
52
  workspaceID?: string;
53
+ permission?: import("@opencode-ai/sdk/v2").PermissionRuleset;
53
54
  };
54
55
  copilot: Partial<CopilotSessionConfig>;
55
56
  claude: Record<string, never>;
@@ -129,7 +129,10 @@ export default defineWorkflow({
129
129
  "Map codebase, count LOC, partition for parallel specialists",
130
130
  },
131
131
  {},
132
- { title: "codebase-scout" },
132
+ {
133
+ title: "codebase-scout",
134
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
135
+ },
133
136
  async (s) => {
134
137
  const data = scoutCodebase(root);
135
138
  if (data.units.length === 0) {
@@ -193,7 +196,10 @@ export default defineWorkflow({
193
196
  description: "Locate prior research docs (codebase-research-locator)",
194
197
  },
195
198
  {},
196
- { title: "history-locator" },
199
+ {
200
+ title: "history-locator",
201
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
202
+ },
197
203
  async (s) => {
198
204
  const result = await s.client.session.prompt({
199
205
  sessionID: s.session.id,
@@ -219,7 +225,10 @@ export default defineWorkflow({
219
225
  description: "Synthesize prior research (codebase-research-analyzer)",
220
226
  },
221
227
  {},
222
- { title: "history-analyzer" },
228
+ {
229
+ title: "history-analyzer",
230
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
231
+ },
223
232
  async (s) => {
224
233
  const result = await s.client.session.prompt({
225
234
  sessionID: s.session.id,
@@ -300,7 +309,10 @@ export default defineWorkflow({
300
309
  description: `Layer 1 dispatch (${batch.length} tasks)`,
301
310
  },
302
311
  {},
303
- { title: `wave1-batch-${batchNumber}` },
312
+ {
313
+ title: `wave1-batch-${batchNumber}`,
314
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
315
+ },
304
316
  async (s) => {
305
317
  const taskSpecs = batch.map((t) => {
306
318
  const builder =
@@ -387,7 +399,10 @@ export default defineWorkflow({
387
399
  description: `Layer 2 dispatch (${batch.length} tasks)`,
388
400
  },
389
401
  {},
390
- { title: `wave2-batch-${batchNumber}` },
402
+ {
403
+ title: `wave2-batch-${batchNumber}`,
404
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
405
+ },
391
406
  async (s) => {
392
407
  const taskSpecs = batch.map((t) => {
393
408
  const specialistPrompt =
@@ -485,7 +500,10 @@ export default defineWorkflow({
485
500
  "Synthesize partition findings + history into final research doc",
486
501
  },
487
502
  {},
488
- { title: "aggregator" },
503
+ {
504
+ title: "aggregator",
505
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
506
+ },
489
507
  async (s) => {
490
508
  const result = await s.client.session.prompt({
491
509
  sessionID: s.session.id,