@nextclaw/x-harness 0.1.0 → 0.2.0
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 +17 -3
- package/SOURCE.md +9 -0
- package/cli/main.js +232 -188
- package/dist/api.d.ts +56 -2
- package/dist/api.js +178 -75
- 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 +9 -0
- package/dist/browser-dom-capabilities.js +131 -0
- package/dist/browser-dom-dependencies.d.ts +16 -0
- package/dist/browser-dom-dependencies.js +1 -0
- package/dist/browser-dom-editing.d.ts +166 -0
- package/dist/browser-dom-editing.js +418 -0
- package/dist/browser-dom-navigation.d.ts +18 -0
- package/dist/browser-dom-navigation.js +181 -0
- package/dist/browser-dom-network.d.ts +1 -0
- package/dist/browser-dom-network.js +128 -0
- package/dist/browser-dom-submission.d.ts +7 -0
- package/dist/browser-dom-submission.js +101 -0
- package/dist/browser-dom.d.ts +14 -2
- package/dist/browser-dom.js +120 -824
- package/dist/browser-native.d.ts +14 -1
- package/dist/browser-native.js +180 -143
- package/dist/browser.js +6 -2
- package/dist/creator-native.d.ts +3 -0
- package/dist/creator-native.js +113 -0
- package/dist/creator.js +1 -1
- package/dist/diagnostics.d.ts +11 -0
- package/dist/diagnostics.js +10 -0
- package/dist/growth-batch.d.ts +8 -0
- package/dist/growth-batch.js +190 -0
- package/dist/growth-constants.d.ts +5 -0
- package/dist/growth-constants.js +13 -0
- package/dist/growth-dependencies.d.ts +33 -0
- package/dist/growth-dependencies.js +1 -0
- package/dist/growth-execution.d.ts +17 -0
- package/dist/growth-execution.js +346 -0
- package/dist/growth-parallel.d.ts +8 -0
- package/dist/growth-parallel.js +332 -0
- package/dist/growth-plan.d.ts +9 -0
- package/dist/growth-plan.js +229 -0
- package/dist/growth-policy.d.ts +18 -0
- package/dist/growth-policy.js +208 -0
- package/dist/growth-publishing.d.ts +10 -0
- package/dist/growth-publishing.js +266 -0
- package/dist/growth-reading.d.ts +8 -0
- package/dist/growth-reading.js +157 -0
- package/dist/growth-recovery.d.ts +9 -0
- package/dist/growth-recovery.js +187 -0
- package/dist/growth-state.d.ts +16 -0
- package/dist/growth-state.js +220 -0
- package/dist/growth-tasks.d.ts +20 -0
- package/dist/growth-tasks.js +164 -0
- package/dist/growth.d.ts +26 -1
- package/dist/growth.js +211 -1908
- package/dist/platform-capabilities.d.ts +8 -0
- package/dist/platform-capabilities.js +29 -0
- package/dist/platform-collections.d.ts +14 -0
- package/dist/platform-collections.js +250 -0
- package/dist/platform-dependencies.d.ts +43 -0
- package/dist/platform-dependencies.js +1 -0
- package/dist/platform-diagnostics.d.ts +14 -0
- package/dist/platform-diagnostics.js +63 -0
- package/dist/platform-observation.d.ts +16 -0
- package/dist/platform-observation.js +96 -0
- package/dist/platform-rules.d.ts +17 -0
- package/dist/platform-rules.js +41 -0
- package/dist/platform-targets.d.ts +14 -0
- package/dist/platform-targets.js +154 -0
- package/dist/platform-writing.d.ts +16 -0
- package/dist/platform-writing.js +202 -0
- package/dist/platform.d.ts +23 -2
- package/dist/platform.js +125 -678
- package/dist/readers-actions.d.ts +2 -0
- package/dist/readers-actions.js +66 -0
- package/dist/readers-ax-host.d.ts +12 -0
- package/dist/readers-ax-host.js +134 -0
- package/dist/readers-ax-parser.d.ts +1 -0
- package/dist/readers-ax-parser.js +69 -0
- package/dist/readers-ax-program.d.ts +1 -0
- package/dist/readers-ax-program.js +5 -0
- package/dist/readers-ax-tree.d.ts +19 -0
- package/dist/readers-ax-tree.js +111 -0
- package/dist/readers-ax-views.d.ts +87 -0
- package/dist/readers-ax-views.js +247 -0
- package/dist/readers-cli.d.ts +6 -0
- package/dist/readers-cli.js +79 -0
- package/dist/readers-composer.d.ts +2 -0
- package/dist/readers-composer.js +131 -0
- package/dist/readers-navigation.d.ts +2 -0
- package/dist/readers-navigation.js +66 -0
- package/dist/readers-posts.d.ts +2 -0
- package/dist/readers-posts.js +141 -0
- package/dist/readers-profile.d.ts +3 -0
- package/dist/readers-profile.js +157 -0
- package/dist/readers.d.ts +9 -20
- package/dist/readers.js +9 -1045
- package/dist/results.js +1 -1
- package/dist/round-state.js +20 -3
- package/dist/store.d.ts +22 -4
- package/dist/store.js +173 -151
- package/dist/types.d.ts +10 -0
- package/dist/types.js +1 -0
- package/generated/observation.mjs +3 -2
- package/package.json +23 -9
- package/skill/SKILL.md +17 -36
- package/skill/references/api.md +7 -34
- package/skill/references/reads.md +23 -0
- package/skill/references/recovery.md +25 -0
- package/skill/references/setup.md +23 -0
- package/skill/references/writes.md +51 -0
package/dist/results.js
CHANGED
|
@@ -21,7 +21,7 @@ export function diagnostic(value) {
|
|
|
21
21
|
if (typeof raw.code !== "string")
|
|
22
22
|
return undefined;
|
|
23
23
|
return {
|
|
24
|
-
...pick(raw, ["operation", "category"]),
|
|
24
|
+
...pick(raw, ["operation", "category", "next", "evidenceRef"]),
|
|
25
25
|
code: stableReason(raw.code),
|
|
26
26
|
};
|
|
27
27
|
}
|
package/dist/round-state.js
CHANGED
|
@@ -109,8 +109,8 @@ export function checkRound(state, { now = Date.now(), evidenceExists = existsSyn
|
|
|
109
109
|
reply.unknown, "未提交欠量仍在;未知提交不能冻结新对象");
|
|
110
110
|
if (exit.reason === "outside-window") {
|
|
111
111
|
const shanghaiHour = (new Date(now).getUTCHours() + 8) % 24;
|
|
112
|
-
require(shanghaiHour <
|
|
113
|
-
|
|
112
|
+
require(shanghaiHour <
|
|
113
|
+
7, "仍在上海07至24点评论执行窗口,不能以时段截止结束");
|
|
114
114
|
}
|
|
115
115
|
if (exit.reason === "user-stop")
|
|
116
116
|
require(typeof exit.instructionEvidenceRef === "string" &&
|
|
@@ -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.d.ts
CHANGED
|
@@ -75,6 +75,8 @@ export interface Diagnostic {
|
|
|
75
75
|
operation?: string;
|
|
76
76
|
category?: string;
|
|
77
77
|
code: string;
|
|
78
|
+
next?: "inspect-local-proof" | "restore-proof-store";
|
|
79
|
+
evidenceRef?: EvidenceRef;
|
|
78
80
|
}
|
|
79
81
|
export interface ReadResult {
|
|
80
82
|
status: "observed" | "needsDecision" | "needsCapability";
|
|
@@ -196,6 +198,7 @@ export interface ActionResult {
|
|
|
196
198
|
diagnostic?: Diagnostic;
|
|
197
199
|
}
|
|
198
200
|
export interface RoundResult {
|
|
201
|
+
diagnostic?: Diagnostic;
|
|
199
202
|
status: string;
|
|
200
203
|
taskId: string;
|
|
201
204
|
mayEnd: boolean;
|
|
@@ -220,6 +223,7 @@ export interface RoundResult {
|
|
|
220
223
|
};
|
|
221
224
|
}
|
|
222
225
|
export interface BatchResult {
|
|
226
|
+
diagnostic?: Diagnostic;
|
|
223
227
|
status: string;
|
|
224
228
|
taskId: string;
|
|
225
229
|
batchId?: string;
|
|
@@ -265,6 +269,12 @@ export interface IndependentExecution {
|
|
|
265
269
|
tabId: string;
|
|
266
270
|
evidenceRef: EvidenceRef;
|
|
267
271
|
scopeEvidenceRef: EvidenceRef;
|
|
272
|
+
windowAuthorization?: {
|
|
273
|
+
actionId: string;
|
|
274
|
+
userAuthorized: true;
|
|
275
|
+
expiresAt: string;
|
|
276
|
+
evidenceRef: EvidenceRef;
|
|
277
|
+
};
|
|
268
278
|
}
|
|
269
279
|
export interface PublishOptions {
|
|
270
280
|
taskId: string;
|
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) ||
|
|
@@ -487,7 +487,8 @@ export const readXObservation = (query) => {
|
|
|
487
487
|
const read = (node) => {
|
|
488
488
|
if (node.nodeType === 3)
|
|
489
489
|
return node.textContent ?? "";
|
|
490
|
-
if (node.nodeType === 1 &&
|
|
490
|
+
if (node.nodeType === 1 &&
|
|
491
|
+
node.tagName === "A") {
|
|
491
492
|
const href = node.getAttribute("href") ?? "";
|
|
492
493
|
if (/^\/\w{1,15}\/status\/\d+$/.test(href))
|
|
493
494
|
return `https://x.com${href}`;
|
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.0",
|
|
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": "269a29c44c871bac51265653a4e559ffacbf2d84"
|
|
46
|
+
}
|
package/skill/SKILL.md
CHANGED
|
@@ -1,52 +1,33 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: x-harness
|
|
3
|
-
version: 0.
|
|
4
|
-
description: Use X through
|
|
3
|
+
version: 0.2.0
|
|
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
|
|
|
7
7
|
# X Harness
|
|
8
8
|
|
|
9
|
-
Use the
|
|
9
|
+
Use the root business API. Load only the current task reference; reuse unchanged instructions and a valid binding. Normal use needs no implementation source or type-file reading.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Bind once
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
CLI: `{{CLI_PATH}}`
|
|
13
|
+
Read current host CUA documentation; bind a real logged-in `tab` or native `app`, exactly one. Preserve other tasks' tabs/editors.
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
`
|
|
18
|
-
|
|
19
|
-
Requires Node 22+, a host that exposes compatible CUA `tab` or native `app` methods, and the user's own X login. Codex desktop CUA is validated. Installing this Skill into Claude only installs instructions; confirm that host's browser methods before claiming it can execute. If browser access/login is absent, report the actual missing condition; never fabricate execution or borrow the publisher's account.
|
|
20
|
-
|
|
21
|
-
Read the host's CUA documentation before using its controls. Bind/create a task tab through the host's documented API, preserving the user's session. A compatible tab has `url()`, `playwright.evaluate()`, locator methods and optional navigation/AX/screenshot methods; native apps have `getAXState()`, click/key/paste methods. Use the provided connection; do not launch an unrelated automation driver.
|
|
22
|
-
|
|
23
|
-
Initialize the user's own workspace once with `node <CLI> init --directory <user workspace> --account <user handle>`. It returns `ledgerPath`, `proofDirectory`, `taskId` and account. Existing project configurations and ledgers remain the owner; reuse those instead of creating duplicate state. Never import the publisher's operations directories, policies or credentials.
|
|
24
|
-
|
|
25
|
-
In the host's Node-capable REPL:
|
|
15
|
+
Installed module: `{{MODULE_URL}}`; CLI: `{{CLI_PATH}}`.
|
|
16
|
+
In this repository use the absolute file URL of `<harness-repository>/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).
|
|
26
17
|
|
|
27
18
|
```js
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const result = await x.readCreator({
|
|
31
|
-
handle: "USER_HANDLE",
|
|
32
|
-
target: 5,
|
|
33
|
-
mode: "decision",
|
|
34
|
-
});
|
|
35
|
-
// Print only the needed result; keep the full browser observation in the REPL.
|
|
19
|
+
var { createXGrowthHarness } = await import(moduleURL);
|
|
20
|
+
var x = createXGrowthHarness({ tab, ledgerPath, proofDirectory });
|
|
36
21
|
```
|
|
37
22
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
## Execution
|
|
41
|
-
|
|
42
|
-
Choose the single named task: readCreator/readBookmarks/searchSources/readPost/readPerson/readFeedback/inspectAccount; publishPost/replyToPost/followOne/followBatch; inspectRound/runGrowthRound; verifySubmission/verifyPublishedPost/verifyFollow. `receipt` is the default compact result; `decision` includes full relevant evidence; `delta` rereads and returns changed complete evidence plus unchanged IDs. Do not trim the target's body to save tokens.
|
|
43
|
-
|
|
44
|
-
Writing needs explicit authorization from the current user, verified identity, their own ledger plan, reviewed prepared actions and existing evidence files. Initialization plans zero writes. Set the authorized planned count in the original ledger before execution; never expand an authorized scope yourself. Check the installed types for action, quality and executor fields; retain one owner for the ledger, do not invent approvals or evidence. The current runtime writes only 07:00–22:00 Asia/Shanghai and follow candidate gates require reviewed Chinese-language, blue-check candidates; disclose these conditions when relevant.
|
|
45
|
-
|
|
46
|
-
Interpret returned status literally: `verified` requires public proof; `unknown` is not success. For an unknown submission, call verifySubmission on the same action before any new submission. `needsCapability`, `needsDecision`, `waiting` and partial windows must be reported, with the next required condition. Capture visual media before making claims about its contents.
|
|
23
|
+
## Load by task
|
|
47
24
|
|
|
48
|
-
|
|
25
|
+
| Current need | Read only |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| Creator/bookmarks/search/post/person/feedback/account | [reads](references/reads.md) |
|
|
28
|
+
| Publish/reply/follow or a round | [writes](references/writes.md), preamble + common review + matching operation heading |
|
|
29
|
+
| Unknown submission, missing capability or connection failure | [recovery](references/recovery.md) |
|
|
49
30
|
|
|
50
|
-
|
|
31
|
+
Default `receipt` is for status; use `decision` on the first call needing content, `delta` for changed evidence with valid prior context. Keep observations in REPL/proof; return complete relevant evidence and actionable warnings. Never truncate target text or print full DOM/AX. Root calls own capability checks, intent, submission, verification and settlement; verified results need no extra preflight/reload/verify.
|
|
51
32
|
|
|
52
|
-
|
|
33
|
+
Writes require user authorization, verified identity, reviewed evidence and the original ledger plan; project rules own policy. Unknown submission: verify the same action before any new submission. Follow recovery for other failures; read source only to repair a concrete defect/unsupported capability, verify, then return to root calls.
|