Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/skills/simulator-workflow/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ The `bazel_ios_build_and_run` tool chains these steps (each failure short-circui

Env vars are passed via `SIMCTL_CHILD_*` prefix convention — simctl forwards them to the launched process.

For **Cursor DEBUG MODE** (NDJSON hypothesis logs), use the `swift-agent-debug-log` skill and `bazel_ios_agent_debug_*` tools:

- `AGENT_DEBUG_LOG_PATH` / `AGENT_DEBUG_SESSION_ID` via `launchEnv` on `build_and_run` or `agent_debug_repro`
- Simulator fallback: `Documents/agent-debug.ndjson` + `bazel_ios_agent_debug_log_pull`
- Do not rely on the sim app writing directly to `.cursor/debug-*.log` on the host

## Platform CPU Flags

| Platform | Flag |
Expand Down
109 changes: 109 additions & 0 deletions .agents/skills/swift-agent-debug-log/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
name: swift-agent-debug-log
description: >-
Instrument Swift/iOS apps for Cursor DEBUG MODE: NDJSON hypothesis logs on the
host or simulator Documents. Use with XcodeBazelMCP agent_debug tools (clear,
read, pull, repro) instead of Read/delete_file on host paths from sim code.
---

# Swift Agent Debug Log (Cursor DEBUG MODE)

XcodeBazelMCP owns **runtime** (build, launch, read logs). This skill is the **protocol** (NDJSON schema, `hypothesisId`, session cleanup).

## Log path strategy

| Runtime | Where Swift writes | How the agent reads |
|---------|-------------------|---------------------|
| macOS / unit tests | Host path from env | `bazel_ios_agent_debug_log_read` |
| iOS Simulator | `Documents/agent-debug.ndjson` (sandbox-safe) | `bazel_ios_agent_debug_log_pull` or env if using host mount |

**Never** hardcode repo paths like `Apps/Consumer/.cursor/debug-*.log` in app code — the sim sandbox cannot write there.

### Environment (preferred for host / tests)

`bazel_ios_build_and_run` / `bazel_ios_agent_debug_repro` pass via `launchEnv` → `SIMCTL_CHILD_*`:

- `AGENT_DEBUG_LOG_PATH` — absolute host path (e.g. `<workspace>/.cursor/debug-{session}.log`)
- `AGENT_DEBUG_SESSION_ID` — Cursor debug session id

Swift reads `ProcessInfo.processInfo.environment` and appends one NDJSON object per line.

### Simulator fallback

If no host path is writable, append NDJSON to:

`FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]/agent-debug.ndjson`

Then pull with `bazel_ios_agent_debug_log_pull` (`destPath` optional copy to `.cursor/debug-*.log`).

## Swift instrumentation (minimal)

```swift
func agentDebugLog(
location: String,
message: String,
data: [String: Any] = [:],
hypothesisId: String,
runId: String = "pre-fix"
) {
let env = ProcessInfo.processInfo.environment
let sessionId = env["AGENT_DEBUG_SESSION_ID"] ?? ""
let payload: [String: Any] = [
"sessionId": sessionId,
"location": location,
"message": message,
"data": data,
"hypothesisId": hypothesisId,
"runId": runId,
"timestamp": Int(Date().timeIntervalSince1970 * 1000),
]
guard let line = try? JSONSerialization.data(withJSONObject: payload),
let str = String(data: line, encoding: .utf8) else { return }
let row = str + "\n"
if let path = env["AGENT_DEBUG_LOG_PATH"], !path.isEmpty {
if let handle = FileHandle(forWritingAtPath: path) {
handle.seekToEndOfFile(); handle.write(Data(row.utf8)); try? handle.close()
} else {
FileManager.default.createFile(atPath: path, contents: Data(row.utf8))
}
return
}
let doc = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let url = doc.appendingPathComponent("agent-debug.ndjson")
if let handle = try? FileHandle(forWritingTo: url) {
handle.seekToEndOfFile(); handle.write(Data(row.utf8)); try? handle.close()
} else {
try? row.write(to: url, atomically: true, encoding: .utf8)
}
}
```

Wrap calls in `// #region agent log` … `// #endregion` so Xcode folds them.

## Agent workflow (use MCP tools, not raw file tools)

1. `bazel_ios_agent_debug_log_clear` — `{ "logPath": "<abs>/.cursor/debug-{session}.log" }`
2. `bazel_ios_agent_debug_repro` or `bazel_ios_build_and_run` with `launchEnv` / repro sets `AGENT_DEBUG_*`
3. User reproduces the bug
4. `bazel_ios_agent_debug_log_read` — filter `hypothesisId`, `runId`; use `hypothesisStatusHints` for CONFIRMED/REJECTED hints
5. If empty on host → `bazel_ios_agent_debug_log_pull` with `bundleId` + optional `destPath`

MCP resource: `xcodebazel://agent-debug-log?path=<url-encoded-abs-path>`

## NDJSON line schema

```json
{
"sessionId": "522bed",
"location": "MyType.swift:42",
"message": "branch taken",
"data": { "count": 3 },
"hypothesisId": "A",
"runId": "pre-fix",
"timestamp": 1733456789000
}
```

## Optional: os_log transport

`bazel_ios_log_capture_start` with `jsonLinesOnly: true` or `messageContains: "agentDebugLog"` — noisier than file pull; use when you cannot add file IO.
109 changes: 109 additions & 0 deletions skills/swift-agent-debug-log/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
name: swift-agent-debug-log
description: >-
Instrument Swift/iOS apps for Cursor DEBUG MODE: NDJSON hypothesis logs on the
host or simulator Documents. Use with XcodeBazelMCP agent_debug tools (clear,
read, pull, repro) instead of Read/delete_file on host paths from sim code.
---

# Swift Agent Debug Log (Cursor DEBUG MODE)

XcodeBazelMCP owns **runtime** (build, launch, read logs). This skill is the **protocol** (NDJSON schema, `hypothesisId`, session cleanup).

## Log path strategy

| Runtime | Where Swift writes | How the agent reads |
|---------|-------------------|---------------------|
| macOS / unit tests | Host path from env | `bazel_ios_agent_debug_log_read` |
| iOS Simulator | `Documents/agent-debug.ndjson` (sandbox-safe) | `bazel_ios_agent_debug_log_pull` or env if using host mount |

**Never** hardcode repo paths like `Apps/Consumer/.cursor/debug-*.log` in app code — the sim sandbox cannot write there.

### Environment (preferred for host / tests)

`bazel_ios_build_and_run` / `bazel_ios_agent_debug_repro` pass via `launchEnv` → `SIMCTL_CHILD_*`:

- `AGENT_DEBUG_LOG_PATH` — absolute host path (e.g. `<workspace>/.cursor/debug-{session}.log`)
- `AGENT_DEBUG_SESSION_ID` — Cursor debug session id

Swift reads `ProcessInfo.processInfo.environment` and appends one NDJSON object per line.

### Simulator fallback

If no host path is writable, append NDJSON to:

`FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]/agent-debug.ndjson`

Then pull with `bazel_ios_agent_debug_log_pull` (`destPath` optional copy to `.cursor/debug-*.log`).

## Swift instrumentation (minimal)

```swift
func agentDebugLog(
location: String,
message: String,
data: [String: Any] = [:],
hypothesisId: String,
runId: String = "pre-fix"
) {
let env = ProcessInfo.processInfo.environment
let sessionId = env["AGENT_DEBUG_SESSION_ID"] ?? ""
let payload: [String: Any] = [
"sessionId": sessionId,
"location": location,
"message": message,
"data": data,
"hypothesisId": hypothesisId,
"runId": runId,
"timestamp": Int(Date().timeIntervalSince1970 * 1000),
]
guard let line = try? JSONSerialization.data(withJSONObject: payload),
let str = String(data: line, encoding: .utf8) else { return }
let row = str + "\n"
if let path = env["AGENT_DEBUG_LOG_PATH"], !path.isEmpty {
if let handle = FileHandle(forWritingAtPath: path) {
handle.seekToEndOfFile(); handle.write(Data(row.utf8)); try? handle.close()
} else {
FileManager.default.createFile(atPath: path, contents: Data(row.utf8))
}
return
}
let doc = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let url = doc.appendingPathComponent("agent-debug.ndjson")
if let handle = try? FileHandle(forWritingTo: url) {
handle.seekToEndOfFile(); handle.write(Data(row.utf8)); try? handle.close()
} else {
try? row.write(to: url, atomically: true, encoding: .utf8)
}
}
```

Wrap calls in `// #region agent log` … `// #endregion` so Xcode folds them.

## Agent workflow (use MCP tools, not raw file tools)

1. `bazel_ios_agent_debug_log_clear` — `{ "logPath": "<abs>/.cursor/debug-{session}.log" }`
2. `bazel_ios_agent_debug_repro` or `bazel_ios_build_and_run` with `launchEnv` / repro sets `AGENT_DEBUG_*`
3. User reproduces the bug
4. `bazel_ios_agent_debug_log_read` — filter `hypothesisId`, `runId`; use `hypothesisStatusHints` for CONFIRMED/REJECTED hints
5. If empty on host → `bazel_ios_agent_debug_log_pull` with `bundleId` + optional `destPath`

MCP resource: `xcodebazel://agent-debug-log?path=<url-encoded-abs-path>`

## NDJSON line schema

```json
{
"sessionId": "522bed",
"location": "MyType.swift:42",
"message": "branch taken",
"data": { "count": 3 },
"hypothesisId": "A",
"runId": "pre-fix",
"timestamp": 1733456789000
}
```

## Optional: os_log transport

`bazel_ios_log_capture_start` with `jsonLinesOnly: true` or `messageContains: "agentDebugLog"` — noisier than file pull; use when you cannot add file IO.
4 changes: 2 additions & 2 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ describe('CLI help', () => {
});

describe('CLI tools', () => {
it('lists all 112 tools', () => {
it('lists all 116 tools', () => {
const out = run(['tools']);
const toolLines = out
.split('\n')
.filter((line) => line.match(/^[a-z_]+$/));
expect(toolLines.length).toBe(112);
expect(toolLines.length).toBe(116);
expect(out).toContain('bazel_ios_build');
expect(out).toContain('bazel_macos_build');
expect(out).toContain('bazel_tvos_build');
Expand Down
66 changes: 64 additions & 2 deletions src/cli/commands.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { createInterface } from 'node:readline';
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync, cpSync } from 'node:fs';
import { spawn } from 'node:child_process';
import { join, resolve } from 'node:path';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { homedir } from 'node:os';
import { callBazelTool, callBazelToolStreaming } from '../tools/index.js';
import type { JsonObject } from '../types/index.js';

Expand All @@ -27,6 +29,54 @@ export async function printTool(name: string, args: JsonObject): Promise<void> {
if (result.isError) process.exitCode = 1;
}

function bundledSkillsDir(): string | null {
const here = dirname(fileURLToPath(import.meta.url));
for (const candidate of [join(here, '..', 'skills'), join(here, '..', '..', 'skills')]) {
if (existsSync(candidate)) return candidate;
}
const cwdSkill = join(process.cwd(), 'skills');
if (existsSync(cwdSkill)) return cwdSkill;
return null;
}

function installBundledSkills(): void {
const source = bundledSkillsDir();
if (!source) {
console.log('No bundled skills directory found (skip skill copy).');
return;
}

const skillNames = readdirSync(source, { withFileTypes: true })
.filter((e) => e.isDirectory())
.map((e) => e.name);

const destRoots = [
join(homedir(), '.cursor', 'skills'),
join(process.cwd(), '.agents', 'skills'),
];

for (const skillName of skillNames) {
const srcSkill = join(source, skillName, 'SKILL.md');
if (!existsSync(srcSkill)) continue;
const content = readFileSync(srcSkill, 'utf-8');

for (const root of destRoots) {
const destDir = join(root, skillName);
mkdirSync(destDir, { recursive: true });
const destFile = join(destDir, 'SKILL.md');
writeFileSync(destFile, content);
console.log(`Installed skill: ${destFile}`);
}

const agentsMirror = join(process.cwd(), '.agents', 'skills', skillName);
if (!existsSync(agentsMirror)) {
mkdirSync(dirname(agentsMirror), { recursive: true });
cpSync(join(source, skillName), agentsMirror, { recursive: true });
console.log(`Installed skill: ${agentsMirror}`);
}
}
}

export function runSkillInit(): void {
const skillContent = `# XcodeBazelMCP Skill

Expand All @@ -46,6 +96,16 @@ Key commands:
- \`bazel_ios_set_defaults\` — Set default target, simulator, build mode
- \`bazel_ios_clean\` — Clean build outputs
- \`bazel_ios_log_capture_start\` / \`bazel_ios_log_capture_stop\` — Capture simulator logs
- \`bazel_ios_agent_debug_log_clear\` / \`read\` / \`pull\` / \`repro\` — Cursor DEBUG MODE NDJSON workflow

## Cursor debug mode (swift-agent-debug-log)

1. \`bazel_ios_agent_debug_log_clear\` with \`logPath\` → \`.cursor/debug-{session}.log\`
2. \`bazel_ios_agent_debug_repro\` (or \`build_and_run\` with \`launchEnv\`: \`AGENT_DEBUG_LOG_PATH\`, \`AGENT_DEBUG_SESSION_ID\`)
3. User reproduces the bug in the simulator
4. \`bazel_ios_agent_debug_log_read\` on the host log, or \`bazel_ios_agent_debug_log_pull\` if Swift wrote to \`Documents/agent-debug.ndjson\`

See skill \`swift-agent-debug-log\` (installed by \`xcodebazelmcp init\`).

All build/test/query tools support \`streaming: true\` for real-time output via MCP progress notifications.

Expand Down Expand Up @@ -83,6 +143,8 @@ xcodebazelmcp coverage //tests:tests
writeFileSync(fallback, skillContent);
console.log(`Installed: ${fallback}`);
}

installBundledSkills();
}

export async function runUpgrade(args: string[]): Promise<void> {
Expand Down
Loading
Loading