@bastani/atomic 0.6.5-0 → 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.
- package/.agents/skills/ado-commit/SKILL.md +2 -0
- package/.agents/skills/ado-create-pr/SKILL.md +2 -0
- package/.agents/skills/advanced-evaluation/SKILL.md +2 -0
- package/.agents/skills/ast-grep/SKILL.md +2 -0
- package/.agents/skills/bdi-mental-states/SKILL.md +2 -0
- package/.agents/skills/bun/SKILL.md +156 -122
- package/.agents/skills/context-compression/SKILL.md +2 -0
- package/.agents/skills/context-degradation/SKILL.md +2 -0
- package/.agents/skills/context-fundamentals/SKILL.md +2 -0
- package/.agents/skills/context-optimization/SKILL.md +2 -0
- package/.agents/skills/create-spec/SKILL.md +2 -0
- package/.agents/skills/docx/SKILL.md +2 -0
- package/.agents/skills/evaluation/SKILL.md +2 -0
- package/.agents/skills/explain-code/SKILL.md +2 -0
- package/.agents/skills/filesystem-context/SKILL.md +2 -0
- package/.agents/skills/find-skills/SKILL.md +2 -0
- package/.agents/skills/gh-commit/SKILL.md +2 -0
- package/.agents/skills/gh-create-pr/SKILL.md +2 -0
- package/.agents/skills/hosted-agents/SKILL.md +2 -0
- package/.agents/skills/impeccable/SKILL.md +117 -304
- package/.agents/skills/impeccable/agents/openai.yaml +4 -0
- package/.agents/skills/{adapt/SKILL.md → impeccable/reference/adapt.md} +2 -11
- package/.agents/skills/{animate/SKILL.md → impeccable/reference/animate.md} +15 -15
- package/.agents/skills/{audit/SKILL.md → impeccable/reference/audit.md} +8 -22
- package/.agents/skills/{bolder/SKILL.md → impeccable/reference/bolder.md} +9 -13
- package/.agents/skills/impeccable/reference/brand.md +114 -0
- package/.agents/skills/{clarify/SKILL.md → impeccable/reference/clarify.md} +2 -11
- package/.agents/skills/{colorize/SKILL.md → impeccable/reference/colorize.md} +23 -12
- package/.agents/skills/impeccable/reference/craft.md +152 -29
- package/.agents/skills/{critique/SKILL.md → impeccable/reference/critique.md} +25 -37
- package/.agents/skills/{delight/SKILL.md → impeccable/reference/delight.md} +9 -11
- package/.agents/skills/{distill/SKILL.md → impeccable/reference/distill.md} +2 -13
- package/.agents/skills/impeccable/reference/document.md +427 -0
- package/.agents/skills/impeccable/reference/extract.md +1 -1
- package/.agents/skills/{harden/SKILL.md → impeccable/reference/harden.md} +1 -43
- package/.agents/skills/{layout/SKILL.md → impeccable/reference/layout.md} +27 -11
- package/.agents/skills/impeccable/reference/live.md +594 -0
- package/.agents/skills/impeccable/reference/motion-design.md +12 -2
- package/.agents/skills/impeccable/reference/onboard.md +234 -0
- package/.agents/skills/{optimize/SKILL.md → impeccable/reference/optimize.md} +4 -12
- package/.agents/skills/{overdrive/SKILL.md → impeccable/reference/overdrive.md} +9 -21
- package/.agents/skills/{critique → impeccable}/reference/personas.md +1 -1
- package/.agents/skills/{polish/SKILL.md → impeccable/reference/polish.md} +31 -23
- package/.agents/skills/impeccable/reference/product.md +62 -0
- package/.agents/skills/{quieter/SKILL.md → impeccable/reference/quieter.md} +7 -11
- package/.agents/skills/impeccable/reference/shape.md +151 -0
- package/.agents/skills/impeccable/reference/teach.md +156 -0
- package/.agents/skills/{typeset/SKILL.md → impeccable/reference/typeset.md} +19 -11
- package/.agents/skills/impeccable/reference/typography.md +31 -14
- package/.agents/skills/impeccable/scripts/cleanup-deprecated.mjs +87 -17
- package/.agents/skills/impeccable/scripts/command-metadata.json +94 -0
- package/.agents/skills/impeccable/scripts/design-parser.mjs +820 -0
- package/.agents/skills/impeccable/scripts/detect-csp.mjs +198 -0
- package/.agents/skills/impeccable/scripts/is-generated.mjs +69 -0
- package/.agents/skills/impeccable/scripts/live-accept.mjs +595 -0
- package/.agents/skills/impeccable/scripts/live-browser.js +4781 -0
- package/.agents/skills/impeccable/scripts/live-inject.mjs +445 -0
- package/.agents/skills/impeccable/scripts/live-poll.mjs +186 -0
- package/.agents/skills/impeccable/scripts/live-server.mjs +694 -0
- package/.agents/skills/impeccable/scripts/live-wrap.mjs +571 -0
- package/.agents/skills/impeccable/scripts/live.mjs +247 -0
- package/.agents/skills/impeccable/scripts/load-context.mjs +141 -0
- package/.agents/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
- package/.agents/skills/impeccable/scripts/pin.mjs +214 -0
- package/.agents/skills/init/SKILL.md +2 -0
- package/.agents/skills/liteparse/SKILL.md +1 -0
- package/.agents/skills/memory-systems/SKILL.md +2 -0
- package/.agents/skills/multi-agent-patterns/SKILL.md +2 -0
- package/.agents/skills/opentui/SKILL.md +1 -0
- package/.agents/skills/pdf/SKILL.md +2 -0
- package/.agents/skills/playwright-cli/SKILL.md +51 -5
- package/.agents/skills/playwright-cli/references/playwright-tests.md +1 -1
- package/.agents/skills/playwright-cli/references/running-code.md +10 -0
- package/.agents/skills/playwright-cli/references/session-management.md +56 -0
- package/.agents/skills/playwright-cli/references/spec-driven-testing.md +305 -0
- package/.agents/skills/playwright-cli/references/test-generation.md +49 -3
- package/.agents/skills/pptx/SKILL.md +2 -0
- package/.agents/skills/project-development/SKILL.md +2 -0
- package/.agents/skills/prompt-engineer/SKILL.md +2 -0
- package/.agents/skills/research-codebase/SKILL.md +2 -0
- package/.agents/skills/ripgrep/SKILL.md +2 -0
- package/.agents/skills/skill-creator/LICENSE.txt +1 -1
- package/.agents/skills/skill-creator/SKILL.md +2 -0
- package/.agents/skills/sl-commit/SKILL.md +2 -0
- package/.agents/skills/sl-submit-diff/SKILL.md +2 -0
- package/.agents/skills/tdd/SKILL.md +4 -0
- package/.agents/skills/tool-design/SKILL.md +2 -0
- package/.agents/skills/typescript-advanced-types/SKILL.md +2 -1
- package/.agents/skills/typescript-expert/SKILL.md +7 -1
- package/.agents/skills/typescript-react-reviewer/SKILL.md +2 -1
- package/.agents/skills/workflow-creator/SKILL.md +75 -72
- package/.agents/skills/workflow-creator/references/session-config.md +48 -1
- package/.agents/skills/xlsx/SKILL.md +2 -0
- package/.opencode/opencode.json +4 -2
- package/dist/sdk/runtime/executor.d.ts +8 -0
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/runtime/port-discovery.d.ts +71 -0
- package/dist/sdk/runtime/port-discovery.d.ts.map +1 -0
- package/dist/sdk/runtime/tmux.d.ts +10 -0
- package/dist/sdk/runtime/tmux.d.ts.map +1 -1
- package/dist/sdk/types.d.ts +1 -0
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts +15 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/sdk/runtime/executor.test.ts +254 -1
- package/src/sdk/runtime/executor.ts +135 -89
- package/src/sdk/runtime/port-discovery.test.ts +573 -0
- package/src/sdk/runtime/port-discovery.ts +496 -0
- package/src/sdk/runtime/tmux.ts +16 -0
- package/src/sdk/types.ts +1 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +24 -6
- package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +52 -13
- package/src/sdk/workflows/builtin/ralph/claude/index.ts +31 -3
- package/src/sdk/workflows/builtin/ralph/copilot/index.ts +16 -0
- package/src/sdk/workflows/builtin/ralph/helpers/prompts.ts +70 -3
- package/src/sdk/workflows/builtin/ralph/opencode/index.ts +50 -6
- package/.agents/skills/shape/SKILL.md +0 -96
- /package/.agents/skills/{critique → impeccable}/reference/cognitive-load.md +0 -0
- /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
|
+
}
|
package/src/sdk/runtime/tmux.ts
CHANGED
|
@@ -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
|
@@ -129,7 +129,10 @@ export default defineWorkflow({
|
|
|
129
129
|
"Map codebase, count LOC, partition for parallel specialists",
|
|
130
130
|
},
|
|
131
131
|
{},
|
|
132
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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,
|