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/src/commands/workflow.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
1
3
|
import type { Command } from "commander";
|
|
2
4
|
import type { EmitContext } from "../commander.ts";
|
|
3
|
-
import { workflowSubscriptionOnly } from "../core/config.ts";
|
|
5
|
+
import { resolveBinName, workflowSubscriptionOnly } from "../core/config.ts";
|
|
4
6
|
import { createBuiltinHarnessRegistry } from "../core/harnesses/index.ts";
|
|
5
7
|
import { findCoordRoot } from "../core/hooks/resolve/coord-root.ts";
|
|
6
8
|
import type { PolicyIsolation } from "../core/policy/index.ts";
|
|
7
9
|
import { loadPolicyFile } from "../core/policy/index.ts";
|
|
8
|
-
import type { WorkflowApprovalStatus } from "../core/workflow/approvals.ts";
|
|
10
|
+
import type { WorkflowApproval, WorkflowApprovalStatus } from "../core/workflow/approvals.ts";
|
|
9
11
|
import { WorkflowParkedError } from "../core/workflow/engine.ts";
|
|
12
|
+
import type { WorkspaceProvider } from "../core/workflow/index.ts";
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
15
|
* `workflow run <script>`: execute a workflow script — bounded, schema-gated,
|
|
@@ -28,6 +31,7 @@ interface WorkflowRunOpts {
|
|
|
28
31
|
allowApiBilling?: boolean;
|
|
29
32
|
policy?: string;
|
|
30
33
|
isolation?: PolicyIsolation;
|
|
34
|
+
workspaceRoot?: string;
|
|
31
35
|
approvalTo?: string;
|
|
32
36
|
json?: boolean;
|
|
33
37
|
}
|
|
@@ -47,6 +51,21 @@ interface WorkflowApprovalDecisionOpts {
|
|
|
47
51
|
json?: boolean;
|
|
48
52
|
}
|
|
49
53
|
|
|
54
|
+
interface WorkflowIntegrationPrepareOpts {
|
|
55
|
+
policy?: string;
|
|
56
|
+
reviewer?: string;
|
|
57
|
+
reason?: string;
|
|
58
|
+
targetRoot?: string;
|
|
59
|
+
acceptUnknown?: string[];
|
|
60
|
+
approvalTo?: string;
|
|
61
|
+
json?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface WorkflowConfirmedMutationOpts {
|
|
65
|
+
yes?: boolean;
|
|
66
|
+
json?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
50
69
|
export function registerWorkflowCommand(program: Command, emit: EmitContext): void {
|
|
51
70
|
const registry = createBuiltinHarnessRegistry();
|
|
52
71
|
const harnesses = registry.ids();
|
|
@@ -89,6 +108,10 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
89
108
|
"--isolation <mode>",
|
|
90
109
|
"Host-created execution boundary: shared | worktree | sandbox | remote (default shared)",
|
|
91
110
|
)
|
|
111
|
+
.option(
|
|
112
|
+
"--workspace-root <dir>",
|
|
113
|
+
"Explicit writable parent for the built-in local Git worktree provider (requires --isolation worktree)",
|
|
114
|
+
)
|
|
92
115
|
.option("--approval-to <address>", "Address durable ASK requests (default: operator)")
|
|
93
116
|
.option("--json", "Emit the full RunReport as JSON")
|
|
94
117
|
.action(async (script: string, opts: WorkflowRunOpts) => {
|
|
@@ -117,6 +140,13 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
117
140
|
});
|
|
118
141
|
process.exit(1);
|
|
119
142
|
}
|
|
143
|
+
if (opts.workspaceRoot && opts.isolation !== "worktree") {
|
|
144
|
+
emit.error({
|
|
145
|
+
code: "bad_workspace_root",
|
|
146
|
+
message: "--workspace-root requires --isolation worktree",
|
|
147
|
+
});
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
120
150
|
// Flag beats config; the two flags contradict each other by design.
|
|
121
151
|
const subscriptionOnly = opts.subscriptionOnly || workflowSubscriptionOnly(coordRoot);
|
|
122
152
|
if (subscriptionOnly && opts.allowApiBilling) {
|
|
@@ -129,7 +159,15 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
129
159
|
process.exit(1);
|
|
130
160
|
}
|
|
131
161
|
try {
|
|
132
|
-
const { runWorkflow } = await import(
|
|
162
|
+
const { createLocalGitWorktreeProvider, runWorkflow } = await import(
|
|
163
|
+
"../core/workflow/index.ts"
|
|
164
|
+
);
|
|
165
|
+
const workspace = opts.workspaceRoot
|
|
166
|
+
? {
|
|
167
|
+
provider: createLocalGitWorktreeProvider({ coordRoot }),
|
|
168
|
+
writableRoots: [resolve(opts.workspaceRoot)],
|
|
169
|
+
}
|
|
170
|
+
: undefined;
|
|
133
171
|
const report = await runWorkflow(script, {
|
|
134
172
|
coordRoot,
|
|
135
173
|
spawners: registry.spawners(),
|
|
@@ -152,6 +190,7 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
152
190
|
approvalMode: "park",
|
|
153
191
|
approvalAddressee: opts.approvalTo,
|
|
154
192
|
isolation: opts.isolation,
|
|
193
|
+
workspace,
|
|
155
194
|
// CLI harness subprocesses inherit the host network. A policy that
|
|
156
195
|
// forbids network must therefore deny dispatch unless a future host
|
|
157
196
|
// adapter creates and declares a network-disabled boundary.
|
|
@@ -189,7 +228,7 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
189
228
|
emit.text(
|
|
190
229
|
`run ${err.runId} parked\napproval: ${err.approvalId}\n` +
|
|
191
230
|
`journal: ${err.journalPath}\n` +
|
|
192
|
-
`resume after resolution:
|
|
231
|
+
`resume after resolution: ${resolveBinName()} workflow resume ${err.runId}\n`,
|
|
193
232
|
);
|
|
194
233
|
}
|
|
195
234
|
return;
|
|
@@ -220,14 +259,25 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
220
259
|
process.exit(1);
|
|
221
260
|
}
|
|
222
261
|
try {
|
|
223
|
-
const { assertWorkflowRunResumable, runWorkflow } =
|
|
224
|
-
"../core/workflow/index.ts"
|
|
225
|
-
);
|
|
262
|
+
const { assertWorkflowRunResumable, createLocalGitWorktreeProvider, runWorkflow } =
|
|
263
|
+
await import("../core/workflow/index.ts");
|
|
226
264
|
const { manifest } = assertWorkflowRunResumable(coordRoot, runId);
|
|
265
|
+
const binding = manifest.execution.workspace_binding;
|
|
266
|
+
if (binding && binding.provider.id !== "local-git-worktree") {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`workflow run ${runId} requires unavailable workspace provider ${binding.provider.id}`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
227
271
|
const report = await runWorkflow(manifest.script.path, {
|
|
228
272
|
coordRoot,
|
|
229
273
|
spawners: registry.spawners(),
|
|
230
274
|
resumeRunId: runId,
|
|
275
|
+
workspace: binding
|
|
276
|
+
? {
|
|
277
|
+
provider: createLocalGitWorktreeProvider({ coordRoot }),
|
|
278
|
+
writableRoots: [binding.writable_root.configured],
|
|
279
|
+
}
|
|
280
|
+
: undefined,
|
|
231
281
|
harnessEvidence: Object.fromEntries(
|
|
232
282
|
registry
|
|
233
283
|
.list()
|
|
@@ -281,6 +331,273 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
281
331
|
}
|
|
282
332
|
});
|
|
283
333
|
|
|
334
|
+
workflow
|
|
335
|
+
.command("workspace <run-id>")
|
|
336
|
+
.description("Show validated allocation, verification, integration, and cleanup state.")
|
|
337
|
+
.option("--json", "Emit the validated workspace status as JSON")
|
|
338
|
+
.action(async (runId: string, opts: { json?: boolean }) => {
|
|
339
|
+
const coordRoot = findCoordRoot();
|
|
340
|
+
if (!coordRoot) {
|
|
341
|
+
emit.error({
|
|
342
|
+
code: "no_coord_root",
|
|
343
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
344
|
+
});
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
const { inspectWorkflowWorkspace, renderWorkflowWorkspaceStatus } = await import(
|
|
348
|
+
"../core/workflow/index.ts"
|
|
349
|
+
);
|
|
350
|
+
const inspection = inspectWorkflowWorkspace(coordRoot, runId);
|
|
351
|
+
if (!inspection.ok) {
|
|
352
|
+
emit.error({ code: "workflow_workspace_invalid", message: inspection.error });
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
if (opts.json) {
|
|
356
|
+
emit.config({ format: "json" });
|
|
357
|
+
emit.data(inspection.value);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
emit.text(renderWorkflowWorkspaceStatus(inspection.value));
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
workflow
|
|
364
|
+
.command("workspaces")
|
|
365
|
+
.description("List validated isolated and shared-compatibility workspace decisions.")
|
|
366
|
+
.option("--json", "Emit workspace inspections as JSON")
|
|
367
|
+
.action(async (opts: { json?: boolean }) => {
|
|
368
|
+
const coordRoot = findCoordRoot();
|
|
369
|
+
if (!coordRoot) {
|
|
370
|
+
emit.error({
|
|
371
|
+
code: "no_coord_root",
|
|
372
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
373
|
+
});
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
const { listWorkflowWorkspaceInspections } = await import("../core/workflow/index.ts");
|
|
377
|
+
const inspections = listWorkflowWorkspaceInspections(coordRoot).filter(
|
|
378
|
+
(inspection) => !inspection.ok || inspection.value.selection !== "shared",
|
|
379
|
+
);
|
|
380
|
+
if (opts.json) {
|
|
381
|
+
emit.config({ format: "json" });
|
|
382
|
+
emit.data(inspections);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (inspections.length === 0) {
|
|
386
|
+
emit.text("no isolated or shared-compatibility workspace runs\n");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
emit.text(
|
|
390
|
+
`${inspections
|
|
391
|
+
.map((inspection) =>
|
|
392
|
+
inspection.ok
|
|
393
|
+
? [
|
|
394
|
+
inspection.value.run_id,
|
|
395
|
+
inspection.value.selection,
|
|
396
|
+
inspection.value.lifecycle.state,
|
|
397
|
+
`verify=${inspection.value.verification.status}`,
|
|
398
|
+
`integration=${inspection.value.integration.state}`,
|
|
399
|
+
`cleanup=${inspection.value.cleanup.state}`,
|
|
400
|
+
].join("\t")
|
|
401
|
+
: `${inspection.run_id}\tinvalid\t${inspection.error}`,
|
|
402
|
+
)
|
|
403
|
+
.join("\n")}\n`,
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const integration = workflow
|
|
408
|
+
.command("integration")
|
|
409
|
+
.description("Prepare and apply proof-gated isolated-workspace integration.");
|
|
410
|
+
|
|
411
|
+
integration
|
|
412
|
+
.command("prepare <run-id>")
|
|
413
|
+
.description("Preview integration and write exact review and policy authority.")
|
|
414
|
+
.option("--policy <file>", "Current host policy JSON/JSONC for the Git mutation")
|
|
415
|
+
.option("--reviewer <name>", "Explicit reviewer for a standalone workflow")
|
|
416
|
+
.option("--reason <text>", "Reason recorded with the standalone review")
|
|
417
|
+
.option("--target-root <dir>", "Explicit target checkout (default: frozen source checkout)")
|
|
418
|
+
.option(
|
|
419
|
+
"--accept-unknown <code>",
|
|
420
|
+
"Accept one verification unknown by code (repeatable)",
|
|
421
|
+
collectOption,
|
|
422
|
+
[],
|
|
423
|
+
)
|
|
424
|
+
.option("--approval-to <address>", "Address a durable policy ASK request")
|
|
425
|
+
.option("--json", "Emit the durable integration plan as JSON")
|
|
426
|
+
.action(async (runId: string, opts: WorkflowIntegrationPrepareOpts) => {
|
|
427
|
+
const coordRoot = findCoordRoot();
|
|
428
|
+
if (!coordRoot) {
|
|
429
|
+
emit.error({
|
|
430
|
+
code: "no_coord_root",
|
|
431
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
432
|
+
});
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
if (!opts.policy) {
|
|
436
|
+
emit.error({
|
|
437
|
+
code: "integration_policy_required",
|
|
438
|
+
message: "integration prepare requires --policy <file>",
|
|
439
|
+
});
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
if (opts.reason && !opts.reviewer) {
|
|
443
|
+
emit.error({
|
|
444
|
+
code: "integration_reviewer_required",
|
|
445
|
+
message: "--reason requires --reviewer <name>",
|
|
446
|
+
});
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
const provider = await builtInProviderForRun(coordRoot, runId);
|
|
451
|
+
const { IntegrationPrepareParkedError, prepareIntegration } = await import(
|
|
452
|
+
"../core/workflow/index.ts"
|
|
453
|
+
);
|
|
454
|
+
try {
|
|
455
|
+
const plan = await prepareIntegration({
|
|
456
|
+
coordRoot,
|
|
457
|
+
runId,
|
|
458
|
+
provider,
|
|
459
|
+
policy: loadPolicyFile(opts.policy),
|
|
460
|
+
targetRoot: opts.targetRoot,
|
|
461
|
+
review: opts.reviewer ? { actor: opts.reviewer, reason: opts.reason } : undefined,
|
|
462
|
+
acceptedUnknowns: opts.acceptUnknown,
|
|
463
|
+
approvalAddressee: opts.approvalTo,
|
|
464
|
+
});
|
|
465
|
+
if (opts.json) {
|
|
466
|
+
emit.config({ format: "json" });
|
|
467
|
+
emit.data(plan);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
emit.text(
|
|
471
|
+
`integration plan ${plan.plan_id} authorized\n` +
|
|
472
|
+
`source: ${plan.provider_preview.source_commit}\n` +
|
|
473
|
+
`target: ${plan.provider_preview.target_ref} at ${plan.provider_preview.target_commit}\n` +
|
|
474
|
+
`changes: ${plan.provider_preview.changed_paths.length}\n` +
|
|
475
|
+
`apply: ${resolveBinName()} workflow integration apply ${runId} --yes\n`,
|
|
476
|
+
);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
if (error instanceof IntegrationPrepareParkedError) {
|
|
479
|
+
if (opts.json) {
|
|
480
|
+
emit.config({ format: "json" });
|
|
481
|
+
emit.data({
|
|
482
|
+
status: "parked",
|
|
483
|
+
runId: error.runId,
|
|
484
|
+
planId: error.planId,
|
|
485
|
+
approvalId: error.approvalId,
|
|
486
|
+
});
|
|
487
|
+
} else {
|
|
488
|
+
emit.text(
|
|
489
|
+
`integration preparation parked\n` +
|
|
490
|
+
`run: ${error.runId}\n` +
|
|
491
|
+
`plan: ${error.planId}\n` +
|
|
492
|
+
`approval: ${error.approvalId}\n` +
|
|
493
|
+
`approve: ${resolveBinName()} workflow approvals approve ${error.approvalId}\n`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
throw error;
|
|
499
|
+
}
|
|
500
|
+
} catch (error) {
|
|
501
|
+
emit.error({
|
|
502
|
+
code: "workflow_integration_prepare_failed",
|
|
503
|
+
message: error instanceof Error ? error.message : String(error),
|
|
504
|
+
});
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
integration
|
|
510
|
+
.command("apply <run-id>")
|
|
511
|
+
.description("Apply an authorized durable integration plan.")
|
|
512
|
+
.option("--yes", "Confirm the target-branch mutation")
|
|
513
|
+
.option("--json", "Emit the durable integration receipt as JSON")
|
|
514
|
+
.action(async (runId: string, opts: WorkflowConfirmedMutationOpts) => {
|
|
515
|
+
const coordRoot = findCoordRoot();
|
|
516
|
+
if (!coordRoot) {
|
|
517
|
+
emit.error({
|
|
518
|
+
code: "no_coord_root",
|
|
519
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
520
|
+
});
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
if (!opts.yes) {
|
|
524
|
+
emit.error({
|
|
525
|
+
code: "integration_confirmation_required",
|
|
526
|
+
message: "integration apply changes the target branch; pass --yes to confirm",
|
|
527
|
+
});
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
const provider = await builtInProviderForRun(coordRoot, runId);
|
|
532
|
+
const { applyIntegration } = await import("../core/workflow/index.ts");
|
|
533
|
+
const receipt = await applyIntegration({ coordRoot, runId, provider });
|
|
534
|
+
if (opts.json) {
|
|
535
|
+
emit.config({ format: "json" });
|
|
536
|
+
emit.data(receipt);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
emit.text(
|
|
540
|
+
`integration ${receipt.status}: ${receipt.target_ref} at ${receipt.target_commit}\n` +
|
|
541
|
+
`receipt: ${receipt.receipt_id}\n` +
|
|
542
|
+
`cleanup: ${resolveBinName()} workflow cleanup ${runId} --yes\n`,
|
|
543
|
+
);
|
|
544
|
+
} catch (error) {
|
|
545
|
+
emit.error({
|
|
546
|
+
code: "workflow_integration_apply_failed",
|
|
547
|
+
message: error instanceof Error ? error.message : String(error),
|
|
548
|
+
});
|
|
549
|
+
process.exit(1);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
workflow
|
|
554
|
+
.command("cleanup <run-id>")
|
|
555
|
+
.description("Conservatively release a provider-owned isolated workspace.")
|
|
556
|
+
.option("--yes", "Confirm the worktree and provider-branch removal attempt")
|
|
557
|
+
.option("--json", "Emit the cleanup attempt or receipt as JSON")
|
|
558
|
+
.action(async (runId: string, opts: WorkflowConfirmedMutationOpts) => {
|
|
559
|
+
const coordRoot = findCoordRoot();
|
|
560
|
+
if (!coordRoot) {
|
|
561
|
+
emit.error({
|
|
562
|
+
code: "no_coord_root",
|
|
563
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
564
|
+
});
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
if (!opts.yes) {
|
|
568
|
+
emit.error({
|
|
569
|
+
code: "cleanup_confirmation_required",
|
|
570
|
+
message: "workspace cleanup may remove a worktree and branch; pass --yes to confirm",
|
|
571
|
+
});
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
574
|
+
try {
|
|
575
|
+
const provider = await builtInProviderForRun(coordRoot, runId);
|
|
576
|
+
const { cleanupWorkspace } = await import("../core/workflow/index.ts");
|
|
577
|
+
const result = await cleanupWorkspace({ coordRoot, runId, provider });
|
|
578
|
+
if (opts.json) {
|
|
579
|
+
emit.config({ format: "json" });
|
|
580
|
+
emit.data(result);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const receipt =
|
|
584
|
+
"receipt_id" in result && typeof result.receipt_id === "string"
|
|
585
|
+
? `\nreceipt: ${result.receipt_id}`
|
|
586
|
+
: "";
|
|
587
|
+
const reason =
|
|
588
|
+
"reason" in result && typeof result.reason === "string"
|
|
589
|
+
? `\nreason: ${result.reason}`
|
|
590
|
+
: "";
|
|
591
|
+
emit.text(`workspace cleanup ${result.status}: ${result.binding_id}${receipt}${reason}\n`);
|
|
592
|
+
} catch (error) {
|
|
593
|
+
emit.error({
|
|
594
|
+
code: "workflow_cleanup_failed",
|
|
595
|
+
message: error instanceof Error ? error.message : String(error),
|
|
596
|
+
});
|
|
597
|
+
process.exit(1);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
|
|
284
601
|
const approvals = workflow
|
|
285
602
|
.command("approvals")
|
|
286
603
|
.description("Inspect and resolve durable workflow policy approvals.");
|
|
@@ -382,7 +699,7 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
382
699
|
emit.text(
|
|
383
700
|
`${renderWorkflowApproval(resolved.approval)}` +
|
|
384
701
|
`${resolved.applied ? "decision recorded" : "decision already recorded"}\n` +
|
|
385
|
-
|
|
702
|
+
approvalNextActionHint(coordRoot, resolved.approval),
|
|
386
703
|
);
|
|
387
704
|
}
|
|
388
705
|
} catch (err) {
|
|
@@ -427,3 +744,44 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
427
744
|
}
|
|
428
745
|
});
|
|
429
746
|
}
|
|
747
|
+
|
|
748
|
+
async function builtInProviderForRun(coordRoot: string, runId: string): Promise<WorkspaceProvider> {
|
|
749
|
+
const { createLocalGitWorktreeProvider, readWorkflowRunManifest } = await import(
|
|
750
|
+
"../core/workflow/index.ts"
|
|
751
|
+
);
|
|
752
|
+
const manifest = readWorkflowRunManifest(coordRoot, runId);
|
|
753
|
+
const binding = manifest.execution.workspace_binding;
|
|
754
|
+
if (!binding) {
|
|
755
|
+
throw new Error(`workflow run ${runId} has no isolated workspace binding`);
|
|
756
|
+
}
|
|
757
|
+
if (binding.provider.id !== "local-git-worktree") {
|
|
758
|
+
throw new Error(
|
|
759
|
+
`workflow run ${runId} requires unavailable workspace provider ${binding.provider.id}`,
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
return createLocalGitWorktreeProvider({ coordRoot });
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function collectOption(value: string, previous: string[]): string[] {
|
|
766
|
+
return [...previous, value];
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** Next operator command after resolving an approval. Mid-run parks resume the
|
|
770
|
+
* workflow; integration prepare parks after the run is already terminal, so
|
|
771
|
+
* the durable plan is authorized by re-running prepare (resume would fail). */
|
|
772
|
+
function approvalNextActionHint(coordRoot: string, approval: WorkflowApproval): string {
|
|
773
|
+
const bin = resolveBinName();
|
|
774
|
+
const runId = approval.request.run_id;
|
|
775
|
+
const integrationPlanPath = join(
|
|
776
|
+
coordRoot,
|
|
777
|
+
".harnery",
|
|
778
|
+
"workflows",
|
|
779
|
+
runId,
|
|
780
|
+
"integration",
|
|
781
|
+
"plan.json",
|
|
782
|
+
);
|
|
783
|
+
if (existsSync(integrationPlanPath)) {
|
|
784
|
+
return `prepare: ${bin} workflow integration prepare ${runId}\n`;
|
|
785
|
+
}
|
|
786
|
+
return `resume: ${bin} workflow resume ${runId}\n`;
|
|
787
|
+
}
|