taphound 0.2.0-dev.1

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 (176) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +200 -0
  3. package/README.zh-CN.md +200 -0
  4. package/assets/brand/taphound-mark.svg +9 -0
  5. package/assets/skills/taphound-ai-journey/GUIDE.md +862 -0
  6. package/assets/skills/taphound-ai-journey/SKILL.md +377 -0
  7. package/assets/skills/taphound-ai-journey/prompts/analyze-project.md +526 -0
  8. package/assets/skills/taphound-ai-journey/prompts/check-completion.md +52 -0
  9. package/assets/skills/taphound-ai-journey/prompts/generate-step.md +109 -0
  10. package/assets/skills/taphound-ai-journey/schemas/observe-output.json +42 -0
  11. package/assets/skills/taphound-ai-journey/schemas/project-context.json +81 -0
  12. package/assets/skills/taphound-ai-journey/schemas/proposed-step-envelope.json +267 -0
  13. package/assets/skills/taphound-ai-journey/templates/project-context.example.json +49 -0
  14. package/dist/adapters/adb/adb-adapter.d.ts +23 -0
  15. package/dist/adapters/adb/adb-adapter.js +218 -0
  16. package/dist/adapters/android-cli/android-cli-adapter.d.ts +11 -0
  17. package/dist/adapters/android-cli/android-cli-adapter.js +64 -0
  18. package/dist/adapters/android-cli/layout-parser.d.ts +3 -0
  19. package/dist/adapters/android-cli/layout-parser.js +174 -0
  20. package/dist/adapters/clock/system-clock.d.ts +5 -0
  21. package/dist/adapters/clock/system-clock.js +13 -0
  22. package/dist/adapters/filesystem/artifact-store.d.ts +4 -0
  23. package/dist/adapters/filesystem/artifact-store.js +73 -0
  24. package/dist/adapters/filesystem/generation-meta-writer.d.ts +16 -0
  25. package/dist/adapters/filesystem/generation-meta-writer.js +34 -0
  26. package/dist/adapters/filesystem/generation-session-store.d.ts +68 -0
  27. package/dist/adapters/filesystem/generation-session-store.js +1603 -0
  28. package/dist/adapters/filesystem/journey-writer.d.ts +16 -0
  29. package/dist/adapters/filesystem/journey-writer.js +34 -0
  30. package/dist/adapters/filesystem/project-bound-file.d.ts +6 -0
  31. package/dist/adapters/filesystem/project-bound-file.js +240 -0
  32. package/dist/adapters/filesystem/project-file-inspector.d.ts +8 -0
  33. package/dist/adapters/filesystem/project-file-inspector.js +198 -0
  34. package/dist/adapters/filesystem/skill-installer.d.ts +10 -0
  35. package/dist/adapters/filesystem/skill-installer.js +39 -0
  36. package/dist/adapters/process/node-process-runner.d.ts +8 -0
  37. package/dist/adapters/process/node-process-runner.js +155 -0
  38. package/dist/adapters/prompt/inquirer-generation-prompt.d.ts +55 -0
  39. package/dist/adapters/prompt/inquirer-generation-prompt.js +213 -0
  40. package/dist/adapters/prompt/inquirer-init-prompt.d.ts +32 -0
  41. package/dist/adapters/prompt/inquirer-init-prompt.js +55 -0
  42. package/dist/adapters/prompt/inquirer-recorder-prompt.d.ts +47 -0
  43. package/dist/adapters/prompt/inquirer-recorder-prompt.js +157 -0
  44. package/dist/application/assertion/expectation-evaluator.d.ts +61 -0
  45. package/dist/application/assertion/expectation-evaluator.js +207 -0
  46. package/dist/application/collector/logcat-collector.d.ts +39 -0
  47. package/dist/application/collector/logcat-collector.js +107 -0
  48. package/dist/application/context/context-validator.d.ts +27 -0
  49. package/dist/application/context/context-validator.js +120 -0
  50. package/dist/application/doctor/doctor-service.d.ts +35 -0
  51. package/dist/application/doctor/doctor-service.js +178 -0
  52. package/dist/application/generation/focused-input.d.ts +2 -0
  53. package/dist/application/generation/focused-input.js +9 -0
  54. package/dist/application/generation/generation-confirmation-service.d.ts +76 -0
  55. package/dist/application/generation/generation-confirmation-service.js +383 -0
  56. package/dist/application/generation/generation-finalizer.d.ts +67 -0
  57. package/dist/application/generation/generation-finalizer.js +646 -0
  58. package/dist/application/generation/generation-publisher.d.ts +46 -0
  59. package/dist/application/generation/generation-publisher.js +178 -0
  60. package/dist/application/generation/generation-starter.d.ts +31 -0
  61. package/dist/application/generation/generation-starter.js +102 -0
  62. package/dist/application/generation/generation-step-executor.d.ts +52 -0
  63. package/dist/application/generation/generation-step-executor.js +668 -0
  64. package/dist/application/generation/proposed-step-validator.d.ts +11 -0
  65. package/dist/application/generation/proposed-step-validator.js +88 -0
  66. package/dist/application/generation/risk-evaluator.d.ts +9 -0
  67. package/dist/application/generation/risk-evaluator.js +14 -0
  68. package/dist/application/generation/runtime-observer.d.ts +39 -0
  69. package/dist/application/generation/runtime-observer.js +157 -0
  70. package/dist/application/init/init-service.d.ts +17 -0
  71. package/dist/application/init/init-service.js +44 -0
  72. package/dist/application/interaction/action-executor.d.ts +25 -0
  73. package/dist/application/interaction/action-executor.js +103 -0
  74. package/dist/application/interaction/fallback-resolver.d.ts +24 -0
  75. package/dist/application/interaction/fallback-resolver.js +48 -0
  76. package/dist/application/interaction/scroll-to-executor.d.ts +40 -0
  77. package/dist/application/interaction/scroll-to-executor.js +191 -0
  78. package/dist/application/locator/locator-resolver.d.ts +20 -0
  79. package/dist/application/locator/locator-resolver.js +89 -0
  80. package/dist/application/project/project-describer.d.ts +14 -0
  81. package/dist/application/project/project-describer.js +8 -0
  82. package/dist/application/recorder/locator-selector.d.ts +10 -0
  83. package/dist/application/recorder/locator-selector.js +80 -0
  84. package/dist/application/recorder/recorder-service.d.ts +41 -0
  85. package/dist/application/recorder/recorder-service.js +331 -0
  86. package/dist/application/report/report-writer.d.ts +10 -0
  87. package/dist/application/report/report-writer.js +39 -0
  88. package/dist/application/runtime/activity-waiter.d.ts +33 -0
  89. package/dist/application/runtime/activity-waiter.js +73 -0
  90. package/dist/application/runtime/launch-failure.d.ts +2 -0
  91. package/dist/application/runtime/launch-failure.js +20 -0
  92. package/dist/application/runtime/process-waiter.d.ts +27 -0
  93. package/dist/application/runtime/process-waiter.js +73 -0
  94. package/dist/application/runtime/step-runner.d.ts +50 -0
  95. package/dist/application/runtime/step-runner.js +429 -0
  96. package/dist/application/runtime/verify-runtime.d.ts +44 -0
  97. package/dist/application/runtime/verify-runtime.js +350 -0
  98. package/dist/application/wait/idle-waiter.d.ts +30 -0
  99. package/dist/application/wait/idle-waiter.js +90 -0
  100. package/dist/cli/commands/context.d.ts +3 -0
  101. package/dist/cli/commands/context.js +94 -0
  102. package/dist/cli/commands/doctor.d.ts +3 -0
  103. package/dist/cli/commands/doctor.js +51 -0
  104. package/dist/cli/commands/generation.d.ts +3 -0
  105. package/dist/cli/commands/generation.js +518 -0
  106. package/dist/cli/commands/init.d.ts +3 -0
  107. package/dist/cli/commands/init.js +104 -0
  108. package/dist/cli/commands/project.d.ts +3 -0
  109. package/dist/cli/commands/project.js +58 -0
  110. package/dist/cli/commands/record.d.ts +3 -0
  111. package/dist/cli/commands/record.js +88 -0
  112. package/dist/cli/commands/verify.d.ts +3 -0
  113. package/dist/cli/commands/verify.js +106 -0
  114. package/dist/cli/dependencies.d.ts +66 -0
  115. package/dist/cli/dependencies.js +204 -0
  116. package/dist/cli/main.d.ts +8 -0
  117. package/dist/cli/main.js +73 -0
  118. package/dist/cli/output.d.ts +16 -0
  119. package/dist/cli/output.js +22 -0
  120. package/dist/cli/program.d.ts +3 -0
  121. package/dist/cli/program.js +43 -0
  122. package/dist/domain/activity.d.ts +7 -0
  123. package/dist/domain/activity.js +65 -0
  124. package/dist/domain/app-process.d.ts +7 -0
  125. package/dist/domain/app-process.js +10 -0
  126. package/dist/domain/config.d.ts +15 -0
  127. package/dist/domain/config.js +17 -0
  128. package/dist/domain/failure.d.ts +4 -0
  129. package/dist/domain/failure.js +43 -0
  130. package/dist/domain/generation.d.ts +912 -0
  131. package/dist/domain/generation.js +342 -0
  132. package/dist/domain/init.d.ts +42 -0
  133. package/dist/domain/init.js +114 -0
  134. package/dist/domain/journey.d.ts +673 -0
  135. package/dist/domain/journey.js +109 -0
  136. package/dist/domain/layout.d.ts +35 -0
  137. package/dist/domain/layout.js +33 -0
  138. package/dist/domain/locator.d.ts +8 -0
  139. package/dist/domain/locator.js +5 -0
  140. package/dist/domain/project-context.d.ts +101 -0
  141. package/dist/domain/project-context.js +64 -0
  142. package/dist/domain/proposed-step.d.ts +341 -0
  143. package/dist/domain/proposed-step.js +87 -0
  144. package/dist/domain/report.d.ts +349 -0
  145. package/dist/domain/report.js +135 -0
  146. package/dist/domain/runtime-snapshot.d.ts +16 -0
  147. package/dist/domain/runtime-snapshot.js +46 -0
  148. package/dist/ports/adb.d.ts +42 -0
  149. package/dist/ports/adb.js +1 -0
  150. package/dist/ports/android-cli.d.ts +21 -0
  151. package/dist/ports/android-cli.js +1 -0
  152. package/dist/ports/artifact-store.d.ts +12 -0
  153. package/dist/ports/artifact-store.js +1 -0
  154. package/dist/ports/clock.d.ts +4 -0
  155. package/dist/ports/clock.js +1 -0
  156. package/dist/ports/generation-meta-writer.d.ts +10 -0
  157. package/dist/ports/generation-meta-writer.js +1 -0
  158. package/dist/ports/generation-prompt.d.ts +19 -0
  159. package/dist/ports/generation-prompt.js +6 -0
  160. package/dist/ports/generation-session-store.d.ts +32 -0
  161. package/dist/ports/generation-session-store.js +26 -0
  162. package/dist/ports/init-prompt.d.ts +8 -0
  163. package/dist/ports/init-prompt.js +6 -0
  164. package/dist/ports/journey-writer.d.ts +10 -0
  165. package/dist/ports/journey-writer.js +1 -0
  166. package/dist/ports/process-runner.d.ts +32 -0
  167. package/dist/ports/process-runner.js +1 -0
  168. package/dist/ports/project-bound-file.d.ts +8 -0
  169. package/dist/ports/project-bound-file.js +1 -0
  170. package/dist/ports/project-file-inspector.d.ts +33 -0
  171. package/dist/ports/project-file-inspector.js +1 -0
  172. package/dist/ports/recorder-prompt.d.ts +29 -0
  173. package/dist/ports/recorder-prompt.js +1 -0
  174. package/dist/ports/skill-installer.d.ts +8 -0
  175. package/dist/ports/skill-installer.js +1 -0
  176. package/package.json +64 -0
@@ -0,0 +1,350 @@
1
+ import { resolve } from "node:path";
2
+ import { normalizeActivity } from "../../domain/activity.js";
3
+ import { exitCodeForFailure } from "../../domain/failure.js";
4
+ import { hashJourney } from "../../domain/report.js";
5
+ import { LogcatCollector } from "../collector/logcat-collector.js";
6
+ import { ActivityWaiter } from "./activity-waiter.js";
7
+ import { launchFailure } from "./launch-failure.js";
8
+ import { ProcessWaiter } from "./process-waiter.js";
9
+ import { StepRunner } from "./step-runner.js";
10
+ function commandFailed(result) {
11
+ return result.exitCode !== 0
12
+ || result.timedOut
13
+ || result.cancelled
14
+ || result.spawnError !== undefined;
15
+ }
16
+ function commandMessage(result, fallback) {
17
+ return result.stderr.trim() || result.spawnError || fallback;
18
+ }
19
+ function logcatStopFailed(result) {
20
+ if (result.timedOut
21
+ || result.cancelled
22
+ || result.spawnError !== undefined) {
23
+ return true;
24
+ }
25
+ return result.exitCode !== 0
26
+ && result.signal !== "SIGTERM"
27
+ && result.signal !== "SIGKILL";
28
+ }
29
+ function errorMessage(error) {
30
+ return error instanceof Error ? error.message : String(error);
31
+ }
32
+ function layerForFailure(code) {
33
+ if (code === "APP_NOT_INSTALLED"
34
+ || code === "APP_LAUNCH_FAILED"
35
+ || code === "APP_CRASHED") {
36
+ return "run";
37
+ }
38
+ if (code === "ACTIVITY_BEFORE_MISMATCH"
39
+ || code === "ACTIVITY_AFTER_MISMATCH") {
40
+ return "activityCheckpoint";
41
+ }
42
+ if (code.startsWith("EXPECT_")) {
43
+ return "explicitExpect";
44
+ }
45
+ if (code === "COLLECTION_FAILED") {
46
+ return "collection";
47
+ }
48
+ return "structural";
49
+ }
50
+ export class VerifyRuntime {
51
+ dependencies;
52
+ constructor(dependencies) {
53
+ this.dependencies = dependencies;
54
+ }
55
+ async verify(input) {
56
+ const startedAt = this.dependencies.now();
57
+ const runId = this.dependencies.createRunId();
58
+ const launchActivity = normalizeActivity(input.config.run.packageName, input.config.run.activity);
59
+ const session = await this.dependencies.artifactStore.begin(resolve(input.projectRoot, input.config.artifactsDir), runId);
60
+ const logcat = new LogcatCollector(this.dependencies.adb, this.dependencies.clock);
61
+ let logcatStarted = false;
62
+ let primaryFailure;
63
+ const secondaryErrors = [];
64
+ const collectionErrors = [];
65
+ const steps = [];
66
+ const layers = {
67
+ run: "notRun",
68
+ structural: "notRun",
69
+ activityCheckpoint: "notRun",
70
+ explicitExpect: "notRun",
71
+ collection: "passed"
72
+ };
73
+ const setPrimary = (code, message, phase, stepIndex) => {
74
+ if (primaryFailure !== undefined) {
75
+ return;
76
+ }
77
+ primaryFailure = {
78
+ code,
79
+ message,
80
+ phase,
81
+ ...(stepIndex === undefined ? {} : { stepIndex })
82
+ };
83
+ layers[layerForFailure(code)] = "failed";
84
+ };
85
+ const collectionFailure = (message) => {
86
+ layers.collection = "failed";
87
+ const failure = {
88
+ code: "COLLECTION_FAILED",
89
+ message,
90
+ phase: "collection"
91
+ };
92
+ collectionErrors.push(failure);
93
+ };
94
+ try {
95
+ try {
96
+ const installed = await this.dependencies.adb.isInstalled({
97
+ packageName: input.config.run.packageName,
98
+ deviceSerial: input.deviceSerial,
99
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
100
+ timeoutMs: input.config.idle.timeoutMs
101
+ });
102
+ if (!installed) {
103
+ setPrimary("APP_NOT_INSTALLED", `Package ${input.config.run.packageName} is not installed on ${input.deviceSerial}`, "install");
104
+ }
105
+ }
106
+ catch (error) {
107
+ setPrimary("APP_NOT_INSTALLED", errorMessage(error), "install");
108
+ }
109
+ if (primaryFailure === undefined) {
110
+ try {
111
+ await logcat.start({
112
+ deviceSerial: input.deviceSerial,
113
+ ...(input.signal === undefined ? {} : { signal: input.signal })
114
+ });
115
+ logcatStarted = true;
116
+ }
117
+ catch (error) {
118
+ layers.collection = "failed";
119
+ setPrimary("COLLECTION_FAILED", errorMessage(error), "collection");
120
+ }
121
+ if (logcatStarted) {
122
+ const identity = {
123
+ packageName: input.config.run.packageName,
124
+ deviceSerial: input.deviceSerial,
125
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
126
+ timeoutMs: input.config.idle.timeoutMs
127
+ };
128
+ const stopped = await this.dependencies.adb.forceStop(identity);
129
+ const launched = commandFailed(stopped)
130
+ ? undefined
131
+ : await this.dependencies.adb.launchActivity({
132
+ packageName: input.config.run.packageName,
133
+ activity: launchActivity,
134
+ deviceSerial: input.deviceSerial,
135
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
136
+ timeoutMs: input.config.idle.timeoutMs
137
+ });
138
+ const launchError = launched === undefined
139
+ ? commandMessage(stopped, "App reset failed")
140
+ : launchFailure(launched);
141
+ if (launchError !== undefined) {
142
+ layers.run = "failed";
143
+ setPrimary("APP_LAUNCH_FAILED", launchError, "run");
144
+ }
145
+ else {
146
+ try {
147
+ const launchReadinessStartedAt = this.dependencies.clock.now();
148
+ const processReadiness = await new ProcessWaiter(this.dependencies.adb, this.dependencies.clock).wait({
149
+ packageName: input.config.run.packageName,
150
+ deviceSerial: input.deviceSerial,
151
+ pollIntervalMs: input.config.idle.pollIntervalMs,
152
+ timeoutMs: input.config.idle.timeoutMs,
153
+ ...(input.signal === undefined ? {} : { signal: input.signal })
154
+ });
155
+ if (processReadiness.status === "timeout") {
156
+ setPrimary("APP_LAUNCH_FAILED", "App process was not found after launch", "readiness");
157
+ }
158
+ else if (processReadiness.status === "cancelled") {
159
+ setPrimary("INTERNAL_ERROR", "Verification was cancelled", "readiness");
160
+ }
161
+ else {
162
+ logcat.scopeToPids(processReadiness.pids);
163
+ const firstStep = input.journey.steps[0];
164
+ if (firstStep === undefined) {
165
+ throw new Error("Journey requires at least one step");
166
+ }
167
+ const remainingReadinessMs = input.config.idle.timeoutMs
168
+ - (this.dependencies.clock.now()
169
+ - launchReadinessStartedAt);
170
+ if (remainingReadinessMs <= 0) {
171
+ setPrimary("APP_LAUNCH_FAILED", `Expected startup Activity ${firstStep.activity.before}, found none before timeout`, "readiness");
172
+ }
173
+ else {
174
+ const readiness = await new ActivityWaiter(this.dependencies.adb, this.dependencies.clock).wait({
175
+ packageName: input.config.run.packageName,
176
+ deviceSerial: input.deviceSerial,
177
+ expectedActivity: firstStep.activity.before,
178
+ pollIntervalMs: input.config.idle.pollIntervalMs,
179
+ timeoutMs: remainingReadinessMs,
180
+ ...(input.signal === undefined ? {} : { signal: input.signal })
181
+ });
182
+ if (readiness.status === "processMissing") {
183
+ setPrimary("APP_LAUNCH_FAILED", "App process exited before reaching the first Journey Activity", "readiness");
184
+ }
185
+ else if (readiness.status === "timeout") {
186
+ setPrimary("APP_LAUNCH_FAILED", `Expected startup Activity ${firstStep.activity.before}, found ${readiness.actual ?? "none"} before timeout`, "readiness");
187
+ }
188
+ else if (readiness.status === "cancelled") {
189
+ setPrimary("INTERNAL_ERROR", "Verification was cancelled", "readiness");
190
+ }
191
+ else {
192
+ await this.dependencies.androidCli.layout({
193
+ deviceSerial: input.deviceSerial,
194
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
195
+ timeoutMs: input.config.idle.timeoutMs
196
+ });
197
+ layers.run = "passed";
198
+ layers.structural = "passed";
199
+ layers.activityCheckpoint = "passed";
200
+ layers.explicitExpect = "passed";
201
+ }
202
+ }
203
+ }
204
+ }
205
+ catch (error) {
206
+ setPrimary("APP_LAUNCH_FAILED", errorMessage(error), "readiness");
207
+ }
208
+ }
209
+ }
210
+ }
211
+ if (primaryFailure === undefined) {
212
+ const createStepRunner = this.dependencies.createStepRunner
213
+ ?? ((options) => new StepRunner(options));
214
+ const runner = createStepRunner({
215
+ adb: this.dependencies.adb,
216
+ androidCli: this.dependencies.androidCli,
217
+ clock: this.dependencies.clock,
218
+ logcat,
219
+ artifacts: session,
220
+ packageName: input.config.run.packageName,
221
+ deviceSerial: input.deviceSerial,
222
+ idle: input.config.idle,
223
+ ...(input.requireFocusedInput === undefined
224
+ ? {}
225
+ : { requireFocusedInput: input.requireFocusedInput }),
226
+ ...(input.generatedReplayPolicy === undefined
227
+ ? {}
228
+ : { generatedReplayPolicy: input.generatedReplayPolicy })
229
+ });
230
+ for (const [index, step] of input.journey.steps.entries()) {
231
+ const result = await runner.run(step, index, input.signal);
232
+ steps.push(result.report);
233
+ if (result.status === "cancelled") {
234
+ setPrimary("INTERNAL_ERROR", "Verification was cancelled", "replay", index);
235
+ break;
236
+ }
237
+ if (result.status === "failed") {
238
+ setPrimary(result.failure.code, result.failure.message, result.failure.phase, result.failure.stepIndex);
239
+ break;
240
+ }
241
+ }
242
+ }
243
+ }
244
+ catch (error) {
245
+ if (primaryFailure === undefined) {
246
+ setPrimary("INTERNAL_ERROR", errorMessage(error), "runtime");
247
+ }
248
+ else {
249
+ secondaryErrors.push({
250
+ code: "INTERNAL_ERROR",
251
+ message: errorMessage(error),
252
+ phase: "runtime"
253
+ });
254
+ }
255
+ }
256
+ const screenshotPath = "screenshot.png";
257
+ let screenshotCollected = false;
258
+ try {
259
+ const screenshot = await this.dependencies.androidCli.captureScreen({
260
+ outputPath: session.path(screenshotPath),
261
+ deviceSerial: input.deviceSerial,
262
+ ...(input.signal === undefined ? {} : { signal: input.signal })
263
+ });
264
+ if (commandFailed(screenshot)) {
265
+ collectionFailure(commandMessage(screenshot, "Screen capture failed"));
266
+ }
267
+ else {
268
+ screenshotCollected = true;
269
+ }
270
+ }
271
+ catch (error) {
272
+ collectionFailure(errorMessage(error));
273
+ }
274
+ let logcatCollected = false;
275
+ if (logcatStarted) {
276
+ try {
277
+ const stopped = await logcat.stop();
278
+ if (logcatStopFailed(stopped)) {
279
+ collectionFailure(commandMessage(stopped, "Logcat stop failed"));
280
+ }
281
+ await session.writeText("logcat.txt", logcat.lines().map((line) => line.raw).join("\n"));
282
+ logcatCollected = true;
283
+ }
284
+ catch (error) {
285
+ collectionFailure(errorMessage(error));
286
+ }
287
+ }
288
+ if (collectionErrors.length > 0) {
289
+ if (primaryFailure === undefined) {
290
+ primaryFailure = collectionErrors.shift();
291
+ }
292
+ secondaryErrors.push(...collectionErrors);
293
+ }
294
+ const finishedAt = this.dependencies.now();
295
+ const failure = primaryFailure;
296
+ const status = failure === undefined
297
+ ? "passed"
298
+ : [
299
+ "CONFIG_INVALID",
300
+ "ENVIRONMENT_MISSING_TOOL",
301
+ "DEVICE_UNAVAILABLE",
302
+ "APP_NOT_INSTALLED",
303
+ "INTERNAL_ERROR"
304
+ ].includes(failure.code)
305
+ ? "error"
306
+ : "failed";
307
+ const report = {
308
+ schemaVersion: 2,
309
+ runId,
310
+ status,
311
+ startedAt: startedAt.toISOString(),
312
+ finishedAt: finishedAt.toISOString(),
313
+ durationMs: Math.max(0, finishedAt.getTime() - startedAt.getTime()),
314
+ project: {
315
+ root: input.projectRoot,
316
+ packageName: input.config.run.packageName,
317
+ launchActivity
318
+ },
319
+ journey: {
320
+ name: input.journey.name,
321
+ sha256: hashJourney(input.journey)
322
+ },
323
+ environment: {
324
+ deviceSerial: input.deviceSerial,
325
+ tools: input.toolVersions
326
+ },
327
+ layers,
328
+ steps,
329
+ artifacts: {
330
+ directory: session.finalDirectory,
331
+ report: "report.json",
332
+ summary: "summary.txt",
333
+ ...(screenshotCollected ? { screenshot: screenshotPath } : {}),
334
+ ...(logcatCollected ? { logcat: "logcat.txt" } : {}),
335
+ stepLogs: steps.flatMap((step) => (step.logcatPath === undefined ? [] : [step.logcatPath]))
336
+ },
337
+ ...(failure === undefined ? {} : { primaryFailure: failure }),
338
+ secondaryErrors,
339
+ fallbackUsed: steps.some((step) => step.locator?.fallbackUsed === true)
340
+ };
341
+ const published = await this.dependencies.reportWriter.writeAndPublish(session, report);
342
+ return {
343
+ status,
344
+ exitCode: failure === undefined ? 0 : exitCodeForFailure(failure.code),
345
+ report,
346
+ reportPath: published.reportPath,
347
+ summaryPath: published.summaryPath
348
+ };
349
+ }
350
+ }
@@ -0,0 +1,30 @@
1
+ import type { FailureCode } from "../../domain/failure.js";
2
+ import type { AndroidCliPort } from "../../ports/android-cli.js";
3
+ import type { Clock } from "../../ports/clock.js";
4
+ export interface IdleConfig {
5
+ pollIntervalMs: number;
6
+ stablePolls: number;
7
+ timeoutMs: number;
8
+ }
9
+ export type IdleResult = {
10
+ status: "stable";
11
+ polls: number;
12
+ durationMs: number;
13
+ } | {
14
+ status: "timeout";
15
+ code: Extract<FailureCode, "IDLE_TIMEOUT">;
16
+ polls: number;
17
+ durationMs: number;
18
+ lastDiff: readonly unknown[];
19
+ } | {
20
+ status: "cancelled";
21
+ polls: number;
22
+ durationMs: number;
23
+ };
24
+ export declare class IdleWaiter {
25
+ private readonly androidCli;
26
+ private readonly clock;
27
+ private readonly deviceSerial;
28
+ constructor(androidCli: AndroidCliPort, clock: Clock, deviceSerial: string);
29
+ waitUntilIdle(config: IdleConfig, signal?: AbortSignal): Promise<IdleResult>;
30
+ }
@@ -0,0 +1,90 @@
1
+ function isAborted(signal) {
2
+ return signal?.aborted === true;
3
+ }
4
+ export class IdleWaiter {
5
+ androidCli;
6
+ clock;
7
+ deviceSerial;
8
+ constructor(androidCli, clock, deviceSerial) {
9
+ this.androidCli = androidCli;
10
+ this.clock = clock;
11
+ this.deviceSerial = deviceSerial;
12
+ }
13
+ async waitUntilIdle(config, signal) {
14
+ const startedAt = this.clock.now();
15
+ let polls = 0;
16
+ let consecutiveEmpty = 0;
17
+ let lastDiff = [];
18
+ for (;;) {
19
+ if (isAborted(signal)) {
20
+ return {
21
+ status: "cancelled",
22
+ polls,
23
+ durationMs: this.clock.now() - startedAt
24
+ };
25
+ }
26
+ const elapsedBeforePoll = this.clock.now() - startedAt;
27
+ polls += 1;
28
+ let diff;
29
+ try {
30
+ diff = await this.androidCli.layoutDiff({
31
+ deviceSerial: this.deviceSerial,
32
+ ...(signal === undefined ? {} : { signal }),
33
+ timeoutMs: Math.max(1, config.timeoutMs - elapsedBeforePoll)
34
+ });
35
+ }
36
+ catch (error) {
37
+ const elapsed = this.clock.now() - startedAt;
38
+ if (isAborted(signal)) {
39
+ return { status: "cancelled", polls, durationMs: elapsed };
40
+ }
41
+ if (elapsed >= config.timeoutMs) {
42
+ return {
43
+ status: "timeout",
44
+ code: "IDLE_TIMEOUT",
45
+ polls,
46
+ durationMs: elapsed,
47
+ lastDiff
48
+ };
49
+ }
50
+ throw error;
51
+ }
52
+ if (diff.length === 0) {
53
+ consecutiveEmpty += 1;
54
+ }
55
+ else {
56
+ consecutiveEmpty = 0;
57
+ lastDiff = diff;
58
+ }
59
+ if (consecutiveEmpty >= config.stablePolls) {
60
+ return {
61
+ status: "stable",
62
+ polls,
63
+ durationMs: this.clock.now() - startedAt
64
+ };
65
+ }
66
+ if (this.clock.now() - startedAt >= config.timeoutMs) {
67
+ return {
68
+ status: "timeout",
69
+ code: "IDLE_TIMEOUT",
70
+ polls,
71
+ durationMs: this.clock.now() - startedAt,
72
+ lastDiff
73
+ };
74
+ }
75
+ try {
76
+ await this.clock.sleep(config.pollIntervalMs, signal);
77
+ }
78
+ catch (error) {
79
+ if (isAborted(signal)) {
80
+ return {
81
+ status: "cancelled",
82
+ polls,
83
+ durationMs: this.clock.now() - startedAt
84
+ };
85
+ }
86
+ throw error;
87
+ }
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import type { CliDependencies } from "../dependencies.js";
3
+ export declare function createContextCommand(dependencies: CliDependencies): Command;
@@ -0,0 +1,94 @@
1
+ import { resolve } from "node:path";
2
+ import { Command } from "commander";
3
+ import { TapHoundConfigSchema } from "../../domain/config.js";
4
+ import { ProjectContextSchema } from "../../domain/project-context.js";
5
+ import { errorMessage, failureOutput, writeJson, writeLine } from "../output.js";
6
+ function exitCodeForContext(command, result) {
7
+ if (result.status === "invalid") {
8
+ return 2;
9
+ }
10
+ return command === "validate" && result.status === "stale" ? 1 : 0;
11
+ }
12
+ function createContextOperation(dependencies, name) {
13
+ return new Command(name)
14
+ .description(name === "validate"
15
+ ? "Validate deterministic Project Context evidence"
16
+ : "Observe deterministic Project Context status")
17
+ .option("--project <path>", "Android project root", dependencies.cwd())
18
+ .option("--config <path>", "TapHound config path", "taphound.config.json")
19
+ .requiredOption("--context <path>", "Project Context path")
20
+ .option("--json", "Emit one machine-readable JSON value")
21
+ .action(async (options) => {
22
+ let config;
23
+ let context;
24
+ try {
25
+ config = TapHoundConfigSchema.parse(await dependencies.readJson(resolve(options.project, options.config)));
26
+ }
27
+ catch (error) {
28
+ const output = failureOutput(2, "CONFIG_INVALID", errorMessage(error));
29
+ if (options.json === true) {
30
+ writeJson(dependencies.stdout, output);
31
+ }
32
+ else {
33
+ writeLine(dependencies.stderr, output.failure.message);
34
+ }
35
+ dependencies.setExitCode(2);
36
+ return;
37
+ }
38
+ try {
39
+ context = ProjectContextSchema.parse(await dependencies.readJson(resolve(options.project, options.context)));
40
+ }
41
+ catch (error) {
42
+ const output = {
43
+ status: "error",
44
+ exitCode: 2,
45
+ failure: {
46
+ code: "CONTEXT_INVALID",
47
+ message: errorMessage(error)
48
+ }
49
+ };
50
+ if (options.json === true) {
51
+ writeJson(dependencies.stdout, output);
52
+ }
53
+ else {
54
+ writeLine(dependencies.stderr, output.failure.message);
55
+ }
56
+ dependencies.setExitCode(2);
57
+ return;
58
+ }
59
+ try {
60
+ const result = await dependencies.contextValidator.validate({
61
+ context,
62
+ projectRoot: options.project,
63
+ config
64
+ });
65
+ const exitCode = exitCodeForContext(name, result);
66
+ const output = { ...result, exitCode };
67
+ if (options.json === true) {
68
+ writeJson(dependencies.stdout, output);
69
+ }
70
+ else {
71
+ writeLine(dependencies.stdout, result.status === "valid"
72
+ ? "Context: valid"
73
+ : `Context: ${result.status} (${result.reason.message})`);
74
+ }
75
+ dependencies.setExitCode(exitCode);
76
+ }
77
+ catch (error) {
78
+ const output = failureOutput(4, "INTERNAL_ERROR", errorMessage(error));
79
+ if (options.json === true) {
80
+ writeJson(dependencies.stdout, output);
81
+ }
82
+ else {
83
+ writeLine(dependencies.stderr, output.failure.message);
84
+ }
85
+ dependencies.setExitCode(4);
86
+ }
87
+ });
88
+ }
89
+ export function createContextCommand(dependencies) {
90
+ return new Command("context")
91
+ .description("Validate or inspect Project Context")
92
+ .addCommand(createContextOperation(dependencies, "validate"))
93
+ .addCommand(createContextOperation(dependencies, "status"));
94
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import type { CliDependencies } from "../dependencies.js";
3
+ export declare function createDoctorCommand(dependencies: CliDependencies): Command;
@@ -0,0 +1,51 @@
1
+ import { resolve } from "node:path";
2
+ import { Command } from "commander";
3
+ import { TapHoundConfigSchema } from "../../domain/config.js";
4
+ import { doctorMessage, errorMessage, failureOutput, writeJson, writeLine } from "../output.js";
5
+ async function configuredPackageName(dependencies, options) {
6
+ try {
7
+ return TapHoundConfigSchema.parse(await dependencies.readJson(resolve(options.project, options.config))).run.packageName;
8
+ }
9
+ catch {
10
+ return undefined;
11
+ }
12
+ }
13
+ export function createDoctorCommand(dependencies) {
14
+ return new Command("doctor")
15
+ .description("Check TapHound tools, permissions, application, and device")
16
+ .option("--project <path>", "Android project root", dependencies.cwd())
17
+ .option("--config <path>", "TapHound config path", "taphound.config.json")
18
+ .option("--device <serial>", "Select an online Android device")
19
+ .option("--json", "Emit machine-readable JSON")
20
+ .action(async (options) => {
21
+ try {
22
+ const packageName = await configuredPackageName(dependencies, options);
23
+ const report = await dependencies.doctor.run({
24
+ ...(packageName === undefined ? {} : { packageName }),
25
+ ...(options.device === undefined
26
+ ? {}
27
+ : { requestedDevice: options.device }),
28
+ ...(dependencies.signal === undefined
29
+ ? {}
30
+ : { signal: dependencies.signal })
31
+ });
32
+ if (options.json === true) {
33
+ writeJson(dependencies.stdout, report);
34
+ }
35
+ else {
36
+ writeLine(dependencies.stdout, doctorMessage(report));
37
+ }
38
+ dependencies.setExitCode(report.status === "passed" ? 0 : 3);
39
+ }
40
+ catch (error) {
41
+ const output = failureOutput(4, "INTERNAL_ERROR", errorMessage(error));
42
+ if (options.json === true) {
43
+ writeJson(dependencies.stdout, output);
44
+ }
45
+ else {
46
+ writeLine(dependencies.stderr, output.failure.message);
47
+ }
48
+ dependencies.setExitCode(4);
49
+ }
50
+ });
51
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import type { CliDependencies } from "../dependencies.js";
3
+ export declare function createGenerationCommand(dependencies: CliDependencies): Command;