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.
- package/LICENSE +202 -0
- package/README.md +200 -0
- package/README.zh-CN.md +200 -0
- package/assets/brand/taphound-mark.svg +9 -0
- package/assets/skills/taphound-ai-journey/GUIDE.md +862 -0
- package/assets/skills/taphound-ai-journey/SKILL.md +377 -0
- package/assets/skills/taphound-ai-journey/prompts/analyze-project.md +526 -0
- package/assets/skills/taphound-ai-journey/prompts/check-completion.md +52 -0
- package/assets/skills/taphound-ai-journey/prompts/generate-step.md +109 -0
- package/assets/skills/taphound-ai-journey/schemas/observe-output.json +42 -0
- package/assets/skills/taphound-ai-journey/schemas/project-context.json +81 -0
- package/assets/skills/taphound-ai-journey/schemas/proposed-step-envelope.json +267 -0
- package/assets/skills/taphound-ai-journey/templates/project-context.example.json +49 -0
- package/dist/adapters/adb/adb-adapter.d.ts +23 -0
- package/dist/adapters/adb/adb-adapter.js +218 -0
- package/dist/adapters/android-cli/android-cli-adapter.d.ts +11 -0
- package/dist/adapters/android-cli/android-cli-adapter.js +64 -0
- package/dist/adapters/android-cli/layout-parser.d.ts +3 -0
- package/dist/adapters/android-cli/layout-parser.js +174 -0
- package/dist/adapters/clock/system-clock.d.ts +5 -0
- package/dist/adapters/clock/system-clock.js +13 -0
- package/dist/adapters/filesystem/artifact-store.d.ts +4 -0
- package/dist/adapters/filesystem/artifact-store.js +73 -0
- package/dist/adapters/filesystem/generation-meta-writer.d.ts +16 -0
- package/dist/adapters/filesystem/generation-meta-writer.js +34 -0
- package/dist/adapters/filesystem/generation-session-store.d.ts +68 -0
- package/dist/adapters/filesystem/generation-session-store.js +1603 -0
- package/dist/adapters/filesystem/journey-writer.d.ts +16 -0
- package/dist/adapters/filesystem/journey-writer.js +34 -0
- package/dist/adapters/filesystem/project-bound-file.d.ts +6 -0
- package/dist/adapters/filesystem/project-bound-file.js +240 -0
- package/dist/adapters/filesystem/project-file-inspector.d.ts +8 -0
- package/dist/adapters/filesystem/project-file-inspector.js +198 -0
- package/dist/adapters/filesystem/skill-installer.d.ts +10 -0
- package/dist/adapters/filesystem/skill-installer.js +39 -0
- package/dist/adapters/process/node-process-runner.d.ts +8 -0
- package/dist/adapters/process/node-process-runner.js +155 -0
- package/dist/adapters/prompt/inquirer-generation-prompt.d.ts +55 -0
- package/dist/adapters/prompt/inquirer-generation-prompt.js +213 -0
- package/dist/adapters/prompt/inquirer-init-prompt.d.ts +32 -0
- package/dist/adapters/prompt/inquirer-init-prompt.js +55 -0
- package/dist/adapters/prompt/inquirer-recorder-prompt.d.ts +47 -0
- package/dist/adapters/prompt/inquirer-recorder-prompt.js +157 -0
- package/dist/application/assertion/expectation-evaluator.d.ts +61 -0
- package/dist/application/assertion/expectation-evaluator.js +207 -0
- package/dist/application/collector/logcat-collector.d.ts +39 -0
- package/dist/application/collector/logcat-collector.js +107 -0
- package/dist/application/context/context-validator.d.ts +27 -0
- package/dist/application/context/context-validator.js +120 -0
- package/dist/application/doctor/doctor-service.d.ts +35 -0
- package/dist/application/doctor/doctor-service.js +178 -0
- package/dist/application/generation/focused-input.d.ts +2 -0
- package/dist/application/generation/focused-input.js +9 -0
- package/dist/application/generation/generation-confirmation-service.d.ts +76 -0
- package/dist/application/generation/generation-confirmation-service.js +383 -0
- package/dist/application/generation/generation-finalizer.d.ts +67 -0
- package/dist/application/generation/generation-finalizer.js +646 -0
- package/dist/application/generation/generation-publisher.d.ts +46 -0
- package/dist/application/generation/generation-publisher.js +178 -0
- package/dist/application/generation/generation-starter.d.ts +31 -0
- package/dist/application/generation/generation-starter.js +102 -0
- package/dist/application/generation/generation-step-executor.d.ts +52 -0
- package/dist/application/generation/generation-step-executor.js +668 -0
- package/dist/application/generation/proposed-step-validator.d.ts +11 -0
- package/dist/application/generation/proposed-step-validator.js +88 -0
- package/dist/application/generation/risk-evaluator.d.ts +9 -0
- package/dist/application/generation/risk-evaluator.js +14 -0
- package/dist/application/generation/runtime-observer.d.ts +39 -0
- package/dist/application/generation/runtime-observer.js +157 -0
- package/dist/application/init/init-service.d.ts +17 -0
- package/dist/application/init/init-service.js +44 -0
- package/dist/application/interaction/action-executor.d.ts +25 -0
- package/dist/application/interaction/action-executor.js +103 -0
- package/dist/application/interaction/fallback-resolver.d.ts +24 -0
- package/dist/application/interaction/fallback-resolver.js +48 -0
- package/dist/application/interaction/scroll-to-executor.d.ts +40 -0
- package/dist/application/interaction/scroll-to-executor.js +191 -0
- package/dist/application/locator/locator-resolver.d.ts +20 -0
- package/dist/application/locator/locator-resolver.js +89 -0
- package/dist/application/project/project-describer.d.ts +14 -0
- package/dist/application/project/project-describer.js +8 -0
- package/dist/application/recorder/locator-selector.d.ts +10 -0
- package/dist/application/recorder/locator-selector.js +80 -0
- package/dist/application/recorder/recorder-service.d.ts +41 -0
- package/dist/application/recorder/recorder-service.js +331 -0
- package/dist/application/report/report-writer.d.ts +10 -0
- package/dist/application/report/report-writer.js +39 -0
- package/dist/application/runtime/activity-waiter.d.ts +33 -0
- package/dist/application/runtime/activity-waiter.js +73 -0
- package/dist/application/runtime/launch-failure.d.ts +2 -0
- package/dist/application/runtime/launch-failure.js +20 -0
- package/dist/application/runtime/process-waiter.d.ts +27 -0
- package/dist/application/runtime/process-waiter.js +73 -0
- package/dist/application/runtime/step-runner.d.ts +50 -0
- package/dist/application/runtime/step-runner.js +429 -0
- package/dist/application/runtime/verify-runtime.d.ts +44 -0
- package/dist/application/runtime/verify-runtime.js +350 -0
- package/dist/application/wait/idle-waiter.d.ts +30 -0
- package/dist/application/wait/idle-waiter.js +90 -0
- package/dist/cli/commands/context.d.ts +3 -0
- package/dist/cli/commands/context.js +94 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +51 -0
- package/dist/cli/commands/generation.d.ts +3 -0
- package/dist/cli/commands/generation.js +518 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.js +104 -0
- package/dist/cli/commands/project.d.ts +3 -0
- package/dist/cli/commands/project.js +58 -0
- package/dist/cli/commands/record.d.ts +3 -0
- package/dist/cli/commands/record.js +88 -0
- package/dist/cli/commands/verify.d.ts +3 -0
- package/dist/cli/commands/verify.js +106 -0
- package/dist/cli/dependencies.d.ts +66 -0
- package/dist/cli/dependencies.js +204 -0
- package/dist/cli/main.d.ts +8 -0
- package/dist/cli/main.js +73 -0
- package/dist/cli/output.d.ts +16 -0
- package/dist/cli/output.js +22 -0
- package/dist/cli/program.d.ts +3 -0
- package/dist/cli/program.js +43 -0
- package/dist/domain/activity.d.ts +7 -0
- package/dist/domain/activity.js +65 -0
- package/dist/domain/app-process.d.ts +7 -0
- package/dist/domain/app-process.js +10 -0
- package/dist/domain/config.d.ts +15 -0
- package/dist/domain/config.js +17 -0
- package/dist/domain/failure.d.ts +4 -0
- package/dist/domain/failure.js +43 -0
- package/dist/domain/generation.d.ts +912 -0
- package/dist/domain/generation.js +342 -0
- package/dist/domain/init.d.ts +42 -0
- package/dist/domain/init.js +114 -0
- package/dist/domain/journey.d.ts +673 -0
- package/dist/domain/journey.js +109 -0
- package/dist/domain/layout.d.ts +35 -0
- package/dist/domain/layout.js +33 -0
- package/dist/domain/locator.d.ts +8 -0
- package/dist/domain/locator.js +5 -0
- package/dist/domain/project-context.d.ts +101 -0
- package/dist/domain/project-context.js +64 -0
- package/dist/domain/proposed-step.d.ts +341 -0
- package/dist/domain/proposed-step.js +87 -0
- package/dist/domain/report.d.ts +349 -0
- package/dist/domain/report.js +135 -0
- package/dist/domain/runtime-snapshot.d.ts +16 -0
- package/dist/domain/runtime-snapshot.js +46 -0
- package/dist/ports/adb.d.ts +42 -0
- package/dist/ports/adb.js +1 -0
- package/dist/ports/android-cli.d.ts +21 -0
- package/dist/ports/android-cli.js +1 -0
- package/dist/ports/artifact-store.d.ts +12 -0
- package/dist/ports/artifact-store.js +1 -0
- package/dist/ports/clock.d.ts +4 -0
- package/dist/ports/clock.js +1 -0
- package/dist/ports/generation-meta-writer.d.ts +10 -0
- package/dist/ports/generation-meta-writer.js +1 -0
- package/dist/ports/generation-prompt.d.ts +19 -0
- package/dist/ports/generation-prompt.js +6 -0
- package/dist/ports/generation-session-store.d.ts +32 -0
- package/dist/ports/generation-session-store.js +26 -0
- package/dist/ports/init-prompt.d.ts +8 -0
- package/dist/ports/init-prompt.js +6 -0
- package/dist/ports/journey-writer.d.ts +10 -0
- package/dist/ports/journey-writer.js +1 -0
- package/dist/ports/process-runner.d.ts +32 -0
- package/dist/ports/process-runner.js +1 -0
- package/dist/ports/project-bound-file.d.ts +8 -0
- package/dist/ports/project-bound-file.js +1 -0
- package/dist/ports/project-file-inspector.d.ts +33 -0
- package/dist/ports/project-file-inspector.js +1 -0
- package/dist/ports/recorder-prompt.d.ts +29 -0
- package/dist/ports/recorder-prompt.js +1 -0
- package/dist/ports/skill-installer.d.ts +8 -0
- package/dist/ports/skill-installer.js +1 -0
- 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
|
+
}
|