@nextclaw/x-harness 0.1.1 → 0.2.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 (80) hide show
  1. package/LICENSE +231 -6
  2. package/README.md +15 -1
  3. package/SOURCE.md +9 -0
  4. package/cli/main.js +232 -200
  5. package/dist/api.d.ts +56 -2
  6. package/dist/api.js +130 -79
  7. package/dist/boundary.js +1 -0
  8. package/dist/browser-common.d.ts +11 -0
  9. package/dist/browser-common.js +34 -18
  10. package/dist/browser-dom-capabilities.d.ts +7 -2
  11. package/dist/browser-dom-capabilities.js +123 -102
  12. package/dist/browser-dom-dependencies.js +1 -0
  13. package/dist/browser-dom-editing.d.ts +20 -2
  14. package/dist/browser-dom-editing.js +148 -116
  15. package/dist/browser-dom-navigation.d.ts +11 -2
  16. package/dist/browser-dom-navigation.js +66 -50
  17. package/dist/browser-dom-submission.d.ts +6 -1
  18. package/dist/browser-dom-submission.js +37 -18
  19. package/dist/browser-dom.d.ts +13 -1
  20. package/dist/browser-dom.js +113 -66
  21. package/dist/browser-native.d.ts +14 -1
  22. package/dist/browser-native.js +161 -128
  23. package/dist/creator-native.d.ts +3 -0
  24. package/dist/creator-native.js +113 -0
  25. package/dist/diagnostics.d.ts +11 -0
  26. package/dist/diagnostics.js +10 -0
  27. package/dist/growth-batch.d.ts +6 -3
  28. package/dist/growth-batch.js +157 -135
  29. package/dist/growth-dependencies.js +1 -0
  30. package/dist/growth-execution.d.ts +8 -2
  31. package/dist/growth-execution.js +68 -37
  32. package/dist/growth-parallel.d.ts +6 -2
  33. package/dist/growth-parallel.js +78 -47
  34. package/dist/growth-plan.d.ts +8 -3
  35. package/dist/growth-plan.js +45 -23
  36. package/dist/growth-policy.d.ts +11 -2
  37. package/dist/growth-policy.js +80 -53
  38. package/dist/growth-publishing.d.ts +7 -2
  39. package/dist/growth-publishing.js +72 -46
  40. package/dist/growth-reading.d.ts +6 -2
  41. package/dist/growth-reading.js +47 -25
  42. package/dist/growth-recovery.d.ts +7 -4
  43. package/dist/growth-recovery.js +177 -148
  44. package/dist/growth-state.d.ts +10 -2
  45. package/dist/growth-state.js +67 -30
  46. package/dist/growth-tasks.d.ts +19 -16
  47. package/dist/growth-tasks.js +153 -90
  48. package/dist/growth.d.ts +25 -1
  49. package/dist/growth.js +207 -124
  50. package/dist/platform-capabilities.d.ts +6 -2
  51. package/dist/platform-capabilities.js +18 -8
  52. package/dist/platform-collections.d.ts +10 -3
  53. package/dist/platform-collections.js +62 -35
  54. package/dist/platform-dependencies.js +1 -0
  55. package/dist/platform-diagnostics.d.ts +7 -2
  56. package/dist/platform-diagnostics.js +31 -19
  57. package/dist/platform-observation.d.ts +10 -2
  58. package/dist/platform-observation.js +44 -23
  59. package/dist/platform-targets.d.ts +10 -3
  60. package/dist/platform-targets.js +39 -14
  61. package/dist/platform-writing.d.ts +10 -2
  62. package/dist/platform-writing.js +63 -32
  63. package/dist/platform.d.ts +22 -1
  64. package/dist/platform.js +120 -52
  65. package/dist/readers-ax-host.d.ts +11 -1
  66. package/dist/readers-ax-host.js +108 -99
  67. package/dist/readers-ax-program.js +5 -0
  68. package/dist/readers-ax-tree.js +1 -0
  69. package/dist/readers-cli.js +79 -0
  70. package/dist/readers.js +9 -0
  71. package/dist/round-state.js +18 -1
  72. package/dist/store.d.ts +22 -4
  73. package/dist/store.js +173 -151
  74. package/dist/types.js +1 -0
  75. package/generated/observation.mjs +1 -1
  76. package/package.json +23 -9
  77. package/skill/SKILL.md +2 -2
  78. package/skill/references/reads.md +1 -1
  79. package/skill/references/recovery.md +4 -4
  80. package/skill/references/setup.md +1 -1
@@ -0,0 +1,79 @@
1
+ import { axParserCode } from "./readers-ax-program.js";
2
+ import { readPosts, changes } from "./readers-posts.js";
3
+ import { readProfile, screenCandidates } from "./readers-profile.js";
4
+ import { readAX } from "./readers-ax-parser.js";
5
+ import { AXHarness, createAXHarness } from "./readers-ax-host.js";
6
+ export function setupCode() {
7
+ return `var xReadPosts = ${readPosts.toString()};\nvar xReadChanges = ${changes.toString()};\nvar xReadProfile = ${readProfile.toString()};\nvar xScreenCandidates = ${screenCandidates.toString()};\nvar xReadAX = ${axParserCode};\nvar xCreateAXHarness = (function(readAX) { const AXHarness = ${AXHarness.toString()}; return ${createAXHarness.toString()}; })(xReadAX);\nvar xAXSessions = {};\nvar xReadCache = {};\n`;
8
+ }
9
+ export function profileCode({ tab = "xtab", handle } = {}) {
10
+ return profilesCode([{ tab, handle }]);
11
+ }
12
+ export function profilesCode(entries) {
13
+ if (!Array.isArray(entries) || !entries.length || entries.length > 5)
14
+ throw new Error("一次读取1~5位");
15
+ for (const { tab, handle } of entries) {
16
+ if (!/^[A-Za-z_$][\w$]*$/.test(tab ?? ""))
17
+ throw new Error("tab必须是已有Tab变量名");
18
+ if (!/^[A-Za-z0-9_]{1,15}$/.test(handle ?? ""))
19
+ throw new Error("需要已观察的X handle");
20
+ }
21
+ const inputs = entries
22
+ .map(({ tab, handle }) => `{tab:${tab},handle:${JSON.stringify(handle)}}`)
23
+ .join(",");
24
+ return `var xProfiles = [];\nfor (var entry of [${inputs}]) {\n var page = new URL(await entry.tab.url());\n if (page.origin !== 'https://x.com' || page.pathname.toLowerCase() !== '/' + entry.handle.toLowerCase()) { xProfiles.push({handle:entry.handle,blue:null,relationship:'unknown',alerts:['profile-url-mismatch']}); continue; }\n xProfiles.push(await entry.tab.playwright.evaluate(xReadProfile, {handle:entry.handle}));\n}\nnodeRepl.write(xScreenCandidates(xProfiles));\n`;
25
+ }
26
+ export function browserCode({ handle = "xtab", offset = 0, limit = 5, targetId = "", fresh = false, } = {}) {
27
+ if (!/^[A-Za-z_$][\w$]*$/.test(handle))
28
+ throw new Error("handle必须是当前REPL已有Tab变量名");
29
+ if (targetId && !/^\d+$/.test(targetId))
30
+ throw new Error("targetId必须是已观察的数字公开ID");
31
+ if (!Number.isInteger(offset) ||
32
+ offset < 0 ||
33
+ !Number.isInteger(limit) ||
34
+ limit < 1 ||
35
+ limit > 5)
36
+ throw new Error("无效读取窗口");
37
+ return `var xReadResult = await ${handle}.playwright.evaluate(xReadPosts, ${JSON.stringify({ offset, limit, targetId })});\nnodeRepl.write(xReadChanges(xReadResult, xReadCache, await ${handle}.url(), ${fresh}));\n`;
38
+ }
39
+ export function axCode({ handle = "chrome", mode = "posts", offset = 0, limit = 1, targetId = "", targetHandle = "", fresh = false, } = {}) {
40
+ if (!/^[A-Za-z_$][\w$]*$/.test(handle))
41
+ throw new Error("handle必须是已有原生App/Tab变量名");
42
+ // Share validation with the pure parser, without requiring a browser.
43
+ readAX("", { mode, offset, limit, targetId, targetHandle });
44
+ return `if (xAXSessions.${handle}?.app !== ${handle}) xAXSessions.${handle} = {app:${handle}, harness:xCreateAXHarness(${handle})};\nvar xAXResult = await xAXSessions.${handle}.harness.observe(${JSON.stringify({ mode, offset, limit, targetId, targetHandle })});\nnodeRepl.write(xReadChanges(xAXResult, xReadCache, xAXResult.page ?? 'unknown', ${fresh}));\n`;
45
+ }
46
+ export function runReaderCLI() {
47
+ try {
48
+ const args = process.argv.slice(2);
49
+ const value = (flag, fallback) => args.includes(flag) ? args[args.indexOf(flag) + 1] : fallback;
50
+ process.stdout.write(args.includes("--setup")
51
+ ? setupCode()
52
+ : args.includes("--ax")
53
+ ? axCode({
54
+ handle: value("--handle", "chrome"),
55
+ mode: value("--mode", "posts"),
56
+ offset: Number(value("--offset", 0)),
57
+ limit: Number(value("--limit", 1)),
58
+ targetId: value("--id", ""),
59
+ targetHandle: value("--person", ""),
60
+ fresh: args.includes("--fresh"),
61
+ })
62
+ : args.includes("--profile")
63
+ ? profileCode({
64
+ tab: value("--handle", "xtab"),
65
+ handle: value("--profile", ""),
66
+ })
67
+ : browserCode({
68
+ handle: value("--handle", "xtab"),
69
+ offset: Number(value("--offset", 0)),
70
+ limit: Number(value("--limit", 5)),
71
+ targetId: value("--id", ""),
72
+ fresh: args.includes("--fresh"),
73
+ }));
74
+ }
75
+ catch (error) {
76
+ console.error(error.message);
77
+ process.exitCode = 1;
78
+ }
79
+ }
@@ -0,0 +1,9 @@
1
+ /** Compatibility exports only. Implementation owners are task-specific modules. */
2
+ export { readPosts, changes } from "./readers-posts.js";
3
+ export { readAX } from "./readers-ax-parser.js";
4
+ export { createAXTabHarness, createAXHarness } from "./readers-ax-host.js";
5
+ export { readProfile, readPeople, screenCandidates, } from "./readers-profile.js";
6
+ export { readComposer, readMediaEditor } from "./readers-composer.js";
7
+ export { readMenu, readNavigation } from "./readers-navigation.js";
8
+ export { attachImage, openReply } from "./readers-actions.js";
9
+ export { setupCode, profileCode, profilesCode, browserCode, axCode, runReaderCLI, } from "./readers-cli.js";
@@ -141,7 +141,24 @@ export function runRoundCLI() {
141
141
  const file = process.argv.slice(2).find((a) => !a.startsWith("--"));
142
142
  if (!file)
143
143
  throw new Error("用法:node scripts/x-round-state.mjs <原台账> [--check-end]");
144
- const result = checkRound(readRound(readFileSync(file, "utf8")));
144
+ const state = readRound(readFileSync(file, "utf8"));
145
+ const executionId = process.argv
146
+ .find((a) => a.startsWith("--execution="))
147
+ ?.slice("--execution=".length);
148
+ let scoped = state;
149
+ if (executionId !== undefined) {
150
+ const execution = state.executions?.[executionId];
151
+ if (!execution?.scopeKinds?.length || !execution.evidenceRef)
152
+ throw new Error("原台账执行者或职责证据缺失");
153
+ scoped = {
154
+ ...state,
155
+ status: execution.status,
156
+ scopeKinds: execution.scopeKinds,
157
+ scopeEvidenceRef: execution.evidenceRef,
158
+ stop: execution.stop,
159
+ };
160
+ }
161
+ const result = checkRound(scoped);
145
162
  console.log(JSON.stringify(result));
146
163
  process.exitCode =
147
164
  result.valid && (!process.argv.includes("--check-end") || result.mayEnd)
package/dist/store.d.ts CHANGED
@@ -24,7 +24,25 @@ export interface RoundStore {
24
24
  }>;
25
25
  recordProof(proof: unknown): Promise<string>;
26
26
  }
27
- export declare function createFileRoundStore({ ledgerPath, proofDirectory, }: {
28
- ledgerPath: string;
29
- proofDirectory: string;
30
- }): RoundStore;
27
+ export declare class FileRoundStore implements RoundStore {
28
+ #private;
29
+ path: any;
30
+ evidenceExists: any;
31
+ processId: any;
32
+ constructor({ ledgerPath, proofDirectory, }: {
33
+ ledgerPath: string;
34
+ proofDirectory: string;
35
+ });
36
+ fingerprint(file: string): string;
37
+ ownerAlive(ownerPid: unknown): boolean;
38
+ transaction(fn: (snapshot: RoundSnapshot) => BoundaryRecord | undefined | Promise<BoundaryRecord | undefined>): Promise<RoundSnapshot>;
39
+ load(): Promise<RoundSnapshot>;
40
+ save(snapshot: RoundSnapshot, state: BoundaryRecord): Promise<RoundSnapshot>;
41
+ withLock<T>(fn: () => T | Promise<T>): Promise<T>;
42
+ recoverAbandonedLock(): Promise<{
43
+ recovered: boolean;
44
+ evidenceRef: string;
45
+ }>;
46
+ recordProof(proof: unknown): Promise<string>;
47
+ }
48
+ export declare function createFileRoundStore(...args: ConstructorParameters<typeof FileRoundStore>): RoundStore;
package/dist/store.js CHANGED
@@ -11,165 +11,187 @@ export class HarnessConflict extends Error {
11
11
  this.code = "CONFLICT";
12
12
  }
13
13
  }
14
- export function createFileRoundStore({ ledgerPath, proofDirectory, }) {
15
- const path = resolve(ledgerPath);
16
- const lockPath = join(dirname(path), ".x-harness.lock");
17
- const proofDir = resolve(proofDirectory);
18
- let held = false;
19
- return {
20
- path,
21
- evidenceExists: existsSync,
22
- fingerprint: (file) => hash(readFileSync(resolve(file))),
23
- processId: typeof process !== "undefined" ? process.pid : null,
24
- ownerAlive(ownerPid) {
25
- if (typeof ownerPid !== "number" ||
26
- !Number.isInteger(ownerPid) ||
27
- ownerPid < 1)
28
- return true;
29
- if (typeof process === "undefined" || typeof process.kill !== "function")
30
- return true;
31
- try {
32
- process.kill(ownerPid, 0);
33
- return true;
34
- }
35
- catch (error) {
36
- if (error.code === "ESRCH")
37
- return false;
38
- throw error;
39
- }
40
- },
41
- // Only bounded local state work belongs here. Browser I/O never holds this lock.
42
- async transaction(fn) {
43
- const start = Date.now();
44
- while (true) {
45
- let entered = false;
46
- try {
47
- return await this.withLock(async () => {
48
- entered = true;
49
- const snapshot = await this.load();
50
- const state = await fn(snapshot);
51
- return state ? this.save(snapshot, state) : snapshot;
52
- });
53
- }
54
- catch (error) {
55
- if (entered ||
56
- error.code !== "CONFLICT" ||
57
- Date.now() - start >= 2000)
58
- throw error;
59
- await new Promise((resolve) => setTimeout(resolve, 10));
60
- }
61
- }
62
- },
63
- async load() {
64
- const text = readFileSync(path, "utf8");
65
- return { state: readRound(text), revision: hash(text), path };
66
- },
67
- async save(snapshot, state) {
68
- if (!held)
69
- throw new HarnessConflict("保存原台账必须持有账号锁");
70
- const text = readFileSync(path, "utf8");
71
- if (snapshot.revision !== hash(text))
72
- throw new HarnessConflict("原台账已变化:重新定向读取,禁止覆盖");
73
- if (readRound(text).batchId !== state.batchId)
74
- throw new HarnessConflict("禁止替换原批次ID");
75
- const next = text.replace(block, () => "```x-round-state\n" + JSON.stringify(state, null, 2) + "\n```");
76
- const temp = `${path}.harness-${randomUUID()}.tmp`;
14
+ export class FileRoundStore {
15
+ #ledgerPath;
16
+ #proofDirectory;
17
+ #path;
18
+ #lockPath;
19
+ #proofDir;
20
+ #held;
21
+ path;
22
+ evidenceExists;
23
+ processId;
24
+ constructor({ ledgerPath, proofDirectory, }) {
25
+ this.recordProof = this.recordProof.bind(this);
26
+ this.recoverAbandonedLock = this.recoverAbandonedLock.bind(this);
27
+ this.withLock = this.withLock.bind(this);
28
+ this.save = this.save.bind(this);
29
+ this.load = this.load.bind(this);
30
+ this.transaction = this.transaction.bind(this);
31
+ this.ownerAlive = this.ownerAlive.bind(this);
32
+ this.fingerprint = this.fingerprint.bind(this);
33
+ this.#ledgerPath = ledgerPath;
34
+ this.#proofDirectory = proofDirectory;
35
+ this.#path = resolve(this.#ledgerPath);
36
+ this.#lockPath = join(dirname(this.#path), ".x-harness.lock");
37
+ this.#proofDir = resolve(this.#proofDirectory);
38
+ this.#held = false;
39
+ this.path = this.#path;
40
+ this.evidenceExists = existsSync;
41
+ this.processId = typeof process !== "undefined" ? process.pid : null;
42
+ }
43
+ fingerprint(file) {
44
+ return hash(readFileSync(resolve(file)));
45
+ }
46
+ ownerAlive(ownerPid) {
47
+ if (typeof ownerPid !== "number" ||
48
+ !Number.isInteger(ownerPid) ||
49
+ ownerPid < 1)
50
+ return true;
51
+ if (typeof process === "undefined" || typeof process.kill !== "function")
52
+ return true;
53
+ try {
54
+ process.kill(ownerPid, 0);
55
+ return true;
56
+ }
57
+ catch (error) {
58
+ if (error.code === "ESRCH")
59
+ return false;
60
+ throw error;
61
+ }
62
+ }
63
+ async transaction(fn) {
64
+ const start = Date.now();
65
+ while (true) {
66
+ let entered = false;
77
67
  try {
78
- writeFileSync(temp, next, {
79
- flag: "wx",
80
- mode: statSync(path).mode & 0o777,
68
+ return await this.withLock(async () => {
69
+ entered = true;
70
+ const snapshot = await this.load();
71
+ const state = await fn(snapshot);
72
+ return state ? this.save(snapshot, state) : snapshot;
81
73
  });
82
- // Other writers must honor the same lock; CAS additionally detects observed external edits.
83
- if (hash(readFileSync(path, "utf8")) !== snapshot.revision)
84
- throw new HarnessConflict("原台账在结算中变化");
85
- renameSync(temp, path);
86
- }
87
- finally {
88
- if (existsSync(temp))
89
- unlinkSync(temp);
90
- }
91
- return { state, revision: hash(next), path };
92
- },
93
- async withLock(fn) {
94
- if (held)
95
- throw new HarnessConflict("账号操作禁止重入");
96
- if (existsSync(`${lockPath}.recovery`))
97
- throw new HarnessConflict("原锁恢复核验进行中");
98
- let fd;
99
- try {
100
- fd = openSync(lockPath, "wx", 0o600);
101
74
  }
102
75
  catch (error) {
103
- if (error.code === "EEXIST")
104
- throw new HarnessConflict("账号锁占用:不并发写、不自动偷锁");
105
- throw error;
106
- }
107
- try {
108
- writeFileSync(fd, JSON.stringify({
109
- pid: typeof process !== "undefined" ? process.pid : null,
110
- acquiredAt: new Date().toISOString(),
111
- ledgerPath: path,
112
- }));
113
- held = true;
114
- return await fn();
115
- }
116
- finally {
117
- held = false;
118
- closeSync(fd);
119
- unlinkSync(lockPath);
76
+ if (entered ||
77
+ error.code !== "CONFLICT" ||
78
+ Date.now() - start >= 2000)
79
+ throw error;
80
+ await new Promise((resolve) => setTimeout(resolve, 10));
120
81
  }
121
- },
122
- async recoverAbandonedLock() {
123
- if (held)
124
- throw new HarnessConflict("不能恢复当前正在持有的锁");
125
- const recovery = `${lockPath}.recovery`;
126
- let fd;
82
+ }
83
+ }
84
+ async load() {
85
+ const text = readFileSync(this.#path, "utf8");
86
+ return { state: readRound(text), revision: hash(text), path: this.#path };
87
+ }
88
+ async save(snapshot, state) {
89
+ if (!this.#held)
90
+ throw new HarnessConflict("保存原台账必须持有账号锁");
91
+ const text = readFileSync(this.#path, "utf8");
92
+ if (snapshot.revision !== hash(text))
93
+ throw new HarnessConflict("原台账已变化:重新定向读取,禁止覆盖");
94
+ if (readRound(text).batchId !== state.batchId)
95
+ throw new HarnessConflict("禁止替换原批次ID");
96
+ const next = text.replace(block, () => "```x-round-state\n" + JSON.stringify(state, null, 2) + "\n```");
97
+ const temp = `${this.#path}.harness-${randomUUID()}.tmp`;
98
+ try {
99
+ writeFileSync(temp, next, {
100
+ flag: "wx",
101
+ mode: statSync(this.#path).mode & 0o777,
102
+ });
103
+ // Other writers must honor the same lock; CAS additionally detects observed external edits.
104
+ if (hash(readFileSync(this.#path, "utf8")) !== snapshot.revision)
105
+ throw new HarnessConflict("原台账在结算中变化");
106
+ renameSync(temp, this.#path);
107
+ }
108
+ finally {
109
+ if (existsSync(temp))
110
+ unlinkSync(temp);
111
+ }
112
+ return { state, revision: hash(next), path: this.#path };
113
+ }
114
+ async withLock(fn) {
115
+ if (this.#held)
116
+ throw new HarnessConflict("账号操作禁止重入");
117
+ if (existsSync(`${this.#lockPath}.recovery`))
118
+ throw new HarnessConflict("原锁恢复核验进行中");
119
+ let fd;
120
+ try {
121
+ fd = openSync(this.#lockPath, "wx", 0o600);
122
+ }
123
+ catch (error) {
124
+ if (error.code === "EEXIST")
125
+ throw new HarnessConflict("账号锁占用:不并发写、不自动偷锁");
126
+ throw error;
127
+ }
128
+ try {
129
+ writeFileSync(fd, JSON.stringify({
130
+ pid: typeof process !== "undefined" ? process.pid : null,
131
+ acquiredAt: new Date().toISOString(),
132
+ ledgerPath: this.#path,
133
+ }));
134
+ this.#held = true;
135
+ return await fn();
136
+ }
137
+ finally {
138
+ this.#held = false;
139
+ closeSync(fd);
140
+ unlinkSync(this.#lockPath);
141
+ }
142
+ }
143
+ async recoverAbandonedLock() {
144
+ if (this.#held)
145
+ throw new HarnessConflict("不能恢复当前正在持有的锁");
146
+ const recovery = `${this.#lockPath}.recovery`;
147
+ let fd;
148
+ try {
149
+ fd = openSync(recovery, "wx", 0o600);
150
+ }
151
+ catch (error) {
152
+ if (error.code === "EEXIST")
153
+ throw new HarnessConflict("另一执行器正在核验原锁");
154
+ throw error;
155
+ }
156
+ try {
157
+ const original = readFileSync(this.#lockPath, "utf8"), owner = JSON.parse(original);
158
+ if (!Number.isInteger(owner.pid) || owner.pid < 1)
159
+ throw new HarnessConflict("锁owner缺失,不能自动移除");
160
+ if (typeof process === "undefined" || typeof process.kill !== "function")
161
+ throw new HarnessConflict("当前宿主无进程核验能力,禁止恢复原锁");
127
162
  try {
128
- fd = openSync(recovery, "wx", 0o600);
163
+ process.kill(owner.pid, 0);
164
+ throw new HarnessConflict("原进程仍在:禁止抢占账号锁");
129
165
  }
130
166
  catch (error) {
131
- if (error.code === "EEXIST")
132
- throw new HarnessConflict("另一执行器正在核验原锁");
133
- throw error;
167
+ if (error.code !== "ESRCH")
168
+ throw error;
134
169
  }
135
- try {
136
- const original = readFileSync(lockPath, "utf8"), owner = JSON.parse(original);
137
- if (!Number.isInteger(owner.pid) || owner.pid < 1)
138
- throw new HarnessConflict("锁owner缺失,不能自动移除");
139
- if (typeof process === "undefined" ||
140
- typeof process.kill !== "function")
141
- throw new HarnessConflict("当前宿主无进程核验能力,禁止恢复原锁");
142
- try {
143
- process.kill(owner.pid, 0);
144
- throw new HarnessConflict("原进程仍在:禁止抢占账号锁");
145
- }
146
- catch (error) {
147
- if (error.code !== "ESRCH")
148
- throw error;
149
- }
150
- const proof = await this.recordProof({
151
- observedAt: new Date().toISOString(),
152
- recovery: "owner-process-confirmed-exited",
153
- owner,
154
- });
155
- if (readFileSync(lockPath, "utf8") !== original)
156
- throw new HarnessConflict("原锁已变化,禁止移除");
157
- unlinkSync(lockPath);
158
- return { recovered: true, evidenceRef: proof };
159
- }
160
- finally {
161
- closeSync(fd);
162
- unlinkSync(recovery);
163
- }
164
- },
165
- async recordProof(proof) {
166
- mkdirSync(proofDir, { recursive: true });
167
- const file = join(proofDir, `${randomUUID()}.json`);
168
- writeFileSync(file, JSON.stringify(proof, null, 2), {
169
- flag: "wx",
170
- mode: 0o600,
170
+ const proof = await this.recordProof({
171
+ observedAt: new Date().toISOString(),
172
+ recovery: "owner-process-confirmed-exited",
173
+ owner,
171
174
  });
172
- return file;
173
- },
174
- };
175
+ if (readFileSync(this.#lockPath, "utf8") !== original)
176
+ throw new HarnessConflict("原锁已变化,禁止移除");
177
+ unlinkSync(this.#lockPath);
178
+ return { recovered: true, evidenceRef: proof };
179
+ }
180
+ finally {
181
+ closeSync(fd);
182
+ unlinkSync(recovery);
183
+ }
184
+ }
185
+ async recordProof(proof) {
186
+ mkdirSync(this.#proofDir, { recursive: true });
187
+ const file = join(this.#proofDir, `${randomUUID()}.json`);
188
+ writeFileSync(file, JSON.stringify(proof, null, 2), {
189
+ flag: "wx",
190
+ mode: 0o600,
191
+ });
192
+ return file;
193
+ }
194
+ }
195
+ export function createFileRoundStore(...args) {
196
+ return new FileRoundStore(...args);
175
197
  }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- // Generated by packages/x-harness/scripts/generate-observation.mjs; edit TS reader owners only.
1
+ // Generated by scripts/generate-observation.mjs; edit TS reader owners only.
2
2
  export const readXObservation = (query) => {
3
3
  const readPosts = function readPosts({ offset = 0, limit = 5, targetId = "", } = {}) {
4
4
  if (!Number.isInteger(offset) ||
package/package.json CHANGED
@@ -1,32 +1,46 @@
1
1
  {
2
2
  "name": "@nextclaw/x-harness",
3
- "version": "0.1.1",
4
- "description": "X tasks for browser-enabled AI agents: compact results, complete evidence and verified writes.",
5
- "license": "SEE LICENSE IN LICENSE",
3
+ "version": "0.2.1",
6
4
  "type": "module",
5
+ "description": "Typed X task harness with compact business results and explicit diagnostics.",
7
6
  "main": "./dist/index.js",
8
7
  "types": "./dist/index.d.ts",
9
8
  "exports": {
10
9
  ".": {
11
10
  "types": "./dist/index.d.ts",
12
11
  "import": "./dist/index.js"
12
+ },
13
+ "./diagnostics": {
14
+ "types": "./dist/diagnostics.d.ts",
15
+ "import": "./dist/diagnostics.js"
13
16
  }
14
17
  },
15
- "bin": {
16
- "x-harness": "./cli/main.js"
17
- },
18
18
  "files": [
19
19
  "dist",
20
20
  "generated",
21
21
  "cli",
22
22
  "skill",
23
23
  "README.md",
24
- "LICENSE"
24
+ "LICENSE",
25
+ "SOURCE.md"
25
26
  ],
27
+ "license": "AGPL-3.0-only",
28
+ "homepage": "https://x-harness.gemigo.app/",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/Peiiii/x-harness.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/Peiiii/x-harness/issues"
35
+ },
26
36
  "engines": {
27
37
  "node": ">=22"
28
38
  },
39
+ "bin": {
40
+ "x-harness": "./cli/main.js"
41
+ },
29
42
  "publishConfig": {
30
43
  "access": "public"
31
- }
32
- }
44
+ },
45
+ "gitHead": "66d1ce5637601897d7d769f74d56eacffc2e865e"
46
+ }
package/skill/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: x-harness
3
- version: 0.1.1
3
+ version: 0.2.1
4
4
  description: Use X through task APIs with compact receipts, complete target evidence and verified writes; requires compatible CUA and the user's X login.
5
5
  ---
6
6
 
@@ -13,7 +13,7 @@ Use the root business API. Load only the current task reference; reuse unchanged
13
13
  Read current host CUA documentation; bind a real logged-in `tab` or native `app`, exactly one. Preserve other tasks' tabs/editors.
14
14
 
15
15
  Installed module: `{{MODULE_URL}}`; CLI: `{{CLI_PATH}}`.
16
- In this repository use the absolute file URL of `<repository>/packages/x-harness/dist/index.js` with original ledger/proof paths. Unresolved placeholders do not require reinstalling an existing repository runtime. Only absent runtime/workspace or an update needs [setup](references/setup.md).
16
+ In a development checkout use the absolute file URL of `<harness-repository>/dist/index.js` with existing ledger/proof paths. Keep an existing repository runtime when placeholders remain unresolved. Only absent runtime/workspace or an update needs [setup](references/setup.md).
17
17
 
18
18
  ```js
19
19
  var { createXGrowthHarness } = await import(moduleURL);
@@ -18,6 +18,6 @@ var result = await x.readCreator({ handle: selectedHandle, target: 5, mode: 'dec
18
18
  var postResult = await x.readPost({ target: observedPost, mode: 'decision' });
19
19
  ```
20
20
 
21
- Targets/proof come from actual observations, never guessed URLs or made-up references. API owns navigation/expansion and bounded collection. Native creator collection may return `needsCapability`; inspect that signal before changing adapter.
21
+ Targets/proof come from actual observations, never guessed URLs or made-up references. API owns navigation/expansion and bounded collection. Native creator collection supports an already selected profile with `navigate:false`; it keeps relative times unknown and stops with partial evidence if the active page changes. Native automatic creator navigation remains `needsCapability`; inspect that signal before changing adapter.
22
22
 
23
23
  Results include status/evidenceRef/missing/IDs/completeness. In `decision`, creator/bookmark/search use `sources`, post `post`, person `person`, feedback `feedback`; account includes accountHandle/accountURL. `observed` alone does not prove every requested field exists. `pageComplete:false` is a collection boundary, not proof of truncated target text. View media before describing it. Return complete chosen evidence; keep other observations local. Only on failure load recovery.
@@ -1,6 +1,6 @@
1
1
  # Recovery by signal
2
2
 
3
- Load on failure/unknown only. Keep operation/status/missing/code/proof/error; never infer success or repeat identical failures.
3
+ Load on failure/unknown only. Keep operation/status/missing/code/proof/error; do not infer success or repeat failures.
4
4
 
5
5
  | Signal | Next action and exit condition |
6
6
  | --- | --- |
@@ -16,10 +16,10 @@ Load on failure/unknown only. Keep operation/status/missing/code/proof/error; ne
16
16
 
17
17
  ## Host connection or native focus
18
18
 
19
- Failure to list/bind Chrome is a host-connection failure before reaching X, not proof of logged-out X. Per current host documentation rediscover the task tab once; validate navigation plus a necessary visible read. Native focus rejection instead needs latest AX URL/window confirmation and one supported focus correction. Do not repeat discovery or restart editors blindly.
19
+ Naming failure is not tab failure or X logout. Once, try a host-supported alternative: real Chrome → owned tab → root read. Preserve editors; no policy bypass/unchanged retries. Success proves current reading, not stable transport/writes. Reuse bindings; after real reset rediscover Chrome identity, not old numeric IDs. Native rejection needs fresh URL/window and one supported correction. App fallback keeps root calls; do not change network settings.
20
20
 
21
- Lost tab: reuse a live owned tab or create a task tab in the same actual browser; preserve other editors. Native fallback changes binding to app, not business calls; unsupported native tasks remain gaps. Network/TUN observations do not authorize setting changes.
21
+ `tab-busy`: inspect proof/claim. Age/null PID is not termination. Only evidenced own-runtime termination permits CAS release matching executor/runtime/token, preserving intent/unknown and others. Verify same ID, never resubmit. Do not reset a working runtime to diagnose another error.
22
22
 
23
23
  ## Repair boundary
24
24
 
25
- Normal use/known recovery need no source. Unresolved runtime defect/unsupported capability permits diagnostics: maintainers repair the owner, verify the failed boundary and return to root; consumers report gap/update/support, without private TS. Never scan adapters for arguments, add parsers, bypass refusal or resubmit unknown.
25
+ Source is only for unresolved defects/capability gaps: maintainers repair the owner, verify the failed boundary and return to root; consumers report gap/update/support, without source. Never scan adapters for arguments, add parsers, bypass refusal or resubmit unknown.
@@ -4,7 +4,7 @@ Load only when runtime/workspace is absent, binding needs refreshing after updat
4
4
 
5
5
  ## Repository runtime
6
6
 
7
- Use `<repository>/packages/x-harness/dist/index.js` as an absolute file URL; if absent run `pnpm --filter @ai-content-studio/x-harness build`. Reuse original ledger/proofs and runtime-context entry; do not initialize a second ledger or download another runtime. Global Skill links to this repository owner.
7
+ Use `<repository>/dist/index.js` as an absolute file URL; if absent run `npm run build`. Reuse original ledger/proofs and private parent runtime-context entry; do not initialize a second ledger or download another runtime. Use this repository’s skill/ as the development Skill owner.
8
8
 
9
9
  ## Independent installation
10
10