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