Skip to content

Commit 9b9a7e2

Browse files
committed
Merge remote-tracking branch 'origin/main'
# Conflicts: # Sources/TritonKitCLI/CLISchemaHostCommands.swift
2 parents c3a80de + beb7729 commit 9b9a7e2

12 files changed

Lines changed: 271 additions & 2 deletions

File tree

CLI/Tests/TritonKitCLITests/DeviceCrossPlatformTests.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,26 @@ struct DeviceCrossPlatformTests {
7676
}
7777
}
7878

79+
@Test("sim screenshot metadata documents raw framebuffer orientation semantics")
80+
func simulatorScreenshotMetadataDocumentsRawFramebufferOrientationSemantics() throws {
81+
let temp = FileManager.default.temporaryDirectory
82+
.appendingPathComponent("triton-sim-screenshot-metadata-\(UUID().uuidString)")
83+
try FileManager.default.createDirectory(at: temp, withIntermediateDirectories: true)
84+
let path = temp.appendingPathComponent("shot.png")
85+
try writeMinimalPNG(width: 768, height: 1024, to: path)
86+
87+
let metadata = try makeSimulatorScreenshotMetadata(outputPath: path.path)
88+
89+
#expect(metadata.path == path.path)
90+
#expect(metadata.contentType == "image/png")
91+
#expect(metadata.pixelWidth == 768)
92+
#expect(metadata.pixelHeight == 1024)
93+
#expect(metadata.orientationSemantics == "raw-simctl-framebuffer")
94+
#expect(metadata.normalizationApplied == false)
95+
#expect(metadata.normalizationStrategy == "metadata-only")
96+
#expect(metadata.note.contains("raw framebuffer"))
97+
}
98+
7999
@Test("app and smoke schemas expose unified device selector with explicit selector forms")
80100
func appAndSmokeSchemasExposeUnifiedDeviceSelector() throws {
81101
let app = try #require(commandSchemas().first { $0.name == "app" })
@@ -358,3 +378,20 @@ private func iosTarget(
358378
transport: nil
359379
)
360380
}
381+
382+
private func writeMinimalPNG(width: UInt32, height: UInt32, to url: URL) throws {
383+
var data = Data([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
384+
data.append(contentsOf: [0x00, 0x00, 0x00, 0x0D])
385+
data.append(contentsOf: [0x49, 0x48, 0x44, 0x52])
386+
data.append(UInt8((width >> 24) & 0xff))
387+
data.append(UInt8((width >> 16) & 0xff))
388+
data.append(UInt8((width >> 8) & 0xff))
389+
data.append(UInt8(width & 0xff))
390+
data.append(UInt8((height >> 24) & 0xff))
391+
data.append(UInt8((height >> 16) & 0xff))
392+
data.append(UInt8((height >> 8) & 0xff))
393+
data.append(UInt8(height & 0xff))
394+
data.append(contentsOf: [8, 6, 0, 0, 0])
395+
data.append(contentsOf: [0, 0, 0, 0])
396+
try data.write(to: url, options: .atomic)
397+
}

CLI/Tests/TritonKitCLITests/SchemaFactSourceTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3818,7 +3818,7 @@ struct SchemaFactSourceTests {
38183818
])
38193819
expectContract(sim, selector: "host.simulator-action", fields: [
38203820
"ok", "action", "runtimeScope", "target", "tool", "exitCode", "riskLevel",
3821-
"sourceCommand", "stdoutTruncated", "stderrTruncated", "artifacts", "note",
3821+
"sourceCommand", "stdoutTruncated", "stderrTruncated", "artifacts", "screenshot", "note",
38223822
])
38233823

38243824
#expect(app.failureCodes.contains("app_launch_failed"))
@@ -4047,6 +4047,7 @@ private func outputContractKindTaxonomy() -> Set<String> {
40474047
"host-device-ready",
40484048
"host-device-selection",
40494049
"host-simulator-list",
4050+
"host-simulator-screenshot-metadata",
40504051
"input-batch-summary",
40514052
"input-result",
40524053
"node-attributes",

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,35 @@ Before filing a public issue, redact private project and personal information. D
1515
| Build, test, run, and diagnose an unknown Apple repo | [CLI Integration Guide](#cli-integration-guide) | Use `triton xcode`, `triton xcresult`, and artifact commands before raw `xcodebuild`. |
1616
| Prepare a HarmonyOS / DevEco Emulator | [Harmony App Integration Guide](#harmony-app-integration-guide) | Host-side HDC adapter works without embedded runtime. |
1717
| Validate a Harmony embedded runtime | [Harmony App Integration Guide](#harmony-app-integration-guide) | Use package id / import path `tritonkit` and `--runtime-base-url` direct checks while the SDK is standalone. |
18+
| Add optional Codex / agent workflows | [Optional Agent Skills](#optional-agent-skills) | Install only the public skills; internal skills are for TritonKit repository maintenance. |
19+
20+
## Optional Agent Skills
21+
22+
TritonKit ships optional Codex / agent skills for AI-assisted adoption, feedback, and local emulator regression work. They are not required to use the iOS runtime, Harmony runtime notes, or macOS `triton` CLI.
23+
24+
External users and adopting projects should install only the public skills from `.agents/tritonkit-skills/public/` or from the release asset `tritonkit-skills.tar.gz`. Current public skills are:
25+
26+
- `tritonkit-dev-feedback`: collect adoption feedback, missing capabilities, confusing behavior, and documentation gaps as actionable TritonKit issues.
27+
- `tritonkit-emulator-cli-takeover`: guide local CLI takeover of iOS Simulator, Android Emulator, and HarmonyOS / DevEco Emulator workflows.
28+
- `tritonkit-real-project-regression`: validate TritonKit against real app projects while isolating external repo changes and preserving machine-readable evidence.
29+
30+
Do not install `.agents/tritonkit-skills/internal/` into adopting projects by default. Internal skills are repo-maintenance, governance, planning, supervision, and implementation workflows for TritonKit maintainers, and release packaging excludes them.
31+
32+
If you install from a source checkout, copy only the public skill directories into your Codex / agent skills directory:
33+
34+
```sh
35+
# Replace AGENT_SKILLS_DIR with your Codex / agent's configured skills directory.
36+
mkdir -p "$AGENT_SKILLS_DIR"
37+
cp -R .agents/tritonkit-skills/public/* "$AGENT_SKILLS_DIR"/
38+
```
39+
40+
If you install from a release asset, extract the public skill bundle into that same configured skills directory:
41+
42+
```sh
43+
tar -xzf tritonkit-skills.tar.gz -C "$AGENT_SKILLS_DIR"
44+
```
45+
46+
Restart the Codex / agent session after installation so the new skills are discovered.
1847

1948
## iOS Embedded Runtime Integration Guide
2049

Sources/TritonKitCLI/CLIHostModels.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,17 @@ struct HostSimulatorReadyEvent: Encodable {
487487
let sourceCommand: String?
488488
}
489489

490+
struct HostSimulatorScreenshotMetadata: Encodable, Equatable {
491+
let path: String
492+
let contentType: String
493+
let pixelWidth: Int?
494+
let pixelHeight: Int?
495+
let orientationSemantics: String
496+
let normalizationApplied: Bool
497+
let normalizationStrategy: String
498+
let note: String
499+
}
500+
490501
struct HostActionOutput: Encodable {
491502
let ok: Bool
492503
let action: String
@@ -502,6 +513,7 @@ struct HostActionOutput: Encodable {
502513
let stdout: String?
503514
let stderr: String?
504515
let artifacts: [String]
516+
let screenshot: HostSimulatorScreenshotMetadata?
505517
let note: String?
506518

507519
init(
@@ -519,6 +531,7 @@ struct HostActionOutput: Encodable {
519531
stdout: String?,
520532
stderr: String?,
521533
artifacts: [String],
534+
screenshot: HostSimulatorScreenshotMetadata? = nil,
522535
note: String?
523536
) {
524537
self.ok = ok
@@ -535,6 +548,7 @@ struct HostActionOutput: Encodable {
535548
self.stdout = stdout
536549
self.stderr = stderr
537550
self.artifacts = artifacts
551+
self.screenshot = screenshot
538552
self.note = note
539553
}
540554
}

Sources/TritonKitCLI/CLIHostRuntime.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func runSimpleHostCommand(
3434
stdout: stdout.isEmpty ? nil : stdout,
3535
stderr: stderr.isEmpty ? nil : stderr,
3636
artifacts: artifacts,
37+
screenshot: action == "sim.screenshot" && artifacts.count == 1 ? (try? makeSimulatorScreenshotMetadata(outputPath: artifacts[0])) : nil,
3738
note: note
3839
)
3940
switch outputFormat {
@@ -1627,3 +1628,35 @@ func failHostValidation(code: String, message: String, hint: String, outputForma
16271628
}
16281629
throw ExitCode.failure
16291630
}
1631+
1632+
func makeSimulatorScreenshotMetadata(outputPath: String) throws -> HostSimulatorScreenshotMetadata {
1633+
let dimensions = try? readPNGDimensions(path: outputPath)
1634+
return HostSimulatorScreenshotMetadata(
1635+
path: outputPath,
1636+
contentType: "image/png",
1637+
pixelWidth: dimensions?.width,
1638+
pixelHeight: dimensions?.height,
1639+
orientationSemantics: "raw-simctl-framebuffer",
1640+
normalizationApplied: false,
1641+
normalizationStrategy: "metadata-only",
1642+
note: "TritonKit preserves the raw framebuffer orientation emitted by `xcrun simctl io screenshot`; compare pixelWidth/pixelHeight and simulator display state before treating this artifact as a display-normalized screenshot."
1643+
)
1644+
}
1645+
1646+
private func readPNGDimensions(path: String) throws -> (width: Int, height: Int) {
1647+
let url = URL(fileURLWithPath: path)
1648+
let data = try Data(contentsOf: url, options: [.mappedIfSafe])
1649+
guard data.count >= 24 else {
1650+
throw RuntimeError("Screenshot metadata could not be read: PNG file is too short.")
1651+
}
1652+
let signature: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
1653+
guard Array(data.prefix(8)) == signature else {
1654+
throw RuntimeError("Screenshot metadata could not be read: output is not a PNG file.")
1655+
}
1656+
guard String(data: data[12..<16], encoding: .ascii) == "IHDR" else {
1657+
throw RuntimeError("Screenshot metadata could not be read: PNG IHDR chunk is missing.")
1658+
}
1659+
let width = data[16..<20].reduce(UInt32(0)) { ($0 << 8) | UInt32($1) }
1660+
let height = data[20..<24].reduce(UInt32(0)) { ($0 << 8) | UInt32($1) }
1661+
return (Int(width), Int(height))
1662+
}

Sources/TritonKitCLI/CLISchemaContracts.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,11 +731,31 @@ func hostActionOutputContract(selector: String, model: String) -> TKCommandOutpu
731731
("stdout", "String?", false, "Bounded stdout sample"),
732732
("stderr", "String?", false, "Bounded stderr sample"),
733733
("artifacts", "[String]", true, "Written artifact paths"),
734+
("screenshot", "HostSimulatorScreenshotMetadata?", false, "Simulator screenshot orientation and pixel metadata"),
734735
("note", "String?", false, "Boundary or follow-up note"),
735736
])
736737
)
737738
}
738739

740+
func hostSimulatorScreenshotMetadataOutputContract() -> TKCommandOutputContract {
741+
TKCommandOutputContract(
742+
selector: "host.simulator-screenshot-metadata",
743+
format: "json",
744+
kind: "host-simulator-screenshot-metadata",
745+
model: "HostSimulatorScreenshotMetadata",
746+
fields: schemaContractFields([
747+
("path", "String", true, "Screenshot artifact path"),
748+
("contentType", "String", true, "Screenshot content type"),
749+
("pixelWidth", "Int?", false, "PNG pixel width when readable"),
750+
("pixelHeight", "Int?", false, "PNG pixel height when readable"),
751+
("orientationSemantics", "String", true, "Screenshot orientation coordinate-space semantics"),
752+
("normalizationApplied", "Bool", true, "Whether TritonKit rotated or otherwise normalized the image"),
753+
("normalizationStrategy", "String", true, "Normalization strategy used for this artifact"),
754+
("note", "String", true, "Human-readable caveat for agents and evidence consumers"),
755+
])
756+
)
757+
}
758+
739759
func hostHarmonyTapOutputContract() -> TKCommandOutputContract {
740760
TKCommandOutputContract(
741761
selector: "host.harmony-tap",

Sources/TritonKitCLI/CLISchemaHostCommands.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ func hostCommandSchemas() -> [TKCommandSchema] {
178178
],
179179
successShape: "{ ok, simulators[] } or { ok, runtimes[], count, verbose, sourceCommand } or { ok, action, simulator?, defaultsPath? } or { ok, action:sim.screenshot, artifact, pixelWidth?, pixelHeight?, display, orientationPolicy, orientationNote } or { ok, action, runtimeScope, target, tool, exitCode, sourceCommand, stdout?, stderr?, stdoutTruncated?, stderrTruncated?, artifacts[], note? } or { ok, action, artifact, stdoutBytes, stderrBytes, stdoutTruncated, stderrTruncated } or JSONL { ok, action, state, ready, attempt, elapsedMs }",
180180
failureShape: "{ ok:false, error:{ code, message, hint, nextAction? } }",
181-
outputSemantics: "Use sim for Apple Simulator host control and maintenance. Destructive operations require explicit confirm flags; agents should resolve/use a simulator before app or smoke flows.",
181+
outputSemantics: "Use sim for Apple Simulator host control and maintenance. Destructive operations require explicit confirm flags; agents should resolve/use a simulator before app or smoke flows. sim screenshot preserves simctl raw framebuffer orientation and returns screenshot metadata so agents do not assume display-normalized orientation.",
182182
artifacts: ["simulator-screenshot", "simulator-video", "simulator-logs", "simulator-diagnostics"],
183183
nextCommands: [
184184
"triton sim use <udid> --json",
@@ -191,6 +191,7 @@ func hostCommandSchemas() -> [TKCommandSchema] {
191191
hostSimulatorListOutputContract(),
192192
hostSimulatorScreenshotOutputContract(),
193193
hostActionOutputContract(selector: "host.simulator-action", model: "HostActionOutput|HostArtifactCaptureOutput|HostSimulatorUseOutput|HostSimulatorReadyEvent"),
194+
hostSimulatorScreenshotMetadataOutputContract(),
194195
],
195196
failureCodes: [
196197
"simulator_not_found",

docs-linhay/memory/2026-06-04.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# 2026-06-04
2+
3+
## Issue #27 Optional Agent Skills README
4+
5+
-`docs-linhay/spaces/20260604-issue-27-agent-skills-readme/` 建立执行计划,验收聚焦根 README 是否明确可选 Codex / agent skills 的 public/internal 边界。
6+
- 根 README 新增 `Optional Agent Skills` 章节:外部用户只安装 `.agents/tritonkit-skills/public/` 或 release asset `tritonkit-skills.tar.gz` 中的 public skills;当前 public skills 为 `tritonkit-dev-feedback``tritonkit-emulator-cli-takeover``tritonkit-real-project-regression``.agents/tritonkit-skills/internal/` 仅用于 TritonKit repo maintenance,不默认安装到 adopting projects;安装后需重启 Codex / agent session。
7+
8+
## Issue 26 simulator screenshot orientation metadata
9+
10+
-`triton sim screenshot --json` 增加可选 `screenshot` metadata,语义为 `raw-simctl-framebuffer`,并暴露 PNG `pixelWidth` / `pixelHeight``normalizationApplied=false``normalizationStrategy=metadata-only`
11+
- schema contract 新增 `host.simulator-screenshot-metadata``sim` output semantics 明确截图保持 `xcrun simctl io screenshot` 原始 framebuffer orientation,agent 不应默认视为 display-normalized。
12+
- 当前未做 PNG 旋转归一化;若后续能可靠取得 display orientation,可另建需求把 metadata-only 演进为 normalized artifact。
13+
- 注意:feature worktree 目录名不是 `TritonKit` 时,`swift test --package-path CLI` 会因 SwiftPM local package identity 失败;本轮通过 `/tmp/.../TritonKit` 临时副本验证 CLI tests。
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Issue 26 - triton sim screenshot iPad mini orientation
2+
3+
## 背景
4+
5+
GitHub Issue #26 报告:在 iPad mini (A17 Pro) Simulator 上执行 `triton sim screenshot` 后,生成 PNG 内容相对用户看到的模拟器显示方向旋转 90 度。当前 JSON 输出只透传 simctl command/stderr,未归一化方向,也未暴露 orientation metadata。
6+
7+
## 目标
8+
9+
- 调查 `triton sim screenshot` host-side 实现与 JSON 输出契约。
10+
- 通过测试覆盖 iPad/simctl screenshot 方向相关行为。
11+
- 在不扩大产品边界的前提下,给出最小修复:优先让输出匹配可预期显示方向;若无法可靠归一化,则至少在 JSON 中暴露明确 orientation/display metadata 与文档说明。
12+
13+
## 非目标
14+
15+
- 不新增 Web/Wails UI。
16+
- 不接入真机、远端 agent、设备云或内置 VLM loop。
17+
- 不直接依赖 XcodeBuildMCP 对外 API;Triton CLI/HTTP schema 仍是 agent 入口。
18+
19+
## BDD 场景与验收
20+
21+
### 场景:agent 获取 simulator screenshot 结果时能判断方向语义
22+
23+
Given 一个 iPad Simulator 已启动并可截图
24+
When agent 执行 `triton sim screenshot --simulator <UDID> --output <png> --json`
25+
Then 命令应成功写出 PNG
26+
And JSON 输出应让 agent 明确知道截图方向是否已归一化或包含足够 metadata 判断方向
27+
And 文档应说明该行为,避免把 raw framebuffer orientation 误当最终证据方向
28+
29+
## 相关链接
30+
31+
- GitHub Issue: https://github.com/NeptuneKit/TritonKit/issues/26
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Issue 26 Implementation Plan
2+
3+
## 验收场景
4+
5+
Given 一个 iPad Simulator 已启动并可截图
6+
When agent 执行 `triton sim screenshot --simulator <UDID> --output <png> --json`
7+
Then 命令应成功写出 PNG
8+
And JSON 输出应包含 `screenshot` metadata,明确当前 artifact 是 `raw-simctl-framebuffer` 语义
9+
And metadata 应暴露 PNG `pixelWidth` / `pixelHeight``normalizationApplied``normalizationStrategy`
10+
And schema contract 应声明 `host.simulator-screenshot-metadata`,避免 agent 将 raw framebuffer 当成 display-normalized screenshot
11+
12+
## 实现记录
13+
14+
- `HostActionOutput` 增加可选 `screenshot` metadata。
15+
- `triton sim screenshot` 写出单个 artifact 时读取 PNG IHDR 宽高,并返回 metadata。
16+
- 当前最小修复采用 `metadata-only` 策略,不对 PNG 做旋转归一化,避免在缺少可靠 display orientation 来源时产生二次误导。
17+
- schema 的 `sim` 命令输出语义与 output contract 同步说明 raw framebuffer orientation。
18+
19+
## 验证
20+
21+
在 issue worktree 目录名不是 `TritonKit` 时,SwiftPM local package identity 会把根包识别成 worktree slug,导致 `CLI/Package.swift``package: "tritonkit"` 依赖解析失败。因此测试通过 `/tmp/tritonkit-issue26-copy/TritonKit` 临时副本执行。
22+
23+
已运行:
24+
25+
```sh
26+
swift test --package-path /tmp/tritonkit-issue26-copy/TritonKit/CLI --filter DeviceCrossPlatformTests.simulatorScreenshotMetadataDocumentsRawFramebufferOrientationSemantics
27+
swift test --package-path /tmp/tritonkit-issue26-copy/TritonKit/CLI --filter SchemaFactSourceTests.hostWorkflowSchemasExposeTargetAndArtifactContracts
28+
swift test --package-path /tmp/tritonkit-issue26-copy/TritonKit/CLI --filter SchemaFactSourceTests.schemaOutputContractsExposeNonemptyFields
29+
swift test --package-path /tmp/tritonkit-issue26-copy/TritonKit/CLI --filter SchemaFactSourceTests.schemaOutputContractKindsStayWithinAgentTaxonomy
30+
swift test --package-path /tmp/tritonkit-issue26-copy/TritonKit/CLI --filter SchemaFactSourceTests.schemaOutputContractModelsStayMachineReadable
31+
swift test --package-path /tmp/tritonkit-issue26-copy/TritonKit/CLI --filter SchemaFactSourceTests.schemaOutputContractSelectorsAndKindsUseStableAgentKeys
32+
```
33+
34+
结果均通过。
35+
36+
## 剩余风险
37+
38+
- 本次没有实现图像旋转归一化;输出仍保持 `xcrun simctl io screenshot` 原始 framebuffer。该选择是为了先让 agent/evidence consumer 具备机器可读方向语义,后续若能可靠取得当前 display orientation,可另起需求实现归一化。
39+
- 未在真实 iPad mini Simulator 上复测,因为当前任务收口以 CLI schema/model 单元测试为主。

0 commit comments

Comments
 (0)