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,157 @@
1
+ import { confirm, input, number as numberPrompt, select } from "@inquirer/prompts";
2
+ const defaultPrompts = {
3
+ select: async (config) => select({
4
+ message: config.message,
5
+ choices: [...config.choices]
6
+ }),
7
+ input: async (config) => input({
8
+ message: config.message,
9
+ ...(config.validate === undefined ? {} : { validate: config.validate })
10
+ }),
11
+ confirm: async (config) => confirm(config),
12
+ number: async (config) => numberPrompt(config)
13
+ };
14
+ const ACTIONS = [
15
+ "click",
16
+ "longClick",
17
+ "inputText",
18
+ "swipe",
19
+ "scrollTo",
20
+ "back",
21
+ "wait",
22
+ "finish",
23
+ "cancel"
24
+ ];
25
+ function selectedString(value, allowed) {
26
+ if (typeof value !== "string"
27
+ || (allowed !== undefined && !allowed.includes(value))) {
28
+ throw new Error("Prompt returned an invalid selection");
29
+ }
30
+ return value;
31
+ }
32
+ function selectedNumber(value, minimum, maximum = Number.POSITIVE_INFINITY) {
33
+ if (typeof value !== "number"
34
+ || !Number.isFinite(value)
35
+ || value < minimum
36
+ || value > maximum) {
37
+ throw new Error("Prompt returned an invalid number");
38
+ }
39
+ return value;
40
+ }
41
+ export class InquirerRecorderPrompt {
42
+ prompts;
43
+ diagnostics;
44
+ constructor(prompts = defaultPrompts, diagnostics = process.stderr) {
45
+ this.prompts = prompts;
46
+ this.diagnostics = diagnostics;
47
+ }
48
+ async selectAction() {
49
+ const value = await this.prompts.select({
50
+ message: "Choose the next TapHound Action",
51
+ choices: ACTIONS.map((action) => ({ name: action, value: action }))
52
+ });
53
+ return selectedString(value, ACTIONS);
54
+ }
55
+ async selectTarget(choices) {
56
+ const value = await this.prompts.select({
57
+ message: "Choose a Layout target",
58
+ choices: choices.map((choice) => ({
59
+ name: choice.label,
60
+ value: choice.id
61
+ }))
62
+ });
63
+ return selectedString(value, choices.map((choice) => choice.id));
64
+ }
65
+ async inputText() {
66
+ const value = await this.prompts.input({
67
+ message: "Text to enter",
68
+ validate: (answer) => answer.length > 0 || "Text must not be empty"
69
+ });
70
+ return selectedString(value);
71
+ }
72
+ async selectSwipeDirection() {
73
+ const directions = ["up", "down", "left", "right"];
74
+ const value = await this.prompts.select({
75
+ message: "Swipe direction",
76
+ choices: directions.map((direction) => ({
77
+ name: direction,
78
+ value: direction
79
+ }))
80
+ });
81
+ return selectedString(value, directions);
82
+ }
83
+ async longClickDuration() {
84
+ return selectedNumber(await this.prompts.number({
85
+ message: "Long-click duration (ms)",
86
+ default: 800,
87
+ min: 1
88
+ }), 1);
89
+ }
90
+ async swipeOptions() {
91
+ const distancePercent = selectedNumber(await this.prompts.number({
92
+ message: "Swipe distance (0–1)",
93
+ default: 0.6,
94
+ min: 0.01,
95
+ max: 1
96
+ }), 0.01, 1);
97
+ const durationMs = selectedNumber(await this.prompts.number({
98
+ message: "Swipe duration (ms)",
99
+ default: 300,
100
+ min: 1
101
+ }), 1);
102
+ return { distancePercent, durationMs };
103
+ }
104
+ async selectFallbackLabel(annotatedScreenshotPath) {
105
+ const enabled = await this.prompts.confirm({
106
+ message: `Add annotated-label fallback from ${annotatedScreenshotPath}?`,
107
+ default: false
108
+ });
109
+ if (enabled !== true) {
110
+ return undefined;
111
+ }
112
+ const label = await this.prompts.input({
113
+ message: "Android CLI annotated label",
114
+ validate: (value) => /^#\d+$/.test(value) || "Use the #number format"
115
+ });
116
+ const selected = selectedString(label);
117
+ if (!/^#\d+$/.test(selected)) {
118
+ throw new Error("Fallback label must use the #number format");
119
+ }
120
+ return selected;
121
+ }
122
+ notifyFailure(message) {
123
+ this.diagnostics.write(`TapHound: ${message}\n`);
124
+ return Promise.resolve();
125
+ }
126
+ async selectScrollContainer(choices) {
127
+ const value = await this.prompts.select({
128
+ message: "Choose the scrollable container",
129
+ choices: choices.map((choice) => ({ name: choice.label, value: choice.id }))
130
+ });
131
+ return selectedString(value, choices.map((choice) => choice.id));
132
+ }
133
+ async scrollTargetDecision(choices) {
134
+ const scrollMore = "__scroll_more__";
135
+ const cancel = "__cancel__";
136
+ const value = await this.prompts.select({
137
+ message: "Select the target once visible, or scroll again",
138
+ choices: [
139
+ ...choices.map((choice) => ({ name: choice.label, value: choice.id })),
140
+ { name: "Scroll again", value: scrollMore },
141
+ { name: "Cancel scrollTo", value: cancel }
142
+ ]
143
+ });
144
+ const selected = selectedString(value, [
145
+ ...choices.map((choice) => choice.id),
146
+ scrollMore,
147
+ cancel
148
+ ]);
149
+ if (selected === scrollMore) {
150
+ return { kind: "scrollMore" };
151
+ }
152
+ if (selected === cancel) {
153
+ return { kind: "cancel" };
154
+ }
155
+ return { kind: "select", id: selected };
156
+ }
157
+ }
@@ -0,0 +1,61 @@
1
+ import type { FailureCode } from "../../domain/failure.js";
2
+ import type { Expectation } from "../../domain/journey.js";
3
+ import type { LayoutElement } from "../../domain/layout.js";
4
+ import type { AdbPort } from "../../ports/adb.js";
5
+ import type { AndroidCliPort } from "../../ports/android-cli.js";
6
+ import type { Clock } from "../../ports/clock.js";
7
+ import type { LogcatCollector } from "../collector/logcat-collector.js";
8
+ export interface ExpectationContext {
9
+ packageName: string;
10
+ deviceSerial: string;
11
+ stepStartedAt: number;
12
+ }
13
+ export interface ExpectationObservationInput {
14
+ timeoutMs: number;
15
+ signal?: AbortSignal | undefined;
16
+ }
17
+ export type ActivityExpectationObservation = {
18
+ status: "observed";
19
+ activity: string;
20
+ } | {
21
+ status: "failed";
22
+ message: string;
23
+ };
24
+ export type LayoutExpectationObservation = {
25
+ status: "observed";
26
+ layout: readonly LayoutElement[];
27
+ } | {
28
+ status: "failed";
29
+ message: string;
30
+ };
31
+ export interface ExpectationObservationBoundary {
32
+ activity?: (input: ExpectationObservationInput) => Promise<ActivityExpectationObservation>;
33
+ layout?: (input: ExpectationObservationInput) => Promise<LayoutExpectationObservation>;
34
+ }
35
+ export type ExpectationResult = {
36
+ status: "passed";
37
+ type: Expectation["type"];
38
+ durationMs: number;
39
+ actual?: string | undefined;
40
+ matchedLine?: string | undefined;
41
+ } | {
42
+ status: "failed";
43
+ type: Expectation["type"];
44
+ code: Extract<FailureCode, "EXPECT_ACTIVITY_FAILED" | "EXPECT_ELEMENT_FAILED" | "EXPECT_LOGCAT_FAILED">;
45
+ message: string;
46
+ durationMs: number;
47
+ actual?: string | undefined;
48
+ } | {
49
+ status: "cancelled";
50
+ type: Expectation["type"];
51
+ durationMs: number;
52
+ };
53
+ export declare class ExpectationEvaluator {
54
+ private readonly adb;
55
+ private readonly androidCli;
56
+ private readonly logcat;
57
+ private readonly clock;
58
+ private readonly pollIntervalMs;
59
+ constructor(adb: AdbPort, androidCli: AndroidCliPort, logcat: LogcatCollector, clock: Clock, pollIntervalMs?: number);
60
+ evaluate(expectation: Expectation, context: ExpectationContext, signal?: AbortSignal, observations?: ExpectationObservationBoundary): Promise<ExpectationResult>;
61
+ }
@@ -0,0 +1,207 @@
1
+ import { resolveLocator } from "../locator/locator-resolver.js";
2
+ function hasElement(elements, locator) {
3
+ return resolveLocator(elements, locator, { requireEnabled: false }).status
4
+ === "found";
5
+ }
6
+ function matchesLogcat(line, expectation, pattern) {
7
+ if (line.tag !== expectation.tag
8
+ || (expectation.level !== undefined
9
+ && line.level !== expectation.level)) {
10
+ return false;
11
+ }
12
+ const content = line.message ?? line.raw;
13
+ return typeof pattern === "string"
14
+ ? content.includes(pattern)
15
+ : pattern.test(content);
16
+ }
17
+ function isAborted(signal) {
18
+ return signal?.aborted === true;
19
+ }
20
+ export class ExpectationEvaluator {
21
+ adb;
22
+ androidCli;
23
+ logcat;
24
+ clock;
25
+ pollIntervalMs;
26
+ constructor(adb, androidCli, logcat, clock, pollIntervalMs = 100) {
27
+ this.adb = adb;
28
+ this.androidCli = androidCli;
29
+ this.logcat = logcat;
30
+ this.clock = clock;
31
+ this.pollIntervalMs = pollIntervalMs;
32
+ }
33
+ async evaluate(expectation, context, signal, observations = {}) {
34
+ const startedAt = this.clock.now();
35
+ const logPattern = expectation.type === "logcat"
36
+ ? (expectation.match === "regex"
37
+ ? new RegExp(expectation.pattern)
38
+ : expectation.pattern)
39
+ : undefined;
40
+ let actual;
41
+ for (;;) {
42
+ if (isAborted(signal)) {
43
+ return {
44
+ status: "cancelled",
45
+ type: expectation.type,
46
+ durationMs: this.clock.now() - startedAt
47
+ };
48
+ }
49
+ const commandTimeoutMs = Math.max(1, expectation.timeoutMs - (this.clock.now() - startedAt));
50
+ try {
51
+ switch (expectation.type) {
52
+ case "activity": {
53
+ const observation = observations.activity === undefined
54
+ ? {
55
+ status: "observed",
56
+ activity: await this.adb.currentActivity({
57
+ packageName: context.packageName,
58
+ deviceSerial: context.deviceSerial,
59
+ ...(signal === undefined ? {} : { signal }),
60
+ timeoutMs: commandTimeoutMs
61
+ })
62
+ }
63
+ : await observations.activity({
64
+ timeoutMs: commandTimeoutMs,
65
+ ...(signal === undefined ? {} : { signal })
66
+ });
67
+ if (isAborted(signal)) {
68
+ return {
69
+ status: "cancelled",
70
+ type: expectation.type,
71
+ durationMs: this.clock.now() - startedAt
72
+ };
73
+ }
74
+ if (observation.status === "failed") {
75
+ return {
76
+ status: "failed",
77
+ type: expectation.type,
78
+ code: "EXPECT_ACTIVITY_FAILED",
79
+ message: observation.message,
80
+ durationMs: this.clock.now() - startedAt
81
+ };
82
+ }
83
+ actual = observation.activity;
84
+ if (actual === expectation.value) {
85
+ return {
86
+ status: "passed",
87
+ type: expectation.type,
88
+ durationMs: this.clock.now() - startedAt,
89
+ actual
90
+ };
91
+ }
92
+ break;
93
+ }
94
+ case "element": {
95
+ const observation = observations.layout === undefined
96
+ ? {
97
+ status: "observed",
98
+ layout: await this.androidCli.layout({
99
+ deviceSerial: context.deviceSerial,
100
+ ...(signal === undefined ? {} : { signal }),
101
+ timeoutMs: commandTimeoutMs
102
+ })
103
+ }
104
+ : await observations.layout({
105
+ timeoutMs: commandTimeoutMs,
106
+ ...(signal === undefined ? {} : { signal })
107
+ });
108
+ if (isAborted(signal)) {
109
+ return {
110
+ status: "cancelled",
111
+ type: expectation.type,
112
+ durationMs: this.clock.now() - startedAt
113
+ };
114
+ }
115
+ if (observation.status === "failed") {
116
+ return {
117
+ status: "failed",
118
+ type: expectation.type,
119
+ code: "EXPECT_ELEMENT_FAILED",
120
+ message: observation.message,
121
+ durationMs: this.clock.now() - startedAt
122
+ };
123
+ }
124
+ if (hasElement(observation.layout, expectation.locator)) {
125
+ return {
126
+ status: "passed",
127
+ type: expectation.type,
128
+ durationMs: this.clock.now() - startedAt
129
+ };
130
+ }
131
+ break;
132
+ }
133
+ case "logcat": {
134
+ const matched = this.logcat
135
+ .linesBetween(context.stepStartedAt, this.clock.now())
136
+ .find((line) => matchesLogcat(line, expectation, logPattern ?? ""));
137
+ if (matched !== undefined) {
138
+ return {
139
+ status: "passed",
140
+ type: expectation.type,
141
+ durationMs: this.clock.now() - startedAt,
142
+ matchedLine: matched.raw
143
+ };
144
+ }
145
+ break;
146
+ }
147
+ }
148
+ }
149
+ catch (error) {
150
+ const elapsed = this.clock.now() - startedAt;
151
+ if (isAborted(signal)) {
152
+ return {
153
+ status: "cancelled",
154
+ type: expectation.type,
155
+ durationMs: elapsed
156
+ };
157
+ }
158
+ if (elapsed >= expectation.timeoutMs) {
159
+ const codes = {
160
+ activity: "EXPECT_ACTIVITY_FAILED",
161
+ element: "EXPECT_ELEMENT_FAILED",
162
+ logcat: "EXPECT_LOGCAT_FAILED"
163
+ };
164
+ return {
165
+ status: "failed",
166
+ type: expectation.type,
167
+ code: codes[expectation.type],
168
+ message: `${expectation.type} Expect command exceeded its timeout`,
169
+ durationMs: elapsed,
170
+ ...(actual === undefined ? {} : { actual })
171
+ };
172
+ }
173
+ throw error;
174
+ }
175
+ const elapsed = this.clock.now() - startedAt;
176
+ if (elapsed >= expectation.timeoutMs) {
177
+ const codes = {
178
+ activity: "EXPECT_ACTIVITY_FAILED",
179
+ element: "EXPECT_ELEMENT_FAILED",
180
+ logcat: "EXPECT_LOGCAT_FAILED"
181
+ };
182
+ return {
183
+ status: "failed",
184
+ type: expectation.type,
185
+ code: codes[expectation.type],
186
+ message: `${expectation.type} Expect did not match before timeout`,
187
+ durationMs: elapsed,
188
+ ...(actual === undefined ? {} : { actual })
189
+ };
190
+ }
191
+ const remaining = expectation.timeoutMs - elapsed;
192
+ try {
193
+ await this.clock.sleep(Math.min(this.pollIntervalMs, remaining), signal);
194
+ }
195
+ catch (error) {
196
+ if (isAborted(signal)) {
197
+ return {
198
+ status: "cancelled",
199
+ type: expectation.type,
200
+ durationMs: this.clock.now() - startedAt
201
+ };
202
+ }
203
+ throw error;
204
+ }
205
+ }
206
+ }
207
+ }
@@ -0,0 +1,39 @@
1
+ import type { AdbPort } from "../../ports/adb.js";
2
+ import type { Clock } from "../../ports/clock.js";
3
+ import type { CommandResult } from "../../ports/process-runner.js";
4
+ export type LogLevel = "V" | "D" | "I" | "W" | "E" | "F" | "A";
5
+ export interface LogcatLine {
6
+ receivedAt: number;
7
+ raw: string;
8
+ pid?: number | undefined;
9
+ tid?: number | undefined;
10
+ level?: LogLevel | undefined;
11
+ tag?: string | undefined;
12
+ message?: string | undefined;
13
+ }
14
+ export interface LogcatMetadata {
15
+ deviceSerial: string;
16
+ pids: readonly number[];
17
+ }
18
+ export interface StartLogcatOptions {
19
+ deviceSerial: string;
20
+ signal?: AbortSignal | undefined;
21
+ }
22
+ export declare class LogcatCollector {
23
+ private readonly adb;
24
+ private readonly clock;
25
+ private readonly collected;
26
+ private readonly stderr;
27
+ private readonly scopedPids;
28
+ private running?;
29
+ private streamMetadata?;
30
+ private stopPromise?;
31
+ constructor(adb: AdbPort, clock: Clock);
32
+ start(options: StartLogcatOptions): Promise<void>;
33
+ scopeToPids(pids: readonly number[]): void;
34
+ metadata(): LogcatMetadata;
35
+ lines(): readonly LogcatLine[];
36
+ diagnosticLines(): readonly string[];
37
+ linesBetween(startedAt: number, finishedAt: number): LogcatLine[];
38
+ stop(): Promise<CommandResult>;
39
+ }
@@ -0,0 +1,107 @@
1
+ const THREADTIME = /^\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d+\s+(\d+)\s+(\d+)\s+([VDIWEFA])\s+([^:]+):\s?(.*)$/;
2
+ function parseLine(raw, receivedAt) {
3
+ const match = THREADTIME.exec(raw);
4
+ if (match === null) {
5
+ return { receivedAt, raw };
6
+ }
7
+ const [, pid, tid, level, tag, message] = match;
8
+ if (pid === undefined
9
+ || tid === undefined
10
+ || level === undefined
11
+ || tag === undefined
12
+ || message === undefined) {
13
+ return { receivedAt, raw };
14
+ }
15
+ return {
16
+ receivedAt,
17
+ raw,
18
+ pid: Number(pid),
19
+ tid: Number(tid),
20
+ level: level,
21
+ tag: tag.trim(),
22
+ message
23
+ };
24
+ }
25
+ function startupFailure(result) {
26
+ return result.stderr.trim()
27
+ || result.spawnError
28
+ || (result.cancelled
29
+ ? "Logcat startup was cancelled"
30
+ : result.timedOut
31
+ ? "Logcat startup timed out"
32
+ : `Logcat exited during startup with code ${String(result.exitCode)}`);
33
+ }
34
+ export class LogcatCollector {
35
+ adb;
36
+ clock;
37
+ collected = [];
38
+ stderr = [];
39
+ scopedPids = new Set();
40
+ running;
41
+ streamMetadata;
42
+ stopPromise;
43
+ constructor(adb, clock) {
44
+ this.adb = adb;
45
+ this.clock = clock;
46
+ }
47
+ async start(options) {
48
+ if (this.running !== undefined) {
49
+ throw new Error("Logcat collector already started");
50
+ }
51
+ const logcatOptions = {
52
+ deviceSerial: options.deviceSerial,
53
+ onStdoutLine: (line) => {
54
+ this.collected.push(parseLine(line, this.clock.now()));
55
+ },
56
+ onStderrLine: (line) => {
57
+ this.stderr.push(line);
58
+ },
59
+ ...(options.signal === undefined ? {} : { signal: options.signal })
60
+ };
61
+ this.running = this.adb.startLogcat(logcatOptions);
62
+ this.streamMetadata = { deviceSerial: options.deviceSerial };
63
+ const startupResult = await this.running.started;
64
+ if (startupResult !== undefined) {
65
+ throw new Error(startupFailure(startupResult));
66
+ }
67
+ }
68
+ scopeToPids(pids) {
69
+ if (this.streamMetadata === undefined) {
70
+ throw new Error("Logcat collector has not started");
71
+ }
72
+ for (const pid of pids) {
73
+ if (!Number.isInteger(pid) || pid <= 0) {
74
+ throw new Error("Logcat PID must be a positive integer");
75
+ }
76
+ this.scopedPids.add(pid);
77
+ }
78
+ }
79
+ metadata() {
80
+ if (this.streamMetadata === undefined) {
81
+ throw new Error("Logcat collector has not started");
82
+ }
83
+ return {
84
+ deviceSerial: this.streamMetadata.deviceSerial,
85
+ pids: [...this.scopedPids].sort((left, right) => left - right)
86
+ };
87
+ }
88
+ lines() {
89
+ if (this.scopedPids.size === 0) {
90
+ return [...this.collected];
91
+ }
92
+ return this.collected.filter((line) => line.pid === undefined || this.scopedPids.has(line.pid));
93
+ }
94
+ diagnosticLines() {
95
+ return [...this.stderr];
96
+ }
97
+ linesBetween(startedAt, finishedAt) {
98
+ return this.lines().filter((line) => line.receivedAt >= startedAt && line.receivedAt <= finishedAt);
99
+ }
100
+ stop() {
101
+ if (this.running === undefined) {
102
+ throw new Error("Logcat collector has not started");
103
+ }
104
+ this.stopPromise ??= this.running.stop();
105
+ return this.stopPromise;
106
+ }
107
+ }
@@ -0,0 +1,27 @@
1
+ import type { TapHoundConfig } from "../../domain/config.js";
2
+ import { type ProjectFileInspector } from "../../ports/project-file-inspector.js";
3
+ export declare const MAX_CONTEXT_EVIDENCE_BYTES: number;
4
+ export type ContextValidationReasonCode = "CONTEXT_SCHEMA_INVALID" | "CONTEXT_IDENTITY_MISMATCH" | "PROJECT_ROOT_UNREADABLE" | "PROJECT_ROOT_NOT_DIRECTORY" | "EVIDENCE_SECRET_PATH" | "EVIDENCE_NOT_FOUND" | "EVIDENCE_UNREADABLE" | "EVIDENCE_NOT_FILE" | "EVIDENCE_PATH_ESCAPE" | "EVIDENCE_CHANGED_IDENTITY" | "EVIDENCE_TOO_LARGE" | "EVIDENCE_HASH_MISMATCH";
5
+ export interface ContextValidationReason {
6
+ code: ContextValidationReasonCode;
7
+ message: string;
8
+ }
9
+ export type ContextValidationResult = {
10
+ status: "valid";
11
+ } | {
12
+ status: "stale";
13
+ reason: ContextValidationReason;
14
+ } | {
15
+ status: "invalid";
16
+ reason: ContextValidationReason;
17
+ };
18
+ export interface ContextValidationInput {
19
+ context: unknown;
20
+ projectRoot: string;
21
+ config: TapHoundConfig;
22
+ }
23
+ export declare class ContextValidator {
24
+ private readonly files;
25
+ constructor(files: ProjectFileInspector);
26
+ readonly validate: (input: ContextValidationInput) => Promise<ContextValidationResult>;
27
+ }