harnery 0.22.0 → 0.23.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/README.md +1 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +91 -2
- package/dist/core/work/state.d.ts +7 -0
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +112 -23
- package/dist/core/workflow/child-env.d.ts +10 -6
- package/dist/core/workflow/child-env.d.ts.map +1 -1
- package/dist/core/workflow/child-env.js +15 -7
- package/dist/core/workflow/durable-record.d.ts +7 -0
- package/dist/core/workflow/durable-record.d.ts.map +1 -0
- package/dist/core/workflow/durable-record.js +83 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +208 -29
- package/dist/core/workflow/index.d.ts +2 -0
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +1 -0
- package/dist/core/workflow/proof.d.ts +4 -0
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +50 -15
- package/dist/core/workflow/run-state.d.ts +3 -0
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +25 -13
- package/dist/core/workflow/types.d.ts +10 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/validate.d.ts +10 -1
- package/dist/core/workflow/validate.d.ts.map +1 -1
- package/dist/core/workflow/validate.js +75 -1
- package/dist/core/workflow/workspaces/attestation-error.d.ts +7 -0
- package/dist/core/workflow/workspaces/attestation-error.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/attestation-error.js +25 -0
- package/dist/core/workflow/workspaces/cancellation.d.ts +8 -0
- package/dist/core/workflow/workspaces/cancellation.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/cancellation.js +95 -0
- package/dist/core/workflow/workspaces/cleanup.d.ts +9 -0
- package/dist/core/workflow/workspaces/cleanup.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/cleanup.js +223 -0
- package/dist/core/workflow/workspaces/execution.d.ts +25 -0
- package/dist/core/workflow/workspaces/execution.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/execution.js +316 -0
- package/dist/core/workflow/workspaces/git.d.ts +25 -0
- package/dist/core/workflow/workspaces/git.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/git.js +102 -0
- package/dist/core/workflow/workspaces/index.d.ts +15 -0
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/index.js +7 -0
- package/dist/core/workflow/workspaces/inspect.d.ts +85 -0
- package/dist/core/workflow/workspaces/inspect.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/inspect.js +255 -0
- package/dist/core/workflow/workspaces/integration.d.ts +24 -0
- package/dist/core/workflow/workspaces/integration.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/integration.js +617 -0
- package/dist/core/workflow/workspaces/leases.d.ts +37 -0
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/leases.js +380 -0
- package/dist/core/workflow/workspaces/lifecycle.d.ts +43 -0
- package/dist/core/workflow/workspaces/lifecycle.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/lifecycle.js +622 -0
- package/dist/core/workflow/workspaces/local-git.d.ts +8 -0
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/local-git.js +1474 -0
- package/dist/core/workflow/workspaces/paths.d.ts +23 -0
- package/dist/core/workflow/workspaces/paths.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/paths.js +191 -0
- package/dist/core/workflow/workspaces/recovery.d.ts +3 -0
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/recovery.js +50 -0
- package/dist/core/workflow/workspaces/state.d.ts +60 -0
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/state.js +296 -0
- package/dist/core/workflow/workspaces/types.d.ts +445 -0
- package/dist/core/workflow/workspaces/types.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/types.js +2 -0
- package/dist/core/workflow/workspaces/validate.d.ts +14 -0
- package/dist/core/workflow/workspaces/validate.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/validate.js +476 -0
- package/package.json +5 -3
- package/src/commands/workflow.ts +110 -4
- package/src/core/work/state.ts +156 -28
- package/src/core/workflow/child-env.ts +18 -7
- package/src/core/workflow/durable-record.ts +94 -0
- package/src/core/workflow/engine.ts +230 -30
- package/src/core/workflow/index.ts +73 -0
- package/src/core/workflow/proof.ts +70 -22
- package/src/core/workflow/run-state.ts +46 -11
- package/src/core/workflow/types.ts +14 -0
- package/src/core/workflow/validate.ts +75 -1
- package/src/core/workflow/workspaces/attestation-error.ts +30 -0
- package/src/core/workflow/workspaces/cancellation.ts +137 -0
- package/src/core/workflow/workspaces/cleanup.ts +308 -0
- package/src/core/workflow/workspaces/execution.ts +412 -0
- package/src/core/workflow/workspaces/git.ts +125 -0
- package/src/core/workflow/workspaces/index.ts +85 -0
- package/src/core/workflow/workspaces/inspect.ts +421 -0
- package/src/core/workflow/workspaces/integration.ts +829 -0
- package/src/core/workflow/workspaces/leases.ts +495 -0
- package/src/core/workflow/workspaces/lifecycle.ts +792 -0
- package/src/core/workflow/workspaces/local-git.ts +1892 -0
- package/src/core/workflow/workspaces/paths.ts +247 -0
- package/src/core/workflow/workspaces/recovery.ts +56 -0
- package/src/core/workflow/workspaces/state.ts +481 -0
- package/src/core/workflow/workspaces/types.ts +517 -0
- package/src/core/workflow/workspaces/validate.ts +595 -0
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
- **Multi-agent coordination:** per-agent heartbeats in `.harnery/active/`, claim-time and commit-time guards, the canonical event stream, councils, a decision docket, per-agent scratchpads, and harness adapters for Claude Code / Cursor / Codex.
|
|
23
23
|
- **Cross-machine presence:** sessions on every machine that shares a repo see each other, labeled by machine. Zero-config over the repo's own git remote (presence refs), with an optional live-socket upgrade via an end-to-end-encrypted relay — the public `relay.harnery.com`, or self-host with `harn relay serve` / the bundled Cloudflare Worker (`relay/worker/`). The relay sees only ciphertext in opaquely-named rooms.
|
|
24
24
|
- **Coordination-aware workflows:** `harn workflow run <script>` executes bounded, schema-gated multi-subagent workflow scripts — plain JS stages fanning out to headless harness-CLI subagents that are born coordination-registered (heartbeats, events, claim guards), with deterministic code deciding the routing between stages. An optional host-owned policy gates dispatch and declared external mutations with `ALLOW`, `ASK`, and `DENY`; CLI `ASK` requests park durably for an explicit approve/deny and same-run resume, while library callers remain fail-closed by default.
|
|
25
|
+
- **Optional isolated workspaces:** add `--isolation worktree --workspace-root <dir>` to run a workflow in a host-owned local Git worktree. Harnery freezes the allocation and provider identity, verifies the workspace before execution and resume, requires review and policy authority before fast-forward integration, and keeps cleanup conservative. `harn workflow workspace <run-id>` and the dashboard show the validated lifecycle; shared execution remains the default.
|
|
25
26
|
- **Durable work above attempts:** `harn work` preserves an objective across bounded workflow attempts, dependencies, approval parks, failures, and review. Reusable workflow code receives the exact work ID, title, objective, acceptance criteria, and attempt identity as frozen typed context. A retry also receives the prior run's bounded error and unresolved proof criteria, while retry authority and attempt ceilings remain unchanged. The manifest, proof, and append-only ledger bind the same context, and the one-shot reconciler reports the next explicit action without silently spawning, retrying, or accepting work.
|
|
26
27
|
- **Bounded goal supervision:** `harn supervisor` freezes a mission or existing root graph, specialist team, and safety limits, then runs scheduling cycles until reviewed mission success, human attention, no progress, or budget exhaustion. Its optional planner can create the first milestone, reassess accepted milestone proof, or recover a terminal graph using only a frozen workflow-template catalog. Plans can require independent bounded specialist review before ordinary approval or auto-apply authority, old generations remain immutable evidence, and all attempts still consume the goal budget. Use explicit foreground ticks or enroll goals in the optional heartbeat-backed background service.
|
|
27
28
|
- **Standalone web UI:** `harn web up` boots a local Next.js dashboard for the coord layer, councils, per-project state, and a local coding-agent status view (Claude Code / Codex / Cursor usage, backed by `harn devtools`). Ships with the git clone, not the npm package (see [Install](#install)).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../src/commands/workflow.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../src/commands/workflow.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAiDnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAmejF"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
1
2
|
import { workflowSubscriptionOnly } from "../core/config.js";
|
|
2
3
|
import { createBuiltinHarnessRegistry } from "../core/harnesses/index.js";
|
|
3
4
|
import { findCoordRoot } from "../core/hooks/resolve/coord-root.js";
|
|
@@ -24,6 +25,7 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
24
25
|
"without this the engine refuses that silent-override state")
|
|
25
26
|
.option("--policy <file>", "Host policy JSON/JSONC applied before dispatch and external mutation")
|
|
26
27
|
.option("--isolation <mode>", "Host-created execution boundary: shared | worktree | sandbox | remote (default shared)")
|
|
28
|
+
.option("--workspace-root <dir>", "Explicit writable parent for the built-in local Git worktree provider (requires --isolation worktree)")
|
|
27
29
|
.option("--approval-to <address>", "Address durable ASK requests (default: operator)")
|
|
28
30
|
.option("--json", "Emit the full RunReport as JSON")
|
|
29
31
|
.action(async (script, opts) => {
|
|
@@ -50,6 +52,13 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
50
52
|
});
|
|
51
53
|
process.exit(1);
|
|
52
54
|
}
|
|
55
|
+
if (opts.workspaceRoot && opts.isolation !== "worktree") {
|
|
56
|
+
emit.error({
|
|
57
|
+
code: "bad_workspace_root",
|
|
58
|
+
message: "--workspace-root requires --isolation worktree",
|
|
59
|
+
});
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
53
62
|
// Flag beats config; the two flags contradict each other by design.
|
|
54
63
|
const subscriptionOnly = opts.subscriptionOnly || workflowSubscriptionOnly(coordRoot);
|
|
55
64
|
if (subscriptionOnly && opts.allowApiBilling) {
|
|
@@ -61,7 +70,13 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
61
70
|
process.exit(1);
|
|
62
71
|
}
|
|
63
72
|
try {
|
|
64
|
-
const { runWorkflow } = await import("../core/workflow/
|
|
73
|
+
const { createLocalGitWorktreeProvider, runWorkflow } = await import("../core/workflow/index.js");
|
|
74
|
+
const workspace = opts.workspaceRoot
|
|
75
|
+
? {
|
|
76
|
+
provider: createLocalGitWorktreeProvider({ coordRoot }),
|
|
77
|
+
writableRoots: [resolve(opts.workspaceRoot)],
|
|
78
|
+
}
|
|
79
|
+
: undefined;
|
|
65
80
|
const report = await runWorkflow(script, {
|
|
66
81
|
coordRoot,
|
|
67
82
|
spawners: registry.spawners(),
|
|
@@ -82,6 +97,7 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
82
97
|
approvalMode: "park",
|
|
83
98
|
approvalAddressee: opts.approvalTo,
|
|
84
99
|
isolation: opts.isolation,
|
|
100
|
+
workspace,
|
|
85
101
|
// CLI harness subprocesses inherit the host network. A policy that
|
|
86
102
|
// forbids network must therefore deny dispatch unless a future host
|
|
87
103
|
// adapter creates and declares a network-disabled boundary.
|
|
@@ -146,12 +162,22 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
146
162
|
process.exit(1);
|
|
147
163
|
}
|
|
148
164
|
try {
|
|
149
|
-
const { assertWorkflowRunResumable, runWorkflow } = await import("../core/workflow/index.js");
|
|
165
|
+
const { assertWorkflowRunResumable, createLocalGitWorktreeProvider, runWorkflow } = await import("../core/workflow/index.js");
|
|
150
166
|
const { manifest } = assertWorkflowRunResumable(coordRoot, runId);
|
|
167
|
+
const binding = manifest.execution.workspace_binding;
|
|
168
|
+
if (binding && binding.provider.id !== "local-git-worktree") {
|
|
169
|
+
throw new Error(`workflow run ${runId} requires unavailable workspace provider ${binding.provider.id}`);
|
|
170
|
+
}
|
|
151
171
|
const report = await runWorkflow(manifest.script.path, {
|
|
152
172
|
coordRoot,
|
|
153
173
|
spawners: registry.spawners(),
|
|
154
174
|
resumeRunId: runId,
|
|
175
|
+
workspace: binding
|
|
176
|
+
? {
|
|
177
|
+
provider: createLocalGitWorktreeProvider({ coordRoot }),
|
|
178
|
+
writableRoots: [binding.writable_root.configured],
|
|
179
|
+
}
|
|
180
|
+
: undefined,
|
|
155
181
|
harnessEvidence: Object.fromEntries(registry
|
|
156
182
|
.list()
|
|
157
183
|
.map((adapter) => [
|
|
@@ -199,6 +225,69 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
199
225
|
process.exit(1);
|
|
200
226
|
}
|
|
201
227
|
});
|
|
228
|
+
workflow
|
|
229
|
+
.command("workspace <run-id>")
|
|
230
|
+
.description("Show validated allocation, verification, integration, and cleanup state.")
|
|
231
|
+
.option("--json", "Emit the validated workspace status as JSON")
|
|
232
|
+
.action(async (runId, opts) => {
|
|
233
|
+
const coordRoot = findCoordRoot();
|
|
234
|
+
if (!coordRoot) {
|
|
235
|
+
emit.error({
|
|
236
|
+
code: "no_coord_root",
|
|
237
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
238
|
+
});
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
const { inspectWorkflowWorkspace, renderWorkflowWorkspaceStatus } = await import("../core/workflow/index.js");
|
|
242
|
+
const inspection = inspectWorkflowWorkspace(coordRoot, runId);
|
|
243
|
+
if (!inspection.ok) {
|
|
244
|
+
emit.error({ code: "workflow_workspace_invalid", message: inspection.error });
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
if (opts.json) {
|
|
248
|
+
emit.config({ format: "json" });
|
|
249
|
+
emit.data(inspection.value);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
emit.text(renderWorkflowWorkspaceStatus(inspection.value));
|
|
253
|
+
});
|
|
254
|
+
workflow
|
|
255
|
+
.command("workspaces")
|
|
256
|
+
.description("List validated isolated and shared-compatibility workspace decisions.")
|
|
257
|
+
.option("--json", "Emit workspace inspections as JSON")
|
|
258
|
+
.action(async (opts) => {
|
|
259
|
+
const coordRoot = findCoordRoot();
|
|
260
|
+
if (!coordRoot) {
|
|
261
|
+
emit.error({
|
|
262
|
+
code: "no_coord_root",
|
|
263
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
264
|
+
});
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
const { listWorkflowWorkspaceInspections } = await import("../core/workflow/index.js");
|
|
268
|
+
const inspections = listWorkflowWorkspaceInspections(coordRoot).filter((inspection) => !inspection.ok || inspection.value.selection !== "shared");
|
|
269
|
+
if (opts.json) {
|
|
270
|
+
emit.config({ format: "json" });
|
|
271
|
+
emit.data(inspections);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (inspections.length === 0) {
|
|
275
|
+
emit.text("no isolated or shared-compatibility workspace runs\n");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
emit.text(`${inspections
|
|
279
|
+
.map((inspection) => inspection.ok
|
|
280
|
+
? [
|
|
281
|
+
inspection.value.run_id,
|
|
282
|
+
inspection.value.selection,
|
|
283
|
+
inspection.value.lifecycle.state,
|
|
284
|
+
`verify=${inspection.value.verification.status}`,
|
|
285
|
+
`integration=${inspection.value.integration.state}`,
|
|
286
|
+
`cleanup=${inspection.value.cleanup.state}`,
|
|
287
|
+
].join("\t")
|
|
288
|
+
: `${inspection.run_id}\tinvalid\t${inspection.error}`)
|
|
289
|
+
.join("\n")}\n`);
|
|
290
|
+
});
|
|
202
291
|
const approvals = workflow
|
|
203
292
|
.command("approvals")
|
|
204
293
|
.description("Inspect and resolve durable workflow policy approvals.");
|
|
@@ -33,6 +33,10 @@ export interface WorkEvent {
|
|
|
33
33
|
run_id?: string;
|
|
34
34
|
attempt?: number;
|
|
35
35
|
trigger?: WorkAttemptTrigger;
|
|
36
|
+
workspace_binding_id?: string;
|
|
37
|
+
proof_sha256?: string;
|
|
38
|
+
terminal_attestation_sha256?: string;
|
|
39
|
+
accepted_unknowns?: string[];
|
|
36
40
|
state?: WorkState;
|
|
37
41
|
next_action?: WorkNextAction;
|
|
38
42
|
}
|
|
@@ -85,6 +89,7 @@ export declare function reconcileAllWorkItems(coordRoot: string, actor?: string)
|
|
|
85
89
|
export declare function acceptWorkItem(coordRoot: string, workId: string, input?: {
|
|
86
90
|
actor?: string;
|
|
87
91
|
reason?: string;
|
|
92
|
+
acceptedUnknowns?: readonly string[];
|
|
88
93
|
}): WorkRecord;
|
|
89
94
|
export declare function cancelWorkItem(coordRoot: string, workId: string, input?: {
|
|
90
95
|
actor?: string;
|
|
@@ -107,6 +112,8 @@ export declare function readWorkItemIgnoringLease(coordRoot: string, workId: str
|
|
|
107
112
|
export declare function assertWorkflowUnchanged(intent: WorkIntent): void;
|
|
108
113
|
/** @internal Runner seam; not a separate persistence contract. */
|
|
109
114
|
export declare function acquireWorkLease(coordRoot: string, workId: string): () => void;
|
|
115
|
+
/** @internal Work journal mutation seam. */
|
|
116
|
+
export declare function acquireWorkEventLease(coordRoot: string, workId: string): () => void;
|
|
110
117
|
/** @internal Runner seam; not a separate persistence contract. */
|
|
111
118
|
export declare function boundedActor(value?: string, fallback?: string): string;
|
|
112
119
|
//# sourceMappingURL=state.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/core/work/state.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/core/work/state.ts"],"names":[],"mappings":"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"}
|
package/dist/core/work/state.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { randomBytes, randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { hostname } from "node:os";
|
|
4
4
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { readWorkflowApproval } from "../workflow/approvals.js";
|
|
6
|
+
import { stableDigest } from "../workflow/durable-record.js";
|
|
6
7
|
import { readWorkflowProof } from "../workflow/proof.js";
|
|
7
8
|
import { readWorkflowRunManifest, workflowScriptDigest } from "../workflow/run-state.js";
|
|
9
|
+
import { isWorkspaceBoundExecutionEvidence } from "../workflow/workspaces/validate.js";
|
|
8
10
|
export const WORK_INTENT_SCHEMA_VERSION = 1;
|
|
9
11
|
export const WORK_EVENT_SCHEMA_VERSION = 1;
|
|
10
12
|
const WORK_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
@@ -135,7 +137,7 @@ export function acceptWorkItem(coordRoot, workId, input = {}) {
|
|
|
135
137
|
return appendGovernanceEvent(coordRoot, workId, "work.accepted", ["in_review"], input);
|
|
136
138
|
}
|
|
137
139
|
export function cancelWorkItem(coordRoot, workId, input = {}) {
|
|
138
|
-
return appendGovernanceEvent(coordRoot, workId, "work.cancelled", ["waiting", "ready", "blocked", "in_review"], input);
|
|
140
|
+
return appendGovernanceEvent(coordRoot, workId, "work.cancelled", ["waiting", "ready", "running", "awaiting_approval", "blocked", "in_review"], input);
|
|
139
141
|
}
|
|
140
142
|
export function reopenWorkItem(coordRoot, workId, input = {}) {
|
|
141
143
|
return appendGovernanceEvent(coordRoot, workId, "work.reopened", ["blocked", "in_review", "succeeded", "cancelled"], input);
|
|
@@ -340,49 +342,107 @@ function workContextForIntent(intent) {
|
|
|
340
342
|
};
|
|
341
343
|
}
|
|
342
344
|
function appendGovernanceEvent(coordRoot, workId, event, allowed, input) {
|
|
343
|
-
const
|
|
345
|
+
const releaseWork = event === "work.cancelled" ? undefined : acquireWorkLease(coordRoot, workId);
|
|
346
|
+
const releaseEvent = acquireWorkEventLease(coordRoot, workId);
|
|
344
347
|
try {
|
|
345
|
-
const current =
|
|
348
|
+
const current = releaseWork
|
|
349
|
+
? readWorkItemIgnoringLease(coordRoot, workId)
|
|
350
|
+
: readWorkItem(coordRoot, workId);
|
|
346
351
|
if (!allowed.includes(current.projection.state)) {
|
|
347
352
|
throw new Error(`work item ${workId} cannot ${event.replace("work.", "")} from state ${current.projection.state}`);
|
|
348
353
|
}
|
|
349
|
-
|
|
354
|
+
const acceptance = event === "work.accepted"
|
|
355
|
+
? acceptanceAuthority(coordRoot, current, input.acceptedUnknowns ?? [])
|
|
356
|
+
: undefined;
|
|
357
|
+
appendWorkEventUnderLease(coordRoot, workId, {
|
|
350
358
|
event,
|
|
351
359
|
actor: boundedActor(input.actor),
|
|
352
360
|
reason: boundedOptional(input.reason, "work reason", MAX_REASON),
|
|
361
|
+
...acceptance,
|
|
353
362
|
});
|
|
354
363
|
}
|
|
355
364
|
finally {
|
|
356
|
-
|
|
365
|
+
releaseEvent();
|
|
366
|
+
releaseWork?.();
|
|
357
367
|
}
|
|
358
368
|
return reconcileWorkItem(coordRoot, workId, input.actor);
|
|
359
369
|
}
|
|
370
|
+
function acceptanceAuthority(coordRoot, current, acceptedUnknowns) {
|
|
371
|
+
const latest = current.projection.attempts.at(-1);
|
|
372
|
+
if (!latest?.proof_path)
|
|
373
|
+
throw new Error("work acceptance requires terminal workflow proof");
|
|
374
|
+
const manifest = readWorkflowRunManifest(coordRoot, latest.run_id);
|
|
375
|
+
const proof = readWorkflowProof(coordRoot, latest.run_id);
|
|
376
|
+
if (proof.run.work_item_id !== current.intent.id ||
|
|
377
|
+
proof.run.attempt_context?.number !== latest.number) {
|
|
378
|
+
throw new Error("work acceptance proof does not match the durable attempt");
|
|
379
|
+
}
|
|
380
|
+
const accepted = [...new Set(acceptedUnknowns)].sort();
|
|
381
|
+
for (const code of accepted) {
|
|
382
|
+
boundedString(code, "accepted unknown code", 200);
|
|
383
|
+
}
|
|
384
|
+
const execution = proof.execution;
|
|
385
|
+
return {
|
|
386
|
+
run_id: latest.run_id,
|
|
387
|
+
attempt: latest.number,
|
|
388
|
+
proof_sha256: createHash("sha256").update(readFileSync(latest.proof_path)).digest("hex"),
|
|
389
|
+
workspace_binding_id: manifest.execution.workspace_binding?.binding_id,
|
|
390
|
+
terminal_attestation_sha256: isWorkspaceBoundExecutionEvidence(execution)
|
|
391
|
+
? stableDigest(execution.terminal_attestation)
|
|
392
|
+
: undefined,
|
|
393
|
+
accepted_unknowns: execution ? accepted : undefined,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
360
396
|
/** @internal Runner seam; not a separate persistence contract. */
|
|
361
397
|
export function appendReconcileIfChanged(coordRoot, record, actor) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
398
|
+
let release;
|
|
399
|
+
try {
|
|
400
|
+
release = acquireWorkEventLease(coordRoot, record.intent.id);
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
if (workEventLeaseIsLive(coordRoot, record.intent.id))
|
|
404
|
+
return;
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
const current = readWorkItemIgnoringLease(coordRoot, record.intent.id);
|
|
409
|
+
const last = [...current.events].reverse().find((event) => event.event === "work.reconciled");
|
|
410
|
+
if (last?.state === current.projection.state &&
|
|
411
|
+
last.reason === current.projection.reason &&
|
|
412
|
+
last.next_action === current.projection.next_action) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
appendWorkEventUnderLease(coordRoot, current.intent.id, {
|
|
416
|
+
event: "work.reconciled",
|
|
417
|
+
actor,
|
|
418
|
+
reason: current.projection.reason,
|
|
419
|
+
state: current.projection.state,
|
|
420
|
+
next_action: current.projection.next_action,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
finally {
|
|
424
|
+
release();
|
|
367
425
|
}
|
|
368
|
-
appendWorkEvent(coordRoot, record.intent.id, {
|
|
369
|
-
event: "work.reconciled",
|
|
370
|
-
actor,
|
|
371
|
-
reason: record.projection.reason,
|
|
372
|
-
state: record.projection.state,
|
|
373
|
-
next_action: record.projection.next_action,
|
|
374
|
-
});
|
|
375
426
|
}
|
|
376
427
|
/** @internal Runner seam; not a separate persistence contract. */
|
|
377
428
|
export function appendWorkEvent(coordRoot, workId, input) {
|
|
429
|
+
const release = acquireWorkEventLease(coordRoot, workId);
|
|
430
|
+
try {
|
|
431
|
+
return appendWorkEventUnderLease(coordRoot, workId, input);
|
|
432
|
+
}
|
|
433
|
+
finally {
|
|
434
|
+
release();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function appendWorkEventUnderLease(coordRoot, workId, input) {
|
|
378
438
|
const events = readWorkEvents(coordRoot, workId);
|
|
379
439
|
if (events.length === 0 && input.event !== "work.created") {
|
|
380
|
-
|
|
440
|
+
appendWorkEventUnderLease(coordRoot, workId, {
|
|
381
441
|
event: "work.created",
|
|
382
442
|
actor: "recovery",
|
|
383
443
|
reason: "recovered creation receipt from immutable intent",
|
|
384
444
|
});
|
|
385
|
-
return
|
|
445
|
+
return appendWorkEventUnderLease(coordRoot, workId, input);
|
|
386
446
|
}
|
|
387
447
|
if (events.length >= MAX_EVENTS)
|
|
388
448
|
throw new Error(`work item ${workId} exceeds ${MAX_EVENTS} events`);
|
|
@@ -479,7 +539,9 @@ function validateWorkEvent(event, workId, seq) {
|
|
|
479
539
|
}
|
|
480
540
|
boundedActor(event.actor);
|
|
481
541
|
boundedOptional(event.reason, "work reason", MAX_REASON);
|
|
482
|
-
if (event.event === "attempt.started" ||
|
|
542
|
+
if (event.event === "attempt.started" ||
|
|
543
|
+
event.event === "attempt.resumed" ||
|
|
544
|
+
(event.event === "work.accepted" && event.run_id !== undefined)) {
|
|
483
545
|
if (!event.run_id ||
|
|
484
546
|
!RUN_ID.test(event.run_id) ||
|
|
485
547
|
!Number.isSafeInteger(event.attempt) ||
|
|
@@ -487,6 +549,22 @@ function validateWorkEvent(event, workId, seq) {
|
|
|
487
549
|
throw new Error(`work item ${workId} event ${seq} has invalid attempt data`);
|
|
488
550
|
}
|
|
489
551
|
}
|
|
552
|
+
if (event.event === "work.accepted" && event.run_id !== undefined) {
|
|
553
|
+
if (event.workspace_binding_id !== undefined) {
|
|
554
|
+
boundedString(event.workspace_binding_id, "workspace binding id", 200);
|
|
555
|
+
}
|
|
556
|
+
if (!/^[a-f0-9]{64}$/.test(event.proof_sha256 ?? "")) {
|
|
557
|
+
throw new Error(`work item ${workId} event ${seq} has invalid proof digest`);
|
|
558
|
+
}
|
|
559
|
+
if (event.workspace_binding_id !== undefined) {
|
|
560
|
+
if (!/^[a-f0-9]{64}$/.test(event.terminal_attestation_sha256 ?? "") ||
|
|
561
|
+
!Array.isArray(event.accepted_unknowns) ||
|
|
562
|
+
event.accepted_unknowns.some((code) => typeof code !== "string" || code.length === 0 || code.length > 200) ||
|
|
563
|
+
new Set(event.accepted_unknowns).size !== event.accepted_unknowns.length) {
|
|
564
|
+
throw new Error(`work item ${workId} event ${seq} has invalid execution review evidence`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
490
568
|
if (event.trigger !== undefined &&
|
|
491
569
|
(event.event !== "attempt.started" ||
|
|
492
570
|
(event.trigger !== "initial" && event.trigger !== "retry"))) {
|
|
@@ -573,8 +651,15 @@ function workDir(coordRoot, workId) {
|
|
|
573
651
|
}
|
|
574
652
|
/** @internal Runner seam; not a separate persistence contract. */
|
|
575
653
|
export function acquireWorkLease(coordRoot, workId) {
|
|
654
|
+
return acquireWorkFileLease(coordRoot, workId, "lease.json", "is already active");
|
|
655
|
+
}
|
|
656
|
+
/** @internal Work journal mutation seam. */
|
|
657
|
+
export function acquireWorkEventLease(coordRoot, workId) {
|
|
658
|
+
return acquireWorkFileLease(coordRoot, workId, "events.lease.json", "event journal is already active");
|
|
659
|
+
}
|
|
660
|
+
function acquireWorkFileLease(coordRoot, workId, filename, activeMessage) {
|
|
576
661
|
readWorkIntent(coordRoot, workId);
|
|
577
|
-
const path = join(workDir(coordRoot, workId),
|
|
662
|
+
const path = join(workDir(coordRoot, workId), filename);
|
|
578
663
|
const owner = {
|
|
579
664
|
pid: process.pid,
|
|
580
665
|
host: hostname(),
|
|
@@ -602,7 +687,7 @@ export function acquireWorkLease(coordRoot, workId) {
|
|
|
602
687
|
if (!acquire()) {
|
|
603
688
|
const existing = readLease(path);
|
|
604
689
|
if (existing && leaseIsLive(existing)) {
|
|
605
|
-
throw new Error(`work item ${workId}
|
|
690
|
+
throw new Error(`work item ${workId} ${activeMessage} under pid ${existing.pid} on ${existing.host}`);
|
|
606
691
|
}
|
|
607
692
|
unlinkSync(path);
|
|
608
693
|
if (!acquire())
|
|
@@ -623,6 +708,10 @@ function workLeaseIsLive(coordRoot, workId) {
|
|
|
623
708
|
const lease = readLease(join(workDir(coordRoot, workId), "lease.json"));
|
|
624
709
|
return lease ? leaseIsLive(lease) : false;
|
|
625
710
|
}
|
|
711
|
+
function workEventLeaseIsLive(coordRoot, workId) {
|
|
712
|
+
const lease = readLease(join(workDir(coordRoot, workId), "events.lease.json"));
|
|
713
|
+
return lease ? leaseIsLive(lease) : false;
|
|
714
|
+
}
|
|
626
715
|
function workflowResumeLeaseIsLive(coordRoot, runId) {
|
|
627
716
|
const lease = readLease(join(coordRoot, ".harnery", "workflows", runId, "resume.lock"));
|
|
628
717
|
return lease ? leaseIsLive(lease) : false;
|
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
* harness CLI under a live session inherits vars that make it exit 1 with
|
|
7
7
|
* empty output; an empty-string var still reads as set. Scrub all three
|
|
8
8
|
* families (CLAUDE*, CODEX*, CURSOR*) regardless of which adapter spawns —
|
|
9
|
-
* a codex child launched from inside a Claude Code session must not
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* a codex child launched from inside a Claude Code session must not inherit
|
|
10
|
+
* CLAUDE* either. Preserve explicit auth/config locators such as CODEX_HOME:
|
|
11
|
+
* changing the home silently changes which subscription account the child
|
|
12
|
+
* uses.
|
|
13
|
+
* 2. **The scrub targets SESSION vars, not credentials or auth locators.** CURSOR_API_KEY
|
|
12
14
|
* matches the CURSOR* prefix but is an auth credential, so it is carved
|
|
13
|
-
* out and re-added — a key-only cursor host must keep working.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
15
|
+
* out and re-added — a key-only cursor host must keep working. CODEX_HOME
|
|
16
|
+
* is likewise preserved because codex reads its ChatGPT subscription login
|
|
17
|
+
* from `$CODEX_HOME/auth.json`. (The other two key vars,
|
|
18
|
+
* ANTHROPIC_API_KEY and OPENAI_API_KEY, don't collide with the scrub
|
|
19
|
+
* prefixes.)
|
|
16
20
|
* 3. **subscriptionOnly deletes every API-key var** (see billing.ts) so the
|
|
17
21
|
* child can only authenticate via its stored subscription login — the
|
|
18
22
|
* guarantee behind `workflow run --subscription-only`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"child-env.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/child-env.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"child-env.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/child-env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAOH,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,wBAAgB,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,GAAE,YAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAkB7F"}
|
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
* harness CLI under a live session inherits vars that make it exit 1 with
|
|
7
7
|
* empty output; an empty-string var still reads as set. Scrub all three
|
|
8
8
|
* families (CLAUDE*, CODEX*, CURSOR*) regardless of which adapter spawns —
|
|
9
|
-
* a codex child launched from inside a Claude Code session must not
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* a codex child launched from inside a Claude Code session must not inherit
|
|
10
|
+
* CLAUDE* either. Preserve explicit auth/config locators such as CODEX_HOME:
|
|
11
|
+
* changing the home silently changes which subscription account the child
|
|
12
|
+
* uses.
|
|
13
|
+
* 2. **The scrub targets SESSION vars, not credentials or auth locators.** CURSOR_API_KEY
|
|
12
14
|
* matches the CURSOR* prefix but is an auth credential, so it is carved
|
|
13
|
-
* out and re-added — a key-only cursor host must keep working.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
15
|
+
* out and re-added — a key-only cursor host must keep working. CODEX_HOME
|
|
16
|
+
* is likewise preserved because codex reads its ChatGPT subscription login
|
|
17
|
+
* from `$CODEX_HOME/auth.json`. (The other two key vars,
|
|
18
|
+
* ANTHROPIC_API_KEY and OPENAI_API_KEY, don't collide with the scrub
|
|
19
|
+
* prefixes.)
|
|
16
20
|
* 3. **subscriptionOnly deletes every API-key var** (see billing.ts) so the
|
|
17
21
|
* child can only authenticate via its stored subscription login — the
|
|
18
22
|
* guarantee behind `workflow run --subscription-only`.
|
|
@@ -24,6 +28,7 @@
|
|
|
24
28
|
*/
|
|
25
29
|
import { API_KEY_VARS } from "./billing.js";
|
|
26
30
|
const SCRUB_PREFIXES = ["CLAUDE", "CODEX", "CURSOR"];
|
|
31
|
+
const PRESERVED_HARNESS_VARS = new Set(["CODEX_HOME"]);
|
|
27
32
|
export function buildChildEnv(runId, opts = {}) {
|
|
28
33
|
const env = {};
|
|
29
34
|
const keyVars = new Set(Object.values(API_KEY_VARS));
|
|
@@ -32,8 +37,11 @@ export function buildChildEnv(runId, opts = {}) {
|
|
|
32
37
|
continue;
|
|
33
38
|
if (opts.subscriptionOnly && keyVars.has(k))
|
|
34
39
|
continue;
|
|
35
|
-
if (SCRUB_PREFIXES.some((p) => k.startsWith(p)) &&
|
|
40
|
+
if (SCRUB_PREFIXES.some((p) => k.startsWith(p)) &&
|
|
41
|
+
!keyVars.has(k) &&
|
|
42
|
+
!PRESERVED_HARNESS_VARS.has(k)) {
|
|
36
43
|
continue;
|
|
44
|
+
}
|
|
37
45
|
env[k] = v;
|
|
38
46
|
}
|
|
39
47
|
env.HARNERY_WORKFLOW_CHILD = "1";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function stableDigest(value: unknown): string;
|
|
2
|
+
export declare function canonicalJson(value: unknown): string;
|
|
3
|
+
export declare function fileSha256(path: string): string;
|
|
4
|
+
export declare function writeImmutableJson(path: string, value: unknown): boolean;
|
|
5
|
+
export declare function fsyncParentDirectory(path: string): void;
|
|
6
|
+
export declare function readJsonRecord<T>(path: string, label: string): T;
|
|
7
|
+
//# sourceMappingURL=durable-record.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durable-record.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/durable-record.ts"],"names":[],"mappings":"AAkBA,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEnD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CA6BxE;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAQvD;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,CAShE"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, linkSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
const JSON_LIMIT = 512 * 1024;
|
|
5
|
+
export function stableDigest(value) {
|
|
6
|
+
return createHash("sha256").update(canonicalJson(value)).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
export function canonicalJson(value) {
|
|
9
|
+
return JSON.stringify(canonicalize(value));
|
|
10
|
+
}
|
|
11
|
+
export function fileSha256(path) {
|
|
12
|
+
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
export function writeImmutableJson(path, value) {
|
|
15
|
+
const body = `${JSON.stringify(value, null, 2)}\n`;
|
|
16
|
+
if (Buffer.byteLength(body) > JSON_LIMIT) {
|
|
17
|
+
throw new Error(`immutable record exceeds ${JSON_LIMIT} bytes`);
|
|
18
|
+
}
|
|
19
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
20
|
+
const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
|
|
21
|
+
let fd;
|
|
22
|
+
try {
|
|
23
|
+
fd = openSync(temporary, "wx", 0o600);
|
|
24
|
+
writeFileSync(fd, body, "utf8");
|
|
25
|
+
fsyncSync(fd);
|
|
26
|
+
closeSync(fd);
|
|
27
|
+
fd = undefined;
|
|
28
|
+
try {
|
|
29
|
+
linkSync(temporary, path);
|
|
30
|
+
chmodSync(path, 0o600);
|
|
31
|
+
fsyncParentDirectory(path);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error.code !== "EEXIST")
|
|
36
|
+
throw error;
|
|
37
|
+
const existing = readFileSync(path, "utf8");
|
|
38
|
+
if (existing !== body)
|
|
39
|
+
throw new Error(`immutable record already exists at ${path}`);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
if (fd !== undefined)
|
|
45
|
+
closeSync(fd);
|
|
46
|
+
if (existsSync(temporary))
|
|
47
|
+
unlinkSync(temporary);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function fsyncParentDirectory(path) {
|
|
51
|
+
const directory = dirname(path);
|
|
52
|
+
const fd = openSync(directory, "r");
|
|
53
|
+
try {
|
|
54
|
+
fsyncSync(fd);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
closeSync(fd);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function readJsonRecord(path, label) {
|
|
61
|
+
if (!existsSync(path))
|
|
62
|
+
throw new Error(`${label} does not exist at ${path}`);
|
|
63
|
+
const size = statSync(path).size;
|
|
64
|
+
if (size <= 0 || size > JSON_LIMIT)
|
|
65
|
+
throw new Error(`${label} has invalid size ${size}`);
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
throw new Error(`cannot parse ${label} at ${path}: ${error.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function canonicalize(value) {
|
|
74
|
+
if (Array.isArray(value))
|
|
75
|
+
return value.map(canonicalize);
|
|
76
|
+
if (value && typeof value === "object") {
|
|
77
|
+
return Object.fromEntries(Object.entries(value)
|
|
78
|
+
.filter(([, item]) => item !== undefined)
|
|
79
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
80
|
+
.map(([key, item]) => [key, canonicalize(item)]));
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAuCH,OAAO,KAAK,EAEV,UAAU,EAEV,SAAS,EAUV,MAAM,YAAY,CAAC;AAyBpB,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAM/E;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;gBAEjB,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;CAOpF;AAED,wBAAsB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CA6E1F"}
|