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,88 @@
1
+ import { GenerationSessionSchema } from "../../domain/generation.js";
2
+ import { ProposedStepSchema } from "../../domain/proposed-step.js";
3
+ import { RuntimeSnapshotSchema, hashRuntimeSnapshot } from "../../domain/runtime-snapshot.js";
4
+ import { resolveLocator } from "../locator/locator-resolver.js";
5
+ import { GenerationOperationError } from "./generation-starter.js";
6
+ function flatten(elements) {
7
+ return elements.flatMap((element) => [
8
+ element,
9
+ ...flatten(element.children)
10
+ ]);
11
+ }
12
+ function rejectCapability(message) {
13
+ throw new GenerationOperationError("ACTION_UNSUPPORTED", message);
14
+ }
15
+ function requireUniqueTarget(snapshot, locator, capability, capabilityName) {
16
+ const resolution = resolveLocator(snapshot.layout, locator);
17
+ if (resolution.status !== "found") {
18
+ rejectCapability(resolution.message);
19
+ }
20
+ if (!capability(resolution.element)) {
21
+ rejectCapability(`Layout target lacks required ${capabilityName} capability`);
22
+ }
23
+ return resolution.element;
24
+ }
25
+ function validateBinding(session, snapshot, proposal) {
26
+ const binding = proposal.binding;
27
+ if (session.state !== "active"
28
+ || session.inFlight !== null
29
+ || session.pendingConfirmation !== null
30
+ || session.verification.status !== "notRun"
31
+ || session.publication.status !== "notRun"
32
+ || binding.generationId !== session.id
33
+ || binding.baseRevision !== session.revision
34
+ || binding.snapshotHash !== session.bindings.snapshotHash
35
+ || snapshot.generationId !== session.id
36
+ || snapshot.baseRevision !== session.revision
37
+ || snapshot.deviceSerial !== session.target.deviceSerial
38
+ || hashRuntimeSnapshot(snapshot) !== binding.snapshotHash) {
39
+ throw new GenerationOperationError("SNAPSHOT_STALE", "Proposal is not bound to the authoritative generation snapshot");
40
+ }
41
+ if (snapshot.expectedPackageName !== session.target.packageName
42
+ || snapshot.foregroundPackageName !== session.target.packageName) {
43
+ throw new GenerationOperationError("PACKAGE_ESCAPE", "Foreground package escaped the generation target");
44
+ }
45
+ if (snapshot.activity !== proposal.activity.before) {
46
+ throw new GenerationOperationError("SNAPSHOT_STALE", "Proposal before Activity does not match the current snapshot");
47
+ }
48
+ }
49
+ function validateAction(snapshot, proposal) {
50
+ if (proposal.action === "click") {
51
+ requireUniqueTarget(snapshot, proposal.locator, (element) => element.clickable === true, "clickable");
52
+ return;
53
+ }
54
+ if (proposal.action === "longClick") {
55
+ requireUniqueTarget(snapshot, proposal.locator, (element) => element.longClickable === true, "longClickable");
56
+ return;
57
+ }
58
+ if (proposal.action === "swipe") {
59
+ requireUniqueTarget(snapshot, proposal.locator, (element) => element.scrollable === true
60
+ && element.bounds !== undefined, "scrollable bounds");
61
+ return;
62
+ }
63
+ if (proposal.action === "scrollTo") {
64
+ requireUniqueTarget(snapshot, proposal.container, (element) => element.scrollable === true
65
+ && element.bounds !== undefined, "scrollable container bounds");
66
+ const target = resolveLocator(snapshot.layout, proposal.locator, { requireEnabled: false });
67
+ if (target.status === "failed" && target.code === "LOCATOR_AMBIGUOUS") {
68
+ rejectCapability(`scrollTo target is ambiguous: ${target.message}`);
69
+ }
70
+ return;
71
+ }
72
+ if (proposal.action === "inputText") {
73
+ const focused = flatten(snapshot.layout).filter((element) => element.enabled && element.focused === true);
74
+ if (focused.length !== 1) {
75
+ rejectCapability("inputText requires exactly one enabled focused visible Layout element");
76
+ }
77
+ }
78
+ }
79
+ export class ProposedStepValidator {
80
+ validate(input) {
81
+ const session = GenerationSessionSchema.parse(input.session);
82
+ const snapshot = RuntimeSnapshotSchema.parse(input.snapshot);
83
+ const proposal = ProposedStepSchema.parse(input.proposal);
84
+ validateBinding(session, snapshot, proposal);
85
+ validateAction(snapshot, proposal);
86
+ return proposal;
87
+ }
88
+ }
@@ -0,0 +1,9 @@
1
+ import type { ProposedStep } from "../../domain/proposed-step.js";
2
+ import type { InteractionPolicy } from "../../domain/project-context.js";
3
+ export type EffectiveRisk = "safe" | "confirmationRequired" | "forbidden";
4
+ export interface RiskEvaluation {
5
+ effectiveRisk: EffectiveRisk;
6
+ }
7
+ export declare class RiskEvaluator {
8
+ evaluate(action: ProposedStep["action"], policy: InteractionPolicy): RiskEvaluation;
9
+ }
@@ -0,0 +1,14 @@
1
+ export class RiskEvaluator {
2
+ evaluate(action, policy) {
3
+ if (policy.forbiddenActions.includes(action)) {
4
+ return { effectiveRisk: "forbidden" };
5
+ }
6
+ if (policy.confirmationRequiredActions.includes(action)) {
7
+ return { effectiveRisk: "confirmationRequired" };
8
+ }
9
+ if (policy.allowedActions.includes(action)) {
10
+ return { effectiveRisk: "safe" };
11
+ }
12
+ return { effectiveRisk: "confirmationRequired" };
13
+ }
14
+ }
@@ -0,0 +1,39 @@
1
+ import { type PendingConfirmation } from "../../domain/generation.js";
2
+ import { type ProposalBinding } from "../../domain/proposed-step.js";
3
+ import { type RuntimeSnapshot } from "../../domain/runtime-snapshot.js";
4
+ import type { AdbPort } from "../../ports/adb.js";
5
+ import type { AndroidCliPort } from "../../ports/android-cli.js";
6
+ import type { GenerationSessionStore } from "../../ports/generation-session-store.js";
7
+ export type RuntimeObservationBinding = ProposalBinding;
8
+ export interface RuntimeObservation {
9
+ binding: RuntimeObservationBinding;
10
+ snapshot: RuntimeSnapshot;
11
+ snapshotHash: string;
12
+ }
13
+ export interface RuntimeObserveInput {
14
+ generationId: string;
15
+ signal?: AbortSignal | undefined;
16
+ }
17
+ export interface RuntimeObserverDependencies {
18
+ store: Pick<GenerationSessionStore, "read" | "writeEvidence" | "produceEvidence" | "commitSnapshot">;
19
+ adb: Pick<AdbPort, "foregroundComponent" | "appProcesses">;
20
+ androidCli: Pick<AndroidCliPort, "layout" | "captureScreen">;
21
+ now: () => Date;
22
+ createAttemptId: () => string;
23
+ }
24
+ export interface SnapshotReobservationGuardDependencies {
25
+ store: Pick<GenerationSessionStore, "read">;
26
+ adb: Pick<AdbPort, "foregroundComponent" | "appProcesses">;
27
+ androidCli: Pick<AndroidCliPort, "layout">;
28
+ now: () => Date;
29
+ }
30
+ export declare class RuntimeObserver {
31
+ private readonly dependencies;
32
+ constructor(dependencies: RuntimeObserverDependencies);
33
+ readonly observe: (input: RuntimeObserveInput) => Promise<RuntimeObservation>;
34
+ }
35
+ export declare class SnapshotReobservationGuard {
36
+ private readonly dependencies;
37
+ constructor(dependencies: SnapshotReobservationGuardDependencies);
38
+ readonly assertFresh: (input: ProposalBinding, signal?: AbortSignal, approvedConfirmation?: PendingConfirmation) => Promise<RuntimeSnapshot>;
39
+ }
@@ -0,0 +1,157 @@
1
+ import { z } from "zod";
2
+ import { primaryAppPid } from "../../domain/app-process.js";
3
+ import { GenerationSessionIdSchema, GenerationSessionSchema } from "../../domain/generation.js";
4
+ import { LayoutElementSchema } from "../../domain/layout.js";
5
+ import { ProposalBindingSchema } from "../../domain/proposed-step.js";
6
+ import { RuntimeSnapshotSchema, hashRuntimeSnapshot } from "../../domain/runtime-snapshot.js";
7
+ import { GenerationOperationError } from "./generation-starter.js";
8
+ function failedCapture(result) {
9
+ return result.exitCode !== 0
10
+ || result.timedOut
11
+ || result.cancelled
12
+ || result.spawnError !== undefined;
13
+ }
14
+ function revisionPath(revision) {
15
+ return `evidence/snapshots/revision-${String(revision).padStart(6, "0")}`;
16
+ }
17
+ function assertObservable(session) {
18
+ if (session.state !== "active"
19
+ || session.inFlight !== null
20
+ || session.pendingConfirmation !== null) {
21
+ throw new Error("Generation session must be active and idle to observe");
22
+ }
23
+ if (session.revision === Number.MAX_SAFE_INTEGER) {
24
+ throw new Error("Generation session revision cannot be incremented");
25
+ }
26
+ }
27
+ async function collectRuntime(dependencies, session, signal) {
28
+ const identity = {
29
+ packageName: session.target.packageName,
30
+ deviceSerial: session.target.deviceSerial,
31
+ ...(signal === undefined ? {} : { signal })
32
+ };
33
+ const foreground = await dependencies.adb.foregroundComponent(identity);
34
+ const pid = primaryAppPid(await dependencies.adb.appProcesses(identity), session.target.packageName);
35
+ const layout = z.array(LayoutElementSchema).parse(await dependencies.androidCli.layout({
36
+ deviceSerial: session.target.deviceSerial,
37
+ ...(signal === undefined ? {} : { signal })
38
+ }));
39
+ return {
40
+ foregroundPackageName: foreground.packageName,
41
+ activity: foreground.activity,
42
+ pid,
43
+ layout
44
+ };
45
+ }
46
+ export class RuntimeObserver {
47
+ dependencies;
48
+ constructor(dependencies) {
49
+ this.dependencies = dependencies;
50
+ }
51
+ observe = async (input) => {
52
+ const current = GenerationSessionSchema.parse(await this.dependencies.store.read(input.generationId));
53
+ assertObservable(current);
54
+ const runtime = await collectRuntime(this.dependencies, current, input.signal);
55
+ const baseRevision = current.revision + 1;
56
+ const attemptId = GenerationSessionIdSchema.parse(this.dependencies.createAttemptId());
57
+ const evidenceDirectory = `${revisionPath(baseRevision)}/${attemptId}`;
58
+ const screenshotPath = `${evidenceDirectory}/screen.png`;
59
+ await this.dependencies.store.produceEvidence(current.id, screenshotPath, async (temporaryPath) => {
60
+ const capture = await this.dependencies.androidCli.captureScreen({
61
+ outputPath: temporaryPath,
62
+ deviceSerial: current.target.deviceSerial,
63
+ ...(input.signal === undefined ? {} : { signal: input.signal })
64
+ });
65
+ if (failedCapture(capture)) {
66
+ throw new Error(capture.stderr.trim()
67
+ || capture.spawnError
68
+ || "Runtime screenshot capture failed");
69
+ }
70
+ });
71
+ const snapshot = RuntimeSnapshotSchema.parse({
72
+ version: 1,
73
+ generationId: current.id,
74
+ baseRevision,
75
+ deviceSerial: current.target.deviceSerial,
76
+ expectedPackageName: current.target.packageName,
77
+ foregroundPackageName: runtime.foregroundPackageName,
78
+ activity: runtime.activity,
79
+ pid: runtime.pid,
80
+ capturedAt: this.dependencies.now().toISOString(),
81
+ screenshotPath,
82
+ layout: runtime.layout
83
+ });
84
+ const snapshotHash = hashRuntimeSnapshot(snapshot);
85
+ await this.dependencies.store.writeEvidence(current.id, `${evidenceDirectory}/snapshot.json`, snapshot);
86
+ const next = GenerationSessionSchema.parse({
87
+ ...current,
88
+ revision: baseRevision,
89
+ bindings: {
90
+ ...current.bindings,
91
+ snapshotHash
92
+ }
93
+ });
94
+ await this.dependencies.store.commitSnapshot(current.id, current.revision, next);
95
+ return {
96
+ binding: {
97
+ generationId: current.id,
98
+ baseRevision: next.revision,
99
+ snapshotHash
100
+ },
101
+ snapshot,
102
+ snapshotHash
103
+ };
104
+ };
105
+ }
106
+ export class SnapshotReobservationGuard {
107
+ dependencies;
108
+ constructor(dependencies) {
109
+ this.dependencies = dependencies;
110
+ }
111
+ assertFresh = async (input, signal, approvedConfirmation) => {
112
+ try {
113
+ const binding = ProposalBindingSchema.parse(input);
114
+ const session = GenerationSessionSchema.parse(await this.dependencies.store.read(binding.generationId));
115
+ if (session.id !== binding.generationId
116
+ || session.state !== "active"
117
+ || session.bindings.snapshotHash !== binding.snapshotHash
118
+ || session.inFlight !== null
119
+ || session.verification.status !== "notRun"
120
+ || session.publication.status !== "notRun"
121
+ || (approvedConfirmation === undefined
122
+ ? (session.revision !== binding.baseRevision
123
+ || session.pendingConfirmation !== null)
124
+ : (approvedConfirmation.status !== "approved"
125
+ || session.revision !== binding.baseRevision + 2
126
+ || JSON.stringify(session.pendingConfirmation)
127
+ !== JSON.stringify(approvedConfirmation)))) {
128
+ throw new GenerationOperationError("SNAPSHOT_STALE", "Proposal snapshot binding is no longer authoritative");
129
+ }
130
+ const runtime = await collectRuntime(this.dependencies, session, signal);
131
+ const snapshot = RuntimeSnapshotSchema.parse({
132
+ version: 1,
133
+ generationId: session.id,
134
+ baseRevision: binding.baseRevision,
135
+ deviceSerial: session.target.deviceSerial,
136
+ expectedPackageName: session.target.packageName,
137
+ foregroundPackageName: runtime.foregroundPackageName,
138
+ activity: runtime.activity,
139
+ pid: runtime.pid,
140
+ capturedAt: this.dependencies.now().toISOString(),
141
+ screenshotPath: "non-authoritative://runtime-reobservation",
142
+ layout: runtime.layout
143
+ });
144
+ if (hashRuntimeSnapshot(snapshot) !== binding.snapshotHash) {
145
+ throw new GenerationOperationError("SNAPSHOT_STALE", "Runtime snapshot changed after proposal");
146
+ }
147
+ return snapshot;
148
+ }
149
+ catch (error) {
150
+ if (error instanceof GenerationOperationError
151
+ && error.code === "SNAPSHOT_STALE") {
152
+ throw error;
153
+ }
154
+ throw new GenerationOperationError("SNAPSHOT_STALE", "Runtime snapshot could not be re-observed");
155
+ }
156
+ };
157
+ }
@@ -0,0 +1,17 @@
1
+ import { type AgentId, type InitResult } from "../../domain/init.js";
2
+ import type { SkillInstallerPort } from "../../ports/skill-installer.js";
3
+ export interface InitServiceDeps {
4
+ installer: SkillInstallerPort;
5
+ cwd: string;
6
+ homedir: string;
7
+ }
8
+ export interface InitInput {
9
+ agents: readonly AgentId[];
10
+ global: boolean;
11
+ }
12
+ export declare class InitService {
13
+ private readonly deps;
14
+ constructor(deps: InitServiceDeps);
15
+ install(input: InitInput): Promise<InitResult>;
16
+ }
17
+ export declare function defaultHomedir(): string;
@@ -0,0 +1,44 @@
1
+ import { homedir } from "node:os";
2
+ import { NoAgentsSelectedError, resolveTargetPaths } from "../../domain/init.js";
3
+ export class InitService {
4
+ deps;
5
+ constructor(deps) {
6
+ this.deps = deps;
7
+ }
8
+ async install(input) {
9
+ if (input.agents.length === 0) {
10
+ throw new NoAgentsSelectedError();
11
+ }
12
+ const targets = resolveTargetPaths(input.agents, input.global, this.deps.cwd, this.deps.homedir);
13
+ const installedPaths = [];
14
+ const skippedPaths = [];
15
+ const allAgents = [];
16
+ for (const target of targets) {
17
+ const result = await this.deps.installer.installTo(target.absolutePath);
18
+ if (result.skipped) {
19
+ skippedPaths.push(target.relativePath);
20
+ }
21
+ else {
22
+ installedPaths.push(target.relativePath);
23
+ }
24
+ for (const agent of target.agents) {
25
+ if (!allAgents.includes(agent)) {
26
+ allAgents.push(agent);
27
+ }
28
+ }
29
+ }
30
+ const result = {
31
+ status: "installed",
32
+ exitCode: 0,
33
+ agents: allAgents,
34
+ paths: installedPaths
35
+ };
36
+ if (skippedPaths.length > 0) {
37
+ return { ...result, skipped: skippedPaths };
38
+ }
39
+ return result;
40
+ }
41
+ }
42
+ export function defaultHomedir() {
43
+ return homedir();
44
+ }
@@ -0,0 +1,25 @@
1
+ import type { FailureCode } from "../../domain/failure.js";
2
+ import type { JourneyStep } from "../../domain/journey.js";
3
+ import type { Bounds } from "../../domain/layout.js";
4
+ import type { AdbPort } from "../../ports/adb.js";
5
+ import type { Point } from "../../ports/android-cli.js";
6
+ export interface ActionTarget {
7
+ point: Point;
8
+ bounds?: Bounds | undefined;
9
+ }
10
+ export type ActionExecutionResult = {
11
+ status: "succeeded";
12
+ } | {
13
+ status: "failed";
14
+ code: Extract<FailureCode, "ACTION_FAILED">;
15
+ message: string;
16
+ };
17
+ export declare class ActionExecutor {
18
+ private readonly adb;
19
+ private readonly deviceSerial;
20
+ constructor(adb: AdbPort, deviceSerial: string);
21
+ execute(step: JourneyStep, target?: ActionTarget, signal?: AbortSignal): Promise<ActionExecutionResult>;
22
+ swipeBounds(bounds: Bounds, direction: Extract<JourneyStep, {
23
+ action: "swipe";
24
+ }>["direction"], distancePercent: number, durationMs: number, signal?: AbortSignal): Promise<ActionExecutionResult>;
25
+ }
@@ -0,0 +1,103 @@
1
+ function failed(message) {
2
+ return {
3
+ status: "failed",
4
+ code: "ACTION_FAILED",
5
+ message
6
+ };
7
+ }
8
+ function commandResult(result) {
9
+ if (result.exitCode !== 0
10
+ || result.timedOut
11
+ || result.cancelled
12
+ || result.spawnError !== undefined) {
13
+ return failed(result.stderr.trim() || "ADB Action failed");
14
+ }
15
+ return { status: "succeeded" };
16
+ }
17
+ function swipePoints(bounds, direction, distancePercent) {
18
+ const centerX = (bounds.left + bounds.right) / 2;
19
+ const centerY = (bounds.top + bounds.bottom) / 2;
20
+ const horizontalDistance = (bounds.right - bounds.left) * distancePercent;
21
+ const verticalDistance = (bounds.bottom - bounds.top) * distancePercent;
22
+ const points = {
23
+ up: {
24
+ from: { x: centerX, y: centerY + verticalDistance / 2 },
25
+ to: { x: centerX, y: centerY - verticalDistance / 2 }
26
+ },
27
+ down: {
28
+ from: { x: centerX, y: centerY - verticalDistance / 2 },
29
+ to: { x: centerX, y: centerY + verticalDistance / 2 }
30
+ },
31
+ left: {
32
+ from: { x: centerX + horizontalDistance / 2, y: centerY },
33
+ to: { x: centerX - horizontalDistance / 2, y: centerY }
34
+ },
35
+ right: {
36
+ from: { x: centerX - horizontalDistance / 2, y: centerY },
37
+ to: { x: centerX + horizontalDistance / 2, y: centerY }
38
+ }
39
+ };
40
+ const selected = points[direction];
41
+ return {
42
+ from: {
43
+ x: Math.round(selected.from.x),
44
+ y: Math.round(selected.from.y)
45
+ },
46
+ to: {
47
+ x: Math.round(selected.to.x),
48
+ y: Math.round(selected.to.y)
49
+ }
50
+ };
51
+ }
52
+ export class ActionExecutor {
53
+ adb;
54
+ deviceSerial;
55
+ constructor(adb, deviceSerial) {
56
+ this.adb = adb;
57
+ this.deviceSerial = deviceSerial;
58
+ }
59
+ async execute(step, target, signal) {
60
+ let result;
61
+ switch (step.action) {
62
+ case "click":
63
+ if (target === undefined) {
64
+ return failed("click requires a resolved target");
65
+ }
66
+ result = await this.adb.tap(target.point, this.deviceSerial, signal);
67
+ break;
68
+ case "longClick":
69
+ if (target === undefined) {
70
+ return failed("longClick requires a resolved target");
71
+ }
72
+ result = await this.adb.longClick(target.point, step.durationMs, this.deviceSerial, signal);
73
+ break;
74
+ case "inputText":
75
+ result = await this.adb.inputText(step.text, this.deviceSerial, signal);
76
+ break;
77
+ case "swipe": {
78
+ if (target === undefined) {
79
+ return failed("swipe requires a resolved target");
80
+ }
81
+ if (target.bounds === undefined) {
82
+ return failed("swipe requires target bounds from a scrollable element");
83
+ }
84
+ const points = swipePoints(target.bounds, step.direction, step.distancePercent);
85
+ result = await this.adb.swipe(points.from, points.to, step.durationMs, this.deviceSerial, signal);
86
+ break;
87
+ }
88
+ case "scrollTo":
89
+ return failed("scrollTo is not executed via ActionExecutor");
90
+ case "back":
91
+ result = await this.adb.back(this.deviceSerial, signal);
92
+ break;
93
+ case "wait":
94
+ return { status: "succeeded" };
95
+ }
96
+ return commandResult(result);
97
+ }
98
+ async swipeBounds(bounds, direction, distancePercent, durationMs, signal) {
99
+ const points = swipePoints(bounds, direction, distancePercent);
100
+ const result = await this.adb.swipe(points.from, points.to, durationMs, this.deviceSerial, signal);
101
+ return commandResult(result);
102
+ }
103
+ }
@@ -0,0 +1,24 @@
1
+ import type { FailureCode } from "../../domain/failure.js";
2
+ import type { JourneyStep } from "../../domain/journey.js";
3
+ import type { AndroidCliPort, Point } from "../../ports/android-cli.js";
4
+ export type FallbackResolution = {
5
+ status: "unavailable";
6
+ } | {
7
+ status: "found";
8
+ point: Point;
9
+ source: "annotatedLabel";
10
+ label: string;
11
+ annotatedScreenshotPath: string;
12
+ } | {
13
+ status: "failed";
14
+ code: Extract<FailureCode, "LOCATOR_NOT_FOUND">;
15
+ message: string;
16
+ label?: string | undefined;
17
+ annotatedScreenshotPath?: string | undefined;
18
+ };
19
+ export declare class FallbackResolver {
20
+ private readonly androidCli;
21
+ private readonly deviceSerial;
22
+ constructor(androidCli: AndroidCliPort, deviceSerial: string);
23
+ resolve(step: JourneyStep, annotatedScreenshotPath: string, signal?: AbortSignal): Promise<FallbackResolution>;
24
+ }
@@ -0,0 +1,48 @@
1
+ export class FallbackResolver {
2
+ androidCli;
3
+ deviceSerial;
4
+ constructor(androidCli, deviceSerial) {
5
+ this.androidCli = androidCli;
6
+ this.deviceSerial = deviceSerial;
7
+ }
8
+ async resolve(step, annotatedScreenshotPath, signal) {
9
+ if ((step.action !== "click" && step.action !== "longClick")
10
+ || step.fallback === undefined) {
11
+ return { status: "unavailable" };
12
+ }
13
+ const capture = await this.androidCli.captureScreen({
14
+ outputPath: annotatedScreenshotPath,
15
+ annotate: true,
16
+ deviceSerial: this.deviceSerial,
17
+ ...(signal === undefined ? {} : { signal })
18
+ });
19
+ if (capture.exitCode !== 0) {
20
+ return {
21
+ status: "failed",
22
+ code: "LOCATOR_NOT_FOUND",
23
+ message: capture.stderr.trim() || "Annotated screen capture failed"
24
+ };
25
+ }
26
+ try {
27
+ const point = await this.androidCli.resolveScreen(annotatedScreenshotPath, step.fallback.label, signal);
28
+ return {
29
+ status: "found",
30
+ point,
31
+ source: "annotatedLabel",
32
+ label: step.fallback.label,
33
+ annotatedScreenshotPath
34
+ };
35
+ }
36
+ catch (error) {
37
+ return {
38
+ status: "failed",
39
+ code: "LOCATOR_NOT_FOUND",
40
+ message: error instanceof Error
41
+ ? error.message
42
+ : "Annotated label resolution failed",
43
+ label: step.fallback.label,
44
+ annotatedScreenshotPath
45
+ };
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,40 @@
1
+ import type { FailureCode } from "../../domain/failure.js";
2
+ import type { JourneyStep } from "../../domain/journey.js";
3
+ import type { LayoutElement } from "../../domain/layout.js";
4
+ import type { AndroidCliPort } from "../../ports/android-cli.js";
5
+ import type { IdleConfig, IdleWaiter } from "../wait/idle-waiter.js";
6
+ import type { ActionExecutor } from "./action-executor.js";
7
+ export type ScrollToExecutionResult = {
8
+ status: "found";
9
+ swipesUsed: number;
10
+ } | {
11
+ status: "failed";
12
+ code: FailureCode;
13
+ message: string;
14
+ swipesUsed: number;
15
+ idle?: {
16
+ polls: number;
17
+ lastDiff: readonly unknown[];
18
+ };
19
+ } | {
20
+ status: "cancelled";
21
+ swipesUsed: number;
22
+ };
23
+ export interface ScrollToExecutorOptions {
24
+ androidCli: Pick<AndroidCliPort, "layout">;
25
+ actionExecutor: Pick<ActionExecutor, "swipeBounds">;
26
+ idleWaiter: Pick<IdleWaiter, "waitUntilIdle">;
27
+ deviceSerial: string;
28
+ idle: IdleConfig;
29
+ readLayout?: (() => Promise<readonly LayoutElement[]>) | undefined;
30
+ beforeSwipe?: (() => Promise<readonly LayoutElement[]>) | undefined;
31
+ beforeMutation?: (() => Promise<void>) | undefined;
32
+ requireLiveContainerCapability?: boolean | undefined;
33
+ }
34
+ export declare class ScrollToExecutor {
35
+ private readonly options;
36
+ constructor(options: ScrollToExecutorOptions);
37
+ execute(step: Extract<JourneyStep, {
38
+ action: "scrollTo";
39
+ }>, signal?: AbortSignal, initialLayout?: readonly LayoutElement[]): Promise<ScrollToExecutionResult>;
40
+ }