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,120 @@
1
+ import { normalizeActivity } from "../../domain/activity.js";
2
+ import { ProjectContextSchema } from "../../domain/project-context.js";
3
+ export const MAX_CONTEXT_EVIDENCE_BYTES = 1024 * 1024;
4
+ const SECRET_FILE_NAMES = new Set([
5
+ ".git-credentials",
6
+ ".netrc",
7
+ ".npmrc",
8
+ ".pypirc",
9
+ "credentials",
10
+ "id_dsa",
11
+ "id_ecdsa",
12
+ "id_ed25519",
13
+ "id_rsa",
14
+ "keystore.properties",
15
+ "local.properties",
16
+ "secret.json",
17
+ "secrets.json"
18
+ ]);
19
+ const SECRET_FILE_EXTENSIONS = [
20
+ ".der",
21
+ ".jks",
22
+ ".key",
23
+ ".keystore",
24
+ ".p12",
25
+ ".pk8",
26
+ ".pem",
27
+ ".pfx",
28
+ ".ppk"
29
+ ];
30
+ function isSecretEvidencePath(path) {
31
+ const components = path
32
+ .replaceAll("\\", "/")
33
+ .split("/")
34
+ .filter((component) => component.length > 0)
35
+ .map((component) => component.toLowerCase());
36
+ const fileName = components.at(-1) ?? "";
37
+ return (components.some((component) => (component.startsWith(".env")
38
+ || component === ".secrets"
39
+ || component === ".credentials"
40
+ || component === ".ssh"
41
+ || component === ".gnupg"))
42
+ || SECRET_FILE_NAMES.has(fileName)
43
+ || /^credentials(?:\.|$)/.test(fileName)
44
+ || /^service-account(?:\.|$)/.test(fileName)
45
+ || SECRET_FILE_EXTENSIONS.some((extension) => fileName.endsWith(extension)));
46
+ }
47
+ function invalid(code, message) {
48
+ return { status: "invalid", reason: { code, message } };
49
+ }
50
+ function invalidInspection(path, inspection) {
51
+ switch (inspection.status) {
52
+ case "rootNotFound":
53
+ case "rootUnreadable":
54
+ return invalid("PROJECT_ROOT_UNREADABLE", "Project root does not exist or cannot be read");
55
+ case "rootNotDirectory":
56
+ return invalid("PROJECT_ROOT_NOT_DIRECTORY", "Project root is not a directory");
57
+ case "notFound":
58
+ return invalid("EVIDENCE_NOT_FOUND", `Evidence file does not exist: ${path}`);
59
+ case "unreadable":
60
+ return invalid("EVIDENCE_UNREADABLE", `Evidence file cannot be read: ${path}`);
61
+ case "escape":
62
+ return invalid("EVIDENCE_PATH_ESCAPE", `Evidence file resolves outside the project: ${path}`);
63
+ case "changedIdentity":
64
+ return invalid("EVIDENCE_CHANGED_IDENTITY", `Evidence file changed during inspection: ${path}`);
65
+ case "notFile":
66
+ return invalid("EVIDENCE_NOT_FILE", `Evidence path is not a file: ${path}`);
67
+ case "tooLarge":
68
+ return invalid("EVIDENCE_TOO_LARGE", `Evidence file exceeds ${String(MAX_CONTEXT_EVIDENCE_BYTES)} bytes: ${path}`);
69
+ }
70
+ }
71
+ export class ContextValidator {
72
+ files;
73
+ constructor(files) {
74
+ this.files = files;
75
+ }
76
+ validate = async (input) => {
77
+ const parsed = ProjectContextSchema.safeParse(input.context);
78
+ if (!parsed.success) {
79
+ return invalid("CONTEXT_SCHEMA_INVALID", "Project Context does not match the version 1 schema");
80
+ }
81
+ let configuredActivity;
82
+ try {
83
+ configuredActivity = normalizeActivity(input.config.run.packageName, input.config.run.activity);
84
+ }
85
+ catch {
86
+ return invalid("CONTEXT_IDENTITY_MISMATCH", "Project Context identity does not match the configured project");
87
+ }
88
+ if (parsed.data.packageName !== input.config.run.packageName
89
+ || parsed.data.launchActivity !== configuredActivity) {
90
+ return invalid("CONTEXT_IDENTITY_MISMATCH", "Project Context identity does not match the configured project");
91
+ }
92
+ let staleReason;
93
+ for (const evidence of parsed.data.manifest.files) {
94
+ if (isSecretEvidencePath(evidence.path)) {
95
+ return invalid("EVIDENCE_SECRET_PATH", `Secret files cannot be context evidence: ${evidence.path}`);
96
+ }
97
+ const inspection = await this.files.inspectProjectFile({
98
+ projectRoot: input.projectRoot,
99
+ relativePath: evidence.path,
100
+ maximumBytes: MAX_CONTEXT_EVIDENCE_BYTES
101
+ });
102
+ if ("resolvedRelativePath" in inspection
103
+ && isSecretEvidencePath(inspection.resolvedRelativePath)) {
104
+ return invalid("EVIDENCE_SECRET_PATH", `Secret files cannot be context evidence: ${evidence.path}`);
105
+ }
106
+ if (inspection.status !== "inspected") {
107
+ return invalidInspection(evidence.path, inspection);
108
+ }
109
+ if (inspection.sha256 !== evidence.sha256) {
110
+ staleReason ??= {
111
+ code: "EVIDENCE_HASH_MISMATCH",
112
+ message: `Evidence file changed: ${evidence.path}`
113
+ };
114
+ }
115
+ }
116
+ return staleReason === undefined
117
+ ? { status: "valid" }
118
+ : { status: "stale", reason: staleReason };
119
+ };
120
+ }
@@ -0,0 +1,35 @@
1
+ import type { FailureCode } from "../../domain/failure.js";
2
+ import type { AdbPort } from "../../ports/adb.js";
3
+ import type { ProcessRunner } from "../../ports/process-runner.js";
4
+ export type DoctorCheckName = "node" | "adb" | "android" | "app" | "permissions" | "device";
5
+ export interface DoctorCheck {
6
+ name: DoctorCheckName;
7
+ status: "passed" | "failed" | "notRun";
8
+ version?: string | undefined;
9
+ message?: string | undefined;
10
+ }
11
+ export interface DoctorReport {
12
+ status: "passed" | "failed";
13
+ checks: DoctorCheck[];
14
+ deviceSerial?: string | undefined;
15
+ failureCode?: Extract<FailureCode, "ENVIRONMENT_MISSING_TOOL" | "DEVICE_UNAVAILABLE" | "APP_NOT_INSTALLED"> | undefined;
16
+ }
17
+ export interface DoctorRunInput {
18
+ packageName?: string | undefined;
19
+ requestedDevice?: string | undefined;
20
+ signal?: AbortSignal | undefined;
21
+ }
22
+ export interface DoctorDependencies {
23
+ runner: ProcessRunner;
24
+ adb: AdbPort;
25
+ nodeVersion: string;
26
+ checkAndroidPermissions: (deviceSerial: string, signal?: AbortSignal) => Promise<{
27
+ status: "passed" | "failed";
28
+ message?: string | undefined;
29
+ }>;
30
+ }
31
+ export declare class DoctorService {
32
+ private readonly dependencies;
33
+ constructor(dependencies: DoctorDependencies);
34
+ run(input?: DoctorRunInput): Promise<DoctorReport>;
35
+ }
@@ -0,0 +1,178 @@
1
+ function firstLine(value) {
2
+ return value.trim().split(/\r?\n/, 1)[0] ?? "unknown";
3
+ }
4
+ function nodeCheck(version) {
5
+ const normalized = version.replace(/^v/, "");
6
+ const major = Number(normalized.split(".", 1)[0]);
7
+ return Number.isInteger(major) && major >= 22
8
+ ? { name: "node", status: "passed", version: normalized }
9
+ : {
10
+ name: "node",
11
+ status: "failed",
12
+ version: normalized,
13
+ message: "TapHound requires Node.js 22 or newer"
14
+ };
15
+ }
16
+ export class DoctorService {
17
+ dependencies;
18
+ constructor(dependencies) {
19
+ this.dependencies = dependencies;
20
+ }
21
+ async run(input = {}) {
22
+ const { packageName, requestedDevice, signal } = input;
23
+ const checks = [nodeCheck(this.dependencies.nodeVersion)];
24
+ const tool = async (name, executable, args) => {
25
+ try {
26
+ const result = await this.dependencies.runner.run({
27
+ executable,
28
+ args,
29
+ ...(signal === undefined ? {} : { signal })
30
+ });
31
+ if (result.exitCode !== 0
32
+ || result.spawnError !== undefined
33
+ || result.cancelled
34
+ || result.timedOut) {
35
+ return {
36
+ name,
37
+ status: "failed",
38
+ message: result.stderr.trim()
39
+ || result.spawnError
40
+ || `${executable} check failed`
41
+ };
42
+ }
43
+ return {
44
+ name,
45
+ status: "passed",
46
+ version: firstLine(result.stdout)
47
+ };
48
+ }
49
+ catch (error) {
50
+ return {
51
+ name,
52
+ status: "failed",
53
+ message: error instanceof Error ? error.message : String(error)
54
+ };
55
+ }
56
+ };
57
+ checks.push(await tool("adb", "adb", ["version"]), await tool("android", "android", ["--version"]));
58
+ let deviceSerial;
59
+ let deviceCheck;
60
+ try {
61
+ const devices = (await this.dependencies.adb.devices(signal)).filter((device) => device.status === "device");
62
+ const selected = requestedDevice === undefined
63
+ ? (devices.length === 1 ? devices[0] : undefined)
64
+ : devices.find((device) => device.serial === requestedDevice);
65
+ if (selected !== undefined) {
66
+ deviceSerial = selected.serial;
67
+ deviceCheck = {
68
+ name: "device",
69
+ status: "passed",
70
+ message: deviceSerial
71
+ };
72
+ }
73
+ else {
74
+ deviceCheck = {
75
+ name: "device",
76
+ status: "failed",
77
+ message: requestedDevice === undefined
78
+ ? `Expected exactly one online device, found ${String(devices.length)}`
79
+ : `Requested device is not online: ${requestedDevice}`
80
+ };
81
+ }
82
+ }
83
+ catch (error) {
84
+ deviceCheck = {
85
+ name: "device",
86
+ status: "failed",
87
+ message: error instanceof Error ? error.message : String(error)
88
+ };
89
+ }
90
+ if (packageName === undefined) {
91
+ checks.push({
92
+ name: "app",
93
+ status: "notRun",
94
+ message: "Installed application probe requires a configured package"
95
+ });
96
+ }
97
+ else if (deviceSerial === undefined) {
98
+ checks.push({
99
+ name: "app",
100
+ status: "notRun",
101
+ message: "Installed application probe requires an online selected device"
102
+ });
103
+ }
104
+ else {
105
+ try {
106
+ const installed = await this.dependencies.adb.isInstalled({
107
+ packageName,
108
+ deviceSerial,
109
+ ...(signal === undefined ? {} : { signal })
110
+ });
111
+ checks.push(installed
112
+ ? { name: "app", status: "passed", message: packageName }
113
+ : {
114
+ name: "app",
115
+ status: "failed",
116
+ message: `Package ${packageName} is not installed on ${deviceSerial}`
117
+ });
118
+ }
119
+ catch (error) {
120
+ checks.push({
121
+ name: "app",
122
+ status: "failed",
123
+ message: error instanceof Error ? error.message : String(error)
124
+ });
125
+ }
126
+ }
127
+ if (deviceSerial === undefined) {
128
+ checks.push({
129
+ name: "permissions",
130
+ status: "notRun",
131
+ message: "Permission probe requires an online selected device"
132
+ });
133
+ }
134
+ else {
135
+ try {
136
+ const permission = await this.dependencies.checkAndroidPermissions(deviceSerial, signal);
137
+ checks.push({
138
+ name: "permissions",
139
+ status: permission.status,
140
+ ...(permission.message === undefined
141
+ ? {}
142
+ : { message: permission.message })
143
+ });
144
+ }
145
+ catch (error) {
146
+ checks.push({
147
+ name: "permissions",
148
+ status: "failed",
149
+ message: error instanceof Error ? error.message : String(error)
150
+ });
151
+ }
152
+ }
153
+ checks.push(deviceCheck);
154
+ const failedCheck = (name) => checks.some((check) => check.name === name && check.status === "failed");
155
+ const environmentFailed = ["node", "adb", "android", "permissions"]
156
+ .some(failedCheck);
157
+ const failureCode = environmentFailed
158
+ ? "ENVIRONMENT_MISSING_TOOL"
159
+ : failedCheck("device")
160
+ ? "DEVICE_UNAVAILABLE"
161
+ : failedCheck("app")
162
+ ? "APP_NOT_INSTALLED"
163
+ : undefined;
164
+ if (failureCode !== undefined) {
165
+ return {
166
+ status: "failed",
167
+ checks,
168
+ ...(deviceSerial === undefined ? {} : { deviceSerial }),
169
+ failureCode
170
+ };
171
+ }
172
+ return {
173
+ status: "passed",
174
+ checks,
175
+ ...(deviceSerial === undefined ? {} : { deviceSerial })
176
+ };
177
+ }
178
+ }
@@ -0,0 +1,2 @@
1
+ import type { LayoutElement } from "../../domain/layout.js";
2
+ export declare function hasExactlyOneEnabledFocusedElement(layout: readonly LayoutElement[]): boolean;
@@ -0,0 +1,9 @@
1
+ function flatten(elements) {
2
+ return elements.flatMap((element) => [
3
+ element,
4
+ ...flatten(element.children)
5
+ ]);
6
+ }
7
+ export function hasExactlyOneEnabledFocusedElement(layout) {
8
+ return flatten(layout).filter((element) => element.enabled && element.focused === true).length === 1;
9
+ }
@@ -0,0 +1,76 @@
1
+ import { type PendingConfirmation } from "../../domain/generation.js";
2
+ import { type ProposedStep } from "../../domain/proposed-step.js";
3
+ import { type RuntimeSnapshot } from "../../domain/runtime-snapshot.js";
4
+ import { type GenerationPromptPort, type ManualProposalInput } from "../../ports/generation-prompt.js";
5
+ import type { GenerationSessionStore } from "../../ports/generation-session-store.js";
6
+ export interface GenerationConfirmationDependencies {
7
+ store: Pick<GenerationSessionStore, "read" | "updateConfirmation" | "writeEvidence" | "readEvidence">;
8
+ prompt: Pick<GenerationPromptPort, "confirm" | "buildManualProposal">;
9
+ now: () => Date;
10
+ generateChallengeId: () => string;
11
+ confirmationTtlMs: number;
12
+ }
13
+ export interface ConfirmationRequestInput {
14
+ generationId: string;
15
+ proposal: ProposedStep;
16
+ snapshot: RuntimeSnapshot;
17
+ source?: "planner" | "manualOverride" | undefined;
18
+ }
19
+ export interface ConfirmationApproveInput extends ConfirmationRequestInput {
20
+ challengeId: string;
21
+ signal?: AbortSignal | undefined;
22
+ }
23
+ export interface StoredConfirmationApproveInput {
24
+ generationId: string;
25
+ challengeId: string;
26
+ signal?: AbortSignal | undefined;
27
+ }
28
+ export interface ManualConfirmationRequestInput {
29
+ generationId: string;
30
+ snapshot: RuntimeSnapshot;
31
+ manual: ManualProposalInput;
32
+ signal?: AbortSignal | undefined;
33
+ }
34
+ export interface PendingManualConfirmationInput {
35
+ generationId: string;
36
+ action: ManualProposalInput["action"];
37
+ }
38
+ export interface ConfirmationRequiredResult {
39
+ status: "confirmationRequired";
40
+ challenge: PendingConfirmation;
41
+ }
42
+ export type ConfirmationRequestResult = {
43
+ status: "approved";
44
+ proposal: ProposedStep;
45
+ } | ConfirmationRequiredResult;
46
+ export interface StoredConfirmationApproveResult {
47
+ status: "approved";
48
+ proposal: ProposedStep;
49
+ snapshot: RuntimeSnapshot;
50
+ source: "planner" | "manualOverride";
51
+ }
52
+ export declare function summarizeProposedStep(step: ProposedStep): string;
53
+ export declare function confirmationEvidencePath(challengeId: string): string;
54
+ export declare class GenerationConfirmationService {
55
+ private readonly dependencies;
56
+ private readonly validator;
57
+ private readonly riskEvaluator;
58
+ constructor(dependencies: GenerationConfirmationDependencies);
59
+ readonly request: (input: ConfirmationRequestInput) => Promise<ConfirmationRequestResult>;
60
+ readonly requestManual: (input: ManualConfirmationRequestInput) => Promise<ConfirmationRequestResult>;
61
+ readonly findPendingManual: (input: PendingManualConfirmationInput) => Promise<ConfirmationRequiredResult | null>;
62
+ readonly confirmStored: (input: StoredConfirmationApproveInput) => Promise<StoredConfirmationApproveResult>;
63
+ readonly confirm: (input: ConfirmationApproveInput) => Promise<{
64
+ status: "approved";
65
+ proposal: ProposedStep;
66
+ }>;
67
+ readonly clearApproved: (input: {
68
+ generationId: string;
69
+ challenge: PendingConfirmation;
70
+ }) => Promise<void>;
71
+ private clearExactChallenge;
72
+ private cancelPrompt;
73
+ private requireExactPendingChallenge;
74
+ private updateConfirmationReconciled;
75
+ private readStoredEvidence;
76
+ }