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,518 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { GenerationFinalizationError, GenerationOutputPathSchema } from "../../application/generation/generation-finalizer.js";
|
|
5
|
+
import { GenerationOperationError } from "../../application/generation/generation-starter.js";
|
|
6
|
+
import { TapHoundConfigSchema } from "../../domain/config.js";
|
|
7
|
+
import { GenerationSessionIdSchema } from "../../domain/generation.js";
|
|
8
|
+
import { ProjectContextSchema } from "../../domain/project-context.js";
|
|
9
|
+
import { ProposedStepSchema } from "../../domain/proposed-step.js";
|
|
10
|
+
import { RuntimeSnapshotSchema } from "../../domain/runtime-snapshot.js";
|
|
11
|
+
import { GenerationSessionStoreError } from "../../ports/generation-session-store.js";
|
|
12
|
+
import { GenerationPromptCancelledError } from "../../ports/generation-prompt.js";
|
|
13
|
+
import { errorMessage, writeJson, writeLine } from "../output.js";
|
|
14
|
+
const PlannerEnvelopeSchema = z.strictObject({
|
|
15
|
+
version: z.literal(1),
|
|
16
|
+
proposal: ProposedStepSchema,
|
|
17
|
+
snapshot: RuntimeSnapshotSchema
|
|
18
|
+
});
|
|
19
|
+
const ManualActionSchema = z.enum([
|
|
20
|
+
"click",
|
|
21
|
+
"longClick",
|
|
22
|
+
"inputText",
|
|
23
|
+
"swipe",
|
|
24
|
+
"scrollTo",
|
|
25
|
+
"back",
|
|
26
|
+
"wait"
|
|
27
|
+
]);
|
|
28
|
+
function writeFailure(dependencies, options, exitCode, code, error) {
|
|
29
|
+
const output = {
|
|
30
|
+
status: "error",
|
|
31
|
+
exitCode,
|
|
32
|
+
failure: {
|
|
33
|
+
code,
|
|
34
|
+
message: errorMessage(error)
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
if (options.json === true) {
|
|
38
|
+
writeJson(dependencies.stdout, output);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
writeLine(dependencies.stderr, output.failure.message);
|
|
42
|
+
}
|
|
43
|
+
dependencies.setExitCode(exitCode);
|
|
44
|
+
}
|
|
45
|
+
async function loadConfig(dependencies, options) {
|
|
46
|
+
try {
|
|
47
|
+
return TapHoundConfigSchema.parse(await dependencies.readJson(resolve(options.project, options.config)));
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw new GenerationOperationError("CONFIG_INVALID", errorMessage(error));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function tools(checks) {
|
|
54
|
+
return Object.fromEntries(checks.flatMap((check) => (check.status === "passed" && check.version !== undefined
|
|
55
|
+
? [[check.name, check.version]]
|
|
56
|
+
: [])));
|
|
57
|
+
}
|
|
58
|
+
function writeSuccess(dependencies, options, output, message) {
|
|
59
|
+
if (options.json === true) {
|
|
60
|
+
writeJson(dependencies.stdout, output);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
writeLine(dependencies.stdout, message);
|
|
64
|
+
}
|
|
65
|
+
dependencies.setExitCode(0);
|
|
66
|
+
}
|
|
67
|
+
function mappedFailure(dependencies, options, error) {
|
|
68
|
+
if (error instanceof z.ZodError || error instanceof SyntaxError) {
|
|
69
|
+
writeFailure(dependencies, options, 2, "CONTEXT_INVALID", error);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (dependencies.signal?.aborted === true) {
|
|
73
|
+
writeFailure(dependencies, options, 1, "RECOVERY_REQUIRED", "Generation command was cancelled");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (error instanceof GenerationFinalizationError) {
|
|
77
|
+
writeFailure(dependencies, options, 1, error.code, error);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (error instanceof GenerationOperationError) {
|
|
81
|
+
const exitCode = error.code === "CONFIG_INVALID"
|
|
82
|
+
|| error.code === "CONTEXT_INVALID"
|
|
83
|
+
? 2
|
|
84
|
+
: 1;
|
|
85
|
+
writeFailure(dependencies, options, exitCode, error.code, error);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (error instanceof GenerationSessionStoreError) {
|
|
89
|
+
const exitCode = error.code === "INVALID_ID"
|
|
90
|
+
|| error.code === "INVALID_SESSION"
|
|
91
|
+
|| error.code === "INVALID_REVISION"
|
|
92
|
+
? 2
|
|
93
|
+
: error.code === "IO_ERROR" || error.code === "LOCK_TIMEOUT"
|
|
94
|
+
? 4
|
|
95
|
+
: 1;
|
|
96
|
+
writeFailure(dependencies, options, exitCode, error.code, error);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (error instanceof GenerationPromptCancelledError) {
|
|
100
|
+
writeFailure(dependencies, options, 1, "RECOVERY_REQUIRED", error);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (error instanceof Error
|
|
104
|
+
&& error.message.includes("requires local TTY")) {
|
|
105
|
+
writeFailure(dependencies, options, 1, "RISK_CONFIRMATION_REQUIRED", error);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
writeFailure(dependencies, options, 4, "INTERNAL_ERROR", error);
|
|
109
|
+
}
|
|
110
|
+
async function executeApproved(dependencies, options, runtime, input) {
|
|
111
|
+
const result = await runtime.executor.execute({
|
|
112
|
+
...input,
|
|
113
|
+
...(dependencies.signal === undefined
|
|
114
|
+
? {}
|
|
115
|
+
: { signal: dependencies.signal })
|
|
116
|
+
});
|
|
117
|
+
if (result.status !== "succeeded") {
|
|
118
|
+
writeFailure(dependencies, options, 1, result.failure.code, result.failure.message);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const session = await runtime.readSession(input.generationId);
|
|
122
|
+
writeSuccess(dependencies, options, {
|
|
123
|
+
status: "succeeded",
|
|
124
|
+
exitCode: 0,
|
|
125
|
+
generationId: input.generationId,
|
|
126
|
+
revision: session.revision,
|
|
127
|
+
stepIndex: session.candidateSteps.length - 1,
|
|
128
|
+
step: result.step,
|
|
129
|
+
source: input.source
|
|
130
|
+
}, `Generation step ${String(session.candidateSteps.length - 1)} succeeded`);
|
|
131
|
+
}
|
|
132
|
+
function requireRuntime(dependencies, projectRoot, config) {
|
|
133
|
+
if (dependencies.generationRuntime === undefined) {
|
|
134
|
+
throw new Error("Generation command runtime is unavailable");
|
|
135
|
+
}
|
|
136
|
+
return dependencies.generationRuntime({ projectRoot, config });
|
|
137
|
+
}
|
|
138
|
+
async function assertRuntimeConfig(runtime, generationId) {
|
|
139
|
+
await runtime.assertConfigIdentity(generationId);
|
|
140
|
+
}
|
|
141
|
+
function createStartCommand(dependencies) {
|
|
142
|
+
return new Command("start")
|
|
143
|
+
.description("Start a Core-owned generation session")
|
|
144
|
+
.option("--project <path>", "Android project root", dependencies.cwd())
|
|
145
|
+
.option("--config <path>", "TapHound config path", "taphound.config.json")
|
|
146
|
+
.requiredOption("--context <path>", "Project Context path")
|
|
147
|
+
.option("--device <serial>", "Select an online Android device")
|
|
148
|
+
.option("--json", "Emit one machine-readable JSON value")
|
|
149
|
+
.action(async (options) => {
|
|
150
|
+
let config;
|
|
151
|
+
try {
|
|
152
|
+
config = TapHoundConfigSchema.parse(await dependencies.readJson(resolve(options.project, options.config)));
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
writeFailure(dependencies, options, 2, "CONFIG_INVALID", error);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
let context;
|
|
159
|
+
try {
|
|
160
|
+
context = ProjectContextSchema.parse(await dependencies.readJson(resolve(options.project, options.context)));
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
writeFailure(dependencies, options, 2, "CONTEXT_INVALID", error);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const doctor = await dependencies.doctor.run({
|
|
168
|
+
packageName: config.run.packageName,
|
|
169
|
+
...(options.device === undefined
|
|
170
|
+
? {}
|
|
171
|
+
: { requestedDevice: options.device }),
|
|
172
|
+
...(dependencies.signal === undefined
|
|
173
|
+
? {}
|
|
174
|
+
: { signal: dependencies.signal })
|
|
175
|
+
});
|
|
176
|
+
if (doctor.status === "failed") {
|
|
177
|
+
writeFailure(dependencies, options, 3, doctor.failureCode ?? "ENVIRONMENT_MISSING_TOOL", doctor.checks.find((check) => check.status === "failed")?.message
|
|
178
|
+
?? "TapHound environment preflight failed");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const deviceSerial = options.device ?? doctor.deviceSerial;
|
|
182
|
+
if (deviceSerial === undefined) {
|
|
183
|
+
throw new Error("Doctor did not select a device");
|
|
184
|
+
}
|
|
185
|
+
const project = await dependencies.projectDescriber.describe({
|
|
186
|
+
projectRoot: options.project,
|
|
187
|
+
config,
|
|
188
|
+
...(dependencies.signal === undefined
|
|
189
|
+
? {}
|
|
190
|
+
: { signal: dependencies.signal })
|
|
191
|
+
});
|
|
192
|
+
const session = await dependencies.generationStarter.start({
|
|
193
|
+
projectRoot: options.project,
|
|
194
|
+
config,
|
|
195
|
+
context,
|
|
196
|
+
project,
|
|
197
|
+
deviceSerial
|
|
198
|
+
});
|
|
199
|
+
const output = {
|
|
200
|
+
status: "started",
|
|
201
|
+
exitCode: 0,
|
|
202
|
+
generationId: session.id,
|
|
203
|
+
revision: session.revision,
|
|
204
|
+
bindings: session.bindings,
|
|
205
|
+
variables: session.variables,
|
|
206
|
+
target: session.target
|
|
207
|
+
};
|
|
208
|
+
if (options.json === true) {
|
|
209
|
+
writeJson(dependencies.stdout, output);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
writeLine(dependencies.stdout, `Generation started: ${session.id}`);
|
|
213
|
+
}
|
|
214
|
+
dependencies.setExitCode(0);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
if (error instanceof GenerationOperationError) {
|
|
218
|
+
const exitCode = error.code === "CONTEXT_STALE" ? 1 : 2;
|
|
219
|
+
writeFailure(dependencies, options, exitCode, error.code, error);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
mappedFailure(dependencies, options, error);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function createObserveCommand(dependencies) {
|
|
227
|
+
return new Command("observe")
|
|
228
|
+
.description("Observe and bind authoritative runtime state")
|
|
229
|
+
.option("--project <path>", "Android project root", dependencies.cwd())
|
|
230
|
+
.option("--config <path>", "TapHound config path", "taphound.config.json")
|
|
231
|
+
.requiredOption("--session <id>", "Generation session id")
|
|
232
|
+
.option("--json", "Emit one machine-readable JSON value")
|
|
233
|
+
.action(async (options) => {
|
|
234
|
+
try {
|
|
235
|
+
const generationId = GenerationSessionIdSchema.parse(options.session);
|
|
236
|
+
const config = await loadConfig(dependencies, options);
|
|
237
|
+
const observation = dependencies.generationRuntime === undefined
|
|
238
|
+
? await dependencies.runtimeObserver.observe({
|
|
239
|
+
projectRoot: options.project,
|
|
240
|
+
generationId,
|
|
241
|
+
...(dependencies.signal === undefined
|
|
242
|
+
? {}
|
|
243
|
+
: { signal: dependencies.signal })
|
|
244
|
+
})
|
|
245
|
+
: await (async () => {
|
|
246
|
+
const runtime = requireRuntime(dependencies, options.project, config);
|
|
247
|
+
await assertRuntimeConfig(runtime, generationId);
|
|
248
|
+
return runtime.observer.observe({
|
|
249
|
+
generationId,
|
|
250
|
+
...(dependencies.signal === undefined
|
|
251
|
+
? {}
|
|
252
|
+
: { signal: dependencies.signal })
|
|
253
|
+
});
|
|
254
|
+
})();
|
|
255
|
+
const output = {
|
|
256
|
+
status: "observed",
|
|
257
|
+
exitCode: 0,
|
|
258
|
+
...observation.binding,
|
|
259
|
+
snapshot: observation.snapshot
|
|
260
|
+
};
|
|
261
|
+
if (options.json === true) {
|
|
262
|
+
writeJson(dependencies.stdout, output);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
writeLine(dependencies.stdout, `Generation observed at revision ${String(observation.binding.baseRevision)}`);
|
|
266
|
+
}
|
|
267
|
+
dependencies.setExitCode(0);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (error instanceof GenerationOperationError) {
|
|
271
|
+
mappedFailure(dependencies, options, error);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (error instanceof GenerationSessionStoreError
|
|
275
|
+
&& (error.code === "REVISION_CONFLICT"
|
|
276
|
+
|| error.code === "INVALID_TRANSITION")) {
|
|
277
|
+
writeFailure(dependencies, options, 1, "SNAPSHOT_STALE", error);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
mappedFailure(dependencies, options, error);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function addCommonOptions(command, dependencies) {
|
|
285
|
+
return command
|
|
286
|
+
.option("--project <path>", "Android project root", dependencies.cwd())
|
|
287
|
+
.option("--config <path>", "TapHound config path", "taphound.config.json")
|
|
288
|
+
.requiredOption("--session <id>", "Generation session id")
|
|
289
|
+
.option("--json", "Emit one machine-readable JSON value");
|
|
290
|
+
}
|
|
291
|
+
function createStepCommand(dependencies) {
|
|
292
|
+
return addCommonOptions(new Command("step")
|
|
293
|
+
.description("Accept and execute one strict planner proposal")
|
|
294
|
+
.requiredOption("--input <path>", "Strict proposal envelope path"), dependencies).action(async (options) => {
|
|
295
|
+
try {
|
|
296
|
+
const generationId = GenerationSessionIdSchema.parse(options.session);
|
|
297
|
+
const config = await loadConfig(dependencies, options);
|
|
298
|
+
const runtime = requireRuntime(dependencies, options.project, config);
|
|
299
|
+
await assertRuntimeConfig(runtime, generationId);
|
|
300
|
+
let envelope;
|
|
301
|
+
try {
|
|
302
|
+
envelope = PlannerEnvelopeSchema.parse(await dependencies.readJson(resolve(options.project, options.input)));
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
throw new GenerationOperationError("CONTEXT_INVALID", errorMessage(error));
|
|
306
|
+
}
|
|
307
|
+
const confirmation = await runtime.confirmation.request({
|
|
308
|
+
generationId,
|
|
309
|
+
proposal: envelope.proposal,
|
|
310
|
+
snapshot: envelope.snapshot,
|
|
311
|
+
source: "planner"
|
|
312
|
+
});
|
|
313
|
+
if (confirmation.status === "confirmationRequired") {
|
|
314
|
+
const session = await runtime.readSession(generationId);
|
|
315
|
+
writeSuccess(dependencies, options, {
|
|
316
|
+
status: "confirmationRequired",
|
|
317
|
+
exitCode: 0,
|
|
318
|
+
generationId,
|
|
319
|
+
revision: session.revision,
|
|
320
|
+
challenge: confirmation.challenge
|
|
321
|
+
}, `Confirmation required: ${confirmation.challenge.challengeId}`);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
await executeApproved(dependencies, options, runtime, {
|
|
325
|
+
generationId,
|
|
326
|
+
proposal: confirmation.proposal,
|
|
327
|
+
snapshot: envelope.snapshot,
|
|
328
|
+
source: "planner"
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
mappedFailure(dependencies, options, error);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
function createConfirmCommand(dependencies) {
|
|
337
|
+
return addCommonOptions(new Command("confirm")
|
|
338
|
+
.description("Approve and execute Core-owned challenge evidence")
|
|
339
|
+
.requiredOption("--challenge <id>", "Core confirmation challenge id"), dependencies).action(async (options) => {
|
|
340
|
+
try {
|
|
341
|
+
const generationId = GenerationSessionIdSchema.parse(options.session);
|
|
342
|
+
const challengeId = GenerationSessionIdSchema.parse(options.challenge);
|
|
343
|
+
const config = await loadConfig(dependencies, options);
|
|
344
|
+
const runtime = requireRuntime(dependencies, options.project, config);
|
|
345
|
+
await assertRuntimeConfig(runtime, generationId);
|
|
346
|
+
const approved = await runtime.confirmation.confirmStored({
|
|
347
|
+
generationId,
|
|
348
|
+
challengeId,
|
|
349
|
+
...(dependencies.signal === undefined
|
|
350
|
+
? {}
|
|
351
|
+
: { signal: dependencies.signal })
|
|
352
|
+
});
|
|
353
|
+
await executeApproved(dependencies, options, runtime, {
|
|
354
|
+
generationId,
|
|
355
|
+
proposal: approved.proposal,
|
|
356
|
+
snapshot: approved.snapshot,
|
|
357
|
+
source: approved.source
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
mappedFailure(dependencies, options, error);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
function createManualCommand(dependencies) {
|
|
366
|
+
return addCommonOptions(new Command("manual")
|
|
367
|
+
.description("Build one local TTY manual override proposal")
|
|
368
|
+
.requiredOption("--action <action>", "Exact ProposedStep action"), dependencies).action(async (options) => {
|
|
369
|
+
try {
|
|
370
|
+
const generationId = GenerationSessionIdSchema.parse(options.session);
|
|
371
|
+
const action = ManualActionSchema.parse(options.action);
|
|
372
|
+
const config = await loadConfig(dependencies, options);
|
|
373
|
+
const runtime = requireRuntime(dependencies, options.project, config);
|
|
374
|
+
await assertRuntimeConfig(runtime, generationId);
|
|
375
|
+
const existing = await runtime.confirmation.findPendingManual({
|
|
376
|
+
generationId,
|
|
377
|
+
action
|
|
378
|
+
});
|
|
379
|
+
if (existing !== null) {
|
|
380
|
+
const session = await runtime.readSession(generationId);
|
|
381
|
+
writeSuccess(dependencies, options, {
|
|
382
|
+
status: "confirmationRequired",
|
|
383
|
+
exitCode: 0,
|
|
384
|
+
generationId,
|
|
385
|
+
revision: session.revision,
|
|
386
|
+
challenge: existing.challenge
|
|
387
|
+
}, `Confirmation required: ${existing.challenge.challengeId}`);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const observation = await runtime.observer.observe({
|
|
391
|
+
generationId,
|
|
392
|
+
...(dependencies.signal === undefined
|
|
393
|
+
? {}
|
|
394
|
+
: { signal: dependencies.signal })
|
|
395
|
+
});
|
|
396
|
+
const confirmation = await runtime.confirmation.requestManual({
|
|
397
|
+
generationId,
|
|
398
|
+
snapshot: observation.snapshot,
|
|
399
|
+
manual: {
|
|
400
|
+
action,
|
|
401
|
+
binding: observation.binding,
|
|
402
|
+
before: observation.snapshot.activity,
|
|
403
|
+
layout: observation.snapshot.layout
|
|
404
|
+
},
|
|
405
|
+
...(dependencies.signal === undefined
|
|
406
|
+
? {}
|
|
407
|
+
: { signal: dependencies.signal })
|
|
408
|
+
});
|
|
409
|
+
if (confirmation.status === "confirmationRequired") {
|
|
410
|
+
const session = await runtime.readSession(generationId);
|
|
411
|
+
writeSuccess(dependencies, options, {
|
|
412
|
+
status: "confirmationRequired",
|
|
413
|
+
exitCode: 0,
|
|
414
|
+
generationId,
|
|
415
|
+
revision: session.revision,
|
|
416
|
+
challenge: confirmation.challenge
|
|
417
|
+
}, `Confirmation required: ${confirmation.challenge.challengeId}`);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
await executeApproved(dependencies, options, runtime, {
|
|
421
|
+
generationId,
|
|
422
|
+
proposal: confirmation.proposal,
|
|
423
|
+
snapshot: observation.snapshot,
|
|
424
|
+
source: "manualOverride"
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
mappedFailure(dependencies, options, error);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function createFinalizeCommand(dependencies) {
|
|
433
|
+
return addCommonOptions(new Command("finalize")
|
|
434
|
+
.description("Verify and publish a generated Journey")
|
|
435
|
+
.requiredOption("--context <path>", "Project Context path")
|
|
436
|
+
.requiredOption("--output <path>", "Project-relative Journey output")
|
|
437
|
+
.option("--name <name>", "Generated Journey name")
|
|
438
|
+
.option("--device <serial>", "Select an online Android device"), dependencies).action(async (options) => {
|
|
439
|
+
try {
|
|
440
|
+
const generationId = GenerationSessionIdSchema.parse(options.session);
|
|
441
|
+
const config = await loadConfig(dependencies, options);
|
|
442
|
+
let context;
|
|
443
|
+
try {
|
|
444
|
+
context = ProjectContextSchema.parse(await dependencies.readJson(resolve(options.project, options.context)));
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
throw new GenerationOperationError("CONTEXT_INVALID", errorMessage(error));
|
|
448
|
+
}
|
|
449
|
+
const outputPath = GenerationOutputPathSchema.parse(options.output);
|
|
450
|
+
const name = options.name === undefined
|
|
451
|
+
? undefined
|
|
452
|
+
: z.string().trim().min(1).parse(options.name);
|
|
453
|
+
const doctor = await dependencies.doctor.run({
|
|
454
|
+
packageName: config.run.packageName,
|
|
455
|
+
...(options.device === undefined
|
|
456
|
+
? {}
|
|
457
|
+
: { requestedDevice: options.device }),
|
|
458
|
+
...(dependencies.signal === undefined
|
|
459
|
+
? {}
|
|
460
|
+
: { signal: dependencies.signal })
|
|
461
|
+
});
|
|
462
|
+
if (doctor.status === "failed") {
|
|
463
|
+
writeFailure(dependencies, options, 3, doctor.failureCode ?? "ENVIRONMENT_MISSING_TOOL", doctor.checks.find((check) => check.status === "failed")?.message
|
|
464
|
+
?? "TapHound environment preflight failed");
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const deviceSerial = options.device ?? doctor.deviceSerial;
|
|
468
|
+
if (deviceSerial === undefined) {
|
|
469
|
+
writeFailure(dependencies, options, 3, "DEVICE_UNAVAILABLE", "Doctor did not select a device");
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const project = await dependencies.projectDescriber.describe({
|
|
473
|
+
projectRoot: options.project,
|
|
474
|
+
config,
|
|
475
|
+
...(dependencies.signal === undefined
|
|
476
|
+
? {}
|
|
477
|
+
: { signal: dependencies.signal })
|
|
478
|
+
});
|
|
479
|
+
const runtime = requireRuntime(dependencies, options.project, config);
|
|
480
|
+
const result = await runtime.finalizer.finalize({
|
|
481
|
+
generationId,
|
|
482
|
+
projectRoot: options.project,
|
|
483
|
+
config,
|
|
484
|
+
context,
|
|
485
|
+
project,
|
|
486
|
+
outputPath,
|
|
487
|
+
...(name === undefined ? {} : { name }),
|
|
488
|
+
deviceSerial,
|
|
489
|
+
toolVersions: tools(doctor.checks),
|
|
490
|
+
...(dependencies.signal === undefined
|
|
491
|
+
? {}
|
|
492
|
+
: { signal: dependencies.signal })
|
|
493
|
+
});
|
|
494
|
+
writeSuccess(dependencies, options, {
|
|
495
|
+
status: "verified",
|
|
496
|
+
exitCode: 0,
|
|
497
|
+
generationId,
|
|
498
|
+
bundlePath: result.bundlePath,
|
|
499
|
+
journeyPath: result.journeyPath,
|
|
500
|
+
metaPath: result.metaPath,
|
|
501
|
+
replayed: result.replayed
|
|
502
|
+
}, `Generation verified: ${result.journeyPath}`);
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
mappedFailure(dependencies, options, error);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
export function createGenerationCommand(dependencies) {
|
|
510
|
+
return new Command("generation")
|
|
511
|
+
.description("Manage deterministic generation sessions")
|
|
512
|
+
.addCommand(createStartCommand(dependencies))
|
|
513
|
+
.addCommand(createObserveCommand(dependencies))
|
|
514
|
+
.addCommand(createStepCommand(dependencies))
|
|
515
|
+
.addCommand(createConfirmCommand(dependencies))
|
|
516
|
+
.addCommand(createManualCommand(dependencies))
|
|
517
|
+
.addCommand(createFinalizeCommand(dependencies));
|
|
518
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { SkillPayloadMissingError } from "../../adapters/filesystem/skill-installer.js";
|
|
3
|
+
import { NoAgentsSelectedError, UnknownAgentError, parseAgentIds } from "../../domain/init.js";
|
|
4
|
+
import { InitPromptCancelledError } from "../../ports/init-prompt.js";
|
|
5
|
+
import { errorMessage, writeJson, writeLine } from "../output.js";
|
|
6
|
+
function writeInitFailure(dependencies, options, exitCode, code, error) {
|
|
7
|
+
const output = {
|
|
8
|
+
status: "error",
|
|
9
|
+
exitCode,
|
|
10
|
+
failure: {
|
|
11
|
+
code,
|
|
12
|
+
message: errorMessage(error)
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
if (options.json === true) {
|
|
16
|
+
writeJson(dependencies.stdout, output);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
writeLine(dependencies.stderr, output.failure.message);
|
|
20
|
+
}
|
|
21
|
+
dependencies.setExitCode(exitCode);
|
|
22
|
+
}
|
|
23
|
+
function writeInitSuccess(dependencies, options, result) {
|
|
24
|
+
if (options.json === true) {
|
|
25
|
+
writeJson(dependencies.stdout, result);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
const agentList = result.agents.join(", ");
|
|
29
|
+
const pathList = result.paths.join("\n ");
|
|
30
|
+
let message = `Installed TapHound Skill for: ${agentList}\n ${pathList}`;
|
|
31
|
+
if (result.skipped !== undefined && result.skipped.length > 0) {
|
|
32
|
+
message += `\n Skipped (already installed): ${result.skipped.join(", ")}`;
|
|
33
|
+
}
|
|
34
|
+
writeLine(dependencies.stdout, message);
|
|
35
|
+
}
|
|
36
|
+
dependencies.setExitCode(0);
|
|
37
|
+
}
|
|
38
|
+
export function createInitCommand(dependencies) {
|
|
39
|
+
return new Command("init")
|
|
40
|
+
.description("Install the TapHound AI Journey Skill for AI agents")
|
|
41
|
+
.option("--global", "Install to user-level directories instead of project-level")
|
|
42
|
+
.option("--agent <ids>", "Comma-separated agent IDs (non-interactive)")
|
|
43
|
+
.option("--json", "Emit one machine-readable JSON value")
|
|
44
|
+
.action(async (options) => {
|
|
45
|
+
let agents;
|
|
46
|
+
if (options.agent !== undefined) {
|
|
47
|
+
try {
|
|
48
|
+
agents = parseAgentIds(options.agent);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error instanceof UnknownAgentError) {
|
|
52
|
+
writeInitFailure(dependencies, options, 2, "UNKNOWN_AGENT", error);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
writeInitFailure(dependencies, options, 4, "INTERNAL_ERROR", error);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (agents.length === 0) {
|
|
59
|
+
writeInitFailure(dependencies, options, 2, "NO_AGENTS_SELECTED", new NoAgentsSelectedError());
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
try {
|
|
65
|
+
agents = await dependencies.initPrompt.selectAgents();
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error instanceof InitPromptCancelledError) {
|
|
69
|
+
writeInitFailure(dependencies, options, 2, "NO_AGENTS_SELECTED", error);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (error instanceof Error && error.message.includes("TTY")) {
|
|
73
|
+
writeInitFailure(dependencies, options, 2, "NO_AGENTS_SELECTED", new NoAgentsSelectedError());
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
writeInitFailure(dependencies, options, 4, "INTERNAL_ERROR", error);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (agents.length === 0) {
|
|
80
|
+
writeInitFailure(dependencies, options, 2, "NO_AGENTS_SELECTED", new NoAgentsSelectedError());
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const input = {
|
|
85
|
+
agents,
|
|
86
|
+
global: options.global === true
|
|
87
|
+
};
|
|
88
|
+
try {
|
|
89
|
+
const result = await dependencies.init.install(input);
|
|
90
|
+
writeInitSuccess(dependencies, options, result);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error instanceof SkillPayloadMissingError) {
|
|
94
|
+
writeInitFailure(dependencies, options, 2, "SKILL_PAYLOAD_MISSING", error);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (error instanceof NoAgentsSelectedError) {
|
|
98
|
+
writeInitFailure(dependencies, options, 2, "NO_AGENTS_SELECTED", error);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
writeInitFailure(dependencies, options, 4, "INTERNAL_ERROR", error);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { TapHoundConfigSchema } from "../../domain/config.js";
|
|
4
|
+
import { errorMessage, failureOutput, writeJson, writeLine } from "../output.js";
|
|
5
|
+
function writeFailure(dependencies, options, exitCode, error) {
|
|
6
|
+
const output = failureOutput(exitCode, exitCode === 2 ? "CONFIG_INVALID" : "INTERNAL_ERROR", errorMessage(error));
|
|
7
|
+
if (options.json === true) {
|
|
8
|
+
writeJson(dependencies.stdout, output);
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
writeLine(dependencies.stderr, output.failure.message);
|
|
12
|
+
}
|
|
13
|
+
dependencies.setExitCode(exitCode);
|
|
14
|
+
}
|
|
15
|
+
export function createProjectCommand(dependencies) {
|
|
16
|
+
const describe = new Command("describe")
|
|
17
|
+
.description("Describe stable Android project facts")
|
|
18
|
+
.option("--project <path>", "Android project root", dependencies.cwd())
|
|
19
|
+
.option("--config <path>", "TapHound config path", "taphound.config.json")
|
|
20
|
+
.option("--json", "Emit one machine-readable JSON value")
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
let config;
|
|
23
|
+
try {
|
|
24
|
+
config = TapHoundConfigSchema.parse(await dependencies.readJson(resolve(options.project, options.config)));
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
writeFailure(dependencies, options, 2, error);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const description = await dependencies.projectDescriber.describe({
|
|
32
|
+
projectRoot: options.project,
|
|
33
|
+
config,
|
|
34
|
+
...(dependencies.signal === undefined
|
|
35
|
+
? {}
|
|
36
|
+
: { signal: dependencies.signal })
|
|
37
|
+
});
|
|
38
|
+
const output = {
|
|
39
|
+
status: "described",
|
|
40
|
+
exitCode: 0,
|
|
41
|
+
...description
|
|
42
|
+
};
|
|
43
|
+
if (options.json === true) {
|
|
44
|
+
writeJson(dependencies.stdout, output);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
writeLine(dependencies.stdout, `${description.packageName}\nActivity: ${description.launchActivity}`);
|
|
48
|
+
}
|
|
49
|
+
dependencies.setExitCode(0);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
writeFailure(dependencies, options, 4, error);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return new Command("project")
|
|
56
|
+
.description("Inspect stable Android project facts")
|
|
57
|
+
.addCommand(describe);
|
|
58
|
+
}
|