canon-ai 3.11.0 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [3.12.0] — 2026-10-21
8
+
9
+ ### Added
10
+
11
+ - **Broken references in a task's handoff or QA summary are fixed by the agent that wrote them, before the commit.** After implement, and again after QA, `canon run` checks the task's own artifacts with the docs-refs checker. If it finds a broken reference, it resumes the same agent session to fix it, up to two attempts. A fix session may change only the task's own artifacts; if it touches anything else, the run stops and names the files. If a reference is still broken after two attempts, canon warns and continues as before. The implement and QA prompts also now ask agents to cite repository files by full path and deleted files as markdown links.
12
+ - **`--full-send` pushes its own scaffold commits to the base branch before opening the draft PR.** It pushes only commits that have the exact scaffold subject and change only task artifacts and pipeline telemetry, and it pushes the checked commit rather than the moving branch. If any other commit is unpushed on the base branch, nothing is pushed and the run stops at the divergence check as before. A rejected push (for example, a protected branch) stops with a hint to push or merge manually. The "Scaffold committed" hint now prints only when such commits exist.
13
+
14
+ ### Changed
15
+
16
+ - **`--pr` and `--push` block only on real merge conflicts with the base.** A base branch that moved on files the task never touched, such as another task's artifacts, no longer stops the run; canon prints one line saying those files merge cleanly. A conflict with `origin/<base>`, including in a file the spec declared, still aborts before the push and names the conflicted files. The task's own out-of-scope changes still abort as before. `--force` bypasses a conflict with a warning. On Git older than 2.38, canon warns that conflicts weren't checked and proceeds.
17
+
18
+ ### Fixed
19
+
20
+ - **`canon watch` reports a full-send run that died after QA as a death, not a clean stop.** A run that crashed while opening its draft PR now shows `reason=death` and exits 4, with the reason on stderr, whether watch was attached during the run or started after it. A tail that is still running keeps watch blocking.
21
+ - **`canon task reset-spec-review` re-arms the spec gate.** The gate disarms itself when it halts, so a reset after that halt used to let the next run go straight through to human review. The reset now re-arms it and says so, unless the task is full-send.
22
+ - **`--ship` no longer warns that the branch delete failed.** It merges with `gh pr merge --squash`, deletes the remote branch itself, and deletes the local branch after the worktree is removed; the old order made `gh` try to delete a branch the worktree still held.
23
+
7
24
  ## [3.11.0] — 2026-10-20
8
25
 
9
26
  ### Changed
package/README.md CHANGED
@@ -257,7 +257,7 @@ Pipeline sessions are headless, so nobody is at the keyboard to answer a permiss
257
257
  - **Worktrees separate branch state by convention, not by enforcement.** Each task, or each bundle when you pass several task ids together, gets its own worktree and branch, and the pipeline's dirty-tree gates inspect the active worktree at phase boundaries. But Claude sessions are also given the main checkout via `--add-dir` and run with permissions skipped, so nothing stops a session from writing there, and an unrelated file dropped into the main checkout is not something those gates look for; tasks with `worktree: false` run in the main checkout outright. Validation commands run with your normal privileges.
258
258
  - **Budget caps bound cost, not access.** Each headless Claude phase runs with `--max-budget-usd`; hitting the cap stops the session. It does not narrow what the session may do beforehand, and interactive sessions (`--interactive`) run uncapped.
259
259
  - **Your code goes to the model providers.** Each agent phase sends prompts and repository context to the provider running it, Anthropic for Claude phases and OpenAI for Codex phases, with `code_review` using both, all through the Claude Code and Codex CLIs under whatever data terms your accounts have. Treat a `canon run` like any other agent session in that respect.
260
- - **Git side effects need a flag.** The pipeline never pushes, opens PRs, merges, or publishes on its own. `--pr` pushes the task branch and opens a draft PR, stopping at `human_review`. `--ship` squash-merges that PR; it checks the task's phase, not whether anyone approved, so PR approval is your prerequisite (or your branch protection's), not something canon enforces. `--full-send` skips the human spec gate and opens the draft PR on clean QA, still stopping before merge; on a `delicate` task it refuses without `--force`.
260
+ - **Git side effects need a flag.** The pipeline never pushes, opens PRs, merges, or publishes on its own. `--pr` pushes the task branch and opens a draft PR, stopping at `human_review`. `--ship` squash-merges that PR; it checks the task's phase, not whether anyone approved, so PR approval is your prerequisite (or your branch protection's), not something canon enforces. `--full-send` skips the human spec gate and opens the draft PR on clean QA, still stopping before merge; its tail also pushes unpushed base commits that carry only task artifacts and pipeline telemetry, and leaves other unpushed commits to the divergence gate unless `--allow-divergent-base` is set. On a `delicate` task it refuses without `--force`.
261
261
 
262
262
  Run canon on repositories where you would be comfortable running your test suite unattended. If that is not your situation, keep it to a machine or container scoped for the purpose. Replacing the blanket permission skip with a narrower confinement is on the roadmap; the constraint to solve is that adopter validation commands legitimately need network and system access the sandbox would block. Security reports go through [SECURITY.md](SECURITY.md).
263
263
 
package/dist/cli/index.js CHANGED
@@ -403,6 +403,11 @@ function filterGitIgnoredPaths(paths, cwd) {
403
403
  const stdout = result.stdout ?? "";
404
404
  return new Set(stdout.split("\0").filter((p) => p.length > 0));
405
405
  }
406
+ var CANON_TASK_ID_PATTERN = "[a-z0-9][a-z0-9._-]*";
407
+ var SCAFFOLD_ARTIFACTS_SUBJECT = new RegExp(`^task\\(${CANON_TASK_ID_PATTERN}\\): commit artifacts pre-pipeline$`);
408
+ var SCAFFOLD_TELEMETRY_SUBJECT = new RegExp(
409
+ `^chore: absorb pre-implement telemetry into scaffold for ${CANON_TASK_ID_PATTERN}(?:, ${CANON_TASK_ID_PATTERN})*$`
410
+ );
406
411
 
407
412
  // src/orchestrator/worktree.ts
408
413
  var PIPELINE_TELEMETRY_FILES = [
@@ -1500,7 +1505,7 @@ function checkTemplates(cwd) {
1500
1505
  }
1501
1506
  function checkCanonVersion(cwd) {
1502
1507
  const versionPath = join(cwd, ".canon", "version");
1503
- const installedVersion = "3.11.0";
1508
+ const installedVersion = "3.12.0";
1504
1509
  if (!existsSync(versionPath)) {
1505
1510
  return { label: ".canon/version", status: "warn", detail: "missing \u2014 run `canon upgrade`" };
1506
1511
  }
@@ -2025,7 +2030,7 @@ function initCmd(_args) {
2025
2030
  }
2026
2031
  function writeCanonVersion(cwd) {
2027
2032
  const versionPath = join2(cwd, ".canon", "version");
2028
- const version = "3.11.0";
2033
+ const version = "3.12.0";
2029
2034
  mkdirSync(dirname2(versionPath), { recursive: true });
2030
2035
  writeFileSync(versionPath, version + "\n");
2031
2036
  }
@@ -2382,6 +2387,9 @@ function findFirstBlockedPhase(status) {
2382
2387
  }
2383
2388
  return null;
2384
2389
  }
2390
+ function isFullSendTailPending(status) {
2391
+ return status.full_send === true && status.status === "human_review" && (status.phases.human_review?.status ?? "pending") === "pending";
2392
+ }
2385
2393
  function findPreviousDonePhase(status, beforePhase) {
2386
2394
  const index = PHASE_ORDER.indexOf(beforePhase);
2387
2395
  for (let i = index - 1; i >= 0; i -= 1) {
@@ -2518,6 +2526,8 @@ function parseWatchArgs(args2) {
2518
2526
  }
2519
2527
  function summarizeIdle(result) {
2520
2528
  switch (result.kind) {
2529
+ case "live":
2530
+ throw new Error("summarizeIdle called for live result");
2521
2531
  case "checkpoint":
2522
2532
  return { state: result.state, reason: "checkpoint", phase: result.phase, verdict: result.verdict, pid: result.pid };
2523
2533
  case "complete":
@@ -2572,6 +2582,15 @@ function classifyAttach(ctx, taskId, probeAlive, now) {
2572
2582
  return { kind: "launch_window", state };
2573
2583
  }
2574
2584
  if (status) {
2585
+ if (isFullSendTailPending(status)) {
2586
+ if (orchestratorLive && ctx.resolvedPid != null) {
2587
+ return { kind: "live", pid: ctx.resolvedPid, state };
2588
+ }
2589
+ const heartbeatPrimary = ctx.heartbeatResult.kind === "found" ? ctx.heartbeatResult.record.task_ids[0] : void 0;
2590
+ const primaryId = typeof heartbeatPrimary === "string" && heartbeatPrimary.length > 0 ? heartbeatPrimary : taskId;
2591
+ const reason = findCrashEvidence(ctx, taskId, probeAlive, primaryId, orchestratorLive);
2592
+ if (reason) return { kind: "death", state, hint: `run \`canon run ${taskId}\` to resume`, reason };
2593
+ }
2575
2594
  const currentPhase = PHASE_ORDER.find((phase) => (status.phases[phase]?.status ?? "pending") !== "done");
2576
2595
  const currentStatus = currentPhase ? status.phases[currentPhase]?.status ?? "pending" : null;
2577
2596
  if (currentPhase && currentPhase !== "human_review" && (currentStatus === "pending" || currentStatus === "in_progress")) {
@@ -2634,6 +2653,13 @@ function classifyIdle(ctx, taskId, probeAlive = () => false, now = Date.now(), p
2634
2653
  return { kind: "auto_block", state: "blocked", phase: blockedPhase, pid: ctx.resolvedPid ?? void 0 };
2635
2654
  }
2636
2655
  if (state === "human_review") {
2656
+ if (isFullSendTailPending(status)) {
2657
+ if (orchestratorLive && ctx.resolvedPid != null) {
2658
+ return { kind: "live", state, pid: ctx.resolvedPid };
2659
+ }
2660
+ const crashReason = findCrashEvidence(ctx, taskId, probeAlive, primaryTaskId, orchestratorLive);
2661
+ if (crashReason) return { kind: "death", state, pid: ctx.resolvedPid ?? void 0, reason: crashReason };
2662
+ }
2637
2663
  const verdict = status.phases.code_review?.verdict || void 0;
2638
2664
  return {
2639
2665
  kind: "checkpoint",
@@ -2689,6 +2715,7 @@ function orchestratorStillProgressing(ctx, probeAlive, now = Date.now()) {
2689
2715
  const status = ctx.statusResult.kind === "ok" && isStatusJson(ctx.statusResult.status) ? ctx.statusResult.status : null;
2690
2716
  if (status == null) return false;
2691
2717
  if (findFirstBlockedPhase(status) && !blockedPhaseLivenessTrusted(ctx, orchestratorLive, now)) return false;
2718
+ if (isFullSendTailPending(status)) return true;
2692
2719
  const state = status.status;
2693
2720
  return state !== "human_review" && state !== "complete";
2694
2721
  }
@@ -3007,6 +3034,11 @@ function watchCmd(args2, deps = {}) {
3007
3034
  return exit(0);
3008
3035
  }
3009
3036
  const idleResult = classifyIdle(freshCtx, taskId, isOrchestratorAlive, now(), rememberedPrimaryTaskId ?? void 0);
3037
+ if (idleResult.kind === "live") {
3038
+ reportPhasePointer(freshCtx);
3039
+ if (parsed.follow) tailRunLog(freshCtx, taskId, { stderr }, tailState, rememberedPrimaryTaskId ?? void 0);
3040
+ continue;
3041
+ }
3010
3042
  if (idleResult.kind === "death" && idleResult.reason) {
3011
3043
  stderr(`canon watch: ${idleResult.reason}; run \`canon run ${taskId}\` to resume
3012
3044
  `);
@@ -3036,7 +3068,7 @@ function watchCmd(args2, deps = {}) {
3036
3068
  }
3037
3069
 
3038
3070
  // src/task/index.ts
3039
- import { spawnSync as spawnSync8 } from "child_process";
3071
+ import { spawnSync as spawnSync9 } from "child_process";
3040
3072
  import fs16 from "fs";
3041
3073
  import path16 from "path";
3042
3074
 
@@ -3049,7 +3081,7 @@ function isInstalledSourcePath(sourcePath) {
3049
3081
  return sourcePath.includes("/node_modules/") || sourcePath.includes("\\node_modules\\") || sourcePath.includes("/_npx/") || sourcePath.includes("\\_npx\\");
3050
3082
  }
3051
3083
  function resolveCanonVersion(explicit) {
3052
- return explicit ?? "3.11.0" ?? "dev";
3084
+ return explicit ?? "3.12.0" ?? "dev";
3053
3085
  }
3054
3086
  function resolveOrchestratorCommit(repoRoot, upstreamCommit, runGitAt) {
3055
3087
  const ownToplevel = captureGitOutput(repoRoot, ["rev-parse", "--show-toplevel"], runGitAt);
@@ -3183,6 +3215,7 @@ function isPristineTaskArtifact(content, basename2, taskId, title) {
3183
3215
  }
3184
3216
 
3185
3217
  // src/orchestrator/validation.ts
3218
+ import { spawnSync as spawnSync8 } from "child_process";
3186
3219
  import fs14 from "fs";
3187
3220
  import path14 from "path";
3188
3221
  function computeLatestValidationResults(handoffContent) {
@@ -3852,13 +3885,13 @@ function writeStatusAtomic(filePath, status) {
3852
3885
  }
3853
3886
  function runGit(args2, options = {}) {
3854
3887
  if (options.stdio === "inherit") {
3855
- return spawnSync8("git", args2, {
3888
+ return spawnSync9("git", args2, {
3856
3889
  cwd: options.cwd ?? process.cwd(),
3857
3890
  encoding: "utf8",
3858
3891
  stdio: "inherit"
3859
3892
  });
3860
3893
  }
3861
- return spawnSync8("git", args2, {
3894
+ return spawnSync9("git", args2, {
3862
3895
  cwd: options.cwd ?? process.cwd(),
3863
3896
  encoding: "utf8",
3864
3897
  stdio: ["ignore", "pipe", "pipe"]
@@ -4562,9 +4595,16 @@ function taskResetSpecReview(id) {
4562
4595
  if (status.sessions && Object.hasOwn(status.sessions, "claude_spec")) {
4563
4596
  delete status.sessions.claude_spec;
4564
4597
  }
4598
+ const rearmGate = status.full_send !== true && status.human_spec_gate !== true;
4599
+ if (rearmGate) status.human_spec_gate = true;
4565
4600
  status.updated = today();
4566
4601
  writeStatusAtomic(statusPath, status);
4567
4602
  console.log(`Reset ${id}: spec \u2192 done, spec_review \u2192 pending (iter=0, verdict cleared, claude_spec session dropped)`);
4603
+ if (rearmGate) {
4604
+ console.log(
4605
+ `Re-armed the spec gate: the next \`canon run ${id}\` stops after spec_review approves. To continue past it instead, run \`canon task set ${id} human_spec_gate false\` first.`
4606
+ );
4607
+ }
4568
4608
  }
4569
4609
  function taskResetCodeReview(id) {
4570
4610
  if (!id) throw new Error("Error: usage: canon task reset-code-review <TASK-ID>");
@@ -4615,7 +4655,7 @@ function taskResetCodeReview(id) {
4615
4655
  );
4616
4656
  }
4617
4657
  function ensureGitAvailable() {
4618
- const result = spawnSync8("git", ["--version"], { stdio: "ignore" });
4658
+ const result = spawnSync9("git", ["--version"], { stdio: "ignore" });
4619
4659
  if (result.error || result.status !== 0) {
4620
4660
  throw new Error("Error: git is required.");
4621
4661
  }
@@ -4996,7 +5036,7 @@ function taskCmd2(args2) {
4996
5036
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, realpathSync as realpathSync2, writeFileSync as writeFileSync2 } from "fs";
4997
5037
  import { fileURLToPath as fileURLToPath4 } from "url";
4998
5038
  import { dirname as dirname4, join as join4 } from "path";
4999
- import { spawnSync as spawnSync9 } from "child_process";
5039
+ import { spawnSync as spawnSync10 } from "child_process";
5000
5040
  var packageDir3 = join4(dirname4(fileURLToPath4(import.meta.url)), "../..");
5001
5041
  function detectInstallType(pkgDirOverride) {
5002
5042
  const dir = pkgDirOverride ?? packageDir3;
@@ -5049,7 +5089,7 @@ function nonInteractiveSshCommand(configuredCommand) {
5049
5089
  return `${command2} -oBatchMode=yes`;
5050
5090
  }
5051
5091
  function defaultGitRunner(args2) {
5052
- const result = spawnSync9("git", args2, {
5092
+ const result = spawnSync10("git", args2, {
5053
5093
  encoding: "utf8",
5054
5094
  timeout: GIT_RESOLUTION_TIMEOUT_MS,
5055
5095
  env: {
@@ -5234,7 +5274,7 @@ function currentPinFromManifest(manifest) {
5234
5274
  return "unknown";
5235
5275
  }
5236
5276
  function bakedVersion() {
5237
- return "3.11.0";
5277
+ return "3.12.0";
5238
5278
  }
5239
5279
  function ownPackageName(pkgDir) {
5240
5280
  try {
@@ -5246,7 +5286,7 @@ function ownPackageName(pkgDir) {
5246
5286
  }
5247
5287
  var NPM_VIEW_TIMEOUT_MS = 3e4;
5248
5288
  function defaultNpmViewRunner(args2, cwd) {
5249
- const result = spawnSync9("npm", args2, { encoding: "utf8", timeout: NPM_VIEW_TIMEOUT_MS, ...cwd ? { cwd } : {} });
5289
+ const result = spawnSync10("npm", args2, { encoding: "utf8", timeout: NPM_VIEW_TIMEOUT_MS, ...cwd ? { cwd } : {} });
5250
5290
  if (result.error) return { status: null, stdout: "", stderr: result.error.message };
5251
5291
  return { status: result.status, stdout: result.stdout ?? "", stderr: (result.stderr ?? "").trim() };
5252
5292
  }
@@ -5302,7 +5342,7 @@ function updateCmd(args2, deps = {}) {
5302
5342
  const pkgDir = deps.packageDir ?? packageDir3;
5303
5343
  const pkgName = ownPackageName(pkgDir);
5304
5344
  const cwd = deps.cwd ?? process.cwd();
5305
- const spawn2 = deps.spawnRunner ?? ((cmd, cmdArgs, opts) => spawnSync9(cmd, cmdArgs, { stdio: "inherit", cwd: opts.cwd }));
5345
+ const spawn2 = deps.spawnRunner ?? ((cmd, cmdArgs, opts) => spawnSync10(cmd, cmdArgs, { stdio: "inherit", cwd: opts.cwd }));
5306
5346
  const runGit2 = deps.gitRunner ?? defaultGitRunner;
5307
5347
  const nowIso = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
5308
5348
  let options;
@@ -5433,7 +5473,7 @@ function updateCmd(args2, deps = {}) {
5433
5473
  import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
5434
5474
  import { fileURLToPath as fileURLToPath5 } from "url";
5435
5475
  import { basename, dirname as dirname5, join as join5, relative as relative3, resolve } from "path";
5436
- import { spawnSync as spawnSync10 } from "child_process";
5476
+ import { spawnSync as spawnSync11 } from "child_process";
5437
5477
 
5438
5478
  // src/lib/canon-owned.ts
5439
5479
  var CANON_OWNED = [
@@ -5542,7 +5582,7 @@ function classifyDestinations(cwd, relPaths) {
5542
5582
  const classes = /* @__PURE__ */ new Map();
5543
5583
  const uniqueRelPaths = [...new Set(relPaths)];
5544
5584
  if (uniqueRelPaths.length === 0) return classes;
5545
- const probe = spawnSync10("git", ["rev-parse", "--is-inside-work-tree"], {
5585
+ const probe = spawnSync11("git", ["rev-parse", "--is-inside-work-tree"], {
5546
5586
  cwd,
5547
5587
  encoding: "utf8",
5548
5588
  stdio: ["ignore", "pipe", "ignore"]
@@ -5554,12 +5594,12 @@ function classifyDestinations(cwd, relPaths) {
5554
5594
  }
5555
5595
  return classes;
5556
5596
  }
5557
- const lsFiles = spawnSync10("git", ["ls-files", "-z", "--", ...uniqueRelPaths], {
5597
+ const lsFiles = spawnSync11("git", ["ls-files", "-z", "--", ...uniqueRelPaths], {
5558
5598
  cwd,
5559
5599
  encoding: "utf8",
5560
5600
  stdio: ["ignore", "pipe", "ignore"]
5561
5601
  });
5562
- const status = spawnSync10("git", ["status", "--porcelain=v1", "-z", "--", ...uniqueRelPaths], {
5602
+ const status = spawnSync11("git", ["status", "--porcelain=v1", "-z", "--", ...uniqueRelPaths], {
5563
5603
  cwd,
5564
5604
  encoding: "utf8",
5565
5605
  stdio: ["ignore", "pipe", "ignore"]
@@ -5711,7 +5751,7 @@ function runUpgrade(cwd, pkgDir, options = {}) {
5711
5751
  cutoverWarnings.push(docsRefsCheckRel);
5712
5752
  }
5713
5753
  const versionPath = join5(cwd, ".canon", "version");
5714
- const newVersion = "3.11.0";
5754
+ const newVersion = "3.12.0";
5715
5755
  const currentVersion = existsSync5(versionPath) ? readFileSync4(versionPath, "utf8").trim() : null;
5716
5756
  if (currentVersion !== newVersion) {
5717
5757
  pending.push({ rel: ".canon/version", projectPath: versionPath, content: newVersion + "\n" });
@@ -5864,7 +5904,7 @@ function upgradeCmd(args2) {
5864
5904
  process.exit(2);
5865
5905
  }
5866
5906
  if (upgraded.length > 0 && !options.noStage) {
5867
- const r = spawnSync10("git", ["add", ...upgraded], { cwd: process.cwd(), stdio: "inherit" });
5907
+ const r = spawnSync11("git", ["add", ...upgraded], { cwd: process.cwd(), stdio: "inherit" });
5868
5908
  if (r.status !== 0) {
5869
5909
  console.error("\nwarning: failed to stage changes \u2014 run `git add` manually.");
5870
5910
  }
@@ -5992,14 +6032,17 @@ canon run options:
5992
6032
  protected/configured docs listed in spec.md's "### Affected Files"
5993
6033
  table (all admitted docs once QA is done). Dirty files outside
5994
6034
  that set die with a remediation message.
5995
- Aborts if HEAD's tree differs from origin/<base> on files not in
5996
- spec's Affected Files (bypass with --force).
6035
+ Aborts if merging origin/<base> conflicts, or branch changes files
6036
+ outside spec's Affected Files (bypass with --force).
5997
6037
  --push At human_review: push branch only, no PR (requires gh). Same allow-list
5998
- as --pr. Aborts if HEAD's tree differs from origin/<base> on files not
5999
- in spec's Affected Files (bypass with --force).
6038
+ as --pr. Aborts if merging origin/<base> conflicts, or branch changes
6039
+ files outside spec's Affected Files (bypass with --force).
6000
6040
  --full-send Skip the spec gate and auto-open a draft PR after clean QA
6001
- --force Acknowledge high-commitment combinations (currently: --full-send on a delicate task)
6002
- --ship Merge the open PR (calls gh pr merge --squash --delete-branch), tear
6041
+ --force Acknowledge high-commitment combinations and bypass
6042
+ explicit safety gates where documented (currently:
6043
+ --full-send on delicate tasks, reroute amendment gate,
6044
+ base-drift gate, and dirty REPO_ROOT worktree-start gate)
6045
+ --ship Merge the open PR (gh pr merge --squash), delete its remote branch, tear
6003
6046
  down the worktree, archive the task dir, and pull the base branch. Run
6004
6047
  after the PR is approved \u2014 do NOT merge the PR manually first. If you
6005
6048
  already merged externally, --ship detects the merged state and resumes
@@ -6020,7 +6063,7 @@ Global:
6020
6063
  `);
6021
6064
  }
6022
6065
  function printVersion() {
6023
- console.log("3.11.0");
6066
+ console.log("3.12.0");
6024
6067
  }
6025
6068
  switch (command) {
6026
6069
  case "doctor":