harnery 0.22.1 → 0.24.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 +302 -5
- 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/spawn-cursor.d.ts +3 -0
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +8 -4
- package/dist/core/workflow/types.d.ts +10 -0
- package/dist/core/workflow/types.d.ts.map +1 -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 +32 -0
- package/dist/core/workflow/workspaces/integration.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/integration.js +631 -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 +397 -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 +366 -8
- 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 +74 -0
- package/src/core/workflow/proof.ts +70 -22
- package/src/core/workflow/run-state.ts +46 -11
- package/src/core/workflow/spawn-cursor.ts +8 -4
- package/src/core/workflow/types.ts +14 -0
- 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 +89 -0
- package/src/core/workflow/workspaces/inspect.ts +421 -0
- package/src/core/workflow/workspaces/integration.ts +845 -0
- package/src/core/workflow/workspaces/leases.ts +517 -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. Inspect with `harn workflow workspace`, then use the separate `workflow integration prepare`, `integration apply`, and `cleanup` phases; 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":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAiEnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAqqBjF"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { resolveBinName, workflowSubscriptionOnly } from "../core/config.js";
|
|
2
4
|
import { createBuiltinHarnessRegistry } from "../core/harnesses/index.js";
|
|
3
5
|
import { findCoordRoot } from "../core/hooks/resolve/coord-root.js";
|
|
4
6
|
import { loadPolicyFile } from "../core/policy/index.js";
|
|
@@ -24,6 +26,7 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
24
26
|
"without this the engine refuses that silent-override state")
|
|
25
27
|
.option("--policy <file>", "Host policy JSON/JSONC applied before dispatch and external mutation")
|
|
26
28
|
.option("--isolation <mode>", "Host-created execution boundary: shared | worktree | sandbox | remote (default shared)")
|
|
29
|
+
.option("--workspace-root <dir>", "Explicit writable parent for the built-in local Git worktree provider (requires --isolation worktree)")
|
|
27
30
|
.option("--approval-to <address>", "Address durable ASK requests (default: operator)")
|
|
28
31
|
.option("--json", "Emit the full RunReport as JSON")
|
|
29
32
|
.action(async (script, opts) => {
|
|
@@ -50,6 +53,13 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
50
53
|
});
|
|
51
54
|
process.exit(1);
|
|
52
55
|
}
|
|
56
|
+
if (opts.workspaceRoot && opts.isolation !== "worktree") {
|
|
57
|
+
emit.error({
|
|
58
|
+
code: "bad_workspace_root",
|
|
59
|
+
message: "--workspace-root requires --isolation worktree",
|
|
60
|
+
});
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
53
63
|
// Flag beats config; the two flags contradict each other by design.
|
|
54
64
|
const subscriptionOnly = opts.subscriptionOnly || workflowSubscriptionOnly(coordRoot);
|
|
55
65
|
if (subscriptionOnly && opts.allowApiBilling) {
|
|
@@ -61,7 +71,13 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
61
71
|
process.exit(1);
|
|
62
72
|
}
|
|
63
73
|
try {
|
|
64
|
-
const { runWorkflow } = await import("../core/workflow/
|
|
74
|
+
const { createLocalGitWorktreeProvider, runWorkflow } = await import("../core/workflow/index.js");
|
|
75
|
+
const workspace = opts.workspaceRoot
|
|
76
|
+
? {
|
|
77
|
+
provider: createLocalGitWorktreeProvider({ coordRoot }),
|
|
78
|
+
writableRoots: [resolve(opts.workspaceRoot)],
|
|
79
|
+
}
|
|
80
|
+
: undefined;
|
|
65
81
|
const report = await runWorkflow(script, {
|
|
66
82
|
coordRoot,
|
|
67
83
|
spawners: registry.spawners(),
|
|
@@ -82,6 +98,7 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
82
98
|
approvalMode: "park",
|
|
83
99
|
approvalAddressee: opts.approvalTo,
|
|
84
100
|
isolation: opts.isolation,
|
|
101
|
+
workspace,
|
|
85
102
|
// CLI harness subprocesses inherit the host network. A policy that
|
|
86
103
|
// forbids network must therefore deny dispatch unless a future host
|
|
87
104
|
// adapter creates and declares a network-disabled boundary.
|
|
@@ -118,7 +135,7 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
118
135
|
else {
|
|
119
136
|
emit.text(`run ${err.runId} parked\napproval: ${err.approvalId}\n` +
|
|
120
137
|
`journal: ${err.journalPath}\n` +
|
|
121
|
-
`resume after resolution:
|
|
138
|
+
`resume after resolution: ${resolveBinName()} workflow resume ${err.runId}\n`);
|
|
122
139
|
}
|
|
123
140
|
return;
|
|
124
141
|
}
|
|
@@ -146,12 +163,22 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
146
163
|
process.exit(1);
|
|
147
164
|
}
|
|
148
165
|
try {
|
|
149
|
-
const { assertWorkflowRunResumable, runWorkflow } = await import("../core/workflow/index.js");
|
|
166
|
+
const { assertWorkflowRunResumable, createLocalGitWorktreeProvider, runWorkflow } = await import("../core/workflow/index.js");
|
|
150
167
|
const { manifest } = assertWorkflowRunResumable(coordRoot, runId);
|
|
168
|
+
const binding = manifest.execution.workspace_binding;
|
|
169
|
+
if (binding && binding.provider.id !== "local-git-worktree") {
|
|
170
|
+
throw new Error(`workflow run ${runId} requires unavailable workspace provider ${binding.provider.id}`);
|
|
171
|
+
}
|
|
151
172
|
const report = await runWorkflow(manifest.script.path, {
|
|
152
173
|
coordRoot,
|
|
153
174
|
spawners: registry.spawners(),
|
|
154
175
|
resumeRunId: runId,
|
|
176
|
+
workspace: binding
|
|
177
|
+
? {
|
|
178
|
+
provider: createLocalGitWorktreeProvider({ coordRoot }),
|
|
179
|
+
writableRoots: [binding.writable_root.configured],
|
|
180
|
+
}
|
|
181
|
+
: undefined,
|
|
155
182
|
harnessEvidence: Object.fromEntries(registry
|
|
156
183
|
.list()
|
|
157
184
|
.map((adapter) => [
|
|
@@ -199,6 +226,249 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
199
226
|
process.exit(1);
|
|
200
227
|
}
|
|
201
228
|
});
|
|
229
|
+
workflow
|
|
230
|
+
.command("workspace <run-id>")
|
|
231
|
+
.description("Show validated allocation, verification, integration, and cleanup state.")
|
|
232
|
+
.option("--json", "Emit the validated workspace status as JSON")
|
|
233
|
+
.action(async (runId, opts) => {
|
|
234
|
+
const coordRoot = findCoordRoot();
|
|
235
|
+
if (!coordRoot) {
|
|
236
|
+
emit.error({
|
|
237
|
+
code: "no_coord_root",
|
|
238
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
239
|
+
});
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
const { inspectWorkflowWorkspace, renderWorkflowWorkspaceStatus } = await import("../core/workflow/index.js");
|
|
243
|
+
const inspection = inspectWorkflowWorkspace(coordRoot, runId);
|
|
244
|
+
if (!inspection.ok) {
|
|
245
|
+
emit.error({ code: "workflow_workspace_invalid", message: inspection.error });
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
if (opts.json) {
|
|
249
|
+
emit.config({ format: "json" });
|
|
250
|
+
emit.data(inspection.value);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
emit.text(renderWorkflowWorkspaceStatus(inspection.value));
|
|
254
|
+
});
|
|
255
|
+
workflow
|
|
256
|
+
.command("workspaces")
|
|
257
|
+
.description("List validated isolated and shared-compatibility workspace decisions.")
|
|
258
|
+
.option("--json", "Emit workspace inspections as JSON")
|
|
259
|
+
.action(async (opts) => {
|
|
260
|
+
const coordRoot = findCoordRoot();
|
|
261
|
+
if (!coordRoot) {
|
|
262
|
+
emit.error({
|
|
263
|
+
code: "no_coord_root",
|
|
264
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
265
|
+
});
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
const { listWorkflowWorkspaceInspections } = await import("../core/workflow/index.js");
|
|
269
|
+
const inspections = listWorkflowWorkspaceInspections(coordRoot).filter((inspection) => !inspection.ok || inspection.value.selection !== "shared");
|
|
270
|
+
if (opts.json) {
|
|
271
|
+
emit.config({ format: "json" });
|
|
272
|
+
emit.data(inspections);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (inspections.length === 0) {
|
|
276
|
+
emit.text("no isolated or shared-compatibility workspace runs\n");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
emit.text(`${inspections
|
|
280
|
+
.map((inspection) => inspection.ok
|
|
281
|
+
? [
|
|
282
|
+
inspection.value.run_id,
|
|
283
|
+
inspection.value.selection,
|
|
284
|
+
inspection.value.lifecycle.state,
|
|
285
|
+
`verify=${inspection.value.verification.status}`,
|
|
286
|
+
`integration=${inspection.value.integration.state}`,
|
|
287
|
+
`cleanup=${inspection.value.cleanup.state}`,
|
|
288
|
+
].join("\t")
|
|
289
|
+
: `${inspection.run_id}\tinvalid\t${inspection.error}`)
|
|
290
|
+
.join("\n")}\n`);
|
|
291
|
+
});
|
|
292
|
+
const integration = workflow
|
|
293
|
+
.command("integration")
|
|
294
|
+
.description("Prepare and apply proof-gated isolated-workspace integration.");
|
|
295
|
+
integration
|
|
296
|
+
.command("prepare <run-id>")
|
|
297
|
+
.description("Preview integration and write exact review and policy authority.")
|
|
298
|
+
.option("--policy <file>", "Current host policy JSON/JSONC for the Git mutation")
|
|
299
|
+
.option("--reviewer <name>", "Explicit reviewer for a standalone workflow")
|
|
300
|
+
.option("--reason <text>", "Reason recorded with the standalone review")
|
|
301
|
+
.option("--target-root <dir>", "Explicit target checkout (default: frozen source checkout)")
|
|
302
|
+
.option("--accept-unknown <code>", "Accept one verification unknown by code (repeatable)", collectOption, [])
|
|
303
|
+
.option("--approval-to <address>", "Address a durable policy ASK request")
|
|
304
|
+
.option("--json", "Emit the durable integration plan as JSON")
|
|
305
|
+
.action(async (runId, opts) => {
|
|
306
|
+
const coordRoot = findCoordRoot();
|
|
307
|
+
if (!coordRoot) {
|
|
308
|
+
emit.error({
|
|
309
|
+
code: "no_coord_root",
|
|
310
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
311
|
+
});
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
if (!opts.policy) {
|
|
315
|
+
emit.error({
|
|
316
|
+
code: "integration_policy_required",
|
|
317
|
+
message: "integration prepare requires --policy <file>",
|
|
318
|
+
});
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
if (opts.reason && !opts.reviewer) {
|
|
322
|
+
emit.error({
|
|
323
|
+
code: "integration_reviewer_required",
|
|
324
|
+
message: "--reason requires --reviewer <name>",
|
|
325
|
+
});
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
const provider = await builtInProviderForRun(coordRoot, runId);
|
|
330
|
+
const { IntegrationPrepareParkedError, prepareIntegration } = await import("../core/workflow/index.js");
|
|
331
|
+
try {
|
|
332
|
+
const plan = await prepareIntegration({
|
|
333
|
+
coordRoot,
|
|
334
|
+
runId,
|
|
335
|
+
provider,
|
|
336
|
+
policy: loadPolicyFile(opts.policy),
|
|
337
|
+
targetRoot: opts.targetRoot,
|
|
338
|
+
review: opts.reviewer ? { actor: opts.reviewer, reason: opts.reason } : undefined,
|
|
339
|
+
acceptedUnknowns: opts.acceptUnknown,
|
|
340
|
+
approvalAddressee: opts.approvalTo,
|
|
341
|
+
});
|
|
342
|
+
if (opts.json) {
|
|
343
|
+
emit.config({ format: "json" });
|
|
344
|
+
emit.data(plan);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
emit.text(`integration plan ${plan.plan_id} authorized\n` +
|
|
348
|
+
`source: ${plan.provider_preview.source_commit}\n` +
|
|
349
|
+
`target: ${plan.provider_preview.target_ref} at ${plan.provider_preview.target_commit}\n` +
|
|
350
|
+
`changes: ${plan.provider_preview.changed_paths.length}\n` +
|
|
351
|
+
`apply: ${resolveBinName()} workflow integration apply ${runId} --yes\n`);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
if (error instanceof IntegrationPrepareParkedError) {
|
|
355
|
+
if (opts.json) {
|
|
356
|
+
emit.config({ format: "json" });
|
|
357
|
+
emit.data({
|
|
358
|
+
status: "parked",
|
|
359
|
+
runId: error.runId,
|
|
360
|
+
planId: error.planId,
|
|
361
|
+
approvalId: error.approvalId,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
emit.text(`integration preparation parked\n` +
|
|
366
|
+
`run: ${error.runId}\n` +
|
|
367
|
+
`plan: ${error.planId}\n` +
|
|
368
|
+
`approval: ${error.approvalId}\n` +
|
|
369
|
+
`approve: ${resolveBinName()} workflow approvals approve ${error.approvalId}\n`);
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
emit.error({
|
|
378
|
+
code: "workflow_integration_prepare_failed",
|
|
379
|
+
message: error instanceof Error ? error.message : String(error),
|
|
380
|
+
});
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
integration
|
|
385
|
+
.command("apply <run-id>")
|
|
386
|
+
.description("Apply an authorized durable integration plan.")
|
|
387
|
+
.option("--yes", "Confirm the target-branch mutation")
|
|
388
|
+
.option("--json", "Emit the durable integration receipt as JSON")
|
|
389
|
+
.action(async (runId, opts) => {
|
|
390
|
+
const coordRoot = findCoordRoot();
|
|
391
|
+
if (!coordRoot) {
|
|
392
|
+
emit.error({
|
|
393
|
+
code: "no_coord_root",
|
|
394
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
395
|
+
});
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
if (!opts.yes) {
|
|
399
|
+
emit.error({
|
|
400
|
+
code: "integration_confirmation_required",
|
|
401
|
+
message: "integration apply changes the target branch; pass --yes to confirm",
|
|
402
|
+
});
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
const provider = await builtInProviderForRun(coordRoot, runId);
|
|
407
|
+
const { applyIntegration } = await import("../core/workflow/index.js");
|
|
408
|
+
const receipt = await applyIntegration({ coordRoot, runId, provider });
|
|
409
|
+
if (opts.json) {
|
|
410
|
+
emit.config({ format: "json" });
|
|
411
|
+
emit.data(receipt);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
emit.text(`integration ${receipt.status}: ${receipt.target_ref} at ${receipt.target_commit}\n` +
|
|
415
|
+
`receipt: ${receipt.receipt_id}\n` +
|
|
416
|
+
`cleanup: ${resolveBinName()} workflow cleanup ${runId} --yes\n`);
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
emit.error({
|
|
420
|
+
code: "workflow_integration_apply_failed",
|
|
421
|
+
message: error instanceof Error ? error.message : String(error),
|
|
422
|
+
});
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
workflow
|
|
427
|
+
.command("cleanup <run-id>")
|
|
428
|
+
.description("Conservatively release a provider-owned isolated workspace.")
|
|
429
|
+
.option("--yes", "Confirm the worktree and provider-branch removal attempt")
|
|
430
|
+
.option("--json", "Emit the cleanup attempt or receipt as JSON")
|
|
431
|
+
.action(async (runId, opts) => {
|
|
432
|
+
const coordRoot = findCoordRoot();
|
|
433
|
+
if (!coordRoot) {
|
|
434
|
+
emit.error({
|
|
435
|
+
code: "no_coord_root",
|
|
436
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
437
|
+
});
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
if (!opts.yes) {
|
|
441
|
+
emit.error({
|
|
442
|
+
code: "cleanup_confirmation_required",
|
|
443
|
+
message: "workspace cleanup may remove a worktree and branch; pass --yes to confirm",
|
|
444
|
+
});
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
const provider = await builtInProviderForRun(coordRoot, runId);
|
|
449
|
+
const { cleanupWorkspace } = await import("../core/workflow/index.js");
|
|
450
|
+
const result = await cleanupWorkspace({ coordRoot, runId, provider });
|
|
451
|
+
if (opts.json) {
|
|
452
|
+
emit.config({ format: "json" });
|
|
453
|
+
emit.data(result);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const receipt = "receipt_id" in result && typeof result.receipt_id === "string"
|
|
457
|
+
? `\nreceipt: ${result.receipt_id}`
|
|
458
|
+
: "";
|
|
459
|
+
const reason = "reason" in result && typeof result.reason === "string"
|
|
460
|
+
? `\nreason: ${result.reason}`
|
|
461
|
+
: "";
|
|
462
|
+
emit.text(`workspace cleanup ${result.status}: ${result.binding_id}${receipt}${reason}\n`);
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
emit.error({
|
|
466
|
+
code: "workflow_cleanup_failed",
|
|
467
|
+
message: error instanceof Error ? error.message : String(error),
|
|
468
|
+
});
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
202
472
|
const approvals = workflow
|
|
203
473
|
.command("approvals")
|
|
204
474
|
.description("Inspect and resolve durable workflow policy approvals.");
|
|
@@ -290,7 +560,7 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
290
560
|
else {
|
|
291
561
|
emit.text(`${renderWorkflowApproval(resolved.approval)}` +
|
|
292
562
|
`${resolved.applied ? "decision recorded" : "decision already recorded"}\n` +
|
|
293
|
-
|
|
563
|
+
approvalNextActionHint(coordRoot, resolved.approval));
|
|
294
564
|
}
|
|
295
565
|
}
|
|
296
566
|
catch (err) {
|
|
@@ -333,3 +603,30 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
333
603
|
}
|
|
334
604
|
});
|
|
335
605
|
}
|
|
606
|
+
async function builtInProviderForRun(coordRoot, runId) {
|
|
607
|
+
const { createLocalGitWorktreeProvider, readWorkflowRunManifest } = await import("../core/workflow/index.js");
|
|
608
|
+
const manifest = readWorkflowRunManifest(coordRoot, runId);
|
|
609
|
+
const binding = manifest.execution.workspace_binding;
|
|
610
|
+
if (!binding) {
|
|
611
|
+
throw new Error(`workflow run ${runId} has no isolated workspace binding`);
|
|
612
|
+
}
|
|
613
|
+
if (binding.provider.id !== "local-git-worktree") {
|
|
614
|
+
throw new Error(`workflow run ${runId} requires unavailable workspace provider ${binding.provider.id}`);
|
|
615
|
+
}
|
|
616
|
+
return createLocalGitWorktreeProvider({ coordRoot });
|
|
617
|
+
}
|
|
618
|
+
function collectOption(value, previous) {
|
|
619
|
+
return [...previous, value];
|
|
620
|
+
}
|
|
621
|
+
/** Next operator command after resolving an approval. Mid-run parks resume the
|
|
622
|
+
* workflow; integration prepare parks after the run is already terminal, so
|
|
623
|
+
* the durable plan is authorized by re-running prepare (resume would fail). */
|
|
624
|
+
function approvalNextActionHint(coordRoot, approval) {
|
|
625
|
+
const bin = resolveBinName();
|
|
626
|
+
const runId = approval.request.run_id;
|
|
627
|
+
const integrationPlanPath = join(coordRoot, ".harnery", "workflows", runId, "integration", "plan.json");
|
|
628
|
+
if (existsSync(integrationPlanPath)) {
|
|
629
|
+
return `prepare: ${bin} workflow integration prepare ${runId}\n`;
|
|
630
|
+
}
|
|
631
|
+
return `resume: ${bin} workflow resume ${runId}\n`;
|
|
632
|
+
}
|
|
@@ -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;
|