@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.
- package/LICENSE +231 -6
- package/README.md +15 -1
- package/SOURCE.md +9 -0
- package/cli/main.js +232 -200
- package/dist/api.d.ts +56 -2
- package/dist/api.js +130 -79
- package/dist/boundary.js +1 -0
- package/dist/browser-common.d.ts +11 -0
- package/dist/browser-common.js +34 -18
- package/dist/browser-dom-capabilities.d.ts +7 -2
- package/dist/browser-dom-capabilities.js +123 -102
- package/dist/browser-dom-dependencies.js +1 -0
- package/dist/browser-dom-editing.d.ts +20 -2
- package/dist/browser-dom-editing.js +148 -116
- package/dist/browser-dom-navigation.d.ts +11 -2
- package/dist/browser-dom-navigation.js +66 -50
- package/dist/browser-dom-submission.d.ts +6 -1
- package/dist/browser-dom-submission.js +37 -18
- package/dist/browser-dom.d.ts +13 -1
- package/dist/browser-dom.js +113 -66
- package/dist/browser-native.d.ts +14 -1
- package/dist/browser-native.js +161 -128
- package/dist/creator-native.d.ts +3 -0
- package/dist/creator-native.js +113 -0
- package/dist/diagnostics.d.ts +11 -0
- package/dist/diagnostics.js +10 -0
- package/dist/growth-batch.d.ts +6 -3
- package/dist/growth-batch.js +157 -135
- package/dist/growth-dependencies.js +1 -0
- package/dist/growth-execution.d.ts +8 -2
- package/dist/growth-execution.js +68 -37
- package/dist/growth-parallel.d.ts +6 -2
- package/dist/growth-parallel.js +78 -47
- package/dist/growth-plan.d.ts +8 -3
- package/dist/growth-plan.js +45 -23
- package/dist/growth-policy.d.ts +11 -2
- package/dist/growth-policy.js +80 -53
- package/dist/growth-publishing.d.ts +7 -2
- package/dist/growth-publishing.js +72 -46
- package/dist/growth-reading.d.ts +6 -2
- package/dist/growth-reading.js +47 -25
- package/dist/growth-recovery.d.ts +7 -4
- package/dist/growth-recovery.js +177 -148
- package/dist/growth-state.d.ts +10 -2
- package/dist/growth-state.js +67 -30
- package/dist/growth-tasks.d.ts +19 -16
- package/dist/growth-tasks.js +153 -90
- package/dist/growth.d.ts +25 -1
- package/dist/growth.js +207 -124
- package/dist/platform-capabilities.d.ts +6 -2
- package/dist/platform-capabilities.js +18 -8
- package/dist/platform-collections.d.ts +10 -3
- package/dist/platform-collections.js +62 -35
- package/dist/platform-dependencies.js +1 -0
- package/dist/platform-diagnostics.d.ts +7 -2
- package/dist/platform-diagnostics.js +31 -19
- package/dist/platform-observation.d.ts +10 -2
- package/dist/platform-observation.js +44 -23
- package/dist/platform-targets.d.ts +10 -3
- package/dist/platform-targets.js +39 -14
- package/dist/platform-writing.d.ts +10 -2
- package/dist/platform-writing.js +63 -32
- package/dist/platform.d.ts +22 -1
- package/dist/platform.js +120 -52
- package/dist/readers-ax-host.d.ts +11 -1
- package/dist/readers-ax-host.js +108 -99
- package/dist/readers-ax-program.js +5 -0
- package/dist/readers-ax-tree.js +1 -0
- package/dist/readers-cli.js +79 -0
- package/dist/readers.js +9 -0
- package/dist/round-state.js +18 -1
- package/dist/store.d.ts +22 -4
- package/dist/store.js +173 -151
- package/dist/types.js +1 -0
- package/generated/observation.mjs +1 -1
- package/package.json +23 -9
- package/skill/SKILL.md +2 -2
- package/skill/references/reads.md +1 -1
- package/skill/references/recovery.md +4 -4
- 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
|
+
}
|
package/dist/readers.js
ADDED
|
@@ -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";
|
package/dist/round-state.js
CHANGED
|
@@ -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
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
163
|
+
process.kill(owner.pid, 0);
|
|
164
|
+
throw new HarnessConflict("原进程仍在:禁止抢占账号锁");
|
|
129
165
|
}
|
|
130
166
|
catch (error) {
|
|
131
|
-
if (error.code
|
|
132
|
-
throw
|
|
133
|
-
throw error;
|
|
167
|
+
if (error.code !== "ESRCH")
|
|
168
|
+
throw error;
|
|
134
169
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>/
|
|
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
|
|