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,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,3 @@
1
+ import { Command } from "commander";
2
+ import type { CliDependencies } from "../dependencies.js";
3
+ export declare function createInitCommand(dependencies: CliDependencies): Command;
@@ -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,3 @@
1
+ import { Command } from "commander";
2
+ import type { CliDependencies } from "../dependencies.js";
3
+ export declare function createProjectCommand(dependencies: CliDependencies): Command;
@@ -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
+ }