@nextclaw/x-harness 0.0.0-stage → 0.1.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 (110) hide show
  1. package/LICENSE +10 -0
  2. package/README.md +41 -2
  3. package/cli/main.js +219 -0
  4. package/dist/api.d.ts +10 -0
  5. package/dist/api.js +308 -0
  6. package/dist/boundary.d.ts +2 -0
  7. package/dist/browser-common.d.ts +4 -0
  8. package/dist/browser-common.js +29 -0
  9. package/dist/browser-dom-capabilities.d.ts +4 -0
  10. package/dist/browser-dom-capabilities.js +110 -0
  11. package/dist/browser-dom-dependencies.d.ts +16 -0
  12. package/dist/browser-dom-editing.d.ts +148 -0
  13. package/dist/browser-dom-editing.js +386 -0
  14. package/dist/browser-dom-navigation.d.ts +9 -0
  15. package/dist/browser-dom-navigation.js +165 -0
  16. package/dist/browser-dom-network.d.ts +1 -0
  17. package/dist/browser-dom-network.js +128 -0
  18. package/dist/browser-dom-submission.d.ts +2 -0
  19. package/dist/browser-dom-submission.js +82 -0
  20. package/dist/browser-dom.d.ts +2 -0
  21. package/dist/browser-dom.js +103 -0
  22. package/dist/browser-native.d.ts +1 -0
  23. package/dist/browser-native.js +215 -0
  24. package/dist/browser.d.ts +4 -0
  25. package/dist/browser.js +147 -0
  26. package/dist/creator.d.ts +29 -0
  27. package/dist/creator.js +164 -0
  28. package/dist/growth-batch.d.ts +5 -0
  29. package/dist/growth-batch.js +168 -0
  30. package/dist/growth-constants.d.ts +5 -0
  31. package/dist/growth-constants.js +13 -0
  32. package/dist/growth-dependencies.d.ts +33 -0
  33. package/dist/growth-execution.d.ts +11 -0
  34. package/dist/growth-execution.js +315 -0
  35. package/dist/growth-parallel.d.ts +4 -0
  36. package/dist/growth-parallel.js +301 -0
  37. package/dist/growth-plan.d.ts +4 -0
  38. package/dist/growth-plan.js +207 -0
  39. package/dist/growth-policy.d.ts +9 -0
  40. package/dist/growth-policy.js +181 -0
  41. package/dist/growth-publishing.d.ts +5 -0
  42. package/dist/growth-publishing.js +240 -0
  43. package/dist/growth-reading.d.ts +4 -0
  44. package/dist/growth-reading.js +135 -0
  45. package/dist/growth-recovery.d.ts +6 -0
  46. package/dist/growth-recovery.js +158 -0
  47. package/dist/growth-state.d.ts +8 -0
  48. package/dist/growth-state.js +183 -0
  49. package/dist/growth-tasks.d.ts +17 -0
  50. package/dist/growth-tasks.js +101 -0
  51. package/dist/growth.d.ts +2 -0
  52. package/dist/growth.js +138 -0
  53. package/dist/index.d.ts +4 -0
  54. package/dist/index.js +22 -0
  55. package/dist/observation.d.ts +1 -0
  56. package/dist/observation.js +2 -0
  57. package/dist/platform-capabilities.d.ts +4 -0
  58. package/dist/platform-capabilities.js +19 -0
  59. package/dist/platform-collections.d.ts +7 -0
  60. package/dist/platform-collections.js +223 -0
  61. package/dist/platform-dependencies.d.ts +43 -0
  62. package/dist/platform-diagnostics.d.ts +9 -0
  63. package/dist/platform-diagnostics.js +51 -0
  64. package/dist/platform-observation.d.ts +8 -0
  65. package/dist/platform-observation.js +75 -0
  66. package/dist/platform-rules.d.ts +17 -0
  67. package/dist/platform-rules.js +41 -0
  68. package/dist/platform-targets.d.ts +7 -0
  69. package/dist/platform-targets.js +129 -0
  70. package/dist/platform-writing.d.ts +8 -0
  71. package/dist/platform-writing.js +171 -0
  72. package/dist/platform.d.ts +2 -0
  73. package/dist/platform.js +60 -0
  74. package/dist/readers-actions.d.ts +2 -0
  75. package/dist/readers-actions.js +66 -0
  76. package/dist/readers-ax-host.d.ts +2 -0
  77. package/dist/readers-ax-host.js +125 -0
  78. package/dist/readers-ax-parser.d.ts +1 -0
  79. package/dist/readers-ax-parser.js +69 -0
  80. package/dist/readers-ax-program.d.ts +1 -0
  81. package/dist/readers-ax-tree.d.ts +19 -0
  82. package/dist/readers-ax-tree.js +110 -0
  83. package/dist/readers-ax-views.d.ts +87 -0
  84. package/dist/readers-ax-views.js +247 -0
  85. package/dist/readers-cli.d.ts +6 -0
  86. package/dist/readers-composer.d.ts +2 -0
  87. package/dist/readers-composer.js +131 -0
  88. package/dist/readers-navigation.d.ts +2 -0
  89. package/dist/readers-navigation.js +66 -0
  90. package/dist/readers-posts.d.ts +2 -0
  91. package/dist/readers-posts.js +141 -0
  92. package/dist/readers-profile.d.ts +3 -0
  93. package/dist/readers-profile.js +157 -0
  94. package/dist/readers.d.ts +9 -0
  95. package/dist/results.d.ts +6 -0
  96. package/dist/results.js +233 -0
  97. package/dist/round-state.d.ts +3 -0
  98. package/dist/round-state.js +155 -0
  99. package/dist/store.d.ts +30 -0
  100. package/dist/store.js +175 -0
  101. package/dist/types.d.ts +407 -0
  102. package/generated/observation.d.mts +1 -0
  103. package/generated/observation.mjs +515 -0
  104. package/package.json +29 -3
  105. package/skill/SKILL.md +33 -0
  106. package/skill/references/api.md +10 -0
  107. package/skill/references/reads.md +23 -0
  108. package/skill/references/recovery.md +25 -0
  109. package/skill/references/setup.md +23 -0
  110. package/skill/references/writes.md +51 -0
package/LICENSE ADDED
@@ -0,0 +1,10 @@
1
+ X Harness — Proprietary Free-Use License
2
+ Copyright (c) 2026 Wang Pei. All rights reserved.
3
+
4
+ Permission is granted, without charge, to install and use the distributed runtime and accompanying Skill for personal and commercial work. You may make backup copies and modify the Skill text for your own use.
5
+
6
+ This is not an open-source license. Redistribution, resale, sublicensing, publishing modified runtime versions, or use of the runtime to provide a competing hosted product requires prior written permission from the copyright holder. No rights to the private TypeScript source repository are granted. Do not remove copyright or license notices.
7
+
8
+ The software is provided "as is", without warranty. To the extent permitted by applicable law, the copyright holder is not liable for damages arising from its use. You are responsible for your account, permissions, content and compliance with the services you use. This license does not grant access to any platform, account or private data.
9
+
10
+ Contact: https://x.com/XiaotiaoWang
package/README.md CHANGED
@@ -1,3 +1,42 @@
1
- # Temporary Holding Version
1
+ # X Harness
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Named X task APIs for browser-enabled AI agents. A reading task returns the relevant complete text and evidence; a write task records intent, submits once, verifies the public result and settles the user's ledger. Default receipts are compact; `decision` keeps complete target evidence; `delta` only returns changed evidence.
4
+
5
+ Website: https://x-harness.gemigo.app/
6
+
7
+ ## Install and update
8
+
9
+ ```sh
10
+ npx @nextclaw/x-harness@latest setup --target codex
11
+ npx @nextclaw/x-harness@latest update --target codex
12
+ ```
13
+
14
+ Requires Node.js 22+ and a compatible browser-enabled AI host. Codex desktop with CUA is the validated host. `--target claude` installs Skill text into Claude's Skill directory; the Claude host must separately provide compatible CUA tab/app methods. Installing text alone does not add browser control.
15
+
16
+ The runtime lives in `~/.x-harness/runtime`; the Skill is managed in the selected host's Skill directory. Updates preserve your independent workspaces, back up the previous runtime and Skill, and refuse to overwrite unrecognized directories. Bind the new moduleURL reported by update; its versioned path refreshes the complete module graph, including in a persistent REPL. The stable CLI entry remains usable.
17
+
18
+ Initialize your own workspace (use the stable CLI path reported by setup):
19
+
20
+ ```sh
21
+ node ~/.x-harness/runtime/node_modules/@nextclaw/x-harness/cli/main.js init --directory ./x-workspace --account your_handle
22
+ ```
23
+
24
+ Setup prints the absolute module URL and CLI path. Ask your agent to use the installed `x-harness` Skill. Your agent binds a real logged-in CUA tab once, then uses `createXGrowthHarness({ tab, ledgerPath, proofDirectory })`. It does not use your publisher's account or ledger.
25
+
26
+ ```js
27
+ const { createXGrowthHarness } = await import(moduleURL);
28
+ const x = createXGrowthHarness({ tab, ledgerPath, proofDirectory });
29
+ const result = await x.readCreator({
30
+ handle: "your_handle",
31
+ target: 5,
32
+ mode: "decision",
33
+ });
34
+ ```
35
+
36
+ Read APIs: `readCreator`, `readBookmarks`, `searchSources`, `readPost`, `readPerson`, `readFeedback`, `inspectAccount`, `inspectRound`.
37
+ Write APIs: `publishPost`, `replyToPost`, `followOne`, `followBatch`, `runGrowthRound`.
38
+ Recovery: `verifySubmission`, `verifyPublishedPost`, `verifyFollow`.
39
+
40
+ Writing requires the user's explicit task authorization, reviewed prepared actions and their own ledger plan. Initialization enables no planned writes. Current runtime windows (Asia/Shanghai): originals/follows 07:00–22:00; replies 07:00–24:00. Follow any stricter original project contract; follow quality gates currently require reviewed Chinese-language, blue-check candidates. Read tasks do not require that candidate policy. Missing browser capability, login or decision evidence is returned explicitly. Unknown submissions are verified before any further submission.
41
+
42
+ Free to use under the included proprietary license. The distributed JavaScript is readable; the private TypeScript repository is not published. This is not an official X product. See `LICENSE` for permitted use.
package/cli/main.js ADDED
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync, renameSync, rmSync, cpSync, mkdtempSync, openSync, closeSync, unlinkSync, readdirSync, } from "node:fs";
3
+ import { join, resolve, dirname, relative } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { randomUUID } from "node:crypto";
7
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
8
+ const release = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
9
+ const args = process.argv.slice(2);
10
+ const command = args.shift() ?? "help";
11
+ const flags = new Map();
12
+ for (let i = 0; i < args.length; i += 2) {
13
+ const key = args[i], value = args[i + 1];
14
+ if (!["--target", "--home", "--directory", "--account"].includes(key) ||
15
+ !value ||
16
+ value.startsWith("--") ||
17
+ flags.has(key))
18
+ throw Error(`Invalid argument: ${key}`);
19
+ flags.set(key, value);
20
+ }
21
+ const customHome = flags.get("--home");
22
+ const home = resolve(customHome ?? join(homedir(), ".x-harness"));
23
+ const readJSON = (path) => JSON.parse(readFileSync(path, "utf8"));
24
+ const output = (value) => process.stdout.write(JSON.stringify(value, null, 2) + "\n");
25
+ const marker = (path, file) => existsSync(join(path, file)) &&
26
+ [release.name, "@peiiii/x-harness"].includes(readJSON(join(path, file)).managedBy);
27
+ function guard(path, file) {
28
+ if (existsSync(path) &&
29
+ (lstatSync(path).isSymbolicLink() || !marker(path, file)))
30
+ throw Error(`Refusing to overwrite unmanaged directory: ${path}`);
31
+ }
32
+ function skillPath(target) {
33
+ if (!["codex", "claude"].includes(target))
34
+ throw Error("Target must be codex or claude");
35
+ return customHome
36
+ ? join(home, "skills", target, "x-harness")
37
+ : join(homedir(), `.${target}`, "skills", "x-harness");
38
+ }
39
+ function location() {
40
+ return {
41
+ version: release.version,
42
+ moduleURL: pathToFileURL(join(packageRoot, "dist", "index.js")).href,
43
+ cliPath: fileURLToPath(import.meta.url),
44
+ home,
45
+ };
46
+ }
47
+ function stageRuntime(stage, runtime) {
48
+ const stagedRuntime = join(stage, "runtime");
49
+ const suffix = join("versions", release.version, "node_modules", ...release.name.split("/"));
50
+ const installedRoot = join(stagedRuntime, suffix);
51
+ cpSync(packageRoot, installedRoot, { recursive: true });
52
+ if (readJSON(join(installedRoot, "package.json")).version !== release.version)
53
+ throw Error("Installed version mismatch");
54
+ const finalRoot = join(runtime, suffix);
55
+ const stableRoot = join(stagedRuntime, "node_modules", ...release.name.split("/"));
56
+ const stableCLI = join(runtime, "node_modules", ...release.name.split("/"), "cli", "main.js");
57
+ mkdirSync(join(stableRoot, "cli"), { recursive: true });
58
+ writeFileSync(join(stableRoot, "package.json"), JSON.stringify({ type: "module" }));
59
+ const entry = relative(dirname(stableCLI), join(finalRoot, "cli", "main.js"))
60
+ .split("\\")
61
+ .join("/");
62
+ writeFileSync(join(stableRoot, "cli", "main.js"), `#!/usr/bin/env node\nawait import(new URL(${JSON.stringify(entry)}, import.meta.url));\n`);
63
+ return { stagedRuntime, installedRoot, finalRoot, stableCLI };
64
+ }
65
+ function setup() {
66
+ const target = flags.get("--target") ?? "codex";
67
+ const stateFile = join(home, "installation.json");
68
+ if (!existsSync(stateFile) &&
69
+ readdirSync(home).some((name) => name !== ".installation.lock"))
70
+ throw Error(`Unmanaged installation home: ${home}`);
71
+ const previous = existsSync(stateFile)
72
+ ? readJSON(stateFile)
73
+ : { managedBy: release.name, targets: [] };
74
+ if (![release.name, "@peiiii/x-harness"].includes(previous.managedBy))
75
+ throw Error("Unmanaged installation state");
76
+ const targets = [...new Set([...previous.targets, target])];
77
+ const runtime = join(home, "runtime");
78
+ guard(runtime, ".x-harness-managed.json");
79
+ for (const t of targets)
80
+ guard(skillPath(t), ".x-harness-managed.json");
81
+ mkdirSync(home, { recursive: true, mode: 0o700 });
82
+ const stage = mkdtempSync(join(home, ".setup-"));
83
+ const backup = join(home, "backups", `${Date.now()}-${randomUUID()}`);
84
+ const changes = [];
85
+ let completed = false;
86
+ try {
87
+ // Persist npx's already fetched runtime once; versioned URLs invalidate the whole module graph.
88
+ const { stagedRuntime, installedRoot, finalRoot, stableCLI } = stageRuntime(stage, runtime);
89
+ const metadata = { managedBy: release.name, version: release.version };
90
+ writeFileSync(join(stagedRuntime, ".x-harness-managed.json"), JSON.stringify(metadata));
91
+ const installs = [{ src: stagedRuntime, dest: runtime, key: "runtime" }];
92
+ for (const t of targets) {
93
+ const stagedSkill = join(stage, `skill-${t}`);
94
+ cpSync(join(installedRoot, "skill"), stagedSkill, { recursive: true });
95
+ const content = readFileSync(join(stagedSkill, "SKILL.md"), "utf8")
96
+ .replaceAll("{{MODULE_URL}}", pathToFileURL(join(finalRoot, "dist", "index.js")).href)
97
+ .replaceAll("{{CLI_PATH}}", stableCLI);
98
+ writeFileSync(join(stagedSkill, "SKILL.md"), content);
99
+ writeFileSync(join(stagedSkill, ".x-harness-managed.json"), JSON.stringify(metadata));
100
+ installs.push({
101
+ src: stagedSkill,
102
+ dest: skillPath(t),
103
+ key: `skill-${t}`,
104
+ });
105
+ }
106
+ // Install/download succeeds before replacing either managed artifact.
107
+ for (const item of installs) {
108
+ const saved = join(backup, item.key), hadOld = existsSync(item.dest);
109
+ mkdirSync(dirname(item.dest), { recursive: true });
110
+ if (hadOld) {
111
+ mkdirSync(backup, { recursive: true });
112
+ renameSync(item.dest, saved);
113
+ }
114
+ changes.push({ dest: item.dest, backup: saved, hadOld });
115
+ renameSync(item.src, item.dest);
116
+ }
117
+ const state = { ...metadata, targets, updatedAt: new Date().toISOString() };
118
+ const nextState = join(stage, "installation.json");
119
+ writeFileSync(nextState, JSON.stringify(state, null, 2));
120
+ renameSync(nextState, stateFile);
121
+ completed = true;
122
+ output({
123
+ status: "installed",
124
+ ...state,
125
+ moduleURL: pathToFileURL(join(finalRoot, "dist", "index.js")).href,
126
+ cliPath: stableCLI,
127
+ skillPaths: targets.map(skillPath),
128
+ backup: changes.some((c) => c.hadOld) ? backup : null,
129
+ next: "Start a fresh module binding using the installed x-harness Skill.",
130
+ });
131
+ }
132
+ finally {
133
+ if (!completed)
134
+ for (const change of changes.reverse()) {
135
+ rmSync(change.dest, { recursive: true, force: true });
136
+ if (change.hadOld)
137
+ renameSync(change.backup, change.dest);
138
+ }
139
+ rmSync(stage, { recursive: true, force: true });
140
+ }
141
+ }
142
+ function init() {
143
+ const directory = resolve(flags.get("--directory") ?? "./x-workspace");
144
+ const account = (flags.get("--account") ?? "").replace(/^@/, "");
145
+ if (!/^[a-zA-Z0-9_]{1,15}$/.test(account))
146
+ throw Error("Provide your own X handle with --account");
147
+ const configPath = join(directory, "x-harness.json");
148
+ if (existsSync(configPath)) {
149
+ const config = readJSON(configPath);
150
+ if (config.accountHandle !== account)
151
+ throw Error("Existing workspace belongs to a different account");
152
+ output({ status: "existing", configPath, ...config });
153
+ return;
154
+ }
155
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
156
+ const ledgerPath = join(directory, "round.md"), proofDirectory = join(directory, "proofs");
157
+ if (existsSync(ledgerPath))
158
+ throw Error("Existing ledger is unmanaged; use its configuration instead of overwriting");
159
+ const taskId = `x-${randomUUID()}`;
160
+ const state = {
161
+ batchId: taskId,
162
+ status: "RUNNING",
163
+ updatedAt: new Date().toISOString(),
164
+ accountHandle: account,
165
+ items: ["follow", "original", "reply"].map((kind) => ({
166
+ kind,
167
+ planned: 0,
168
+ verified: 0,
169
+ unknown: 0,
170
+ actionable: true,
171
+ })),
172
+ execution: { owner: "harness", exclusive: true },
173
+ attempts: [],
174
+ actions: [],
175
+ };
176
+ mkdirSync(proofDirectory);
177
+ writeFileSync(ledgerPath, `# @${account} · Your X workspace\n\n\`\`\`x-round-state\n${JSON.stringify(state, null, 2)}\n\`\`\`\n`, { flag: "wx", mode: 0o600 });
178
+ const config = { accountHandle: account, taskId, ledgerPath, proofDirectory };
179
+ writeFileSync(configPath, JSON.stringify(config, null, 2), {
180
+ flag: "wx",
181
+ mode: 0o600,
182
+ });
183
+ output({ status: "initialized", configPath, ...config });
184
+ }
185
+ try {
186
+ if (["setup", "update"].includes(command)) {
187
+ mkdirSync(home, { recursive: true, mode: 0o700 });
188
+ const lockPath = join(home, ".installation.lock");
189
+ const lock = openSync(lockPath, "wx", 0o600);
190
+ try {
191
+ setup();
192
+ }
193
+ finally {
194
+ closeSync(lock);
195
+ unlinkSync(lockPath);
196
+ }
197
+ }
198
+ else if (command === "init")
199
+ init();
200
+ else if (command === "location")
201
+ output(location());
202
+ else if (command === "help")
203
+ output({
204
+ setup: "npx @nextclaw/x-harness@latest setup --target codex",
205
+ update: "npx @nextclaw/x-harness@latest update --target codex",
206
+ init: "node <cliPath> init --directory ./x-workspace --account YOUR_HANDLE",
207
+ targets: ["codex", "claude"],
208
+ requires: "Node 22+; compatible CUA browser host and your own X login",
209
+ });
210
+ else
211
+ throw Error(`Unknown command: ${command}`);
212
+ }
213
+ catch (error) {
214
+ process.stderr.write(JSON.stringify({
215
+ status: "failed",
216
+ reason: error instanceof Error ? error.message : String(error),
217
+ }) + "\n");
218
+ process.exitCode = 1;
219
+ }
package/dist/api.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { XGrowthHarness } from "./types.js";
2
+ import type { BoundaryRecord } from "./boundary.js";
3
+ /** Internal dependencies; only the normal X factory is exported from the root. */
4
+ export interface BusinessDependencies {
5
+ platform: BoundaryRecord;
6
+ store: import("./store.js").RoundStore;
7
+ clock?: () => number;
8
+ executor?: BoundaryRecord;
9
+ }
10
+ export declare function createBusinessHarness(dependencies: BusinessDependencies): XGrowthHarness;
package/dist/api.js ADDED
@@ -0,0 +1,308 @@
1
+ import { createGrowthHarness } from "./growth.js";
2
+ import { readResult, decisions, diagnostic, stableReason } from "./results.js";
3
+ export function createBusinessHarness(dependencies) {
4
+ const { store } = dependencies;
5
+ const core = createGrowthHarness(dependencies);
6
+ const readCache = new Map();
7
+ // All public operations share the same error boundary. Full errors remain local proof.
8
+ async function invoke(operation, args) {
9
+ try {
10
+ const raw = await core[operation](args);
11
+ // Legacy core decisions can contain a human error without a diagnostic proof.
12
+ // Expose a bounded recovery route; consumers never need implementation source.
13
+ const errors = [
14
+ raw.reason,
15
+ ...(raw.decision ?? []).flatMap((d) => [
16
+ d.reason,
17
+ d.error,
18
+ ]),
19
+ ].filter((reason) => typeof reason === "string" && stableReason(reason) !== reason);
20
+ if (errors.length) {
21
+ const evidenceRef = await store.recordProof({
22
+ operation,
23
+ error: errors[0],
24
+ ...(errors.length > 1 ? { errors } : {}),
25
+ });
26
+ return {
27
+ ...raw,
28
+ ...(raw.reason ? { reason: stableReason(raw.reason) } : {}),
29
+ evidenceRef,
30
+ ...(raw.decision
31
+ ? {
32
+ decision: raw.decision.map((d) => ({
33
+ ...d,
34
+ reason: stableReason(d.reason),
35
+ })),
36
+ }
37
+ : {}),
38
+ diagnostic: {
39
+ operation,
40
+ category: "boundary",
41
+ code: "harness-boundary-error",
42
+ next: "inspect-local-proof",
43
+ evidenceRef,
44
+ },
45
+ };
46
+ }
47
+ return raw;
48
+ }
49
+ catch (error) {
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ let evidenceRef;
52
+ try {
53
+ evidenceRef = await store.recordProof({ operation, error: message });
54
+ }
55
+ catch {
56
+ /* Evidence failure is explicit below. */
57
+ }
58
+ return {
59
+ status: "needsDecision",
60
+ boundaryFailed: true,
61
+ fresh: false,
62
+ complete: false,
63
+ reason: evidenceRef
64
+ ? stableReason(message)
65
+ : "evidence-store-unavailable",
66
+ ...(evidenceRef
67
+ ? { evidenceRef }
68
+ : { missing: ["local-proof-unavailable"] }),
69
+ diagnostic: {
70
+ operation,
71
+ category: "boundary",
72
+ code: evidenceRef
73
+ ? "harness-boundary-error"
74
+ : "evidence-store-unavailable",
75
+ next: evidenceRef ? "inspect-local-proof" : "restore-proof-store",
76
+ ...(evidenceRef ? { evidenceRef } : {}),
77
+ },
78
+ };
79
+ }
80
+ }
81
+ async function read(operation, args = {}, mode = "receipt") {
82
+ if (!["receipt", "decision", "delta"].includes(mode))
83
+ return {
84
+ status: "needsDecision",
85
+ complete: false,
86
+ reason: "read-mode-invalid",
87
+ };
88
+ // Always collect fresh evidence once. Projection, not a second browser call, chooses output.
89
+ const raw = await invoke(operation, { ...args, mode: "decision" });
90
+ const output = readResult(operation, raw, mode);
91
+ if (mode === "receipt")
92
+ return output;
93
+ const collection = output.sources ??
94
+ output.feedback ??
95
+ (output.post ? [output.post] : output.person ? [output.person] : []);
96
+ const changed = [], unchangedIds = [];
97
+ for (const entry of collection) {
98
+ const id = "id" in entry ? entry.id : "handle" in entry ? entry.handle : null;
99
+ if (!id) {
100
+ changed.push(entry);
101
+ continue;
102
+ }
103
+ const key = JSON.stringify([operation, id]), value = JSON.stringify(entry);
104
+ if (readCache.get(key) === value)
105
+ unchangedIds.push(id);
106
+ else
107
+ changed.push(entry);
108
+ readCache.set(key, value);
109
+ }
110
+ // Creator metadata changes independently from the post collection.
111
+ if (operation === "readCreator") {
112
+ for (const name of ["person", "creator"]) {
113
+ if (!output[name])
114
+ continue;
115
+ const key = JSON.stringify([operation, name, args.handle]);
116
+ const value = JSON.stringify(output[name], (k, v) => k === "observedAt" ? undefined : v);
117
+ if (mode === "delta" && readCache.get(key) === value)
118
+ delete output[name];
119
+ readCache.set(key, value);
120
+ }
121
+ }
122
+ // A decision has delivered complete evidence and seeds the next delta.
123
+ if (mode === "decision")
124
+ return output;
125
+ if (output.sources)
126
+ output.sources = changed;
127
+ if (output.feedback)
128
+ output.feedback = changed;
129
+ if (output.post && !changed.length)
130
+ delete output.post;
131
+ if (operation !== "readCreator" && output.person && !changed.length)
132
+ delete output.person;
133
+ output.unchangedIds = unchangedIds;
134
+ return output;
135
+ }
136
+ async function compactRound(operation, args) {
137
+ const raw = await invoke(operation, args);
138
+ const out = {
139
+ status: raw.status,
140
+ taskId: args.taskId,
141
+ mayEnd: raw.mayEnd === true,
142
+ };
143
+ if (raw.scopeKinds)
144
+ out.scopeKinds = raw.scopeKinds;
145
+ if (raw.actual)
146
+ out.actual = Object.fromEntries(Object.entries(raw.actual).map(([kind, v]) => {
147
+ const counts = v;
148
+ return [kind, { verified: counts.verified, unknown: counts.unknown }];
149
+ }));
150
+ if (raw.gaps)
151
+ out.gaps = raw.gaps.map((v) => ({
152
+ kind: v.kind,
153
+ remaining: v.remaining,
154
+ unknown: v.unknown,
155
+ }));
156
+ const issues = decisions(raw.decision);
157
+ if (issues.length)
158
+ out.decision = issues;
159
+ if (raw.evidenceRef)
160
+ out.evidenceRef = raw.evidenceRef;
161
+ const d = diagnostic(raw.diagnostic);
162
+ if (d)
163
+ out.diagnostic = d;
164
+ if (Array.isArray(raw.settlementErrors))
165
+ out.settlementErrors = raw.settlementErrors
166
+ .map(stableReason)
167
+ .filter((v) => v !== undefined);
168
+ if (raw.reason)
169
+ out.decision = [
170
+ ...(out.decision ?? []),
171
+ { reason: stableReason(raw.reason), status: "needsDecision" },
172
+ ];
173
+ if (raw.missing)
174
+ out.decision = [...(out.decision ?? []), { missing: raw.missing }];
175
+ if (raw.checkpoint)
176
+ out.checkpoint = {
177
+ taskId: args.taskId,
178
+ executorId: raw.checkpoint.executorId,
179
+ completed: raw.checkpoint.completed,
180
+ resume: "runGrowthRound",
181
+ };
182
+ return out;
183
+ }
184
+ async function single(operation, args, selector) {
185
+ const raw = await invoke(operation, args);
186
+ let snapshot;
187
+ try {
188
+ snapshot = await store.load();
189
+ }
190
+ catch {
191
+ return {
192
+ status: "needsDecision",
193
+ taskId: args.taskId,
194
+ reason: "original-ledger-unavailable",
195
+ evidenceRef: raw.evidenceRef,
196
+ };
197
+ }
198
+ if (snapshot.state.batchId !== args.taskId)
199
+ return {
200
+ status: "needsDecision",
201
+ taskId: args.taskId,
202
+ reason: "original-task-id-required",
203
+ evidenceRef: raw.evidenceRef,
204
+ };
205
+ const action = snapshot.state.actions?.find(selector);
206
+ const issues = decisions(raw.decision);
207
+ const issue = issues.find((d) => d.actionId === action?.id) ??
208
+ issues.find((d) => !d.actionId);
209
+ const unresolved = action && ["intent", "unknown"].includes(action.status);
210
+ const status = unresolved
211
+ ? "unknown"
212
+ : raw.boundaryFailed
213
+ ? "needsDecision"
214
+ : issue?.status === "waiting" || raw.status === "waiting"
215
+ ? "waiting"
216
+ : raw.status === "needsCapability"
217
+ ? "needsCapability"
218
+ : action &&
219
+ ["verified", "preexisting", "skipped"].includes(action.status)
220
+ ? action.status
221
+ : "needsDecision";
222
+ const out = { status, taskId: args.taskId };
223
+ if (action?.id)
224
+ out.actionId = action.id;
225
+ if (action?.publicId) {
226
+ out.publicId = action.publicId;
227
+ out.publicURL = `https://x.com/${action.accountHandle}/status/${action.publicId}`;
228
+ }
229
+ const evidenceRef = action?.resultEvidence ?? raw.evidenceRef;
230
+ if (evidenceRef)
231
+ out.evidenceRef = evidenceRef;
232
+ const reason = stableReason(issue?.reason ?? raw.reason);
233
+ if (reason)
234
+ out.reason = reason;
235
+ if (issue?.resumeAt)
236
+ out.resumeAt = issue.resumeAt;
237
+ const missing = issue?.missing ?? raw.missing;
238
+ if (Array.isArray(missing) && missing.length)
239
+ out.missing = missing;
240
+ if (unresolved && !evidenceRef)
241
+ out.missing = [
242
+ ...new Set([...(out.missing ?? []), "result-proof-unavailable"]),
243
+ ];
244
+ const d = diagnostic(raw.diagnostic);
245
+ if (d)
246
+ out.diagnostic = d;
247
+ return out;
248
+ }
249
+ return {
250
+ readCreator: (args) => read("readCreator", args, args.mode),
251
+ readBookmarks: (args) => read("readBookmarks", args, args.mode),
252
+ searchSources: (args) => read("searchSources", args, args.mode),
253
+ readPost: ({ target, mode }) => read("readPost", target, mode),
254
+ readPerson: ({ target, mode }) => read("readPerson", target, mode),
255
+ readFeedback: (args) => read("readFeedback", args, args.mode),
256
+ inspectAccount: (args = {}) => read("inspectAccount", args, args.mode),
257
+ inspectRound: (args) => compactRound("inspectRound", args),
258
+ runGrowthRound: (args) => compactRound("runGrowthRound", args),
259
+ publishPost: (args) => single("publishPost", args, (a) => a.kind === "original" && a.draftPath === args.draftPath),
260
+ replyToPost: (args) => single("replyToPost", args, (a) => a.kind === "reply" &&
261
+ a.postId === args.postId &&
262
+ a.text === args.text),
263
+ followOne: (args) => single("followOne", args, (a) => a.kind === "follow" &&
264
+ a.handle?.toLowerCase() === args.handle.toLowerCase()),
265
+ verifySubmission: (args) => single("verifySubmission", args, (a) => a.id === args.actionId),
266
+ verifyPublishedPost: (args) => single("verifyPublishedPost", args, (a) => a.kind === "original" && a.draftPath === args.draftPath),
267
+ followBatch: async (args) => {
268
+ const raw = await invoke("followBatch", args);
269
+ const output = {
270
+ status: raw.status ?? (raw.mayEnd ? "verified" : "needsDecision"),
271
+ taskId: args.taskId,
272
+ planned: raw.planned ?? args.handles.length,
273
+ attempted: raw.attempted ?? 0,
274
+ verified: raw.verifiedThisCall ?? 0,
275
+ unknown: raw.unknownInBatch ?? 0,
276
+ remaining: raw.remaining ?? args.handles.length,
277
+ mayEnd: raw.mayEnd === true,
278
+ };
279
+ if (raw.batchId)
280
+ output.batchId = raw.batchId;
281
+ if (raw.evidenceRef)
282
+ output.evidenceRef = raw.evidenceRef;
283
+ const d = diagnostic(raw.diagnostic);
284
+ if (d)
285
+ output.diagnostic = d;
286
+ const issues = decisions(raw.decision);
287
+ if (raw.reason)
288
+ issues.push({ reason: raw.reason, status: "needsDecision" });
289
+ if (raw.missing)
290
+ issues.push({ missing: raw.missing });
291
+ if (issues.length)
292
+ output.decision = issues;
293
+ return output;
294
+ },
295
+ verifyFollow: async (args) => {
296
+ const raw = await invoke("verifyFollow", {
297
+ ...args,
298
+ mode: args.mode ?? "receipt",
299
+ });
300
+ const out = readResult("verifyFollow", raw, "receipt");
301
+ out.handle = args.handle;
302
+ out.verified = raw.verified === true;
303
+ if (args.mode === "decision" && raw.observation)
304
+ out.person = readResult("readPerson", raw.observation, "decision").person;
305
+ return out;
306
+ },
307
+ };
308
+ }
@@ -0,0 +1,2 @@
1
+ /** Internal dynamic UI/ledger boundary. Public business contracts are separately typed. */
2
+ export type BoundaryRecord = Record<string, any>;
@@ -0,0 +1,4 @@
1
+ export declare const plain: (label: any) => any;
2
+ export declare const valueOf: (label: any) => any;
3
+ export declare const missingMethods: (object: any, names: any) => any;
4
+ export declare function serialized(port: any): any;
@@ -0,0 +1,29 @@
1
+ export const plain = (label) => label.replace(/^(?:按钮|link|文本输入区域|文本输入区|文本区域|文本栏) /, "");
2
+ export const valueOf = (label) => label.match(/, Value: ([\s\S]*)$/)?.[1] ?? null;
3
+ export const missingMethods = (object, names) => names.filter((name) => typeof object?.[name] !== "function");
4
+ export function serialized(port) {
5
+ let busy = false;
6
+ const run = async (fn) => {
7
+ if (busy)
8
+ throw new Error("浏览器harness操作进行中,禁止并发");
9
+ busy = true;
10
+ try {
11
+ return await fn();
12
+ }
13
+ finally {
14
+ busy = false;
15
+ }
16
+ };
17
+ return {
18
+ ...port,
19
+ observe: (query) => run(() => port.observe(query)),
20
+ act: (command) => run(() => port.act(command)),
21
+ screenshot: () => run(() => port.screenshot()),
22
+ ...(port.collectCreator
23
+ ? {
24
+ collectCreator: (args) => run(() => port.collectCreator(args)),
25
+ }
26
+ : {}),
27
+ };
28
+ }
29
+ // AX indices never cross this port. Only semantic commands reach the platform layer.
@@ -0,0 +1,4 @@
1
+ import type { DOMDependencies } from "./browser-dom-dependencies.js";
2
+ export declare function createDOMCapabilities(dependencies: Pick<DOMDependencies, "tab" | "nativeSubmit">): {
3
+ capabilities(operation: any): any;
4
+ };