@agentxm/workspace-operations 0.28.12 → 0.28.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/src/index.d.ts +11 -10
  2. package/dist/src/index.js +16 -12
  3. package/dist/src/operations/scan-plan-readiness.d.ts +1 -1
  4. package/dist/src/operations/scan-plan-readiness.js +1 -1
  5. package/dist/src/plan/apply-plan.js +8 -8
  6. package/dist/src/plan/execution-candidate.d.ts +7 -1
  7. package/dist/src/plan/execution-candidate.js +9 -2
  8. package/dist/src/plan/interruption-resolution.d.ts +50 -0
  9. package/dist/src/plan/interruption-resolution.js +158 -0
  10. package/dist/src/plan/operation-journal.d.ts +9 -9
  11. package/dist/src/plan/operation-journal.js +5 -20
  12. package/dist/src/plan/operation-resolution.d.ts +1 -1
  13. package/dist/src/plan/plan-execution-fixtures.d.ts +2 -2
  14. package/dist/src/plan/plan-execution-fixtures.js +2 -2
  15. package/dist/src/plan/plan-execution.d.ts +48 -0
  16. package/dist/src/plan/plan-execution.js +40 -0
  17. package/dist/src/plan/plan.d.ts +9 -19
  18. package/dist/src/plan/resolve-plan-interaction.d.ts +1 -1
  19. package/dist/src/plan/resolve-plan-interaction.js +1 -1
  20. package/dist/src/plan/resolve-plan.d.ts +38 -23
  21. package/dist/src/plan/resolve-plan.js +114 -96
  22. package/dist/src/plan/step-failure-conversions.d.ts +2 -1
  23. package/dist/src/testing.d.ts +22 -1
  24. package/dist/src/testing.js +23 -1
  25. package/package.json +12 -16
  26. package/dist/src/live.d.ts +0 -13
  27. package/dist/src/live.js +0 -12
  28. package/dist/src/operations/load-workspace.d.ts +0 -44
  29. package/dist/src/operations/load-workspace.js +0 -80
  30. package/dist/src/operations/memory-transition-lock.d.ts +0 -20
  31. package/dist/src/operations/memory-transition-lock.js +0 -57
  32. package/dist/src/operations/transaction.d.ts +0 -60
  33. package/dist/src/operations/transaction.js +0 -358
  34. package/dist/src/operations/transition-lock.d.ts +0 -72
  35. package/dist/src/operations/transition-lock.js +0 -298
@@ -1,4 +1,44 @@
1
+ /**
2
+ * The one derivation of an approval mode from what a request carries.
3
+ *
4
+ * It lives with plan execution rather than in a transport adapter because the
5
+ * distinction it draws — between a route that offers preapproval and one that
6
+ * does not — is what `resolveExecutionCandidate` uses to decide whether an
7
+ * unattended apply blocks naming interactive approval or names a flag.
8
+ */
9
+ export const confirmableRiskApproval = (requested) => requested.preapproved === true
10
+ ? "preapproved"
11
+ : requested.preapproved === false
12
+ ? "prompt-if-interactive"
13
+ : "interactive-only";
1
14
  export const previewPlanExecution = { request: { mode: "preview" } };
15
+ /**
16
+ * The one derivation of a plan execution from what a request carries.
17
+ *
18
+ * It lives here for the same reason `confirmableRiskApproval` does: the
19
+ * preview arm of `PlanExecution` has no field an approval could occupy, so
20
+ * advance approval accompanying a preview is dropped by this derivation
21
+ * rather than by a transport adapter's own convention. A preview therefore
22
+ * cannot spend an approval it never receives, and two requests that differ
23
+ * only in approval reach `resolveExecutionCandidate` as the same execution.
24
+ */
25
+ export const requestedPlanExecution = (options) => options.intent.preview
26
+ ? {
27
+ ...previewPlanExecution,
28
+ ...(options.configuredAgentOperations === undefined
29
+ ? {}
30
+ : { configuredAgentOperations: options.configuredAgentOperations }),
31
+ }
32
+ : applyPlanExecution({
33
+ approval: confirmableRiskApproval(options.intent.yes === undefined ? {} : { preapproved: options.intent.yes }),
34
+ ...(options.acceptedPolicies === undefined
35
+ ? {}
36
+ : { acceptedPolicies: options.acceptedPolicies }),
37
+ recovery: options.recovery,
38
+ ...(options.configuredAgentOperations === undefined
39
+ ? {}
40
+ : { configuredAgentOperations: options.configuredAgentOperations }),
41
+ });
2
42
  export const applyPlanExecution = (options) => ({
3
43
  request: {
4
44
  mode: "apply",
@@ -23,7 +23,7 @@ import { type ExtensionType } from "@agentxm/extension-model/unstable/extensions
23
23
  import type { ArtifactChange } from "@agentxm/workspace-state";
24
24
  import type { ConfiguredAgentOutcome } from "@agentxm/workspace-state";
25
25
  import type { DeprecationView } from "@agentxm/extension-model/unstable/extensions/deprecation";
26
- import type { ReleaseAgeOperationEvidence } from "@agentxm/registry-protocol/unstable/registry/release-age-policy";
26
+ import type { RegistryBindingProposal, ReleaseAgeOperationEvidence } from "@agentxm/extension-resolution";
27
27
  import type { SuggestedAction } from "@agentxm/registry-protocol/unstable/suggested-action";
28
28
  export declare const PlanPolicyIds: readonly ["ignore-version-constraints", "accept-warnings"];
29
29
  export declare const PlanPolicyIdSchema: Schema.Literals<readonly ["ignore-version-constraints", "accept-warnings"]>;
@@ -111,21 +111,6 @@ export interface JobStepArtifactSource {
111
111
  readonly directory?: string;
112
112
  readonly gitTreeHash?: string;
113
113
  }
114
- /**
115
- * The Registry identity a step proposes to accept. Trust classification
116
- * compares it with the accepted resolution for the same configured target,
117
- * so a change of publisher is identified from structured data rather than
118
- * from warning text.
119
- */
120
- export interface RegistryBindingProposal {
121
- readonly extensionType: ExtensionType;
122
- /** The configured (local) name whose accepted resolution the step replaces. */
123
- readonly target: string;
124
- readonly owner: string;
125
- readonly packageName: string;
126
- readonly version: string;
127
- readonly publisherBindingId: string;
128
- }
129
114
  export interface RegistryLifecycleEvidence {
130
115
  readonly deprecation: DeprecationView;
131
116
  }
@@ -253,9 +238,14 @@ export interface Plan<Requirements = never, Output = never> {
253
238
  /** Persisted inputs outside workspace state that materially determine this plan. */
254
239
  readonly materialPaths?: ReadonlyArray<string>;
255
240
  /** Local plans roll back candidate-wide; remote effects report truthful partial outcomes. */
256
- readonly executionCapabilities?: {
257
- readonly rollback: "local-atomic" | "non-rollbackable";
258
- };
241
+ readonly executionCapabilities?: PlanExecutionCapabilities;
242
+ }
243
+ /**
244
+ * What a plan promises about undoing itself. A producer declares this so the
245
+ * atomicity a result reports is the one the plan's owner decided.
246
+ */
247
+ export interface PlanExecutionCapabilities {
248
+ readonly rollback: "local-atomic" | "non-rollbackable";
259
249
  }
260
250
  export interface ExecutedJob<Output = never> {
261
251
  readonly steps: ReadonlyArray<CompletedJobStep<Output>>;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Plan-resolution interaction port.
3
3
  *
4
- * `previewOrApplyPlan` presents candidates and obtains the apply confirmation
4
+ * `resolveExecutionCandidate` presents candidates and obtains the apply confirmation
5
5
  * exclusively through this service. The CLI runtime provides the renderer- and
6
6
  * prompt-backed implementation; wording and verbosity gating belong to that
7
7
  * implementation, never to the kernel. Progress is not an interaction: the
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Plan-resolution interaction port.
3
3
  *
4
- * `previewOrApplyPlan` presents candidates and obtains the apply confirmation
4
+ * `resolveExecutionCandidate` presents candidates and obtains the apply confirmation
5
5
  * exclusively through this service. The CLI runtime provides the renderer- and
6
6
  * prompt-backed implementation; wording and verbosity gating belong to that
7
7
  * implementation, never to the kernel. Progress is not an interaction: the
@@ -1,42 +1,57 @@
1
1
  /**
2
- * Plan preview/apply function.
2
+ * Candidate preparation and resolution.
3
3
  *
4
- * Orchestrates `augmentPlanWithReconciliation`, `scanPlanReadiness`,
5
- * and `applyPlan` with the `ResolvePlanInteraction` port, and produces
6
- * one `OperationResolution` at every termination path. Channels project the
7
- * returned resolution; presentation and prompting live behind the port, and
8
- * per-type outcome refinement behind the optional
9
- * `ConfiguredAgentOutcomesProvider` port.
4
+ * `prepareExecutionCandidate` turns a plan into the immutable execution
5
+ * candidate every later step refers to: it augments the plan with lockfile
6
+ * reconciliation, projects configured-agent outcomes, scans readiness,
7
+ * assembles the semantic risk conditions, and fingerprints the material
8
+ * preimages. `resolveExecutionCandidate` presents that candidate, fails
9
+ * closed on blockers and missing policies, previews or confirms, acquires
10
+ * the workspace transition, revalidates the same candidate under it, applies
11
+ * it closure by closure, and produces one `OperationResolution` at every
12
+ * termination path. Presentation and prompting live behind the
13
+ * `ResolvePlanInteraction` port; per-type outcome refinement behind the
14
+ * `ConfiguredAgentOutcomesProvider`.
10
15
  *
11
- * This is a free function, not a method on WorkspaceMutationsService.
12
16
  *
13
17
  * @experimental This API is unstable and may change without notice.
14
18
  */
15
- import * as FileSystem from "effect/FileSystem";
16
19
  import * as Path from "effect/Path";
17
20
  import * as Effect from "effect/Effect";
18
21
  import { ApprovalRecoveryMissing, StepFailure } from "./errors.js";
19
22
  import { type ExecutionCandidate } from "./execution-candidate.js";
20
23
  import type { Plan } from "./plan.js";
21
24
  import { type OperationResolution } from "./operation-resolution.js";
22
- import { WorkspaceMutations } from "@agentxm/workspace-state";
25
+ import { OperationJournal } from "./operation-journal.js";
26
+ import { ConfiguredAgentOutcomesProvider, WorkspaceMutations } from "@agentxm/workspace-state";
27
+ import { FootprintRecorder } from "@agentxm/workspace-transactions";
23
28
  import { ResolvePlanInteraction } from "./resolve-plan-interaction.js";
24
- import { type PlanExecution } from "./plan-execution.js";
29
+ import { type ConfiguredAgentOperation, type PlanExecution } from "./plan-execution.js";
30
+ export interface PrepareExecutionCandidateOptions {
31
+ /** Operations whose configured-agent outcomes the candidate projects and, after apply, verifies. */
32
+ readonly configuredAgentOperations?: ReadonlyArray<ConfiguredAgentOperation>;
33
+ }
25
34
  /**
26
- * Preview or apply (display, confirm, and execute) a plan using the workspace read model.
35
+ * Prepare the immutable execution candidate for a plan: augment with lockfile
36
+ * reconciliation, project configured-agent outcomes, scan readiness, assemble
37
+ * risk conditions, and fingerprint every material preimage. Preview,
38
+ * confirmation, and apply all refer to the candidate this returns.
39
+ */
40
+ export declare const prepareExecutionCandidate: <Requirements, Output>(plan: Plan<Requirements, Output>, options?: PrepareExecutionCandidateOptions | undefined) => Effect.Effect<ExecutionCandidate<Requirements, Output>, import("./errors.js").CandidateFingerprintFailed | import("@agentxm/workspace-state").LockfileValidationError | import("@agentxm/workspace-state").WorkspaceSettingsReadFailure, Path.Path | import("effect/FileSystem").FileSystem | WorkspaceMutations | ConfiguredAgentOutcomesProvider>;
41
+ export interface ResolveExecutionCandidateOptions<Requirements, Output> {
42
+ /** A typed pre-apply gate that runs under the transition before revalidation. */
43
+ readonly beforeApply?: (candidate: ExecutionCandidate<Requirements, Output>) => Effect.Effect<void, StepFailure, Requirements>;
44
+ }
45
+ /**
46
+ * Preview or apply a prepared candidate.
27
47
  *
28
- * Steps:
29
- * 1. Augment plan with lockfile reconciliation if needed
30
- * 2. Scan for errors/warnings
31
- * 3. Construct and display the exact candidate
32
- * 4. Fail closed on blockers and missing named policies
33
- * 5. Preview or approve confirmable semantic risk
34
- * 6. Revalidate and apply the same candidate
48
+ * 1. Present the immutable candidate.
49
+ * 2. Fail closed on blockers and missing named policies.
50
+ * 3. Preview, or approve confirmable semantic risk.
51
+ * 4. Acquire the workspace transition, revalidate the same candidate under
52
+ * it, and apply it closure by closure.
35
53
  *
36
54
  * Every termination path resolves to one `OperationResolution`.
37
55
  */
38
- export declare const previewOrApplyPlan: <Requirements, Output>(plan: Plan<Requirements, Output>, options: {
39
- execution: PlanExecution;
40
- beforeApply?: (candidate: ExecutionCandidate<Requirements, Output>) => Effect.Effect<void, StepFailure, Requirements>;
41
- }) => Effect.Effect<OperationResolution<Output>, import("./errors.js").CandidateFingerprintFailed | ApprovalRecoveryMissing | import("./errors.js").PlanInteractionFailed | import("@agentxm/workspace-state").WorkspaceTransitionAcquireFailure | import("@agentxm/workspace-state").LockfileValidationError | import("@agentxm/workspace-state").WorkspaceSettingsReadFailure, Path.Path | FileSystem.FileSystem | ResolvePlanInteraction | WorkspaceMutations | Exclude<Requirements, import("effect/Scope").Scope>>;
56
+ export declare const resolveExecutionCandidate: <Requirements, Output>(candidate: ExecutionCandidate<Requirements, Output>, execution: PlanExecution, options?: ResolveExecutionCandidateOptions<Requirements, Output> | undefined) => Effect.Effect<OperationResolution<Output>, ApprovalRecoveryMissing | import("./errors.js").PlanInteractionFailed | import("@agentxm/workspace-state").WorkspaceSettingsReadFailure | import("@agentxm/workspace-transactions").WorkspaceTransitionAcquireFailure, OperationJournal | Path.Path | import("effect/FileSystem").FileSystem | ResolvePlanInteraction | WorkspaceMutations | ConfiguredAgentOutcomesProvider | import("@agentxm/workspace-transactions").WorkspaceTransactionScope | Exclude<Requirements, import("effect/Scope").Scope> | FootprintRecorder>;
42
57
  //# sourceMappingURL=resolve-plan.d.ts.map
@@ -1,22 +1,24 @@
1
1
  /**
2
- * Plan preview/apply function.
2
+ * Candidate preparation and resolution.
3
3
  *
4
- * Orchestrates `augmentPlanWithReconciliation`, `scanPlanReadiness`,
5
- * and `applyPlan` with the `ResolvePlanInteraction` port, and produces
6
- * one `OperationResolution` at every termination path. Channels project the
7
- * returned resolution; presentation and prompting live behind the port, and
8
- * per-type outcome refinement behind the optional
9
- * `ConfiguredAgentOutcomesProvider` port.
4
+ * `prepareExecutionCandidate` turns a plan into the immutable execution
5
+ * candidate every later step refers to: it augments the plan with lockfile
6
+ * reconciliation, projects configured-agent outcomes, scans readiness,
7
+ * assembles the semantic risk conditions, and fingerprints the material
8
+ * preimages. `resolveExecutionCandidate` presents that candidate, fails
9
+ * closed on blockers and missing policies, previews or confirms, acquires
10
+ * the workspace transition, revalidates the same candidate under it, applies
11
+ * it closure by closure, and produces one `OperationResolution` at every
12
+ * termination path. Presentation and prompting live behind the
13
+ * `ResolvePlanInteraction` port; per-type outcome refinement behind the
14
+ * `ConfiguredAgentOutcomesProvider`.
10
15
  *
11
- * This is a free function, not a method on WorkspaceMutationsService.
12
16
  *
13
17
  * @experimental This API is unstable and may change without notice.
14
18
  */
15
19
  import * as Cause from "effect/Cause";
16
- import * as FileSystem from "effect/FileSystem";
17
20
  import * as Path from "effect/Path";
18
21
  import * as Effect from "effect/Effect";
19
- import * as Layer from "effect/Layer";
20
22
  import * as Option from "effect/Option";
21
23
  import * as Ref from "effect/Ref";
22
24
  import { ApprovalRecoveryMissing, STALE_CANDIDATE_DETAIL, StaleExecutionCandidate, StepFailure, } from "./errors.js";
@@ -25,17 +27,13 @@ import { isExecutionCandidateFresh, makeExecutionCandidate, } from "./execution-
25
27
  import { augmentPlanWithReconciliation } from "../operations/augment-plan.js";
26
28
  import { scanPlanReadiness } from "../operations/scan-plan-readiness.js";
27
29
  import { declaredAtomicity, executedUnits, makeOperationResolution, plannedUnits, unitIdOf, } from "./operation-resolution.js";
28
- import { appendResolvedUnit, appendStartedUnit, recordJournalPhase, recordOperationJournal, } from "./operation-journal.js";
30
+ import { OperationJournal, appendResolvedUnit, appendStartedUnit, recordJournalPhase, recordOperationJournal, } from "./operation-journal.js";
29
31
  import { CurrentOperationUnit, observeUnit, publishOperationEvent, publishPhaseStarted, publishWaitEnded, publishWaiting, } from "./operation-events.js";
30
- import { WorkspaceMutations } from "@agentxm/workspace-state";
31
- import { readPendingClosureRestorationFailures, WorkspaceRestorationIncomplete, } from "@agentxm/workspace-state";
32
- import { rollbackWorkspaceClosure, settleWorkspaceClosure, withWorkspaceClosure, } from "../operations/transaction.js";
33
- import { readFootprint } from "@agentxm/workspace-state";
32
+ import { ConfiguredAgentOutcomesProvider, WorkspaceMutations, configuredAgentLifecycleOutcomes, } from "@agentxm/workspace-state";
33
+ import { FootprintRecorder, WorkspaceRestorationIncomplete, acquireWorkspaceTransition, pendingClosureRestorations, readFootprint, rollbackWorkspaceClosure, runWorkspaceTransaction, settleWorkspaceClosure, withWorkspaceClosure, } from "@agentxm/workspace-transactions";
34
34
  import { InterruptionSignalSource } from "./interruption-signal.js";
35
35
  import { ResolvePlanInteraction } from "./resolve-plan-interaction.js";
36
36
  import { confirmationRecoverySuggestions, namedPolicyRecoverySuggestions, } from "./plan-execution.js";
37
- import { ConfiguredAgentOutcomesProvider } from "@agentxm/workspace-state";
38
- import { configuredAgentLifecycleOutcomes } from "@agentxm/workspace-state";
39
37
  import { candidateFingerprintFailedToStepFailure, configuredAgentOutcomesUnavailableToStepFailure, restorationIncompleteToStepFailure, workspaceStateReadFailureToStepFailure, workspaceTransactionFailureToStepFailure, } from "./step-failure-conversions.js";
40
38
  /** Publish a phase transition to the lifecycle stream and the journal. */
41
39
  const enterPhase = (phase) => publishPhaseStarted(phase).pipe(Effect.andThen(recordJournalPhase(phase)));
@@ -87,59 +85,30 @@ const withExecutedAgentOutcomes = (plan, outcomes) => ({
87
85
  })),
88
86
  });
89
87
  /**
90
- * Preview or apply (display, confirm, and execute) a plan using the workspace read model.
91
- *
92
- * Steps:
93
- * 1. Augment plan with lockfile reconciliation if needed
94
- * 2. Scan for errors/warnings
95
- * 3. Construct and display the exact candidate
96
- * 4. Fail closed on blockers and missing named policies
97
- * 5. Preview or approve confirmable semantic risk
98
- * 6. Revalidate and apply the same candidate
99
- *
100
- * Every termination path resolves to one `OperationResolution`.
88
+ * The generic lifecycle outcomes for one operation, refined by the provider's
89
+ * per-type override when the operation enables the extension.
101
90
  */
102
- export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (plan, options) {
103
- const ws = yield* WorkspaceMutations;
104
- const interaction = yield* ResolvePlanInteraction;
105
- const fs = yield* FileSystem.FileSystem;
106
- const path = yield* Path.Path;
107
- const fsLayer = Layer.mergeAll(Layer.succeed(FileSystem.FileSystem, fs), Layer.succeed(Path.Path, path));
108
- const mode = options.execution.request.mode;
109
- yield* publishPhaseStarted("planning");
110
- // Step 1: Lockfile reconciliation, observed as one planning unit.
111
- const augmented = yield* observeUnit({ id: "lockfile-reconciliation", label: "lockfile reconciliation" }, augmentPlanWithReconciliation(plan, () => ws.getLockfileState()));
112
- const operations = options.execution.configuredAgentOperations ?? [];
113
- const configuredAgents = operations.length === 0 ? [] : yield* ws.getConfiguredAgents();
114
- const outcomesProvider = yield* Effect.serviceOption(ConfiguredAgentOutcomesProvider);
115
- const outcomesOverrideFor = (extensionType) => Option.isSome(outcomesProvider)
116
- ? outcomesProvider.value.byExtensionType[extensionType]
117
- : undefined;
118
- const outcomesFor = (operation, state) => {
119
- const generic = configuredAgentLifecycleOutcomes({
120
- type: operation.extensionType,
121
- name: operation.name,
122
- agentIds: configuredAgents,
123
- scope: ws.scope,
124
- state,
125
- targetState: operation.plannedState,
126
- installed: state === "projected",
127
- observedAgentIds: state === "projected" ? configuredAgents : [],
128
- });
129
- const override = outcomesOverrideFor(operation.extensionType);
130
- if (operation.plannedState === "enabled" && override !== undefined) {
131
- return override(state).pipe(Effect.map((outcomes) => outcomes.filter(({ name }) => name === operation.name)), Effect.map((outcomes) => (outcomes.length === 0 ? generic : outcomes)), Effect.catch(() => Effect.succeed(generic)));
132
- }
133
- return Effect.succeed(generic);
134
- };
135
- const projectedOutcomes = (yield* Effect.forEach(operations, (operation) => outcomesFor(operation, "projected"))).flat();
136
- const augmentedPlan = operations.length === 0
137
- ? augmented.plan
138
- : withPlannedAgentOutcomes(augmented.plan, projectedOutcomes);
139
- // Step 2: Scan readiness and construct semantic risk conditions.
140
- const readiness = scanPlanReadiness(augmentedPlan);
141
- const declaredConditionIds = new Set((augmentedPlan.riskConditions ?? []).map((condition) => condition.id));
142
- const readinessBlockers = augmentedPlan.jobs.flatMap((job) => job.steps.flatMap((step) => step.readiness === "error"
91
+ const outcomesFor = (ws, provider, configuredAgents, operation, state) => {
92
+ const generic = configuredAgentLifecycleOutcomes({
93
+ type: operation.extensionType,
94
+ name: operation.name,
95
+ agentIds: configuredAgents,
96
+ scope: ws.scope,
97
+ state,
98
+ targetState: operation.plannedState,
99
+ installed: state === "projected",
100
+ observedAgentIds: state === "projected" ? configuredAgents : [],
101
+ });
102
+ const override = provider.byExtensionType[operation.extensionType];
103
+ if (operation.plannedState === "enabled" && override !== undefined) {
104
+ return override(state).pipe(Effect.map((outcomes) => outcomes.filter(({ name }) => name === operation.name)), Effect.map((outcomes) => (outcomes.length === 0 ? generic : outcomes)), Effect.catch(() => Effect.succeed(generic)));
105
+ }
106
+ return Effect.succeed(generic);
107
+ };
108
+ /** The readiness blockers a plan's error steps contribute beyond declared conditions. */
109
+ const readinessBlockersOf = (plan) => {
110
+ const declaredConditionIds = new Set((plan.riskConditions ?? []).map((condition) => condition.id));
111
+ return plan.jobs.flatMap((job) => job.steps.flatMap((step) => step.readiness === "error"
143
112
  ? (step.blockingConditionIds ?? []).length > 0 &&
144
113
  (step.blockingConditionIds ?? []).every((id) => declaredConditionIds.has(id))
145
114
  ? []
@@ -152,6 +121,27 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
152
121
  },
153
122
  ]
154
123
  : []));
124
+ };
125
+ /**
126
+ * Prepare the immutable execution candidate for a plan: augment with lockfile
127
+ * reconciliation, project configured-agent outcomes, scan readiness, assemble
128
+ * risk conditions, and fingerprint every material preimage. Preview,
129
+ * confirmation, and apply all refer to the candidate this returns.
130
+ */
131
+ export const prepareExecutionCandidate = Effect.fn("prepareExecutionCandidate")(function* (plan, options) {
132
+ const ws = yield* WorkspaceMutations;
133
+ const provider = yield* ConfiguredAgentOutcomesProvider;
134
+ yield* publishPhaseStarted("planning");
135
+ // Lockfile reconciliation, observed as one planning unit.
136
+ const augmented = yield* observeUnit({ id: "lockfile-reconciliation", label: "lockfile reconciliation" }, augmentPlanWithReconciliation(plan, () => ws.getLockfileState()));
137
+ const operations = options?.configuredAgentOperations ?? [];
138
+ const configuredAgents = operations.length === 0 ? [] : yield* ws.getConfiguredAgents();
139
+ const projectedOutcomes = (yield* Effect.forEach(operations, (operation) => outcomesFor(ws, provider, configuredAgents, operation, "projected"))).flat();
140
+ const augmentedPlan = operations.length === 0
141
+ ? augmented.plan
142
+ : withPlannedAgentOutcomes(augmented.plan, projectedOutcomes);
143
+ // Readiness blockers and unmet preconditions join the declared risk
144
+ // conditions, so the candidate carries every reason it could be refused.
155
145
  const preconditionBlockers = (augmentedPlan.preconditions ?? []).flatMap((precondition) => precondition.status === "unmet"
156
146
  ? [
157
147
  {
@@ -166,18 +156,46 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
166
156
  : []);
167
157
  const riskConditions = [
168
158
  ...(augmentedPlan.riskConditions ?? []),
169
- ...readinessBlockers,
159
+ ...readinessBlockersOf(augmentedPlan),
170
160
  ...preconditionBlockers,
171
161
  ];
172
162
  const candidatePlan = {
173
163
  ...augmentedPlan,
174
164
  ...(riskConditions.length === 0 ? {} : { riskConditions }),
175
165
  };
176
- const candidate = yield* makeExecutionCandidate(candidatePlan, {
166
+ return yield* makeExecutionCandidate(candidatePlan, {
177
167
  settingsPath: ws.layout.settingsPath,
178
168
  lockPath: ws.layout.lockPath,
179
169
  baseDir: ws.baseDir,
180
- }).pipe(Effect.provide(fsLayer));
170
+ }, operations);
171
+ });
172
+ /**
173
+ * Preview or apply a prepared candidate.
174
+ *
175
+ * 1. Present the immutable candidate.
176
+ * 2. Fail closed on blockers and missing named policies.
177
+ * 3. Preview, or approve confirmable semantic risk.
178
+ * 4. Acquire the workspace transition, revalidate the same candidate under
179
+ * it, and apply it closure by closure.
180
+ *
181
+ * Every termination path resolves to one `OperationResolution`.
182
+ */
183
+ export const resolveExecutionCandidate = Effect.fn("resolveExecutionCandidate")(function* (candidate, execution, options) {
184
+ const ws = yield* WorkspaceMutations;
185
+ const provider = yield* ConfiguredAgentOutcomesProvider;
186
+ const interaction = yield* ResolvePlanInteraction;
187
+ const path = yield* Path.Path;
188
+ // The journal and footprint recorder are requirements of the operation
189
+ // boundary: the settlement observers below record into them.
190
+ const journal = yield* OperationJournal;
191
+ yield* FootprintRecorder;
192
+ const mode = execution.request.mode;
193
+ const candidatePlan = candidate.plan;
194
+ const operations = candidate.configuredAgentOperations;
195
+ const configuredAgents = operations.length === 0 ? [] : yield* ws.getConfiguredAgents();
196
+ const readiness = scanPlanReadiness(candidatePlan);
197
+ const readinessBlockers = readinessBlockersOf(candidatePlan);
198
+ const riskConditions = candidatePlan.riskConditions ?? [];
181
199
  const atomicity = declaredAtomicity(candidatePlan);
182
200
  const resolutionBase = {
183
201
  name: candidatePlan.name,
@@ -218,13 +236,13 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
218
236
  failure: over.failure,
219
237
  suggestions: candidatePlan.failureSuggestions,
220
238
  });
221
- // Step 3: Display the immutable candidate before any policy terminal or effect.
239
+ // Display the immutable candidate before any policy terminal or effect.
222
240
  if (mode === "preview") {
223
241
  yield* enterPhase("preview");
224
242
  }
225
243
  const hasConfirmableRisk = riskConditions.some((condition) => condition.level === "confirmable");
226
244
  yield* interaction.presentPlan(candidatePlan, { mode });
227
- // Step 4: Hard blockers dominate preview, overrides, and confirmation.
245
+ // Hard blockers dominate preview, overrides, and confirmation.
228
246
  if (readiness.hasErrors) {
229
247
  const firstError = readinessBlockers[0];
230
248
  return notExecuted({
@@ -250,18 +268,18 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
250
268
  },
251
269
  });
252
270
  }
253
- // Step 5: Preview is speculative and never grants approval to a later invocation.
254
- if (options.execution.request.mode === "preview") {
271
+ // Preview is speculative and never grants approval to a later invocation.
272
+ if (execution.request.mode === "preview") {
255
273
  return makeOperationResolution({
256
274
  ...resolutionBase,
257
275
  atomicity: { declared: atomicity, applied: "closure-atomic" },
258
276
  units: plannedUnits(candidatePlan.jobs),
259
277
  });
260
278
  }
261
- if (!("approvalRecovery" in options.execution)) {
279
+ if (!("approvalRecovery" in execution)) {
262
280
  return yield* new ApprovalRecoveryMissing();
263
281
  }
264
- const applyExecution = options.execution;
282
+ const applyExecution = execution;
265
283
  const overrideConditions = riskConditions.filter((condition) => condition.level === "override-required");
266
284
  const missingOverrides = overrideConditions.filter((condition) => !applyExecution.request.acceptedPolicies.has(condition.policy));
267
285
  if (missingOverrides.length > 0) {
@@ -281,8 +299,8 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
281
299
  suggestions: escapes,
282
300
  });
283
301
  }
284
- // Step 5b: Confirmation. A condition that consents only at a prompt — a
285
- // publisher change, or any confirmable condition met by a command with no
302
+ // Confirmation. A condition that consents only at a prompt — a publisher
303
+ // change, or any confirmable condition met by a command with no
286
304
  // preapprovable confirmation — is never satisfied by preapproval. A
287
305
  // preapprovable condition is satisfied by explicit preapproval; otherwise
288
306
  // it is asked when a prompt can open and blocks when one cannot, naming
@@ -319,7 +337,7 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
319
337
  }
320
338
  }
321
339
  }
322
- // Step 6: Acquire the workspace transition — planning, network acquisition,
340
+ // Acquire the workspace transition — planning, network acquisition,
323
341
  // preview, and confirmation ran without it — then revalidate every material
324
342
  // candidate preimage and apply the exact candidate while holding it.
325
343
  const totalUnits = candidate.plan.jobs.reduce((count, job) => count + job.steps.length, 0);
@@ -329,16 +347,16 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
329
347
  description: candidatePlan.description,
330
348
  jobs: [{ concurrency: 1, steps: [step] }],
331
349
  }, { restored: false })[0]?.state ?? "committed";
332
- let startedUnits = 0;
333
- let resolvedUnits = 0;
350
+ const startedUnits = yield* Ref.make(0);
351
+ const resolvedUnits = yield* Ref.make(0);
334
352
  const applyFreshCandidate = Effect.gen(function* () {
335
353
  yield* enterPhase("validation");
336
- if (!(yield* isExecutionCandidateFresh(candidate).pipe(Effect.provide(fsLayer)))) {
354
+ if (!(yield* isExecutionCandidateFresh(candidate))) {
337
355
  return yield* new StaleExecutionCandidate({ candidate: candidatePlan.name });
338
356
  }
339
- if (options.beforeApply !== undefined) {
357
+ if (options?.beforeApply !== undefined) {
340
358
  yield* options.beforeApply(candidate);
341
- if (!(yield* isExecutionCandidateFresh(candidate).pipe(Effect.provide(fsLayer)))) {
359
+ if (!(yield* isExecutionCandidateFresh(candidate))) {
342
360
  return yield* new StaleExecutionCandidate({ candidate: candidatePlan.name });
343
361
  }
344
362
  }
@@ -361,29 +379,29 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
361
379
  return yield* applyPlan(closureScopedPlan, {
362
380
  // The started fact is journaled before the run's first effect, so an
363
381
  // interruption mid-run reports the unit in flight, never not attempted.
364
- onStepStarted: (step) => appendStartedUnit(unitIdOf(step)).pipe(Effect.andThen(publishOperationEvent((seq, atMs) => ({
382
+ onStepStarted: (step) => appendStartedUnit(unitIdOf(step)).pipe(Effect.provideService(OperationJournal, journal), Effect.andThen(Ref.getAndUpdate(startedUnits, (index) => index + 1)), Effect.flatMap((index) => publishOperationEvent((seq, atMs) => ({
365
383
  _tag: "UnitStarted",
366
384
  seq,
367
385
  atMs,
368
386
  unitId: unitIdOf(step),
369
387
  label: step.label,
370
- index: startedUnits++,
388
+ index,
371
389
  total: totalUnits,
372
390
  })))),
373
391
  // Settlement runs before the next interruptible boundary: the journal
374
392
  // fact and the closure's snapshot disposition are recorded together —
375
393
  // a settled closure's commits stand, a failed closure restores only
376
394
  // itself, and later ready closures continue.
377
- onStepCompleted: (step) => appendResolvedUnit(step).pipe(Effect.andThen(step.result.result === "error"
395
+ onStepCompleted: (step) => appendResolvedUnit(step).pipe(Effect.provideService(OperationJournal, journal), Effect.andThen(step.result.result === "error"
378
396
  ? rollbackWorkspaceClosure(unitIdOf(step))
379
- : settleWorkspaceClosure(unitIdOf(step))), Effect.andThen(publishOperationEvent((seq, atMs) => ({
397
+ : settleWorkspaceClosure(unitIdOf(step))), Effect.andThen(Ref.getAndUpdate(resolvedUnits, (index) => index + 1)), Effect.flatMap((index) => publishOperationEvent((seq, atMs) => ({
380
398
  _tag: "UnitResolved",
381
399
  seq,
382
400
  atMs,
383
401
  unitId: unitIdOf(step),
384
402
  label: step.label,
385
403
  state: resolvedUnitState(step),
386
- index: resolvedUnits++,
404
+ index,
387
405
  total: totalUnits,
388
406
  })))),
389
407
  });
@@ -402,7 +420,7 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
402
420
  // apply-level failure is a closure rollback that did not complete and
403
421
  // verify — the typed restoration fact derives the retained truth from
404
422
  // the in-memory pending record alone.
405
- const pendingRestoration = yield* readPendingClosureRestorationFailures;
423
+ const pendingRestoration = yield* pendingClosureRestorations;
406
424
  if (Option.isSome(pendingRestoration) && pendingRestoration.value.failures.length > 0) {
407
425
  const pending = pendingRestoration.value;
408
426
  const first = pending.failures[0];
@@ -433,7 +451,7 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
433
451
  return { ...result, candidateId: candidate.id };
434
452
  }
435
453
  const currentOutcomes = (yield* Effect.forEach(operations, (operation) => {
436
- const override = outcomesOverrideFor(operation.extensionType);
454
+ const override = provider.byExtensionType[operation.extensionType];
437
455
  return operation.plannedState === "enabled" && override !== undefined
438
456
  ? override("current").pipe(Effect.map((outcomes) => outcomes.filter(({ name }) => name === operation.name)), Effect.mapError(configuredAgentOutcomesUnavailableToStepFailure))
439
457
  : operation.plannedState === "enabled"
@@ -482,11 +500,11 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
482
500
  (value.error._tag === "StepFailure" || value.error._tag === "StaleExecutionCandidate");
483
501
  const guardedApply = (candidatePlan.executionCapabilities?.rollback === "non-rollbackable"
484
502
  ? applyCandidate
485
- : ws.runTransaction({
503
+ : runWorkspaceTransaction({
486
504
  targets: [],
487
505
  transition: applyCandidate,
488
506
  validate: () => Effect.void,
489
- onRestorationStarted: enterPhase("restoration"),
507
+ onRestorationStarted: enterPhase("restoration").pipe(Effect.provideService(OperationJournal, journal)),
490
508
  // Closures protect the shared settings and lockfile at their own
491
509
  // first touch; claiming them here would let a late failure tear an
492
510
  // earlier closure's settled commit out of the shared files.
@@ -514,7 +532,7 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
514
532
  const transitionSubject = "workspace-transition";
515
533
  const applyResult = yield* Effect.scoped(Effect.gen(function* () {
516
534
  const waited = yield* Ref.make(false);
517
- const contention = yield* ws.acquireTransition({
535
+ const contention = yield* acquireWorkspaceTransition({
518
536
  command: applyExecution.approvalRecovery.command.join(" "),
519
537
  candidateId: candidate.id,
520
538
  // Contention is a first-class lifecycle fact: observers render the
@@ -7,7 +7,8 @@
7
7
  *
8
8
  * @experimental This API is unstable and may change without notice.
9
9
  */
10
- import type { ConfiguredAgentOutcomesUnavailable, WorkspaceRestorationIncomplete, WorkspaceStateReadFailure, WorkspaceTransactionFailure } from "@agentxm/workspace-state";
10
+ import type { ConfiguredAgentOutcomesUnavailable, WorkspaceStateReadFailure } from "@agentxm/workspace-state";
11
+ import type { WorkspaceRestorationIncomplete, WorkspaceTransactionFailure } from "@agentxm/workspace-transactions";
11
12
  import { StepFailure, type CandidateFingerprintFailed } from "./errors.js";
12
13
  /** Translate a scoped settings- or lockfile-read failure, naming the fix. */
13
14
  export declare const workspaceStateReadFailureToStepFailure: (error: WorkspaceStateReadFailure) => StepFailure;
@@ -7,7 +7,28 @@
7
7
  * @experimental This API is unstable and may change without notice.
8
8
  * @packageDocumentation
9
9
  */
10
+ import * as Layer from "effect/Layer";
11
+ import { ConfiguredAgentOutcomesProvider } from "@agentxm/workspace-state";
12
+ import { FootprintRecorder } from "@agentxm/workspace-transactions";
13
+ import { OperationJournal } from "./plan/operation-journal.js";
14
+ /**
15
+ * An empty journal for one test invocation, standing in for the
16
+ * per-invocation journal the CLI's operation lifecycle creates around every
17
+ * command. Provide it wherever a test drives a plan directly instead of
18
+ * through that lifecycle. A test that asserts on what was recorded should
19
+ * build the service itself with `makeOperationJournal` and keep the ref.
20
+ */
21
+ export declare const OperationJournalTest: Layer.Layer<OperationJournal>;
22
+ /** A fresh footprint recorder for one test invocation. */
23
+ export declare const FootprintRecorderTest: Layer.Layer<FootprintRecorder>;
24
+ /**
25
+ * Every per-invocation service `resolveExecutionCandidate` acquires that the CLI's
26
+ * operation lifecycle opens around a command: the journal, the footprint
27
+ * recorder, and a configured-agent outcome provider with no per-type
28
+ * refinement. A test that drives a plan directly needs all of them, so this
29
+ * is the layer to reach for rather than assembling the three by hand.
30
+ */
31
+ export declare const PlanInvocationTest: Layer.Layer<OperationJournal | FootprintRecorder | ConfiguredAgentOutcomesProvider>;
10
32
  export { ResolvePlanInteractionTest, type ResolvePlanInteractionTestState, } from "./plan/resolve-plan-interaction.js";
11
33
  export { interactiveOnlyPlanExecution, preapprovedPlanExecution, promptablePlanExecution, } from "./plan/plan-execution-fixtures.js";
12
- export { makeMemoryTransitionLockWorld, type MemoryTransitionLockWorld, } from "./operations/memory-transition-lock.js";
13
34
  //# sourceMappingURL=testing.d.ts.map