swallowkit 2.0.0-alpha.4 → 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 (113) hide show
  1. package/dist/cli/commands/add-auth.js +37 -6
  2. package/dist/cli/commands/add-auth.js.map +1 -1
  3. package/dist/cli/commands/init.d.ts +2 -0
  4. package/dist/cli/commands/init.d.ts.map +1 -1
  5. package/dist/cli/commands/init.js +3 -1
  6. package/dist/cli/commands/init.js.map +1 -1
  7. package/dist/cli/commands/provision.d.ts +2 -1
  8. package/dist/cli/commands/provision.d.ts.map +1 -1
  9. package/dist/cli/commands/provision.js +66 -4
  10. package/dist/cli/commands/provision.js.map +1 -1
  11. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  12. package/dist/cli/commands/scaffold.js +45 -1
  13. package/dist/cli/commands/scaffold.js.map +1 -1
  14. package/dist/cli/index.d.ts.map +1 -1
  15. package/dist/cli/index.js +15 -2
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/core/operations/create-model.d.ts +2 -0
  18. package/dist/core/operations/create-model.d.ts.map +1 -1
  19. package/dist/core/operations/create-model.js +60 -8
  20. package/dist/core/operations/create-model.js.map +1 -1
  21. package/dist/core/operations/provision-deployment.d.ts +45 -0
  22. package/dist/core/operations/provision-deployment.d.ts.map +1 -0
  23. package/dist/core/operations/provision-deployment.js +151 -0
  24. package/dist/core/operations/provision-deployment.js.map +1 -0
  25. package/dist/core/operations/provision-operations.d.ts +39 -5
  26. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  27. package/dist/core/operations/provision-operations.js +609 -103
  28. package/dist/core/operations/provision-operations.js.map +1 -1
  29. package/dist/core/project/boundaries.js +3 -3
  30. package/dist/core/project/boundaries.js.map +1 -1
  31. package/dist/core/project/capabilities.d.ts +9 -0
  32. package/dist/core/project/capabilities.d.ts.map +1 -1
  33. package/dist/core/project/capabilities.js +20 -6
  34. package/dist/core/project/capabilities.js.map +1 -1
  35. package/dist/core/project/react-project.js +1 -1
  36. package/dist/core/project/state.d.ts +7 -0
  37. package/dist/core/project/state.d.ts.map +1 -1
  38. package/dist/core/project/state.js +86 -0
  39. package/dist/core/project/state.js.map +1 -1
  40. package/dist/core/project/workflows.d.ts.map +1 -1
  41. package/dist/core/project/workflows.js +43 -7
  42. package/dist/core/project/workflows.js.map +1 -1
  43. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  44. package/dist/core/scaffold/auth-generator.js +18 -19
  45. package/dist/core/scaffold/auth-generator.js.map +1 -1
  46. package/dist/core/scaffold/native-schema-generator.d.ts +2 -0
  47. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  48. package/dist/core/scaffold/native-schema-generator.js +16 -1
  49. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  50. package/dist/core/verify/index.d.ts +4 -3
  51. package/dist/core/verify/index.d.ts.map +1 -1
  52. package/dist/core/verify/index.js +98 -4
  53. package/dist/core/verify/index.js.map +1 -1
  54. package/dist/machine/contracts.d.ts +1 -0
  55. package/dist/machine/contracts.d.ts.map +1 -1
  56. package/dist/machine/errors.d.ts +3 -2
  57. package/dist/machine/errors.d.ts.map +1 -1
  58. package/dist/machine/errors.js +2 -1
  59. package/dist/machine/errors.js.map +1 -1
  60. package/dist/machine/index.d.ts.map +1 -1
  61. package/dist/machine/index.js +20 -13
  62. package/dist/machine/index.js.map +1 -1
  63. package/dist/mcp/index.d.ts +3 -2
  64. package/dist/mcp/index.d.ts.map +1 -1
  65. package/dist/mcp/index.js +76 -15
  66. package/dist/mcp/index.js.map +1 -1
  67. package/dist/utils/azure-cli.d.ts +16 -4
  68. package/dist/utils/azure-cli.d.ts.map +1 -1
  69. package/dist/utils/azure-cli.js +166 -4
  70. package/dist/utils/azure-cli.js.map +1 -1
  71. package/dist/utils/azure-diagnostics.d.ts +16 -0
  72. package/dist/utils/azure-diagnostics.d.ts.map +1 -0
  73. package/dist/utils/azure-diagnostics.js +68 -0
  74. package/dist/utils/azure-diagnostics.js.map +1 -0
  75. package/package.json +6 -3
  76. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +6 -6
  77. package/src/__tests__/agent-instructions.test.ts +3 -0
  78. package/src/__tests__/agent-loop-phase34.test.ts +114 -8
  79. package/src/__tests__/azure-cli.test.ts +5 -2
  80. package/src/__tests__/fixtures/az wrapper.cmd +2 -0
  81. package/src/__tests__/fixtures/azure-cli.cjs +130 -0
  82. package/src/__tests__/fixtures/provision-runner.cjs +28 -0
  83. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  84. package/src/__tests__/mcp.test.ts +71 -0
  85. package/src/__tests__/multi-auth.test.ts +115 -4
  86. package/src/__tests__/plan-apply.test.ts +111 -0
  87. package/src/__tests__/provision-runtime.test.ts +562 -0
  88. package/src/cli/commands/add-auth.ts +36 -6
  89. package/src/cli/commands/init.ts +4 -4
  90. package/src/cli/commands/provision.ts +69 -5
  91. package/src/cli/commands/scaffold.ts +65 -1
  92. package/src/cli/index.ts +12 -2
  93. package/src/core/operations/create-model.ts +51 -13
  94. package/src/core/operations/provision-deployment.ts +167 -0
  95. package/src/core/operations/provision-operations.ts +597 -145
  96. package/src/core/project/agent-templates/AGENTS.md +46 -3
  97. package/src/core/project/agent-templates/CLAUDE.md +9 -0
  98. package/src/core/project/agent-templates/copilot-instructions.md +10 -0
  99. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  100. package/src/core/project/boundaries.ts +3 -3
  101. package/src/core/project/capabilities.ts +29 -6
  102. package/src/core/project/react-project.ts +1 -1
  103. package/src/core/project/state.ts +70 -0
  104. package/src/core/project/workflows.ts +43 -7
  105. package/src/core/scaffold/auth-generator.ts +18 -19
  106. package/src/core/scaffold/native-schema-generator.ts +16 -0
  107. package/src/core/verify/index.ts +95 -5
  108. package/src/machine/contracts.ts +1 -0
  109. package/src/machine/errors.ts +4 -2
  110. package/src/machine/index.ts +23 -16
  111. package/src/mcp/index.ts +68 -20
  112. package/src/utils/azure-cli.ts +167 -6
  113. package/src/utils/azure-diagnostics.ts +75 -0
@@ -5,16 +5,17 @@
5
5
  * - structure: プロジェクト構造規約 (validateProject)
6
6
  * - drift: 生成物と現在状態の乖離 (detectDrift)
7
7
  * - typecheck: TypeScript 型チェック (tsc --noEmit / typecheck script)
8
+ * - backend-build: standalone Functions のコンパイル (C#/TypeScript) または構文検証 (Python)
8
9
  * - build/lint/test: package.json scripts に同名 script がある場合のみ実行 (なければ skip)
9
10
  * - custom: swallowkit.config の verify.checks で定義されたプロジェクト固有チェック
10
11
  *
11
- * 既定実行セットは structure/drift/typecheck + custom。build/lint/test は --checks で明示指定。
12
+ * 既定実行セットは structure/drift/typecheck/backend-build + custom。build/lint/test は --checks で明示指定。
12
13
  */
13
14
 
14
15
  import * as fs from "fs";
15
16
  import * as path from "path";
16
17
  import { spawnSync } from "child_process";
17
- import { loadConfig } from "../config";
18
+ import { loadConfig, loadConfigFromEnv } from "../config";
18
19
  import { detectDrift, DriftFinding } from "../project/drift";
19
20
  import { saveLastVerifyState } from "../project/state";
20
21
  import { ProjectViolation, validateProject } from "../project/validation";
@@ -60,11 +61,11 @@ export interface VerifyResult {
60
61
  verifiedAt: string;
61
62
  }
62
63
 
63
- export const AVAILABLE_VERIFY_CHECKS = ["structure", "drift", "typecheck", "build", "lint", "test"] as const;
64
+ export const AVAILABLE_VERIFY_CHECKS = ["structure", "drift", "typecheck", "backend-build", "build", "lint", "test"] as const;
64
65
  export type VerifyCheckId = (typeof AVAILABLE_VERIFY_CHECKS)[number];
65
66
 
66
67
  /** 既定で実行される built-in チェック。build/lint/test は明示指定時のみ。 */
67
- export const DEFAULT_VERIFY_CHECKS = ["structure", "drift", "typecheck"] as const;
68
+ export const DEFAULT_VERIFY_CHECKS = ["structure", "drift", "typecheck", "backend-build"] as const;
68
69
 
69
70
  async function runStructureCheck(projectRoot: string): Promise<VerifyCheckResult> {
70
71
  const startedAt = Date.now();
@@ -137,6 +138,7 @@ interface CommandCheckSpec {
137
138
  args: string[];
138
139
  timeoutMs?: number;
139
140
  failAction: string;
141
+ shell?: boolean;
140
142
  }
141
143
 
142
144
  function runCommandCheck(projectRoot: string, spec: CommandCheckSpec): VerifyCheckResult {
@@ -145,9 +147,10 @@ function runCommandCheck(projectRoot: string, spec: CommandCheckSpec): VerifyChe
145
147
  try {
146
148
  const result = spawnSync(spec.command, spec.args, {
147
149
  cwd: projectRoot,
148
- shell: true,
150
+ shell: spec.shell ?? true,
149
151
  encoding: "utf-8",
150
152
  timeout: spec.timeoutMs ?? 5 * 60 * 1000,
153
+ maxBuffer: 10 * 1024 * 1024,
151
154
  });
152
155
 
153
156
  const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
@@ -180,6 +183,11 @@ function runCommandCheck(projectRoot: string, spec: CommandCheckSpec): VerifyChe
180
183
  command: commandLabel,
181
184
  exitCode,
182
185
  logTail: tailLines(output, 50),
186
+ ...(spec.id === "backend-build" ? {
187
+ diagnostics: [...new Set(output.split(/\r?\n/).filter((line) =>
188
+ /\(\d+,\d+\):\s*error\s+\w+/.test(line) ||
189
+ /^\s*(File ".+", line \d+|(?:Sorry: )?(?:Syntax|Indentation|Tab)Error:|\*\*\* Error compiling)/.test(line)))],
190
+ } : {}),
183
191
  },
184
192
  suggestedActions: exitCode === 0 ? [] : [spec.failAction],
185
193
  };
@@ -201,6 +209,86 @@ function runCommandCheck(projectRoot: string, spec: CommandCheckSpec): VerifyChe
201
209
  }
202
210
  }
203
211
 
212
+ function backendUnavailable(reason: string): VerifyCheckResult {
213
+ return {
214
+ id: "backend-build",
215
+ title: "Standalone Functions compilation",
216
+ status: "skip",
217
+ durationMs: 0,
218
+ fixable: false,
219
+ evidence: { logTail: [reason], diagnostics: [reason] },
220
+ suggestedActions: ["Install or restore the backend toolchain, then re-run verify with --checks \"backend-build\". A skipped check is not backend validation."],
221
+ };
222
+ }
223
+
224
+ function runBackendBuildCheck(projectRoot: string): VerifyCheckResult {
225
+ const functionsRoot = path.join(projectRoot, "functions");
226
+ if (!fs.existsSync(functionsRoot)) return backendUnavailable("functions/ not found; no standalone backend was checked.");
227
+ const configPath = ["swallowkit.config.json", "swallowkit.config.js", ".swallowkitrc.json"]
228
+ .map((name) => path.join(projectRoot, name)).find((file) => fs.existsSync(file));
229
+ if (!configPath) return backendUnavailable("SwallowKit configuration not found; backend language is unknown.");
230
+ // Same effective language as generation (getBackendLanguage): env override, then file, invalid -> typescript.
231
+ const configured = loadConfigFromEnv().backend?.language ?? loadConfig(configPath, true, true).backend?.language;
232
+ const language = configured && ["typescript", "csharp", "python"].includes(configured) ? configured : "typescript";
233
+ let command: string;
234
+ let args: string[];
235
+ if (language === "csharp") {
236
+ command = "dotnet";
237
+ args = ["build", "--nologo"];
238
+ const probe = spawnSync(command, ["--version"], { cwd: functionsRoot, encoding: "utf-8", timeout: 30_000 });
239
+ if (probe.error && "code" in probe.error && probe.error.code === "ENOENT") return backendUnavailable("dotnet SDK is not installed or is not on PATH; C# compilation was skipped.");
240
+ } else if (language === "python") {
241
+ const venvPython = process.platform === "win32"
242
+ ? path.join(functionsRoot, ".venv", "Scripts", "python.exe")
243
+ : path.join(functionsRoot, ".venv", "bin", "python");
244
+ const candidates = fs.existsSync(venvPython)
245
+ ? [{ command: venvPython, args: [] as string[] }]
246
+ : process.platform === "win32"
247
+ ? [{ command: "python", args: [] }, { command: "python3", args: [] }, { command: "py", args: ["-3"] }]
248
+ : [{ command: "python3", args: [] }, { command: "python", args: [] }];
249
+ const python = candidates.find((candidate) => {
250
+ const probe = spawnSync(candidate.command, [...candidate.args, "-c", "import sys; sys.exit(0 if sys.version_info.major == 3 else 1)"], {
251
+ cwd: functionsRoot, encoding: "utf-8", timeout: 30_000,
252
+ });
253
+ // Preserve execution errors for an installed interpreter instead of silently falling back.
254
+ return probe.status === 0 || (probe.error && (!("code" in probe.error) || probe.error.code !== "ENOENT"));
255
+ });
256
+ if (!python) return backendUnavailable("Python 3 is not available in functions/.venv or on PATH; Python syntax compilation was skipped.");
257
+ command = python.command;
258
+ // -f: never trust cached bytecode, which can survive same-size edits within one mtime second.
259
+ args = [...python.args, "-m", "compileall", "-q", "-f", "-x", "[/\\\\](?:\\.venv|\\.codegen-venv|node_modules|__pycache__)(?:[/\\\\]|$)", "."];
260
+ } else {
261
+ let tscPath: string;
262
+ try {
263
+ tscPath = require.resolve("typescript/bin/tsc", { paths: [functionsRoot] });
264
+ } catch (error) {
265
+ if (error instanceof Error && "code" in error && error.code === "MODULE_NOT_FOUND") {
266
+ return backendUnavailable("The project's TypeScript compiler is not installed; restore dependencies before verifying functions/.");
267
+ }
268
+ throw error;
269
+ }
270
+ const sharedBuild = runSharedBuildIfPresent(projectRoot);
271
+ if (sharedBuild.ran && !sharedBuild.ok) {
272
+ return {
273
+ id: "backend-build", title: "Standalone Functions compilation", status: sharedBuild.unavailable ? "error" : "fail",
274
+ durationMs: 0, fixable: !sharedBuild.unavailable,
275
+ evidence: { command: sharedBuild.command, logTail: sharedBuild.logTail,
276
+ ...(sharedBuild.unavailable ? { errorCode: "verification-command-unavailable" as const } : {}) },
277
+ suggestedActions: ["Fix the shared package build before compiling the TypeScript backend."],
278
+ };
279
+ }
280
+ command = process.execPath;
281
+ args = [tscPath, "--project", "tsconfig.json", "--noEmit"];
282
+ }
283
+ const result = runCommandCheck(functionsRoot, {
284
+ id: "backend-build", title: "Standalone Functions compilation", command, args, shell: false,
285
+ timeoutMs: 10 * 60 * 1000,
286
+ failAction: "Fix the backend compilation errors in evidence.diagnostics / evidence.logTail, then re-run verify.",
287
+ });
288
+ result.evidence.command = `${result.evidence.command} (in functions/)`;
289
+ return result;
290
+ }
291
+
204
292
  function readPackageScripts(projectRoot: string): Record<string, string> {
205
293
  const packageJsonPath = path.join(projectRoot, "package.json");
206
294
  if (!fs.existsSync(packageJsonPath)) return {};
@@ -430,6 +518,8 @@ export async function runVerify(
430
518
  checks.push(await runDriftCheck(projectRoot));
431
519
  } else if (id === "typecheck") {
432
520
  checks.push(runTypecheckCheck(projectRoot));
521
+ } else if (id === "backend-build") {
522
+ checks.push(runBackendBuildCheck(projectRoot));
433
523
  } else if (id === "build" || id === "lint" || id === "test") {
434
524
  checks.push(runScriptCheck(projectRoot, id));
435
525
  } else {
@@ -23,6 +23,7 @@ export interface MachineErrorResponse {
23
23
  ok: false;
24
24
  command: string;
25
25
  status?: MachineOperationStatus;
26
+ nextActions?: MachineNextAction[];
26
27
  error: {
27
28
  code: string;
28
29
  message: string;
@@ -1,16 +1,18 @@
1
- import type { MachineOperationStatus } from "./contracts";
1
+ import type { MachineNextAction, MachineOperationStatus } from "./contracts";
2
2
 
3
3
  export class MachineCommandError extends Error {
4
4
  readonly code: string;
5
5
  readonly details?: unknown;
6
6
  readonly status?: MachineOperationStatus;
7
+ readonly nextActions?: MachineNextAction[];
7
8
 
8
- constructor(code: string, message: string, details?: unknown, status?: MachineOperationStatus) {
9
+ constructor(code: string, message: string, details?: unknown, status?: MachineOperationStatus, nextActions?: MachineNextAction[]) {
9
10
  super(message);
10
11
  this.name = "MachineCommandError";
11
12
  this.code = code;
12
13
  this.details = details;
13
14
  this.status = status;
15
+ this.nextActions = nextActions;
14
16
  }
15
17
  }
16
18
 
@@ -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";
@@ -58,6 +58,7 @@ function writeMachineError(command: string, error: unknown): void {
58
58
  ok: false,
59
59
  command,
60
60
  status: resolveMachineErrorStatus(error),
61
+ ...(error instanceof MachineCommandError && error.nextActions?.length ? { nextActions: error.nextActions } : {}),
61
62
  error: toMachineError(error),
62
63
  };
63
64
  writeMachineResponse(response);
@@ -140,6 +141,17 @@ function createMachineProgram(): Command {
140
141
  .showHelpAfterError(false);
141
142
 
142
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
+ });
143
155
  inspect
144
156
  .command("project")
145
157
  .description("Inspect SwallowKit project metadata")
@@ -372,7 +384,7 @@ function createMachineProgram(): Command {
372
384
 
373
385
  plan
374
386
  .command("provision")
375
- .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)")
376
388
  .requiredOption("-g, --resource-group <name>", "Resource group name")
377
389
  .requiredOption("--location <region>", "Primary location for Functions and Cosmos DB")
378
390
  .requiredOption("--swa-location <region>", "Static Web App location")
@@ -381,7 +393,8 @@ function createMachineProgram(): Command {
381
393
  .option("--adopt-existing", "Adopt an existing untagged resource group without deploying main.bicep", false)
382
394
  .option("--baseline <version>", "Baseline version for adoption", (value) => Number(value))
383
395
  .option("--reconcile", "Explicitly redeploy infra/main.bicep", false)
384
- .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 }) => {
385
398
  await handleMachineAction(
386
399
  "plan-provision",
387
400
  async () => {
@@ -395,6 +408,7 @@ function createMachineProgram(): Command {
395
408
  adoptExisting: options.adoptExisting,
396
409
  baseline: options.baseline,
397
410
  reconcile: options.reconcile,
411
+ resumeDeployment: options.resumeDeployment,
398
412
  });
399
413
  },
400
414
  (planData) => ({
@@ -545,32 +559,25 @@ function createMachineProgram(): Command {
545
559
  .description("Apply an approved provisioning plan (always requires --approve; creates billable Azure resources)")
546
560
  .requiredOption("--plan <planId>", "Provision plan to apply")
547
561
  .option("--approve", "Confirm human approval of the provisioning plan", false)
548
- .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 }) => {
549
564
  await handleMachineAction(
550
565
  "apply-provision",
551
566
  async () => {
552
567
  ensureProjectForMachine();
553
- return applyProvisionOperation({ planId: options.plan, approve: options.approve });
568
+ return applyProvisionOperation({ planId: options.plan, approve: options.approve, waitSeconds: options.waitSeconds });
554
569
  },
555
- () => ({
556
- status: "complete",
557
- nextActions: [
558
- {
559
- command: "swallowkit machine inspect infra",
560
- description: "Re-inspect infra assets after provisioning.",
561
- },
562
- ],
563
- })
570
+ (result) => ({ status: result.status, nextActions: result.nextActions })
564
571
  );
565
572
  });
566
573
 
567
574
  const verify = new Command("verify");
568
575
  verify
569
576
  .command("project")
570
- .description("Run verification checks (structure, drift, typecheck) with machine-readable evidence")
577
+ .description("Run verification checks (structure, drift, typecheck, backend-build) with machine-readable evidence")
571
578
  .option(
572
579
  "--checks <ids>",
573
- "Comma-separated check ids to run (structure,drift,typecheck,build,lint,test, plus custom ids from swallowkit.config verify.checks)"
580
+ "Comma-separated check ids to run (structure,drift,typecheck,backend-build,build,lint,test, plus custom ids from swallowkit.config verify.checks); quote CSV values in PowerShell"
574
581
  )
575
582
  .option("--compact", "Suppress info-severity findings/violations from evidence to keep agent-facing output small", false)
576
583
  .action(async (options: { checks?: string; compact?: boolean }) => {
package/src/mcp/index.ts CHANGED
@@ -5,9 +5,10 @@ import * as path from "path";
5
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
7
  import * as z from "zod/v4";
8
- import type { MachineNextAction, MachineOperationStatus, MachineResponse } from "../machine/contracts";
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,13 +25,18 @@ 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 ToolContentResult = { content: Array<{ type: "text"; text: string }> };
28
+ type MachineCliRunner = (args: string[], signal?: AbortSignal) => Promise<{ stdout: string; stderr: string; exitCode: number }>;
29
+ type ToolContentResult = { content: Array<{ type: "text"; text: string }>; isError?: boolean };
30
+ class MachineCliError extends Error {
31
+ constructor(message: string, readonly response: MachineErrorResponse) {
32
+ super(message);
33
+ }
34
+ }
29
35
  type ToolDefinition = {
30
36
  name: string;
31
37
  description: string;
32
38
  inputSchema: z.ZodTypeAny;
33
- handler: (input: any) => Promise<ToolContentResult>;
39
+ handler: (input: any, signal?: AbortSignal) => Promise<ToolContentResult>;
34
40
  };
35
41
  type ExecaModule = typeof import("execa");
36
42
 
@@ -43,29 +49,45 @@ function resolveMachineCliEntrypoint(): string {
43
49
  return path.resolve(__dirname, "..", "cli", "index.js");
44
50
  }
45
51
 
46
- 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 }> {
47
53
  const { execa } = await importModule("execa");
48
- const result = await execa(process.execPath, [resolveMachineCliEntrypoint(), "machine", ...args], {
54
+ const child = execa(process.execPath, [resolveMachineCliEntrypoint(), "machine", ...args], {
49
55
  reject: false,
50
56
  });
51
-
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); }
52
62
  return {
53
63
  stdout: result.stdout,
54
64
  stderr: result.stderr,
55
- exitCode: result.exitCode ?? 0,
65
+ exitCode: result.exitCode ?? 1,
56
66
  };
57
67
  }
58
68
 
59
69
  async function executeMachineCommand<TData>(
60
70
  args: string[],
61
- runMachineCli: MachineCliRunner
71
+ runMachineCli: MachineCliRunner,
72
+ signal?: AbortSignal
62
73
  ): Promise<MachineSuccessPayload<TData>> {
63
- 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));
64
85
 
65
86
  let parsed: MachineResponse<TData>;
66
87
  try {
67
88
  parsed = JSON.parse(result.stdout) as MachineResponse<TData>;
68
89
  } catch {
90
+ if (signal?.aborted) throw interrupted();
69
91
  throw new Error(result.stderr || result.stdout || "Machine CLI returned invalid JSON.");
70
92
  }
71
93
 
@@ -73,7 +95,7 @@ async function executeMachineCommand<TData>(
73
95
  const detailsSuffix =
74
96
  parsed.error.details !== undefined ? `\ndetails: ${JSON.stringify(parsed.error.details)}` : "";
75
97
  const statusSuffix = parsed.status ? ` (status: ${parsed.status})` : "";
76
- throw new Error(`[${parsed.error.code}] ${parsed.error.message}${statusSuffix}${detailsSuffix}`);
98
+ throw new MachineCliError(`[${parsed.error.code}] ${parsed.error.message}${statusSuffix}${detailsSuffix}`, parsed);
77
99
  }
78
100
 
79
101
  return parsed;
@@ -308,7 +330,7 @@ export function buildSwallowKitToolDefinitions(
308
330
  },
309
331
  {
310
332
  name: "swallowkit_verify_project",
311
- description: "Run verification checks (structure, drift, typecheck by default; build, lint, test and custom checks from swallowkit.config verify.checks are also available) and return machine-readable evidence. summary.done=true means the project passed all checks. Set compact=true to suppress info-severity findings and keep the output small.",
333
+ description: "Run verification checks (structure, drift, typecheck, backend-build by default; build, lint, test and custom checks from swallowkit.config verify.checks are also available) and return machine-readable evidence. backend-build compiles standalone C#/TypeScript Functions or checks Python syntax; unavailable toolchains are skipped with a reason. summary.done=true means no checks failed, not that skipped checks were validated or runtime/auth flows work. Set compact=true to suppress info-severity findings and keep the output small.",
312
334
  inputSchema: z.object({
313
335
  checks: z.array(z.string()).optional(),
314
336
  compact: z.boolean().optional(),
@@ -418,14 +440,16 @@ export function buildSwallowKitToolDefinitions(
418
440
  adoptExisting: z.boolean().optional(),
419
441
  baseline: z.number().int().nonnegative().optional(),
420
442
  reconcile: z.boolean().optional(),
443
+ resumeDeployment: z.string().optional(),
421
444
  }),
422
- 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 }) => {
423
446
  const args = ["plan", "provision", "--resource-group", resourceGroup, "--location", location, "--swa-location", swaLocation];
424
447
  if (subscription) args.push("--subscription", subscription);
425
448
  if (whatIf) args.push("--what-if");
426
449
  if (adoptExisting) args.push("--adopt-existing");
427
450
  if (baseline !== undefined) args.push("--baseline", String(baseline));
428
451
  if (reconcile) args.push("--reconcile");
452
+ if (resumeDeployment) args.push("--resume-deployment", resumeDeployment);
429
453
 
430
454
  const response = await executeMachineCommand(args, runMachineCli);
431
455
  return jsonTextContent(withOperationStatus(response));
@@ -433,16 +457,32 @@ export function buildSwallowKitToolDefinitions(
433
457
  },
434
458
  {
435
459
  name: "swallowkit_apply_provision",
436
- 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.",
437
461
  inputSchema: z.object({
438
462
  planId: z.string(),
439
463
  approve: z.boolean().optional(),
464
+ waitSeconds: z.number().int().min(0).max(3600).optional(),
440
465
  }),
441
- handler: async ({ planId, approve }: { planId: string; approve?: boolean }) => {
466
+ handler: async ({ planId, approve, waitSeconds }: { planId: string; approve?: boolean; waitSeconds?: number }, signal?: AbortSignal) => {
442
467
  const args = ["apply", "provision", "--plan", planId];
443
468
  if (approve) args.push("--approve");
469
+ if (waitSeconds !== undefined) args.push("--wait-seconds", String(waitSeconds));
444
470
 
445
- 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);
446
486
  return jsonTextContent(withOperationStatus(response));
447
487
  },
448
488
  },
@@ -451,9 +491,17 @@ export function buildSwallowKitToolDefinitions(
451
491
  // Every response reports which SwallowKit version answered and why it was selected.
452
492
  return definitions.map((definition) => ({
453
493
  ...definition,
454
- handler: async (input: unknown) => {
455
- const result = await definition.handler(input);
456
- return jsonTextContent(withSwallowKitMetadata(JSON.parse(result.content[0].text)));
494
+ handler: async (input: unknown, signal?: AbortSignal) => {
495
+ try {
496
+ const result = await definition.handler(input, signal);
497
+ return jsonTextContent(withSwallowKitMetadata(JSON.parse(result.content[0].text)));
498
+ } catch (error) {
499
+ // Prerequisite failures carry nextActions; keep them structured so agents can follow them.
500
+ if (error instanceof MachineCliError && error.response.nextActions?.length) {
501
+ return { ...jsonTextContent(withSwallowKitMetadata(error.response)), isError: true };
502
+ }
503
+ throw error;
504
+ }
457
505
  },
458
506
  }));
459
507
  }
@@ -475,7 +523,7 @@ export function createSwallowKitMcpServer(
475
523
  description: tool.description,
476
524
  inputSchema: tool.inputSchema,
477
525
  },
478
- tool.handler
526
+ (input, extra) => tool.handler(input, extra.signal)
479
527
  );
480
528
  }
481
529