swallowkit 2.0.0-alpha.5 → 2.0.0-alpha.6

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 (64) hide show
  1. package/dist/cli/commands/provision.d.ts +2 -1
  2. package/dist/cli/commands/provision.d.ts.map +1 -1
  3. package/dist/cli/commands/provision.js +66 -4
  4. package/dist/cli/commands/provision.js.map +1 -1
  5. package/dist/cli/index.d.ts.map +1 -1
  6. package/dist/cli/index.js +13 -0
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/core/operations/provision-deployment.d.ts +45 -0
  9. package/dist/core/operations/provision-deployment.d.ts.map +1 -0
  10. package/dist/core/operations/provision-deployment.js +151 -0
  11. package/dist/core/operations/provision-deployment.js.map +1 -0
  12. package/dist/core/operations/provision-operations.d.ts +39 -5
  13. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  14. package/dist/core/operations/provision-operations.js +609 -103
  15. package/dist/core/operations/provision-operations.js.map +1 -1
  16. package/dist/core/project/capabilities.d.ts +8 -0
  17. package/dist/core/project/capabilities.d.ts.map +1 -1
  18. package/dist/core/project/capabilities.js +11 -3
  19. package/dist/core/project/capabilities.js.map +1 -1
  20. package/dist/core/project/state.d.ts +7 -0
  21. package/dist/core/project/state.d.ts.map +1 -1
  22. package/dist/core/project/state.js +86 -0
  23. package/dist/core/project/state.js.map +1 -1
  24. package/dist/core/project/workflows.d.ts.map +1 -1
  25. package/dist/core/project/workflows.js +28 -2
  26. package/dist/core/project/workflows.js.map +1 -1
  27. package/dist/machine/index.d.ts.map +1 -1
  28. package/dist/machine/index.js +17 -11
  29. package/dist/machine/index.js.map +1 -1
  30. package/dist/mcp/index.d.ts +2 -2
  31. package/dist/mcp/index.d.ts.map +1 -1
  32. package/dist/mcp/index.js +58 -12
  33. package/dist/mcp/index.js.map +1 -1
  34. package/dist/utils/azure-cli.d.ts +16 -4
  35. package/dist/utils/azure-cli.d.ts.map +1 -1
  36. package/dist/utils/azure-cli.js +166 -4
  37. package/dist/utils/azure-cli.js.map +1 -1
  38. package/dist/utils/azure-diagnostics.d.ts +16 -0
  39. package/dist/utils/azure-diagnostics.d.ts.map +1 -0
  40. package/dist/utils/azure-diagnostics.js +68 -0
  41. package/dist/utils/azure-diagnostics.js.map +1 -0
  42. package/package.json +1 -1
  43. package/src/__tests__/agent-instructions.test.ts +3 -0
  44. package/src/__tests__/agent-loop-phase34.test.ts +19 -8
  45. package/src/__tests__/azure-cli.test.ts +5 -2
  46. package/src/__tests__/fixtures/az wrapper.cmd +2 -0
  47. package/src/__tests__/fixtures/azure-cli.cjs +130 -0
  48. package/src/__tests__/fixtures/provision-runner.cjs +28 -0
  49. package/src/__tests__/mcp.test.ts +56 -0
  50. package/src/__tests__/provision-runtime.test.ts +562 -0
  51. package/src/cli/commands/provision.ts +69 -5
  52. package/src/cli/index.ts +10 -0
  53. package/src/core/operations/provision-deployment.ts +167 -0
  54. package/src/core/operations/provision-operations.ts +597 -145
  55. package/src/core/project/agent-templates/AGENTS.md +18 -0
  56. package/src/core/project/agent-templates/CLAUDE.md +5 -0
  57. package/src/core/project/agent-templates/copilot-instructions.md +6 -0
  58. package/src/core/project/capabilities.ts +19 -3
  59. package/src/core/project/state.ts +70 -0
  60. package/src/core/project/workflows.ts +28 -2
  61. package/src/machine/index.ts +20 -14
  62. package/src/mcp/index.ts +50 -15
  63. package/src/utils/azure-cli.ts +167 -6
  64. package/src/utils/azure-diagnostics.ts +75 -0
@@ -401,6 +401,24 @@ Use the `swallowkit-provision` Agent Skill or runbook and never bypass its
401
401
  approval gate. A broad request to deploy does not itself grant the explicit
402
402
  approval required by that workflow.
403
403
 
404
+ Provision applies Bicep once and tracks the original Azure deployment with a
405
+ bounded wait. A local timeout/error, output truncation, or Ctrl+C is not proof
406
+ that Azure failed. `in-progress` means Azure is Running/Accepted; `unknown`
407
+ means its result could not be read; `partial` means Azure succeeded but
408
+ tag/ledger completion is pending. Never re-submit Bicep, reconcile, adopt,
409
+ cancel, or delete just to repair waiting or migration bookkeeping.
410
+
411
+ Retain `.swallowkit/state/provision/` execution checkpoints. Use
412
+ `{{runCmd}} swallowkit machine inspect provision --plan <runId> --wait-seconds 300`
413
+ for read-only status/wait, and after explicit human approval use
414
+ `{{runCmd}} swallowkit machine apply provision --plan <runId> --approve`
415
+ to resume. `runId` equals the original `planId`; fingerprints, subscription,
416
+ deployment identity and migration checks remain in force. Only
417
+ `status: "complete"` / `outcome: "succeeded"` means Azure Succeeded plus verified
418
+ tags and local ledger. Do not print deployment output secrets or raw `az --debug`.
419
+ For older untracked baselines use `plan provision --resume-deployment <name>`
420
+ with original target/regions; it blocks unless ARM/parameter evidence matches.
421
+
404
422
  ## SwallowKit Agent Skills and Runbooks
405
423
 
406
424
  SwallowKit installs task-specific Agent Skills under:
@@ -24,6 +24,11 @@ Always-on rules:
24
24
  after apply).
25
25
  - Use plan/apply for deterministic generation.
26
26
  - Respect all `requires-human` states and provisioning approval gates.
27
+ - Provision timeouts/Ctrl+C do not prove Azure failed. Inspect the original
28
+ execution checkpoint with `machine inspect provision --plan <runId>`; resume
29
+ `machine apply provision --plan <runId> --approve` only after explicit human
30
+ approval. Preserve checkpoints; never re-submit Bicep/reconcile/cancel/delete
31
+ to repair waiting or tag/ledger failures. Do not print deployment output secrets.
27
32
  - Run SwallowKit verification before considering implementation complete.
28
33
  - Default verification includes `backend-build` for standalone Functions.
29
34
  Skipped toolchains are not validation; Python compilation checks syntax only.
@@ -23,6 +23,12 @@ Key rules:
23
23
  after apply).
24
24
  - Use plan/apply for deterministic generation.
25
25
  - Respect `requires-human` states and other human approval gates.
26
+ - Provision timeouts/Ctrl+C do not prove Azure failed. Read `status`, `outcome`,
27
+ and `nextActions`; inspect the original run with `machine inspect provision
28
+ --plan <runId>`, then resume `machine apply provision --plan <runId> --approve`
29
+ only after explicit human approval. Preserve execution checkpoints; never
30
+ re-submit Bicep or use reconcile/cancel/delete to repair waiting or tag/ledger
31
+ failures. Do not print deployment output secrets.
26
32
  - Run SwallowKit verification before completion.
27
33
  - Default verification includes `backend-build` for standalone Functions.
28
34
  Skipped toolchains are not validation; Python compilation checks syntax only.
@@ -61,6 +61,14 @@ export interface CapabilitiesContract {
61
61
  notes: string[];
62
62
  };
63
63
  machineCommands: string[];
64
+ provisioning: {
65
+ execution: string;
66
+ completion: string;
67
+ tracking: string;
68
+ recovery: string;
69
+ diagnostics: string;
70
+ approval: string;
71
+ };
64
72
  verifyChecks: {
65
73
  defaultChecks: string[];
66
74
  optInChecks: string[];
@@ -214,16 +222,24 @@ export function inspectCapabilities(): CapabilitiesContract {
214
222
  notes: ["Editing dev-seeds/*.json alone does nothing; seeds are only applied via dev --seed-env."],
215
223
  },
216
224
  machineCommands: [
217
- "inspect project | entities | routes | artifacts | boundaries | infra | drift | capabilities",
225
+ "inspect project | entities | routes | artifacts | boundaries | infra | drift | capabilities; inspect provision --plan <runId> [--wait-seconds <0-3600>]",
218
226
  "validate project",
219
227
  "generate model <names...> | generate scaffold <model>",
220
- "plan scaffold <models...> [--api-only] [--skip-infra] | plan auth --provider <p> [--scheme <name>] [--allowed-providers <csv>] | plan provision -g <rg> --location <loc> --swa-location <loc> [--adopt-existing --baseline 0 --what-if] [--reconcile --what-if]",
221
- "apply scaffold [models...] --plan <id> [--approve] [--skip-infra] | apply auth --plan <id> [--approve] | apply provision --plan <id> --approve",
228
+ "plan scaffold <models...> [--api-only] [--skip-infra] | plan auth --provider <p> [--scheme <name>] [--allowed-providers <csv>] | plan provision -g <rg> --location <loc> --swa-location <loc> [--adopt-existing --baseline 0 --what-if] [--reconcile --what-if] [--resume-deployment <name>]",
229
+ "apply scaffold [models...] --plan <id> [--approve] [--skip-infra] | apply auth --plan <id> [--approve] | apply provision --plan <id> --approve [--wait-seconds <0-3600>] (also resumes the same execution checkpoint)",
222
230
  "verify project [--checks <ids>] [--compact]",
223
231
  "explain failure [--check <id>]",
224
232
  "(regular CLI) migrations add-model <model> [--dry-run] — generate only the container migration after scaffold --skip-infra",
225
233
  "(regular CLI) configure-mcp [--version auto|latest|<semver>] — re-select the MCP launcher version in .mcp.json",
226
234
  ],
235
+ provisioning: {
236
+ execution: "Submit Bicep once with az deployment group create --no-wait, then bounded state polling (default 300 seconds, configurable 0-3600). Azure CLI invocations are bounded separately. Warning-only stderr with exit 0 is not a failure; output capture limits do not terminate az.",
237
+ completion: "status complete/outcome succeeded requires Azure Succeeded plus verified migration tags and local ledger completion. Running/Accepted -> in-progress; unreadable/not-found state -> blocked/outcome unknown; bookkeeping interruption after Azure Succeeded -> blocked/outcome partial; Failed/Canceled -> failed with Azure causes.",
238
+ tracking: "Approved runs persist before Azure writes under .swallowkit/state/provision/. runId equals the original planId and preserves the subscription, RG, deployment ID/correlation, fingerprints and migration state. inspect provision is read-only; apply provision --plan resumes without re-submitting attempted deployments. Ctrl+C/termination may leave Azure running; never cancel/delete without explicit consent.",
239
+ recovery: "For an older untracked baseline, plan provision --resume-deployment <name> compares the existing ARM template and non-secure parameters to the local Bicep/parameters. It observes that deployment and only tags/records it after Succeeded. Mismatches, unreadable evidence, secure parameters or non-baseline migrations block; do not use reconcile just to repair bookkeeping.",
240
+ diagnostics: "Results include bounded/redacted stdout/stderr, omitted-output flags, exit code, signal, failure kind, elapsed time and deployment identity. Deployment output values are not returned. Never print --debug output, keys, tokens or connection strings.",
241
+ approval: "Initial apply and every resume require explicit human approval. A passing local command or an ok envelope is not proof of completed Azure provisioning; inspect status, outcome and nextActions.",
242
+ },
227
243
  verifyChecks: {
228
244
  defaultChecks: ["structure", "drift", "typecheck", "backend-build"],
229
245
  optInChecks: ["build", "lint", "test"],
@@ -57,6 +57,76 @@ export function deletePlanState(planId: string, projectRoot: string = process.cw
57
57
  }
58
58
  }
59
59
 
60
+ function provisionRunPath(runId: string, projectRoot: string): string {
61
+ if (!/^[a-f0-9]{12}$/.test(runId)) throw new Error("Invalid provision run ID.");
62
+ return path.join(projectRoot, SWALLOWKIT_STATE_DIR, "provision", `${runId}.json`);
63
+ }
64
+
65
+ export function saveProvisionRun<T extends { runId: string }>(run: T, projectRoot: string = process.cwd()): void {
66
+ ensureStateDir(projectRoot);
67
+ const file = provisionRunPath(run.runId, projectRoot);
68
+ fs.mkdirSync(path.dirname(file), { recursive: true });
69
+ const temporary = `${file}.tmp-${process.pid}`;
70
+ fs.writeFileSync(temporary, `${JSON.stringify(run, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
71
+ fs.renameSync(temporary, file);
72
+ }
73
+
74
+ export function loadProvisionRun<T>(runId: string, projectRoot: string = process.cwd()): T | null {
75
+ const file = provisionRunPath(runId, projectRoot);
76
+ if (!fs.existsSync(file)) return null;
77
+ // Unlike a preview, discarding a corrupt execution checkpoint could replay Azure writes.
78
+ return JSON.parse(fs.readFileSync(file, "utf-8")) as T;
79
+ }
80
+
81
+ export function listProvisionRuns<T>(projectRoot: string = process.cwd()): T[] {
82
+ const directory = path.join(projectRoot, SWALLOWKIT_STATE_DIR, "provision");
83
+ if (!fs.existsSync(directory)) return [];
84
+ return fs.readdirSync(directory).filter((name) => /^[a-f0-9]{12}\.json$/.test(name))
85
+ .map((name) => loadProvisionRun<T>(name.slice(0, -5), projectRoot)!);
86
+ }
87
+
88
+ /** A live local executor must not share an Azure target with another executor. */
89
+ export function lockProvisionTarget(key: string, projectRoot: string = process.cwd()): () => void {
90
+ if (!/^[a-f0-9]{64}$/.test(key)) throw new Error("Invalid provision lock key.");
91
+ const directory = path.join(ensureStateDir(projectRoot), "provision");
92
+ fs.mkdirSync(directory, { recursive: true });
93
+ const file = path.join(directory, `${key}.lock`);
94
+ const create = () => {
95
+ const descriptor = fs.openSync(file, "wx", 0o600);
96
+ try { fs.writeFileSync(descriptor, String(process.pid)); } finally { fs.closeSync(descriptor); }
97
+ };
98
+ try {
99
+ create();
100
+ } catch (error) {
101
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
102
+ const recoveryFile = `${file}.recovery`;
103
+ let descriptor: number;
104
+ try {
105
+ descriptor = fs.openSync(recoveryFile, "wx", 0o600);
106
+ } catch (recoveryError) {
107
+ if ((recoveryError as NodeJS.ErrnoException).code !== "EEXIST") throw recoveryError;
108
+ throw new Error("Provision lock recovery is already in progress. Inspect the checkpoint before retrying.");
109
+ }
110
+ fs.closeSync(descriptor);
111
+ try {
112
+ const pid = Number(fs.readFileSync(file, "utf-8"));
113
+ if (!Number.isInteger(pid) || pid < 1) throw new Error("Invalid provision lock; inspect the checkpoint before removing the lock.");
114
+ try {
115
+ process.kill(pid, 0);
116
+ } catch (probeError) {
117
+ if ((probeError as NodeJS.ErrnoException).code !== "ESRCH") throw probeError;
118
+ fs.unlinkSync(file);
119
+ create();
120
+ return () => fs.unlinkSync(file);
121
+ }
122
+ throw new Error("Another local provision executor is using this Azure target. Inspect its run; do not start a second deployment.");
123
+ } finally {
124
+ fs.unlinkSync(recoveryFile);
125
+ }
126
+ }
127
+ return () => fs.unlinkSync(file);
128
+ }
129
+
60
130
  const LAST_VERIFY_FILE = "last-verify.json";
61
131
 
62
132
  export function saveLastVerifyState(result: unknown, projectRoot: string = process.cwd()): void {
@@ -214,6 +214,9 @@ Goal: bootstrap Azure once, then apply only pending versioned infrastructure mig
214
214
  - Inspect infra locally first — no \`az\` calls are needed for inspection.
215
215
  - Never edit an applied migration. Create a corrective migration instead.
216
216
  - Never use \`--reconcile\` for an ordinary model or resource addition.
217
+ - A local az error/timeout or Ctrl+C does not prove Azure failed. Never
218
+ re-submit, cancel, or delete a deployment to repair a wait or bookkeeping failure.
219
+ - Respect \`status\`, \`outcome\`, and \`nextActions\`, not just \`ok\`.
217
220
 
218
221
  ## Steps
219
222
 
@@ -221,7 +224,7 @@ Goal: bootstrap Azure once, then apply only pending versioned infrastructure mig
221
224
  \`${runCmd} swallowkit machine inspect infra\`
222
225
  - review parameters, modules, outputs, and container wiring warnings.
223
226
  2. Compute the provisioning plan (no resources are created):
224
- \`${runCmd} swallowkit machine plan provision -g <resource-group> --location <region>\`
227
+ \`${runCmd} swallowkit machine plan provision -g <resource-group> --location <region> --swa-location <swa-region>\`
225
228
  - the plan reports \`bootstrap\`, \`migrate\`, \`noop\`, or \`adoption-required\`.
226
229
  - it lists only pending migrations and the exact \`az\` commands that would run.
227
230
  - for an older untagged environment, first run \`${runCmd} swallowkit migrations init --legacy-baseline\`, then plan with \`--adopt-existing --baseline 0 --what-if\`.
@@ -229,12 +232,35 @@ Goal: bootstrap Azure once, then apply only pending versioned infrastructure mig
229
232
  3. Present the plan to the human and wait for explicit approval.
230
233
  4. Only after approval:
231
234
  \`${runCmd} swallowkit machine apply provision --plan <planId> --approve\`
232
- 5. Verify the deployment outputs and report them.
235
+ 5. If \`in-progress\`, \`unknown\`, or \`partial\`, retain the execution
236
+ checkpoint and read/wait for the original deployment:
237
+ \`${runCmd} swallowkit machine inspect provision --plan <runId> --wait-seconds 300\`.
238
+ Inspection never writes Azure tags or the local ledger.
239
+ 6. After fresh explicit human approval, resume:
240
+ \`${runCmd} swallowkit machine apply provision --plan <runId> --approve\`.
241
+ Attempted deployments are not re-submitted; remaining reviewed steps and
242
+ tags/ledger finish only after Azure \`Succeeded\`.
243
+ 7. Report completion only when \`status: "complete"\` and
244
+ \`outcome: "succeeded"\`. Never report deployment output secrets.
245
+
246
+ For an older untracked bootstrap, plan with \`--resume-deployment <name>\`
247
+ and the original RG, subscription and regions. Recovery compares its ARM
248
+ template and non-secure parameters to local Bicep before approval. Mismatch,
249
+ unreadable evidence or secure parameters block; do not work around this by
250
+ using reconcile/adoption or replacing the migration manifest.
233
251
 
234
252
  ## Terminal states
235
253
 
236
254
  \`apply provision\` without \`--approve\` fails with \`approval-required\` —
237
255
  this is the guardrail working as intended, not an error to work around.
256
+
257
+ Azure \`Running\`/\`Accepted\` means \`in-progress\`. An unreadable or missing
258
+ deployment means \`unknown\`, not Azure failure. Azure \`Failed\`/\`Canceled\`
259
+ returns sanitized Azure and operation causes. Azure \`Succeeded\` with incomplete
260
+ tag/ledger writes means \`partial\`; resume bookkeeping without redeploying.
261
+ The default wait is 300 seconds (0-3600 via \`--wait-seconds\`), not unlimited.
262
+ Warning-only stderr with exit 0 is accepted. Diagnostics report local exit,
263
+ signal, elapsed time and truncated/redacted output separately from Azure state.
238
264
  `,
239
265
  };
240
266
 
@@ -3,7 +3,7 @@ import { isSwallowKitProject } from "../core/config";
3
3
  import { createModelOperation } from "../core/operations/create-model";
4
4
  import { applyScaffoldOperation } from "../core/operations/scaffold-apply";
5
5
  import { applyAuthOperation, planAuthOperation } from "../core/operations/auth-operations";
6
- import { applyProvisionOperation, planProvisionOperation } from "../core/operations/provision-operations";
6
+ import { applyProvisionOperation, inspectProvisionOperation, planProvisionOperation } from "../core/operations/provision-operations";
7
7
  import { ProcessExitInterceptError } from "../core/operations/runtime";
8
8
  import { inspectInfra } from "../core/project/infra";
9
9
  import { runMachineScaffoldOperation } from "../core/operations/scaffold-machine";
@@ -141,6 +141,17 @@ function createMachineProgram(): Command {
141
141
  .showHelpAfterError(false);
142
142
 
143
143
  const inspect = new Command("inspect");
144
+ inspect
145
+ .command("provision")
146
+ .description("Read/wait for an original provision deployment without writes or replay")
147
+ .requiredOption("--plan <runId>", "Original provision plan/execution checkpoint ID")
148
+ .option("--wait-seconds <seconds>", "Bounded read-only wait, 0-3600 seconds (default: 0)", Number)
149
+ .action(async (options: { plan: string; waitSeconds?: number }) => {
150
+ await handleMachineAction("inspect-provision", async () => {
151
+ ensureProjectForMachine();
152
+ return inspectProvisionOperation({ planId: options.plan, waitSeconds: options.waitSeconds });
153
+ }, (result) => ({ status: result.status, nextActions: result.nextActions }));
154
+ });
144
155
  inspect
145
156
  .command("project")
146
157
  .description("Inspect SwallowKit project metadata")
@@ -373,7 +384,7 @@ function createMachineProgram(): Command {
373
384
 
374
385
  plan
375
386
  .command("provision")
376
- .description("Preflight Azure provisioning locally; use --what-if for an az what-if analysis (requires az login)")
387
+ .description("Review local infrastructure and current Azure state without writes (requires az login)")
377
388
  .requiredOption("-g, --resource-group <name>", "Resource group name")
378
389
  .requiredOption("--location <region>", "Primary location for Functions and Cosmos DB")
379
390
  .requiredOption("--swa-location <region>", "Static Web App location")
@@ -382,7 +393,8 @@ function createMachineProgram(): Command {
382
393
  .option("--adopt-existing", "Adopt an existing untagged resource group without deploying main.bicep", false)
383
394
  .option("--baseline <version>", "Baseline version for adoption", (value) => Number(value))
384
395
  .option("--reconcile", "Explicitly redeploy infra/main.bicep", false)
385
- .action(async (options: { resourceGroup: string; location: string; swaLocation: string; subscription?: string; whatIf?: boolean; adoptExisting?: boolean; baseline?: number; reconcile?: boolean }) => {
396
+ .option("--resume-deployment <name>", "Recover a matching older baseline deployment without re-submitting Bicep")
397
+ .action(async (options: { resourceGroup: string; location: string; swaLocation: string; subscription?: string; whatIf?: boolean; adoptExisting?: boolean; baseline?: number; reconcile?: boolean; resumeDeployment?: string }) => {
386
398
  await handleMachineAction(
387
399
  "plan-provision",
388
400
  async () => {
@@ -396,6 +408,7 @@ function createMachineProgram(): Command {
396
408
  adoptExisting: options.adoptExisting,
397
409
  baseline: options.baseline,
398
410
  reconcile: options.reconcile,
411
+ resumeDeployment: options.resumeDeployment,
399
412
  });
400
413
  },
401
414
  (planData) => ({
@@ -546,22 +559,15 @@ function createMachineProgram(): Command {
546
559
  .description("Apply an approved provisioning plan (always requires --approve; creates billable Azure resources)")
547
560
  .requiredOption("--plan <planId>", "Provision plan to apply")
548
561
  .option("--approve", "Confirm human approval of the provisioning plan", false)
549
- .action(async (options: { plan: string; approve?: boolean }) => {
562
+ .option("--wait-seconds <seconds>", "Bounded state wait, 0-3600 seconds (default: 300)", Number)
563
+ .action(async (options: { plan: string; approve?: boolean; waitSeconds?: number }) => {
550
564
  await handleMachineAction(
551
565
  "apply-provision",
552
566
  async () => {
553
567
  ensureProjectForMachine();
554
- return applyProvisionOperation({ planId: options.plan, approve: options.approve });
568
+ return applyProvisionOperation({ planId: options.plan, approve: options.approve, waitSeconds: options.waitSeconds });
555
569
  },
556
- () => ({
557
- status: "complete",
558
- nextActions: [
559
- {
560
- command: "swallowkit machine inspect infra",
561
- description: "Re-inspect infra assets after provisioning.",
562
- },
563
- ],
564
- })
570
+ (result) => ({ status: result.status, nextActions: result.nextActions })
565
571
  );
566
572
  });
567
573
 
package/src/mcp/index.ts CHANGED
@@ -8,6 +8,7 @@ import * as z from "zod/v4";
8
8
  import type { MachineErrorResponse, MachineNextAction, MachineOperationStatus, MachineResponse } from "../machine/contracts";
9
9
  import { getSwallowKitVersion } from "../version";
10
10
  import { detectFromProject, getCommands } from "../utils/package-manager";
11
+ import { provisionNextActions } from "../core/operations/provision-operations";
11
12
  import {
12
13
  MCP_DELEGATED_FROM_ENV,
13
14
  McpVersionContext,
@@ -24,7 +25,7 @@ interface MachineSuccessPayload<TData> {
24
25
  data: TData;
25
26
  }
26
27
 
27
- type MachineCliRunner = (args: string[]) => Promise<{ stdout: string; stderr: string; exitCode: number }>;
28
+ type MachineCliRunner = (args: string[], signal?: AbortSignal) => Promise<{ stdout: string; stderr: string; exitCode: number }>;
28
29
  type ToolContentResult = { content: Array<{ type: "text"; text: string }>; isError?: boolean };
29
30
  class MachineCliError extends Error {
30
31
  constructor(message: string, readonly response: MachineErrorResponse) {
@@ -35,7 +36,7 @@ type ToolDefinition = {
35
36
  name: string;
36
37
  description: string;
37
38
  inputSchema: z.ZodTypeAny;
38
- handler: (input: any) => Promise<ToolContentResult>;
39
+ handler: (input: any, signal?: AbortSignal) => Promise<ToolContentResult>;
39
40
  };
40
41
  type ExecaModule = typeof import("execa");
41
42
 
@@ -48,29 +49,45 @@ function resolveMachineCliEntrypoint(): string {
48
49
  return path.resolve(__dirname, "..", "cli", "index.js");
49
50
  }
50
51
 
51
- async function defaultMachineCliRunner(args: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> {
52
+ async function defaultMachineCliRunner(args: string[], signal?: AbortSignal): Promise<{ stdout: string; stderr: string; exitCode: number }> {
52
53
  const { execa } = await importModule("execa");
53
- const result = await execa(process.execPath, [resolveMachineCliEntrypoint(), "machine", ...args], {
54
+ const child = execa(process.execPath, [resolveMachineCliEntrypoint(), "machine", ...args], {
54
55
  reject: false,
55
56
  });
56
-
57
+ const abort = () => child.kill("SIGTERM", { forceKillAfterTimeout: 6_000 });
58
+ signal?.addEventListener("abort", abort, { once: true });
59
+ if (signal?.aborted) abort();
60
+ let result: Awaited<typeof child>;
61
+ try { result = await child; } finally { signal?.removeEventListener("abort", abort); }
57
62
  return {
58
63
  stdout: result.stdout,
59
64
  stderr: result.stderr,
60
- exitCode: result.exitCode ?? 0,
65
+ exitCode: result.exitCode ?? 1,
61
66
  };
62
67
  }
63
68
 
64
69
  async function executeMachineCommand<TData>(
65
70
  args: string[],
66
- runMachineCli: MachineCliRunner
71
+ runMachineCli: MachineCliRunner,
72
+ signal?: AbortSignal
67
73
  ): Promise<MachineSuccessPayload<TData>> {
68
- const result = await runMachineCli(args);
74
+ const interrupted = () => {
75
+ const runId = args[args.indexOf("--plan") + 1];
76
+ const response: MachineErrorResponse = {
77
+ ok: false, command: args.slice(0, 2).join("-"), status: "blocked",
78
+ error: { code: "provision-interrupted", message: "The local MCP wait was interrupted. Azure may continue; inspect the original checkpoint. No Azure cancellation or successful result is implied.", details: { runId, outcome: "unknown", localFailure: "cancelled" } },
79
+ nextActions: /^[a-f0-9]{12}$/.test(runId) ? provisionNextActions(runId) : [],
80
+ };
81
+ return new MachineCliError(response.error.message, response);
82
+ };
83
+ if (signal?.aborted) throw interrupted();
84
+ const result = await (signal ? runMachineCli(args, signal) : runMachineCli(args));
69
85
 
70
86
  let parsed: MachineResponse<TData>;
71
87
  try {
72
88
  parsed = JSON.parse(result.stdout) as MachineResponse<TData>;
73
89
  } catch {
90
+ if (signal?.aborted) throw interrupted();
74
91
  throw new Error(result.stderr || result.stdout || "Machine CLI returned invalid JSON.");
75
92
  }
76
93
 
@@ -423,14 +440,16 @@ export function buildSwallowKitToolDefinitions(
423
440
  adoptExisting: z.boolean().optional(),
424
441
  baseline: z.number().int().nonnegative().optional(),
425
442
  reconcile: z.boolean().optional(),
443
+ resumeDeployment: z.string().optional(),
426
444
  }),
427
- handler: async ({ resourceGroup, location, swaLocation, subscription, whatIf, adoptExisting, baseline, reconcile }: { resourceGroup: string; location: string; swaLocation: string; subscription?: string; whatIf?: boolean; adoptExisting?: boolean; baseline?: number; reconcile?: boolean }) => {
445
+ handler: async ({ resourceGroup, location, swaLocation, subscription, whatIf, adoptExisting, baseline, reconcile, resumeDeployment }: { resourceGroup: string; location: string; swaLocation: string; subscription?: string; whatIf?: boolean; adoptExisting?: boolean; baseline?: number; reconcile?: boolean; resumeDeployment?: string }) => {
428
446
  const args = ["plan", "provision", "--resource-group", resourceGroup, "--location", location, "--swa-location", swaLocation];
429
447
  if (subscription) args.push("--subscription", subscription);
430
448
  if (whatIf) args.push("--what-if");
431
449
  if (adoptExisting) args.push("--adopt-existing");
432
450
  if (baseline !== undefined) args.push("--baseline", String(baseline));
433
451
  if (reconcile) args.push("--reconcile");
452
+ if (resumeDeployment) args.push("--resume-deployment", resumeDeployment);
434
453
 
435
454
  const response = await executeMachineCommand(args, runMachineCli);
436
455
  return jsonTextContent(withOperationStatus(response));
@@ -438,16 +457,32 @@ export function buildSwallowKitToolDefinitions(
438
457
  },
439
458
  {
440
459
  name: "swallowkit_apply_provision",
441
- description: "Apply an approved provisioning plan. Creates billable Azure resources; approve=true is always required and must reflect explicit human consent.",
460
+ description: "Apply or resume an approved provision checkpoint with bounded state polling. Never resubmit an attempted deployment; finish tags/ledger only after Azure Succeeded. Check status/nextActions, not just ok. approve=true always requires explicit human consent.",
442
461
  inputSchema: z.object({
443
462
  planId: z.string(),
444
463
  approve: z.boolean().optional(),
464
+ waitSeconds: z.number().int().min(0).max(3600).optional(),
445
465
  }),
446
- handler: async ({ planId, approve }: { planId: string; approve?: boolean }) => {
466
+ handler: async ({ planId, approve, waitSeconds }: { planId: string; approve?: boolean; waitSeconds?: number }, signal?: AbortSignal) => {
447
467
  const args = ["apply", "provision", "--plan", planId];
448
468
  if (approve) args.push("--approve");
469
+ if (waitSeconds !== undefined) args.push("--wait-seconds", String(waitSeconds));
449
470
 
450
- const response = await executeMachineCommand(args, runMachineCli);
471
+ const response = await executeMachineCommand(args, runMachineCli, signal);
472
+ return jsonTextContent(withOperationStatus(response));
473
+ },
474
+ },
475
+ {
476
+ name: "swallowkit_inspect_provision",
477
+ description: "Read or bounded-wait for the original provision deployment. No create, cancel, delete, tag, or ledger writes. Azure Succeeded alone is partial until the original approved checkpoint finishes bookkeeping.",
478
+ inputSchema: z.object({
479
+ planId: z.string(),
480
+ waitSeconds: z.number().int().min(0).max(3600).optional(),
481
+ }),
482
+ handler: async ({ planId, waitSeconds }: { planId: string; waitSeconds?: number }, signal?: AbortSignal) => {
483
+ const args = ["inspect", "provision", "--plan", planId];
484
+ if (waitSeconds !== undefined) args.push("--wait-seconds", String(waitSeconds));
485
+ const response = await executeMachineCommand(args, runMachineCli, signal);
451
486
  return jsonTextContent(withOperationStatus(response));
452
487
  },
453
488
  },
@@ -456,9 +491,9 @@ export function buildSwallowKitToolDefinitions(
456
491
  // Every response reports which SwallowKit version answered and why it was selected.
457
492
  return definitions.map((definition) => ({
458
493
  ...definition,
459
- handler: async (input: unknown) => {
494
+ handler: async (input: unknown, signal?: AbortSignal) => {
460
495
  try {
461
- const result = await definition.handler(input);
496
+ const result = await definition.handler(input, signal);
462
497
  return jsonTextContent(withSwallowKitMetadata(JSON.parse(result.content[0].text)));
463
498
  } catch (error) {
464
499
  // Prerequisite failures carry nextActions; keep them structured so agents can follow them.
@@ -488,7 +523,7 @@ export function createSwallowKitMcpServer(
488
523
  description: tool.description,
489
524
  inputSchema: tool.inputSchema,
490
525
  },
491
- tool.handler
526
+ (input, extra) => tool.handler(input, extra.signal)
492
527
  );
493
528
  }
494
529