@nextclaw/x-harness 0.1.0 → 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.
- package/README.md +2 -2
- package/cli/main.js +23 -11
- package/dist/api.js +53 -1
- package/dist/browser-dom-capabilities.d.ts +4 -0
- package/dist/browser-dom-capabilities.js +110 -0
- package/dist/browser-dom-dependencies.d.ts +16 -0
- package/dist/browser-dom-editing.d.ts +148 -0
- package/dist/browser-dom-editing.js +386 -0
- package/dist/browser-dom-navigation.d.ts +9 -0
- package/dist/browser-dom-navigation.js +165 -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 +2 -0
- package/dist/browser-dom-submission.js +82 -0
- package/dist/browser-dom.d.ts +1 -1
- package/dist/browser-dom.js +51 -802
- package/dist/browser-native.js +32 -28
- package/dist/browser.js +6 -2
- package/dist/creator.js +1 -1
- package/dist/growth-batch.d.ts +5 -0
- package/dist/growth-batch.js +168 -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-execution.d.ts +11 -0
- package/dist/growth-execution.js +315 -0
- package/dist/growth-parallel.d.ts +4 -0
- package/dist/growth-parallel.js +301 -0
- package/dist/growth-plan.d.ts +4 -0
- package/dist/growth-plan.js +207 -0
- package/dist/growth-policy.d.ts +9 -0
- package/dist/growth-policy.js +181 -0
- package/dist/growth-publishing.d.ts +5 -0
- package/dist/growth-publishing.js +240 -0
- package/dist/growth-reading.d.ts +4 -0
- package/dist/growth-reading.js +135 -0
- package/dist/growth-recovery.d.ts +6 -0
- package/dist/growth-recovery.js +158 -0
- package/dist/growth-state.d.ts +8 -0
- package/dist/growth-state.js +183 -0
- package/dist/growth-tasks.d.ts +17 -0
- package/dist/growth-tasks.js +101 -0
- package/dist/growth.d.ts +1 -0
- package/dist/growth.js +122 -1902
- package/dist/platform-capabilities.d.ts +4 -0
- package/dist/platform-capabilities.js +19 -0
- package/dist/platform-collections.d.ts +7 -0
- package/dist/platform-collections.js +223 -0
- package/dist/platform-dependencies.d.ts +43 -0
- package/dist/platform-diagnostics.d.ts +9 -0
- package/dist/platform-diagnostics.js +51 -0
- package/dist/platform-observation.d.ts +8 -0
- package/dist/platform-observation.js +75 -0
- package/dist/platform-rules.d.ts +17 -0
- package/dist/platform-rules.js +41 -0
- package/dist/platform-targets.d.ts +7 -0
- package/dist/platform-targets.js +129 -0
- package/dist/platform-writing.d.ts +8 -0
- package/dist/platform-writing.js +171 -0
- package/dist/platform.d.ts +1 -1
- package/dist/platform.js +56 -677
- package/dist/readers-actions.d.ts +2 -0
- package/dist/readers-actions.js +66 -0
- package/dist/readers-ax-host.d.ts +2 -0
- package/dist/readers-ax-host.js +125 -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-tree.d.ts +19 -0
- package/dist/readers-ax-tree.js +110 -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-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/results.js +1 -1
- package/dist/round-state.js +2 -2
- package/dist/types.d.ts +10 -0
- package/generated/observation.mjs +2 -1
- package/package.json +1 -1
- 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/readers.js +0 -1045
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { same } from "./growth-constants.js";
|
|
3
|
+
export function createParallelRound(dependencies) {
|
|
4
|
+
const { load, scope, result, store, executor, runtimeId, stamp, evidenceExists, execute, clock, releaseSession, session, } = dependencies;
|
|
5
|
+
async function parallelRound({ taskId, decision, selector, requestedKind, maxActions = 10, } = {}) {
|
|
6
|
+
if (!executor)
|
|
7
|
+
throw new Error("parallel-executor-required");
|
|
8
|
+
if (session.running)
|
|
9
|
+
return {
|
|
10
|
+
status: "needsDecision",
|
|
11
|
+
mayEnd: false,
|
|
12
|
+
taskId,
|
|
13
|
+
decision: [{ reason: "same-tab-operation-running" }],
|
|
14
|
+
};
|
|
15
|
+
session.running = true;
|
|
16
|
+
let completed = 0;
|
|
17
|
+
const needs = [];
|
|
18
|
+
try {
|
|
19
|
+
if (!Number.isInteger(maxActions) || maxActions < 1 || maxActions > 100)
|
|
20
|
+
throw new Error("maxActions须为1–100");
|
|
21
|
+
let snapshot = await load(taskId);
|
|
22
|
+
const activeKinds = scope(snapshot.state);
|
|
23
|
+
if (requestedKind && !activeKinds.includes(requestedKind))
|
|
24
|
+
return {
|
|
25
|
+
...result(snapshot),
|
|
26
|
+
status: "needsDecision",
|
|
27
|
+
decision: [{ reason: "kind-outside-round-scope" }],
|
|
28
|
+
};
|
|
29
|
+
snapshot = await store.transaction((latest) => {
|
|
30
|
+
if (latest.state.batchId !== taskId)
|
|
31
|
+
throw new Error("original-task-changed");
|
|
32
|
+
const next = structuredClone(latest.state);
|
|
33
|
+
next.executions ??= {};
|
|
34
|
+
if (Object.values(next.executions).some((e) => (e.tabId === executor.tabId || e.id === executor.id) &&
|
|
35
|
+
e.runtimeId !== runtimeId &&
|
|
36
|
+
(e.activity || e.actionId) &&
|
|
37
|
+
store.ownerAlive(e.pid)))
|
|
38
|
+
throw new Error("same-tab-or-executor-claimed");
|
|
39
|
+
next.executions[executor.id] = {
|
|
40
|
+
...next.executions[executor.id],
|
|
41
|
+
...executor,
|
|
42
|
+
runtimeId,
|
|
43
|
+
pid: store.processId,
|
|
44
|
+
activity: "round",
|
|
45
|
+
status: "RUNNING",
|
|
46
|
+
updatedAt: stamp(),
|
|
47
|
+
};
|
|
48
|
+
if (decision?.type === "prepareActions") {
|
|
49
|
+
if (!Array.isArray(decision.actions) ||
|
|
50
|
+
!same(next.accountHandle, decision.accountHandle) ||
|
|
51
|
+
!evidenceExists(decision.ownershipEvidenceRef))
|
|
52
|
+
throw new Error("需原账号核验和完整动作队列");
|
|
53
|
+
next.actions ??= [];
|
|
54
|
+
for (const action of decision.actions) {
|
|
55
|
+
if (!action.id ||
|
|
56
|
+
!activeKinds.includes(action.kind) ||
|
|
57
|
+
action.status !== "prepared" ||
|
|
58
|
+
next.actions.some((a) => a.id === action.id))
|
|
59
|
+
throw new Error("原prepared动作ID无效");
|
|
60
|
+
next.actions.push(structuredClone(action));
|
|
61
|
+
}
|
|
62
|
+
if (next.items.some((i) => next.actions.filter((a) => a.kind === i.kind && a.status === "prepared").length >
|
|
63
|
+
i.planned - i.verified - i.unknown))
|
|
64
|
+
throw new Error("队列超过原目标");
|
|
65
|
+
if (decision.sharedAttempts) {
|
|
66
|
+
if (!Array.isArray(decision.sharedAttempts))
|
|
67
|
+
throw new Error("共享尝试须为数组");
|
|
68
|
+
next.attempts = [
|
|
69
|
+
...(next.attempts ?? []),
|
|
70
|
+
...structuredClone(decision.sharedAttempts),
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
if (decision.attemptsCoverage) {
|
|
74
|
+
if (!decision.attemptsCoverage.complete ||
|
|
75
|
+
!evidenceExists(decision.attemptsCoverage.evidenceRef))
|
|
76
|
+
throw new Error("共享历史缺证据");
|
|
77
|
+
next.attemptsCoverage = structuredClone(decision.attemptsCoverage);
|
|
78
|
+
}
|
|
79
|
+
if (decision.nextScheduledCheckAt)
|
|
80
|
+
next.executions[executor.id].nextScheduledCheckAt =
|
|
81
|
+
decision.nextScheduledCheckAt;
|
|
82
|
+
}
|
|
83
|
+
else if (decision) {
|
|
84
|
+
const action = next.actions?.find((a) => a.id === decision.actionId);
|
|
85
|
+
if (!action ||
|
|
86
|
+
!activeKinds.includes(action.kind) ||
|
|
87
|
+
action.status !== "prepared" ||
|
|
88
|
+
action.claim ||
|
|
89
|
+
typeof decision.approved !== "boolean" ||
|
|
90
|
+
!evidenceExists(decision.evidenceRef))
|
|
91
|
+
throw new Error("审查只绑定职责内未领取的原prepared动作");
|
|
92
|
+
if (!decision.approved)
|
|
93
|
+
action.status = "skipped";
|
|
94
|
+
else
|
|
95
|
+
action.quality = {
|
|
96
|
+
approved: true,
|
|
97
|
+
evidenceRef: decision.evidenceRef,
|
|
98
|
+
...(action.kind === "follow"
|
|
99
|
+
? {
|
|
100
|
+
handle: action.handle,
|
|
101
|
+
blue: decision.blue === true,
|
|
102
|
+
chinese: decision.chinese === true,
|
|
103
|
+
}
|
|
104
|
+
: {
|
|
105
|
+
approvedText: decision.approvedText,
|
|
106
|
+
...(action.kind === "original"
|
|
107
|
+
? { draftFingerprint: decision.draftFingerprint }
|
|
108
|
+
: {}),
|
|
109
|
+
}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
next.updatedAt = stamp();
|
|
113
|
+
return next;
|
|
114
|
+
});
|
|
115
|
+
for (const kind of activeKinds) {
|
|
116
|
+
const queue = (snapshot.state.actions ?? [])
|
|
117
|
+
.filter((a) => a.kind === kind && (!selector || selector(a)))
|
|
118
|
+
.sort((a, b) => Number(["intent", "unknown"].includes(b.status)) -
|
|
119
|
+
Number(["intent", "unknown"].includes(a.status)));
|
|
120
|
+
if (snapshot.state.items.find((i) => i.kind === kind).unknown >
|
|
121
|
+
snapshot.state.actions.filter((a) => a.kind === kind && ["intent", "unknown"].includes(a.status)).length) {
|
|
122
|
+
needs.push({
|
|
123
|
+
status: "needsDecision",
|
|
124
|
+
reason: "original-unmapped-unknown-needs-dedup",
|
|
125
|
+
kind,
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
for (const original of queue) {
|
|
130
|
+
if (completed >= maxActions)
|
|
131
|
+
break;
|
|
132
|
+
if (["verified", "preexisting", "skipped"].includes(original.status))
|
|
133
|
+
continue;
|
|
134
|
+
try {
|
|
135
|
+
snapshot = await store.transaction((latest) => {
|
|
136
|
+
const next = structuredClone(latest.state), action = next.actions.find((a) => a.id === original.id);
|
|
137
|
+
if (next.batchId !== taskId)
|
|
138
|
+
throw new Error("original-task-changed");
|
|
139
|
+
if (["verified", "preexisting", "skipped"].includes(action.status))
|
|
140
|
+
return null;
|
|
141
|
+
const sameTarget = (other) => other.kind === action.kind &&
|
|
142
|
+
(kind === "follow"
|
|
143
|
+
? same(other.handle, action.handle)
|
|
144
|
+
: kind === "original"
|
|
145
|
+
? other.draftPath === action.draftPath
|
|
146
|
+
: other.postId === action.postId &&
|
|
147
|
+
other.text === action.text);
|
|
148
|
+
if (next.actions.some((a) => a.id !== action.id &&
|
|
149
|
+
sameTarget(a) &&
|
|
150
|
+
(a.claim ||
|
|
151
|
+
["intent", "unknown", "verified"].includes(a.status))))
|
|
152
|
+
throw new Error("same-target-already-claimed-or-submitted");
|
|
153
|
+
if (action.claim && store.ownerAlive(action.claim.pid))
|
|
154
|
+
throw new Error("same-action-claimed");
|
|
155
|
+
if (Object.values(next.executions).some((e) => e.tabId === executor.tabId &&
|
|
156
|
+
e.actionId &&
|
|
157
|
+
e.runtimeId !== runtimeId &&
|
|
158
|
+
store.ownerAlive(e.pid)))
|
|
159
|
+
throw new Error("same-tab-claimed");
|
|
160
|
+
session.claim = {
|
|
161
|
+
actionId: action.id,
|
|
162
|
+
token: randomUUID(),
|
|
163
|
+
executorId: executor.id,
|
|
164
|
+
runtimeId,
|
|
165
|
+
pid: store.processId,
|
|
166
|
+
acquiredAt: stamp(),
|
|
167
|
+
};
|
|
168
|
+
action.claim = session.claim;
|
|
169
|
+
next.executions[executor.id].actionId = action.id;
|
|
170
|
+
next.updatedAt = stamp();
|
|
171
|
+
return next;
|
|
172
|
+
});
|
|
173
|
+
if (!session.claim)
|
|
174
|
+
continue;
|
|
175
|
+
const outcome = await execute(snapshot, snapshot.state.actions.find((a) => a.id === original.id));
|
|
176
|
+
snapshot = outcome.snapshot;
|
|
177
|
+
completed++;
|
|
178
|
+
if (outcome.need) {
|
|
179
|
+
needs.push(outcome.need);
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
needs.push({
|
|
185
|
+
status: "needsDecision",
|
|
186
|
+
kind,
|
|
187
|
+
actionId: original.id,
|
|
188
|
+
reason: /claimed|same-target/.test(error.message)
|
|
189
|
+
? "action-or-tab-claimed"
|
|
190
|
+
: /budget|capacity/.test(error.message)
|
|
191
|
+
? "shared-reservation-unavailable"
|
|
192
|
+
: "harness-boundary-error",
|
|
193
|
+
error: error.message.slice(0, 240),
|
|
194
|
+
});
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
if (session.claim) {
|
|
199
|
+
const owned = session.claim;
|
|
200
|
+
session.claim = null;
|
|
201
|
+
snapshot = await store.transaction((latest) => {
|
|
202
|
+
const next = structuredClone(latest.state), action = next.actions?.find((a) => a.id === owned.actionId);
|
|
203
|
+
if (action?.claim?.token !== owned.token)
|
|
204
|
+
return null;
|
|
205
|
+
delete action.claim;
|
|
206
|
+
if (next.executions[executor.id]?.runtimeId === runtimeId)
|
|
207
|
+
delete next.executions[executor.id].actionId;
|
|
208
|
+
next.updatedAt = stamp();
|
|
209
|
+
return next;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
snapshot = await store.transaction(async (latest) => {
|
|
216
|
+
const next = structuredClone(latest.state), own = next.executions[executor.id];
|
|
217
|
+
const pending = next.items.filter((i) => activeKinds.includes(i.kind) &&
|
|
218
|
+
(i.verified < i.planned || i.unknown));
|
|
219
|
+
own.status = pending.length ? "RUNNING" : "COMPLETE";
|
|
220
|
+
const scheduled = own.nextScheduledCheckAt ??
|
|
221
|
+
next.execution?.nextScheduledCheckAt ??
|
|
222
|
+
next.stop?.nextScheduledCheckAt;
|
|
223
|
+
const waits = pending.map((i) => needs.find((n) => n.status === "waiting" &&
|
|
224
|
+
next.actions.find((a) => a.id === n.actionId)?.kind ===
|
|
225
|
+
i.kind));
|
|
226
|
+
if (pending.length &&
|
|
227
|
+
pending.every((i) => !i.unknown &&
|
|
228
|
+
next.actions.filter((a) => a.kind === i.kind && a.status === "prepared").length >=
|
|
229
|
+
i.planned - i.verified) &&
|
|
230
|
+
waits.every((w) => w && Date.parse(w.resumeAt) > clock()) &&
|
|
231
|
+
Date.parse(scheduled) > clock()) {
|
|
232
|
+
for (let i = 0; i < pending.length; i++) {
|
|
233
|
+
const proof = await store.recordProof({
|
|
234
|
+
observedAt: stamp(),
|
|
235
|
+
...waits[i],
|
|
236
|
+
attempts: next.attempts,
|
|
237
|
+
});
|
|
238
|
+
Object.assign(pending[i], {
|
|
239
|
+
actionable: false,
|
|
240
|
+
reason: `harness:${waits[i].reason}`,
|
|
241
|
+
resumeAt: waits[i].resumeAt,
|
|
242
|
+
resumeWhen: `不早于${waits[i].resumeAt}重核`,
|
|
243
|
+
blockEvidence: [proof],
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
own.status = "WAITING";
|
|
247
|
+
own.stop = {
|
|
248
|
+
independentActionsHandled: true,
|
|
249
|
+
nextScheduledCheckAt: scheduled,
|
|
250
|
+
nextEligibleCheckAt: waits.map((w) => w.resumeAt).sort()[0],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
next.status = next.items.every((i) => i.verified === i.planned && !i.unknown)
|
|
254
|
+
? "COMPLETE"
|
|
255
|
+
: "RUNNING";
|
|
256
|
+
own.updatedAt = next.updatedAt = stamp();
|
|
257
|
+
return next;
|
|
258
|
+
});
|
|
259
|
+
const out = result(snapshot, needs);
|
|
260
|
+
if (!out.mayEnd && !needs.length && completed >= maxActions)
|
|
261
|
+
return {
|
|
262
|
+
...out,
|
|
263
|
+
status: "checkpoint",
|
|
264
|
+
checkpoint: {
|
|
265
|
+
taskId,
|
|
266
|
+
executorId: executor.id,
|
|
267
|
+
completed,
|
|
268
|
+
resume: "resumeGrowthRound",
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
if (!out.mayEnd && !needs.length)
|
|
272
|
+
out.decision = [
|
|
273
|
+
{ reason: "prepared-queue-does-not-cover-original-targets" },
|
|
274
|
+
];
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
return {
|
|
279
|
+
status: "needsDecision",
|
|
280
|
+
mayEnd: false,
|
|
281
|
+
taskId,
|
|
282
|
+
evidenceRef: store.path,
|
|
283
|
+
decision: [
|
|
284
|
+
{
|
|
285
|
+
reason: "harness-boundary-error",
|
|
286
|
+
error: error.message.slice(0, 240),
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
try {
|
|
293
|
+
await releaseSession(taskId);
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
session.running = false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return { parallelRound };
|
|
301
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { GrowthDependencies } from "./growth-dependencies.js";
|
|
2
|
+
export declare function createPlannedRound(dependencies: Pick<GrowthDependencies, "store" | "load" | "result" | "evidenceExists" | "save" | "execute" | "clock" | "stamp">): {
|
|
3
|
+
round: ({ taskId, decision, selector, requestedKind, }?: any) => Promise<any>;
|
|
4
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { kinds, same } from "./growth-constants.js";
|
|
2
|
+
export function createPlannedRound(dependencies) {
|
|
3
|
+
const { store, load, result, evidenceExists, save, execute, clock, stamp } = dependencies;
|
|
4
|
+
async function applyRoundDecision(snapshot, decision, activeKinds) {
|
|
5
|
+
if (decision?.type === "prepareActions") {
|
|
6
|
+
if (!Array.isArray(decision.actions) ||
|
|
7
|
+
!/^[A-Za-z0-9_]{1,15}$/.test(decision.accountHandle ?? "") ||
|
|
8
|
+
!evidenceExists(decision.ownershipEvidenceRef) ||
|
|
9
|
+
(snapshot.state.execution?.owner &&
|
|
10
|
+
snapshot.state.execution.owner !== "harness"))
|
|
11
|
+
throw new Error("需原账号执行权核验和完整动作队列;不接管已有执行者");
|
|
12
|
+
if (snapshot.state.accountHandle &&
|
|
13
|
+
!same(snapshot.state.accountHandle, decision.accountHandle))
|
|
14
|
+
throw new Error("不能变更原账号");
|
|
15
|
+
const next = structuredClone(snapshot.state);
|
|
16
|
+
next.actions ??= [];
|
|
17
|
+
for (const action of decision.actions) {
|
|
18
|
+
if (!action.id ||
|
|
19
|
+
!activeKinds.includes(action.kind) ||
|
|
20
|
+
action.status !== "prepared" ||
|
|
21
|
+
next.actions.some((a) => a.id === action.id))
|
|
22
|
+
throw new Error("仅添加职责范围内唯一ID的prepared原动作");
|
|
23
|
+
next.actions.push(structuredClone(action));
|
|
24
|
+
}
|
|
25
|
+
if (next.items.some((item) => next.actions.filter((a) => a.kind === item.kind && a.status === "prepared").length >
|
|
26
|
+
item.planned - item.verified - item.unknown))
|
|
27
|
+
throw new Error("prepared队列不能越过原批次未决目标");
|
|
28
|
+
next.accountHandle = decision.accountHandle;
|
|
29
|
+
next.execution = {
|
|
30
|
+
...next.execution,
|
|
31
|
+
owner: "harness",
|
|
32
|
+
exclusive: true,
|
|
33
|
+
ownershipEvidenceRef: decision.ownershipEvidenceRef,
|
|
34
|
+
...(decision.nextScheduledCheckAt
|
|
35
|
+
? { nextScheduledCheckAt: decision.nextScheduledCheckAt }
|
|
36
|
+
: {}),
|
|
37
|
+
};
|
|
38
|
+
if (decision.attemptsCoverage) {
|
|
39
|
+
if (!decision.attemptsCoverage.complete ||
|
|
40
|
+
!evidenceExists(decision.attemptsCoverage.evidenceRef))
|
|
41
|
+
throw new Error("共享历史须完整取证;不能默认为零");
|
|
42
|
+
next.attemptsCoverage = structuredClone(decision.attemptsCoverage);
|
|
43
|
+
}
|
|
44
|
+
if (decision.sharedAttempts) {
|
|
45
|
+
if (!Array.isArray(decision.sharedAttempts))
|
|
46
|
+
throw new Error("共享尝试记录必须为数组");
|
|
47
|
+
next.attempts = [
|
|
48
|
+
...(next.attempts ?? []),
|
|
49
|
+
...structuredClone(decision.sharedAttempts),
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
snapshot = await save(snapshot, next);
|
|
53
|
+
}
|
|
54
|
+
else if (decision) {
|
|
55
|
+
const action = snapshot.state.actions?.find((a) => a.id === decision.actionId);
|
|
56
|
+
if (action && !activeKinds.includes(action.kind))
|
|
57
|
+
throw new Error("kind-outside-round-scope");
|
|
58
|
+
if (!action ||
|
|
59
|
+
action.status !== "prepared" ||
|
|
60
|
+
typeof decision.approved !== "boolean" ||
|
|
61
|
+
!evidenceExists(decision.evidenceRef))
|
|
62
|
+
throw new Error("决策仅能绑定原prepared动作及真实审查证据");
|
|
63
|
+
const next = structuredClone(snapshot.state), target = next.actions.find((a) => a.id === action.id);
|
|
64
|
+
if (!decision.approved)
|
|
65
|
+
target.status = "skipped";
|
|
66
|
+
else
|
|
67
|
+
target.quality = {
|
|
68
|
+
approved: true,
|
|
69
|
+
evidenceRef: decision.evidenceRef,
|
|
70
|
+
...(action.kind === "follow"
|
|
71
|
+
? {
|
|
72
|
+
handle: action.handle,
|
|
73
|
+
blue: decision.blue === true,
|
|
74
|
+
chinese: decision.chinese === true,
|
|
75
|
+
}
|
|
76
|
+
: {
|
|
77
|
+
approvedText: decision.approvedText,
|
|
78
|
+
...(action.kind === "original"
|
|
79
|
+
? { draftFingerprint: decision.draftFingerprint }
|
|
80
|
+
: {}),
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
snapshot = await save(snapshot, next);
|
|
84
|
+
}
|
|
85
|
+
return snapshot;
|
|
86
|
+
}
|
|
87
|
+
async function round({ taskId, decision, selector, requestedKind, } = {}) {
|
|
88
|
+
try {
|
|
89
|
+
return await store.withLock(async () => {
|
|
90
|
+
let snapshot = await load(taskId);
|
|
91
|
+
const activeKinds = snapshot.state.scopeKinds ?? kinds;
|
|
92
|
+
if (requestedKind && !activeKinds.includes(requestedKind))
|
|
93
|
+
return {
|
|
94
|
+
...result(snapshot),
|
|
95
|
+
status: "needsDecision",
|
|
96
|
+
mayEnd: false,
|
|
97
|
+
decision: [
|
|
98
|
+
{ kind: requestedKind, reason: "kind-outside-round-scope" },
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
snapshot = await applyRoundDecision(snapshot, decision, activeKinds);
|
|
102
|
+
const needs = [];
|
|
103
|
+
for (const kind of activeKinds) {
|
|
104
|
+
const queue = (snapshot.state.actions ?? [])
|
|
105
|
+
.filter((a) => a.kind === kind && (!selector || selector(a)))
|
|
106
|
+
.sort((a, b) => Number(["intent", "unknown"].includes(b.status)) -
|
|
107
|
+
Number(["intent", "unknown"].includes(a.status)));
|
|
108
|
+
// A selector narrows this invocation, not the original unresolved ledger.
|
|
109
|
+
const unresolved = (snapshot.state.actions ?? []).filter((a) => a.kind === kind && ["intent", "unknown"].includes(a.status)).length;
|
|
110
|
+
if (snapshot.state.items.find((i) => i.kind === kind).unknown >
|
|
111
|
+
unresolved) {
|
|
112
|
+
needs.push({
|
|
113
|
+
status: "needsDecision",
|
|
114
|
+
reason: "original-unmapped-unknown-needs-dedup",
|
|
115
|
+
kind,
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
for (const original of queue) {
|
|
120
|
+
const item = snapshot.state.items.find((i) => i.kind === kind);
|
|
121
|
+
const action = snapshot.state.actions.find((a) => a.id === original.id);
|
|
122
|
+
if (["verified", "preexisting", "skipped"].includes(action.status))
|
|
123
|
+
continue;
|
|
124
|
+
if (item.verified >= item.planned && action.status === "prepared")
|
|
125
|
+
continue;
|
|
126
|
+
const outcome = await execute(snapshot, action);
|
|
127
|
+
snapshot = outcome.snapshot;
|
|
128
|
+
if (outcome.need) {
|
|
129
|
+
needs.push(outcome.need);
|
|
130
|
+
break;
|
|
131
|
+
} // Stop only this kind; continue independent kinds.
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (snapshot.state.items
|
|
135
|
+
.filter((i) => activeKinds.includes(i.kind))
|
|
136
|
+
.every((i) => i.verified === i.planned && i.unknown === 0)) {
|
|
137
|
+
const next = structuredClone(snapshot.state);
|
|
138
|
+
next.status = "COMPLETE";
|
|
139
|
+
snapshot = await save(snapshot, next);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
const pending = snapshot.state.items.filter((i) => activeKinds.includes(i.kind) &&
|
|
143
|
+
(i.verified < i.planned || i.unknown > 0));
|
|
144
|
+
const waits = pending.map((item) => needs.find((n) => n.status === "waiting" &&
|
|
145
|
+
snapshot.state.actions.find((a) => a.id === n.actionId)
|
|
146
|
+
?.kind === item.kind));
|
|
147
|
+
const scheduled = snapshot.state.execution?.nextScheduledCheckAt ??
|
|
148
|
+
snapshot.state.stop?.nextScheduledCheckAt;
|
|
149
|
+
const covered = pending.every((item) => item.unknown === 0 &&
|
|
150
|
+
(snapshot.state.actions ?? []).filter((a) => a.kind === item.kind && a.status === "prepared").length >=
|
|
151
|
+
item.planned - item.verified);
|
|
152
|
+
if (covered &&
|
|
153
|
+
waits.every((w) => w && Date.parse(w.resumeAt) > clock()) &&
|
|
154
|
+
Date.parse(scheduled) > clock()) {
|
|
155
|
+
const next = structuredClone(snapshot.state);
|
|
156
|
+
for (let i = 0; i < pending.length; i++) {
|
|
157
|
+
const item = next.items.find((a) => a.kind === pending[i].kind), wait = waits[i];
|
|
158
|
+
const proof = await store.recordProof({
|
|
159
|
+
observedAt: stamp(),
|
|
160
|
+
reason: wait.reason,
|
|
161
|
+
resumeAt: wait.resumeAt,
|
|
162
|
+
attempts: next.attempts,
|
|
163
|
+
});
|
|
164
|
+
Object.assign(item, {
|
|
165
|
+
actionable: false,
|
|
166
|
+
reason: `harness:${wait.reason}`,
|
|
167
|
+
resumeAt: wait.resumeAt,
|
|
168
|
+
resumeWhen: `不早于${wait.resumeAt}重核预算及fresh状态`,
|
|
169
|
+
blockEvidence: [proof],
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
next.status = "WAITING";
|
|
173
|
+
next.stop = {
|
|
174
|
+
independentActionsHandled: true,
|
|
175
|
+
nextScheduledCheckAt: scheduled,
|
|
176
|
+
nextEligibleCheckAt: waits.map((w) => w.resumeAt).sort()[0],
|
|
177
|
+
};
|
|
178
|
+
snapshot = await save(snapshot, next);
|
|
179
|
+
}
|
|
180
|
+
else if (!needs.length)
|
|
181
|
+
needs.push({
|
|
182
|
+
status: "needsDecision",
|
|
183
|
+
reason: "prepared-queue-does-not-cover-original-targets",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return result(snapshot, needs);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
return {
|
|
191
|
+
status: "needsDecision",
|
|
192
|
+
mayEnd: false,
|
|
193
|
+
taskId,
|
|
194
|
+
evidenceRef: store.path,
|
|
195
|
+
decision: [
|
|
196
|
+
{
|
|
197
|
+
reason: error.code === "CONFLICT"
|
|
198
|
+
? "original-ledger-conflict"
|
|
199
|
+
: "harness-boundary-error",
|
|
200
|
+
error: error.message.slice(0, 240),
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { round };
|
|
207
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GrowthDependencies } from "./growth-dependencies.js";
|
|
2
|
+
export declare function createGrowthPolicy(dependencies: Pick<GrowthDependencies, "executor" | "clock" | "evidenceExists" | "platform" | "store">): {
|
|
3
|
+
authorizedWindow: (state: any, kind: string) => boolean;
|
|
4
|
+
scope: (state: any) => any;
|
|
5
|
+
project: (state: any) => any;
|
|
6
|
+
capability: (operation: any, input?: any) => any;
|
|
7
|
+
waitBudget: (state: any) => any;
|
|
8
|
+
guardInput: (state: any, action: any, independentExecution?: any) => any;
|
|
9
|
+
};
|