@sma1lboy/rove 0.9.281-next.31 → 0.9.281

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 (110) hide show
  1. package/dist/cli/chunk-01tghgf3.js +8 -0
  2. package/dist/cli/{chunk-jk4f0xsh.js → chunk-03fv92ya.js} +1 -1
  3. package/dist/cli/{chunk-7aaw17vr.js → chunk-08mb3qss.js} +1 -1
  4. package/dist/cli/{chunk-y7mjhjby.js → chunk-0gbg7bg8.js} +1 -1
  5. package/dist/cli/{chunk-zv0rrhck.js → chunk-0nkk13z9.js} +1 -1
  6. package/dist/cli/{chunk-zp0q931y.js → chunk-14tevt5v.js} +1 -1
  7. package/dist/cli/{chunk-1g6vhgfe.js → chunk-16e6jqq6.js} +1 -1
  8. package/dist/cli/{chunk-3hxm56h6.js → chunk-1a57ejnh.js} +1 -1
  9. package/dist/cli/{chunk-4z1a65bz.js → chunk-32y7f5dr.js} +1 -1
  10. package/dist/cli/{chunk-j54c5a0a.js → chunk-3m0ydze9.js} +1 -1
  11. package/dist/cli/{chunk-12wn9jtf.js → chunk-3pgp6z3k.js} +1 -1
  12. package/dist/cli/{chunk-amq030gb.js → chunk-3qj9yw5c.js} +2 -2
  13. package/dist/cli/chunk-453qpfae.js +2 -0
  14. package/dist/cli/{chunk-gk8efsx0.js → chunk-46qs744w.js} +1 -1
  15. package/dist/cli/{chunk-am5cpb9n.js → chunk-4vkrv4eb.js} +1 -1
  16. package/dist/cli/{chunk-hfvzvj91.js → chunk-4yr1534j.js} +1 -1
  17. package/dist/cli/{chunk-byh32qnx.js → chunk-5swmvt20.js} +1 -1
  18. package/dist/cli/{chunk-j8d9sk5j.js → chunk-6wt08a28.js} +1 -1
  19. package/dist/cli/{chunk-f262ywvg.js → chunk-7911jkq6.js} +1 -1
  20. package/dist/cli/chunk-8ng1756g.js +3 -0
  21. package/dist/cli/{chunk-ebdp0z1d.js → chunk-95kk75wh.js} +1 -1
  22. package/dist/cli/{chunk-6mk6ceb6.js → chunk-9qpea48c.js} +2 -2
  23. package/dist/cli/chunk-9vwb9f7q.js +7 -0
  24. package/dist/cli/{chunk-nqwza8b0.js → chunk-a5xbsvx2.js} +1 -1
  25. package/dist/cli/{chunk-t7dyf50m.js → chunk-ap3fgbbt.js} +1 -1
  26. package/dist/cli/{chunk-5kgq379z.js → chunk-b5fdqynx.js} +1 -1
  27. package/dist/cli/{chunk-x85rcep5.js → chunk-bbq6wc47.js} +1 -1
  28. package/dist/cli/{chunk-np398f2a.js → chunk-bbt217yd.js} +1 -1
  29. package/dist/cli/{chunk-w2tcb3rn.js → chunk-bm6qhcck.js} +1 -1
  30. package/dist/cli/{chunk-txvjg1g9.js → chunk-btxfcc77.js} +3 -3
  31. package/dist/cli/chunk-c9ptxthj.js +2 -0
  32. package/dist/cli/{chunk-zkby63ej.js → chunk-cs410j8b.js} +1 -1
  33. package/dist/cli/{chunk-rvjnafyt.js → chunk-cx78tyg1.js} +1 -1
  34. package/dist/cli/{chunk-4drm07mp.js → chunk-cx8wnz54.js} +1 -1
  35. package/dist/cli/{chunk-t2p22ww7.js → chunk-ejfs22aq.js} +1 -1
  36. package/dist/cli/chunk-f23manyf.js +8 -0
  37. package/dist/cli/{chunk-7nnpp89t.js → chunk-ga82ras9.js} +1 -1
  38. package/dist/cli/{chunk-f7egph8g.js → chunk-gzkes5bw.js} +1 -1
  39. package/dist/cli/{chunk-f0sahrfw.js → chunk-hmy01yg2.js} +16 -14
  40. package/dist/cli/{chunk-3a7x7jg7.js → chunk-hnxs5shy.js} +16 -17
  41. package/dist/cli/{chunk-bw2y7670.js → chunk-j2e7jgh8.js} +1 -1
  42. package/dist/cli/{chunk-qc9sj5wh.js → chunk-j5y7hhc2.js} +1 -1
  43. package/dist/cli/{chunk-qzhfs668.js → chunk-jar50zcp.js} +1 -1
  44. package/dist/cli/{chunk-r98h6b42.js → chunk-k2rmj0v3.js} +2 -2
  45. package/dist/cli/{chunk-bv050ssb.js → chunk-k3h4cpdy.js} +1 -1
  46. package/dist/cli/{chunk-wz307hf6.js → chunk-k9xabf0t.js} +1 -1
  47. package/dist/cli/{chunk-02x0ahxk.js → chunk-kp43j420.js} +1 -1
  48. package/dist/cli/{chunk-6cfs0nqe.js → chunk-kt5ngemc.js} +1 -1
  49. package/dist/cli/chunk-nkgn0bqq.js +2 -0
  50. package/dist/cli/{chunk-fkgemeww.js → chunk-q2nhmaq9.js} +2 -2
  51. package/dist/cli/{chunk-hd1szhba.js → chunk-q6cwy6v5.js} +1 -1
  52. package/dist/cli/{chunk-jkgp816v.js → chunk-q72w3dsx.js} +1 -1
  53. package/dist/cli/{chunk-hmsvt5nj.js → chunk-rqm2kn0c.js} +1 -1
  54. package/dist/cli/{chunk-q6tsx6zc.js → chunk-sm96b61g.js} +1 -1
  55. package/dist/cli/chunk-sqdmngys.js +2 -0
  56. package/dist/cli/{chunk-k2j0tg1h.js → chunk-tgv90744.js} +7 -3
  57. package/dist/cli/{chunk-vbvbwjad.js → chunk-twy9bfps.js} +1 -1
  58. package/dist/cli/{chunk-5g6v6tq5.js → chunk-v3ecphrz.js} +1 -1
  59. package/dist/cli/{chunk-mjzjah9d.js → chunk-vtay379p.js} +1 -1
  60. package/dist/cli/chunk-vx44yxra.js +3 -0
  61. package/dist/cli/{chunk-w003rpr2.js → chunk-w3kvkax7.js} +1 -1
  62. package/dist/cli/{chunk-s1xn6wav.js → chunk-w6ft4djp.js} +1 -1
  63. package/dist/cli/{chunk-8hernv9y.js → chunk-w8dmmnys.js} +1 -1
  64. package/dist/cli/{chunk-9f34p80j.js → chunk-ww0zctb7.js} +1 -1
  65. package/dist/cli/{chunk-qwwt4ch2.js → chunk-x119xqa6.js} +1 -1
  66. package/dist/cli/{chunk-nrx8pf9p.js → chunk-x35dfb1x.js} +1 -1
  67. package/dist/cli/{chunk-ggjmcqpt.js → chunk-x94e0rw4.js} +3 -3
  68. package/dist/cli/chunk-xaam0d8r.js +4 -0
  69. package/dist/cli/{chunk-azt0c2b5.js → chunk-xekk4k54.js} +1 -1
  70. package/dist/cli/{chunk-mqvkd2g1.js → chunk-xqvyzv62.js} +1 -1
  71. package/dist/cli/chunk-xxz3np4w.js +2 -0
  72. package/dist/cli/{chunk-p11emdb2.js → chunk-y07cp9mb.js} +1 -1
  73. package/dist/cli/{chunk-5dq0n3h7.js → chunk-y2v1x23z.js} +1 -1
  74. package/dist/cli/chunk-yn01fet6.js +3 -0
  75. package/dist/cli/{chunk-cakyme4s.js → chunk-z8rvxvx5.js} +1 -1
  76. package/dist/cli/chunk-zr058kbe.js +3 -0
  77. package/dist/cli/index.js +3 -3
  78. package/dist/cli/rove.js +3 -3
  79. package/dist/skills/rove/SKILL.md +4 -2
  80. package/dist/skills/rove/references/api-flags.md +7 -4
  81. package/dist/voice/asr_server.py +318 -0
  82. package/dist/voice/engine.py +174 -0
  83. package/dist/voice/fetch_model.py +30 -0
  84. package/dist/voice/learn.py +184 -0
  85. package/dist/voice/live.py +93 -0
  86. package/dist/voice/phonetic.py +87 -0
  87. package/dist/voice/polish.py +84 -0
  88. package/dist/voice/polish_prompt.md +50 -0
  89. package/dist/voice/pyproject.toml +28 -0
  90. package/dist/voice/uv.lock +815 -0
  91. package/dist/voice/vocab.py +193 -0
  92. package/dist/web/assets/index-Bo92_NP5.css +1 -0
  93. package/dist/web/assets/index-Ct8dDnCc.js +154 -0
  94. package/dist/web/index.html +3 -3
  95. package/dist/web/relay/index.html +5 -5
  96. package/package.json +4 -3
  97. package/dist/cli/chunk-0g1h35mr.js +0 -2
  98. package/dist/cli/chunk-1wwqf6kx.js +0 -3
  99. package/dist/cli/chunk-26ydgwym.js +0 -2
  100. package/dist/cli/chunk-2pfh3zm0.js +0 -33
  101. package/dist/cli/chunk-2wrt80ft.js +0 -3
  102. package/dist/cli/chunk-7fya3y0n.js +0 -2
  103. package/dist/cli/chunk-d2wndk4q.js +0 -2
  104. package/dist/cli/chunk-mey3vksj.js +0 -2
  105. package/dist/cli/chunk-nv8eyjep.js +0 -3
  106. package/dist/cli/chunk-ve76p2mx.js +0 -3
  107. package/dist/cli/chunk-wfgrn8p2.js +0 -4
  108. package/dist/cli/chunk-xmjknfy2.js +0 -7
  109. package/dist/web/assets/index-Dc0Khk0q.js +0 -149
  110. package/dist/web/assets/index-T8cCSdnk.css +0 -1
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- import{PER_SESSION,classifyTier,describeTierBlock,isAutoRoutingTier,listFiles,liveSourceProtocol,loadPreviewData,planWorktreeHandoff,readAutoRoutingTable,readClassifierConfig,resolveBase,runWorktreeGit,screenErrorLine,searchTasks,statusFiles,statusFilesBranch,taskGroupOf,tierBlock}from"./chunk-k2j0tg1h.js";import{ensurePluginEnginesLoaded,registeredEngineIds}from"./chunk-kdnk8m5j.js";import{resolveActiveTaskId}from"./chunk-qcjkactr.js";import{listPaneLaunches}from"./chunk-rjfdzjhj.js";import{detectEngineStatus}from"./chunk-zpxpwgds.js";import{resolveEditorLaunch}from"./chunk-agvtzads.js";import{DeviceLogin,GH_ENV,GITHUB_HOST,runTool,spawnGhLogin}from"./chunk-pm91e38v.js";import{PLACEHOLDER_TASK_TITLE,SHELL_TASK_TITLE,deriveTitleFromPrompt}from"./chunk-2xsxcdp6.js";import{ENGINE_ACTIVITY_KINDS,TASK_ACTIVITY_STATES}from"./chunk-17e2tgyg.js";import{DIRTY_WORKTREE_CODE,EMPTY_BRANCH_DIRTY_WORKTREE_CODE,canonicalize}from"./chunk-1vxtd8rh.js";import{canonicalEngineTab}from"./chunk-q8ffd1pc.js";import{ISSUE_STATUSES}from"./chunk-4hestsjp.js";import{ROW_TOKEN_MAX_TEXT,ROW_TOKEN_TONES}from"./chunk-atn06ncf.js";import{DEFAULT_FEEDBACK_CATEGORY_SLUG,submitFeedback}from"./chunk-2pfh3zm0.js";import{cloneDirFor,cloneRepo,dispatcherEnvPayload,findCheckouts,forwardVerb,isCleanCheckout,ownSession,parseRepoRef,pickCheckout,primaryCheckouts,ptyScopeOf,readOwnDispatcher,recordSend,replyTabOf,repoIdOf,repoIdentity,requireMachine,resolveDispatcherTab,verifiedSelfSession,withPeerProvenance}from"./chunk-p11emdb2.js";import{dedupeMachines,listMachines}from"./chunk-vytrhm50.js";import{normalizeRemoteUrl,resolvePtyOwner}from"./chunk-vp29jgbt.js";import{ulid}from"./chunk-qkehg2c1.js";import{ENGINE_NOT_OBSERVED_REASON,ENGINE_START_POLL_MS,ENGINE_START_PROBE_MS,deliverHostedPrompt,deliverToKey,ensurePtyHost,findEngineKey,killTaskSessions,listSessions,listSessionsOrNull,openAdmittedEngine,openPtyHost,outcomeFields,restoredTabLaunch,tabEngineBase,taskKeys}from"./chunk-s1xn6wav.js";import{awaitEngineProcess,enginePresence,findHostedEngineKey,hostedSessionFailureLine,isHostedTaskKey,listHostedSessions,openHostedSessionHost,readPersistedTerminalDefaultColors,sessionArgvNamesEngine,withDeliveryLock}from"./chunk-nv8eyjep.js";import{allocateTab,applyTabOps,closeTabsSnapshot,collapseSplit,findEditorTab,initialSplit,joinTaskTabs,leaves,markCliTabSession,patchTabSplit,publishCliTabSnapshot,readDaemonTabs,readTabsSnapshot,removeLeaf,renameLeaf,renameTabsSnapshot,seedCanonicalTab,seedShellHome,shellIdentityInput,splitActive,splitLeafPtyKey,tabPtyKeyFor,taskEngineArgv,unregisteredTabIds,wireSplit}from"./chunk-3hxm56h6.js";import{RemoteEngineLaunchError,buildEngineSessionLaunch,launchReposOf,repoEnv,trustEngineWorktree,trustExtraRepos}from"./chunk-qecygh3f.js";import{GENERIC_PROTOCOL,describePreset,engineLaunchArgv,engineResumeArgv,listEnginePresets,protocolEntry,resolveCommandProtocol,roveApiInvocation,sessionProtocol,withPinnedSessionId}from"./chunk-rjq38twt.js";import{ApiError,GROUP_MOD,VERB_GROUP_IDS,cliCaller,helpStep,promptMayHaveLanded,requireVerbInstalled,splitDaemonCode,verbInstalled}from"./chunk-r5j8fycq.js";import{isBuiltinModInstalled}from"./chunk-xfgqae9t.js";import{aliasKey,hostedKeyFor,isUnderKey,ptyScopeFor,unclaimedSessions}from"./chunk-g9ba25q5.js";import{resolveSessionShell}from"./chunk-tn9gtf18.js";import{foregroundEngineIn,parsePsSnapshot,psSnapshot}from"./chunk-5rgrde6b.js";import{engineEntry,getCapabilities,supportsStructuredHistory,vendorsWithEffortLevels,vendorsWithTurnReader}from"./chunk-4gjvmfa9.js";import{CONTRIB_ENGINE_IDS,installedEngineIds}from"./chunk-hzz020xx.js";import{terminalRows}from"./chunk-etfh8f4z.js";import{resolvePreferredVendor}from"./chunk-9rj2fx6v.js";import{addSavedRepo,getSavedRepos,isRemoteRepoKey}from"./chunk-rfp0ayra.js";import{readOnlyGitProcessEnv}from"./chunk-6xppe9bj.js";import{CURRENT_VERSION}from"./chunk-2wrt80ft.js";import{parseBoolLiteral}from"./chunk-zsfyj36g.js";import{expandTilde}from"./chunk-2r1fgng9.js";import{errorMessage}from"./chunk-2brn8e1y.js";import{loadStateFile}from"./chunk-32j6y5d1.js";import{DELIVERY_OUTCOMES,DELIVERY_SUMMARY_MAX_CHARS,MAX_DELIVERIES_PER_TASK,ROUTINE_RESPONSE_MAX_CHARS,SHOULD_RUN_SKIP_EXIT_CODE,SKIP_REASONS,deliveryFooter}from"./chunk-qzzmd63x.js";import{ROVE_API_VERSION}from"./chunk-96kvwzj8.js";import{MULTI_REPO_CAP,canonicalPrUrl,gitRepoOf,lineageRootId,repoNameOf,rollupPr,taskCwd,taskRepos,todoCount,uniqueRepoName,workspaceCapabilities}from"./chunk-x1jxxmtc.js";import{DEFAULT_TASK_VENDOR}from"./chunk-xy8be792.js";import{ALL_VENDORS,DEFAULT_VENDOR,coerceVendorId}from"./chunk-h2m7em1e.js";import{homeDir}from"./chunk-z1nj22va.js";import{ROVE_PRODUCT_NAME}from"./chunk-zt7wfnva.js";import{pathWithin,samePath}from"./chunk-pn5ygqn6.js";import{defaultDaemonSocketPath}from"./chunk-eqdbfgq5.js";import{__require}from"./chunk-91sykzqq.js";import{readFileSync as readFileSync2}from"fs";import{resolve as resolve2}from"path";import{readFileSync}from"fs";import{isAbsolute,resolve}from"path";var FANOUT_CAP=10;function parsePositiveInt(raw){if(!/^\d+$/.test(raw.trim()))return;let n=Number.parseInt(raw,10);return Number.isSafeInteger(n)&&n>0?n:void 0}function parseNonNegativeInt(raw){if(!/^\d+$/.test(raw.trim()))return;let n=Number.parseInt(raw,10);return Number.isSafeInteger(n)&&n>=0?n:void 0}var FANOUT_STEP=helpStep("add"),F={repo:(required=!0)=>({name:"repo",type:"string",required,placeholder:"PATH",description:"Repo root (git toplevel). Relative paths resolve against $PWD."}),taskId:(required=!0)=>({name:"task-id",type:"string",required,placeholder:"ID",description:"Target task id (from `list` / `add`)."}),vendor:()=>({name:"vendor",type:"enum",values:ALL_VENDORS,placeholder:"V",description:"Engine vendor for the task."}),command:()=>({name:"command",type:"string",placeholder:"CMD",description:"Engine launch command, verbatim \u2014 an engine id from `engine-list` (e.g. claude) or a full command line (e.g. 'codex --search'). Unvalidated: probe an unfamiliar engine's flags with `<cmd> --help` first. Omitted = the repo's default engine."}),title:()=>({name:"title",type:"string",placeholder:"T",description:"Human task title."}),prompt:(required,desc)=>({name:"prompt",type:"string",required,placeholder:"TEXT",description:required?`${desc} Required unless --prompt-file is given.`:desc}),promptFile:()=>({name:"prompt-file",type:"string",placeholder:"PATH",description:"Read the prompt from this file instead of --prompt (`-` = stdin). Use it whenever the text has backticks, $vars, or quotes you don't want the shell to touch. Exactly one of --prompt / --prompt-file."})},REPEAT_SEP="\x00";function setFlag(flags,key,value,repeatable){let prev=flags.get(key);flags.set(key,repeatable.has(key)&&prev!==void 0?`${prev}${REPEAT_SEP}${value}`:value)}function parseFlags(argv,booleanFlags=new Set,repeatableFlags=new Set){let flags=new Map,pretty=!1,help=!1;for(let i=0;i<argv.length;i++){let arg=argv[i];if(!arg.startsWith("--")&&arg!=="-h")throw new ApiError(`unexpected positional arg: ${arg}`,"BAD_FLAG");if(arg==="-h"){help=!0;continue}let eq=arg.indexOf("=");if(eq!==-1){let key2=arg.slice(2,eq),value=arg.slice(eq+1);if(key2==="envelope")continue;if(key2==="pretty")pretty=value!=="false"&&value!=="0";else if(key2==="help")help=value!=="false"&&value!=="0";else setFlag(flags,key2,value,repeatableFlags);continue}let key=arg.slice(2);if(key==="envelope")continue;if(key==="pretty"){pretty=!0;continue}if(key==="help"){help=!0;continue}if(booleanFlags.has(key)){let next2=argv[i+1];if(next2!==void 0&&parseBoolLiteral(next2)!==void 0)flags.set(key,next2),i+=1;else flags.set(key,"true");continue}let next=argv[i+1];if(next===void 0||next.startsWith("--"))throw new ApiError(`flag --${key} requires a value`,"BAD_FLAG");setFlag(flags,key,next,repeatableFlags),i+=1}return{flags,pretty,help}}function validateAgainstSpec(verb,flags){let known=new Set(verb.flags.map((f)=>f.name));for(let key of flags.keys())if(!known.has(key))throw new ApiError(`unknown flag --${key} for "${verb.name}"`,"BAD_FLAG",helpStep(verb.name));for(let f of verb.flags){let satisfied=flags.get(f.name)||f.name==="prompt"&&flags.get("prompt-file")||verb.name==="add"&&f.name==="repo"&&(flags.get("fork")||flags.get("no-worktree")||flags.get("isolate-in"));if(f.required&&!satisfied)throw new ApiError(`--${f.name} is required for "${verb.name}"`,"MISSING_FLAG",helpStep(verb.name));if(f.type==="enum"&&f.values){let raw=flags.get(f.name);if(raw!==void 0&&!f.values.includes(raw)){if(f.name==="vendor"&&registeredEngineIds().includes(raw))continue;throw new ApiError(`--${f.name} must be one of ${f.values.join(", ")}`,"BAD_FLAG",helpStep(verb.name))}}if(f.type==="int"){let raw=flags.get(f.name);if(raw!==void 0&&parsePositiveInt(raw)===void 0)throw new ApiError(`--${f.name} must be a positive integer`,"BAD_FLAG")}if(f.type==="uint"){let raw=flags.get(f.name);if(raw!==void 0&&parseNonNegativeInt(raw)===void 0)throw new ApiError(`--${f.name} must be a non-negative integer`,"BAD_FLAG")}}}class VerbArgs{verb;flags;caller;constructor(verb,flags,caller=cliCaller()){this.verb=verb;this.flags=flags;this.caller=caller}resolvePath(name,raw){let expanded=expandTilde(raw);if(isAbsolute(expanded))return expanded;if(this.caller.cwd===void 0)throw new ApiError(`--${name} must be an absolute path: this caller has no working directory`,"BAD_FLAG");return resolve(this.caller.cwd,expanded)}spec(name){let f=this.verb.flags.find((s)=>s.name===name);if(!f)throw Error(`internal: --${name} is not declared on verb "${this.verb.name}"`);return f}str(name){this.spec(name);let v=this.flags.get(name);return v&&v.length>0?v:void 0}present(name){return this.spec(name),this.flags.get(name)!==void 0}list(name){if(!this.spec(name).repeatable)throw Error(`internal: --${name} is not repeatable on verb "${this.verb.name}"`);let raw=this.flags.get(name);return raw===void 0?[]:raw.split(REPEAT_SEP).filter((v)=>v.length>0)}promptText(){if(this.promptMemo!==void 0)return this.promptMemo.value;let value=this.readPromptText();return this.promptMemo={value},value}promptMemo;readPromptText(){let inline=this.verb.flags.some((f)=>f.name==="prompt")?this.str("prompt"):void 0,file=this.str("prompt-file");if(inline!==void 0&&file!==void 0)throw new ApiError("pass --prompt or --prompt-file, not both","BAD_FLAG",helpStep(this.verb.name));if(file===void 0)return inline;if(file==="-"&&!this.caller.cli)throw new ApiError("--prompt-file - reads stdin, which this caller cannot send; pass --prompt","BAD_FLAG");let text=readFileSync(file==="-"?0:this.resolvePath("prompt-file",file),"utf8");if(text.trim().length===0)throw new ApiError(`--prompt-file ${file} is empty`,"BAD_FLAG");return text}require(name){let v=this.str(name);if(v===void 0)throw new ApiError(`--${name} is required`,"MISSING_FLAG");return v}enumOf(name){let f=this.spec(name),v=this.str(name);if(v===void 0)return;if(f.values&&!f.values.includes(v))throw new ApiError(`--${name} must be one of ${f.values.join(", ")}`,"BAD_FLAG",helpStep(this.verb.name));return v}requireEnum(name){return this.require(name),this.enumOf(name)}vendor(){let value=this.str("vendor");if(value===void 0)return;let builtins=this.spec("vendor").values??ALL_VENDORS;if(builtins.includes(value))return value;if(registeredEngineIds().includes(value))return value;throw new ApiError(`--vendor must be a built-in (${builtins.join(", ")}) or a registered engine id \u2014 see \`engine-list\``,"BAD_FLAG",helpStep(this.verb.name))}bool(name){this.spec(name);let raw=this.str(name);if(raw===void 0)return;let value=parseBoolLiteral(raw);if(value===void 0)throw new ApiError(`--${name} must be a boolean (true/false)`,"BAD_FLAG");return value}int(name){this.spec(name);let raw=this.str(name);if(raw===void 0)return;let n=parsePositiveInt(raw);if(n===void 0)throw new ApiError(`--${name} must be a positive integer`,"BAD_FLAG");return n}nonNegativeInt(name){this.spec(name);let raw=this.str(name);if(raw===void 0)return;let n=parseNonNegativeInt(raw);if(n===void 0)throw new ApiError(`--${name} must be a non-negative integer`,"BAD_FLAG");return n}path(name){let v=this.str(name);return v===void 0?void 0:this.resolvePath(name,v)}requirePath(name){return this.resolvePath(name,this.require(name))}requireRepo(name){return this.repoPath(name,this.require(name))}repoPath(name,raw){return isRemoteRepoKey(raw)?raw:this.resolvePath(name,raw)}}function parseAgentsSpec(spec){let out=[];for(let part of spec.split(",")){let trimmed=part.trim();if(!trimmed)continue;let colon=trimmed.indexOf(":");if(colon===-1)throw new ApiError(`--agents entry "${trimmed}" must be engine:count`,"BAD_FLAG",FANOUT_STEP);let vendor=trimmed.slice(0,colon);if(!ALL_VENDORS.includes(vendor)&&!registeredEngineIds().includes(vendor))throw new ApiError(`--agents engine "${vendor}" must be a built-in (${ALL_VENDORS.join(", ")}) or a registered engine id \u2014 see \`engine-list\``,"BAD_FLAG",FANOUT_STEP);let count=parsePositiveInt(trimmed.slice(colon+1));if(count===void 0)throw new ApiError(`--agents count for "${vendor}" must be a positive integer`,"BAD_FLAG",FANOUT_STEP);if(out.length+count>FANOUT_CAP)throw new ApiError(`--agents requests ${out.length+count} agents, exceeds the cap of ${FANOUT_CAP}`,"BAD_FLAG",FANOUT_STEP);for(let i=0;i<count;i++)out.push(vendor)}if(out.length===0)throw new ApiError('--agents specified no agents (e.g. "claude:2,codex:1")',"BAD_FLAG",FANOUT_STEP);return out}function buildCountPlan(count,vendor){if(count>FANOUT_CAP)throw new ApiError(`--count ${count} exceeds the parallel cap of ${FANOUT_CAP} \u2014 spawn in batches`,"BAD_FLAG",FANOUT_STEP);return Array(count).fill(vendor)}function daemonOf(ctx){if(!ctx.client)throw new ApiError("daemon required","BAD_DAEMON");return ctx.client}async function repoFilter(runtime,repoFlag,repos){let target=await runtime.resolveRepoRoot(repoFlag);if(!await runtime.isUsableRepo(target))throw new ApiError(`--repo ${repoFlag} is not a readable git repository (resolved to ${target}) \u2014 the project may have moved or been deleted; \`rove api list\` still shows its tasks`,"REPO_UNRESOLVABLE");let resolved=new Map,unresolvable=new Set;for(let repo of new Set(repos)){let root=await runtime.resolveRepoRoot(repo);if(resolved.set(repo,root),!samePath(root,target)&&!await runtime.isUsableRepo(root))unresolvable.add(repo)}return{target,matches:(repo)=>samePath(resolved.get(repo),target),unresolvableRepos:[...unresolvable].sort()}}async function simpleRpc(ctx,name,payload){let request=name;return daemonOf(ctx).request(request,payload)}async function handlePtyList(){let[{RoveDaemonClient},{defaultPtyHostSocketPath}]=await Promise.all([import("./chunk-fq2pefc5.js"),import("./chunk-1qvrn21x.js")]),client=new RoveDaemonClient(defaultPtyHostSocketPath());try{return await client.connect(),await client.request("pty.list",{})}catch{return{sessions:null}}finally{client.close()}}function readJsonBody(source){let raw=readFileSync2(source==="-"?0:resolve2(process.cwd(),expandTilde(source)),"utf8"),parsed;try{parsed=JSON.parse(raw)}catch{throw new ApiError(`--json ${source} is not valid JSON`,"BAD_FLAG")}if(!parsed||typeof parsed!=="object"||Array.isArray(parsed))throw new ApiError("--json must be an object: {delivery, outcome, summary, evidence?, next?}","BAD_FLAG");let body=parsed,evidence=body.evidence;if(evidence!==void 0&&(!Array.isArray(evidence)||evidence.some((e)=>typeof e!=="string")))throw new ApiError("evidence must be an array of strings","BAD_FLAG");for(let key of["delivery","outcome","summary","next"])if(body[key]!==void 0&&typeof body[key]!=="string")throw new ApiError(`${key} must be a string`,"BAD_FLAG");return body}var FIELD_FLAGS=["delivery","outcome","summary","next"];function readBody(ctx){let json=ctx.args.str("json"),evidence=ctx.args.list("evidence");if(json===void 0)return{delivery:ctx.args.str("delivery"),outcome:ctx.args.str("outcome"),summary:ctx.args.str("summary"),evidence,next:ctx.args.str("next")};if(FIELD_FLAGS.some((f)=>ctx.args.str(f)!==void 0)||evidence.length>0)throw new ApiError("pass --json OR the individual flags, not both","BAD_FLAG");return readJsonBody(json)}async function report(ctx){let body=readBody(ctx);if(!body.delivery)throw new ApiError("--delivery is required","MISSING_FLAG");if(!body.outcome||!DELIVERY_OUTCOMES.includes(body.outcome))throw new ApiError(`--outcome must be one of ${DELIVERY_OUTCOMES.join("|")}`,"MISSING_FLAG");if(!body.summary?.trim())throw new ApiError("--summary is required","MISSING_FLAG");if(body.summary.length>DELIVERY_SUMMARY_MAX_CHARS)throw new ApiError(`summary is ${body.summary.length} characters; the cap is ${DELIVERY_SUMMARY_MAX_CHARS}`,"RESPONSE_TOO_LARGE",{hint:"Summarize: lead with the conclusion, put the detail in --evidence (commits, PR urls, paths)."});let res=await simpleRpc(ctx,"delivery.report",{deliveryId:body.delivery,outcome:body.outcome,summary:body.summary,...body.evidence?.length?{evidence:body.evidence}:{},...body.next?{next:body.next}:{}});if(res.delivery===null)throw new ApiError(`no delivery ${body.delivery}`,"DELIVERY_NOT_FOUND",{hint:"Copy the id from the [ROVE] report line of the prompt you were given; `deliveries --task-id <id>` lists them."});return res}var REPORT_VERB={name:"report",group:"drive",summary:`Close the report a delivered prompt asked for. Run it as the LAST step of the turn, before you stop: a turn that ends without it is flagged \`report_missing\` in the owner's Inbox. --delivery is the id in the prompt's [ROVE] / [ROVE ROUTINE] line. Re-reporting replaces the earlier report; a late report on a flagged delivery clears the flag. --summary \u2264 ${DELIVERY_SUMMARY_MAX_CHARS} chars; put commits, PR urls, test results and paths in repeatable --evidence, and what the next tick should do in --next. Use --json - (stdin) for text with quotes or backticks.`,flags:[{name:"delivery",type:"string",placeholder:"ID",description:"Delivery id, e.g. d-41."},{name:"outcome",type:"enum",values:DELIVERY_OUTCOMES,placeholder:"O",description:"done = finished what was asked; partial = some of it; blocked = needs the owner or an external fix; failed = tried and could not."},{name:"summary",type:"string",placeholder:"TEXT",description:"What was done (or why not)."},{name:"evidence",type:"string",placeholder:"X",repeatable:!0,description:"A commit, PR url, test command + result, or file path. Repeat the flag for several."},{name:"next",type:"string",placeholder:"TEXT",description:"What the next tick should do."},{name:"json",type:"string",placeholder:"PATH",description:"Read {delivery, outcome, summary, evidence?, next?} as JSON from this file (`-` = stdin) instead of the flags."}],handler:report},DELIVERIES_VERB={name:"deliveries",group:"read",summary:`A task's armed deliveries, newest first (newest ${MAX_DELIVERIES_PER_TASK} are kept): status awaiting|reported|missing|skipped, the report (outcome, summary, evidence, next) and, for missing, why (cause turn-complete|turn-failed|engine-dead|stalled|interrupted|session-end|rate-limited + note; rate-limited raises no Inbox row and does not block) plus dismissedAt once the owner dismissed it (a missing row blocks \`send --if-runnable\` and routine ticks until a late report or that dismissal). A skipped row is a \`send --if-runnable\` tick should-run refused: \`skipped\` is the verdict {reason, detail?, until?, gateIds?}, \`repeats\` how many identical refusals (same tab, same reason) the one row stands for. The per-tick log a loop prompt should read instead of a growing MEMO file.`,flags:[F.taskId(!0),{name:"limit",type:"int",placeholder:"N",description:"At most N entries (default: all retained)."}],handler:async(ctx)=>simpleRpc(ctx,"delivery.list",{taskId:ctx.args.require("task-id"),...ctx.args.int("limit")!==void 0?{limit:ctx.args.int("limit")}:{}})};async function listAllEnginePresets(){let presets=await enginePresetsInList(),byProtocol=new Map,modelsOf=(protocol)=>{let pending=byProtocol.get(protocol);if(!pending){let list=engineEntry(protocol).listModels;pending=list?list().catch(()=>null):Promise.resolve(null),byProtocol.set(protocol,pending)}return pending};return Promise.all(presets.map(async(preset)=>({...preset,models:await modelsOf(preset.protocol)})))}async function enginePresetsInList(){ensurePluginEnginesLoaded();let presets=[...listEnginePresets()],seen=new Set(presets.map((p)=>p.id));for(let id of await installedEngineIds())if(!seen.has(id))presets.push(describePreset(id));return presets}async function engineListIds(){return(await enginePresetsInList()).map((p)=>p.id)}var ENGINE_LIST_VERB={name:"engine-list",group:"discover",summary:`List every engine Rove can launch \u2014 built-ins, registered presets, the shipped contrib engines whose CLI is on PATH (${CONTRIB_ENGINE_IDS.join(", ")}), and engines contributed by enabled plugins \u2014 each with its RAW launch command, exactly as it runs. Copy one into \`add --command\` / \`send --tab new --command\` verbatim, or edit its flags first. \`protocol\` is the adapter Rove speaks to it (history, trust, delivery); \`generic\` = none, which still runs fine but loses transcript reads. \`models\` is what the engine can name for \`--model\` (suggestions, not a closed set); null = Rove cannot list them for this engine. Returns { engines }.`,flags:[],offline:!0,handler:async()=>({engines:await listAllEnginePresets()})};function taskEngine(task){let command=task.command?.trim();if(command){let resolved=resolveCommandProtocol(command);if(resolved!==GENERIC_PROTOCOL)return resolved}return sessionProtocol(coerceVendorId(task.vendor))}function assertEngineAcceptsEffort(engine,level,recover){let levels=engineEntry(engine).effortLevels??[];if(levels.length===0)throw new ApiError(`engine ${engine} declares no reasoning effort levels`,"BAD_EFFORT",{engine,hint:`Only engines with declared reasoning levels accept one (${vendorsWithEffortLevels().join(", ")} today). Check the task's engine with \`get-task\`.`,nextCommandArgs:[...recover]});if(!levels.includes(level))throw new ApiError(`engine ${engine} does not accept effort level ${JSON.stringify(level)} \u2014 it declares ${levels.join(", ")}`,"BAD_EFFORT",{engine,levels,hint:`Pass one of: ${levels.join(", ")}.`,nextCommandArgs:[...recover]})}function assertEngineAcceptsModel(engine,model,recover){if(engineEntry(engine).modelArgv)return;throw new ApiError(`engine ${engine} declares no model flag \u2014 it does not accept a model`,"BAD_MODEL",{engine,hint:"Only engines that declare a model flag accept one (claude, codex, kimi, pi, omp today); `engine-list` shows `models` per engine.",nextCommandArgs:[...recover]})}function vendorToRecord(task,engine){let recorded=coerceVendorId(task.vendor);return sessionProtocol(recorded)===engine?recorded:engine}async function readRunnability(daemon,taskId){try{return await daemon.request("task.runnability",{taskId})}catch{return}}async function shouldRun(ctx){let tab=ctx.args.str("tab");if(tab&&tab!=="new"&&!/^tab-[A-Za-z0-9-]+$/.test(tab))throw new ApiError(`--tab must be "new" or a tab id like tab-2 (got ${JSON.stringify(tab)})`,"BAD_TAB");let verdict=await daemonOf(ctx).request("task.runnability",{taskId:ctx.args.require("task-id"),...tab?{tabId:tab}:{}});if(!verdict.run)throw new ApiError(`skip: ${verdict.reason}`,"SKIPPED",{...verdict});return verdict}var SHOULD_RUN_VERB={name:"should-run",group:"read",summary:`Would a tick sent to this task be used? Prints the verdict {run:true} or {run:false, reason, detail?, until?, gateIds?} and exits 0 on run, ${SHOULD_RUN_SKIP_EXIT_CODE} on skip (the verdict is still on stdout). reason, first match wins: ${SKIP_REASONS.join(" | ")}. \`awaiting-owner\` = an open gate (answer or withdraw it), \`report-missing\` = the last delivery ended unreported (late \`report\`, or dismiss its Inbox row), \`quota\` clears itself at \`until\`, \`engine-error\` / \`engine-dead\` clear on the engine's next turn or a restart, \`awaiting-permission\` = the engine waits on an approval, \`busy\` = a turn is running or the last delivery still awaits its report. A task whose only gate is answered-but-undelivered is runnable: the next tick carries the answer. Without --tab the whole task is judged and a dead engine is not a skip (\`send\` revives it); --tab tab-N judges that tab and a dead engine is \`engine-dead\`; --tab new judges only task-level state. To check and send in one step use \`send --if-runnable\`.`,flags:[F.taskId(!0),{name:"tab",type:"string",required:!1,placeholder:"TAB",description:'Judge exactly this tab (e.g. tab-2), or "new" for a tab about to be created. Omitted = the whole task.'}],handler:shouldRun};async function attentionDismiss(ctx){let delivery=ctx.args.require("delivery");if((await daemonOf(ctx).request("attention.dismiss",{deliveryId:delivery})).found===!1)throw new ApiError(`no delivery ${delivery}`,"DELIVERY_NOT_FOUND",{hint:"`deliveries --task-id <id>` lists a task's delivery ids; the Inbox row names it as d-N."});return{ok:!0,delivery,cleared:!0}}var ATTENTION_DISMISS_VERB={name:"attention-dismiss",group:"drive",summary:'Dismiss a `report_missing` Inbox row by its delivery id (d-N): the owner\'s deliberate "seen, carry on" for a tick that ended without a report. It unblocks `should-run` for the task. Opening or visiting the row never does this, and a late `report` clears it on its own. Idempotent; an unknown id is DELIVERY_NOT_FOUND.',flags:[{name:"delivery",type:"string",required:!0,placeholder:"ID",description:"Delivery id, e.g. d-41."}],handler:attentionDismiss};import path from"path";var TASK_STATUSES=["backlog","in_progress","in_review","done","canceled","error"];var TASK_FIELDS=["title","branch","parent","project","command","model","effort","pinned","kept","require-report","auto-fix-ci","status"],REPORT_FLAGS=["report-branch","report-pr","report-summary"];async function renameTab(ctx,taskId,tabId){let title=ctx.args.str("title"),others=[...TASK_FIELDS,...REPORT_FLAGS].filter((f)=>f!=="title"&&ctx.args.str(f)!==void 0);if(title===void 0||others.length>0)throw new ApiError("--tab only renames a tab: pass --title and no other field","BAD_FLAG",{hint:"Update the task's own fields in a separate call without --tab."});let daemon=daemonOf(ctx);if(!await renameTabsSnapshot(daemon,taskId,tabId,title))throw new ApiError(`task ${taskId} has no tab ${tabId}`,"TAB_NOT_FOUND",{hint:"`get-task` lists the addressable tab ids in .tabs[].id",nextCommandArgs:["api","get-task","--task-id",taskId]});let reply=await daemon.request("terminalTab.rename",{taskId,tabId,title});return{ok:!0,taskId,tabId,title,renamed:!0,clients:reply.clients??0}}async function resolveTarget(ctx,verb="update"){let explicit=ctx.args.str("task-id");if(explicit)return explicit;let self=await ownSession(daemonOf(ctx));if(!self)throw new ApiError("no --task-id given and the caller is not a verified rove session \u2014 pass --task-id ID","MISSING_TARGET",helpStep(verb));return self.taskId}async function projectRoot(ctx){let raw=ctx.args.str("project")?.trim();if(raw===void 0)return;if(raw==="")throw new ApiError("--project cannot be empty \u2014 pass the repo's path","BAD_FLAG");let root=await ctx.runtime.resolveRepoRoot(path.resolve(ctx.caller.cwd??process.cwd(),raw));if(!await ctx.runtime.isUsableRepo(root))throw new ApiError(`--project ${raw} is not inside a git repository`,"NOT_A_GIT_REPO",{hint:"clone or `git init` the project first, then pass its path"});return root}var NO_PARENT="none";async function reparentGroup(ctx,groupId){let parent=ctx.args.str("parent"),stray=["task-id","tab",...TASK_FIELDS.filter((f)=>f!=="parent"),...REPORT_FLAGS].filter((f)=>ctx.args.str(f)!==void 0);if(parent===void 0||parent===""||stray.length>0)throw new ApiError(`--group moves a whole fan-out round and takes only --parent ID (or \`none\`)${stray.length>0?` \u2014 drop ${stray.map((f)=>`--${f}`).join(", ")}`:""}`,"BAD_FLAG",helpStep("update"));let daemon=daemonOf(ctx),{tasks}=await daemon.request("task.list"),members=tasks.filter((t)=>t.groupId===groupId);if(members.length===0)throw new ApiError(`no tasks in group ${groupId}`,"TASK_NOT_FOUND",{hint:"the groupId is the one `add --count` returned; any surviving sibling's `.groupId` in `list` recovers it",nextCommandArgs:["api","list"]});if(members.some((t)=>t.id===parent))throw new ApiError(`${parent} is itself a member of group ${groupId} \u2014 a round cannot move under one of its own`,"PARENT_IN_GROUP",{hint:"pick a parent outside the round, or move the members one by one with --task-id"});let parentTaskId=parent===NO_PARENT?null:parent,replyTab=parentTaskId===null?void 0:await replyTabOf(ctx.runtime,daemon,parentTaskId),results=[],failures=0;for(let task of members)try{let moved=await simpleRpc(ctx,"task.setParent",{taskId:task.id,parentTaskId,...replyTab?{replyTab}:{}});results.push({taskId:task.id,status:"moved",...moved})}catch(err){failures++;let message=errorMessage(err),coded=err instanceof ApiError?{code:err.code,rest:message}:splitDaemonCode(message);results.push({taskId:task.id,status:"failed",error:coded?.rest??message,...coded?{code:coded.code}:{}})}return{ok:failures===0,groupId,count:members.length,failures,results}}async function updateTask(ctx){let group=ctx.args.str("group");if(group!==void 0)return reparentGroup(ctx,group);let tabId=ctx.args.str("tab"),taskId=tabId!==void 0?ctx.args.require("task-id"):await resolveTarget(ctx);if(tabId!==void 0)return renameTab(ctx,taskId,tabId);let title=ctx.args.str("title"),branch=ctx.args.str("branch"),parent=ctx.args.str("parent"),command=ctx.args.str("command")?.trim(),model=ctx.args.str("model")?.trim(),effort=ctx.args.str("effort")?.trim(),pinned=ctx.args.bool("pinned"),kept=ctx.args.bool("kept"),requireReport=ctx.args.enumOf("require-report"),autoFixCi=ctx.args.enumOf("auto-fix-ci"),status=ctx.args.enumOf("status"),report2={reportBranch:ctx.args.str("report-branch"),reportPr:ctx.args.int("report-pr"),reportSummary:ctx.args.str("report-summary")},reporting=Object.values(report2).some((v)=>v!==void 0);if(TASK_FIELDS.every((f)=>ctx.args.str(f)===void 0)&&!reporting)throw new ApiError(`nothing to update \u2014 pass at least one of ${TASK_FIELDS.map((f)=>`--${f}`).join(", ")}`,"BAD_FLAG",{nextCommandArgs:["api","update","--help"]});if(reporting&&status===void 0)throw new ApiError("--report-* is recorded with a status: pass --status too","BAD_FLAG",{hint:"A worker reporting delivery usually passes --status in_review (or done)."});if(command==="")throw new ApiError("--command cannot be empty","BAD_FLAG");if(parent==="")throw new ApiError("--parent cannot be empty \u2014 pass a task id, or `none` for top level","BAD_FLAG");let daemon=daemonOf(ctx),protocol=command?resolveCommandProtocol(command):void 0,project=await projectRoot(ctx),task,engine;if(model!==void 0||effort!==void 0){task=(await daemon.request("task.get",{taskId})).task,engine=taskEngine(command?{...task,command}:task);let recover=["api","get-task","--task-id",taskId];if(model!==void 0)assertEngineAcceptsModel(engine,model,recover);if(effort!==void 0)assertEngineAcceptsEffort(engine,effort,recover)}let applied=[],step=async(fields,method,payload)=>{let reply;try{reply=await simpleRpc(ctx,method,{taskId,...payload})}catch(err){if(applied.length===0)throw err;let message=err instanceof Error?err.message:String(err),code=err instanceof ApiError?err.code:"PARTIAL_UPDATE";throw new ApiError(`--${fields.join("/--")} failed after ${applied.join(", ")} applied: ${message}`,code,{applied:[...applied],failed:fields})}return applied.push(...fields),reply},reparented;if(parent!==void 0){let parentTaskId=parent===NO_PARENT?null:parent,replyTab=parentTaskId===null?void 0:await replyTabOf(ctx.runtime,daemon,parentTaskId);reparented=await step(["parent"],"task.setParent",{parentTaskId,...replyTab?{replyTab}:{}})}let regrouped;if(project!==void 0)regrouped=await step(["project"],"task.moveToProject",{repo:project});if(branch!==void 0)await step(["branch"],"task.setBranch",{branch});if(command)await step(["command"],"task.setCommand",{command,vendor:protocol});if(task&&engine){let recorded=vendorToRecord(protocol?{...task,vendor:protocol}:task,engine),fields=[...model!==void 0?["model"]:[],...effort!==void 0?["effort"]:[]];await step(fields,"task.setVendor",{vendor:recorded,model,effort})}if(title!==void 0)await step(["title"],"task.rename",{title});if(pinned!==void 0)await step(["pinned"],"task.pin",{pinned});if(kept!==void 0)await step(["kept"],"task.keep",{kept});if(requireReport!==void 0)await step(["require-report"],"task.requireReport",{required:requireReport==="on"});if(autoFixCi!==void 0)await step(["auto-fix-ci"],"task.autoFixCi",{enabled:autoFixCi==="on"});if(status!==void 0)await step(["status"],"task.status",{status,...report2});return{ok:!0,taskId,updated:applied,...reparented?{parent:reparented}:{},...regrouped?{project:{repo:regrouped.repo,moved:regrouped.moved,note:"regrouped only \u2014 the PTYs, cwd, worktree and branch stay where they were"}}:{},...command?{command,protocol,...protocol===GENERIC_PROTOCOL?{generic:!0}:{}}:{},...engine?{engine}:{}}}var EDIT_VERBS=[{name:"update",group:"edit",summary:"Change a task's metadata \u2014 any combination of title, branch (git branch -m if materialized, else recorded), parent, engine --command / --model / --effort (take effect on the next session rebuild), pin, and status + worker report \u2014 in one call. The flags are checked before anything is written; then they apply in order parent \u2192 branch \u2192 engine \u2192 title/pin/status, and a refusal mid-way names what already applied (`applied`). --parent ID re-parents the task without touching its engine, tabs, worktree or branch: the stored parent is ID's top-level ancestor (children never nest, as with `add` from a child; the task's own children come along, listed in `.parent.moved`) and the task's reply address becomes ID itself, so its bare `send` reaches ID (the caller's own tab when run from inside ID, else ID's live engine tab, else tab-1). `--parent none` makes the task top level and drops a reply address that pointed into its old tree. A parent must live on the same machine as the task. Archiving the stored parent, or `delete --cascade` on it, takes the task along. --group G --parent ID moves a whole fan-out round instead and takes nothing else. With --tab, renames that Terminal Tab instead (the API twin of the TUI's f2) and takes only --title. The status is a cosmetic LABEL: `--status canceled` closes, stops and cleans up nothing \u2014 `delete` ends a task. The --report-* fields record what the WORKER CLAIMS it delivered (`.report` on `get-task` / `collect`), deliberately apart from `.prStatus`, which the daemon polled from the forge; they merge onto any previous report and need --status. Returns { updated } \u2014 the fields written, in order.",flags:[{...F.taskId(!1),description:"Target task id (from `list` / `add`). Default: the calling session's own task, from the verified $ROVE_TASK_ID (an unverifiable env is MISSING_TARGET, never a guess)."},{name:"title",type:"string",placeholder:"T",description:"New task title (or, with --tab, the tab's name)."},{name:"tab",type:"string",placeholder:"TAB",description:"Rename this Terminal Tab (id from `get-task` .tabs[].id, e.g. tab-2) instead of the task; needs --task-id, and only --title may accompany it. TAB_NOT_FOUND when the task's snapshot names no such tab."},{name:"branch",type:"string",placeholder:"B",description:"New branch name."},{name:"parent",type:"string",placeholder:"ID|none",description:"Move the task under this task (its engine, tabs, worktree and branch stay as they are), or `none` to make it top level. Refusals: PARENT_NOT_FOUND, PARENT_IS_SELF, PARENT_CYCLE, TASK_NOT_MOVABLE, REMOTE_PARENT_UNSUPPORTED."},{name:"project",type:"string",placeholder:"PATH",description:"Move a top-level task (and its children) under the project at PATH (relative paths resolve against $PWD; any directory inside the repo names its main checkout), which becomes a project if it was not one. Only the grouping changes: the live session, tabs, cwd, worktree and branch stay exactly where they are, so a running engine keeps its conversation. Moving a worktree task back to the repo that cut it clears the link. Refusals: NOT_A_GIT_REPO, SAME_PROJECT, TASK_NOT_MOVABLE (a project's main task, a routine session or a multi-repo task), CHILD_NOT_MOVABLE (move its parent; children follow), REMOTE_MOVE_UNSUPPORTED."},{name:"group",type:"string",placeholder:"GROUPID",description:"With --parent: move every task of one fan-out round (the `groupId` that `add --count` returns). Takes nothing but --parent; a member's refusal is recorded in `results`, and a parent inside the round is PARENT_IN_GROUP."},{...F.command(),description:"New engine launch command \u2014 an engine id from `engine-list` or a full command line. The protocol Rove speaks to it is derived from it; the result reports which (`generic: true` when the command names no engine Rove knows)."},{name:"model",type:"string",placeholder:"MODEL",description:"Pin a model, passed to the engine VERBATIM in its own spelling (claude alias or id, codex slug, pi/omp pattern); `engine-list` .models are suggestions, not a closed list. BAD_MODEL when the engine declares no model flag."},{name:"effort",type:"string",placeholder:"LEVEL",description:"Reasoning effort level the task's engine declares (codex: none/low/medium/high/xhigh/max; pi, omp: off/minimal/low/medium/high/xhigh/max; claude: low/medium/high/xhigh/max). BAD_EFFORT names the accepted levels."},{name:"pinned",type:"bool",placeholder:"BOOL",description:"true pins the task to the top of the sidebar, false unpins."},{name:"kept",type:"bool",placeholder:"BOOL",description:"true marks the task as the attempt kept from a parallel round (`add --count` / `--agents`); false clears the mark. A flag only: it archives nothing, and other attempts of the round are untouched."},{name:"require-report",type:"enum",values:["on","off"],description:"on: every later `send` to this task arms a delivery the agent must close with `report --delivery <id>` (a turn that ends without one raises a `report_missing` Inbox row); off: back to plain sends."},{name:"auto-fix-ci",type:"enum",values:["on","off"],description:"on: when this task's PR checks go red on a new head sha and its engine is at rest, the daemon types the fix-CI prompt (with the failing logs) into the engine, at most once per sha and 3 times in all, then stops and raises an Inbox row; the count starts over when the checks pass. Never interrupts a running turn. off: stop and forget the count."},{name:"status",type:"enum",values:TASK_STATUSES,description:"New lifecycle label (cosmetic \u2014 see summary)."},{name:"report-branch",type:"string",placeholder:"B",description:"The branch the worker says holds the work (a CLAIM \u2014 `.branch` is the task's actual branch)."},{name:"report-pr",type:"int",placeholder:"N",description:"The PR number the worker says it opened (a CLAIM \u2014 `.prStatus.number` is what the daemon read from the forge)."},{name:"report-summary",type:"string",placeholder:"TEXT",description:"One line of what was delivered."}],handler:updateTask}];async function todo(ctx){let{args}=ctx,taskId=await resolveTarget(ctx,"todo"),add=args.str("add"),check=args.str("check"),uncheck=args.str("uncheck"),edit=args.str("edit"),text=args.str("text"),remove=args.str("remove"),notes=args.bool("clear-notes")===!0?"":args.str("notes");if(edit===void 0!==(text===void 0))throw new ApiError("--edit ID and --text T go together","BAD_FLAG");let ops=[...add!==void 0?[{op:"add",text:add}]:[],...check!==void 0?[{op:"check",id:check,done:!0}]:[],...uncheck!==void 0?[{op:"check",id:uncheck,done:!1}]:[],...edit!==void 0&&text!==void 0?[{op:"edit",id:edit,text}]:[],...remove!==void 0?[{op:"remove",id:remove}]:[]],after=await simpleRpc(ctx,"task.notes",{taskId,...notes!==void 0?{notes}:{},...ops.length>0?{ops}:{}}),count=todoCount(after.todos);return{ok:!0,taskId,notes:after.notes??"",todos:after.todos,done:count?.done??0,total:count?.total??0}}var TODO_VERB={name:"todo",group:"edit",summary:"Read or change a task's notes and checklist \u2014 the owner's own review list (shown in the web Files panel's Notes tab and as `done/total` on the task row). With no flag it lists them. Flags combine in one call and apply together, in this order: --add, --check, --uncheck, --edit/--text, --remove; one refused op (TODO_NOT_FOUND, TODO_EMPTY, TODO_TOO_LONG, TODO_LIMIT) changes nothing. A todo id is `t<N>` as listed. Default target: the calling session's own task. Returns { ok, taskId, notes, todos: [{ id, text, done }], done, total }.",flags:[{...F.taskId(!1),description:"Target task id (from `list` / `add`). Default: the calling session's own task."},{name:"add",type:"string",placeholder:"TEXT",description:"Add an unchecked todo (one line, up to 500 characters; 100 per task)."},{name:"check",type:"string",placeholder:"ID",description:"Mark todo ID done."},{name:"uncheck",type:"string",placeholder:"ID",description:"Mark todo ID not done."},{name:"edit",type:"string",placeholder:"ID",description:"Change todo ID's text to --text."},{name:"text",type:"string",placeholder:"TEXT",description:"The new text for --edit."},{name:"remove",type:"string",placeholder:"ID",description:"Delete todo ID."},{name:"notes",type:"string",placeholder:"TEXT",description:"Replace the task's free-text notes (up to 20000 characters); Use --clear-notes to empty them."},{name:"clear-notes",type:"bool",description:"Empty the free-text notes."}],handler:todo};var API_SCHEMA_VERSION=ROVE_API_VERSION,GLOBAL_FLAGS=[{name:"pretty",type:"bool",description:"Pretty-print stdout JSON."},{name:"help",type:"bool",description:"Show usage for the verb and exit."},{name:"envelope",type:"bool",description:"Print every outcome as one { apiVersion, ok, result | error } on stdout \u2014 what the daemon's and the bridge's api.invoke return."},{name:"machine",type:"string",placeholder:"ALIAS",description:"Run the verb in that registered machine's daemon (`rove machine list`); paths are paths there. A --task-id on another machine goes there without it."}];function displayValues(f){if(!f.values)return;if(f.name!=="vendor")return f.values;let registered=registeredEngineIds().filter((id)=>!f.values.includes(id));return registered.length>0?[...f.values,...registered]:f.values}function flagJson(f){let values=displayValues(f);return{name:f.name,type:f.type,required:f.required??!1,...values?{values}:{},...f.default!==void 0?{default:f.default}:{},...f.placeholder?{placeholder:f.placeholder}:{},...f.repeatable?{repeatable:!0}:{},description:f.description}}function installedVerbs(){return VERBS.filter(verbInstalled)}function installedGroups(){return Object.fromEntries(Object.entries(VERB_GROUPS).filter(([group])=>{let mod=GROUP_MOD[group];return mod===void 0||isBuiltinModInstalled(mod)}))}function verbSchema(v){return{name:v.name,group:v.group,summary:v.summary,offline:v.offline??!1,cliOnly:v.cliOnly??!1,flags:v.flags.map(flagJson)}}function schemaIndex(){let cliName=ROVE_PRODUCT_NAME;return{apiVersion:API_SCHEMA_VERSION,roveVersion:CURRENT_VERSION,hint:`Compact index. Drill into ONE verb: \`${cliName} api schema --verb <name>\` (or \`${cliName} api <verb> --help\`). One group: \`--group <g>\`. Whole spec: \`--all\`.`,groups:installedGroups(),verbs:installedVerbs().map((v)=>({name:v.name,group:v.group,summary:v.summary})),globalFlags:GLOBAL_FLAGS,aliases:VERB_ALIASES}}function groupSchema(group){let verbs=VERBS.filter((v)=>v.group===group);if(verbs[0])requireVerbInstalled(verbs[0]);if(verbs.length===0)throw new ApiError(`unknown group: ${group}. Groups: ${Object.keys(installedGroups()).join(", ")}`,"BAD_VERB",{hint:"that group does not exist \u2014 the compact index lists every group and the verbs in it",nextCommandArgs:["api","schema"]});return{group,verbs:verbs.map((v)=>({name:v.name,summary:v.summary}))}}function fullSchema(){return{apiVersion:API_SCHEMA_VERSION,roveVersion:CURRENT_VERSION,output:{success:"one JSON object on stdout, newline-terminated, exit 0",error:'{"error":{"message","code"}} on stderr, exit != 0',pretty:"--pretty indents stdout JSON",envelope:'--envelope prints {"apiVersion","ok","result"} or {"apiVersion","ok","error"} on stdout instead, exit codes unchanged'},transports:{cli:"rove api <verb> --flag value",daemon:'socket request "api.invoke" {verb, params, machine?} \u2192 the envelope; params are keyed by flag name',bridge:'WebSocket op "api.invoke" {verb, params, machine?} \u2192 result, or the full error object'},globalFlags:GLOBAL_FLAGS,aliases:VERB_ALIASES,groups:installedGroups(),verbs:installedVerbs().map(verbSchema)}}function flagSignature(verb){return verb.flags.map((f)=>{let meta=f.type==="enum"&&f.values?displayValues(f).join("|"):f.placeholder??(f.type==="bool"?"":"X"),core=meta?`--${f.name} ${meta}`:`--${f.name}`;return f.required?core:`[${core}]`}).join(" ")}function verbHelp(verb){let lines=[`${ROVE_PRODUCT_NAME} api ${verb.name} ${flagSignature(verb)}`.trimEnd(),"",verb.summary,""],alias=Object.entries(VERB_ALIASES).find(([,canon])=>canon===verb.name)?.[0];if(alias)lines.push(`Alias: ${alias}`,"");if(verb.flags.length>0){lines.push("Flags:");for(let f of verb.flags){let req=f.required?" (required)":f.repeatable?" (repeatable)":"",def=f.default!==void 0?` [default: ${f.default}]`:"",vals=f.type==="enum"&&f.values?` {${displayValues(f).join("|")}}`:"";lines.push(` --${f.name}${vals}${req}${def} ${f.description}`)}lines.push("")}return lines.push("Global: [--pretty] [--help] [--envelope] [--machine ALIAS]"),lines.join(`
2
+ import{PER_SESSION,classifyTier,describeTierBlock,isAutoRoutingTier,listFiles,liveSourceProtocol,loadPreviewData,planWorktreeHandoff,readAutoRoutingTable,readClassifierConfig,resolveBase,runWorktreeGit,screenErrorLine,searchTasks,statusFiles,statusFilesBranch,taskGroupOf,tierBlock}from"./chunk-tgv90744.js";import{ensurePluginEnginesLoaded,registeredEngineIds}from"./chunk-kdnk8m5j.js";import{resolveActiveTaskId}from"./chunk-qcjkactr.js";import{listPaneLaunches}from"./chunk-rjfdzjhj.js";import{detectEngineStatus}from"./chunk-zpxpwgds.js";import{resolveEditorLaunch}from"./chunk-agvtzads.js";import{DeviceLogin,GH_ENV,GITHUB_HOST,runTool,spawnGhLogin}from"./chunk-pm91e38v.js";import{PLACEHOLDER_TASK_TITLE,SHELL_TASK_TITLE,deriveTitleFromPrompt}from"./chunk-2xsxcdp6.js";import{ENGINE_ACTIVITY_KINDS,TASK_ACTIVITY_STATES}from"./chunk-17e2tgyg.js";import{DIRTY_WORKTREE_CODE,EMPTY_BRANCH_DIRTY_WORKTREE_CODE,canonicalize}from"./chunk-1vxtd8rh.js";import{canonicalEngineTab}from"./chunk-q8ffd1pc.js";import{ISSUE_STATUSES}from"./chunk-4hestsjp.js";import{ROW_TOKEN_MAX_TEXT,ROW_TOKEN_TONES}from"./chunk-atn06ncf.js";import{DEFAULT_FEEDBACK_KIND,FEEDBACK_KINDS,isFeedbackKind,submitFeedback}from"./chunk-01tghgf3.js";import{cloneDirFor,cloneRepo,dispatcherEnvPayload,findCheckouts,forwardVerb,isCleanCheckout,ownSession,parseRepoRef,pickCheckout,primaryCheckouts,ptyScopeOf,readOwnDispatcher,recordSend,replyTabOf,repoIdOf,repoIdentity,requireMachine,resolveDispatcherTab,verifiedSelfSession,withPeerProvenance}from"./chunk-y07cp9mb.js";import{dedupeMachines,listMachines}from"./chunk-vytrhm50.js";import{normalizeRemoteUrl,resolvePtyOwner}from"./chunk-vp29jgbt.js";import{ulid}from"./chunk-qkehg2c1.js";import{ENGINE_NOT_OBSERVED_REASON,ENGINE_START_POLL_MS,ENGINE_START_PROBE_MS,deliverHostedPrompt,deliverToKey,ensurePtyHost,findEngineKey,killTaskSessions,listSessions,listSessionsOrNull,openAdmittedEngine,openPtyHost,outcomeFields,restoredTabLaunch,tabEngineBase,taskKeys}from"./chunk-w6ft4djp.js";import{awaitEngineProcess,enginePresence,findHostedEngineKey,hostedSessionFailureLine,isHostedTaskKey,listHostedSessions,openHostedSessionHost,readPersistedTerminalDefaultColors,sessionArgvNamesEngine,withDeliveryLock}from"./chunk-vx44yxra.js";import{allocateTab,applyTabOps,closeTabsSnapshot,collapseSplit,findEditorTab,initialSplit,joinTaskTabs,leaves,markCliTabSession,patchTabSplit,publishCliTabSnapshot,readDaemonTabs,readTabsSnapshot,removeLeaf,renameLeaf,renameTabsSnapshot,seedCanonicalTab,seedShellHome,shellIdentityInput,splitActive,splitLeafPtyKey,tabPtyKeyFor,taskEngineArgv,unregisteredTabIds,wireSplit}from"./chunk-1a57ejnh.js";import{RemoteEngineLaunchError,buildEngineSessionLaunch,launchReposOf,repoEnv,trustEngineWorktree,trustExtraRepos}from"./chunk-qecygh3f.js";import{GENERIC_PROTOCOL,describePreset,engineLaunchArgv,engineResumeArgv,listEnginePresets,protocolEntry,resolveCommandProtocol,roveApiInvocation,sessionProtocol,withPinnedSessionId}from"./chunk-rjq38twt.js";import{ApiError,GROUP_MOD,VERB_GROUP_IDS,cliCaller,helpStep,promptMayHaveLanded,requireVerbInstalled,splitDaemonCode,verbInstalled}from"./chunk-r5j8fycq.js";import{isBuiltinModInstalled}from"./chunk-xfgqae9t.js";import{aliasKey,hostedKeyFor,isUnderKey,ptyScopeFor,unclaimedSessions}from"./chunk-g9ba25q5.js";import{resolveSessionShell}from"./chunk-tn9gtf18.js";import{foregroundEngineIn,parsePsSnapshot,psSnapshot}from"./chunk-5rgrde6b.js";import{engineEntry,getCapabilities,supportsStructuredHistory,vendorsWithEffortLevels,vendorsWithTurnReader}from"./chunk-4gjvmfa9.js";import{CONTRIB_ENGINE_IDS,installedEngineIds}from"./chunk-hzz020xx.js";import{terminalRows}from"./chunk-etfh8f4z.js";import{resolvePreferredVendor}from"./chunk-9rj2fx6v.js";import{addSavedRepo,getSavedRepos,isRemoteRepoKey}from"./chunk-rfp0ayra.js";import{readOnlyGitProcessEnv}from"./chunk-6xppe9bj.js";import{CURRENT_VERSION}from"./chunk-yn01fet6.js";import{parseBoolLiteral}from"./chunk-zsfyj36g.js";import{expandTilde}from"./chunk-2r1fgng9.js";import{errorMessage}from"./chunk-2brn8e1y.js";import{loadStateFile}from"./chunk-32j6y5d1.js";import{DELIVERY_OUTCOMES,DELIVERY_SUMMARY_MAX_CHARS,MAX_DELIVERIES_PER_TASK,ROUTINE_RESPONSE_MAX_CHARS,SHOULD_RUN_SKIP_EXIT_CODE,SKIP_REASONS,deliveryFooter}from"./chunk-qzzmd63x.js";import{ROVE_API_VERSION}from"./chunk-96kvwzj8.js";import{MULTI_REPO_CAP,canonicalPrUrl,gitRepoOf,lineageRootId,repoNameOf,rollupPr,taskCwd,taskRepos,todoCount,uniqueRepoName,workspaceCapabilities}from"./chunk-x1jxxmtc.js";import{DEFAULT_TASK_VENDOR}from"./chunk-xy8be792.js";import{ALL_VENDORS,DEFAULT_VENDOR,coerceVendorId}from"./chunk-h2m7em1e.js";import{homeDir}from"./chunk-z1nj22va.js";import{ROVE_PRODUCT_NAME}from"./chunk-zt7wfnva.js";import{pathWithin,samePath}from"./chunk-pn5ygqn6.js";import{defaultDaemonSocketPath}from"./chunk-eqdbfgq5.js";import{__require}from"./chunk-91sykzqq.js";import{readFileSync as readFileSync2}from"fs";import{resolve as resolve2}from"path";import{readFileSync}from"fs";import{isAbsolute,resolve}from"path";var FANOUT_CAP=10;function parsePositiveInt(raw){if(!/^\d+$/.test(raw.trim()))return;let n=Number.parseInt(raw,10);return Number.isSafeInteger(n)&&n>0?n:void 0}function parseNonNegativeInt(raw){if(!/^\d+$/.test(raw.trim()))return;let n=Number.parseInt(raw,10);return Number.isSafeInteger(n)&&n>=0?n:void 0}var FANOUT_STEP=helpStep("add"),F={repo:(required=!0)=>({name:"repo",type:"string",required,placeholder:"PATH",description:"Repo root (git toplevel). Relative paths resolve against $PWD."}),taskId:(required=!0)=>({name:"task-id",type:"string",required,placeholder:"ID",description:"Target task id (from `list` / `add`)."}),vendor:()=>({name:"vendor",type:"enum",values:ALL_VENDORS,placeholder:"V",description:"Engine vendor for the task."}),command:()=>({name:"command",type:"string",placeholder:"CMD",description:"Engine launch command, verbatim \u2014 an engine id from `engine-list` (e.g. claude) or a full command line (e.g. 'codex --search'). Unvalidated: probe an unfamiliar engine's flags with `<cmd> --help` first. Omitted = the repo's default engine."}),title:()=>({name:"title",type:"string",placeholder:"T",description:"Human task title."}),prompt:(required,desc)=>({name:"prompt",type:"string",required,placeholder:"TEXT",description:required?`${desc} Required unless --prompt-file is given.`:desc}),promptFile:()=>({name:"prompt-file",type:"string",placeholder:"PATH",description:"Read the prompt from this file instead of --prompt (`-` = stdin). Use it whenever the text has backticks, $vars, or quotes you don't want the shell to touch. Exactly one of --prompt / --prompt-file."})},REPEAT_SEP="\x00";function setFlag(flags,key,value,repeatable){let prev=flags.get(key);flags.set(key,repeatable.has(key)&&prev!==void 0?`${prev}${REPEAT_SEP}${value}`:value)}function parseFlags(argv,booleanFlags=new Set,repeatableFlags=new Set){let flags=new Map,pretty=!1,help=!1;for(let i=0;i<argv.length;i++){let arg=argv[i];if(!arg.startsWith("--")&&arg!=="-h")throw new ApiError(`unexpected positional arg: ${arg}`,"BAD_FLAG");if(arg==="-h"){help=!0;continue}let eq=arg.indexOf("=");if(eq!==-1){let key2=arg.slice(2,eq),value=arg.slice(eq+1);if(key2==="envelope")continue;if(key2==="pretty")pretty=value!=="false"&&value!=="0";else if(key2==="help")help=value!=="false"&&value!=="0";else setFlag(flags,key2,value,repeatableFlags);continue}let key=arg.slice(2);if(key==="envelope")continue;if(key==="pretty"){pretty=!0;continue}if(key==="help"){help=!0;continue}if(booleanFlags.has(key)){let next2=argv[i+1];if(next2!==void 0&&parseBoolLiteral(next2)!==void 0)flags.set(key,next2),i+=1;else flags.set(key,"true");continue}let next=argv[i+1];if(next===void 0||next.startsWith("--"))throw new ApiError(`flag --${key} requires a value`,"BAD_FLAG");setFlag(flags,key,next,repeatableFlags),i+=1}return{flags,pretty,help}}function validateAgainstSpec(verb,flags){let known=new Set(verb.flags.map((f)=>f.name));for(let key of flags.keys())if(!known.has(key))throw new ApiError(`unknown flag --${key} for "${verb.name}"`,"BAD_FLAG",helpStep(verb.name));for(let f of verb.flags){let satisfied=flags.get(f.name)||f.name==="prompt"&&flags.get("prompt-file")||verb.name==="add"&&f.name==="repo"&&(flags.get("fork")||flags.get("no-worktree")||flags.get("isolate-in"));if(f.required&&!satisfied)throw new ApiError(`--${f.name} is required for "${verb.name}"`,"MISSING_FLAG",helpStep(verb.name));if(f.type==="enum"&&f.values){let raw=flags.get(f.name);if(raw!==void 0&&!f.values.includes(raw)){if(f.name==="vendor"&&registeredEngineIds().includes(raw))continue;throw new ApiError(`--${f.name} must be one of ${f.values.join(", ")}`,"BAD_FLAG",helpStep(verb.name))}}if(f.type==="int"){let raw=flags.get(f.name);if(raw!==void 0&&parsePositiveInt(raw)===void 0)throw new ApiError(`--${f.name} must be a positive integer`,"BAD_FLAG")}if(f.type==="uint"){let raw=flags.get(f.name);if(raw!==void 0&&parseNonNegativeInt(raw)===void 0)throw new ApiError(`--${f.name} must be a non-negative integer`,"BAD_FLAG")}}}class VerbArgs{verb;flags;caller;constructor(verb,flags,caller=cliCaller()){this.verb=verb;this.flags=flags;this.caller=caller}resolvePath(name,raw){let expanded=expandTilde(raw);if(isAbsolute(expanded))return expanded;if(this.caller.cwd===void 0)throw new ApiError(`--${name} must be an absolute path: this caller has no working directory`,"BAD_FLAG");return resolve(this.caller.cwd,expanded)}spec(name){let f=this.verb.flags.find((s)=>s.name===name);if(!f)throw Error(`internal: --${name} is not declared on verb "${this.verb.name}"`);return f}str(name){this.spec(name);let v=this.flags.get(name);return v&&v.length>0?v:void 0}present(name){return this.spec(name),this.flags.get(name)!==void 0}list(name){if(!this.spec(name).repeatable)throw Error(`internal: --${name} is not repeatable on verb "${this.verb.name}"`);let raw=this.flags.get(name);return raw===void 0?[]:raw.split(REPEAT_SEP).filter((v)=>v.length>0)}promptText(){if(this.promptMemo!==void 0)return this.promptMemo.value;let value=this.readPromptText();return this.promptMemo={value},value}promptMemo;readPromptText(){let inline=this.verb.flags.some((f)=>f.name==="prompt")?this.str("prompt"):void 0,file=this.str("prompt-file");if(inline!==void 0&&file!==void 0)throw new ApiError("pass --prompt or --prompt-file, not both","BAD_FLAG",helpStep(this.verb.name));if(file===void 0)return inline;if(file==="-"&&!this.caller.cli)throw new ApiError("--prompt-file - reads stdin, which this caller cannot send; pass --prompt","BAD_FLAG");let text=readFileSync(file==="-"?0:this.resolvePath("prompt-file",file),"utf8");if(text.trim().length===0)throw new ApiError(`--prompt-file ${file} is empty`,"BAD_FLAG");return text}require(name){let v=this.str(name);if(v===void 0)throw new ApiError(`--${name} is required`,"MISSING_FLAG");return v}enumOf(name){let f=this.spec(name),v=this.str(name);if(v===void 0)return;if(f.values&&!f.values.includes(v))throw new ApiError(`--${name} must be one of ${f.values.join(", ")}`,"BAD_FLAG",helpStep(this.verb.name));return v}requireEnum(name){return this.require(name),this.enumOf(name)}vendor(){let value=this.str("vendor");if(value===void 0)return;let builtins=this.spec("vendor").values??ALL_VENDORS;if(builtins.includes(value))return value;if(registeredEngineIds().includes(value))return value;throw new ApiError(`--vendor must be a built-in (${builtins.join(", ")}) or a registered engine id \u2014 see \`engine-list\``,"BAD_FLAG",helpStep(this.verb.name))}bool(name){this.spec(name);let raw=this.str(name);if(raw===void 0)return;let value=parseBoolLiteral(raw);if(value===void 0)throw new ApiError(`--${name} must be a boolean (true/false)`,"BAD_FLAG");return value}int(name){this.spec(name);let raw=this.str(name);if(raw===void 0)return;let n=parsePositiveInt(raw);if(n===void 0)throw new ApiError(`--${name} must be a positive integer`,"BAD_FLAG");return n}nonNegativeInt(name){this.spec(name);let raw=this.str(name);if(raw===void 0)return;let n=parseNonNegativeInt(raw);if(n===void 0)throw new ApiError(`--${name} must be a non-negative integer`,"BAD_FLAG");return n}path(name){let v=this.str(name);return v===void 0?void 0:this.resolvePath(name,v)}requirePath(name){return this.resolvePath(name,this.require(name))}requireRepo(name){return this.repoPath(name,this.require(name))}repoPath(name,raw){return isRemoteRepoKey(raw)?raw:this.resolvePath(name,raw)}}function parseAgentsSpec(spec){let out=[];for(let part of spec.split(",")){let trimmed=part.trim();if(!trimmed)continue;let colon=trimmed.indexOf(":");if(colon===-1)throw new ApiError(`--agents entry "${trimmed}" must be engine:count`,"BAD_FLAG",FANOUT_STEP);let vendor=trimmed.slice(0,colon);if(!ALL_VENDORS.includes(vendor)&&!registeredEngineIds().includes(vendor))throw new ApiError(`--agents engine "${vendor}" must be a built-in (${ALL_VENDORS.join(", ")}) or a registered engine id \u2014 see \`engine-list\``,"BAD_FLAG",FANOUT_STEP);let count=parsePositiveInt(trimmed.slice(colon+1));if(count===void 0)throw new ApiError(`--agents count for "${vendor}" must be a positive integer`,"BAD_FLAG",FANOUT_STEP);if(out.length+count>FANOUT_CAP)throw new ApiError(`--agents requests ${out.length+count} agents, exceeds the cap of ${FANOUT_CAP}`,"BAD_FLAG",FANOUT_STEP);for(let i=0;i<count;i++)out.push(vendor)}if(out.length===0)throw new ApiError('--agents specified no agents (e.g. "claude:2,codex:1")',"BAD_FLAG",FANOUT_STEP);return out}function buildCountPlan(count,vendor){if(count>FANOUT_CAP)throw new ApiError(`--count ${count} exceeds the parallel cap of ${FANOUT_CAP} \u2014 spawn in batches`,"BAD_FLAG",FANOUT_STEP);return Array(count).fill(vendor)}function daemonOf(ctx){if(!ctx.client)throw new ApiError("daemon required","BAD_DAEMON");return ctx.client}async function repoFilter(runtime,repoFlag,repos){let target=await runtime.resolveRepoRoot(repoFlag);if(!await runtime.isUsableRepo(target))throw new ApiError(`--repo ${repoFlag} is not a readable git repository (resolved to ${target}) \u2014 the project may have moved or been deleted; \`rove api list\` still shows its tasks`,"REPO_UNRESOLVABLE");let resolved=new Map,unresolvable=new Set;for(let repo of new Set(repos)){let root=await runtime.resolveRepoRoot(repo);if(resolved.set(repo,root),!samePath(root,target)&&!await runtime.isUsableRepo(root))unresolvable.add(repo)}return{target,matches:(repo)=>samePath(resolved.get(repo),target),unresolvableRepos:[...unresolvable].sort()}}async function simpleRpc(ctx,name,payload){let request=name;return daemonOf(ctx).request(request,payload)}async function handlePtyList(){let[{RoveDaemonClient},{defaultPtyHostSocketPath}]=await Promise.all([import("./chunk-fq2pefc5.js"),import("./chunk-1qvrn21x.js")]),client=new RoveDaemonClient(defaultPtyHostSocketPath());try{return await client.connect(),await client.request("pty.list",{})}catch{return{sessions:null}}finally{client.close()}}function readJsonBody(source){let raw=readFileSync2(source==="-"?0:resolve2(process.cwd(),expandTilde(source)),"utf8"),parsed;try{parsed=JSON.parse(raw)}catch{throw new ApiError(`--json ${source} is not valid JSON`,"BAD_FLAG")}if(!parsed||typeof parsed!=="object"||Array.isArray(parsed))throw new ApiError("--json must be an object: {delivery, outcome, summary, evidence?, next?}","BAD_FLAG");let body=parsed,evidence=body.evidence;if(evidence!==void 0&&(!Array.isArray(evidence)||evidence.some((e)=>typeof e!=="string")))throw new ApiError("evidence must be an array of strings","BAD_FLAG");for(let key of["delivery","outcome","summary","next"])if(body[key]!==void 0&&typeof body[key]!=="string")throw new ApiError(`${key} must be a string`,"BAD_FLAG");return body}var FIELD_FLAGS=["delivery","outcome","summary","next"];function readBody(ctx){let json=ctx.args.str("json"),evidence=ctx.args.list("evidence");if(json===void 0)return{delivery:ctx.args.str("delivery"),outcome:ctx.args.str("outcome"),summary:ctx.args.str("summary"),evidence,next:ctx.args.str("next")};if(FIELD_FLAGS.some((f)=>ctx.args.str(f)!==void 0)||evidence.length>0)throw new ApiError("pass --json OR the individual flags, not both","BAD_FLAG");return readJsonBody(json)}async function report(ctx){let body=readBody(ctx);if(!body.delivery)throw new ApiError("--delivery is required","MISSING_FLAG");if(!body.outcome||!DELIVERY_OUTCOMES.includes(body.outcome))throw new ApiError(`--outcome must be one of ${DELIVERY_OUTCOMES.join("|")}`,"MISSING_FLAG");if(!body.summary?.trim())throw new ApiError("--summary is required","MISSING_FLAG");if(body.summary.length>DELIVERY_SUMMARY_MAX_CHARS)throw new ApiError(`summary is ${body.summary.length} characters; the cap is ${DELIVERY_SUMMARY_MAX_CHARS}`,"RESPONSE_TOO_LARGE",{hint:"Summarize: lead with the conclusion, put the detail in --evidence (commits, PR urls, paths)."});let res=await simpleRpc(ctx,"delivery.report",{deliveryId:body.delivery,outcome:body.outcome,summary:body.summary,...body.evidence?.length?{evidence:body.evidence}:{},...body.next?{next:body.next}:{}});if(res.delivery===null)throw new ApiError(`no delivery ${body.delivery}`,"DELIVERY_NOT_FOUND",{hint:"Copy the id from the [ROVE] report line of the prompt you were given; `deliveries --task-id <id>` lists them."});return res}var REPORT_VERB={name:"report",group:"drive",summary:`Close the report a delivered prompt asked for. Run it as the LAST step of the turn, before you stop: a turn that ends without it is flagged \`report_missing\` in the owner's Inbox. --delivery is the id in the prompt's [ROVE] / [ROVE ROUTINE] line. Re-reporting replaces the earlier report; a late report on a flagged delivery clears the flag. --summary \u2264 ${DELIVERY_SUMMARY_MAX_CHARS} chars; put commits, PR urls, test results and paths in repeatable --evidence, and what the next tick should do in --next. Use --json - (stdin) for text with quotes or backticks.`,flags:[{name:"delivery",type:"string",placeholder:"ID",description:"Delivery id, e.g. d-41."},{name:"outcome",type:"enum",values:DELIVERY_OUTCOMES,placeholder:"O",description:"done = finished what was asked; partial = some of it; blocked = needs the owner or an external fix; failed = tried and could not."},{name:"summary",type:"string",placeholder:"TEXT",description:"What was done (or why not)."},{name:"evidence",type:"string",placeholder:"X",repeatable:!0,description:"A commit, PR url, test command + result, or file path. Repeat the flag for several."},{name:"next",type:"string",placeholder:"TEXT",description:"What the next tick should do."},{name:"json",type:"string",placeholder:"PATH",description:"Read {delivery, outcome, summary, evidence?, next?} as JSON from this file (`-` = stdin) instead of the flags."}],handler:report},DELIVERIES_VERB={name:"deliveries",group:"read",summary:`A task's armed deliveries, newest first (newest ${MAX_DELIVERIES_PER_TASK} are kept): status awaiting|reported|missing|skipped, the report (outcome, summary, evidence, next) and, for missing, why (cause turn-complete|turn-failed|engine-dead|stalled|interrupted|session-end|rate-limited + note; rate-limited raises no Inbox row and does not block) plus dismissedAt once the owner dismissed it (a missing row blocks \`send --if-runnable\` and routine ticks until a late report or that dismissal). A skipped row is a \`send --if-runnable\` tick should-run refused: \`skipped\` is the verdict {reason, detail?, until?, gateIds?}, \`repeats\` how many identical refusals (same tab, same reason) the one row stands for. The per-tick log a loop prompt should read instead of a growing MEMO file.`,flags:[F.taskId(!0),{name:"limit",type:"int",placeholder:"N",description:"At most N entries (default: all retained)."}],handler:async(ctx)=>simpleRpc(ctx,"delivery.list",{taskId:ctx.args.require("task-id"),...ctx.args.int("limit")!==void 0?{limit:ctx.args.int("limit")}:{}})};async function listAllEnginePresets(){let presets=await enginePresetsInList(),byProtocol=new Map,modelsOf=(protocol)=>{let pending=byProtocol.get(protocol);if(!pending){let list=engineEntry(protocol).listModels;pending=list?list().catch(()=>null):Promise.resolve(null),byProtocol.set(protocol,pending)}return pending};return Promise.all(presets.map(async(preset)=>({...preset,models:await modelsOf(preset.protocol)})))}async function enginePresetsInList(){ensurePluginEnginesLoaded();let presets=[...listEnginePresets()],seen=new Set(presets.map((p)=>p.id));for(let id of await installedEngineIds())if(!seen.has(id))presets.push(describePreset(id));return presets}async function engineListIds(){return(await enginePresetsInList()).map((p)=>p.id)}var ENGINE_LIST_VERB={name:"engine-list",group:"discover",summary:`List every engine Rove can launch \u2014 built-ins, registered presets, the shipped contrib engines whose CLI is on PATH (${CONTRIB_ENGINE_IDS.join(", ")}), and engines contributed by enabled plugins \u2014 each with its RAW launch command, exactly as it runs. Copy one into \`add --command\` / \`send --tab new --command\` verbatim, or edit its flags first. \`protocol\` is the adapter Rove speaks to it (history, trust, delivery); \`generic\` = none, which still runs fine but loses transcript reads. \`models\` is what the engine can name for \`--model\` (suggestions, not a closed set); null = Rove cannot list them for this engine. Returns { engines }.`,flags:[],offline:!0,handler:async()=>({engines:await listAllEnginePresets()})};function taskEngine(task){let command=task.command?.trim();if(command){let resolved=resolveCommandProtocol(command);if(resolved!==GENERIC_PROTOCOL)return resolved}return sessionProtocol(coerceVendorId(task.vendor))}function assertEngineAcceptsEffort(engine,level,recover){let levels=engineEntry(engine).effortLevels??[];if(levels.length===0)throw new ApiError(`engine ${engine} declares no reasoning effort levels`,"BAD_EFFORT",{engine,hint:`Only engines with declared reasoning levels accept one (${vendorsWithEffortLevels().join(", ")} today). Check the task's engine with \`get-task\`.`,nextCommandArgs:[...recover]});if(!levels.includes(level))throw new ApiError(`engine ${engine} does not accept effort level ${JSON.stringify(level)} \u2014 it declares ${levels.join(", ")}`,"BAD_EFFORT",{engine,levels,hint:`Pass one of: ${levels.join(", ")}.`,nextCommandArgs:[...recover]})}function assertEngineAcceptsModel(engine,model,recover){if(engineEntry(engine).modelArgv)return;throw new ApiError(`engine ${engine} declares no model flag \u2014 it does not accept a model`,"BAD_MODEL",{engine,hint:"Only engines that declare a model flag accept one (claude, codex, kimi, pi, omp today); `engine-list` shows `models` per engine.",nextCommandArgs:[...recover]})}function vendorToRecord(task,engine){let recorded=coerceVendorId(task.vendor);return sessionProtocol(recorded)===engine?recorded:engine}async function readRunnability(daemon,taskId){try{return await daemon.request("task.runnability",{taskId})}catch{return}}async function shouldRun(ctx){let tab=ctx.args.str("tab");if(tab&&tab!=="new"&&!/^tab-[A-Za-z0-9-]+$/.test(tab))throw new ApiError(`--tab must be "new" or a tab id like tab-2 (got ${JSON.stringify(tab)})`,"BAD_TAB");let verdict=await daemonOf(ctx).request("task.runnability",{taskId:ctx.args.require("task-id"),...tab?{tabId:tab}:{}});if(!verdict.run)throw new ApiError(`skip: ${verdict.reason}`,"SKIPPED",{...verdict});return verdict}var SHOULD_RUN_VERB={name:"should-run",group:"read",summary:`Would a tick sent to this task be used? Prints the verdict {run:true} or {run:false, reason, detail?, until?, gateIds?} and exits 0 on run, ${SHOULD_RUN_SKIP_EXIT_CODE} on skip (the verdict is still on stdout). reason, first match wins: ${SKIP_REASONS.join(" | ")}. \`awaiting-owner\` = an open gate (answer or withdraw it), \`report-missing\` = the last delivery ended unreported (late \`report\`, or dismiss its Inbox row), \`quota\` clears itself at \`until\`, \`engine-error\` / \`engine-dead\` clear on the engine's next turn or a restart, \`awaiting-permission\` = the engine waits on an approval, \`busy\` = a turn is running or the last delivery still awaits its report. A task whose only gate is answered-but-undelivered is runnable: the next tick carries the answer. Without --tab the whole task is judged and a dead engine is not a skip (\`send\` revives it); --tab tab-N judges that tab and a dead engine is \`engine-dead\`; --tab new judges only task-level state. To check and send in one step use \`send --if-runnable\`.`,flags:[F.taskId(!0),{name:"tab",type:"string",required:!1,placeholder:"TAB",description:'Judge exactly this tab (e.g. tab-2), or "new" for a tab about to be created. Omitted = the whole task.'}],handler:shouldRun};async function attentionDismiss(ctx){let delivery=ctx.args.require("delivery");if((await daemonOf(ctx).request("attention.dismiss",{deliveryId:delivery})).found===!1)throw new ApiError(`no delivery ${delivery}`,"DELIVERY_NOT_FOUND",{hint:"`deliveries --task-id <id>` lists a task's delivery ids; the Inbox row names it as d-N."});return{ok:!0,delivery,cleared:!0}}var ATTENTION_DISMISS_VERB={name:"attention-dismiss",group:"drive",summary:'Dismiss a `report_missing` Inbox row by its delivery id (d-N): the owner\'s deliberate "seen, carry on" for a tick that ended without a report. It unblocks `should-run` for the task. Opening or visiting the row never does this, and a late `report` clears it on its own. Idempotent; an unknown id is DELIVERY_NOT_FOUND.',flags:[{name:"delivery",type:"string",required:!0,placeholder:"ID",description:"Delivery id, e.g. d-41."}],handler:attentionDismiss};import path from"path";var TASK_STATUSES=["backlog","in_progress","in_review","done","canceled","error"];var TASK_FIELDS=["title","branch","parent","project","command","model","effort","pinned","kept","require-report","auto-fix-ci","status"],REPORT_FLAGS=["report-branch","report-pr","report-summary"];async function renameTab(ctx,taskId,tabId){let title=ctx.args.str("title"),others=[...TASK_FIELDS,...REPORT_FLAGS].filter((f)=>f!=="title"&&ctx.args.str(f)!==void 0);if(title===void 0||others.length>0)throw new ApiError("--tab only renames a tab: pass --title and no other field","BAD_FLAG",{hint:"Update the task's own fields in a separate call without --tab."});let daemon=daemonOf(ctx);if(!await renameTabsSnapshot(daemon,taskId,tabId,title))throw new ApiError(`task ${taskId} has no tab ${tabId}`,"TAB_NOT_FOUND",{hint:"`get-task` lists the addressable tab ids in .tabs[].id",nextCommandArgs:["api","get-task","--task-id",taskId]});let reply=await daemon.request("terminalTab.rename",{taskId,tabId,title});return{ok:!0,taskId,tabId,title,renamed:!0,clients:reply.clients??0}}async function resolveTarget(ctx,verb="update"){let explicit=ctx.args.str("task-id");if(explicit)return explicit;let self=await ownSession(daemonOf(ctx));if(!self)throw new ApiError("no --task-id given and the caller is not a verified rove session \u2014 pass --task-id ID","MISSING_TARGET",helpStep(verb));return self.taskId}async function projectRoot(ctx){let raw=ctx.args.str("project")?.trim();if(raw===void 0)return;if(raw==="")throw new ApiError("--project cannot be empty \u2014 pass the repo's path","BAD_FLAG");let root=await ctx.runtime.resolveRepoRoot(path.resolve(ctx.caller.cwd??process.cwd(),raw));if(!await ctx.runtime.isUsableRepo(root))throw new ApiError(`--project ${raw} is not inside a git repository`,"NOT_A_GIT_REPO",{hint:"clone or `git init` the project first, then pass its path"});return root}var NO_PARENT="none";async function reparentGroup(ctx,groupId){let parent=ctx.args.str("parent"),stray=["task-id","tab",...TASK_FIELDS.filter((f)=>f!=="parent"),...REPORT_FLAGS].filter((f)=>ctx.args.str(f)!==void 0);if(parent===void 0||parent===""||stray.length>0)throw new ApiError(`--group moves a whole fan-out round and takes only --parent ID (or \`none\`)${stray.length>0?` \u2014 drop ${stray.map((f)=>`--${f}`).join(", ")}`:""}`,"BAD_FLAG",helpStep("update"));let daemon=daemonOf(ctx),{tasks}=await daemon.request("task.list"),members=tasks.filter((t)=>t.groupId===groupId);if(members.length===0)throw new ApiError(`no tasks in group ${groupId}`,"TASK_NOT_FOUND",{hint:"the groupId is the one `add --count` returned; any surviving sibling's `.groupId` in `list` recovers it",nextCommandArgs:["api","list"]});if(members.some((t)=>t.id===parent))throw new ApiError(`${parent} is itself a member of group ${groupId} \u2014 a round cannot move under one of its own`,"PARENT_IN_GROUP",{hint:"pick a parent outside the round, or move the members one by one with --task-id"});let parentTaskId=parent===NO_PARENT?null:parent,replyTab=parentTaskId===null?void 0:await replyTabOf(ctx.runtime,daemon,parentTaskId),results=[],failures=0;for(let task of members)try{let moved=await simpleRpc(ctx,"task.setParent",{taskId:task.id,parentTaskId,...replyTab?{replyTab}:{}});results.push({taskId:task.id,status:"moved",...moved})}catch(err){failures++;let message=errorMessage(err),coded=err instanceof ApiError?{code:err.code,rest:message}:splitDaemonCode(message);results.push({taskId:task.id,status:"failed",error:coded?.rest??message,...coded?{code:coded.code}:{}})}return{ok:failures===0,groupId,count:members.length,failures,results}}async function updateTask(ctx){let group=ctx.args.str("group");if(group!==void 0)return reparentGroup(ctx,group);let tabId=ctx.args.str("tab"),taskId=tabId!==void 0?ctx.args.require("task-id"):await resolveTarget(ctx);if(tabId!==void 0)return renameTab(ctx,taskId,tabId);let title=ctx.args.str("title"),branch=ctx.args.str("branch"),parent=ctx.args.str("parent"),command=ctx.args.str("command")?.trim(),model=ctx.args.str("model")?.trim(),effort=ctx.args.str("effort")?.trim(),pinned=ctx.args.bool("pinned"),kept=ctx.args.bool("kept"),requireReport=ctx.args.enumOf("require-report"),autoFixCi=ctx.args.enumOf("auto-fix-ci"),status=ctx.args.enumOf("status"),report2={reportBranch:ctx.args.str("report-branch"),reportPr:ctx.args.int("report-pr"),reportSummary:ctx.args.str("report-summary")},reporting=Object.values(report2).some((v)=>v!==void 0);if(TASK_FIELDS.every((f)=>ctx.args.str(f)===void 0)&&!reporting)throw new ApiError(`nothing to update \u2014 pass at least one of ${TASK_FIELDS.map((f)=>`--${f}`).join(", ")}`,"BAD_FLAG",{nextCommandArgs:["api","update","--help"]});if(reporting&&status===void 0)throw new ApiError("--report-* is recorded with a status: pass --status too","BAD_FLAG",{hint:"A worker reporting delivery usually passes --status in_review (or done)."});if(command==="")throw new ApiError("--command cannot be empty","BAD_FLAG");if(parent==="")throw new ApiError("--parent cannot be empty \u2014 pass a task id, or `none` for top level","BAD_FLAG");let daemon=daemonOf(ctx),protocol=command?resolveCommandProtocol(command):void 0,project=await projectRoot(ctx),task,engine;if(model!==void 0||effort!==void 0){task=(await daemon.request("task.get",{taskId})).task,engine=taskEngine(command?{...task,command}:task);let recover=["api","get-task","--task-id",taskId];if(model!==void 0)assertEngineAcceptsModel(engine,model,recover);if(effort!==void 0)assertEngineAcceptsEffort(engine,effort,recover)}let applied=[],step=async(fields,method,payload)=>{let reply;try{reply=await simpleRpc(ctx,method,{taskId,...payload})}catch(err){if(applied.length===0)throw err;let message=err instanceof Error?err.message:String(err),code=err instanceof ApiError?err.code:"PARTIAL_UPDATE";throw new ApiError(`--${fields.join("/--")} failed after ${applied.join(", ")} applied: ${message}`,code,{applied:[...applied],failed:fields})}return applied.push(...fields),reply},reparented;if(parent!==void 0){let parentTaskId=parent===NO_PARENT?null:parent,replyTab=parentTaskId===null?void 0:await replyTabOf(ctx.runtime,daemon,parentTaskId);reparented=await step(["parent"],"task.setParent",{parentTaskId,...replyTab?{replyTab}:{}})}let regrouped;if(project!==void 0)regrouped=await step(["project"],"task.moveToProject",{repo:project});if(branch!==void 0)await step(["branch"],"task.setBranch",{branch});if(command)await step(["command"],"task.setCommand",{command,vendor:protocol});if(task&&engine){let recorded=vendorToRecord(protocol?{...task,vendor:protocol}:task,engine),fields=[...model!==void 0?["model"]:[],...effort!==void 0?["effort"]:[]];await step(fields,"task.setVendor",{vendor:recorded,model,effort})}if(title!==void 0)await step(["title"],"task.rename",{title});if(pinned!==void 0)await step(["pinned"],"task.pin",{pinned});if(kept!==void 0)await step(["kept"],"task.keep",{kept});if(requireReport!==void 0)await step(["require-report"],"task.requireReport",{required:requireReport==="on"});if(autoFixCi!==void 0)await step(["auto-fix-ci"],"task.autoFixCi",{enabled:autoFixCi==="on"});if(status!==void 0)await step(["status"],"task.status",{status,...report2});return{ok:!0,taskId,updated:applied,...reparented?{parent:reparented}:{},...regrouped?{project:{repo:regrouped.repo,moved:regrouped.moved,note:"regrouped only \u2014 the PTYs, cwd, worktree and branch stay where they were"}}:{},...command?{command,protocol,...protocol===GENERIC_PROTOCOL?{generic:!0}:{}}:{},...engine?{engine}:{}}}var EDIT_VERBS=[{name:"update",group:"edit",summary:"Change a task's metadata \u2014 any combination of title, branch (git branch -m if materialized, else recorded), parent, engine --command / --model / --effort (take effect on the next session rebuild), pin, and status + worker report \u2014 in one call. The flags are checked before anything is written; then they apply in order parent \u2192 branch \u2192 engine \u2192 title/pin/status, and a refusal mid-way names what already applied (`applied`). --parent ID re-parents the task without touching its engine, tabs, worktree or branch: the stored parent is ID's top-level ancestor (children never nest, as with `add` from a child; the task's own children come along, listed in `.parent.moved`) and the task's reply address becomes ID itself, so its bare `send` reaches ID (the caller's own tab when run from inside ID, else ID's live engine tab, else tab-1). `--parent none` makes the task top level and drops a reply address that pointed into its old tree. A parent must live on the same machine as the task. Archiving the stored parent, or `delete --cascade` on it, takes the task along. --group G --parent ID moves a whole fan-out round instead and takes nothing else. With --tab, renames that Terminal Tab instead (the API twin of the TUI's f2) and takes only --title. The status is a cosmetic LABEL: `--status canceled` closes, stops and cleans up nothing \u2014 `delete` ends a task. The --report-* fields record what the WORKER CLAIMS it delivered (`.report` on `get-task` / `collect`), deliberately apart from `.prStatus`, which the daemon polled from the forge; they merge onto any previous report and need --status. Returns { updated } \u2014 the fields written, in order.",flags:[{...F.taskId(!1),description:"Target task id (from `list` / `add`). Default: the calling session's own task, from the verified $ROVE_TASK_ID (an unverifiable env is MISSING_TARGET, never a guess)."},{name:"title",type:"string",placeholder:"T",description:"New task title (or, with --tab, the tab's name)."},{name:"tab",type:"string",placeholder:"TAB",description:"Rename this Terminal Tab (id from `get-task` .tabs[].id, e.g. tab-2) instead of the task; needs --task-id, and only --title may accompany it. TAB_NOT_FOUND when the task's snapshot names no such tab."},{name:"branch",type:"string",placeholder:"B",description:"New branch name."},{name:"parent",type:"string",placeholder:"ID|none",description:"Move the task under this task (its engine, tabs, worktree and branch stay as they are), or `none` to make it top level. Refusals: PARENT_NOT_FOUND, PARENT_IS_SELF, PARENT_CYCLE, TASK_NOT_MOVABLE, REMOTE_PARENT_UNSUPPORTED."},{name:"project",type:"string",placeholder:"PATH",description:"Move a top-level task (and its children) under the project at PATH (relative paths resolve against $PWD; any directory inside the repo names its main checkout), which becomes a project if it was not one. Only the grouping changes: the live session, tabs, cwd, worktree and branch stay exactly where they are, so a running engine keeps its conversation. Moving a worktree task back to the repo that cut it clears the link. Refusals: NOT_A_GIT_REPO, SAME_PROJECT, TASK_NOT_MOVABLE (a project's main task, a routine session or a multi-repo task), CHILD_NOT_MOVABLE (move its parent; children follow), REMOTE_MOVE_UNSUPPORTED."},{name:"group",type:"string",placeholder:"GROUPID",description:"With --parent: move every task of one fan-out round (the `groupId` that `add --count` returns). Takes nothing but --parent; a member's refusal is recorded in `results`, and a parent inside the round is PARENT_IN_GROUP."},{...F.command(),description:"New engine launch command \u2014 an engine id from `engine-list` or a full command line. The protocol Rove speaks to it is derived from it; the result reports which (`generic: true` when the command names no engine Rove knows)."},{name:"model",type:"string",placeholder:"MODEL",description:"Pin a model, passed to the engine VERBATIM in its own spelling (claude alias or id, codex slug, pi/omp pattern); `engine-list` .models are suggestions, not a closed list. BAD_MODEL when the engine declares no model flag."},{name:"effort",type:"string",placeholder:"LEVEL",description:"Reasoning effort level the task's engine declares (codex: none/low/medium/high/xhigh/max; pi, omp: off/minimal/low/medium/high/xhigh/max; claude: low/medium/high/xhigh/max). BAD_EFFORT names the accepted levels."},{name:"pinned",type:"bool",placeholder:"BOOL",description:"true pins the task to the top of the sidebar, false unpins."},{name:"kept",type:"bool",placeholder:"BOOL",description:"true marks the task as the attempt kept from a parallel round (`add --count` / `--agents`); false clears the mark. A flag only: it archives nothing, and other attempts of the round are untouched."},{name:"require-report",type:"enum",values:["on","off"],description:"on: every later `send` to this task arms a delivery the agent must close with `report --delivery <id>` (a turn that ends without one raises a `report_missing` Inbox row); off: back to plain sends."},{name:"auto-fix-ci",type:"enum",values:["on","off"],description:"on: when this task's PR checks go red on a new head sha and its engine is at rest, the daemon types the fix-CI prompt (with the failing logs) into the engine, at most once per sha and 3 times in all, then stops and raises an Inbox row; the count starts over when the checks pass. Never interrupts a running turn. off: stop and forget the count."},{name:"status",type:"enum",values:TASK_STATUSES,description:"New lifecycle label (cosmetic \u2014 see summary)."},{name:"report-branch",type:"string",placeholder:"B",description:"The branch the worker says holds the work (a CLAIM \u2014 `.branch` is the task's actual branch)."},{name:"report-pr",type:"int",placeholder:"N",description:"The PR number the worker says it opened (a CLAIM \u2014 `.prStatus.number` is what the daemon read from the forge)."},{name:"report-summary",type:"string",placeholder:"TEXT",description:"One line of what was delivered."}],handler:updateTask}];async function todo(ctx){let{args}=ctx,taskId=await resolveTarget(ctx,"todo"),add=args.str("add"),check=args.str("check"),uncheck=args.str("uncheck"),edit=args.str("edit"),text=args.str("text"),remove=args.str("remove"),notes=args.bool("clear-notes")===!0?"":args.str("notes");if(edit===void 0!==(text===void 0))throw new ApiError("--edit ID and --text T go together","BAD_FLAG");let ops=[...add!==void 0?[{op:"add",text:add}]:[],...check!==void 0?[{op:"check",id:check,done:!0}]:[],...uncheck!==void 0?[{op:"check",id:uncheck,done:!1}]:[],...edit!==void 0&&text!==void 0?[{op:"edit",id:edit,text}]:[],...remove!==void 0?[{op:"remove",id:remove}]:[]],after=await simpleRpc(ctx,"task.notes",{taskId,...notes!==void 0?{notes}:{},...ops.length>0?{ops}:{}}),count=todoCount(after.todos);return{ok:!0,taskId,notes:after.notes??"",todos:after.todos,done:count?.done??0,total:count?.total??0}}var TODO_VERB={name:"todo",group:"edit",summary:"Read or change a task's notes and checklist \u2014 the owner's own review list (shown in the web Files panel's Notes tab and as `done/total` on the task row). With no flag it lists them. Flags combine in one call and apply together, in this order: --add, --check, --uncheck, --edit/--text, --remove; one refused op (TODO_NOT_FOUND, TODO_EMPTY, TODO_TOO_LONG, TODO_LIMIT) changes nothing. A todo id is `t<N>` as listed. Default target: the calling session's own task. Returns { ok, taskId, notes, todos: [{ id, text, done }], done, total }.",flags:[{...F.taskId(!1),description:"Target task id (from `list` / `add`). Default: the calling session's own task."},{name:"add",type:"string",placeholder:"TEXT",description:"Add an unchecked todo (one line, up to 500 characters; 100 per task)."},{name:"check",type:"string",placeholder:"ID",description:"Mark todo ID done."},{name:"uncheck",type:"string",placeholder:"ID",description:"Mark todo ID not done."},{name:"edit",type:"string",placeholder:"ID",description:"Change todo ID's text to --text."},{name:"text",type:"string",placeholder:"TEXT",description:"The new text for --edit."},{name:"remove",type:"string",placeholder:"ID",description:"Delete todo ID."},{name:"notes",type:"string",placeholder:"TEXT",description:"Replace the task's free-text notes (up to 20000 characters); Use --clear-notes to empty them."},{name:"clear-notes",type:"bool",description:"Empty the free-text notes."}],handler:todo};var API_SCHEMA_VERSION=ROVE_API_VERSION,GLOBAL_FLAGS=[{name:"pretty",type:"bool",description:"Pretty-print stdout JSON."},{name:"help",type:"bool",description:"Show usage for the verb and exit."},{name:"envelope",type:"bool",description:"Print every outcome as one { apiVersion, ok, result | error } on stdout \u2014 what the daemon's and the bridge's api.invoke return."},{name:"machine",type:"string",placeholder:"ALIAS",description:"Run the verb in that registered machine's daemon (`rove machine list`); paths are paths there. A --task-id on another machine goes there without it."}];function displayValues(f){if(!f.values)return;if(f.name!=="vendor")return f.values;let registered=registeredEngineIds().filter((id)=>!f.values.includes(id));return registered.length>0?[...f.values,...registered]:f.values}function flagJson(f){let values=displayValues(f);return{name:f.name,type:f.type,required:f.required??!1,...values?{values}:{},...f.default!==void 0?{default:f.default}:{},...f.placeholder?{placeholder:f.placeholder}:{},...f.repeatable?{repeatable:!0}:{},description:f.description}}function installedVerbs(){return VERBS.filter(verbInstalled)}function installedGroups(){return Object.fromEntries(Object.entries(VERB_GROUPS).filter(([group])=>{let mod=GROUP_MOD[group];return mod===void 0||isBuiltinModInstalled(mod)}))}function verbSchema(v){return{name:v.name,group:v.group,summary:v.summary,offline:v.offline??!1,cliOnly:v.cliOnly??!1,flags:v.flags.map(flagJson)}}function schemaIndex(){let cliName=ROVE_PRODUCT_NAME;return{apiVersion:API_SCHEMA_VERSION,roveVersion:CURRENT_VERSION,hint:`Compact index. Drill into ONE verb: \`${cliName} api schema --verb <name>\` (or \`${cliName} api <verb> --help\`). One group: \`--group <g>\`. Whole spec: \`--all\`.`,groups:installedGroups(),verbs:installedVerbs().map((v)=>({name:v.name,group:v.group,summary:v.summary})),globalFlags:GLOBAL_FLAGS,aliases:VERB_ALIASES}}function groupSchema(group){let verbs=VERBS.filter((v)=>v.group===group);if(verbs[0])requireVerbInstalled(verbs[0]);if(verbs.length===0)throw new ApiError(`unknown group: ${group}. Groups: ${Object.keys(installedGroups()).join(", ")}`,"BAD_VERB",{hint:"that group does not exist \u2014 the compact index lists every group and the verbs in it",nextCommandArgs:["api","schema"]});return{group,verbs:verbs.map((v)=>({name:v.name,summary:v.summary}))}}function fullSchema(){return{apiVersion:API_SCHEMA_VERSION,roveVersion:CURRENT_VERSION,output:{success:"one JSON object on stdout, newline-terminated, exit 0",error:'{"error":{"message","code"}} on stderr, exit != 0',pretty:"--pretty indents stdout JSON",envelope:'--envelope prints {"apiVersion","ok","result"} or {"apiVersion","ok","error"} on stdout instead, exit codes unchanged'},transports:{cli:"rove api <verb> --flag value",daemon:'socket request "api.invoke" {verb, params, machine?} \u2192 the envelope; params are keyed by flag name',bridge:'WebSocket op "api.invoke" {verb, params, machine?} \u2192 result, or the full error object'},globalFlags:GLOBAL_FLAGS,aliases:VERB_ALIASES,groups:installedGroups(),verbs:installedVerbs().map(verbSchema)}}function flagSignature(verb){return verb.flags.map((f)=>{let meta=f.type==="enum"&&f.values?displayValues(f).join("|"):f.placeholder??(f.type==="bool"?"":"X"),core=meta?`--${f.name} ${meta}`:`--${f.name}`;return f.required?core:`[${core}]`}).join(" ")}function verbHelp(verb){let lines=[`${ROVE_PRODUCT_NAME} api ${verb.name} ${flagSignature(verb)}`.trimEnd(),"",verb.summary,""],alias=Object.entries(VERB_ALIASES).find(([,canon])=>canon===verb.name)?.[0];if(alias)lines.push(`Alias: ${alias}`,"");if(verb.flags.length>0){lines.push("Flags:");for(let f of verb.flags){let req=f.required?" (required)":f.repeatable?" (repeatable)":"",def=f.default!==void 0?` [default: ${f.default}]`:"",vals=f.type==="enum"&&f.values?` {${displayValues(f).join("|")}}`:"";lines.push(` --${f.name}${vals}${req}${def} ${f.description}`)}lines.push("")}return lines.push("Global: [--pretty] [--help] [--envelope] [--machine ALIAS]"),lines.join(`
3
3
  `)}function apiUsage(){let cliName=ROVE_PRODUCT_NAME,rows=installedVerbs().map((v)=>` ${v.name.padEnd(18)} ${v.summary}`);return[`usage: ${cliName} api <verb> [flags] [--pretty] [--help]`,"",`Explore the full surface (names, flags, types) with: ${cliName} api schema`,"","verbs:",...rows,"","Output is one JSON object on stdout (exit 0); errors are JSON on stderr (exit != 0)."].join(`
4
4
  `)}var DEFAULT_PAGE_MESSAGES=40,MAX_PAGE_MESSAGES=50,PAGE_BYTE_BUDGET=131072,STRING_CLIP_CHARS=16384,DEFAULT_TAIL_LINES=40,TERMINAL_TAIL_LINES=200,TERMINAL_TAIL_BYTES=65536;function encodeCursor(cursor){return Buffer.from(JSON.stringify(cursor),"utf8").toString("base64url")}function decodeCursor(raw,taskId){let parsed;try{parsed=JSON.parse(Buffer.from(raw,"base64url").toString("utf8"))}catch{throw new ApiError("invalid cursor (not a read-output cursor)","CURSOR_INVALID")}let c=parsed;if(!(c!==null&&typeof c==="object"&&c.v===1&&typeof c.task==="string"&&(c.tab===void 0||typeof c.tab==="string")&&(c.src==="history"&&typeof c.sid==="string"&&typeof c.idx==="number"||c.src==="terminal"&&typeof c.off==="number")))throw new ApiError("invalid cursor (unknown version or shape)","CURSOR_INVALID");if(c.task!==taskId)throw new ApiError(`cursor belongs to task ${c.task}, not ${taskId}`,"CURSOR_TASK_MISMATCH");return c}function clipStrings(value){if(typeof value==="string"){if(value.length<=STRING_CLIP_CHARS)return value;let points=[...value];if(points.length<=STRING_CLIP_CHARS)return value;return`${points.slice(0,STRING_CLIP_CHARS).join("")}\u2026[+${points.length-STRING_CLIP_CHARS} chars clipped]`}if(Array.isArray(value))return value.map(clipStrings);if(value!==null&&typeof value==="object"){let out={};for(let[k,v]of Object.entries(value))out[k]=clipStrings(v);return out}return value}function buildHistoryPage(messages,at,limit){let forward="from"in at,page=[],bytes=0,limited=!1,i=forward?at.from:at.before-1;for(;forward?i<messages.length:i>=0;forward?i++:i--){if(page.length>=limit)break;let clipped=clipStrings(messages[i]),size=JSON.stringify(clipped).length;if(page.length>0&&bytes+size>PAGE_BYTE_BUDGET){limited=!0;break}page.push(clipped),bytes+=size}if(forward)return{page,start:at.from,end:i,limited};return page.reverse(),{page,start:i+1,end:at.before,limited}}function terminalLines(text){return terminalRows(text)}function boundedTail(text,maxLines=TERMINAL_TAIL_LINES){let lines=terminalLines(text),start=Math.max(0,lines.length-Math.min(maxLines,TERMINAL_TAIL_LINES)),bytes=0;for(let i=lines.length-1;i>=start;i--)if(bytes+=(lines[i]?.length??0)+1,bytes>TERMINAL_TAIL_BYTES&&i<lines.length-1){start=i+1;break}return{tail:lines.slice(start),truncated:start>0}}async function peekTaskTerminal(taskId,engineBin,tab,sinceOffset,scope){let host=await openPtyHost();if(!host)return"host-unreachable";try{let key;if(tab)key=`${taskId}::${tab}`;else{let sessions=await listSessionsOrNull(host.rpc);if(sessions===null)return"host-unreachable";key=findEngineKey(sessions,taskId,engineBin,scope)??unclaimedSessions(sessions,scope).find((s)=>s.key===`${taskId}::tab-1`)?.key}if(!key)return null;let res;try{res=await host.rpc.request("pty.peek",{key,sinceOffset})}catch{return"host-unreachable"}if(!res.exists){if(tab)throw new ApiError(`tab ${tab} has no hosted session on task ${taskId} \u2014 see \`rove api pty-list\` for live tabs`,"TAB_NOT_FOUND");return null}return{pid:res.pid,offset:res.offset,text:Buffer.from(res.data,"base64").toString("utf8"),sinceValid:res.sinceValid,live:res.alive,exit:res.exit??null}}catch(err){if(err instanceof ApiError)throw err;return null}finally{host.close()}}async function engineScreenError(task,opts={}){let pattern=protocolEntry(task.vendor).errorLine;if(!pattern)return;try{let alias=task.ptyAlias,page=await peekTaskTerminal(alias?.taskId??task.id,taskEngineArgv(task)[0],alias?.tabId);if(!page||page==="host-unreachable")return;return screenErrorLine(pattern,boundedTail(page.text).tail,opts)}catch{return}}var ACTIVITY_STATES_DOC="`.activity.state` is one of: `idle` (at rest), `running` (a turn is in progress), `turn_complete` (a turn finished \u2014 also at rest; treat it like idle when deciding whether the engine is free), `permission_needed` (blocked on an approval prompt), `rate_limited` (clears on its own), `error` (the last turn failed; `.activity.detail.note` carries the engine's text, and `.activity.source: \"screen\"` means it was read off the engine's own screen because the engine reported nothing), `dead` (the engine process exited; the tab's exit record says how).",AT_REST=new Set(["idle","turn_complete"]);async function readActivityRegistry(daemon){try{let dbg=await daemon.request("debug.inspect");return{tasks:dbg?.activity?.tasks??{},tabs:dbg?.activity?.tabs??{}}}catch{return null}}async function activityView(registry,task,tab){let entry=tab?registry?.tabs[task.id]?.[tab]:registry?.tasks[task.id];if(!entry)return null;let base={state:entry.state,at:new Date(entry.at).toISOString(),forMs:Math.max(0,Date.now()-entry.at),...entry.detail?{detail:entry.detail}:{}},atRest=AT_REST.has(entry.state);if(tab||!(atRest||entry.state==="running"))return base;let note=await engineScreenError(task,{last:!atRest});return note?{...base,state:"error",detail:{failure:"other",note},source:"screen"}:base}async function assertNotEmptySuccess(daemon,ctx,prompt){if(ctx.args.bool("allow-empty"))return;if(!/^\s*succeeded\s*[:\uff1a]/i.test(prompt))return;let self=await ownSession(daemon);if(!self)return;let sender;try{sender=(await daemon.request("task.get",{taskId:self.taskId})).task}catch{return}if(!workspaceCapabilities(sender).ownsBranch)return;if(!sender.worktreePath)return;let ahead;try{ahead=(await ctx.runtime.readBranchSignals(sender.worktreePath,sender.baseRef)).ahead}catch{return}if(ahead!==0)return;let branch=sender.branch||"your branch";throw new ApiError(`refusing to report success: ${branch} has 0 commits \u2014 "succeeded" means COMMITTED, and this report would reach the coordinator as a clean success with nothing to land`,"EMPTY_SUCCESS_REPORT",{taskId:self.taskId,branch,hint:"commit your work with a real message and send again \u2014 or, if this task genuinely produced no commits (an investigation or a review), re-send with --allow-empty to say so explicitly",nextCommandArgs:["api","send",...ctx.args.str("task-id")?["--task-id",ctx.args.str("task-id")]:[],...ctx.args.str("tab")?["--tab",ctx.args.str("tab")]:[],"--allow-empty","--prompt",prompt]})}function parseOption(raw){let eq=raw.indexOf("=");if(eq<=0||eq===raw.length-1)throw new ApiError(`--option must be key=label (got ${JSON.stringify(raw)})`,"BAD_FLAG",helpStep("gate-raise"));return{key:raw.slice(0,eq).trim(),label:raw.slice(eq+1).trim()}}async function gateRaise(ctx){let{args}=ctx,daemon=daemonOf(ctx),taskId=args.require("task-id"),tabId=args.str("tab");if(!tabId){let self=await ownSession(daemon);if(self?.taskId===taskId)tabId=self.tabId}return{ok:!0,gate:(await daemon.request("gate.raise",{taskId,...tabId?{tabId}:{},question:args.require("question"),options:args.list("option").map(parseOption),...args.str("recommend")?{recommended:args.str("recommend")}:{},...args.str("context")?{context:args.str("context")}:{}})).gate}}async function gateAnswer(ctx){let{args}=ctx,option=args.str("option"),text=args.str("text");if(option===void 0&&text===void 0)throw new ApiError("pass --option KEY and/or --text TEXT","MISSING_FLAG",helpStep("gate-answer"));let res=await daemonOf(ctx).request("gate.answer",{gateId:args.require("gate"),...option!==void 0?{option}:{},...text!==void 0?{text}:{},by:"cli"});return{ok:!0,gate:res.gate,delivered:res.delivered}}async function gateWithdraw(ctx){return{ok:!0,gate:(await daemonOf(ctx).request("gate.withdraw",{gateId:ctx.args.require("gate")})).gate}}async function gates(ctx){let{args}=ctx,repo=args.str("repo")?args.requireRepo("repo"):void 0;return{gates:(await daemonOf(ctx).request("gate.list",{...repo?{repo}:{},...args.str("task-id")?{taskId:args.str("task-id")}:{},status:args.enumOf("status")??"open"})).gates}}async function takeOwedGateAnswers(daemon,taskId,tab){try{let owed=await daemon.request("gate.takeUndelivered",{taskId,...tab?{tabId:tab}:{}});return owed.text?{text:owed.text,gateIds:owed.gateIds}:null}catch{return null}}async function markGateAnswersDelivered(daemon,gateIds){await daemon.request("gate.markDelivered",{gateIds}).catch(()=>{})}function assertQueueFlags(ctx,tab){if(!ctx.args.bool("queue"))return;let clash=tab==="new"?"--tab new":ctx.args.bool("if-runnable")?"--if-runnable":ctx.args.bool("respawn")?"--respawn":null;if(clash)throw new ApiError(`--queue waits for a running turn; it does not combine with ${clash}`,"BAD_FLAG",helpStep("send"))}async function queueIfBusy(ctx,daemon,task,tab,text){if(!ctx.args.bool("queue"))return null;let tabId=tab??canonicalEngineTab(readTabsSnapshot(task.id)?.tabs??[])?.id;if(!tabId)return null;let reply=await daemon.request("queue.add",{taskId:task.id,tabId,text,expectReport:ctx.args.bool("expect-report")===!0});if(!reply.queued||!reply.item)return null;return{ok:!0,taskId:task.id,tabId,queued:!0,queueId:reply.item.id,position:reply.position}}var QUEUE_LIST_VERB={name:"queue-list",group:"read",summary:"Prompts `send --queue` is holding for a busy tab, oldest first: {items:[{id, taskId, tabId, text, expectReport, at, lastError?}]}. Each is pasted when its tab's turn ends, one per turn; `lastError` says why the last hand-over did not land (it stays at the head). Held in the daemon's memory: a daemon restart drops them.",flags:[F.taskId(!1)],handler:async(ctx)=>simpleRpc(ctx,"queue.list",ctx.args.str("task-id")?{taskId:ctx.args.str("task-id")}:{})},QUEUE_EDIT_VERB={name:"queue-edit",group:"drive",summary:'Rewrite a prompt `send --queue` is holding (`queue-list` gives the id), keeping its id and its place in line: {edited:true, item} or {edited:false, reason:"not-found"|"pasting"}. `pasting` means its turn already came and the old text is going in.',flags:[{name:"id",type:"string",required:!0,placeholder:"q-N",description:"The held prompt's id."},F.prompt(!0,"The new text, replacing the held prompt's."),F.promptFile()],handler:async(ctx)=>{let text=ctx.args.promptText();if(text===void 0)throw new ApiError("pass --prompt or --prompt-file","MISSING_FLAG",helpStep("queue-edit"));return simpleRpc(ctx,"queue.edit",{id:ctx.args.require("id"),text})}},QUEUE_CANCEL_VERB={name:"queue-cancel",group:"drive",summary:"Take back a prompt `send --queue` is holding (`queue-list` gives the id): {cancelled: true|false}.",flags:[{name:"id",type:"string",required:!0,placeholder:"q-N",description:"The held prompt's id."}],handler:async(ctx)=>simpleRpc(ctx,"queue.cancel",{id:ctx.args.require("id")})};async function handoffBrief(ctx,task,tabId){if(!/^tab-[A-Za-z0-9-]+$/.test(tabId))throw new ApiError(`--handoff-from must be a tab id like tab-2 (got ${JSON.stringify(tabId)})`,"BAD_TAB");let source=((await readDaemonTabs(daemonOf(ctx),task.id))?.tabs??readTabsSnapshot(task.id)?.tabs??[]).find((t)=>t.id===tabId);if(!source)throw new ApiError(`task ${task.id} has no tab ${tabId}`,"TAB_NOT_FOUND",{taskId:task.id,tabId});if(source.kind!=="engine")throw new ApiError(`${tabId} is not an engine conversation; nothing to hand over`,"BAD_FLAG",helpStep("send"));let tabVendor=source.vendor??task.vendor,plan=await planWorktreeHandoff(source,liveSourceProtocol(source,tabVendor),taskCwd(task),task.title);if(plan.kind==="handoff")return plan.prompt;throw new ApiError(plan.kind==="no-transcript"?`${plan.engine} keeps no transcript Rove can name, so ${tabId} cannot be handed to another engine`:`${tabId} has no conversation yet; send the prompt directly`,plan.kind==="no-transcript"?"NO_TRANSCRIPT":"NO_CONVERSATION",{taskId:task.id,tabId})}function sessionFailed(taskId,message,hint,extra){return new ApiError(message,"SESSION_FAILED",{taskId,...extra,hint,nextCommandArgs:["api","read-output","--task-id",taskId,"--source","terminal"]})}async function ensureHostFor(taskId,hint){try{return await ensurePtyHost()}catch(error){throw sessionFailed(taskId,`failed to start PTY host for ${taskId}: ${error instanceof Error?error.message:String(error)}`,hint)}}var OLD_CONVERSATION=["sessionId","spawned","forkFrom","initialPrompt","liveVendor","lastTitle","autoTitle"];async function assertDeadEngineTab(runtime,daemon,task,tabId){let scope=await ptyScopeOf(daemon,task);if(hostedKeyFor(task.id,tabId,scope)===null||scope.alias?.tabId===tabId)throw new ApiError(`tab ${tabId} runs under another task's conversation; --replace cannot swap it`,"BAD_FLAG",{taskId:task.id,tabId});let row=(await runtime.taskTabs(task.id,taskEngineArgv(task),scope)).tabs.find((t)=>t.id===tabId);if(!row||row.kind!=="engine")throw new ApiError(`${tabId} is not an engine conversation; nothing to replace`,"BAD_FLAG",helpStep("send"));if(row.viewOf)throw new ApiError(`${tabId} is a view of task ${row.viewOf}'s session; --replace cannot swap it`,"BAD_FLAG",{taskId:task.id,tabId});if(row.alive===!1||row.engineAlive===!1)return;throw new ApiError(`${tabId}'s engine is still running; --replace only takes over a dead or exited tab. Drop --replace to continue in a shared child task`,"TAB_ALIVE",{taskId:task.id,tabId})}var REWRITTEN=[...OLD_CONVERSATION,"vendor","engineCommand"];async function undoReplace(daemon,taskId,tabId,before){let set={},unset=[];for(let field of REWRITTEN)if(before[field]===void 0)unset.push(field);else set[field]=before[field];await applyTabOps(daemon,taskId,[{op:"patch",id:tabId,set,unset}]);let host=await ensureHostFor(taskId,"no PTY host to stop the replacement in");try{await host.rpc.request("pty.kill",{key:`${taskId}::${tabId}`,wait:!0})}finally{host.close()}}function replaceDeadTab(ctx,task,tabId,command){let{runtime,daemon}=ctx;return withDeliveryLock(`${task.id}::${tabId}::replace`,async()=>{await assertDeadEngineTab(runtime,daemon,task,tabId);let worktree=taskCwd(task);if(!worktree)throw new ApiError(`task ${task.id} has no worktree yet`,"NO_WORKTREE");let before=(await readDaemonTabs(daemon,task.id))?.tabs.find((t)=>t.id===tabId)??{},protocol=command?resolveCommandProtocol(command):void 0,set=protocol?{vendor:protocol,...command!==protocol?{engineCommand:command}:{}}:void 0,unset=protocol&&command===protocol?[...OLD_CONVERSATION,"engineCommand"]:OLD_CONVERSATION,undo=()=>undoReplace(daemon,task.id,tabId,before);await applyTabOps(daemon,task.id,[{op:"patch",id:tabId,...set?{set}:{},unset}]);let target={id:task.id,worktreePath:worktree,kind:task.kind,vendor:task.vendor,command:task.command,modelEffort:task.modelEffort,model:task.model,repo:task.repo};try{await runtime.reviveTab(daemon,target,tabId,worktree,{replaceExited:!0,awaitEngine:!0})}catch(error){throw await undo().catch(()=>{}),error}return undo})}function csvOf(ctx,name,allowed,verb="list"){let raw=ctx.args.str(name);if(raw===void 0)return;let values=raw.split(",").map((s)=>s.trim()).filter(Boolean);if(values.find((v)=>!allowed.includes(v))!==void 0||values.length===0)throw new ApiError(`--${name} takes a comma list of ${allowed.join(", ")}`,"BAD_FLAG",helpStep(verb));return new Set(values)}function listFilters(ctx){let repo=ctx.args.path("repo"),statuses=csvOf(ctx,"status",TASK_STATUSES),activities=csvOf(ctx,"activity",TASK_ACTIVITY_STATES);if(!repo&&!statuses&&!activities)return null;return{repo,statuses,activities}}async function filterTaskList(ctx,daemon,tasks,filters){let isLocal=(t)=>!t.origin||t.origin.machineId==="local",repo=filters.repo?await repoFilter(ctx.runtime,filters.repo,tasks.filter(isLocal).map((t)=>t.repo)):null,kept=tasks.filter((t)=>(!repo||isLocal(t)&&repo.matches(t.repo))&&(!filters.statuses||filters.statuses.has(t.status))),out=kept;if(filters.activities){let wanted=filters.activities,registry=await readActivityRegistry(daemon),local=kept.filter(isLocal),views=await Promise.all(local.map((t)=>activityView(registry,t)));out=local.flatMap((task,i)=>{let activity=views[i];return activity&&wanted.has(activity.state)?[{...task,activity}]:[]})}return{tasks:out,...repo&&repo.unresolvableRepos.length>0?{unresolvableRepos:repo.unresolvableRepos}:{}}}var TASK_FIELDS2={id:!0,title:!0,repo:!0,branch:!0,worktreePath:!0,kind:!0,scratch:!0,noProject:!0,routine:!0,status:!0,pinned:!0,kept:!0,notes:!0,todos:!0,archived:!0,vendor:!0,command:!0,prStatus:!0,modelEffort:!0,model:!0,tier:!0,groupId:!0,observedLanguage:!0,deletion:!0,quotaResume:!0,scheduledStart:!0,autoFixCi:!0,linkedWorkItem:!0,dispatcher:!0,parentTaskId:!0,sharesWorkspaceOf:!0,workspace:!0,cwdSubpath:!0,ptyAlias:!0,requireReport:!0,prompt:!0,baseRef:!0,worktreeName:!0,gitRepo:!0,extraRepos:!0,sessionPrs:!0,report:!0,origin:!0,repoId:!0,createdAt:!0,updatedAt:!0},LIST_FIELDS=[...Object.keys(TASK_FIELDS2),"repoCount","activity"];function listFields(ctx){let fields=csvOf(ctx,"fields",LIST_FIELDS);return fields?[...fields]:void 0}function projectTasks(tasks,fields){return tasks.map((task)=>{let row={};for(let field of fields)if(field in task)row[field]=task[field];return row})}var MAX_LIVE_ENGINES_KEY="engine.maxLiveSessions";function normalizeMaxLiveEngines(value){let n=typeof value==="number"?value:typeof value==="string"&&value.trim()!==""?Number(value.trim()):Number.NaN;return Number.isInteger(n)&&n>0?n:void 0}function persistedMaxLiveEngines(){return normalizeMaxLiveEngines(loadStateFile()[MAX_LIVE_ENGINES_KEY])}async function deliverToExactTab(rpc,taskId,tabId,cwd,prompt,opts){let key=`${taskId}::${tabId}`,{sessions=[]}=await rpc.request("pty.list",{}),session=sessions.find((s)=>s.key===key);if(!session?.alive){if(session?.restored===!0){if(!opts?.respawn)throw restoredTabError(taskId,tabId,prompt);let launch=opts.respawn(),open=await openAdmittedEngine(rpc,{key,cwd,...launch?{command:launch.command}:{},defaultColors:readPersistedTerminalDefaultColors()},{maxLiveEngines:opts.maxLiveEngines,snapshot:opts.snapshot,taskId,retryArgs:["api","send","--task-id",taskId,"--tab",tabId,"--respawn","--plain","--prompt",prompt]});if(open.respawned!==!0&&open.alive!==!0)throw new ApiError(`tab ${tabId} on task ${taskId} could not be respawned`,"SESSION_FAILED",{taskId,tabId,hint:"the frozen record is still on disk; open the task in the TUI to see the session's own output",nextCommandArgs:["api","read-output","--task-id",taskId,"--tab",tabId,"--source","terminal"]});let enginePid=await awaitEngineProcess(rpc,key,opts.engineBin,{timeoutMs:ENGINE_START_PROBE_MS,intervalMs:ENGINE_START_POLL_MS,snapshot:opts.snapshot});if(enginePid===null)return{session:key,pane:key,started:!1,respawned:!0,engineReady:!1,delivered:!1,reason:await hostedSessionFailureLine(rpc,key)??ENGINE_NOT_OBSERVED_REASON};return await deliverRespawned(rpc,key,prompt,enginePid.vendor)}throw new ApiError(`tab ${tabId} has no live session on task ${taskId} \u2014 see \`rove api pty-list\` for alive tabs`,"TAB_NOT_FOUND")}let presence=await enginePresence(session.pid,opts?.engineBin,opts?.snapshot);if(presence.kind==="unknown")throw new ApiError(`could not read the process table, so tab ${tabId} on task ${taskId} could not be checked for a live engine`,"ENGINE_PROBE_FAILED",{hint:"the `ps` probe failed or timed out; retry, or check the machine's process table",nextCommandArgs:["api","pty-list"]});if(presence.kind!=="engine")throw new ApiError(`tab ${tabId} on task ${taskId} has no live engine process \u2014 it is a plain shell right now`,"ENGINE_NOT_RUNNING",{hint:"spawn a fresh engine tab for this prompt with --tab new, or pick an engine tab from pty-list",nextCommandArgs:["api","pty-list"]});let outcome=await deliverToKey(rpc,key,prompt,{vendor:presence.vendor});return{session:key,pane:key,started:!1,...outcomeFields(outcome)}}async function deliverRespawned(rpc,key,prompt,vendor){let outcome=await deliverToKey(rpc,key,prompt,{vendor});return{session:key,pane:key,started:!1,respawned:!0,...outcomeFields(outcome)}}function restoredTabError(taskId,tabId,prompt){return new ApiError(`tab ${tabId} on task ${taskId} is a freeze-restored session \u2014 its pty host restarted, so the tab exists with its scrollback but nothing is running in it`,"TAB_RESTORED",{taskId,tabId,hint:"revive it with --respawn (resumes the tab's pinned conversation when it has one \u2014 `get-task` shows `sessionId`; a tab without one replays its recorded launch command)",nextCommandArgs:["api","send","--task-id",taskId,"--tab",tabId,"--respawn","--plain","--prompt",prompt]})}import{realpathSync,statSync}from"fs";import{join}from"path";function collision(taskId,key,retryArgs){return new ApiError(`session ${key} already existed; no new tab was created`,"TAB_COLLISION",{taskId,hint:"a session already held the tab id the daemon minted; retry to get a fresh id",nextCommandArgs:retryArgs})}var createdFresh=(open)=>open.created??open.replay.length===0;async function openShell(client,rpc,taskId,cwd,multiRepo,home,type){let shell=resolveSessionShell(),tab=home?await seedShellHome(client,taskId,shell):await allocateTab(client,taskId,{kind:"command",title:null,command:[shell]}),key=`${taskId}::${tab.id}`;try{let open;try{open=await rpc.request("pty.open",{key,cwd,command:[shell],defaultColors:readPersistedTerminalDefaultColors()})}catch(error){if(!home)await closeTabsSnapshot(client,taskId,tab.id).catch(()=>{return});throw error}if(!createdFresh(open))throw collision(taskId,key,["api","tab-new","--task-id",taskId,"--kind","shell"]);let repos=multiRepo?await launchReposOf(client,taskId):void 0;if(await rpc.request("pty.write",{key,data:shellIdentityInput(taskId,tab.id,repos&&repoEnv(repos))}),type!==void 0)await rpc.request("pty.write",{key,data:`${type}\r`})}finally{await rpc.request("pty.detach",{key}).catch(()=>{})}return{tab,key}}async function openEngine(client,rpc,target,cwd){let taskId=target.id,tab=await seedCanonicalTab(client,taskId),key=`${taskId}::${tab.id}`,retryArgs=["api","tab-new","--task-id",taskId,"--kind","engine"];try{let pinned=withPinnedSessionId(engineLaunchArgv({command:target.command,vendor:target.vendor,effort:target.modelEffort,model:target.model}),target.vendor);trustEngineWorktree(target.vendor,cwd);let repos=target.multiRepo?await launchReposOf(client,taskId):void 0;trustExtraRepos(target.vendor,repos);let launch=buildEngineSessionLaunch({task:{id:taskId,kind:target.kind,vendor:target.vendor,repo:target.repo,repos},worktreePath:cwd,shell:resolveSessionShell(),argv:pinned.argv,promptIntent:{kind:"none"},tabId:tab.id}),open=await openAdmittedEngine(rpc,{key:launch.key,cwd,command:launch.command,defaultColors:readPersistedTerminalDefaultColors()},{maxLiveEngines:persistedMaxLiveEngines(),taskId,retryArgs});if(!createdFresh(open)&&open.respawned!==!0)throw collision(taskId,key,retryArgs);if(!open.alive)throw sessionFailed(taskId,`the new engine tab ${tab.id} exited as it started`,"the session was created but nothing stayed running in it \u2014 fix the task's launch command (`api update --command`), then open another tab",{session:key});tab=(await markCliTabSession(client,taskId,tab.id,pinned.sessionId))?.tabs.find((t)=>t.id===tab.id)??tab}finally{await rpc.request("pty.detach",{key}).catch(()=>{})}return{tab,key}}async function openNewTab(client,target,worktree,kind,opts={}){let host=await ensureHostFor(target.id,"no PTY host to run the tab in \u2014 nothing was started; check `rove daemon status`, then retry");try{let opened=kind==="shell"?await openShell(client,host.rpc,target.id,worktree,target.multiRepo===!0,opts.home===!0,opts.type):await openEngine(client,host.rpc,target,worktree);return{tab:opened.tab,session:opened.key}}catch(error){if(error instanceof ApiError)throw error;throw sessionFailed(target.id,`opening a ${kind} tab failed for ${target.id}: ${error instanceof Error?error.message:String(error)}`,"the PTY host or the daemon refused the new tab \u2014 check `rove daemon status`, then retry")}finally{host.close()}}function fileInside(worktree,rel){if(!rel||rel.startsWith("/")||rel.includes("\x00")||rel.split(/[\\/]/).includes(".."))throw new ApiError("--file must be a path relative to the worktree","BAD_FLAG");let abs,root;try{root=realpathSync(worktree),abs=realpathSync(join(worktree,rel))}catch{throw new ApiError(`${rel} does not exist in the worktree`,"FILE_NOT_FOUND")}if(!pathWithin(root,abs))throw new ApiError(`${rel} resolves outside the worktree`,"BAD_FLAG");if(!statSync(abs).isFile())throw new ApiError(`${rel} is not a file`,"BAD_FLAG");return abs}async function spawnCommandTab(client,taskId,cwd,command,title,purpose,retryArgs){let host=await ensureHostFor(taskId,"no PTY host to run the tab in \u2014 nothing was started; check `rove daemon status`, then retry");try{let tab=await allocateTab(client,taskId,{kind:"command",title,command,...purpose?{purpose}:{}}),key=`${taskId}::${tab.id}`;try{let open;try{open=await host.rpc.request("pty.open",{key,cwd,command,defaultColors:readPersistedTerminalDefaultColors()})}catch(error){throw await closeTabsSnapshot(client,taskId,tab.id).catch(()=>{return}),error}if(!createdFresh(open))throw collision(taskId,key,retryArgs)}finally{await host.rpc.request("pty.detach",{key}).catch(()=>{})}return{tab,session:key}}catch(error){if(error instanceof ApiError)throw error;throw sessionFailed(taskId,`opening the tab failed for ${taskId}: ${error instanceof Error?error.message:String(error)}`,"the PTY host or the daemon refused the new tab \u2014 check `rove daemon status`, then retry")}finally{host.close()}}async function launchEditor(client,spec){let abs=fileInside(spec.worktree,spec.file),launch=await resolveEditorLaunch(spec.worktree,abs,spec.line);if(!launch)throw new ApiError("no editor found \u2014 set $VISUAL/$EDITOR, or Settings \u2192 General \u2192 Editor","NO_EDITOR",{hint:"an editor chosen in Settings wins over $EDITOR; `rove config` shows what resolves"});let snapshot=await readDaemonTabs(client,spec.taskId),previous=snapshot?findEditorTab(snapshot)?.id:void 0;return{...await spawnCommandTab(client,spec.taskId,spec.worktree,["sh","-c",launch.command],launch.label,"editor",["api","tab-new","--task-id",spec.taskId,"--kind","editor",`--file=${spec.file}`,...spec.line===void 0?[]:[`--line=${spec.line}`]]),...previous?{replaces:previous}:{},...launch.atLine===void 0?{}:{atLine:launch.atLine}}}function findPane(taskId,pane){let found=listPaneLaunches({socketPath:defaultDaemonSocketPath(),binPath:ROVE_PRODUCT_NAME,taskId}).find((p)=>`${p.pluginId}.${p.paneId}`===pane);if(!found)throw new ApiError(`no enabled plugin pane named ${pane}`,"PANE_NOT_FOUND",{hint:"panes of enabled plugins only; `rove plugin list` shows what is installed"});return found}async function launchPane(client,spec,split){let pane=findPane(spec.taskId,spec.pane),snapshot=await readDaemonTabs(client,spec.taskId),host=snapshot?.tabs.find((t)=>t.id===(spec.hostTab??snapshot.activeId));if(pane.placement==="split"&&host&&host.kind!=="content"){let out=await split(spec.taskId,host.id,spec.worktree,pane);if(out.changed)return{tab:out.tab,session:`${spec.taskId}::${host.id}::${out.leaf}`,leaf:out.leaf}}return spawnCommandTab(client,spec.taskId,spec.worktree,pane.argv,pane.title,void 0,["api","tab-new","--task-id",spec.taskId,"--kind","pane",`--pane=${spec.pane}`])}async function launchViewport(client,spec){let shown=(await readDaemonTabs(client,spec.view.id))?.tabs.find((t)=>t.id==="tab-1"),engine=shown?.kind==="engine"?shown:void 0;return{tab:await allocateTab(client,spec.taskId,{kind:"engine",title:null,...engine?.vendor?{vendor:engine.vendor}:{},sessionId:engine?.sessionId??null,spawned:!0,ptyTask:{id:spec.view.id,worktree:spec.view.worktree}}),session:`${spec.view.id}::tab-1`}}function launchTab(client,spec,split){switch(spec.kind){case"editor":return launchEditor(client,spec);case"pane":return launchPane(client,spec,split);case"viewport":return launchViewport(client,spec)}}var NO_OP={revived:!1,alive:!0,resumed:!1};function engineRelaunch(target,row,worktree){let base=tabEngineBase(target,row),vendor=row.vendor??target.vendor,resumeArgv=row.sessionId&&row.spawned?engineResumeArgv(base,vendor,row.sessionId):null,pinned=resumeArgv?null:withPinnedSessionId(base,vendor,row.sessionId?()=>row.sessionId:void 0);return trustEngineWorktree(vendor,worktree),{launch:buildEngineSessionLaunch({task:{id:target.id,kind:target.kind,vendor,repo:target.repo},worktreePath:worktree,shell:resolveSessionShell(),argv:resumeArgv??pinned?.argv??base,promptIntent:{kind:"none"},tabId:row.id}),resumed:resumeArgv!==null,sessionId:resumeArgv?row.sessionId:pinned?.sessionId}}async function openEngineTab(client,rpc,target,row,worktree){let{launch,resumed,sessionId}=engineRelaunch(target,row,worktree),open=await openAdmittedEngine(rpc,{key:launch.key,cwd:worktree,command:launch.command,defaultColors:readPersistedTerminalDefaultColors()},{maxLiveEngines:persistedMaxLiveEngines(),taskId:target.id,retryArgs:["api","tab-revive","--task-id",target.id,"--tab",row.id]});if(!createdFresh(open)&&open.respawned!==!0)return NO_OP;if(!open.alive)throw sessionFailed(target.id,`the engine in tab ${row.id} exited as it started`,"the session was reopened but nothing stayed running in it \u2014 fix the task's launch command (`api update --command`), then retry",{session:launch.key});return await markCliTabSession(client,target.id,row.id,sessionId??null),{revived:!0,resumed,...sessionId?{sessionId}:{}}}async function openCommandTab(rpc,target,row,worktree){let key=`${target.id}::${row.id}`,open=await rpc.request("pty.open",{key,cwd:worktree,command:row.command,defaultColors:readPersistedTerminalDefaultColors()});if(!createdFresh(open)&&open.respawned!==!0)return NO_OP;if(!open.alive)throw sessionFailed(target.id,`the command in tab ${row.id} exited as it started`,"run `get-task` for the tab",{session:key});if(row.command.length===1&&!row.purpose)await rpc.request("pty.write",{key,data:shellIdentityInput(target.id,row.id)});return{revived:!0,resumed:!1}}async function reviveTab(client,target,tabId,worktree,opts={}){let host=await ensureHostFor(target.id,"no PTY host to run the tab in \u2014 nothing was started; check `rove daemon status`, then retry"),key=`${target.id}::${tabId}`;try{let{sessions=[]}=await host.rpc.request("pty.list",{}),listed=sessions.find((s)=>s.key===key);if(listed?.alive&&!opts.replaceExited)return NO_OP;let row=(await readDaemonTabs(client,target.id))?.tabs.find((t)=>t.id===tabId);if(!row&&listed&&tabId==="tab-1")row=await seedCanonicalTab(client,target.id);if(!row)throw new ApiError(`tab ${tabId} is not a tab of task ${target.id}`,"TAB_NOT_FOUND",{taskId:target.id,hint:"pick a tab id from `get-task` .tabs[]",nextCommandArgs:["api","get-task","--task-id",target.id]});if(row.kind==="content"||row.kind==="engine"&&row.ptyTask){let why=row.kind==="content"?"is a preview with no session":`is a view of task ${row.ptyTask?.id}'s session`;throw new ApiError(`tab ${tabId} on task ${target.id} ${why}`,"TAB_NOT_REVIVABLE",{taskId:target.id,tabId,hint:"pick a shell or engine tab from `get-task` .tabs[], or revive a view's session under its own task",nextCommandArgs:["api","get-task","--task-id",target.id]})}if(listed&&listed.restored!==!0)await host.rpc.request("pty.kill",{key,wait:!0});if(row.kind!=="engine")return await openCommandTab(host.rpc,target,row,worktree);let revived=await openEngineTab(client,host.rpc,target,row,worktree);if(opts.awaitEngine){if(await awaitEngineProcess(host.rpc,key,tabEngineBase(target,row)[0],{timeoutMs:ENGINE_START_PROBE_MS,intervalMs:ENGINE_START_POLL_MS})===null)throw sessionFailed(target.id,`the engine did not start in tab ${tabId}`,"check the engine's launch command")}return revived}catch(error){if(error instanceof ApiError)throw error;if(error instanceof RemoteEngineLaunchError)throw new ApiError(error.message,error.code,{taskId:target.id,hint:"an SSH-backed project's engine cannot be launched from this machine; open the task on the machine that holds it"});throw sessionFailed(target.id,`reviving tab ${tabId} failed for ${target.id}: ${error instanceof Error?error.message:String(error)}`,"the PTY host or the daemon refused the open \u2014 check `rove daemon status`, then retry")}finally{await host.rpc.request("pty.detach",{key}).catch(()=>{}),host.close()}}function tabNotFound(taskId,tabId){return new ApiError(`tab ${tabId} does not exist on task ${taskId}`,"TAB_NOT_FOUND",{hint:"refresh the task's tab ids with get-task, then retry with one of its .tabs[].id values",nextCommandArgs:["api","get-task","--task-id",taskId]})}function locate(taskId,tabId,state){let tab=state&&Array.isArray(state.tabs)?state.tabs.find((t)=>t.id===tabId):void 0;if(!state||!tab)throw tabNotFound(taskId,tabId);if(tab.kind==="content")throw new ApiError(`tab ${tabId} is a content tab; only terminal tabs split`,"TAB_NOT_SPLITTABLE");return{state,tab}}async function editTree(client,taskId,tabId,edit){let{tab}=locate(taskId,tabId,await readDaemonTabs(client,taskId)),before=tab.splitTree??initialSplit(null),after=edit(before);if(after===before)return{tab,before,after};let saved=await patchTabSplit(client,taskId,tabId,collapseSplit(after));if(!saved)throw tabNotFound(taskId,tabId);return{tab:saved,before,after}}async function openLeafShell(rpc,key,cwd,opts={}){if(await rpc.request("pty.open",{key,cwd,command:opts.command??[resolveSessionShell()],defaultColors:readPersistedTerminalDefaultColors(),...opts.size?{cols:opts.size.cols,rows:opts.size.rows}:{}}),opts.identity)await rpc.request("pty.write",{key,data:opts.identity});await rpc.request("pty.detach",{key}).catch(()=>{})}async function splitTab(client,taskId,tabId,worktree,edit){let host=await ensureHostFor(taskId,"no PTY host to run the split in \u2014 nothing was changed; check `rove daemon status`");try{switch(edit.kind){case"open":{let{tab,before,after}=await editTree(client,taskId,tabId,(tree)=>{let from=leaves(tree.root).some((l)=>l.id===edit.from)?edit.from:tree.activeLeafId,base=from===tree.activeLeafId?tree:{...tree,activeLeafId:from},argv=edit.command?.argv??[resolveSessionShell()],next=splitActive(base,edit.orientation,argv,edit.size??null);if(next===base)return tree;return edit.command?renameLeaf(next,next.activeLeafId,edit.command.title):next});if(after===before)return{tab,leaf:before.activeLeafId,changed:!1};let leaf=after.activeLeafId;try{await openLeafShell(host.rpc,splitLeafPtyKey(tabPtyKeyFor(taskId,tab),leaf),worktree,{size:edit.size,command:edit.command?.argv})}catch(error){throw await editTree(client,taskId,tabId,(tree)=>removeLeaf(tree,leaf)??tree).catch(()=>{return}),sessionFailed(taskId,`opening a split shell failed: ${error instanceof Error?error.message:String(error)}`,"the PTY host refused the shell \u2014 the split was taken back; check `rove daemon status`, then retry")}return{tab,leaf,changed:!0}}case"close":{let{tab,before,after}=await editTree(client,taskId,tabId,(tree)=>{let next=removeLeaf(tree,edit.leaf);if(next===null)throw new ApiError(`${edit.leaf} is the only pane; close the tab instead`,"LAST_LEAF");return next});if(after===before)return{tab,leaf:edit.leaf,changed:!1};return await killTaskSessions(host.rpc,[splitLeafPtyKey(tabPtyKeyFor(taskId,tab),edit.leaf)]),{tab,leaf:edit.leaf,changed:!0}}case"rename":{let{tab,before,after}=await editTree(client,taskId,tabId,(tree)=>renameLeaf(tree,edit.leaf,edit.title));return{tab,leaf:edit.leaf,changed:after!==before}}case"reset":{let{tab}=await editTree(client,taskId,tabId,(tree)=>tree),shellTab=tab.kind==="command";if(!shellTab&&edit.leaf==="leaf-1")throw new ApiError("reset respawns a login shell; this pane runs an engine","RESET_ENGINE",{hint:"close the engine tab and open a new one; shell tabs and split shells reset"});let key=splitLeafPtyKey(tabPtyKeyFor(taskId,tab),edit.leaf);return await killTaskSessions(host.rpc,[key],{wait:!0}),await openLeafShell(host.rpc,key,worktree,{size:edit.size,identity:shellTab&&edit.leaf==="leaf-1"?shellIdentityInput(taskId,tab.id):void 0}),{tab,leaf:edit.leaf,changed:!1}}}}finally{host.close()}}function hasLiveEngineTab(snapshot,taskId,sessions,engineAlive,engineBin){let labelled=new Set((snapshot?.tabs??[]).filter((t)=>t.kind==="engine").map((t)=>t.id));return sessions.some((s)=>{if(s.alive!==!0||!isHostedTaskKey(s.key,taskId))return!1;if(engineAlive?.get(s.key)===!1)return!1;if(s.key===`${taskId}::tab-1`)return!0;let tabId=s.key.slice(taskId.length+2);return labelled.has(tabId)||sessionArgvNamesEngine(s.command,engineBin)})}async function ensureWorktree(client,target){if(target.worktreePath)return target.worktreePath;let res=await client.request("task.ensureWorktree",{taskId:target.id});if(!res.worktreePath)throw new ApiError(`task ${target.id} has no worktree`,"NO_WORKTREE");return res.worktreePath}async function deliverHosted(client,target,worktree,prompt){let host=await ensureHostFor(target.id,"no PTY host to run the engine in \u2014 nothing was started; check `rove daemon status`, then retry the same send");try{if(target.tab){let engineBin=engineLaunchArgv({command:target.command,vendor:target.vendor,effort:target.modelEffort,model:target.model})[0],tabId=target.tab,respawnRepos=target.respawn&&target.multiRepo?await launchReposOf(client,target.id):void 0;return await deliverToExactTab(host.rpc,target.id,tabId,worktree,prompt,{engineBin,maxLiveEngines:persistedMaxLiveEngines(),...target.respawn?{respawn:()=>restoredTabLaunch({...target,repos:respawnRepos},tabId,worktree,resolveSessionShell())}:{}})}let{argv,sessionId}=withPinnedSessionId(engineLaunchArgv({command:target.command,vendor:target.vendor,effort:target.modelEffort,model:target.model}),target.vendor);trustEngineWorktree(target.vendor,worktree);let repos=target.multiRepo?await launchReposOf(client,target.id):void 0;trustExtraRepos(target.vendor,repos);let launch=buildEngineSessionLaunch({task:{id:target.id,kind:target.kind,vendor:target.vendor,repo:target.repo,repos},worktreePath:worktree,shell:resolveSessionShell(),argv,promptIntent:target.newTask?{kind:"new-task",prompt}:{kind:"explicit",prompt}}),result=await deliverHostedPrompt(host.rpc,{id:target.id,engineBin:argv[0],scope:await ptyScopeOf(client,{id:target.id}),...target.firstConversation?{firstConversation:!0}:{}},worktree,prompt,launch,{maxLiveEngines:persistedMaxLiveEngines(),retryArgs:["api","send","--task-id",target.id,"--plain","--prompt",prompt]});if(result.started&&!result.delivered)throw sessionFailed(target.id,`failed to start hosted engine session for ${target.id}`,"the session was created but no engine ran in it \u2014 fix the task's launch command (`api update --command`), then retry the send",{session:result.session,engineReady:result.engineReady,...result.reason?{reason:result.reason}:{}});if(await publishCliTabSnapshot(client,target.id,result.started?sessionId:void 0),result.started&&sessionId){let startedTab=result.session.split("::")[1];if(startedTab)await markCliTabSession(client,target.id,startedTab,sessionId)}return result}catch(error){if(error instanceof ApiError)throw error;throw sessionFailed(target.id,`hosted engine session failed for ${target.id}: ${error instanceof Error?error.message:String(error)}`,"the hosted session threw while starting or delivering \u2014 read its terminal tail for the cause, then retry the send")}finally{host.close()}}var realPromptDeliveryOps={deliverHosted};async function closeHeadlessTerminalTab(client,taskId,tabId){let host=await openPtyHost();try{let sessions=host?await listSessions(host.rpc):[],saved=(await readDaemonTabs(client,taskId))?.tabs.find((tab)=>tab.id===tabId),directKey=`${taskId}::${tabId}`,unregisteredAlive=sessions.some((session)=>session.key===directKey&&session.alive);if(!saved&&!unregisteredAlive)throw new ApiError(`tab ${tabId} does not exist on task ${taskId}`,"TAB_NOT_FOUND",{hint:"refresh the task's tab ids with get-task, then retry with one of its .tabs[].id values",nextCommandArgs:["api","get-task","--task-id",taskId]});let closing=saved?await closeTabsSnapshot(client,taskId,tabId):void 0;if(saved&&!closing)throw new ApiError(`tab ${tabId} no longer exists on task ${taskId}`,"TAB_NOT_FOUND",{hint:"the tab closed while this command was running; refresh with get-task before retrying",nextCommandArgs:["api","get-task","--task-id",taskId]});let baseKey=closing?tabPtyKeyFor(taskId,closing):directKey,keys=!(closing?.kind==="engine"&&closing.ptyTask&&closing.ptyTask.tabId===void 0)?sessions.filter((session)=>session.key===baseKey||session.key.startsWith(`${baseKey}::`)).map((session)=>session.key):[],wasAlive=sessions.some((session)=>keys.includes(session.key)&&session.alive);if(host)await killTaskSessions(host.rpc,keys);return{kind:closing?.kind??"engine",wasAlive}}finally{host?.close()}}async function deliverPrompt(client,target,prompt,ops=realPromptDeliveryOps){let worktree=await ensureWorktree(client,target);if(target.newTask)await client.request("task.observeLanguage",{taskId:target.id,text:prompt}).catch(()=>{});let hosted=await ops.deliverHosted(client,target,worktree,prompt);if(!hosted)throw sessionFailed(target.id,`failed to start hosted engine session for ${target.id}`,"delivery returned nothing \u2014 the task exists but has no session; retry with `api send --tab new`");return hosted}async function readLiveTaskIds(tasks){let listed=await handlePtyList();if(!listed?.sessions)return null;let live=new Set;for(let row of listed.sessions){if(row.alive===!1||typeof row.key!=="string")continue;let[taskId,tabId]=row.key.split("::");if(taskId)live.add(resolvePtyOwner(tasks,taskId,tabId))}return live}var defaultApiRuntime={liveTaskIds:readLiveTaskIds,isTaskRunning:async(taskId,engineArgv,scope)=>(await defaultApiRuntime.taskTabs(taskId,engineArgv,scope)).running,taskTabs:async(taskId,engineArgv,scope)=>{let listed=null,host=await openPtyHost();if(host)try{listed=await listSessionsOrNull(host.rpc)}finally{host.close()}let visible=listed?unclaimedSessions(listed,scope):null,sessions=visible??[],hostReachable=listed!==null,liveVendors,engineAlive;try{let{engineProcessIn,foregroundEngineIn:foregroundEngineIn2,parsePsSnapshot:parsePsSnapshot2,psSnapshot:psSnapshot2}=await import("./chunk-sakkdjyn.js"),walkable=sessions.filter((s)=>s.alive&&typeof s.pid==="number"&&s.pid>0);if(walkable.length>0){let rows=parsePsSnapshot2(await psSnapshot2(walkable.map((s)=>s.pid)));liveVendors=new Map(walkable.map((s)=>[s.key,foregroundEngineIn2(rows,s.pid)?.vendor??null])),engineAlive=new Map(walkable.map((s)=>[s.key,engineProcessIn(rows,s.pid,engineArgv)]))}else engineAlive=new Map}catch{}let exits={};try{exits=(await import("./chunk-6f9ypgbs.js")).readPtyExitRecords()}catch{}let snapshot=readTabsSnapshot(taskId),sessionIds=new Map((snapshot?.tabs??[]).map((t)=>[t.id,t.sessionId??void 0]));return{tabs:joinTaskTabs(snapshot,taskId,visible,exits,liveVendors,engineAlive).map((row)=>{let sessionId=sessionIds.get(row.id);return sessionId?{...row,sessionId}:row}),running:hostReachable?hasLiveEngineTab(snapshot,scope?.alias?.taskId??taskId,scope?.alias?sessions.filter((s)=>isUnderKey(s.key,aliasKey(scope.alias))):sessions,engineAlive,engineArgv?.[0]):null}},closeTerminalTab:closeHeadlessTerminalTab,deliverPrompt:(client,target,prompt)=>deliverPrompt(client,target,prompt),openTab:async(client,target,kind,opts)=>openNewTab(client,target,await ensureWorktree(client,target),kind,opts),launchTab:(client,spec)=>launchTab(client,spec,(taskId,tabId,worktree,pane)=>splitTab(client,taskId,tabId,worktree,{kind:"open",orientation:"row",from:"",command:{argv:pane.argv,title:pane.title}})),splitTab,reviveTab,resolveRepoRoot:async(absPath)=>(await import("./chunk-hv79ys32.js")).resolveMainRepoRoot(absPath),isUsableRepo:async(absPath)=>{let{isGitRepo,isRemoteRepoKey:isRemoteRepoKey2}=await import("./chunk-hv79ys32.js");return isRemoteRepoKey2(absPath)||isGitRepo(absPath)},isValidBranchName:async(branch)=>(await import("./chunk-hv79ys32.js")).isValidBranchName(branch),defaultVendor:async(repo)=>{let{getGlobalDefaultVendor,getRepoLastActiveVendor}=await import("./chunk-x1ghdmp1.js");return(repo?getRepoLastActiveVendor(repo):void 0)??getGlobalDefaultVendor()},readWorktreeChanges:async(worktreePath)=>(await import("./chunk-5yfffvqe.js")).readWorktreeChanges(worktreePath),readBranchSignals:async(worktreePath,recordedBaseRef)=>(await import("./chunk-9xtg2k54.js")).readBranchSignals(worktreePath,recordedBaseRef),tearDownSession:async(taskId,scope)=>{let host=await openPtyHost();if(host)try{await killTaskSessions(host.rpc,taskKeys(await listSessions(host.rpc),taskId,scope))}catch{}finally{host.close()}},listeningPorts:async()=>(await import("./chunk-b95q6zhk.js")).scanListeningPorts(),measureCheckout:async(path2)=>{let{measureCheckout}=await import("./chunk-cqrr5g2e.js"),{execHostForWorktreePath}=await import("./chunk-teh40faw.js");return measureCheckout(execHostForWorktreePath(path2),path2)}};async function checkMenu(daemon,taskId,tab,fallback){try{let reply=await daemon.request("send.check",{taskId,...tab?{tabId:tab}:{}});return{waiting:reply.awaiting.length>0?reply.awaiting:null,settleMs:reply.settleMs}}catch{}let tabs;try{tabs=(await daemon.request("status.list")).tasks[taskId]?.tabs}catch{return{waiting:fallback?.state==="permission_needed"?tab?[tab]:[]:null,settleMs:0}}let waiting=Object.entries(tabs??{}).filter(([id,s])=>s.kind==="needs-input"&&(tab===void 0||id===tab)).map(([id])=>id);return{waiting:waiting.length>0?waiting:null,settleMs:0}}async function tabsAwaitingChoice(daemon,taskId,tab,fallback){let check=await checkMenu(daemon,taskId,tab,fallback);if(check.waiting===null&&check.settleMs>0){let{promise,resolve:resolve3}=Promise.withResolvers();setTimeout(resolve3,check.settleMs),await promise,check=await checkMenu(daemon,taskId,tab,fallback)}return check.waiting}function awaitingChoiceError(taskId,tabs){let where=tabs.length>0?` (${tabs.join(", ")})`:"";return new ApiError(`not sent: task ${taskId}${where} is showing an approval or question menu (or came to rest straight from one); a paste would land in it and Enter would confirm the highlighted option`,"AWAITING_PERMISSION",{taskId,...tabs.length>0?{tabIds:tabs}:{},hint:"answer the menu with keys in the task's terminal (the TUI, or the web page's keys), then send again; read the menu with read-output --source terminal",nextCommandArgs:["api","read-output","--task-id",taskId,"--source","terminal"]})}async function spawnSharedChild(daemon,parentTaskId,command,title){let vendor=command?resolveCommandProtocol(command):void 0;return(await daemon.request("task.spawn",{parentTaskId,worktree:!1,...title?{title}:{},...command?{command}:{},...vendor?{vendor}:{}})).task}var FIRST_CONVERSATION_LOCK_MS=120000;function withFirstConversationLock(taskId,fn){return withDeliveryLock(`${taskId}::new-conversation`,fn,{timeoutMs:FIRST_CONVERSATION_LOCK_MS})}async function engineStarted(daemon,runtime,task){if(task.ptyAlias)return!0;let engines=((await readDaemonTabs(daemon,task.id))?.tabs??[]).filter((tab)=>tab.kind==="engine");if(engines.length===0)return!1;if(engines.some((tab)=>tab.spawned))return!0;let{tabs:rows}=await runtime.taskTabs(task.id,taskEngineArgv(task),await ptyScopeOf(daemon,task));return rows.find((row)=>row.id===engines[0]?.id)?.alive===!0}async function conversationHome(daemon,runtime,task,command,prompt){let title=prompt?deriveTitleFromPrompt(prompt):"";if(await engineStarted(daemon,runtime,task))return{task:await spawnSharedChild(daemon,task.id,command,title),child:!0};if(command&&command!==task.command){let vendor=resolveCommandProtocol(command);await daemon.request("task.setCommand",{taskId:task.id,command,...vendor?{vendor}:{}})}if(title&&task.title===PLACEHOLDER_TASK_TITLE)await daemon.request("task.rename",{taskId:task.id,title}).catch(()=>{return});return{task:(await daemon.request("task.get",{taskId:task.id})).task,child:!1}}var ENGINE_REFUSALS={ENGINE_LIMIT:!0,ENGINE_ADMISSION_BUSY:!0,ENGINE_ADMISSION_UNAVAILABLE:!0};async function failSharedChild(daemon,err,childTaskId,retryArgs){if(!(err instanceof ApiError))throw err;if(ENGINE_REFUSALS[err.code])throw await daemon.request("task.delete",{taskId:childTaskId}).catch(()=>{return}),new ApiError(err.message,err.code,{...err.data,nextCommandArgs:retryArgs});throw new ApiError(err.message,err.code,{...err.data,childTaskId})}async function issueUpdate(ctx){let title=ctx.args.str("title"),body=ctx.args.str("body"),task=ctx.args.str("task"),status=ctx.args.enumOf("status");if(title===void 0&&body===void 0&&task===void 0&&status===void 0)throw new ApiError("issue-update requires --title, --body, --task, and/or --status","MISSING_FLAG");let repoRoot=ctx.args.requireRepo("repo"),id=ctx.args.int("id"),state;if(title!==void 0||body!==void 0||task!==void 0)state=await simpleRpc(ctx,"issue.mutate",{repoRoot,op:{type:"update",id,title,body,...task===void 0?{}:{taskId:task==="none"?null:task}}});if(status!==void 0)state=await simpleRpc(ctx,"issue.mutate",{repoRoot,op:{type:"setStatus",id,status}});return state}function requirePromptText(ctx,verb){let text=ctx.args.promptText();if(text===void 0)throw new ApiError("--prompt (or --prompt-file) is required","MISSING_FLAG",helpStep(verb));return text}async function resolveSendTask(ctx,daemon,tab){let explicit=ctx.args.str("task-id");if(explicit)return{taskId:explicit,tab};let dispatcher=await readOwnDispatcher(daemon);if(dispatcher)return{taskId:dispatcher.taskId,tab:tab??await resolveDispatcherTab(ctx.runtime,daemon,dispatcher)};let active=await resolveActiveTaskId(daemon);if(!active)throw new ApiError("no --task-id given and no active task \u2014 open a task first or pass --task-id","MISSING_TARGET");return{taskId:active,tab}}async function send(ctx){let rollback={};try{if(ctx.args.str("tab")!=="new")return await sendNow(ctx,rollback);let{taskId}=await resolveSendTask(ctx,daemonOf(ctx),"new");return await withFirstConversationLock(taskId,()=>sendNow(ctx,rollback))}catch(err){throw await rollback.undo?.().catch(()=>{}),err}}async function sendNow(ctx,rollback){let daemon=daemonOf(ctx),handoffFrom=ctx.args.str("handoff-from"),tab=ctx.args.str("tab");if(handoffFrom&&tab!=="new")throw new ApiError(`--handoff-from starts a new conversation; pass --tab new (got --tab ${tab??"<canonical>"})`,"BAD_FLAG",helpStep("send"));let replace=ctx.args.bool("replace")===!0;if(replace&&!handoffFrom)throw new ApiError("--replace takes over a tab for a handoff; pass --handoff-from tab-N","BAD_FLAG",helpStep("send"));let prompt=handoffFrom?ctx.args.promptText()??"":requirePromptText(ctx,"send");if(tab&&tab!=="new"&&!/^tab-[A-Za-z0-9-]+$/.test(tab))throw new ApiError(`--tab must be "new" or a tab id like tab-2 (got ${JSON.stringify(tab)})`,"BAD_TAB");let tabCommand=ctx.args.str("command");if(tabCommand&&tab!=="new")throw new ApiError(`--command only applies to a new tab; pass --tab new (got --tab ${tab??"<canonical>"})`,"BAD_FLAG",helpStep("send"));let respawn=ctx.args.bool("respawn");if(assertQueueFlags(ctx,tab),respawn&&(tab===void 0||tab==="new"))throw new ApiError(`--respawn addresses one frozen tab; pass --tab tab-N (got --tab ${tab??"<canonical>"})`,"BAD_FLAG",helpStep("send"));let sendTo=await resolveSendTask(ctx,daemon,tab),taskId=sendTo.taskId;tab=sendTo.tab;let res=await daemon.request("task.get",{taskId});if(handoffFrom)prompt=[await handoffBrief(ctx,res.task,handoffFrom),prompt].filter(Boolean).join(`
5
5
 
@@ -7,9 +7,9 @@ import{PER_SESSION,classifyTier,describeTierBlock,isAutoRoutingTier,listFiles,li
7
7
 
8
8
  ${plain}`:plain,expectReport=ctx.args.bool("expect-report")===!0,pinnedTab=tab?{tabId:tab}:{},admit=ctx.args.bool("if-runnable")?await daemon.request("send.admit",{taskId,...tab?{tabId:tab}:{},...respawn?{respawn}:{},expectReport,promptHead:prompt}):void 0;if(admit?.skipped)throw new ApiError(`not sent: ${admit.skipped.reason}`,"SKIPPED",{taskId,skipped:admit.skipped,...admit.deliveryId?{deliveryId:admit.deliveryId}:{}});let claimId=admit?.claimId,armed=expectReport||res.task.requireReport===!0,deliveryId=admit?admit.deliveryId:armed?(await daemon.request("delivery.arm",{taskId,...pinnedTab,promptHead:prompt})).deliveryId:void 0,text=deliveryId?`${withAnswers}
9
9
 
10
- ${deliveryFooter(deliveryId,roveApiInvocation())}`:withAnswers,settle=async(delivered2,session)=>{if(claimId)await daemon.request("send.release",{claimId,delivered:delivered2}).catch(()=>{});let landedTab=session?.split("::")[1]??(tab&&tab!=="new"?tab:void 0);if(await recordSend(daemon,{taskId,tabId:landedTab,prompt,delivered:delivered2,deliveryId}),!deliveryId)return;await daemon.request("delivery.settle",{deliveryId,delivered:delivered2,...landedTab?{tabId:landedTab}:{}}).catch(()=>{})},delivered;try{delivered=await ctx.runtime.deliverPrompt(daemon,{id:alias&&tab===alias.tabId?alias.taskId:taskId,worktreePath:taskCwd(res.task),kind:res.task.kind,vendor:res.task.vendor,command:res.task.command,modelEffort:res.task.modelEffort,model:res.task.model,repo:res.task.repo,...res.task.extraRepos?.length||res.task.sharesWorkspaceOf?{multiRepo:!0}:{},tab,respawn,...freshConversation&&!childTaskId?{firstConversation:!0}:{}},text)}catch(err){if(await settle(!1),!childTaskId)throw err;return failSharedChild(daemon,err,childTaskId,["api","send","--task-id",requestedTaskId,"--tab","new",...tabCommand?["--command",tabCommand]:[],"--plain","--prompt",prompt])}if(!delivered.delivered){let mayLand=promptMayHaveLanded(delivered);if(mayLand)rollback.undo=void 0;throw await settle(mayLand,mayLand?delivered.session:void 0),new ApiError(`prompt was not confirmed in ${taskId}'s engine (paste did not land)`,"NOT_DELIVERED",childTaskId?{childTaskId}:void 0)}if(await settle(!0,delivered.session),owed)await markGateAnswersDelivered(daemon,owed.gateIds);if(freshConversation)await daemon.request("task.setPrompt",{taskId,prompt}).catch((err)=>{console.error(`[rove api send] task.setPrompt failed for ${taskId}:`,err)});return{ok:!0,taskId:requestedTaskId,...childTaskId?{childTaskId}:{},...replacedTab?{replacedTab}:{},session:delivered.session,...deliveryId?{deliveryId}:{},started:delivered.started,engineReady:delivered.engineReady,delivered:delivered.delivered,...delivered.bytes===void 0?{}:{bytes:delivered.bytes},...delivered.promptEcho?{promptEcho:delivered.promptEcho}:{},...delivered.reason?{reason:delivered.reason}:{},...delivered.respawned?{respawned:!0}:{},...delivered.frozenTabs?.length?{frozenTabs:delivered.frozenTabs}:{},...target&&TARGET_FAILED.has(target.state)?{targetState:target.state,...target.detail?{targetDetail:target.detail}:{}}:{}}}var TARGET_FAILED=new Set(["error","dead"]);async function dispatch(ctx){let daemon=daemonOf(ctx),taskId=ctx.args.require("task-id"),text=requirePromptText(ctx,"dispatch"),tabId=ctx.args.str("tab"),self=await ownSession(daemon).catch(()=>null),reply=await daemon.request("session.deliver",{taskId,text,...tabId!==void 0?{tabId}:{},source:"dispatcher",...self?{fromTaskId:self.taskId,fromTabId:self.tabId}:{}});return{ok:!0,taskId,...reply?.tabId!==void 0?{tabId:reply.tabId}:tabId!==void 0?{tabId}:{},routed:"session.deliver",...reply?.delivered!==void 0?{delivered:reply.delivered}:{},...reply?.reason!==void 0?{reason:reply.reason}:{},...reply?.layer!==void 0?{layer:reply.layer}:{},...reply?.clients!==void 0?{clients:reply.clients}:{}}}var DISPATCH_VERB={name:"dispatch",group:"drive",summary:'Route text into a task\'s live session. The dispatcher\'s messenger (docs/design/dispatcher.md); unlike `send`, it never starts an engine \u2014 it requires an already-hosted session. The daemon pastes into it and reports `delivered`; `delivered:false` with `reason:"busy"` means a human is mid-message, and `reason:"broadcast"` means no hosted session answered and the text went out on the session.deliver channel for a browser to pick up (unconfirmable; `clients: 0` proves it reached nobody).',flags:[F.taskId(!0),F.prompt(!0,"Text delivered into the task's engine session."),F.promptFile(),{name:"tab",type:"string",required:!1,placeholder:"TAB",description:"Deliver into exactly this tab (e.g. tab-3) instead of the canonical engine tab."}],handler:dispatch};async function note(ctx){let daemon=daemonOf(ctx),taskId=ctx.args.require("task-id"),text=ctx.args.require("text");return await daemon.request("note.file",{taskId,text})}async function getTask(ctx){let daemon=daemonOf(ctx),taskId=ctx.args.require("task-id"),res=await daemon.request("task.get",{taskId}),{tabs,running}=await ctx.runtime.taskTabs(taskId,taskEngineArgv(res.task),await ptyScopeOf(daemon,res.task)),runnability=await readRunnability(daemon,taskId);return{task:res.task,running,tabs,...runnability?{runnability}:{}}}async function list(ctx){let daemon=daemonOf(ctx),local=await daemon.request("task.list"),{mergeTaskList}=await import("./chunk-j8d9sk5j.js"),all=ctx.caller.cli?await mergeTaskList(local):local,wantArchived=ctx.args.bool("archived")??!1,merged={...all,tasks:(all.tasks??[]).filter((task)=>task.archived===!0===wantArchived)},filters=listFilters(ctx),fields=listFields(ctx),result=filters?{...merged,...await filterTaskList(ctx,daemon,merged.tasks??[],filters)}:merged;if(!result.tasks)return result;let tasks=result.tasks.map((t)=>t.extraRepos?.length?{...t,repoCount:t.extraRepos.length+1}:t);return{...result,tasks:fields?projectTasks(tasks,fields):tasks}}async function setActive(ctx){let daemon=daemonOf(ctx),taskId=ctx.args.bool("none")?null:ctx.args.require("task-id");return await daemon.request("task.setActive",{taskId}),{ok:!0,activeTaskId:taskId}}var RECOVER=["api","engine-list"];async function routineRunEngine(ctx,repo,vendor){return vendor??(repo?await ctx.runtime.defaultVendor(repo):void 0)??DEFAULT_TASK_VENDOR}async function pinnedEngine(ctx,repo,vendor,model,effort){let engine=await routineRunEngine(ctx,repo,vendor),protocol=taskEngine({vendor:engine});if(model)assertEngineAcceptsModel(protocol,model,RECOVER);if(effort)assertEngineAcceptsEffort(protocol,effort,RECOVER);return engine}async function createEngineFields(ctx,repo){let vendor=ctx.args.vendor(),model=ctx.args.str("model")?.trim(),effort=ctx.args.str("effort")?.trim();if(!model&&!effort)return vendor?{vendor}:{};return{vendor:await pinnedEngine(ctx,repo,vendor,model,effort),...model?{model}:{},...effort?{effort}:{}}}async function updateEngineFields(ctx,id){let hasVendor=ctx.args.present("vendor"),hasModel=ctx.args.present("model"),hasEffort=ctx.args.present("effort");if(!hasVendor&&!hasModel&&!hasEffort)return{};let vendor=ctx.args.vendor(),model=ctx.args.str("model")?.trim()||void 0,effort=ctx.args.str("effort")?.trim()||void 0,fields={...hasVendor?{vendor:vendor??null}:{},...hasModel?{model:model??null}:{},...hasEffort?{effort:effort??null}:{}},{automations}=await daemonOf(ctx).request("automation.list",{}),routine=automations.find((a)=>a.id===id);if(!routine)return fields;let nextModel=hasModel?model:routine.model,nextEffort=hasEffort?effort:routine.modelEffort;if(!nextModel&&!nextEffort)return fields;let nextVendor=hasVendor?vendor:routine.vendor,engine=await pinnedEngine(ctx,routine.repo,nextVendor,nextModel,nextEffort);return engine===routine.vendor&&!hasVendor?fields:{...fields,vendor:engine}}var SCHEDULE_FLAG={name:"schedule",type:"string",placeholder:"CRON",description:"Five-field cron in the daemon host's local time, e.g. '0 9 * * MON-FRI'."},PRECHECK_FLAGS=[{name:"precheck",type:"string",placeholder:"CMD",description:"Shell command run in the repo BEFORE the engine starts. Non-zero exit skips the run without spawning an agent \u2014 the cheap way to avoid burning a turn when nothing changed."},{name:"precheck-timeout",type:"int",placeholder:"SEC",default:"120",description:"Seconds before the precheck is killed and the run skipped."}],SCRIPT_FLAGS=[{name:"script",type:"string",placeholder:"CMD",description:"Run this shell command in the repo instead of an engine: no task, no worktree, no agent turn. Exit 0 records `ran`, anything else `script_failed`; non-empty stdout (last 4000 chars) is the run's response. Replaces --prompt and the engine flags. '' on update turns it back into a prompt routine."},{name:"script-timeout",type:"int",placeholder:"SEC",default:"120",description:"Seconds before the script is killed and the run recorded as script_failed (max 600)."}],NOTIFY_FLAG={name:"notify",type:"bool",description:"Hand every run's response, and runs that need a human, to the bound chat channel's agent (`rove weixin login`), which decides what to tell you. Off by default."};function scriptPayload(ctx){if(!ctx.args.present("script")){if(ctx.args.present("script-timeout"))throw new ApiError("--script-timeout requires --script (pass the command again to change its timeout)","BAD_FLAG");return{}}let command=ctx.args.str("script");if(command===void 0)return{script:null};return{script:{command,timeoutSeconds:ctx.args.int("script-timeout")??120}}}var GRACE_FLAG={name:"grace",type:"uint",placeholder:"MIN",default:"60",description:"How late a missed occurrence may still run when the daemon was down. Only the most recent missed occurrence is ever run."},WEBHOOK_FLAG={name:"webhook",type:"bool",description:"Also fire on a signed POST to the web server's /hooks/routines/<id> (GitHub's X-Hub-Signature-256). Turning it on generates the secret, shown as .webhook.secret; paste it into the GitHub webhook with content type application/json. The precheck still runs, with ROVE_WEBHOOK_EVENT and ROVE_WEBHOOK_ACTION set, and the prompt starts with what fired it. Off again forgets the secret."},PERSISTENT_FLAG={name:"persistent-session",type:"bool",description:"Re-deliver into ONE standing task instead of a fresh worktree per run \u2014 for a routine that needs yesterday's context (a trend check). Its task is folded behind the sidebar's routine count row. Leave off for a routine that EDITS code: a week of runs on one branch is a branch nobody can land."},MODEL_FLAG={name:"model",type:"string",placeholder:"MODEL",description:"Model every run's engine launches on, in the engine's own spelling (`engine-list` shows suggestions). Rejected (BAD_MODEL) on an engine that declares no model flag."},EFFORT_FLAG={name:"effort",type:"string",placeholder:"LEVEL",description:"Reasoning effort every run's engine launches at. Validated against the engine's declared levels (BAD_EFFORT otherwise)."},TARGET_FLAGS=[{name:"target-task",type:"string",placeholder:"ID",description:"Existing task id. Requires --target-tab; never creates or revives a session. Pass both empty to clear on update."},{name:"target-tab",type:"string",placeholder:"TAB",description:"Exact existing engine tab id, e.g. tab-2. Requires --target-task."}];function targetPayload(ctx){let hasTask=ctx.args.present("target-task"),hasTab=ctx.args.present("target-tab");if(!hasTask&&!hasTab)return{};if(!hasTask||!hasTab)throw new ApiError("--target-task and --target-tab must be supplied together","BAD_FLAG");let taskId=ctx.args.str("target-task"),tabId=ctx.args.str("target-tab");if(!taskId&&!tabId)return{target:null};if(!taskId||!tabId)throw new ApiError("both target values must be nonempty, or both empty to clear","BAD_FLAG");return{target:{kind:"existing-tab",taskId,tabId}}}function precheckPayload(ctx){if(!ctx.args.present("precheck")){if(ctx.args.present("precheck-timeout"))throw new ApiError("--precheck-timeout requires --precheck (pass the command again to change its timeout)","BAD_FLAG");return{}}let command=ctx.args.str("precheck");if(command===void 0)return{precheck:null};return{precheck:{command,timeoutSeconds:ctx.args.int("precheck-timeout")??120}}}var ROUTINE_VERBS=[{name:"routine-list",group:"routine",summary:"List scheduled routines with their next run time.",flags:[],handler:(ctx)=>simpleRpc(ctx,"automation.list",{})},{name:"routine-create",group:"routine",summary:"Schedule a prompt. Each firing creates a fresh task (worktree + engine, or with --no-repo a directory session) and delivers it. Use --target-task and --target-tab for an existing conversation, or --script to run a command with no engine. An enabled routine keeps the daemon alive.",flags:[{...F.repo(!1),description:"Repo root (git toplevel) every run works in. Relative paths resolve against $PWD. Required unless --no-repo."},{name:"no-repo",type:"bool",description:"A routine with no project: each run works in its own directory (<home>/.rove/routines/<id>) and its session sits in the sidebar's Ungrouped group. Takes no --base-branch."},{name:"name",type:"string",required:!0,placeholder:"N",description:"Routine name."},F.prompt(!1,"Text delivered to the selected conversation or a new session. Required unless --script."),F.promptFile(),{...SCHEDULE_FLAG,required:!0},{...F.vendor(),description:"Engine every run launches. Omitted with --model/--effort: the repo's default engine (falling back to the built-in default) is resolved, validated and stored. Omitted without pins: runs launch the built-in default engine."},MODEL_FLAG,EFFORT_FLAG,{name:"base-branch",type:"string",placeholder:"B",description:"Base ref each run's worktree branches from."},...PRECHECK_FLAGS,...SCRIPT_FLAGS,NOTIFY_FLAG,WEBHOOK_FLAG,GRACE_FLAG,PERSISTENT_FLAG,...TARGET_FLAGS,{name:"disabled",type:"bool",description:"Create it paused instead of active."}],handler:async(ctx)=>{let noRepo=ctx.args.bool("no-repo")===!0,hasRepo=ctx.args.str("repo")!==void 0;if(noRepo&&hasRepo)throw new ApiError("--repo and --no-repo conflict \u2014 pass one","CONFLICTING_FLAGS");if(!noRepo&&!hasRepo)throw new ApiError("pass exactly one of --repo PATH or --no-repo","MISSING_FLAG");let repo=noRepo?void 0:ctx.args.requireRepo("repo"),script=scriptPayload(ctx);if(script.script&&ctx.args.promptText()!==void 0)throw new ApiError("pass --prompt or --script, not both","BAD_FLAG");return simpleRpc(ctx,"automation.create",{...repo?{repo}:{},name:ctx.args.require("name"),...script.script?script:{prompt:requirePromptText(ctx,"routine-create")},schedule:ctx.args.require("schedule"),...ctx.args.bool("notify")?{notify:!0}:{},...ctx.args.bool("webhook")?{webhook:!0}:{},...await createEngineFields(ctx,repo),...ctx.args.str("base-branch")?{baseRef:ctx.args.str("base-branch")}:{},...precheckPayload(ctx),...targetPayload(ctx),...ctx.args.nonNegativeInt("grace")!==void 0?{missedRunGraceMinutes:ctx.args.nonNegativeInt("grace")}:{},...ctx.args.bool("persistent-session")?{persistentSession:!0}:{},...ctx.args.bool("disabled")?{enabled:!1}:{}})}},{name:"routine-update",group:"routine",summary:"Change a routine. A new --schedule re-anchors its next run; --precheck '' clears the precheck; --enabled false pauses it (disabling the last active one releases the daemon's keep-alive hold) and --enabled true resumes it.",flags:[{name:"id",type:"string",required:!0,placeholder:"ID",description:"Routine id."},{name:"name",type:"string",placeholder:"N",description:"New name."},F.prompt(!1,"New prompt."),F.promptFile(),SCHEDULE_FLAG,{...F.vendor(),type:"string",description:"New engine vendor; empty clears it (before binding an existing target). While model/effort stay pinned, a cleared vendor is re-resolved from the repo default and stored."},{...MODEL_FLAG,description:`${MODEL_FLAG.description} '' clears it.`},{...EFFORT_FLAG,description:`${EFFORT_FLAG.description} '' clears it.`},{name:"base-branch",type:"string",placeholder:"B",description:"New base ref ('' to clear)."},...PRECHECK_FLAGS,...SCRIPT_FLAGS,{...NOTIFY_FLAG,placeholder:"BOOL"},{...WEBHOOK_FLAG,placeholder:"BOOL"},GRACE_FLAG,PERSISTENT_FLAG,...TARGET_FLAGS,{name:"enabled",type:"bool",placeholder:"BOOL",description:"false pauses the routine, true resumes it."}],handler:async(ctx)=>{let id=ctx.args.require("id");return simpleRpc(ctx,"automation.update",{id,...ctx.args.str("name")!==void 0?{name:ctx.args.str("name")}:{},...ctx.args.promptText()!==void 0?{prompt:ctx.args.promptText()}:{},...ctx.args.str("schedule")!==void 0?{schedule:ctx.args.str("schedule")}:{},...await updateEngineFields(ctx,id),...ctx.args.present("base-branch")?{baseRef:ctx.args.str("base-branch")??null}:{},...precheckPayload(ctx),...scriptPayload(ctx),...ctx.args.present("notify")?{notify:ctx.args.bool("notify")??!0}:{},...ctx.args.present("webhook")?{webhook:ctx.args.bool("webhook")??!0}:{},...targetPayload(ctx),...ctx.args.nonNegativeInt("grace")!==void 0?{missedRunGraceMinutes:ctx.args.nonNegativeInt("grace")}:{},...ctx.args.present("persistent-session")?{persistentSession:ctx.args.bool("persistent-session")??!0}:{},...ctx.args.present("enabled")?{enabled:ctx.args.bool("enabled")??!0}:{}})}},{name:"routine-delete",group:"routine",summary:"Delete a routine and its run history. Tasks it already created are untouched.",flags:[{name:"id",type:"string",required:!0,placeholder:"ID",description:"Routine id."}],handler:(ctx)=>simpleRpc(ctx,"automation.delete",{id:ctx.args.require("id")})},{name:"routine-run-now",group:"routine",summary:"Run a routine immediately, skipping its precheck (asking for it IS the answer). Does not shift its schedule.",flags:[{name:"id",type:"string",required:!0,placeholder:"ID",description:"Routine id."}],handler:(ctx)=>simpleRpc(ctx,"automation.runNow",{id:ctx.args.require("id")})},{name:"routine-respond",group:"routine",summary:`Report this run's result. Every delivered routine prompt starts with a [ROVE ROUTINE] line naming its run; this is the routine-flavoured spelling of \`report --delivery <id> --outcome done --summary <text>\` and writes the same delivery report, so \`deliveries\` shows it too. One response per run \u2014 responding again replaces it. Cap ${ROUTINE_RESPONSE_MAX_CHARS} characters; markdown renders on the Routines page.`,flags:[{name:"run",type:"string",required:!0,placeholder:"RUN_ID",description:"Run id from the [ROVE ROUTINE] line."},{name:"text",type:"string",placeholder:"TEXT",description:"The response. Exactly one of --text / --prompt-file."},{...F.promptFile(),description:"Read the response from this file (`-` = stdin) \u2014 use it for anything multi-line or with backticks/$vars."}],handler:async(ctx)=>{let runId=ctx.args.require("run"),inline=ctx.args.str("text"),file=ctx.args.promptText();if(inline!==void 0&&file!==void 0)throw new ApiError("pass --text or --prompt-file, not both","BAD_FLAG");let text=inline??file;if(text===void 0)throw new ApiError("--text (or --prompt-file) is required","MISSING_FLAG");if(text.length>ROUTINE_RESPONSE_MAX_CHARS)throw new ApiError(`response is ${text.length} characters; the cap is ${ROUTINE_RESPONSE_MAX_CHARS}`,"RESPONSE_TOO_LARGE",{hint:"Summarize: lead with the conclusion, link or name the files that hold the detail."});let res=await simpleRpc(ctx,"automation.respond",{runId,text});if(res.run===null)throw new ApiError(`no routine run ${runId}`,"RUN_NOT_FOUND",{hint:"Copy the id from the [ROVE ROUTINE] line of the prompt you were given; `routine-runs --id <routine>` lists run ids."});return res}},{name:"routine-runs",group:"routine",summary:"Run history, newest first. Statuses: dispatched, revived (standing session respawned \u2014 files kept, conversation did not), skipped_cancelled (disabled, changed or stopped before delivery), skipped_precheck (nothing to do), skipped_blocked (should-run refused the tick: `blocked` is the verdict {reason, detail?, until?, gateIds?} \u2014 busy, awaiting-owner, report-missing, quota, awaiting-permission; not an alarm, the cause is already in the Inbox; a manual run-now bypasses it), skipped_missed, skipped_unavailable, ran / script_failed (a --script routine: exit 0 or not; stdout is the response), dispatch_failed (also a should-run refusal for engine-dead or engine-error, which have no Inbox row of their own: it keeps `blocked` and raises the routine alarm). Each run carries `deliveryId`, `deliveryStatus` (awaiting|reported|missing) and `response` ({text, at}) once its agent reported (routine-respond or report).",flags:[{name:"id",type:"string",required:!0,placeholder:"ID",description:"Routine id."}],handler:(ctx)=>simpleRpc(ctx,"automation.runs",{id:ctx.args.require("id")})}];async function applyPostCreateFlags(daemon,taskId,args){let status=args.enumOf("status");if(status)await daemon.request("task.status",{taskId,status});let pin=args.bool("pin");if(pin!==void 0)await daemon.request("task.pin",{taskId,pinned:pin});let requireReport=args.bool("require-report");if(requireReport!==void 0)await daemon.request("task.requireReport",{taskId,required:requireReport});return Boolean(status)||pin!==void 0||requireReport!==void 0}async function deliverArmed(ctx,daemon,taskId,target,text){if(ctx.args.bool("require-report")!==!0&&ctx.args.bool("expect-report")!==!0)return ctx.runtime.deliverPrompt(daemon,target,text);let{deliveryId}=await daemon.request("delivery.arm",{taskId,tabId:"tab-1",promptHead:text}),settle=(delivered)=>daemon.request("delivery.settle",{deliveryId,delivered,tabId:"tab-1"}).catch(()=>{});try{let delivered=await ctx.runtime.deliverPrompt(daemon,target,`${text}
10
+ ${deliveryFooter(deliveryId,roveApiInvocation())}`:withAnswers,settle=async(delivered2,session)=>{if(claimId)await daemon.request("send.release",{claimId,delivered:delivered2}).catch(()=>{});let landedTab=session?.split("::")[1]??(tab&&tab!=="new"?tab:void 0);if(await recordSend(daemon,{taskId,tabId:landedTab,prompt,delivered:delivered2,deliveryId}),!deliveryId)return;await daemon.request("delivery.settle",{deliveryId,delivered:delivered2,...landedTab?{tabId:landedTab}:{}}).catch(()=>{})},delivered;try{delivered=await ctx.runtime.deliverPrompt(daemon,{id:alias&&tab===alias.tabId?alias.taskId:taskId,worktreePath:taskCwd(res.task),kind:res.task.kind,vendor:res.task.vendor,command:res.task.command,modelEffort:res.task.modelEffort,model:res.task.model,repo:res.task.repo,...res.task.extraRepos?.length||res.task.sharesWorkspaceOf?{multiRepo:!0}:{},tab,respawn,...freshConversation&&!childTaskId?{firstConversation:!0}:{}},text)}catch(err){if(await settle(!1),!childTaskId)throw err;return failSharedChild(daemon,err,childTaskId,["api","send","--task-id",requestedTaskId,"--tab","new",...tabCommand?["--command",tabCommand]:[],"--plain","--prompt",prompt])}if(!delivered.delivered){let mayLand=promptMayHaveLanded(delivered);if(mayLand)rollback.undo=void 0;throw await settle(mayLand,mayLand?delivered.session:void 0),new ApiError(`prompt was not confirmed in ${taskId}'s engine (paste did not land)`,"NOT_DELIVERED",childTaskId?{childTaskId}:void 0)}if(await settle(!0,delivered.session),owed)await markGateAnswersDelivered(daemon,owed.gateIds);if(freshConversation)await daemon.request("task.setPrompt",{taskId,prompt}).catch((err)=>{console.error(`[rove api send] task.setPrompt failed for ${taskId}:`,err)});return{ok:!0,taskId:requestedTaskId,...childTaskId?{childTaskId}:{},...replacedTab?{replacedTab}:{},session:delivered.session,...deliveryId?{deliveryId}:{},started:delivered.started,engineReady:delivered.engineReady,delivered:delivered.delivered,...delivered.bytes===void 0?{}:{bytes:delivered.bytes},...delivered.promptEcho?{promptEcho:delivered.promptEcho}:{},...delivered.reason?{reason:delivered.reason}:{},...delivered.respawned?{respawned:!0}:{},...delivered.frozenTabs?.length?{frozenTabs:delivered.frozenTabs}:{},...target&&TARGET_FAILED.has(target.state)?{targetState:target.state,...target.detail?{targetDetail:target.detail}:{}}:{}}}var TARGET_FAILED=new Set(["error","dead"]);async function dispatch(ctx){let daemon=daemonOf(ctx),taskId=ctx.args.require("task-id"),text=requirePromptText(ctx,"dispatch"),tabId=ctx.args.str("tab"),self=await ownSession(daemon).catch(()=>null),reply=await daemon.request("session.deliver",{taskId,text,...tabId!==void 0?{tabId}:{},source:"dispatcher",...self?{fromTaskId:self.taskId,fromTabId:self.tabId}:{}});return{ok:!0,taskId,...reply?.tabId!==void 0?{tabId:reply.tabId}:tabId!==void 0?{tabId}:{},routed:"session.deliver",...reply?.delivered!==void 0?{delivered:reply.delivered}:{},...reply?.reason!==void 0?{reason:reply.reason}:{},...reply?.layer!==void 0?{layer:reply.layer}:{},...reply?.clients!==void 0?{clients:reply.clients}:{}}}var DISPATCH_VERB={name:"dispatch",group:"drive",summary:'Route text into a task\'s live session. The dispatcher\'s messenger (docs/design/dispatcher.md); unlike `send`, it never starts an engine \u2014 it requires an already-hosted session. The daemon pastes into it and reports `delivered`; `delivered:false` with `reason:"busy"` means a human is mid-message, and `reason:"broadcast"` means no hosted session answered and the text went out on the session.deliver channel for a browser to pick up (unconfirmable; `clients: 0` proves it reached nobody).',flags:[F.taskId(!0),F.prompt(!0,"Text delivered into the task's engine session."),F.promptFile(),{name:"tab",type:"string",required:!1,placeholder:"TAB",description:"Deliver into exactly this tab (e.g. tab-3) instead of the canonical engine tab."}],handler:dispatch};async function note(ctx){let daemon=daemonOf(ctx),taskId=ctx.args.require("task-id"),text=ctx.args.require("text");return await daemon.request("note.file",{taskId,text})}async function getTask(ctx){let daemon=daemonOf(ctx),taskId=ctx.args.require("task-id"),res=await daemon.request("task.get",{taskId}),{tabs,running}=await ctx.runtime.taskTabs(taskId,taskEngineArgv(res.task),await ptyScopeOf(daemon,res.task)),runnability=await readRunnability(daemon,taskId);return{task:res.task,running,tabs,...runnability?{runnability}:{}}}async function list(ctx){let daemon=daemonOf(ctx),local=await daemon.request("task.list"),{mergeTaskList}=await import("./chunk-6wt08a28.js"),all=ctx.caller.cli?await mergeTaskList(local):local,wantArchived=ctx.args.bool("archived")??!1,merged={...all,tasks:(all.tasks??[]).filter((task)=>task.archived===!0===wantArchived)},filters=listFilters(ctx),fields=listFields(ctx),result=filters?{...merged,...await filterTaskList(ctx,daemon,merged.tasks??[],filters)}:merged;if(!result.tasks)return result;let tasks=result.tasks.map((t)=>t.extraRepos?.length?{...t,repoCount:t.extraRepos.length+1}:t);return{...result,tasks:fields?projectTasks(tasks,fields):tasks}}async function setActive(ctx){let daemon=daemonOf(ctx),taskId=ctx.args.bool("none")?null:ctx.args.require("task-id");return await daemon.request("task.setActive",{taskId}),{ok:!0,activeTaskId:taskId}}var RECOVER=["api","engine-list"];async function routineRunEngine(ctx,repo,vendor){return vendor??(repo?await ctx.runtime.defaultVendor(repo):void 0)??DEFAULT_TASK_VENDOR}async function pinnedEngine(ctx,repo,vendor,model,effort){let engine=await routineRunEngine(ctx,repo,vendor),protocol=taskEngine({vendor:engine});if(model)assertEngineAcceptsModel(protocol,model,RECOVER);if(effort)assertEngineAcceptsEffort(protocol,effort,RECOVER);return engine}async function createEngineFields(ctx,repo){let vendor=ctx.args.vendor(),model=ctx.args.str("model")?.trim(),effort=ctx.args.str("effort")?.trim();if(!model&&!effort)return vendor?{vendor}:{};return{vendor:await pinnedEngine(ctx,repo,vendor,model,effort),...model?{model}:{},...effort?{effort}:{}}}async function updateEngineFields(ctx,id){let hasVendor=ctx.args.present("vendor"),hasModel=ctx.args.present("model"),hasEffort=ctx.args.present("effort");if(!hasVendor&&!hasModel&&!hasEffort)return{};let vendor=ctx.args.vendor(),model=ctx.args.str("model")?.trim()||void 0,effort=ctx.args.str("effort")?.trim()||void 0,fields={...hasVendor?{vendor:vendor??null}:{},...hasModel?{model:model??null}:{},...hasEffort?{effort:effort??null}:{}},{automations}=await daemonOf(ctx).request("automation.list",{}),routine=automations.find((a)=>a.id===id);if(!routine)return fields;let nextModel=hasModel?model:routine.model,nextEffort=hasEffort?effort:routine.modelEffort;if(!nextModel&&!nextEffort)return fields;let nextVendor=hasVendor?vendor:routine.vendor,engine=await pinnedEngine(ctx,routine.repo,nextVendor,nextModel,nextEffort);return engine===routine.vendor&&!hasVendor?fields:{...fields,vendor:engine}}var SCHEDULE_FLAG={name:"schedule",type:"string",placeholder:"CRON",description:"Five-field cron in the daemon host's local time, e.g. '0 9 * * MON-FRI'."},PRECHECK_FLAGS=[{name:"precheck",type:"string",placeholder:"CMD",description:"Shell command run in the repo BEFORE the engine starts. Non-zero exit skips the run without spawning an agent \u2014 the cheap way to avoid burning a turn when nothing changed."},{name:"precheck-timeout",type:"int",placeholder:"SEC",default:"120",description:"Seconds before the precheck is killed and the run skipped."}],SCRIPT_FLAGS=[{name:"script",type:"string",placeholder:"CMD",description:"Run this shell command in the repo instead of an engine: no task, no worktree, no agent turn. Exit 0 records `ran`, anything else `script_failed`; non-empty stdout (last 4000 chars) is the run's response. Replaces --prompt and the engine flags. '' on update turns it back into a prompt routine."},{name:"script-timeout",type:"int",placeholder:"SEC",default:"120",description:"Seconds before the script is killed and the run recorded as script_failed (max 600)."}],NOTIFY_FLAG={name:"notify",type:"bool",description:"Hand every run's response, and runs that need a human, to the bound chat channel's agent (`rove weixin login`), which decides what to tell you. Off by default."};function scriptPayload(ctx){if(!ctx.args.present("script")){if(ctx.args.present("script-timeout"))throw new ApiError("--script-timeout requires --script (pass the command again to change its timeout)","BAD_FLAG");return{}}let command=ctx.args.str("script");if(command===void 0)return{script:null};return{script:{command,timeoutSeconds:ctx.args.int("script-timeout")??120}}}var GRACE_FLAG={name:"grace",type:"uint",placeholder:"MIN",default:"60",description:"How late a missed occurrence may still run when the daemon was down. Only the most recent missed occurrence is ever run."},WEBHOOK_FLAG={name:"webhook",type:"bool",description:"Also fire on a signed POST to the web server's /hooks/routines/<id> (GitHub's X-Hub-Signature-256). Turning it on generates the secret, shown as .webhook.secret; paste it into the GitHub webhook with content type application/json. The precheck still runs, with ROVE_WEBHOOK_EVENT and ROVE_WEBHOOK_ACTION set, and the prompt starts with what fired it. Off again forgets the secret."},PERSISTENT_FLAG={name:"persistent-session",type:"bool",description:"Re-deliver into ONE standing task instead of a fresh worktree per run \u2014 for a routine that needs yesterday's context (a trend check). Its task is folded behind the sidebar's routine count row. Leave off for a routine that EDITS code: a week of runs on one branch is a branch nobody can land."},MODEL_FLAG={name:"model",type:"string",placeholder:"MODEL",description:"Model every run's engine launches on, in the engine's own spelling (`engine-list` shows suggestions). Rejected (BAD_MODEL) on an engine that declares no model flag."},EFFORT_FLAG={name:"effort",type:"string",placeholder:"LEVEL",description:"Reasoning effort every run's engine launches at. Validated against the engine's declared levels (BAD_EFFORT otherwise)."},TARGET_FLAGS=[{name:"target-task",type:"string",placeholder:"ID",description:"Existing task id. Requires --target-tab; never creates or revives a session. Pass both empty to clear on update."},{name:"target-tab",type:"string",placeholder:"TAB",description:"Exact existing engine tab id, e.g. tab-2. Requires --target-task."}];function targetPayload(ctx){let hasTask=ctx.args.present("target-task"),hasTab=ctx.args.present("target-tab");if(!hasTask&&!hasTab)return{};if(!hasTask||!hasTab)throw new ApiError("--target-task and --target-tab must be supplied together","BAD_FLAG");let taskId=ctx.args.str("target-task"),tabId=ctx.args.str("target-tab");if(!taskId&&!tabId)return{target:null};if(!taskId||!tabId)throw new ApiError("both target values must be nonempty, or both empty to clear","BAD_FLAG");return{target:{kind:"existing-tab",taskId,tabId}}}function precheckPayload(ctx){if(!ctx.args.present("precheck")){if(ctx.args.present("precheck-timeout"))throw new ApiError("--precheck-timeout requires --precheck (pass the command again to change its timeout)","BAD_FLAG");return{}}let command=ctx.args.str("precheck");if(command===void 0)return{precheck:null};return{precheck:{command,timeoutSeconds:ctx.args.int("precheck-timeout")??120}}}var ROUTINE_VERBS=[{name:"routine-list",group:"routine",summary:"List scheduled routines with their next run time.",flags:[],handler:(ctx)=>simpleRpc(ctx,"automation.list",{})},{name:"routine-create",group:"routine",summary:"Schedule a prompt. Each firing creates a fresh task (worktree + engine, or with --no-repo a directory session) and delivers it. Use --target-task and --target-tab for an existing conversation, or --script to run a command with no engine. An enabled routine keeps the daemon alive.",flags:[{...F.repo(!1),description:"Repo root (git toplevel) every run works in. Relative paths resolve against $PWD. Required unless --no-repo."},{name:"no-repo",type:"bool",description:"A routine with no project: each run works in its own directory (<home>/.rove/routines/<id>) and its session sits in the sidebar's Ungrouped group. Takes no --base-branch."},{name:"name",type:"string",required:!0,placeholder:"N",description:"Routine name."},F.prompt(!1,"Text delivered to the selected conversation or a new session. Required unless --script."),F.promptFile(),{...SCHEDULE_FLAG,required:!0},{...F.vendor(),description:"Engine every run launches. Omitted with --model/--effort: the repo's default engine (falling back to the built-in default) is resolved, validated and stored. Omitted without pins: runs launch the built-in default engine."},MODEL_FLAG,EFFORT_FLAG,{name:"base-branch",type:"string",placeholder:"B",description:"Base ref each run's worktree branches from."},...PRECHECK_FLAGS,...SCRIPT_FLAGS,NOTIFY_FLAG,WEBHOOK_FLAG,GRACE_FLAG,PERSISTENT_FLAG,...TARGET_FLAGS,{name:"disabled",type:"bool",description:"Create it paused instead of active."}],handler:async(ctx)=>{let noRepo=ctx.args.bool("no-repo")===!0,hasRepo=ctx.args.str("repo")!==void 0;if(noRepo&&hasRepo)throw new ApiError("--repo and --no-repo conflict \u2014 pass one","CONFLICTING_FLAGS");if(!noRepo&&!hasRepo)throw new ApiError("pass exactly one of --repo PATH or --no-repo","MISSING_FLAG");let repo=noRepo?void 0:ctx.args.requireRepo("repo"),script=scriptPayload(ctx);if(script.script&&ctx.args.promptText()!==void 0)throw new ApiError("pass --prompt or --script, not both","BAD_FLAG");return simpleRpc(ctx,"automation.create",{...repo?{repo}:{},name:ctx.args.require("name"),...script.script?script:{prompt:requirePromptText(ctx,"routine-create")},schedule:ctx.args.require("schedule"),...ctx.args.bool("notify")?{notify:!0}:{},...ctx.args.bool("webhook")?{webhook:!0}:{},...await createEngineFields(ctx,repo),...ctx.args.str("base-branch")?{baseRef:ctx.args.str("base-branch")}:{},...precheckPayload(ctx),...targetPayload(ctx),...ctx.args.nonNegativeInt("grace")!==void 0?{missedRunGraceMinutes:ctx.args.nonNegativeInt("grace")}:{},...ctx.args.bool("persistent-session")?{persistentSession:!0}:{},...ctx.args.bool("disabled")?{enabled:!1}:{}})}},{name:"routine-update",group:"routine",summary:"Change a routine. A new --schedule re-anchors its next run; --precheck '' clears the precheck; --enabled false pauses it (disabling the last active one releases the daemon's keep-alive hold) and --enabled true resumes it.",flags:[{name:"id",type:"string",required:!0,placeholder:"ID",description:"Routine id."},{name:"name",type:"string",placeholder:"N",description:"New name."},F.prompt(!1,"New prompt."),F.promptFile(),SCHEDULE_FLAG,{...F.vendor(),type:"string",description:"New engine vendor; empty clears it (before binding an existing target). While model/effort stay pinned, a cleared vendor is re-resolved from the repo default and stored."},{...MODEL_FLAG,description:`${MODEL_FLAG.description} '' clears it.`},{...EFFORT_FLAG,description:`${EFFORT_FLAG.description} '' clears it.`},{name:"base-branch",type:"string",placeholder:"B",description:"New base ref ('' to clear)."},...PRECHECK_FLAGS,...SCRIPT_FLAGS,{...NOTIFY_FLAG,placeholder:"BOOL"},{...WEBHOOK_FLAG,placeholder:"BOOL"},GRACE_FLAG,PERSISTENT_FLAG,...TARGET_FLAGS,{name:"enabled",type:"bool",placeholder:"BOOL",description:"false pauses the routine, true resumes it."}],handler:async(ctx)=>{let id=ctx.args.require("id");return simpleRpc(ctx,"automation.update",{id,...ctx.args.str("name")!==void 0?{name:ctx.args.str("name")}:{},...ctx.args.promptText()!==void 0?{prompt:ctx.args.promptText()}:{},...ctx.args.str("schedule")!==void 0?{schedule:ctx.args.str("schedule")}:{},...await updateEngineFields(ctx,id),...ctx.args.present("base-branch")?{baseRef:ctx.args.str("base-branch")??null}:{},...precheckPayload(ctx),...scriptPayload(ctx),...ctx.args.present("notify")?{notify:ctx.args.bool("notify")??!0}:{},...ctx.args.present("webhook")?{webhook:ctx.args.bool("webhook")??!0}:{},...targetPayload(ctx),...ctx.args.nonNegativeInt("grace")!==void 0?{missedRunGraceMinutes:ctx.args.nonNegativeInt("grace")}:{},...ctx.args.present("persistent-session")?{persistentSession:ctx.args.bool("persistent-session")??!0}:{},...ctx.args.present("enabled")?{enabled:ctx.args.bool("enabled")??!0}:{}})}},{name:"routine-delete",group:"routine",summary:"Delete a routine and its run history. Tasks it already created are untouched.",flags:[{name:"id",type:"string",required:!0,placeholder:"ID",description:"Routine id."}],handler:(ctx)=>simpleRpc(ctx,"automation.delete",{id:ctx.args.require("id")})},{name:"routine-run-now",group:"routine",summary:"Run a routine immediately, skipping its precheck (asking for it IS the answer). Does not shift its schedule.",flags:[{name:"id",type:"string",required:!0,placeholder:"ID",description:"Routine id."}],handler:(ctx)=>simpleRpc(ctx,"automation.runNow",{id:ctx.args.require("id")})},{name:"routine-respond",group:"routine",summary:`Report this run's result. Every delivered routine prompt starts with a [ROVE ROUTINE] line naming its run; this is the routine-flavoured spelling of \`report --delivery <id> --outcome done --summary <text>\` and writes the same delivery report, so \`deliveries\` shows it too. One response per run \u2014 responding again replaces it. Cap ${ROUTINE_RESPONSE_MAX_CHARS} characters; markdown renders on the Routines page.`,flags:[{name:"run",type:"string",required:!0,placeholder:"RUN_ID",description:"Run id from the [ROVE ROUTINE] line."},{name:"text",type:"string",placeholder:"TEXT",description:"The response. Exactly one of --text / --prompt-file."},{...F.promptFile(),description:"Read the response from this file (`-` = stdin) \u2014 use it for anything multi-line or with backticks/$vars."}],handler:async(ctx)=>{let runId=ctx.args.require("run"),inline=ctx.args.str("text"),file=ctx.args.promptText();if(inline!==void 0&&file!==void 0)throw new ApiError("pass --text or --prompt-file, not both","BAD_FLAG");let text=inline??file;if(text===void 0)throw new ApiError("--text (or --prompt-file) is required","MISSING_FLAG");if(text.length>ROUTINE_RESPONSE_MAX_CHARS)throw new ApiError(`response is ${text.length} characters; the cap is ${ROUTINE_RESPONSE_MAX_CHARS}`,"RESPONSE_TOO_LARGE",{hint:"Summarize: lead with the conclusion, link or name the files that hold the detail."});let res=await simpleRpc(ctx,"automation.respond",{runId,text});if(res.run===null)throw new ApiError(`no routine run ${runId}`,"RUN_NOT_FOUND",{hint:"Copy the id from the [ROVE ROUTINE] line of the prompt you were given; `routine-runs --id <routine>` lists run ids."});return res}},{name:"routine-runs",group:"routine",summary:"Run history, newest first. Statuses: dispatched, revived (standing session respawned \u2014 files kept, conversation did not), skipped_cancelled (disabled, changed or stopped before delivery), skipped_precheck (nothing to do), skipped_blocked (should-run refused the tick: `blocked` is the verdict {reason, detail?, until?, gateIds?} \u2014 busy, awaiting-owner, report-missing, quota, awaiting-permission; not an alarm, the cause is already in the Inbox; a manual run-now bypasses it), skipped_missed, skipped_unavailable, ran / script_failed (a --script routine: exit 0 or not; stdout is the response), dispatch_failed (also a should-run refusal for engine-dead or engine-error, which have no Inbox row of their own: it keeps `blocked` and raises the routine alarm). Each run carries `deliveryId`, `deliveryStatus` (awaiting|reported|missing) and `response` ({text, at}) once its agent reported (routine-respond or report).",flags:[{name:"id",type:"string",required:!0,placeholder:"ID",description:"Routine id."}],handler:(ctx)=>simpleRpc(ctx,"automation.runs",{id:ctx.args.require("id")})}];async function applyPostCreateFlags(daemon,taskId,args){let status=args.enumOf("status");if(status)await daemon.request("task.status",{taskId,status});let pin=args.bool("pin");if(pin!==void 0)await daemon.request("task.pin",{taskId,pinned:pin});let requireReport=args.bool("require-report");if(requireReport!==void 0)await daemon.request("task.requireReport",{taskId,required:requireReport});return Boolean(status)||pin!==void 0||requireReport!==void 0}async function deliverArmed(ctx,daemon,taskId,target,text){if(ctx.args.bool("require-report")!==!0&&ctx.args.bool("expect-report")!==!0)return ctx.runtime.deliverPrompt(daemon,target,text);let{deliveryId}=await daemon.request("delivery.arm",{taskId,tabId:"tab-1",promptHead:text}),settle=(delivered)=>daemon.request("delivery.settle",{deliveryId,delivered,tabId:"tab-1"}).catch(()=>{});try{let delivered=await ctx.runtime.deliverPrompt(daemon,target,`${text}
11
11
 
12
- ${deliveryFooter(deliveryId,roveApiInvocation())}`);return await settle(promptMayHaveLanded(delivered)),{...delivered,deliveryId}}catch(err){throw await settle(!1),err}}async function persistPrompt(daemon,taskId,prompt){try{return await daemon.request("task.setPrompt",{taskId,prompt}),!0}catch(err){return console.error(`[rove api add] task.setPrompt failed for ${taskId} \u2014 no "Run again" for this task:`,err),!1}}async function engineChoice(ctx,repo){let command=ctx.args.str("command");if(command)return{command,vendor:resolveCommandProtocol(command)};let fallback=await ctx.runtime.defaultVendor(repo);return fallback?{command:fallback,vendor:fallback}:{}}function enginePayload(choice,effort,model,tier){return{...choice.command?{command:choice.command}:{},...choice.vendor?{vendor:choice.vendor}:{},...effort?{effort}:{},...model?{model}:{},...tier?{tier}:{}}}async function autoTier(prompt){if(!prompt?.trim())throw new ApiError("--tier auto classifies the first message \u2014 pass --prompt/--prompt-file, or name a tier explicitly","BAD_FLAG",helpStep("add"));let outcome=await classifyTier(prompt,readClassifierConfig());if(outcome.kind==="picked"){let{tier,confidence}=outcome.verdict;return{tier,note:`auto \u2192 ${tier} (confidence ${confidence.toFixed(2)})`}}let detail=outcome.detail?`: ${outcome.detail}`:"";return{note:`auto \u2192 no tier (${outcome.reason}${detail})`}}async function tierFields(ctx,prompt){let requested=ctx.args.str("tier")?.trim();if(!requested)return{};if(requested!=="auto"&&!isAutoRoutingTier(requested))throw new ApiError(`--tier must be one of swift, standard, deep, auto (got ${JSON.stringify(requested)})`,"BAD_FLAG",helpStep("add"));for(let flag of["command","model","effort","agents"])if(ctx.args.str(flag))throw new ApiError(`--${flag} conflicts with --tier, which already fills the engine, model and effort from the auto-routing table \u2014 pass one or the other`,"CONFLICTING_FLAGS",helpStep("add"));let table=readAutoRoutingTable();if(!table){if(requested==="auto")return{note:"auto \u2192 no tier (auto routing is not configured)"};throw new ApiError("auto routing is not configured \u2014 a tier has no engine (autoRouting.<tier>.engine in state.json); set it in Settings \u2192 Auto routing","TIER_UNAVAILABLE",helpStep("add"))}let tier,note2;if(requested==="auto"){let picked=await autoTier(prompt);if(note2=picked.note,!picked.tier)return{note:note2};tier=picked.tier}else tier=requested;let target=table[tier],status=await detectEngineStatus(target.engine),block=tierBlock(target,{engineIds:new Set(await engineListIds()),accountKind:()=>status.account?.kind??null});if(block){if(requested==="auto")return{note:`${note2} \u2014 unusable: ${describeTierBlock(block)}`};throw new ApiError(`tier ${tier} cannot start: ${describeTierBlock(block)}`,"TIER_UNAVAILABLE",{tier,block,hint:"Retarget the tier in Settings \u2192 Auto routing, or pass --command/--model/--effort by hand.",nextCommandArgs:["api","engine-list"]})}let vendor=resolveCommandProtocol(target.engine);try{if(target.model)assertEngineAcceptsModel(vendor,target.model,["api","engine-list"]);if(target.effort)assertEngineAcceptsEffort(vendor,target.effort,["api","engine-list"])}catch(err){if(requested!=="auto")throw err;return{note:`${note2} \u2014 unusable: ${err instanceof Error?err.message:String(err)}`}}return{fields:{choice:{command:target.engine,vendor},effort:target.effort,model:target.model,tier},...note2?{note:note2}:{}}}async function withTierNote(note2,body){if(Object.keys(note2).length===0)return body();try{return await body()}catch(err){if(!(err instanceof ApiError))throw err;throw new ApiError(err.message,err.code,{...err.data,...note2})}}function modelFor(ctx,engines){let model=ctx.args.str("model")?.trim();if(!model)return;for(let engine of new Set(engines))assertEngineAcceptsModel(engine,model,["api","engine-list"]);return model}function effortFor(ctx,engines){let level=ctx.args.str("effort")?.trim();if(!level)return;for(let engine of new Set(engines))assertEngineAcceptsEffort(engine,level,["api","engine-list"]);return level}function foreignModelWarnings(engines,model){if(!model)return[];return[...new Set(engines)].filter((engine)=>protocolEntry(engine).foreignModel?.(model)).map((engine)=>`engine ${engine} is unlikely to run model ${JSON.stringify(model)} \u2014 that spelling belongs to another vendor; \`engine-list\` shows ${engine}'s models`)}function launchedEngine(task){return{vendor:task.vendor??null,command:task.command??null,model:task.model??null,effort:task.modelEffort??null}}function parentFlags(ctx){let{args}=ctx,parent=args.str("parent"),noParent=args.bool("no-parent")===!0,noWorktree=args.bool("no-worktree")===!0,isolateIn=args.str("isolate-in"),has=(flag)=>args.str(flag)!==void 0||args.bool(flag)===!0,conflict=(a,b)=>{throw new ApiError(`--${a} and --${b} conflict`,"CONFLICTING_FLAGS",helpStep("add"))};if(parent&&noParent)conflict("parent","no-parent");if(noWorktree&&isolateIn)conflict("no-worktree","isolate-in");for(let[flag,on]of[["no-worktree",noWorktree],["isolate-in",isolateIn!==void 0]]){if(!on)continue;if(noParent)conflict(flag,"no-parent");for(let other of["repo","fork","base-for","workspace"])if(has(other))conflict(flag,other)}if(noWorktree){for(let other of["base-branch","branch","worktree-name","count","agents"])if(has(other))conflict("no-worktree",other)}return{parent,noParent,noWorktree,isolateIn,takesParentRepo:noWorktree||isolateIn!==void 0}}async function resolveAddParent(ctx,daemon,flags,repo){if(flags.parent)return await getParent(daemon,flags.parent);if(flags.noParent)return null;if(!ctx.caller.cli||ctx.caller.forwarded)return requireOrNull(flags);let self=await verifiedSelfSession();if(!self)return requireOrNull(flags);let{tasks}=await daemon.request("task.list",{}).catch(()=>({tasks:[]})),caller=tasks.find((task)=>task.id===resolvePtyOwner(tasks,self.taskId,self.tabId));if(!caller)return requireOrNull(flags);if(flags.takesParentRepo)return caller;let root=tasks.find((task)=>task.id===lineageRootId(tasks,caller.id));if(!root||root.routine||isRemoteRepoKey(root.repo)||repo===void 0)return null;return samePath(await ctx.runtime.resolveRepoRoot(root.repo),repo)?caller:null}function requireOrNull(flags){if(!flags.takesParentRepo)return null;let flag=flags.noWorktree?"--no-worktree":"--isolate-in";throw new ApiError(`${flag} needs a parent and the caller is not a verified rove session \u2014 pass --parent ID`,"MISSING_TARGET",helpStep("add"))}async function getParent(daemon,taskId){try{return(await daemon.request("task.get",{taskId})).task}catch{throw new ApiError(`no task ${taskId} on this machine \u2014 a parent must live beside its child`,"PARENT_NOT_FOUND",{hint:"list the ids with `rove api list`",nextCommandArgs:["api","list"]})}}async function placeAdd(ctx,parent,flags,create){if(!parent)return{kind:"top"};let parentId=parent.id;if(flags.noWorktree)return{kind:"shared",parentId};if(flags.isolateIn)return{kind:"cut",parentId,isolateIn:flags.isolateIn};return!create.fork&&!create.ownWorkspace&&!create.baseRef&&!create.multiRepo&&!isRemoteRepoKey(parent.repo)&&samePath(await ctx.runtime.resolveRepoRoot(parent.repo),create.repo)?{kind:"cut",parentId}:{kind:"nested",parentId}}function placedRequest(placement,create){if(placement.kind==="top")return{method:"task.create",payload:create};if(placement.kind==="nested")return{method:"task.create",payload:{...create,parentTaskId:placement.parentId}};let{repo:_repo,extraRepos:_extra,forkFrom:_fork,...rest}=create,payload={...rest,parentTaskId:placement.parentId};if(placement.kind==="shared")return{method:"task.spawn",payload:{...payload,worktree:!1}};return{method:"task.spawn",payload:{...payload,worktree:!0,lazy:!0,...placement.isolateIn?{isolateIn:placement.isolateIn}:{}}}}async function addParallel(ctx,repo,repoSet,placement,count,agentsSpec){let daemon=daemonOf(ctx),{args}=ctx,prompt=args.promptText();if(!prompt)throw new ApiError("--count/--agents spawn parallel attempts of ONE prompt \u2014 pass --prompt","MISSING_FLAG",helpStep("add"));if(args.str("branch"))throw new ApiError("--branch names ONE branch and cannot be shared by parallel siblings \u2014 drop it (each sibling gets its own auto branch) or spawn them one at a time","BAD_FLAG",helpStep("add"));if(args.str("worktree-name"))throw new ApiError("--worktree-name names ONE directory and cannot be shared by parallel siblings \u2014 drop it (each sibling gets its own generated name) or spawn them one at a time","BAD_FLAG",helpStep("add"));if(agentsSpec){let conflict=count!==void 0?"--count":args.str("command")?"--command":null;if(conflict)throw new ApiError(`${conflict} conflicts with --agents, which already names each sibling's engine and how many \u2014 pass one or the other`,"BAD_FLAG",helpStep("add"))}let title=args.str("title")||deriveTitleFromPrompt(prompt),baseRef=args.str("base-branch")??repoSet.primaryBaseRef,picked=await tierFields(ctx,prompt),tier=picked.fields,choice=tier?.choice??await engineChoice(ctx,repo),plan=agentsSpec?parseAgentsSpec(agentsSpec):buildCountPlan(count??1,choice.vendor??DEFAULT_VENDOR);if(plan.length>FANOUT_CAP)throw new ApiError(`a parallel round of ${plan.length} exceeds the cap of ${FANOUT_CAP} \u2014 spawn in batches`,"BAD_FLAG");let effort=tier?tier.effort:effortFor(ctx,plan),model=tier?tier.model:modelFor(ctx,plan),warnings=foreignModelWarnings(plan,model),groupId=ulid(),created=[],createFailure=null,dispatcher=await dispatcherEnvPayload(daemon);for(let[i,vendor]of plan.entries()){let engine=agentsSpec?{command:vendor,vendor}:{...choice,vendor},payload={repo,groupId,...dispatcher,...enginePayload(engine,effort,model,tier?.tier)};if(title)payload.title=plan.length>1?`${title} #${i+1}/${plan.length}`:title;if(baseRef)payload.baseRef=baseRef;if(repoSet.extraRepos.length>0)payload.extraRepos=repoSet.extraRepos;try{let request=placedRequest(placement,payload),res=await daemon.request(request.method,request.payload);created.push({taskId:res.taskId,vendor,task:res.task})}catch(err){let code=err instanceof ApiError?err.code:"CREATE_FAILED",message=err instanceof Error?err.message:String(err);createFailure={vendor,error:{message,code}};break}}for(let{taskId}of created)await applyPostCreateFlags(daemon,taskId,args);let settled=await Promise.allSettled(created.map(({taskId,vendor,task})=>deliverArmed(ctx,daemon,taskId,{id:taskId,worktreePath:taskCwd(task),kind:task.kind,vendor,command:task.command,modelEffort:task.modelEffort,model:task.model,repo:task.repo,...task.extraRepos?.length?{multiRepo:!0}:{},newTask:!0},prompt))),tasks=[],failures=[],persistedPrompts=[];if(settled.forEach((r,i)=>{let{taskId,vendor,task}=created[i];if(r.status==="fulfilled"&&r.value.delivered){let row={ok:!0,taskId,title:task.title,branch:task.branch,vendor,engine:launchedEngine(task),started:r.value.started,engineReady:r.value.engineReady,session:r.value.session,...r.value.reason?{reason:r.value.reason}:{}};tasks.push(row),persistedPrompts.push(persistPrompt(daemon,taskId,prompt).then((ok)=>{if(!ok)row.promptPersisted=!1}));return}let err=r.status==="rejected"?r.reason:new ApiError(`prompt was not confirmed in ${taskId}'s engine`,"NOT_DELIVERED"),code=err instanceof ApiError?err.code:"DELIVER_FAILED",message=err instanceof Error?err.message:String(err);failures.push({ok:!1,taskId,vendor,engine:launchedEngine(task),error:{message,code}})}),createFailure)failures.push({ok:!1,vendor:createFailure.vendor,error:createFailure.error});await Promise.all(persistedPrompts);let result={count:created.length,requested:plan.length,groupId,home:homeDir(),tasks,failures,...picked.note?{tierAuto:picked.note}:{},...warnings.length>0?{warnings}:{}};if(failures.length>0)throw new ApiError(`add delivered ${tasks.length}/${plan.length}`,"PARTIAL_FANOUT",result);return result}async function localCheckouts(ctx){let{tasks}=await daemonOf(ctx).request("task.list");return[...new Set([...getSavedRepos(),...tasks.map((t)=>t.repo)])].flatMap((path2)=>{let identity=repoIdentity(path2);return identity?[{path:path2,identity}]:[]})}async function resolveLocalRepo(ctx,want,allowFork=!1){let candidates=await localCheckouts(ctx),matches=findCheckouts(want,candidates),id=want.origin??(want.root?`root:${want.root}`:""),picked=pickCheckout(matches,{primary:primaryCheckouts()[id],allowFork,isClean:isCleanCheckout});return{repo:picked.path,...picked.fork?{fork:picked.fork}:{},checkouts:matches.map((m)=>m.path),searched:candidates.length}}var REPO_MATCH_VERB={name:"repo-match",group:"discover",summary:"Find this machine's checkout of a repo by identity: `--root` (its first commit) or `--origin` (normalized remote URL, `host/owner/name`). Several checkouts: the primary (`rove machine primary`) wins, else a clean one. A checkout with the same root but another origin is a fork and only matches with --allow-fork. Searches the saved repos and every task's repo. Returns { repo } (null when none matches), { fork } (a fork that was skipped), { checkouts } and { searched }.",flags:[{name:"origin",type:"string",placeholder:"URL",description:"Remote URL; any git form, compared normalized."},{name:"root",type:"string",placeholder:"SHA",description:"The repo's root commit."},{name:"allow-fork",type:"bool",description:"Accept a fork (same root commit, other origin)."}],handler:async(ctx)=>{let origin=ctx.args.str("origin"),root=ctx.args.str("root");if(!origin&&!root)throw new ApiError("pass --origin or --root","BAD_FLAG");return resolveLocalRepo(ctx,{...origin?{origin:normalizeRemoteUrl(origin)}:{},...root?{root}:{}},ctx.args.bool("allow-fork")===!0)}},REPO_LIST_VERB={name:"repo-list",group:"discover",summary:"This machine's repos by id (`host/owner/name`, or `root:<sha>` with no origin): each with its checkouts and which one is primary. Read from the saved repos and every task's repo. Returns { repos: [{ id, primary, checkouts }] }.",flags:[],handler:async(ctx)=>{let primaries=primaryCheckouts(),byId=new Map;for(let c of await localCheckouts(ctx)){let id=repoIdOf(c.identity);if(!id)continue;byId.set(id,[...byId.get(id)??[],c.path])}return{repos:[...byId].map(([id,checkouts])=>({id,primary:primaries[id]??null,checkouts})).sort((a,b)=>a.id.localeCompare(b.id))}}};async function resolveRepoFlag(ctx,raw){let ref=parseRepoRef(raw,ctx.caller.cwd??process.cwd());if(ref.kind==="path")return ctx.args.repoPath("repo",raw);let found=await resolveLocalRepo(ctx,{origin:ref.id});if(found.repo)return found.repo;if(ctx.args.bool("clone")!==!0)throw new ApiError(`no checkout of ${ref.id} on this machine`,"NO_LOCAL_CHECKOUT",{hint:`pass --clone to clone it into ${cloneDirFor(ref.id)}, or --repo with a checkout's path`});try{let dir=cloneRepo(ref);return addSavedRepo(dir),dir}catch(err){throw new ApiError(`could not clone ${ref.cloneUrl}: ${errorMessage(err)}`,"CLONE_FAILED",{hint:"check this machine can fetch it (`git ls-remote <url>`), or clone it yourself and pass its path"})}}async function resolveRepoSet(ctx,primary,extraRefs){let{args,runtime}=ctx,baseFor=args.list("base-for");if(extraRefs.length===0){if(baseFor.length>0)throw new ApiError("--base-for names a repo of a multi-repo task \u2014 pass --repo more than once","BAD_FLAG",helpStep("add"));return{extraRepos:[]}}if(1+extraRefs.length>MULTI_REPO_CAP)throw new ApiError(`${1+extraRefs.length} repos exceed the cap of ${MULTI_REPO_CAP} per task (the first --repo is the primary)`,"TOO_MANY_REPOS",helpStep("add"));if(isRemoteRepoKey(primary))throw new ApiError(`--repo ${primary} is a remote project; a multi-repo task must keep every repo local`,"REMOTE_EXTRA_UNSUPPORTED");let names=new Set([repoNameOf(primary)]),repos=[{name:repoNameOf(primary),repo:primary}];for(let raw of extraRefs){let requested=await resolveRepoFlag(ctx,raw);if(isRemoteRepoKey(requested))throw new ApiError(`--repo ${raw} is a remote project; a multi-repo task must keep every repo local`,"REMOTE_EXTRA_UNSUPPORTED",helpStep("add"));let root=await runtime.resolveRepoRoot(requested);if(!await runtime.isUsableRepo(root))throw new ApiError(`--repo ${raw} is not a git repository \u2014 every repo of a task needs one to cut its worktree and branch from`,"NOT_A_GIT_REPO",helpStep("add"));if(repos.some((r)=>samePath(r.repo,root)))throw new ApiError(`--repo ${raw} resolves to ${root}, already part of this task`,"REPO_DUPLICATE",helpStep("add"));let name=uniqueRepoName(repoNameOf(root),names);names.add(name),repos.push({name,repo:root})}let baseRefs=new Map;for(let spec of baseFor){let eq=spec.indexOf("="),key=spec.slice(0,eq),ref=spec.slice(eq+1);if(eq<=0||!ref)throw new ApiError(`--base-for ${JSON.stringify(spec)} must be REPO=REF`,"BAD_FLAG",helpStep("add"));let keyRoot=key.includes("/")?await runtime.resolveRepoRoot(args.repoPath("base-for",key)):void 0,target;for(let r of repos)if(r.name===key||keyRoot!==void 0&&samePath(await runtime.resolveRepoRoot(r.repo),keyRoot)){target=r;break}if(!target)throw new ApiError(`--base-for ${key}: no such repo in this task (repos: ${repos.map((r)=>r.name).join(", ")})`,"BAD_FLAG",helpStep("add"));if(baseRefs.has(target.name))throw new ApiError(`--base-for ${key} given twice`,"BAD_FLAG",helpStep("add"));baseRefs.set(target.name,ref)}let primaryBaseRef=baseRefs.get(repos[0]?.name??""),explicit=args.str("base-branch");if(primaryBaseRef&&explicit&&primaryBaseRef!==explicit)throw new ApiError(`--base-branch ${explicit} and --base-for ${repos[0]?.name}=${primaryBaseRef} both set the primary's base \u2014 pass one`,"BAD_FLAG",helpStep("add"));return{extraRepos:repos.slice(1).map((r)=>{let baseRef=baseRefs.get(r.name);return{repo:r.repo,...baseRef?{baseRef}:{}}}),...primaryBaseRef?{primaryBaseRef}:{}}}async function add(ctx){let{args,runtime}=ctx,daemon=daemonOf(ctx),flags=parentFlags(ctx),refs=args.list("repo"),fork=await resolveFork(ctx),inherited=flags.takesParentRepo?await resolveAddParent(ctx,daemon,flags,void 0):null,requestedRepo=inherited?inherited.repo:fork?fork.repo:await resolveRepoFlag(ctx,refs[0]??""),repo=inherited?inherited.repo:await runtime.resolveRepoRoot(requestedRepo);if(!inherited&&!await runtime.isUsableRepo(repo))throw new ApiError(`--repo ${repo} is not a git repository \u2014 a task needs one to cut its worktree and branch from (run \`git init\` there, or point --repo at a checkout)`,"NOT_A_REPO",helpStep("add"));let count=args.int("count"),agentsSpec=args.str("agents"),parallel=count!==void 0||agentsSpec!==void 0;if(args.bool("start")&&(parallel||args.promptText()))throw new ApiError("--start boots one idle engine without a first message: it conflicts with --prompt/--prompt-file (which already start the engine) and with --count/--agents","CONFLICTING_FLAGS",helpStep("add"));let branch=parallel?void 0:args.str("branch");if(branch&&!await runtime.isValidBranchName(branch))throw new ApiError(`--branch ${JSON.stringify(branch)} is not a valid git branch name (no spaces, no leading "-", no "..", "~^:?*[\\", no trailing ".lock") \u2014 see \`git check-ref-format --branch\``,"INVALID_BRANCH",helpStep("add"));let resolvedFrom=pathWithin(repo,requestedRepo)?{repoResolvedFrom:requestedRepo}:void 0,repoSet=inherited?{extraRepos:[]}:await resolveRepoSet(ctx,repo,refs.slice(1)),workspace=args.str("workspace");if(workspace&&(parallel||fork||repoSet.extraRepos.length>0))throw new ApiError("--workspace takes one task in one repo: it conflicts with --fork, --count, --agents, --no-worktree, --isolate-in and extra repos","CONFLICTING_FLAGS",helpStep("add"));let parent=inherited??await resolveAddParent(ctx,daemon,flags,repo),placement=await placeAdd(ctx,parent,flags,{repo,fork:fork!==void 0,baseRef:args.str("base-branch")??repoSet.primaryBaseRef,multiRepo:repoSet.extraRepos.length>0,ownWorkspace:workspace!==void 0}),result=parallel?await addParallel(ctx,repo,repoSet,placement,count,agentsSpec):await addOne(ctx,repo,repoSet,placement);return resolvedFrom&&result&&typeof result==="object"?{...result,...resolvedFrom}:result}var FORK_EXCLUSIVE=["repo","base-branch","base-for","count","agents"];async function resolveFork(ctx){let taskId=ctx.args.str("fork");if(!taskId)return;let clash=FORK_EXCLUSIVE.find((flag)=>ctx.args.str(flag)!==void 0);if(clash)throw new ApiError(`--fork and --${clash} conflict \u2014 a fork is a sibling in the parent's repo, starting at the parent's HEAD`,"CONFLICTING_FLAGS",helpStep("add"));let{task}=await daemonOf(ctx).request("task.get",{taskId});return{taskId,repo:task.repo}}async function typedEngineFields(ctx,repo){let choice=await engineChoice(ctx,repo),engines=choice.vendor?[choice.vendor]:[];return{choice,effort:effortFor(ctx,engines),model:modelFor(ctx,engines)}}function deliveryTarget(task,newTask=!1){return{id:task.id,worktreePath:taskCwd(task),kind:task.kind,vendor:task.vendor,command:task.command,modelEffort:task.modelEffort,model:task.model,repo:task.repo,...task.extraRepos?.length||task.sharesWorkspaceOf?{multiRepo:!0}:{},...newTask?{newTask:!0}:{}}}async function addOne(ctx,repo,repoSet,placement){let daemon=daemonOf(ctx),{args}=ctx,prompt=args.promptText(),picked=await tierFields(ctx,prompt),fields=picked.fields??await typedEngineFields(ctx,repo),tierNote=picked.note?{tierAuto:picked.note}:{},warnings=foreignModelWarnings([fields.choice.vendor??DEFAULT_VENDOR],fields.model),warned=warnings.length>0?{warnings}:{},payload={repo,...await dispatcherEnvPayload(daemon),...enginePayload(fields.choice,fields.effort,fields.model,fields.tier)},fork=args.str("fork"),title=args.str("title")||(prompt&&!fork?deriveTitleFromPrompt(prompt):"");if(title)payload.title=title;let branch=args.str("branch");if(branch)payload.branch=branch;let baseRef=args.str("base-branch")??repoSet.primaryBaseRef;if(baseRef)payload.baseRef=baseRef;if(fork)payload.forkFrom=fork;let workspace=args.str("workspace");if(workspace)payload.workspace=workspace;if(repoSet.extraRepos.length>0)payload.extraRepos=repoSet.extraRepos;let worktreeName=args.str("worktree-name");if(worktreeName)payload.worktreeName=worktreeName;let request=placedRequest(placement,payload),res=await daemon.request(request.method,request.payload),placed=res.parentDirty===void 0?{}:{parentDirty:res.parentDirty,sharedLinks:res.sharedLinks??[]},taskId=res.taskId;if(args.bool("activate"))await daemon.request("task.setActive",{taskId});let task=res.task;if(await applyPostCreateFlags(daemon,taskId,args))task=(await daemon.request("task.get",{taskId})).task;if(!prompt){if(!args.bool("start"))return{taskId,task,engine:launchedEngine(task),home:homeDir(),...placed,started:!1,...warned};try{let opened=await ctx.runtime.openTab(daemon,deliveryTarget(task),"engine");return{taskId,task,engine:launchedEngine(task),home:homeDir(),...placed,started:!0,tabId:opened.tab.id,session:opened.session,...warned}}catch(err){if(!(err instanceof ApiError))throw err;throw new ApiError(err.message,err.code,{...err.data,taskId,...placed,...warned})}}let brief=await withPeerProvenance(daemon,taskId,prompt),delivered=await withTierNote(tierNote,()=>deliverArmed(ctx,daemon,taskId,deliveryTarget(task,!0),brief));if(!delivered.delivered)throw new ApiError(`task ${taskId} created but the prompt was not delivered (paste did not land)`,"NOT_DELIVERED",{taskId,engine:launchedEngine(task),...placed,...tierNote,...warned});let promptPersisted=await persistPrompt(daemon,taskId,prompt);return task=(await daemon.request("task.get",{taskId})).task,{taskId,task,engine:launchedEngine(task),home:homeDir(),...placed,started:delivered.started,...delivered.deliveryId?{deliveryId:delivered.deliveryId}:{},engineReady:delivered.engineReady,session:delivered.session,delivered:delivered.delivered,...delivered.bytes===void 0?{}:{bytes:delivered.bytes},...delivered.promptEcho?{promptEcho:delivered.promptEcho}:{},...delivered.reason?{reason:delivered.reason}:{},...promptPersisted?{}:{promptPersisted:!1},...tierNote,...warned}}var CREATE_VERBS=[{name:"add",group:"create",summary:`Create a task (shows in the sidebar immediately). With --prompt it also starts the engine and delivers it; with --start and no prompt the engine boots idle in the task's own tab-1 (reply started:true, tabId, session), where a bare add only creates the row (started:false). PARALLEL ATTEMPTS: --count N spawns N sibling tasks of the SAME prompt, each in its own worktree/branch (--agents claude:2,codex:1 for a mixed fleet); capped at ${FANOUT_CAP}, prefer 3-4. NESTING (children never nest): called from a verified rove session ($ROVE_TASK_ID/$ROVE_TAB_ID that the pty host confirms), the new task becomes a CHILD of the caller's top-level task and shows under it in the sidebar \u2014 but only when its repo is that task's own repo and that task is not a routine session; otherwise (another repo, --machine, an unverifiable env, a daemon or routine caller) it stays top level, never an error. --parent ID nests under that task instead, in any repo; --no-parent opts out; --count/--agents siblings all get the parent; \`dispatcher\` still records the exact calling task and tab. A child in the parent's repo cuts its worktree from the parent's CURRENT branch tip (a \`dir\` task inside a git repo gets a worktree at the repo root and runs in the same relative subdirectory) instead of the repo default \u2014 unless --base-branch, --fork or several --repo name the fork point. \`parentDirty:true\` in the reply means the parent has uncommitted changes the new worktree does not carry; \`sharedLinks\` lists the parent workspace's top-level symlinks to OTHER git checkouts, which a new worktree does not isolate (--isolate-in LINK cuts the worktree in the linked repo instead). --no-worktree shares the parent's directory instead of cutting a worktree (read-only parallel work: two engines writing one tree will collide); it and --isolate-in need a parent (MISSING_TARGET without one). The child's bare \`send\` replies to the session that added it; \`.task.parentTaskId\` (and \`.task.sharesWorkspaceOf\` for a shared child) say where it landed. Does NOT steal focus \u2014 pass --activate to make it the active task. The sidebar shows .task.title and .task.branch \u2014 name a task by those (or its id), never by the directory in .task.worktreePath, which the UI never renders. \`.engine\` ({ vendor, command, model, effort }; per row with --count/--agents) is what the task actually launches once the flags and the repo default are resolved \u2014 check it before trusting the run. \`.warnings\` flags a --model that plainly belongs to another engine's vendor (e.g. codex given a claude-* id); the task is still created. Alias: spawn-task.`,flags:[{name:"repo",type:"string",required:!0,repeatable:!0,placeholder:"PATH|REPO",description:"Repo root (git toplevel; relative paths resolve against $PWD), or a repo id: `owner/name` (GitHub), `host/owner/name`, or a remote URL. An id resolves to this machine's checkout of it (the primary one, else a clean one) \u2014 or, with --machine, to that machine's. REPEAT it for a multi-repo task (max 8, all local): the FIRST is the primary \u2014 the engine's cwd, the sidebar project, `.task.repo/.branch/.worktreePath` \u2014 and each further one gets its own worktree on the SAME branch name (suffixed `-2` only in a repo where the name is taken), listed in `.task.extraRepos[]` ({ name, repo, branch, worktreePath, baseRef }; paths fill in once the worktrees exist). The engine sees the extras through its add-dir flag where it has one, `$ROVE_TASK_REPOS` / `$ROVE_REPO_<NAME>` env, and a note on the first prompt. `--count` gives every sibling the same repo set. Errors: TOO_MANY_REPOS, REPO_DUPLICATE, NOT_A_GIT_REPO (names the repo), REMOTE_EXTRA_UNSUPPORTED."},{name:"clone",type:"bool",description:"With a repo id this machine (or --machine's) has no checkout of: clone it into ~/.rove/repos/<host>/<owner>/<name> first."},{name:"fork",type:"string",placeholder:"TASK_ID",description:"Fork that task: the new task is a sibling in the SAME repo whose worktree is a copy-on-write clone of the parent's, so it starts with the parent's uncommitted edits, staged changes, untracked files and gitignored dirs (node_modules, build output), on a new branch rooted at the parent's HEAD. The clone is made during this call and is instant on APFS (macOS) or a reflink filesystem (btrfs, XFS); it costs no disk until either side writes. Replaces --repo (taken from the parent; --repo, --base-branch, --base-for, --count and --agents conflict with it: CONFLICTING_FLAGS). Title defaults to '<parent title> (fork)'. Refused with FORK_UNSUPPORTED \u2014 nothing created \u2014 when the parent is a main or directory task, is multi-repo or remote, or its worktree sits on a volume that cannot clone files; the parent is never modified. Scratch state the parent keeps in its git directory (a half-done merge or rebase) does not come along."},{name:"workspace",type:"enum",values:["layerfs"],description:"EXPERIMENTAL, Linux only: put the task's files in a LayerFS Workspace inside a per-task Docker sandbox instead of a git worktree (`experimental.layerfs` in state.json names its env file; Store calls need passwordless sudo). Terminals run inside the sandbox at the task's path; checkpoint/rewind use LayerFS commits; `export-branch` brings its git branch back for landing. Refused with LAYERFS_UNAVAILABLE / LAYERFS_NEEDS_ROOT \u2014 nothing created \u2014 when it cannot run here. Conflicts with --fork, --count, --agents, --no-worktree, --isolate-in and extra repos (CONFLICTING_FLAGS); inside a session it still nests under the caller, as its own layerfs task rather than a worktree cut from the parent."},{name:"allow-fork",type:"bool",description:"With --machine: accept a checkout there that shares the repo's history under another origin (a fork)."},{name:"parent",type:"string",placeholder:"ID",description:"Nest the new task under this task (stored under its top-level ancestor), whatever its repo \u2014 the rules of `update --parent`. Default: the verified calling session, when the repo is its own; see the summary. PARENT_NOT_FOUND if no such task."},{name:"no-parent",type:"bool",description:"Create a top-level task even from inside a rove session. Conflicts with --parent, --no-worktree and --isolate-in (CONFLICTING_FLAGS)."},{name:"no-worktree",type:"bool",description:"Share the parent's directory instead of cutting a worktree. The parent is --parent or the calling session; the child takes its repo, so --repo, --fork, --base-branch, --base-for, --branch, --worktree-name, --count, --agents and --isolate-in conflict with it."},{name:"isolate-in",type:"string",placeholder:"LINK",description:"Name of a top-level symlink in the parent's workspace that points at another git checkout (see `sharedLinks`): cut the worktree in THAT repo and run the child there. NOT_A_LINKED_REPO when it is not one. Needs a parent; --repo, --fork, --base-for and --no-worktree conflict with it."},F.title(),{name:"branch",type:"string",placeholder:"B",description:"Explicit branch name (else derived from the title in the repo's own style). Single task only."},{name:"base-branch",type:"string",placeholder:"B",description:"Base ref the worktree branches from."},{name:"base-for",type:"string",repeatable:!0,placeholder:"REPO=REF",description:"Per-repo base for a multi-repo task: REPO is the repo's name (its directory name, `-2`-suffixed on a clash; `.task.extraRepos[].name`) or its path, REF the ref its worktree branches from. Repeatable. --base-branch stays the primary's base; naming the primary here too is an error unless they agree."},{name:"worktree-name",type:"string",placeholder:"NAME",description:"Name the worktree DIRECTORY instead of taking one from the animal pool \u2014 so the caller can predict `.task.worktreePath` (`<worktrees root>/<NAME>`) rather than reading it back with get-task. One path segment of letters/digits/`.`/`_`/`-`, not starting with `.` (INVALID_WORKTREE_NAME otherwise). A name already in use in this repo \u2014 by a live task, a directory on disk, or a concurrent create \u2014 is REFUSED with WORKTREE_NAME_TAKEN, never silently suffixed `-v2`. Single task only."},F.command(),{name:"effort",type:"string",placeholder:"LEVEL",description:"Reasoning effort for the FIRST session, not just later ones. Validated against the engine's declared levels (codex: none/low/medium/high/xhigh/max; pi and OMP: off/minimal/low/medium/high/xhigh/max; claude declares none) \u2014 free-form, since a plugin engine may declare its own. With --agents, every engine in the plan must declare it."},{name:"tier",type:"enum",values:["swift","standard","deep","auto"],description:"Auto-routing tier: fills the engine, model and effort from the table in Settings \u2192 Auto routing (autoRouting.<tier>.* in state.json) and records the tier on the task (.task.tier). Exclusive with --command/--model/--effort/--agents (CONFLICTING_FLAGS). A tier whose target cannot start \u2014 engine not in engine-list, not logged in, model or effort its engine cannot carry \u2014 is refused up front (TIER_UNAVAILABLE) with the reason. `auto` asks the classifier named by `autoRouting.classifier` (OFF by default \u2014 nothing is sent anywhere until you set it, and with `jev` the prompt goes to TypeSafe, a third party that is not your engine vendor) to read --prompt and pick the tier; needs --prompt/--prompt-file, and NEVER fails the create \u2014 off, no key, a timeout, or an answer below `autoRouting.classifierThreshold` all create the task with the ordinary defaults and say so in `.tierAuto`."},{name:"model",type:"string",placeholder:"MODEL",description:"Model for the FIRST session and every rebuild, in the engine's own spelling (claude alias/full id, codex slug, pi/omp pattern or provider/id) \u2014 passed verbatim, so `engine-list`'s `models` are suggestions, not a closed list. Rejected (BAD_MODEL) on an engine that declares no model flag; with --agents, every engine in the plan must."},{name:"count",type:"int",placeholder:"N",description:`Spawn N sibling tasks of one prompt (parallel attempts, cap ${FANOUT_CAP}). Requires --prompt.`},{name:"agents",type:"string",placeholder:"claude:2,codex:1",description:"Per-ENGINE counts for a mixed parallel round (alternative to --count). Engine ids only \u2014 see `engine-list`."},{name:"status",type:"enum",values:TASK_STATUSES,default:"backlog",description:"Initial lifecycle status."},{name:"pin",type:"bool",description:"Pin the task to the top of the sidebar."},{name:"require-report",type:"bool",description:"Hold every `send` to this task, and its first --prompt, to a report: each arms a delivery the agent closes with `report --delivery <id>`, and a turn that ends without one raises a `report_missing` Inbox row. For tasks ticked unattended. Change later with `update --require-report on|off`."},{name:"expect-report",type:"bool",description:"Arm a delivery for this task's first --prompt only (as `send --expect-report` does for a send); later sends are not armed."},{name:"activate",type:"bool",default:"false",description:"Make this the active task (pulls every mounted TUI's Tasks-pane focus). Off by default."},{name:"start",type:"bool",description:"With no --prompt: also start the task's engine idle in its own tab-1 (materializes the worktree) instead of leaving a backlog row. Replies `started: true`, `tabId`, `session`; a refusal (ENGINE_LIMIT, ...) keeps the task and names it in `taskId`. One task only: CONFLICTING_FLAGS with --prompt/--prompt-file, --count and --agents."},F.prompt(!1,"Optional first message \u2014 when set, materializes the worktree, starts the engine, and pastes it. Required with --count/--agents."),F.promptFile()],handler:add}];async function readStdin(){let chunks=[];for await(let chunk of process.stdin)chunks.push(Buffer.from(chunk));return Buffer.concat(chunks)}var PANE_GRAPHICS_VERB={name:"pane-graphics",group:"drive",cliOnly:!0,summary:"Write opaque graphics bytes (read from stdin) to the terminal of every TUI attached to this task \u2014 the transport a pane needs because its own tty is a PTY slave, not the emulator, so it cannot reach the real terminal or measure it. Rove parses nothing: the bytes are your protocol. Call it ONCE with nothing on stdin to be given `imageId` (allocated per tab, so two panes never collide) plus `cellWidth`/`cellHeight` in pixels; build your payload around those, then pipe each frame in with --image-id to replace that picture in place. `wrote` says whether anything was broadcast. Returns `ok: false` with `unsupported` when no attached terminal reported a cell size (`no-cell-size`) or two reported different ones (`mixed-cell-size`) \u2014 fall back to whatever you draw without graphics. Task defaults to $ROVE_TASK_ID, then the active task.",flags:[F.taskId(!1),{name:"tab",type:"string",required:!0,placeholder:"TAB",description:"Terminal Tab whose cells the picture is for (e.g. tab-3), from get-task .tabs[].id."},{name:"image-id",type:"int",placeholder:"N",description:"Reuse an id this tab was already given, replacing that picture in place. Omit to be handed a fresh one."}],handler:async(ctx)=>{let client=daemonOf(ctx),taskId=ctx.args.str("task-id")??process.env.ROVE_TASK_ID??await resolveActiveTaskId(client);if(!taskId)throw new ApiError("no target task: pass --task-id (no $ROVE_TASK_ID, no active task)","MISSING_TARGET");let data=process.stdin.isTTY?Buffer.alloc(0):await readStdin(),imageId=ctx.args.int("image-id");return simpleRpc(ctx,"graphics.write",{taskId,tabId:ctx.args.require("tab"),...data.length>0?{data:data.toString("base64")}:{},...imageId!==void 0?{imageId}:{}})}};function interruptSequenceFor(task){let vendor=task.vendor,sequence=vendor?getCapabilities(vendor)?.interruptSequence:void 0;if(sequence)return sequence;throw new ApiError(`engine ${vendor??"(unknown)"} has not declared how it is interrupted`,"UNSUPPORTED",{taskId:task.id,...vendor?{vendor}:{},hint:"only engines that declare an interrupt sequence can be interrupted \u2014 stop this one by hand in its tab, or close the tab with `tab-close`",nextCommandArgs:["api","get-task","--task-id",task.id]})}async function interruptTask(ctx){let taskId=ctx.args.str("task-id"),groupId=ctx.args.str("group"),all=ctx.args.bool("all")??!1,tabFlag=ctx.args.str("tab"),targets=[taskId!==void 0,groupId!==void 0,all].filter(Boolean).length;if(targets>1)throw new ApiError("pass one of --task-id, --group or --all","BAD_FLAG");if(targets===0)throw new ApiError("interrupt needs --task-id ID, --group GROUPID or --all","MISSING_TARGET");if(taskId!==void 0)return interruptOne(ctx,taskId,tabFlag);if(tabFlag)throw new ApiError("--tab names one task's tab; it only goes with --task-id","BAD_FLAG");return interruptMany(ctx,groupId)}async function interruptMany(ctx,groupId){let daemon=daemonOf(ctx),{tasks}=await daemon.request("task.list"),members=groupId===void 0?tasks:tasks.filter((t)=>t.groupId===groupId);if(groupId!==void 0&&members.length===0)throw new ApiError(`no tasks in group ${groupId}`,"TASK_NOT_FOUND",{hint:"the groupId is the one `add --count` returned; any surviving sibling's `.groupId` in `list` recovers it",nextCommandArgs:["api","list"]});let activity=await readActivityRegistry(daemon);if(!activity)throw new ApiError("could not read engine activity, so which tasks are running is unknown","BAD_DAEMON",{hint:"interrupt the tasks one at a time with --task-id",nextCommandArgs:["api","inspect"]});let results=[];for(let member of members){let scope=ptyScopeFor(tasks,member.id),runningTabs=Object.entries(activity.tabs[member.id]??{}).filter(([tabId,entry])=>entry.state==="running"&&hostedKeyFor(member.id,tabId,scope)!==null);if(runningTabs.length===0){results.push({taskId:member.id,status:"skipped",state:activity.tasks[member.id]?.state??null});continue}for(let[tabId]of runningTabs)try{results.push({...await interruptOne(ctx,member.id,tabId),status:"interrupted"})}catch(err){results.push({taskId:member.id,tabId,status:"failed",error:errorMessage(err),...err instanceof ApiError?{code:err.code}:{}})}}let count=(status)=>results.filter((r)=>r.status===status).length;return{...groupId!==void 0?{groupId}:{},count:members.length,interrupted:count("interrupted"),failures:count("failed"),results}}async function interruptOne(ctx,taskId,tabFlag){let{task}=await daemonOf(ctx).request("task.get",{taskId}),sequence=interruptSequenceFor({...task,id:taskId}),host=await openHostedSessionHost();if(!host)throw new ApiError(`no pty host to reach task ${taskId}`,"NO_ENGINE_TAB",{taskId,hint:"nothing is hosting this task's engine \u2014 `pty-list` says whether a host is up at all",nextCommandArgs:["api","pty-list"]});try{let sessions=await listHostedSessions(host.rpc),scope=await ptyScopeOf(daemonOf(ctx),task),key=tabFlag?hostedKeyFor(taskId,tabFlag,scope):findHostedEngineKey(sessions,taskId,taskEngineArgv(task)[0],scope);if(!key||!sessions.some((session)=>session.key===key&&session.alive))throw new ApiError(tabFlag?`task ${taskId} has no live tab ${tabFlag}`:`task ${taskId} has no live engine tab to interrupt`,tabFlag?"TAB_NOT_FOUND":"NO_ENGINE_TAB",{taskId,...tabFlag?{tabId:tabFlag}:{},hint:"`get-task` lists the live tabs in .tabs[]",nextCommandArgs:["api","get-task","--task-id",taskId]});return await host.rpc.request("pty.write",{key,data:sequence}),{ok:!0,taskId,tabId:key.split("::")[1]??"tab-1",vendor:task.vendor,interrupted:!0,bytes:Buffer.byteLength(sequence,"utf8")}}finally{host.close()}}var INTERRUPT_VERB={name:"interrupt",group:"drive",summary:"Stop the turn a task's engine is currently running \u2014 the headless twin of pressing the engine's own interrupt key. Writes the ENGINE-declared interrupt bytes to its pty; the session, its conversation and its worktree all survive, which is what separates this from `tab-close` and `delete`. Use it when a worker runs away: `send` cannot reach it (a busy engine's composer is not quiet), and until now the only levers left destroyed something. Engines that have not declared an interrupt sequence (every `generic` protocol engine) are refused with UNSUPPORTED rather than guessed at \u2014 Esc and ctrl-C mean opposite things across engines. Returns { taskId, tabId, vendor, interrupted, bytes }; `interrupted` reports the WRITE, so read `collect`'s .activity.state for the effect. Pass --group (one fan-out round) or --all (every task on this machine) instead of --task-id to stop many at once: only tabs whose own activity is `running` are written to, and the reply is { groupId?, count, interrupted, failures, results } with one entry per attempted tab, or a skipped entry for a member with no known running tabs. count counts members; interrupted and failures count tabs. Entries have status interrupted, skipped (with its .state) or failed (with .error/.code).",flags:[F.taskId(!1),{name:"group",type:"string",placeholder:"GROUPID",description:"Interrupt every running task of one fan-out round (the `groupId` that `add --count` returns). Mutually exclusive with --task-id and --all."},{name:"all",type:"bool",description:"Interrupt every running task on this machine. Mutually exclusive with --task-id and --group."},{name:"tab",type:"string",placeholder:"TAB",description:"Interrupt this exact tab (id from `get-task` .tabs[].id). Omitted = the task's canonical engine tab, the same one a bare `send` targets. Only with --task-id."}],handler:interruptTask};var PANE_VERB={name:"pane-open",group:"drive",summary:"Open a terminal pane in a task's workspace: split the focused tab (default, or --tab's tab) or open a separate command tab, optionally running a command. Broadcast over the daemon's tab.open channel \u2014 an attached TUI showing the task performs the split. Task defaults to $ROVE_TASK_ID, then the active task. Returns the resolved `title` (the label `pane-close --title` must match \u2014 derived from the command's first word when --title is omitted) plus `clients` (attached connections, the caller included) and `reached` (false = no front-end subscribes to the channel, so nobody performed the split).",flags:[F.taskId(!1),{name:"tab",type:"string",placeholder:"TAB",description:"Host tab for the split (e.g. tab-3) instead of the focused tab (split placement only)."},{name:"command",type:"string",placeholder:"CMD",description:"Shell command the pane runs (via the login shell's `-ilc`, so pipes/args and your shell rc's PATH/exports work); the pane closes when it exits. Omit for an interactive shell."},{name:"direction",type:"enum",values:["right","down"],default:"right",description:"Split orientation relative to the active pane (split placement only)."},{name:"placement",type:"enum",values:["split","tab"],default:"split",description:"`split` joins the focused tab's split group; `tab` opens a separate command tab."},{name:"title",type:"string",placeholder:"TEXT",description:`Pane label (default: the command's first word, else "shell").`}],handler:async(ctx)=>{let client=daemonOf(ctx),taskId=ctx.args.str("task-id")??process.env.ROVE_TASK_ID??await resolveActiveTaskId(client);if(!taskId)throw new ApiError("no target task: pass --task-id (no $ROVE_TASK_ID, no active task)","MISSING_TARGET");let command=ctx.args.str("command"),shell=resolveSessionShell(),argv=command?[shell,"-ilc",command]:[shell,"-il"],title=ctx.args.str("title")??(command?command.trim().split(/\s+/)[0]??"shell":"shell"),tabId=ctx.args.str("tab");return{...await simpleRpc(ctx,"tab.open",{taskId,argv,title,...tabId!==void 0?{tabId}:{},placement:ctx.args.str("placement")??"split",direction:ctx.args.str("direction")??"right"}),title}}},PANE_CLOSE_VERB={name:"pane-close",group:"drive",summary:"Close panes opened by pane-open: every split pane / command tab in the task whose label matches --title. Broadcast over the daemon's tab.close channel \u2014 an attached TUI showing the task performs the close (headless no-op). Task defaults to $ROVE_TASK_ID, then the active task. Returns `clients` (attached connections, the caller included) and `reached` (false = nobody performed the close).",flags:[F.taskId(!1),{name:"title",type:"string",required:!0,placeholder:"TEXT",description:"Pane label to close \u2014 the --title the pane was opened with (engine panes are never closed)."},{name:"tab",type:"string",placeholder:"TAB",description:"Scope the title match to one tab (e.g. tab-3) instead of every tab of the task."}],handler:async(ctx)=>{let client=daemonOf(ctx),taskId=ctx.args.str("task-id")??process.env.ROVE_TASK_ID??await resolveActiveTaskId(client);if(!taskId)throw new ApiError("no target task: pass --task-id (no $ROVE_TASK_ID, no active task)","MISSING_TARGET");let tabId=ctx.args.str("tab");return simpleRpc(ctx,"tab.close",{taskId,title:ctx.args.str("title"),...tabId!==void 0?{tabId}:{}})}},TAB_CLOSE_VERB={name:"tab-close",group:"drive",summary:"Close one Terminal Tab by the id returned in get-task .tabs[]. Runs the same close path as ctrl+w when a TUI is attached; otherwise removes the persisted tab snapshot and ends its hosted PTYs directly. Engine, shell/command, and content tabs are all valid. Closing the last tab leaves the task open with no session.",flags:[F.taskId(!0),{name:"tab",type:"string",required:!0,placeholder:"TAB",description:"Exact Terminal Tab id from get-task .tabs[].id (for example tab-3)."}],handler:async(ctx)=>{let taskId=ctx.args.require("task-id"),tabId=ctx.args.require("tab"),daemon=daemonOf(ctx);if((await daemon.request("terminalTab.close",{taskId,tabId})).handled)return{ok:!0,taskId,tabId,handledBy:"tui"};let result=await ctx.runtime.closeTerminalTab(daemon,taskId,tabId);return{ok:!0,taskId,tabId,handledBy:"headless",...result}}};async function launchKind(ctx,daemon,task,kind){let taskId=task.id,worktree=taskCwd(task);if(!worktree)throw new ApiError(`task ${taskId} has no worktree yet`,"NO_WORKTREE");let spec;if(kind==="editor"){let line=ctx.args.int("line");spec={kind,taskId,worktree,file:ctx.args.require("file"),...line===void 0?{}:{line}}}else if(kind==="pane")spec={kind,taskId,worktree,pane:ctx.args.require("pane"),hostTab:ctx.args.str("tab")};else{let viewId=ctx.args.require("view-task");if(viewId===taskId)throw new ApiError("--view-task must name another task","BAD_FLAG",helpStep("tab-new"));let{task:shown}=await daemon.request("task.get",{taskId:viewId});if(!shown.worktreePath)throw new ApiError(`task ${viewId} has no worktree yet`,"NO_WORKTREE");if(shown.repo!==task.repo)throw new ApiError("--view-task must be a task of the same project","BAD_FLAG",helpStep("tab-new"));spec={kind,taskId,view:{id:viewId,worktree:shown.worktreePath}}}let launched=await ctx.runtime.launchTab(daemon,spec),reply=await daemon.request("terminalTab.add",{taskId,tab:launched.tab});if(launched.replaces){if(!(await daemon.request("terminalTab.close",{taskId,tabId:launched.replaces})).handled)await ctx.runtime.closeTerminalTab(daemon,taskId,launched.replaces)}return{ok:!0,taskId,tabId:launched.tab.id,session:launched.session,kind,...launched.leaf?{leaf:launched.leaf}:{},...launched.atLine===void 0?{}:{atLine:launched.atLine},clients:reply.clients??0}}async function openShellTab(ctx,daemon,task,type){let taskId=task.id,own=task.scratch===!0&&((await readDaemonTabs(daemon,taskId))?.tabs.length??0)===0,row=own?task:await spawnSharedChild(daemon,taskId,void 0,SHELL_TASK_TITLE);try{let opened=await ctx.runtime.openTab(daemon,{id:row.id,worktreePath:taskCwd(row),kind:row.kind,vendor:row.vendor,command:row.command,repo:row.repo,...task.extraRepos?.length?{multiRepo:!0}:{}},"shell",{home:!0,...type===void 0?{}:{type}}),shell={ok:!0,taskId,tabId:opened.tab.id,session:opened.session,kind:"shell"};if(!own)return{...shell,childTaskId:row.id};let reply=await daemon.request("terminalTab.add",{taskId,tab:opened.tab});return{...shell,clients:reply.clients??0}}catch(err){if(own)throw err;return failSharedChild(daemon,err,row.id,["api","tab-new","--task-id",taskId,"--kind","shell"])}}var TAB_NEW_VERB={name:"tab-new",group:"drive",summary:"Open a new Terminal Tab with no first message: a login `shell`, or an `engine` conversation that starts idle (the TUI's sidebar \"New shell\" and picker Enter). An `engine` starts in the task's own `tab-1` while the task has no conversation yet, else in a new shared child task of --task-id (same directory; the reply then carries `childTaskId`) whose engine starts in its own `tab-1`, never a second engine tab on one task; it respects engine.maxLiveSessions. Unlike `send --tab new` it needs no prompt. A `shell` is always its own row: a new shared child task of --task-id (same directory, title `shell`; the reply carries `childTaskId`) whose own `tab-1` runs the login shell, spawned in the task's worktree (creating it if needed). A fresh, tab-less Ungrouped (scratch) task is the exception: its shell is its `tab-1`, no child. When the shell exits the row stays, empty, and \u23CE starts a fresh shell; nothing deletes it. Three more kinds run something Rove resolves itself, never a command line from the caller: `editor` opens `--file` (relative to the worktree, at `--line` when given) in the editor from Settings \u2192 General \u2192 Editor / $VISUAL / $EDITOR, in the task's one editor slot (a previous editor tab is closed once the new one is up); `pane` opens a registered plugin pane (`--pane <plugin>.<pane>`), split into `--tab` (default: the active tab) or as its own tab as the pane's manifest says; `viewport` adds a tab onto another task's first engine session (`--view-task`, same project) without starting anything. Broadcast over the daemon's tab.add channel so an attached TUI absorbs the row (`clients` = attached connections; the non-engine kinds and an Ungrouped task's own shell only). Returns the new `tabId` and its pty `session` key; the tab then lists in get-task .tabs[].",flags:[F.taskId(!0),{name:"kind",type:"enum",values:["shell","engine","editor","pane","viewport"],required:!0,placeholder:"KIND",description:"`shell` = the user's login shell; `engine` = an idle engine in a new shared child task; `editor` = a file in the user's editor; `pane` = a plugin pane; `viewport` = another task's engine session."},{name:"command",type:"string",placeholder:"CMD",description:"Engine only: run the new child task on this engine (an engine id from `engine-list` or a full command line), as `send --tab new --command` does. Omitted = the task's own engine."},{name:"latest",type:"bool",description:"Engine tabs only: open on the engine this project used last (the TUI's ctrl+t), else the default engine, instead of the task's own. Ignored beside `--command`."},{name:"file",type:"string",placeholder:"PATH",description:"Editor tabs only: the file to open, relative to the task's worktree."},{name:"line",type:"int",placeholder:"N",description:"Editor tabs only: open at this line. `atLine: false` in the result means the editor takes no line argument Rove knows; add `{line}` to editor.customCommand."},{name:"pane",type:"string",placeholder:"ID",description:"Pane tabs only: `<plugin id>.<pane id>` of an enabled plugin's pane."},{name:"tab",type:"string",placeholder:"TAB",description:"Pane tabs only: the tab a split pane joins (e.g. tab-3); default the task's active tab."},{name:"view-task",type:"string",placeholder:"ID",description:"Viewport tabs only: the task (same project) whose first engine session the tab shows."},{name:"type",type:"string",placeholder:"TEXT",description:"Shell tabs only: type TEXT into the new shell once it is up, then press Enter (e.g. a dev server's command). The tab stays a normal shell: Ctrl+C stops what it started."}],handler:async(ctx)=>{let daemon=daemonOf(ctx),taskId=ctx.args.require("task-id"),kind=ctx.args.requireEnum("kind"),latest=ctx.args.bool("latest")===!0;if(ctx.args.str("line")!==void 0&&kind!=="editor")throw new ApiError("--line only applies to an editor tab; pass --kind editor","BAD_FLAG",helpStep("tab-new"));if((ctx.args.str("command")||latest)&&kind!=="engine")throw new ApiError("--command and --latest only apply to an engine tab; pass --kind engine","BAD_FLAG",helpStep("tab-new"));let type=ctx.args.str("type");if(type!==void 0&&kind!=="shell")throw new ApiError("--type only applies to a shell tab; pass --kind shell","BAD_FLAG",helpStep("tab-new"));let{task}=await daemon.request("task.get",{taskId}),preferred=latest?resolvePreferredVendor(task.repo):void 0,command=ctx.args.str("command")||(preferred&&preferred!==task.vendor?preferred:void 0);if(kind==="editor"||kind==="pane"||kind==="viewport")return launchKind(ctx,daemon,task,kind);if(kind==="engine")return withFirstConversationLock(taskId,async()=>{let home=await conversationHome(daemon,ctx.runtime,task,command),target={id:home.task.id,worktreePath:taskCwd(home.task),kind:home.task.kind,vendor:home.task.vendor,command:home.task.command,modelEffort:home.task.modelEffort,model:home.task.model,repo:home.task.repo,...task.extraRepos?.length?{multiRepo:!0}:{}};if(!home.child){let opened=await ctx.runtime.openTab(daemon,target,kind),reply=await daemon.request("terminalTab.add",{taskId,tab:opened.tab});return{ok:!0,taskId,tabId:opened.tab.id,session:opened.session,kind,clients:reply.clients??0}}try{let opened=await ctx.runtime.openTab(daemon,target,kind);return{ok:!0,taskId,childTaskId:home.task.id,tabId:opened.tab.id,session:opened.session,kind}}catch(err){return failSharedChild(daemon,err,home.task.id,["api","tab-new","--task-id",taskId,"--kind","engine",...command?["--command",command]:[]])}});return openShellTab(ctx,daemon,task,type)}},TAB_SPLIT_VERB={name:"tab-split",group:"drive",summary:"Edit one Terminal Tab's split panes without a TUI: `open` splits a pane (the TUI's ctrl+\\ right / ctrl+= down) into a new login shell in the task's worktree, `close` ends a pane, `rename` names it (F2), `reset` kills a shell pane and spawns a fresh one (F5; engine panes refuse). The layout is the tab's persisted split tree and each pane beyond `leaf-1` is its own hosted session `<taskId>::<tabId>::<leaf>`, so an attached TUI absorbs the new row over tab.add and a later `tab-close` sweeps the panes. The shell is always the login shell: no command line is taken. Returns the pane acted on, whether the tree changed (`false` when the pane is too small to split), and the tab's `split` layout (null once back to one pane).",flags:[F.taskId(!0),{name:"tab",type:"string",required:!0,placeholder:"TAB",description:"Exact Terminal Tab id from get-task .tabs[].id (for example tab-2)."},{name:"action",type:"enum",values:["open","close","rename","reset"],required:!0,placeholder:"ACTION",description:"`open` a new pane, `close` one, `rename` one, or `reset` a shell pane."},{name:"direction",type:"enum",values:["right","down"],default:"right",description:"`open` only: split toward the right (side by side) or down (stacked)."},{name:"leaf",type:"string",placeholder:"LEAF",description:"The pane to act on (`leaf-1` is the tab's own process); for `open`, the pane being split. Default: the tree's active pane."},{name:"title",type:"string",placeholder:"TEXT",description:"`rename` only: the pane's name; empty clears it back to the default."},{name:"cols",type:"int",placeholder:"N",description:"Cells wide of the pane acted on; with --rows, `open` refuses a split that would leave a pane under 20x6."},{name:"rows",type:"int",placeholder:"N",description:"Cells high of the pane acted on (see --cols)."}],handler:async(ctx)=>{let daemon=daemonOf(ctx),taskId=ctx.args.require("task-id"),tabId=ctx.args.require("tab"),action=ctx.args.requireEnum("action"),{task}=await daemon.request("task.get",{taskId});if(!task.worktreePath)throw new ApiError(`task ${taskId} has no worktree yet`,"NO_WORKTREE");let cols=ctx.args.int("cols"),rows=ctx.args.int("rows");if(cols===void 0!==(rows===void 0))throw new ApiError("--cols and --rows go together","BAD_FLAG",helpStep("tab-split"));let size=cols!==void 0&&rows!==void 0?{cols,rows}:void 0,leaf=ctx.args.str("leaf")??"";if(action!=="open"&&!leaf)throw new ApiError(`--leaf is required to ${action}`,"BAD_FLAG",helpStep("tab-split"));let edit=action==="open"?{kind:"open",orientation:ctx.args.str("direction")==="down"?"column":"row",from:leaf,size}:action==="rename"?{kind:"rename",leaf,title:ctx.args.str("title")??""}:action==="reset"?{kind:"reset",leaf,size}:{kind:"close",leaf},out=await ctx.runtime.splitTab(daemon,taskId,tabId,taskCwd(task),edit),reply=out.changed?await daemon.request("terminalTab.add",{taskId,tab:out.tab}):{};return{ok:!0,taskId,tabId,leaf:out.leaf,changed:out.changed,split:wireSplit(out.tab.splitTree),clients:reply.clients??0}}};var VIEWER_SHAPE="{ viewer, surface: tui|web|phone, taskId, tabId?, panel (has a side panel: web desktop only), panelOpen?, narrow?, local (same machine as the daemon) }",VIEWERS_VERB={name:"viewers",group:"read",summary:`Every UI attached and showing a task right now, so an agent knows what the human can see before it drives anything. Returns { viewers: [${VIEWER_SHAPE}] }. A TUI is surface tui (no panel, local). \`panel: true\` = a web page with a side panel, the only screen \`panel-show\` can open; \`local: false\` = behind a tunnel, so its \`localhost\` is not yours. An empty list = nobody is looking. --task-id keeps one task's viewers.`,flags:[F.taskId(!1)],handler:(ctx)=>simpleRpc(ctx,"ui.viewers",{taskId:ctx.args.str("task-id")})},PANEL_SHOW_VERB={name:"panel-show",group:"drive",summary:"Bring up the web side panel on a view of a task: the Browser (--url), a file in the Files view or the Diff view (--path). Every web client remembers the view for the task; a web page currently on that task with a panel opens it (no focus steal, no task switch). A TUI has no panel: nothing happens there. Task defaults to $ROVE_TASK_ID, then the active task. Returns { shown (pages that open it now), remembered (web pages that keep it for later), viewers (as `viewers`, for the task), url? { reachable, frameable, reason? } }. `url.reachable: false` = a localhost URL and the viewer is remote; `url.frameable: false` = the site forbids framing (X-Frame-Options / CSP frame-ancestors) or was refused by the probe, so tell the human the link instead.",flags:[F.taskId(!1),{name:"view",type:"enum",values:["browser","files","diff"],required:!0,description:"Which panel view to show."},{name:"url",type:"string",placeholder:"URL",description:"Browser view: the http(s) page to load. Only localhost is framed over plain http."},{name:"path",type:"string",placeholder:"FILE",description:"Files / Diff view: a file relative to the task's worktree (no `..`)."}],handler:async(ctx)=>{let taskId=ctx.args.str("task-id")??process.env.ROVE_TASK_ID??await resolveActiveTaskId(daemonOf(ctx));if(!taskId)throw new ApiError("no target task: pass --task-id (no $ROVE_TASK_ID, no active task)","MISSING_TARGET");return simpleRpc(ctx,"panel.show",{taskId,view:ctx.args.str("view"),url:ctx.args.str("url"),path:ctx.args.str("path")})}};var DEFAULT_TTL_SECONDS=60;function sourceOf(env=process.env){return env.ROVE_PLUGIN_ID??"cli"}async function rowToken(ctx){let{args}=ctx,taskId=args.require("task-id"),clear=args.bool("clear")===!0,text=args.str("text");if(!clear&&!text)throw new ApiError("row-token needs --text, or --clear to remove one","MISSING_TEXT");let key=args.str("key"),tone=args.str("tone"),ttl=args.int("ttl");return simpleRpc(ctx,"task.rowToken",{taskId,source:sourceOf(),...key?{key}:{},...clear?{clear:!0}:{text,...tone?{tone}:{}},...ttl!==void 0?{ttlMs:ttl*1000}:{}})}var ROW_TOKEN_VERB={name:"row-token",group:"drive",summary:`Put one short label on a task's sidebar/board row, with a TTL \u2014 the surface a coordination plugin paints its own state on. Every token EXPIRES (default ${DEFAULT_TTL_SECONDS}s, max 1h): a plugin refreshes the label to keep it, and one that dies has its labels fade instead of leaving stale state on screen. A plugin writes into its own slot (\`ROVE_PLUGIN_ID\` + --key, 2 slots per plugin per task) and can say nothing else: the derived group, the activity badge, the PR chip, the title and the branch are host-owned. \`--tone\` names a ROLE, not a colour, so the active theme still decides how it looks. Text is trimmed to ${ROW_TOKEN_MAX_TEXT} characters. Returns { ok, token } (or { ok, cleared } with --clear).`,flags:[F.taskId(!0),{name:"text",type:"string",placeholder:"LABEL",description:`The label, trimmed to ${ROW_TOKEN_MAX_TEXT} characters. Required unless --clear.`},{name:"ttl",type:"int",default:String(DEFAULT_TTL_SECONDS),placeholder:"SECONDS",description:"How long the label survives without a refresh (1\u20263600, clamped)."},{name:"key",type:"string",default:"default",placeholder:"SLOT",description:"Which of your two slots on this row to write. Writing the same key again replaces the token."},{name:"tone",type:"enum",values:ROW_TOKEN_TONES,description:"Semantic role the theme colours. Omitted = the row's own muted tone."},{name:"clear",type:"bool",description:"Remove the token at --key now, or every token of yours on this row when --key is omitted."}],handler:rowToken};var TAB_REVIVE_VERB={name:"tab-revive",group:"drive",summary:"Reopen one Terminal Tab's own session when no PTY runs in it: a dead or freeze-restored tab, or one whose key the PTY host lost (a host restart that dropped its freeze records). A running tab is left alone (`revived: false, alive: true`). With --restart-exited, an engine tab whose session is alive but whose engine has exited (get-task .tabs[].engineAlive false: only the fallback shell is left) is closed and relaunched like a dead one; an engine still running, or a shell tab, is still left alone. An engine tab resumes its persisted conversation (`resumed: true`) or, with none recorded, starts a fresh one with no first message \u2014 the task's original prompt is never replayed; it respects engine.maxLiveSessions. A shell or command tab runs its recorded command in the task's worktree. Refused: a tab id the task does not hold (TAB_NOT_FOUND), a split leaf (`tab-split --action reset` is its reopen), and viewport or preview tabs (TAB_NOT_REVIVABLE). A task on another registered machine runs there, like every verb. Returns { taskId, tabId, revived, resumed, sessionId? }.",flags:[F.taskId(!0),{name:"tab",type:"string",required:!0,placeholder:"TAB",description:"Exact Terminal Tab id from get-task .tabs[].id (for example tab-2)."},{name:"restart-exited",type:"bool",placeholder:"BOOL",description:"Also relaunch a tab whose engine exited inside a still-running session (keepAlive's fallback shell); without it that tab counts as running."}],handler:async(ctx)=>{let daemon=daemonOf(ctx),taskId=ctx.args.require("task-id"),tabId=ctx.args.require("tab");if(tabId.includes("::")){let[tab,...leaf]=tabId.split("::");throw new ApiError(`${tabId} is a split pane, not a tab`,"BAD_FLAG",{taskId,hint:"reopen a split pane with `tab-split --action reset`",nextCommandArgs:["api","tab-split","--task-id",taskId,"--tab",tab,"--action","reset","--leaf",leaf.join("::")]})}let{task}=await daemon.request("task.get",{taskId}),scope=await ptyScopeOf(daemon,task);if(hostedKeyFor(taskId,tabId,scope)===null)throw new ApiError(`tab ${tabId} on task ${taskId} belongs to a child task's conversation`,"TAB_NOT_FOUND",{taskId,tabId,hint:"address the child itself with its own --task-id (`list` shows the lineage)",nextCommandArgs:["api","get-task","--task-id",taskId]});let alias=scope.alias;if(alias?.tabId===tabId)throw new ApiError(`tab ${tabId} of task ${taskId} runs under task ${alias.taskId}`,"TAB_NOT_REVIVABLE",{taskId,tabId,hint:"revive it under the task that hosts the session",nextCommandArgs:["api","tab-revive","--task-id",alias.taskId,"--tab",alias.tabId]});let worktree=taskCwd(task);if(!worktree)throw new ApiError(`task ${taskId} has no worktree yet`,"NO_WORKTREE");let exited=ctx.args.bool("restart-exited")===!0&&(await ctx.runtime.taskTabs(taskId,taskEngineArgv(task),scope)).tabs.some((t)=>t.id===tabId&&t.kind==="engine"&&t.alive===!0&&t.engineAlive===!1),revived=await ctx.runtime.reviveTab(daemon,{id:taskId,worktreePath:worktree,kind:task.kind,vendor:task.vendor,command:task.command,modelEffort:task.modelEffort,model:task.model,repo:task.repo},tabId,worktree,{replaceExited:exited});return{ok:!0,taskId,tabId,...revived}}};var DRIVE_VERBS=[{name:"send",group:"drive",summary:"Paste a follow-up prompt into a task's running engine (one full turn). Without --task-id, a task spawned from another Rove session replies to its dispatcher's tab (then that task's live canonical engine; nothing alive = DISPATCHER_UNREACHABLE, never a silent spawn); otherwise the active task. Sent from inside another Rove task ($ROVE_TASK_ID), the prompt is prefixed with [ROVE PEER] provenance \u2014 who sent it and how to reply (tab-precise) \u2014 so agent-to-agent messaging needs no coordinator. The prompt is pasted into the target composer and submitted; delivery is refused only when there is physically nothing to write to (no such tab, dead PTY, no engine process). A `succeeded:` report sent from a managed task whose branch has 0 commits is REFUSED (EMPTY_SUCCESS_REPORT) \u2014 commit first, or pass --allow-empty when the task genuinely produced no commits. `delivered: true` means the bytes landed, nothing more: when the target session was already in `error` or `dead`, the reply carries `targetState` (+ `targetDetail`) \u2014 read it before waiting on an answer.",flags:[F.taskId(!1),F.prompt(!1,"Text pasted + submitted into the engine pane. One of --prompt / --prompt-file is required, except with --handoff-from."),F.promptFile(),{name:"tab",type:"string",required:!1,placeholder:"TAB",description:'Tab addressing: "new" sends the prompt to a new conversation: `tab-1` of the target itself while it has none yet, else a shared child task of the target (same directory) \u2014 the reply then adds `childTaskId`, and `session` is the child\'s; "tab-N" delivers to that exact alive tab (error when dead/absent). Omitted = the canonical engine tab.'},{...F.command(),description:"Engine launch command for the child `--tab new` creates \u2014 the API twin of the TUI's ctrl+e pick. Lets one directory run two agents on the same files (e.g. hand the stuck work to codex without leaving the branch). An engine id from `engine-list` or a full command line; stored on the child, so it survives restarts. Only valid with --tab new."},{name:"handoff-from",type:"string",required:!1,placeholder:"TAB",description:"Continue another tab's conversation in the new one (`--tab new`, usually with --command naming the other engine): the prompt becomes a handoff brief \u2014 the task title, the source engine's transcript path, the user's last request and a bounded excerpt of the latest turns \u2014 followed by --prompt when given, so --prompt is optional. Same directory and branch; the source tab is only read. Refuses a tab with no conversation yet (NO_CONVERSATION) or an engine whose transcript Rove cannot name (NO_TRANSCRIPT)."},{name:"replace",type:"bool",required:!1,description:"With --handoff-from tab-N: put the new engine in that tab's own slot instead of a shared child task. Only for a source whose engine is dead or exited (`get-task` .tabs[]: `alive: false`, or `engineAlive: false` = only the fallback shell is left); a running engine is refused (TAB_ALIVE) and keeps the child. Same task, same tab id, same worktree: the tab is repointed at the --command engine with a fresh conversation (the old session is left untouched on disk), launched, and handed the brief. The reply adds `replacedTab`. The shipped UIs pass this exact rule on their own; a script chooses it."},{name:"respawn",type:"bool",required:!1,description:"Revive a FREEZE-RESTORED --tab tab-N before delivering. After a pty-host restart (reboot, crash) a tab keeps its scrollback and launch command but nothing runs in it; without this flag such a tab is refused (TAB_RESTORED) rather than silently re-run. With it, the tab is respawned in place and resumes its pinned conversation when it has one (`get-task` shows each tab's sessionId) \u2014 a tab with none replays its recorded launch command, which for claude carries the task's original first prompt. Only valid with --tab tab-N."},{name:"plain",type:"bool",required:!1,description:"Deliver the prompt verbatim \u2014 skip the [ROVE PEER] provenance prefix."},{name:"allow-empty",type:"bool",required:!1,description:"Report success from a task with zero commits (EMPTY_SUCCESS_REPORT is refused otherwise). For work that legitimately produces no commits \u2014 an investigation, a review, a question answered."},{name:"expect-report",type:"bool",required:!1,description:"Arm a delivery: the prompt gets a [ROVE] footer naming `report --delivery <id>`, the reply carries `deliveryId`, and a turn that ends without that report raises a `report_missing` Inbox row. Implied for a task added with --require-report. See `deliveries`."},{name:"if-runnable",type:"bool",required:!1,description:"Send only when `should-run` says the task can use a tick. The check, the delivery claim and the paste share one decision, so two ticks arriving together cannot both pass. When it refuses, nothing is pasted and the reply is `{skipped: {run:false, reason, detail?, until?, gateIds?}}` with exit 3 (a skip recorded as a `skipped` delivery when the send would have owed a report). A skip is dropped, never held for later. See `should-run`."},{name:"queue",type:"bool",required:!1,description:"Wait for a running turn instead of landing in it. A tab that is idle takes the prompt now, as without the flag; one that is working, waiting on a permission menu or rate-limited has it held by the daemon and pasted when its turn ends, one held prompt per turn, in order. The reply is then `{queued:true, queueId, position, tabId}`. Without it a prompt sent mid-turn lands in that turn: Codex drops the work in flight, Claude Code folds it in. A report (`--expect-report`) is armed when the prompt is pasted, not when it is held. See `queue-list`, `queue-edit`, `queue-cancel`; a daemon restart drops held prompts."}],handler:send},DISPATCH_VERB,INTERRUPT_VERB,QUEUE_EDIT_VERB,QUEUE_CANCEL_VERB,{name:"note",group:"drive",summary:"File a one-line field note \u2014 a resolved, repo-level gotcha worth sharing. Appended to the repo's durable note store (every future session on this repo starts with it) and forwarded to the dispatcher session for live relay (docs/design/dispatcher.md).",flags:[F.taskId(!0),{name:"text",type:"string",required:!0,placeholder:"TEXT",description:"One line: the verified conclusion another session could act on."}],handler:note},{name:"note-list",group:"drive",summary:"Read a repo's accumulated field notes, newest first. Returns { notes } \u2014 each with the `id` note-delete takes.",flags:[F.repo(!0)],handler:(ctx)=>simpleRpc(ctx,"note.list",{repo:ctx.args.requireRepo("repo")})},{name:"note-delete",group:"drive",summary:"Retire one field note by the id `note-list` reports. The note store is not an archive: its newest entries are injected into every fresh session on the repo, so a note whose fact has stopped being true keeps being handed to agents as if it still were. Returns { deleted } \u2014 false (not an error) when the id names nothing, which is also what an already-evicted note answers.",flags:[F.repo(!0),{name:"id",type:"int",required:!0,placeholder:"N",description:"Note id from `note-list`."}],handler:(ctx)=>simpleRpc(ctx,"note.delete",{repo:ctx.args.requireRepo("repo"),id:ctx.args.int("id")})},PANE_VERB,PANE_CLOSE_VERB,PANE_GRAPHICS_VERB,TAB_CLOSE_VERB,TAB_NEW_VERB,TAB_SPLIT_VERB,TAB_REVIVE_VERB,PANEL_SHOW_VERB,{name:"notify",group:"drive",summary:"Show a toast in every attached Rove UI \u2014 broadcast over the daemon's notice.event channel. Agents/scripts use it to surface 'done / needs input / error' moments without touching the task's session. Returns `clients` (attached connections; 0 = no UI showed the toast).",flags:[{name:"title",type:"string",required:!0,placeholder:"TEXT",description:"Toast text (one line)."},{name:"body",type:"string",placeholder:"TEXT",description:"Optional second line under the title \u2014 context, not a second message."},{name:"kind",type:"string",default:"done",placeholder:"KIND",description:`Free-form kind tag. "done", "needs_input" and "error" get the TUI's severity styling/unread mark; any other value renders neutrally.`},F.taskId(!1),{name:"source",type:"string",placeholder:"TAG",description:"Free-form origin tag (e.g. an agent name) recorded on the event."}],handler:async(ctx)=>{return simpleRpc(ctx,"notice.send",{title:ctx.args.str("title"),body:ctx.args.str("body"),kind:ctx.args.str("kind")??"done",taskId:ctx.args.str("task-id"),source:ctx.args.str("source")})}},{name:"prompt",group:"drive",summary:"Ask the human for a line of text through the attached TUI's input dialog (plugins' host-provided prompt). Blocks until answered, cancelled, or timed out; returns { value } or { cancelled, reason }. With no front-end attached it returns at once with reason `no-gui`.",flags:[{name:"title",type:"string",required:!0,placeholder:"TEXT",description:"Dialog title (shown verbatim)."},{name:"placeholder",type:"string",placeholder:"TEXT",description:"Input placeholder."},{name:"initial",type:"string",placeholder:"TEXT",description:"Pre-filled input value."},{name:"timeout",type:"string",placeholder:"MS",description:"Give up after this many milliseconds (default 120000, max 600000)."}],handler:async(ctx)=>{let timeoutRaw=ctx.args.str("timeout"),timeoutMs=timeoutRaw?Number.parseInt(timeoutRaw,10):void 0;return simpleRpc(ctx,"ui.prompt",{title:ctx.args.str("title"),placeholder:ctx.args.str("placeholder"),initial:ctx.args.str("initial"),...timeoutMs&&Number.isFinite(timeoutMs)?{timeoutMs}:{}})}},{name:"engine-report",group:"drive",summary:"Report a normalized engine-activity verb for a task \u2014 the public face of the same engine.reportEvent RPC the built-in hook adapters use. Lets a plugin-contributed engine (or any wrapper script) drive the sidebar badge, attention inbox, and plugin event stream without a built-in hook adapter. Kinds: session-start|turn-start|turn-complete|turn-failed|turn-interrupted|awaiting-input|session-end (state kinds) plus tool-pre|tool-post|tool-failed|pre-compact|post-compact|subagent-start|subagent-stop (plugin-only).",flags:[F.taskId(!1),{name:"kind",type:"enum",values:ENGINE_ACTIVITY_KINDS,required:!0,placeholder:"KIND",description:"Normalized activity verb (see summary). Unknown kinds are rejected."},{name:"engine",type:"string",placeholder:"ID",description:"Engine id producing the report (a plugin engine id, or a built-in vendor)."},{name:"tab",type:"string",placeholder:"TAB",description:"Terminal tab id the session runs in (defaults to $ROVE_TAB_ID / $ROVE_TAB_ID)."},{name:"detail",type:"string",placeholder:"JSON",description:`Optional detail JSON, e.g. '{"failure":"rate_limit"}' or '{"waiting":"input"}'.`}],handler:async(ctx)=>{let taskId=ctx.args.str("task-id")??process.env.ROVE_TASK_ID,tabId=ctx.args.str("tab")??process.env.ROVE_TAB_ID,detailRaw=ctx.args.str("detail"),detail;if(detailRaw!==void 0)try{detail=JSON.parse(detailRaw)}catch{throw new ApiError("--detail must be valid JSON","BAD_FLAG",helpStep("engine-report"))}if(!taskId&&ctx.caller.cwd===void 0)throw new ApiError("no target task: pass --task-id (this caller has no cwd to map)","MISSING_TARGET");return simpleRpc(ctx,"engine.reportEvent",{kind:ctx.args.str("kind"),...taskId?{taskId}:{cwd:ctx.caller.cwd},...ctx.args.str("engine")?{engine:ctx.args.str("engine")}:{},...tabId?{tabId}:{},...detail!==void 0?{detail}:{}})}},ROW_TOKEN_VERB,{name:"set-active",group:"drive",summary:"Set the shared active task (the focus every Tasks pane highlights). Pass --none to clear.",flags:[F.taskId(!1),{name:"none",type:"bool",description:"Clear the active task instead of setting one."}],handler:setActive}];async function collect(ctx){let daemon=daemonOf(ctx),{args,runtime}=ctx,idsFlag=args.str("task-ids"),repoFlag=args.path("repo"),groupFlag=args.str("group"),taskIds,unresolvableRepos=[];if(idsFlag)taskIds=idsFlag.split(",").map((s)=>s.trim()).filter(Boolean);else if(repoFlag||groupFlag){let{tasks}=await daemon.request("task.list"),filter=repoFlag?await repoFilter(runtime,repoFlag,tasks.map((t)=>t.repo)):null;unresolvableRepos=filter?.unresolvableRepos??[],taskIds=[];for(let t of tasks){if(groupFlag&&t.groupId!==groupFlag)continue;if(filter&&!filter.matches(t.repo))continue;taskIds.push(t.id)}}else throw new ApiError("collect needs --task-ids id1,id2, --group GROUPID, or --repo PATH","MISSING_TARGET");let registry=await readActivityRegistry(daemon),out=[];for(let taskId of taskIds){let{task}=await daemon.request("task.get",{taskId}),{tabs,running}=await runtime.taskTabs(taskId,taskEngineArgv(task),await ptyScopeOf(daemon,task)),repos=taskRepos(task),signals=await Promise.all(repos.map(async(r)=>({changes:r.worktreePath?await runtime.readWorktreeChanges(r.worktreePath):null,base:r.worktreePath?await runtime.readBranchSignals(r.worktreePath,r.baseRef):{baseRef:null,ahead:null,behind:null,diff:null}}))),{changes,base}=signals[0]??{changes:null,base:null},activity=await activityView(registry,task),runnability=await readRunnability(daemon,task.id);out.push({taskId:task.id,title:task.title,branch:task.branch,worktreePath:task.worktreePath,vendor:task.vendor,status:task.status,...task.groupId?{groupId:task.groupId}:{},...task.report?{report:task.report}:{},...task.dispatcher?{dispatcher:task.dispatcher}:{},running,activity,...runnability?{runnability}:{},tabs,changes,base,...task.extraRepos?.length?{repos:repos.map((r,i)=>({name:r.name,branch:r.branch,worktreePath:r.worktreePath,changes:signals[i]?.changes??null,base:signals[i]?.base??null}))}:{}})}return{tasks:out,...unresolvableRepos.length>0?{unresolvableRepos}:{}}}async function feedback(ctx){return{ok:!0,discussion:submitFeedback({title:ctx.args.require("title"),body:ctx.args.require("body"),categorySlug:ctx.args.str("category")})}}var FEEDBACK_VERBS=[{name:"feedback",group:"feedback",summary:"Create a GitHub Discussion in the Rove repo's Feedback category through `gh`.",flags:[{name:"title",type:"string",required:!0,placeholder:"T",description:"Discussion title."},{name:"body",type:"string",required:!0,placeholder:"TEXT",description:"Discussion body."},{name:"category",type:"string",default:DEFAULT_FEEDBACK_CATEGORY_SLUG,placeholder:"SLUG",description:"Discussion category slug."}],offline:!0,handler:feedback}];var GATE_VERBS=[{name:"gate-raise",group:"drive",summary:"Ask the OWNER a question only they can answer, instead of burying it in a MEMO or report. Opens a decision gate: it shows in their Inbox as `decision_needed` and in `gates`; their answer is pasted back into this tab as `[ROVE GATE g-N answered] \u2026`. Raising counts as ending your turn on a blocker \u2014 ask, then stop and wait for the answer. Returns { gate } with the `g-N` id.",flags:[F.taskId(!0),{name:"tab",type:"string",placeholder:"tab-N",description:"Tab the answer is pasted into. Default: the tab you are running in (when it is this task's), else the task's first engine tab."},{name:"question",type:"string",required:!0,placeholder:"TEXT",description:"The question, \u2264 500 chars. Make it answerable without opening the task."},{name:"option",type:"string",repeatable:!0,placeholder:"KEY=LABEL",description:"A choice the owner can pick, e.g. `--option a=ship-now --option b=wait`. Up to 6; omit for a free-text-only question."},{name:"recommend",type:"string",placeholder:"KEY",description:"The option key you recommend; shown marked in the owner's dialog."},{name:"context",type:"string",placeholder:"TEXT",description:"\u2264 4000 chars: why it matters and what each option costs."}],handler:gateRaise},{name:"gate-answer",group:"drive",summary:"Answer an open decision gate. The answer is saved first, then pasted into the gate's tab; with no live engine the gate is answered but `handoff: undelivered`, and the next `send` to that tab (or routine firing into it) carries it. A prompt typed into the TUI, or a TUI revive, does not. Pass --option (must be one of the gate's keys), --text, or both. Returns { gate, delivered }.",flags:[{name:"gate",type:"string",required:!0,placeholder:"g-N",description:"Gate id (from `gates`)."},{name:"option",type:"string",placeholder:"KEY",description:"The chosen option's key."},{name:"text",type:"string",placeholder:"TEXT",description:"Free-text answer, or a note to an option."}],handler:gateAnswer},{name:"gate-withdraw",group:"drive",summary:"Close a decision gate that no longer needs an answer (the question went moot), or an answer the engine never received and the owner no longer wants delivered. A delivered one cannot be withdrawn. Deleting a task closes its gates automatically.",flags:[{name:"gate",type:"string",required:!0,placeholder:"g-N",description:"Gate id."}],handler:gateWithdraw},{name:"gates",group:"read",summary:"List decision gates. Default: the open ones \u2014 questions waiting on the owner. `--status answered` shows answered gates, whose `.handoff` says whether the engine has the answer (`undelivered` = it rides the next `send` or routine firing into that tab).",flags:[F.repo(!1),F.taskId(!1),{name:"status",type:"enum",values:["open","answered","withdrawn","all"],default:"open",description:"Which gates to list."}],handler:gates}];var PROBE_MS=8000,VERSION_MS=4000;function parseVersion(tool,result){if(result.missing||result.code!==0)return null;return new RegExp(`${tool} version (\\S+)`).exec(result.stdout)?.[1]??null}function parseAuthStatus(stdout){let parsed;try{parsed=JSON.parse(stdout)}catch{return null}let hosts=parsed?.hosts;if(!hosts||typeof hosts!=="object")return null;let entries=hosts[GITHUB_HOST],list2=Array.isArray(entries)?entries:[],active=list2.find((e)=>e.active===!0)??list2[0];if(!active)return{account:null,protocol:null,scopes:[]};if(active.state!=="success"||typeof active.login!=="string")return{account:null,protocol:null,scopes:[],...typeof active.error==="string"?{detail:active.error.trim().slice(0,200)}:{}};let scopes=typeof active.scopes==="string"?active.scopes.split(",").map((s)=>s.trim()):[];return{account:active.login,protocol:active.gitProtocol==="ssh"||active.gitProtocol==="https"?active.gitProtocol:null,scopes:scopes.filter(Boolean)}}async function accountViaApi(run){let user=await run("gh",["api","user","-q",".login"],{timeoutMs:PROBE_MS,env:GH_ENV}),login=user.stdout.trim();if(user.code!==0||!login)return{account:null,protocol:null,scopes:[],...user.timedOut?{detail:"timed out"}:{}};let value=(await run("gh",["config","get","-h",GITHUB_HOST,"git_protocol"],{timeoutMs:VERSION_MS,env:GH_ENV})).stdout.trim();return{account:login,protocol:value==="ssh"||value==="https"?value:null,scopes:[]}}async function ghAccount(run){let res=await run("gh",["auth","status","--hostname",GITHUB_HOST,"--json","hosts"],{timeoutMs:PROBE_MS,env:GH_ENV});if(res.timedOut)return{account:null,protocol:null,scopes:[],detail:"timed out"};return parseAuthStatus(res.stdout)??await accountViaApi(run)}function parseGitUsesGh(configOutput){return configOutput.split(`
12
+ ${deliveryFooter(deliveryId,roveApiInvocation())}`);return await settle(promptMayHaveLanded(delivered)),{...delivered,deliveryId}}catch(err){throw await settle(!1),err}}async function persistPrompt(daemon,taskId,prompt){try{return await daemon.request("task.setPrompt",{taskId,prompt}),!0}catch(err){return console.error(`[rove api add] task.setPrompt failed for ${taskId} \u2014 no "Run again" for this task:`,err),!1}}async function engineChoice(ctx,repo){let command=ctx.args.str("command");if(command)return{command,vendor:resolveCommandProtocol(command)};let fallback=await ctx.runtime.defaultVendor(repo);return fallback?{command:fallback,vendor:fallback}:{}}function enginePayload(choice,effort,model,tier){return{...choice.command?{command:choice.command}:{},...choice.vendor?{vendor:choice.vendor}:{},...effort?{effort}:{},...model?{model}:{},...tier?{tier}:{}}}async function autoTier(prompt){if(!prompt?.trim())throw new ApiError("--tier auto classifies the first message \u2014 pass --prompt/--prompt-file, or name a tier explicitly","BAD_FLAG",helpStep("add"));let outcome=await classifyTier(prompt,readClassifierConfig());if(outcome.kind==="picked"){let{tier,confidence}=outcome.verdict;return{tier,note:`auto \u2192 ${tier} (confidence ${confidence.toFixed(2)})`}}let detail=outcome.detail?`: ${outcome.detail}`:"";return{note:`auto \u2192 no tier (${outcome.reason}${detail})`}}async function tierFields(ctx,prompt){let requested=ctx.args.str("tier")?.trim();if(!requested)return{};if(requested!=="auto"&&!isAutoRoutingTier(requested))throw new ApiError(`--tier must be one of swift, standard, deep, auto (got ${JSON.stringify(requested)})`,"BAD_FLAG",helpStep("add"));for(let flag of["command","model","effort","agents"])if(ctx.args.str(flag))throw new ApiError(`--${flag} conflicts with --tier, which already fills the engine, model and effort from the auto-routing table \u2014 pass one or the other`,"CONFLICTING_FLAGS",helpStep("add"));let table=readAutoRoutingTable();if(!table){if(requested==="auto")return{note:"auto \u2192 no tier (auto routing is not configured)"};throw new ApiError("auto routing is not configured \u2014 a tier has no engine (autoRouting.<tier>.engine in state.json); set it in Settings \u2192 Auto routing","TIER_UNAVAILABLE",helpStep("add"))}let tier,note2;if(requested==="auto"){let picked=await autoTier(prompt);if(note2=picked.note,!picked.tier)return{note:note2};tier=picked.tier}else tier=requested;let target=table[tier],status=await detectEngineStatus(target.engine),block=tierBlock(target,{engineIds:new Set(await engineListIds()),accountKind:()=>status.account?.kind??null});if(block){if(requested==="auto")return{note:`${note2} \u2014 unusable: ${describeTierBlock(block)}`};throw new ApiError(`tier ${tier} cannot start: ${describeTierBlock(block)}`,"TIER_UNAVAILABLE",{tier,block,hint:"Retarget the tier in Settings \u2192 Auto routing, or pass --command/--model/--effort by hand.",nextCommandArgs:["api","engine-list"]})}let vendor=resolveCommandProtocol(target.engine);try{if(target.model)assertEngineAcceptsModel(vendor,target.model,["api","engine-list"]);if(target.effort)assertEngineAcceptsEffort(vendor,target.effort,["api","engine-list"])}catch(err){if(requested!=="auto")throw err;return{note:`${note2} \u2014 unusable: ${err instanceof Error?err.message:String(err)}`}}return{fields:{choice:{command:target.engine,vendor},effort:target.effort,model:target.model,tier},...note2?{note:note2}:{}}}async function withTierNote(note2,body){if(Object.keys(note2).length===0)return body();try{return await body()}catch(err){if(!(err instanceof ApiError))throw err;throw new ApiError(err.message,err.code,{...err.data,...note2})}}function modelFor(ctx,engines){let model=ctx.args.str("model")?.trim();if(!model)return;for(let engine of new Set(engines))assertEngineAcceptsModel(engine,model,["api","engine-list"]);return model}function effortFor(ctx,engines){let level=ctx.args.str("effort")?.trim();if(!level)return;for(let engine of new Set(engines))assertEngineAcceptsEffort(engine,level,["api","engine-list"]);return level}function foreignModelWarnings(engines,model){if(!model)return[];return[...new Set(engines)].filter((engine)=>protocolEntry(engine).foreignModel?.(model)).map((engine)=>`engine ${engine} is unlikely to run model ${JSON.stringify(model)} \u2014 that spelling belongs to another vendor; \`engine-list\` shows ${engine}'s models`)}function launchedEngine(task){return{vendor:task.vendor??null,command:task.command??null,model:task.model??null,effort:task.modelEffort??null}}function parentFlags(ctx){let{args}=ctx,parent=args.str("parent"),noParent=args.bool("no-parent")===!0,noWorktree=args.bool("no-worktree")===!0,isolateIn=args.str("isolate-in"),has=(flag)=>args.str(flag)!==void 0||args.bool(flag)===!0,conflict=(a,b)=>{throw new ApiError(`--${a} and --${b} conflict`,"CONFLICTING_FLAGS",helpStep("add"))};if(parent&&noParent)conflict("parent","no-parent");if(noWorktree&&isolateIn)conflict("no-worktree","isolate-in");for(let[flag,on]of[["no-worktree",noWorktree],["isolate-in",isolateIn!==void 0]]){if(!on)continue;if(noParent)conflict(flag,"no-parent");for(let other of["repo","fork","base-for","workspace"])if(has(other))conflict(flag,other)}if(noWorktree){for(let other of["base-branch","branch","worktree-name","count","agents"])if(has(other))conflict("no-worktree",other)}return{parent,noParent,noWorktree,isolateIn,takesParentRepo:noWorktree||isolateIn!==void 0}}async function resolveAddParent(ctx,daemon,flags,repo){if(flags.parent)return await getParent(daemon,flags.parent);if(flags.noParent)return null;if(!ctx.caller.cli||ctx.caller.forwarded)return requireOrNull(flags);let self=await verifiedSelfSession();if(!self)return requireOrNull(flags);let{tasks}=await daemon.request("task.list",{}).catch(()=>({tasks:[]})),caller=tasks.find((task)=>task.id===resolvePtyOwner(tasks,self.taskId,self.tabId));if(!caller)return requireOrNull(flags);if(flags.takesParentRepo)return caller;let root=tasks.find((task)=>task.id===lineageRootId(tasks,caller.id));if(!root||root.routine||isRemoteRepoKey(root.repo)||repo===void 0)return null;return samePath(await ctx.runtime.resolveRepoRoot(root.repo),repo)?caller:null}function requireOrNull(flags){if(!flags.takesParentRepo)return null;let flag=flags.noWorktree?"--no-worktree":"--isolate-in";throw new ApiError(`${flag} needs a parent and the caller is not a verified rove session \u2014 pass --parent ID`,"MISSING_TARGET",helpStep("add"))}async function getParent(daemon,taskId){try{return(await daemon.request("task.get",{taskId})).task}catch{throw new ApiError(`no task ${taskId} on this machine \u2014 a parent must live beside its child`,"PARENT_NOT_FOUND",{hint:"list the ids with `rove api list`",nextCommandArgs:["api","list"]})}}async function placeAdd(ctx,parent,flags,create){if(!parent)return{kind:"top"};let parentId=parent.id;if(flags.noWorktree)return{kind:"shared",parentId};if(flags.isolateIn)return{kind:"cut",parentId,isolateIn:flags.isolateIn};return!create.fork&&!create.ownWorkspace&&!create.baseRef&&!create.multiRepo&&!isRemoteRepoKey(parent.repo)&&samePath(await ctx.runtime.resolveRepoRoot(parent.repo),create.repo)?{kind:"cut",parentId}:{kind:"nested",parentId}}function placedRequest(placement,create){if(placement.kind==="top")return{method:"task.create",payload:create};if(placement.kind==="nested")return{method:"task.create",payload:{...create,parentTaskId:placement.parentId}};let{repo:_repo,extraRepos:_extra,forkFrom:_fork,...rest}=create,payload={...rest,parentTaskId:placement.parentId};if(placement.kind==="shared")return{method:"task.spawn",payload:{...payload,worktree:!1}};return{method:"task.spawn",payload:{...payload,worktree:!0,lazy:!0,...placement.isolateIn?{isolateIn:placement.isolateIn}:{}}}}async function addParallel(ctx,repo,repoSet,placement,count,agentsSpec){let daemon=daemonOf(ctx),{args}=ctx,prompt=args.promptText();if(!prompt)throw new ApiError("--count/--agents spawn parallel attempts of ONE prompt \u2014 pass --prompt","MISSING_FLAG",helpStep("add"));if(args.str("branch"))throw new ApiError("--branch names ONE branch and cannot be shared by parallel siblings \u2014 drop it (each sibling gets its own auto branch) or spawn them one at a time","BAD_FLAG",helpStep("add"));if(args.str("worktree-name"))throw new ApiError("--worktree-name names ONE directory and cannot be shared by parallel siblings \u2014 drop it (each sibling gets its own generated name) or spawn them one at a time","BAD_FLAG",helpStep("add"));if(agentsSpec){let conflict=count!==void 0?"--count":args.str("command")?"--command":null;if(conflict)throw new ApiError(`${conflict} conflicts with --agents, which already names each sibling's engine and how many \u2014 pass one or the other`,"BAD_FLAG",helpStep("add"))}let title=args.str("title")||deriveTitleFromPrompt(prompt),baseRef=args.str("base-branch")??repoSet.primaryBaseRef,picked=await tierFields(ctx,prompt),tier=picked.fields,choice=tier?.choice??await engineChoice(ctx,repo),plan=agentsSpec?parseAgentsSpec(agentsSpec):buildCountPlan(count??1,choice.vendor??DEFAULT_VENDOR);if(plan.length>FANOUT_CAP)throw new ApiError(`a parallel round of ${plan.length} exceeds the cap of ${FANOUT_CAP} \u2014 spawn in batches`,"BAD_FLAG");let effort=tier?tier.effort:effortFor(ctx,plan),model=tier?tier.model:modelFor(ctx,plan),warnings=foreignModelWarnings(plan,model),groupId=ulid(),created=[],createFailure=null,dispatcher=await dispatcherEnvPayload(daemon);for(let[i,vendor]of plan.entries()){let engine=agentsSpec?{command:vendor,vendor}:{...choice,vendor},payload={repo,groupId,...dispatcher,...enginePayload(engine,effort,model,tier?.tier)};if(title)payload.title=plan.length>1?`${title} #${i+1}/${plan.length}`:title;if(baseRef)payload.baseRef=baseRef;if(repoSet.extraRepos.length>0)payload.extraRepos=repoSet.extraRepos;try{let request=placedRequest(placement,payload),res=await daemon.request(request.method,request.payload);created.push({taskId:res.taskId,vendor,task:res.task})}catch(err){let code=err instanceof ApiError?err.code:"CREATE_FAILED",message=err instanceof Error?err.message:String(err);createFailure={vendor,error:{message,code}};break}}for(let{taskId}of created)await applyPostCreateFlags(daemon,taskId,args);let settled=await Promise.allSettled(created.map(({taskId,vendor,task})=>deliverArmed(ctx,daemon,taskId,{id:taskId,worktreePath:taskCwd(task),kind:task.kind,vendor,command:task.command,modelEffort:task.modelEffort,model:task.model,repo:task.repo,...task.extraRepos?.length?{multiRepo:!0}:{},newTask:!0},prompt))),tasks=[],failures=[],persistedPrompts=[];if(settled.forEach((r,i)=>{let{taskId,vendor,task}=created[i];if(r.status==="fulfilled"&&r.value.delivered){let row={ok:!0,taskId,title:task.title,branch:task.branch,vendor,engine:launchedEngine(task),started:r.value.started,engineReady:r.value.engineReady,session:r.value.session,...r.value.reason?{reason:r.value.reason}:{}};tasks.push(row),persistedPrompts.push(persistPrompt(daemon,taskId,prompt).then((ok)=>{if(!ok)row.promptPersisted=!1}));return}let err=r.status==="rejected"?r.reason:new ApiError(`prompt was not confirmed in ${taskId}'s engine`,"NOT_DELIVERED"),code=err instanceof ApiError?err.code:"DELIVER_FAILED",message=err instanceof Error?err.message:String(err);failures.push({ok:!1,taskId,vendor,engine:launchedEngine(task),error:{message,code}})}),createFailure)failures.push({ok:!1,vendor:createFailure.vendor,error:createFailure.error});await Promise.all(persistedPrompts);let result={count:created.length,requested:plan.length,groupId,home:homeDir(),tasks,failures,...picked.note?{tierAuto:picked.note}:{},...warnings.length>0?{warnings}:{}};if(failures.length>0)throw new ApiError(`add delivered ${tasks.length}/${plan.length}`,"PARTIAL_FANOUT",result);return result}async function localCheckouts(ctx){let{tasks}=await daemonOf(ctx).request("task.list");return[...new Set([...getSavedRepos(),...tasks.map((t)=>t.repo)])].flatMap((path2)=>{let identity=repoIdentity(path2);return identity?[{path:path2,identity}]:[]})}async function resolveLocalRepo(ctx,want,allowFork=!1){let candidates=await localCheckouts(ctx),matches=findCheckouts(want,candidates),id=want.origin??(want.root?`root:${want.root}`:""),picked=pickCheckout(matches,{primary:primaryCheckouts()[id],allowFork,isClean:isCleanCheckout});return{repo:picked.path,...picked.fork?{fork:picked.fork}:{},checkouts:matches.map((m)=>m.path),searched:candidates.length}}var REPO_MATCH_VERB={name:"repo-match",group:"discover",summary:"Find this machine's checkout of a repo by identity: `--root` (its first commit) or `--origin` (normalized remote URL, `host/owner/name`). Several checkouts: the primary (`rove machine primary`) wins, else a clean one. A checkout with the same root but another origin is a fork and only matches with --allow-fork. Searches the saved repos and every task's repo. Returns { repo } (null when none matches), { fork } (a fork that was skipped), { checkouts } and { searched }.",flags:[{name:"origin",type:"string",placeholder:"URL",description:"Remote URL; any git form, compared normalized."},{name:"root",type:"string",placeholder:"SHA",description:"The repo's root commit."},{name:"allow-fork",type:"bool",description:"Accept a fork (same root commit, other origin)."}],handler:async(ctx)=>{let origin=ctx.args.str("origin"),root=ctx.args.str("root");if(!origin&&!root)throw new ApiError("pass --origin or --root","BAD_FLAG");return resolveLocalRepo(ctx,{...origin?{origin:normalizeRemoteUrl(origin)}:{},...root?{root}:{}},ctx.args.bool("allow-fork")===!0)}},REPO_LIST_VERB={name:"repo-list",group:"discover",summary:"This machine's repos by id (`host/owner/name`, or `root:<sha>` with no origin): each with its checkouts and which one is primary. Read from the saved repos and every task's repo. Returns { repos: [{ id, primary, checkouts }] }.",flags:[],handler:async(ctx)=>{let primaries=primaryCheckouts(),byId=new Map;for(let c of await localCheckouts(ctx)){let id=repoIdOf(c.identity);if(!id)continue;byId.set(id,[...byId.get(id)??[],c.path])}return{repos:[...byId].map(([id,checkouts])=>({id,primary:primaries[id]??null,checkouts})).sort((a,b)=>a.id.localeCompare(b.id))}}};async function resolveRepoFlag(ctx,raw){let ref=parseRepoRef(raw,ctx.caller.cwd??process.cwd());if(ref.kind==="path")return ctx.args.repoPath("repo",raw);let found=await resolveLocalRepo(ctx,{origin:ref.id});if(found.repo)return found.repo;if(ctx.args.bool("clone")!==!0)throw new ApiError(`no checkout of ${ref.id} on this machine`,"NO_LOCAL_CHECKOUT",{hint:`pass --clone to clone it into ${cloneDirFor(ref.id)}, or --repo with a checkout's path`});try{let dir=cloneRepo(ref);return addSavedRepo(dir),dir}catch(err){throw new ApiError(`could not clone ${ref.cloneUrl}: ${errorMessage(err)}`,"CLONE_FAILED",{hint:"check this machine can fetch it (`git ls-remote <url>`), or clone it yourself and pass its path"})}}async function resolveRepoSet(ctx,primary,extraRefs){let{args,runtime}=ctx,baseFor=args.list("base-for");if(extraRefs.length===0){if(baseFor.length>0)throw new ApiError("--base-for names a repo of a multi-repo task \u2014 pass --repo more than once","BAD_FLAG",helpStep("add"));return{extraRepos:[]}}if(1+extraRefs.length>MULTI_REPO_CAP)throw new ApiError(`${1+extraRefs.length} repos exceed the cap of ${MULTI_REPO_CAP} per task (the first --repo is the primary)`,"TOO_MANY_REPOS",helpStep("add"));if(isRemoteRepoKey(primary))throw new ApiError(`--repo ${primary} is a remote project; a multi-repo task must keep every repo local`,"REMOTE_EXTRA_UNSUPPORTED");let names=new Set([repoNameOf(primary)]),repos=[{name:repoNameOf(primary),repo:primary}];for(let raw of extraRefs){let requested=await resolveRepoFlag(ctx,raw);if(isRemoteRepoKey(requested))throw new ApiError(`--repo ${raw} is a remote project; a multi-repo task must keep every repo local`,"REMOTE_EXTRA_UNSUPPORTED",helpStep("add"));let root=await runtime.resolveRepoRoot(requested);if(!await runtime.isUsableRepo(root))throw new ApiError(`--repo ${raw} is not a git repository \u2014 every repo of a task needs one to cut its worktree and branch from`,"NOT_A_GIT_REPO",helpStep("add"));if(repos.some((r)=>samePath(r.repo,root)))throw new ApiError(`--repo ${raw} resolves to ${root}, already part of this task`,"REPO_DUPLICATE",helpStep("add"));let name=uniqueRepoName(repoNameOf(root),names);names.add(name),repos.push({name,repo:root})}let baseRefs=new Map;for(let spec of baseFor){let eq=spec.indexOf("="),key=spec.slice(0,eq),ref=spec.slice(eq+1);if(eq<=0||!ref)throw new ApiError(`--base-for ${JSON.stringify(spec)} must be REPO=REF`,"BAD_FLAG",helpStep("add"));let keyRoot=key.includes("/")?await runtime.resolveRepoRoot(args.repoPath("base-for",key)):void 0,target;for(let r of repos)if(r.name===key||keyRoot!==void 0&&samePath(await runtime.resolveRepoRoot(r.repo),keyRoot)){target=r;break}if(!target)throw new ApiError(`--base-for ${key}: no such repo in this task (repos: ${repos.map((r)=>r.name).join(", ")})`,"BAD_FLAG",helpStep("add"));if(baseRefs.has(target.name))throw new ApiError(`--base-for ${key} given twice`,"BAD_FLAG",helpStep("add"));baseRefs.set(target.name,ref)}let primaryBaseRef=baseRefs.get(repos[0]?.name??""),explicit=args.str("base-branch");if(primaryBaseRef&&explicit&&primaryBaseRef!==explicit)throw new ApiError(`--base-branch ${explicit} and --base-for ${repos[0]?.name}=${primaryBaseRef} both set the primary's base \u2014 pass one`,"BAD_FLAG",helpStep("add"));return{extraRepos:repos.slice(1).map((r)=>{let baseRef=baseRefs.get(r.name);return{repo:r.repo,...baseRef?{baseRef}:{}}}),...primaryBaseRef?{primaryBaseRef}:{}}}async function add(ctx){let{args,runtime}=ctx,daemon=daemonOf(ctx),flags=parentFlags(ctx),refs=args.list("repo"),fork=await resolveFork(ctx),inherited=flags.takesParentRepo?await resolveAddParent(ctx,daemon,flags,void 0):null,requestedRepo=inherited?inherited.repo:fork?fork.repo:await resolveRepoFlag(ctx,refs[0]??""),repo=inherited?inherited.repo:await runtime.resolveRepoRoot(requestedRepo);if(!inherited&&!await runtime.isUsableRepo(repo))throw new ApiError(`--repo ${repo} is not a git repository \u2014 a task needs one to cut its worktree and branch from (run \`git init\` there, or point --repo at a checkout)`,"NOT_A_REPO",helpStep("add"));let count=args.int("count"),agentsSpec=args.str("agents"),parallel=count!==void 0||agentsSpec!==void 0;if(args.bool("start")&&(parallel||args.promptText()))throw new ApiError("--start boots one idle engine without a first message: it conflicts with --prompt/--prompt-file (which already start the engine) and with --count/--agents","CONFLICTING_FLAGS",helpStep("add"));let branch=parallel?void 0:args.str("branch");if(branch&&!await runtime.isValidBranchName(branch))throw new ApiError(`--branch ${JSON.stringify(branch)} is not a valid git branch name (no spaces, no leading "-", no "..", "~^:?*[\\", no trailing ".lock") \u2014 see \`git check-ref-format --branch\``,"INVALID_BRANCH",helpStep("add"));let resolvedFrom=pathWithin(repo,requestedRepo)?{repoResolvedFrom:requestedRepo}:void 0,repoSet=inherited?{extraRepos:[]}:await resolveRepoSet(ctx,repo,refs.slice(1)),workspace=args.str("workspace");if(workspace&&(parallel||fork||repoSet.extraRepos.length>0))throw new ApiError("--workspace takes one task in one repo: it conflicts with --fork, --count, --agents, --no-worktree, --isolate-in and extra repos","CONFLICTING_FLAGS",helpStep("add"));let parent=inherited??await resolveAddParent(ctx,daemon,flags,repo),placement=await placeAdd(ctx,parent,flags,{repo,fork:fork!==void 0,baseRef:args.str("base-branch")??repoSet.primaryBaseRef,multiRepo:repoSet.extraRepos.length>0,ownWorkspace:workspace!==void 0}),result=parallel?await addParallel(ctx,repo,repoSet,placement,count,agentsSpec):await addOne(ctx,repo,repoSet,placement);return resolvedFrom&&result&&typeof result==="object"?{...result,...resolvedFrom}:result}var FORK_EXCLUSIVE=["repo","base-branch","base-for","count","agents"];async function resolveFork(ctx){let taskId=ctx.args.str("fork");if(!taskId)return;let clash=FORK_EXCLUSIVE.find((flag)=>ctx.args.str(flag)!==void 0);if(clash)throw new ApiError(`--fork and --${clash} conflict \u2014 a fork is a sibling in the parent's repo, starting at the parent's HEAD`,"CONFLICTING_FLAGS",helpStep("add"));let{task}=await daemonOf(ctx).request("task.get",{taskId});return{taskId,repo:task.repo}}async function typedEngineFields(ctx,repo){let choice=await engineChoice(ctx,repo),engines=choice.vendor?[choice.vendor]:[];return{choice,effort:effortFor(ctx,engines),model:modelFor(ctx,engines)}}function deliveryTarget(task,newTask=!1){return{id:task.id,worktreePath:taskCwd(task),kind:task.kind,vendor:task.vendor,command:task.command,modelEffort:task.modelEffort,model:task.model,repo:task.repo,...task.extraRepos?.length||task.sharesWorkspaceOf?{multiRepo:!0}:{},...newTask?{newTask:!0}:{}}}async function addOne(ctx,repo,repoSet,placement){let daemon=daemonOf(ctx),{args}=ctx,prompt=args.promptText(),picked=await tierFields(ctx,prompt),fields=picked.fields??await typedEngineFields(ctx,repo),tierNote=picked.note?{tierAuto:picked.note}:{},warnings=foreignModelWarnings([fields.choice.vendor??DEFAULT_VENDOR],fields.model),warned=warnings.length>0?{warnings}:{},payload={repo,...await dispatcherEnvPayload(daemon),...enginePayload(fields.choice,fields.effort,fields.model,fields.tier)},fork=args.str("fork"),title=args.str("title")||(prompt&&!fork?deriveTitleFromPrompt(prompt):"");if(title)payload.title=title;let branch=args.str("branch");if(branch)payload.branch=branch;let baseRef=args.str("base-branch")??repoSet.primaryBaseRef;if(baseRef)payload.baseRef=baseRef;if(fork)payload.forkFrom=fork;let workspace=args.str("workspace");if(workspace)payload.workspace=workspace;if(repoSet.extraRepos.length>0)payload.extraRepos=repoSet.extraRepos;let worktreeName=args.str("worktree-name");if(worktreeName)payload.worktreeName=worktreeName;let request=placedRequest(placement,payload),res=await daemon.request(request.method,request.payload),placed=res.parentDirty===void 0?{}:{parentDirty:res.parentDirty,sharedLinks:res.sharedLinks??[]},taskId=res.taskId;if(args.bool("activate"))await daemon.request("task.setActive",{taskId});let task=res.task;if(await applyPostCreateFlags(daemon,taskId,args))task=(await daemon.request("task.get",{taskId})).task;if(!prompt){if(!args.bool("start"))return{taskId,task,engine:launchedEngine(task),home:homeDir(),...placed,started:!1,...warned};try{let opened=await ctx.runtime.openTab(daemon,deliveryTarget(task),"engine");return{taskId,task,engine:launchedEngine(task),home:homeDir(),...placed,started:!0,tabId:opened.tab.id,session:opened.session,...warned}}catch(err){if(!(err instanceof ApiError))throw err;throw new ApiError(err.message,err.code,{...err.data,taskId,...placed,...warned})}}let brief=await withPeerProvenance(daemon,taskId,prompt),delivered=await withTierNote(tierNote,()=>deliverArmed(ctx,daemon,taskId,deliveryTarget(task,!0),brief));if(!delivered.delivered)throw new ApiError(`task ${taskId} created but the prompt was not delivered (paste did not land)`,"NOT_DELIVERED",{taskId,engine:launchedEngine(task),...placed,...tierNote,...warned});let promptPersisted=await persistPrompt(daemon,taskId,prompt);return task=(await daemon.request("task.get",{taskId})).task,{taskId,task,engine:launchedEngine(task),home:homeDir(),...placed,started:delivered.started,...delivered.deliveryId?{deliveryId:delivered.deliveryId}:{},engineReady:delivered.engineReady,session:delivered.session,delivered:delivered.delivered,...delivered.bytes===void 0?{}:{bytes:delivered.bytes},...delivered.promptEcho?{promptEcho:delivered.promptEcho}:{},...delivered.reason?{reason:delivered.reason}:{},...promptPersisted?{}:{promptPersisted:!1},...tierNote,...warned}}var CREATE_VERBS=[{name:"add",group:"create",summary:`Create a task (shows in the sidebar immediately). With --prompt it also starts the engine and delivers it; with --start and no prompt the engine boots idle in the task's own tab-1 (reply started:true, tabId, session), where a bare add only creates the row (started:false). PARALLEL ATTEMPTS: --count N spawns N sibling tasks of the SAME prompt, each in its own worktree/branch (--agents claude:2,codex:1 for a mixed fleet); capped at ${FANOUT_CAP}, prefer 3-4. NESTING (children never nest): called from a verified rove session ($ROVE_TASK_ID/$ROVE_TAB_ID that the pty host confirms), the new task becomes a CHILD of the caller's top-level task and shows under it in the sidebar \u2014 but only when its repo is that task's own repo and that task is not a routine session; otherwise (another repo, --machine, an unverifiable env, a daemon or routine caller) it stays top level, never an error. --parent ID nests under that task instead, in any repo; --no-parent opts out; --count/--agents siblings all get the parent; \`dispatcher\` still records the exact calling task and tab. A child in the parent's repo cuts its worktree from the parent's CURRENT branch tip (a \`dir\` task inside a git repo gets a worktree at the repo root and runs in the same relative subdirectory) instead of the repo default \u2014 unless --base-branch, --fork or several --repo name the fork point. \`parentDirty:true\` in the reply means the parent has uncommitted changes the new worktree does not carry; \`sharedLinks\` lists the parent workspace's top-level symlinks to OTHER git checkouts, which a new worktree does not isolate (--isolate-in LINK cuts the worktree in the linked repo instead). --no-worktree shares the parent's directory instead of cutting a worktree (read-only parallel work: two engines writing one tree will collide); it and --isolate-in need a parent (MISSING_TARGET without one). The child's bare \`send\` replies to the session that added it; \`.task.parentTaskId\` (and \`.task.sharesWorkspaceOf\` for a shared child) say where it landed. Does NOT steal focus \u2014 pass --activate to make it the active task. The sidebar shows .task.title and .task.branch \u2014 name a task by those (or its id), never by the directory in .task.worktreePath, which the UI never renders. \`.engine\` ({ vendor, command, model, effort }; per row with --count/--agents) is what the task actually launches once the flags and the repo default are resolved \u2014 check it before trusting the run. \`.warnings\` flags a --model that plainly belongs to another engine's vendor (e.g. codex given a claude-* id); the task is still created. Alias: spawn-task.`,flags:[{name:"repo",type:"string",required:!0,repeatable:!0,placeholder:"PATH|REPO",description:"Repo root (git toplevel; relative paths resolve against $PWD), or a repo id: `owner/name` (GitHub), `host/owner/name`, or a remote URL. An id resolves to this machine's checkout of it (the primary one, else a clean one) \u2014 or, with --machine, to that machine's. REPEAT it for a multi-repo task (max 8, all local): the FIRST is the primary \u2014 the engine's cwd, the sidebar project, `.task.repo/.branch/.worktreePath` \u2014 and each further one gets its own worktree on the SAME branch name (suffixed `-2` only in a repo where the name is taken), listed in `.task.extraRepos[]` ({ name, repo, branch, worktreePath, baseRef }; paths fill in once the worktrees exist). The engine sees the extras through its add-dir flag where it has one, `$ROVE_TASK_REPOS` / `$ROVE_REPO_<NAME>` env, and a note on the first prompt. `--count` gives every sibling the same repo set. Errors: TOO_MANY_REPOS, REPO_DUPLICATE, NOT_A_GIT_REPO (names the repo), REMOTE_EXTRA_UNSUPPORTED."},{name:"clone",type:"bool",description:"With a repo id this machine (or --machine's) has no checkout of: clone it into ~/.rove/repos/<host>/<owner>/<name> first."},{name:"fork",type:"string",placeholder:"TASK_ID",description:"Fork that task: the new task is a sibling in the SAME repo whose worktree is a copy-on-write clone of the parent's, so it starts with the parent's uncommitted edits, staged changes, untracked files and gitignored dirs (node_modules, build output), on a new branch rooted at the parent's HEAD. The clone is made during this call and is instant on APFS (macOS) or a reflink filesystem (btrfs, XFS); it costs no disk until either side writes. Replaces --repo (taken from the parent; --repo, --base-branch, --base-for, --count and --agents conflict with it: CONFLICTING_FLAGS). Title defaults to '<parent title> (fork)'. Refused with FORK_UNSUPPORTED \u2014 nothing created \u2014 when the parent is a main or directory task, is multi-repo or remote, or its worktree sits on a volume that cannot clone files; the parent is never modified. Scratch state the parent keeps in its git directory (a half-done merge or rebase) does not come along."},{name:"workspace",type:"enum",values:["layerfs"],description:"EXPERIMENTAL, Linux only: put the task's files in a LayerFS Workspace inside a per-task Docker sandbox instead of a git worktree (`experimental.layerfs` in state.json names its env file; Store calls need passwordless sudo). Terminals run inside the sandbox at the task's path; checkpoint/rewind use LayerFS commits; `export-branch` brings its git branch back for landing. Refused with LAYERFS_UNAVAILABLE / LAYERFS_NEEDS_ROOT \u2014 nothing created \u2014 when it cannot run here. Conflicts with --fork, --count, --agents, --no-worktree, --isolate-in and extra repos (CONFLICTING_FLAGS); inside a session it still nests under the caller, as its own layerfs task rather than a worktree cut from the parent."},{name:"allow-fork",type:"bool",description:"With --machine: accept a checkout there that shares the repo's history under another origin (a fork)."},{name:"parent",type:"string",placeholder:"ID",description:"Nest the new task under this task (stored under its top-level ancestor), whatever its repo \u2014 the rules of `update --parent`. Default: the verified calling session, when the repo is its own; see the summary. PARENT_NOT_FOUND if no such task."},{name:"no-parent",type:"bool",description:"Create a top-level task even from inside a rove session. Conflicts with --parent, --no-worktree and --isolate-in (CONFLICTING_FLAGS)."},{name:"no-worktree",type:"bool",description:"Share the parent's directory instead of cutting a worktree. The parent is --parent or the calling session; the child takes its repo, so --repo, --fork, --base-branch, --base-for, --branch, --worktree-name, --count, --agents and --isolate-in conflict with it."},{name:"isolate-in",type:"string",placeholder:"LINK",description:"Name of a top-level symlink in the parent's workspace that points at another git checkout (see `sharedLinks`): cut the worktree in THAT repo and run the child there. NOT_A_LINKED_REPO when it is not one. Needs a parent; --repo, --fork, --base-for and --no-worktree conflict with it."},F.title(),{name:"branch",type:"string",placeholder:"B",description:"Explicit branch name (else derived from the title in the repo's own style). Single task only."},{name:"base-branch",type:"string",placeholder:"B",description:"Base ref the worktree branches from."},{name:"base-for",type:"string",repeatable:!0,placeholder:"REPO=REF",description:"Per-repo base for a multi-repo task: REPO is the repo's name (its directory name, `-2`-suffixed on a clash; `.task.extraRepos[].name`) or its path, REF the ref its worktree branches from. Repeatable. --base-branch stays the primary's base; naming the primary here too is an error unless they agree."},{name:"worktree-name",type:"string",placeholder:"NAME",description:"Name the worktree DIRECTORY instead of taking one from the animal pool \u2014 so the caller can predict `.task.worktreePath` (`<worktrees root>/<NAME>`) rather than reading it back with get-task. One path segment of letters/digits/`.`/`_`/`-`, not starting with `.` (INVALID_WORKTREE_NAME otherwise). A name already in use in this repo \u2014 by a live task, a directory on disk, or a concurrent create \u2014 is REFUSED with WORKTREE_NAME_TAKEN, never silently suffixed `-v2`. Single task only."},F.command(),{name:"effort",type:"string",placeholder:"LEVEL",description:"Reasoning effort for the FIRST session, not just later ones. Validated against the engine's declared levels (codex: none/low/medium/high/xhigh/max; pi and OMP: off/minimal/low/medium/high/xhigh/max; claude declares none) \u2014 free-form, since a plugin engine may declare its own. With --agents, every engine in the plan must declare it."},{name:"tier",type:"enum",values:["swift","standard","deep","auto"],description:"Auto-routing tier: fills the engine, model and effort from the table in Settings \u2192 Auto routing (autoRouting.<tier>.* in state.json) and records the tier on the task (.task.tier). Exclusive with --command/--model/--effort/--agents (CONFLICTING_FLAGS). A tier whose target cannot start \u2014 engine not in engine-list, not logged in, model or effort its engine cannot carry \u2014 is refused up front (TIER_UNAVAILABLE) with the reason. `auto` asks the classifier named by `autoRouting.classifier` (OFF by default \u2014 nothing is sent anywhere until you set it, and with `jev` the prompt goes to TypeSafe, a third party that is not your engine vendor) to read --prompt and pick the tier; needs --prompt/--prompt-file, and NEVER fails the create \u2014 off, no key, a timeout, or an answer below `autoRouting.classifierThreshold` all create the task with the ordinary defaults and say so in `.tierAuto`."},{name:"model",type:"string",placeholder:"MODEL",description:"Model for the FIRST session and every rebuild, in the engine's own spelling (claude alias/full id, codex slug, pi/omp pattern or provider/id) \u2014 passed verbatim, so `engine-list`'s `models` are suggestions, not a closed list. Rejected (BAD_MODEL) on an engine that declares no model flag; with --agents, every engine in the plan must."},{name:"count",type:"int",placeholder:"N",description:`Spawn N sibling tasks of one prompt (parallel attempts, cap ${FANOUT_CAP}). Requires --prompt.`},{name:"agents",type:"string",placeholder:"claude:2,codex:1",description:"Per-ENGINE counts for a mixed parallel round (alternative to --count). Engine ids only \u2014 see `engine-list`."},{name:"status",type:"enum",values:TASK_STATUSES,default:"backlog",description:"Initial lifecycle status."},{name:"pin",type:"bool",description:"Pin the task to the top of the sidebar."},{name:"require-report",type:"bool",description:"Hold every `send` to this task, and its first --prompt, to a report: each arms a delivery the agent closes with `report --delivery <id>`, and a turn that ends without one raises a `report_missing` Inbox row. For tasks ticked unattended. Change later with `update --require-report on|off`."},{name:"expect-report",type:"bool",description:"Arm a delivery for this task's first --prompt only (as `send --expect-report` does for a send); later sends are not armed."},{name:"activate",type:"bool",default:"false",description:"Make this the active task (pulls every mounted TUI's Tasks-pane focus). Off by default."},{name:"start",type:"bool",description:"With no --prompt: also start the task's engine idle in its own tab-1 (materializes the worktree) instead of leaving a backlog row. Replies `started: true`, `tabId`, `session`; a refusal (ENGINE_LIMIT, ...) keeps the task and names it in `taskId`. One task only: CONFLICTING_FLAGS with --prompt/--prompt-file, --count and --agents."},F.prompt(!1,"Optional first message \u2014 when set, materializes the worktree, starts the engine, and pastes it. Required with --count/--agents."),F.promptFile()],handler:add}];async function readStdin(){let chunks=[];for await(let chunk of process.stdin)chunks.push(Buffer.from(chunk));return Buffer.concat(chunks)}var PANE_GRAPHICS_VERB={name:"pane-graphics",group:"drive",cliOnly:!0,summary:"Write opaque graphics bytes (read from stdin) to the terminal of every TUI attached to this task \u2014 the transport a pane needs because its own tty is a PTY slave, not the emulator, so it cannot reach the real terminal or measure it. Rove parses nothing: the bytes are your protocol. Call it ONCE with nothing on stdin to be given `imageId` (allocated per tab, so two panes never collide) plus `cellWidth`/`cellHeight` in pixels; build your payload around those, then pipe each frame in with --image-id to replace that picture in place. `wrote` says whether anything was broadcast. Returns `ok: false` with `unsupported` when no attached terminal reported a cell size (`no-cell-size`) or two reported different ones (`mixed-cell-size`) \u2014 fall back to whatever you draw without graphics. Task defaults to $ROVE_TASK_ID, then the active task.",flags:[F.taskId(!1),{name:"tab",type:"string",required:!0,placeholder:"TAB",description:"Terminal Tab whose cells the picture is for (e.g. tab-3), from get-task .tabs[].id."},{name:"image-id",type:"int",placeholder:"N",description:"Reuse an id this tab was already given, replacing that picture in place. Omit to be handed a fresh one."}],handler:async(ctx)=>{let client=daemonOf(ctx),taskId=ctx.args.str("task-id")??process.env.ROVE_TASK_ID??await resolveActiveTaskId(client);if(!taskId)throw new ApiError("no target task: pass --task-id (no $ROVE_TASK_ID, no active task)","MISSING_TARGET");let data=process.stdin.isTTY?Buffer.alloc(0):await readStdin(),imageId=ctx.args.int("image-id");return simpleRpc(ctx,"graphics.write",{taskId,tabId:ctx.args.require("tab"),...data.length>0?{data:data.toString("base64")}:{},...imageId!==void 0?{imageId}:{}})}};function interruptSequenceFor(task){let vendor=task.vendor,sequence=vendor?getCapabilities(vendor)?.interruptSequence:void 0;if(sequence)return sequence;throw new ApiError(`engine ${vendor??"(unknown)"} has not declared how it is interrupted`,"UNSUPPORTED",{taskId:task.id,...vendor?{vendor}:{},hint:"only engines that declare an interrupt sequence can be interrupted \u2014 stop this one by hand in its tab, or close the tab with `tab-close`",nextCommandArgs:["api","get-task","--task-id",task.id]})}async function interruptTask(ctx){let taskId=ctx.args.str("task-id"),groupId=ctx.args.str("group"),all=ctx.args.bool("all")??!1,tabFlag=ctx.args.str("tab"),targets=[taskId!==void 0,groupId!==void 0,all].filter(Boolean).length;if(targets>1)throw new ApiError("pass one of --task-id, --group or --all","BAD_FLAG");if(targets===0)throw new ApiError("interrupt needs --task-id ID, --group GROUPID or --all","MISSING_TARGET");if(taskId!==void 0)return interruptOne(ctx,taskId,tabFlag);if(tabFlag)throw new ApiError("--tab names one task's tab; it only goes with --task-id","BAD_FLAG");return interruptMany(ctx,groupId)}async function interruptMany(ctx,groupId){let daemon=daemonOf(ctx),{tasks}=await daemon.request("task.list"),members=groupId===void 0?tasks:tasks.filter((t)=>t.groupId===groupId);if(groupId!==void 0&&members.length===0)throw new ApiError(`no tasks in group ${groupId}`,"TASK_NOT_FOUND",{hint:"the groupId is the one `add --count` returned; any surviving sibling's `.groupId` in `list` recovers it",nextCommandArgs:["api","list"]});let activity=await readActivityRegistry(daemon);if(!activity)throw new ApiError("could not read engine activity, so which tasks are running is unknown","BAD_DAEMON",{hint:"interrupt the tasks one at a time with --task-id",nextCommandArgs:["api","inspect"]});let results=[];for(let member of members){let scope=ptyScopeFor(tasks,member.id),runningTabs=Object.entries(activity.tabs[member.id]??{}).filter(([tabId,entry])=>entry.state==="running"&&hostedKeyFor(member.id,tabId,scope)!==null);if(runningTabs.length===0){results.push({taskId:member.id,status:"skipped",state:activity.tasks[member.id]?.state??null});continue}for(let[tabId]of runningTabs)try{results.push({...await interruptOne(ctx,member.id,tabId),status:"interrupted"})}catch(err){results.push({taskId:member.id,tabId,status:"failed",error:errorMessage(err),...err instanceof ApiError?{code:err.code}:{}})}}let count=(status)=>results.filter((r)=>r.status===status).length;return{...groupId!==void 0?{groupId}:{},count:members.length,interrupted:count("interrupted"),failures:count("failed"),results}}async function interruptOne(ctx,taskId,tabFlag){let{task}=await daemonOf(ctx).request("task.get",{taskId}),sequence=interruptSequenceFor({...task,id:taskId}),host=await openHostedSessionHost();if(!host)throw new ApiError(`no pty host to reach task ${taskId}`,"NO_ENGINE_TAB",{taskId,hint:"nothing is hosting this task's engine \u2014 `pty-list` says whether a host is up at all",nextCommandArgs:["api","pty-list"]});try{let sessions=await listHostedSessions(host.rpc),scope=await ptyScopeOf(daemonOf(ctx),task),key=tabFlag?hostedKeyFor(taskId,tabFlag,scope):findHostedEngineKey(sessions,taskId,taskEngineArgv(task)[0],scope);if(!key||!sessions.some((session)=>session.key===key&&session.alive))throw new ApiError(tabFlag?`task ${taskId} has no live tab ${tabFlag}`:`task ${taskId} has no live engine tab to interrupt`,tabFlag?"TAB_NOT_FOUND":"NO_ENGINE_TAB",{taskId,...tabFlag?{tabId:tabFlag}:{},hint:"`get-task` lists the live tabs in .tabs[]",nextCommandArgs:["api","get-task","--task-id",taskId]});return await host.rpc.request("pty.write",{key,data:sequence}),{ok:!0,taskId,tabId:key.split("::")[1]??"tab-1",vendor:task.vendor,interrupted:!0,bytes:Buffer.byteLength(sequence,"utf8")}}finally{host.close()}}var INTERRUPT_VERB={name:"interrupt",group:"drive",summary:"Stop the turn a task's engine is currently running \u2014 the headless twin of pressing the engine's own interrupt key. Writes the ENGINE-declared interrupt bytes to its pty; the session, its conversation and its worktree all survive, which is what separates this from `tab-close` and `delete`. Use it when a worker runs away: `send` cannot reach it (a busy engine's composer is not quiet), and until now the only levers left destroyed something. Engines that have not declared an interrupt sequence (every `generic` protocol engine) are refused with UNSUPPORTED rather than guessed at \u2014 Esc and ctrl-C mean opposite things across engines. Returns { taskId, tabId, vendor, interrupted, bytes }; `interrupted` reports the WRITE, so read `collect`'s .activity.state for the effect. Pass --group (one fan-out round) or --all (every task on this machine) instead of --task-id to stop many at once: only tabs whose own activity is `running` are written to, and the reply is { groupId?, count, interrupted, failures, results } with one entry per attempted tab, or a skipped entry for a member with no known running tabs. count counts members; interrupted and failures count tabs. Entries have status interrupted, skipped (with its .state) or failed (with .error/.code).",flags:[F.taskId(!1),{name:"group",type:"string",placeholder:"GROUPID",description:"Interrupt every running task of one fan-out round (the `groupId` that `add --count` returns). Mutually exclusive with --task-id and --all."},{name:"all",type:"bool",description:"Interrupt every running task on this machine. Mutually exclusive with --task-id and --group."},{name:"tab",type:"string",placeholder:"TAB",description:"Interrupt this exact tab (id from `get-task` .tabs[].id). Omitted = the task's canonical engine tab, the same one a bare `send` targets. Only with --task-id."}],handler:interruptTask};var PANE_VERB={name:"pane-open",group:"drive",summary:"Open a terminal pane in a task's workspace: split the focused tab (default, or --tab's tab) or open a separate command tab, optionally running a command. Broadcast over the daemon's tab.open channel \u2014 an attached TUI showing the task performs the split. Task defaults to $ROVE_TASK_ID, then the active task. Returns the resolved `title` (the label `pane-close --title` must match \u2014 derived from the command's first word when --title is omitted) plus `clients` (attached connections, the caller included) and `reached` (false = no front-end subscribes to the channel, so nobody performed the split).",flags:[F.taskId(!1),{name:"tab",type:"string",placeholder:"TAB",description:"Host tab for the split (e.g. tab-3) instead of the focused tab (split placement only)."},{name:"command",type:"string",placeholder:"CMD",description:"Shell command the pane runs (via the login shell's `-ilc`, so pipes/args and your shell rc's PATH/exports work); the pane closes when it exits. Omit for an interactive shell."},{name:"direction",type:"enum",values:["right","down"],default:"right",description:"Split orientation relative to the active pane (split placement only)."},{name:"placement",type:"enum",values:["split","tab"],default:"split",description:"`split` joins the focused tab's split group; `tab` opens a separate command tab."},{name:"title",type:"string",placeholder:"TEXT",description:`Pane label (default: the command's first word, else "shell").`}],handler:async(ctx)=>{let client=daemonOf(ctx),taskId=ctx.args.str("task-id")??process.env.ROVE_TASK_ID??await resolveActiveTaskId(client);if(!taskId)throw new ApiError("no target task: pass --task-id (no $ROVE_TASK_ID, no active task)","MISSING_TARGET");let command=ctx.args.str("command"),shell=resolveSessionShell(),argv=command?[shell,"-ilc",command]:[shell,"-il"],title=ctx.args.str("title")??(command?command.trim().split(/\s+/)[0]??"shell":"shell"),tabId=ctx.args.str("tab");return{...await simpleRpc(ctx,"tab.open",{taskId,argv,title,...tabId!==void 0?{tabId}:{},placement:ctx.args.str("placement")??"split",direction:ctx.args.str("direction")??"right"}),title}}},PANE_CLOSE_VERB={name:"pane-close",group:"drive",summary:"Close panes opened by pane-open: every split pane / command tab in the task whose label matches --title. Broadcast over the daemon's tab.close channel \u2014 an attached TUI showing the task performs the close (headless no-op). Task defaults to $ROVE_TASK_ID, then the active task. Returns `clients` (attached connections, the caller included) and `reached` (false = nobody performed the close).",flags:[F.taskId(!1),{name:"title",type:"string",required:!0,placeholder:"TEXT",description:"Pane label to close \u2014 the --title the pane was opened with (engine panes are never closed)."},{name:"tab",type:"string",placeholder:"TAB",description:"Scope the title match to one tab (e.g. tab-3) instead of every tab of the task."}],handler:async(ctx)=>{let client=daemonOf(ctx),taskId=ctx.args.str("task-id")??process.env.ROVE_TASK_ID??await resolveActiveTaskId(client);if(!taskId)throw new ApiError("no target task: pass --task-id (no $ROVE_TASK_ID, no active task)","MISSING_TARGET");let tabId=ctx.args.str("tab");return simpleRpc(ctx,"tab.close",{taskId,title:ctx.args.str("title"),...tabId!==void 0?{tabId}:{}})}},TAB_CLOSE_VERB={name:"tab-close",group:"drive",summary:"Close one Terminal Tab by the id returned in get-task .tabs[]. Runs the same close path as ctrl+w when a TUI is attached; otherwise removes the persisted tab snapshot and ends its hosted PTYs directly. Engine, shell/command, and content tabs are all valid. Closing the last tab leaves the task open with no session.",flags:[F.taskId(!0),{name:"tab",type:"string",required:!0,placeholder:"TAB",description:"Exact Terminal Tab id from get-task .tabs[].id (for example tab-3)."}],handler:async(ctx)=>{let taskId=ctx.args.require("task-id"),tabId=ctx.args.require("tab"),daemon=daemonOf(ctx);if((await daemon.request("terminalTab.close",{taskId,tabId})).handled)return{ok:!0,taskId,tabId,handledBy:"tui"};let result=await ctx.runtime.closeTerminalTab(daemon,taskId,tabId);return{ok:!0,taskId,tabId,handledBy:"headless",...result}}};async function launchKind(ctx,daemon,task,kind){let taskId=task.id,worktree=taskCwd(task);if(!worktree)throw new ApiError(`task ${taskId} has no worktree yet`,"NO_WORKTREE");let spec;if(kind==="editor"){let line=ctx.args.int("line");spec={kind,taskId,worktree,file:ctx.args.require("file"),...line===void 0?{}:{line}}}else if(kind==="pane")spec={kind,taskId,worktree,pane:ctx.args.require("pane"),hostTab:ctx.args.str("tab")};else{let viewId=ctx.args.require("view-task");if(viewId===taskId)throw new ApiError("--view-task must name another task","BAD_FLAG",helpStep("tab-new"));let{task:shown}=await daemon.request("task.get",{taskId:viewId});if(!shown.worktreePath)throw new ApiError(`task ${viewId} has no worktree yet`,"NO_WORKTREE");if(shown.repo!==task.repo)throw new ApiError("--view-task must be a task of the same project","BAD_FLAG",helpStep("tab-new"));spec={kind,taskId,view:{id:viewId,worktree:shown.worktreePath}}}let launched=await ctx.runtime.launchTab(daemon,spec),reply=await daemon.request("terminalTab.add",{taskId,tab:launched.tab});if(launched.replaces){if(!(await daemon.request("terminalTab.close",{taskId,tabId:launched.replaces})).handled)await ctx.runtime.closeTerminalTab(daemon,taskId,launched.replaces)}return{ok:!0,taskId,tabId:launched.tab.id,session:launched.session,kind,...launched.leaf?{leaf:launched.leaf}:{},...launched.atLine===void 0?{}:{atLine:launched.atLine},clients:reply.clients??0}}async function openShellTab(ctx,daemon,task,type){let taskId=task.id,own=task.scratch===!0&&((await readDaemonTabs(daemon,taskId))?.tabs.length??0)===0,row=own?task:await spawnSharedChild(daemon,taskId,void 0,SHELL_TASK_TITLE);try{let opened=await ctx.runtime.openTab(daemon,{id:row.id,worktreePath:taskCwd(row),kind:row.kind,vendor:row.vendor,command:row.command,repo:row.repo,...task.extraRepos?.length?{multiRepo:!0}:{}},"shell",{home:!0,...type===void 0?{}:{type}}),shell={ok:!0,taskId,tabId:opened.tab.id,session:opened.session,kind:"shell"};if(!own)return{...shell,childTaskId:row.id};let reply=await daemon.request("terminalTab.add",{taskId,tab:opened.tab});return{...shell,clients:reply.clients??0}}catch(err){if(own)throw err;return failSharedChild(daemon,err,row.id,["api","tab-new","--task-id",taskId,"--kind","shell"])}}var TAB_NEW_VERB={name:"tab-new",group:"drive",summary:"Open a new Terminal Tab with no first message: a login `shell`, or an `engine` conversation that starts idle (the TUI's sidebar \"New shell\" and picker Enter). An `engine` starts in the task's own `tab-1` while the task has no conversation yet, else in a new shared child task of --task-id (same directory; the reply then carries `childTaskId`) whose engine starts in its own `tab-1`, never a second engine tab on one task; it respects engine.maxLiveSessions. Unlike `send --tab new` it needs no prompt. A `shell` is always its own row: a new shared child task of --task-id (same directory, title `shell`; the reply carries `childTaskId`) whose own `tab-1` runs the login shell, spawned in the task's worktree (creating it if needed). A fresh, tab-less Ungrouped (scratch) task is the exception: its shell is its `tab-1`, no child. When the shell exits the row stays, empty, and \u23CE starts a fresh shell; nothing deletes it. Three more kinds run something Rove resolves itself, never a command line from the caller: `editor` opens `--file` (relative to the worktree, at `--line` when given) in the editor from Settings \u2192 General \u2192 Editor / $VISUAL / $EDITOR, in the task's one editor slot (a previous editor tab is closed once the new one is up); `pane` opens a registered plugin pane (`--pane <plugin>.<pane>`), split into `--tab` (default: the active tab) or as its own tab as the pane's manifest says; `viewport` adds a tab onto another task's first engine session (`--view-task`, same project) without starting anything. Broadcast over the daemon's tab.add channel so an attached TUI absorbs the row (`clients` = attached connections; the non-engine kinds and an Ungrouped task's own shell only). Returns the new `tabId` and its pty `session` key; the tab then lists in get-task .tabs[].",flags:[F.taskId(!0),{name:"kind",type:"enum",values:["shell","engine","editor","pane","viewport"],required:!0,placeholder:"KIND",description:"`shell` = the user's login shell; `engine` = an idle engine in a new shared child task; `editor` = a file in the user's editor; `pane` = a plugin pane; `viewport` = another task's engine session."},{name:"command",type:"string",placeholder:"CMD",description:"Engine only: run the new child task on this engine (an engine id from `engine-list` or a full command line), as `send --tab new --command` does. Omitted = the task's own engine."},{name:"latest",type:"bool",description:"Engine tabs only: open on the engine this project used last (the TUI's ctrl+t), else the default engine, instead of the task's own. Ignored beside `--command`."},{name:"file",type:"string",placeholder:"PATH",description:"Editor tabs only: the file to open, relative to the task's worktree."},{name:"line",type:"int",placeholder:"N",description:"Editor tabs only: open at this line. `atLine: false` in the result means the editor takes no line argument Rove knows; add `{line}` to editor.customCommand."},{name:"pane",type:"string",placeholder:"ID",description:"Pane tabs only: `<plugin id>.<pane id>` of an enabled plugin's pane."},{name:"tab",type:"string",placeholder:"TAB",description:"Pane tabs only: the tab a split pane joins (e.g. tab-3); default the task's active tab."},{name:"view-task",type:"string",placeholder:"ID",description:"Viewport tabs only: the task (same project) whose first engine session the tab shows."},{name:"type",type:"string",placeholder:"TEXT",description:"Shell tabs only: type TEXT into the new shell once it is up, then press Enter (e.g. a dev server's command). The tab stays a normal shell: Ctrl+C stops what it started."}],handler:async(ctx)=>{let daemon=daemonOf(ctx),taskId=ctx.args.require("task-id"),kind=ctx.args.requireEnum("kind"),latest=ctx.args.bool("latest")===!0;if(ctx.args.str("line")!==void 0&&kind!=="editor")throw new ApiError("--line only applies to an editor tab; pass --kind editor","BAD_FLAG",helpStep("tab-new"));if((ctx.args.str("command")||latest)&&kind!=="engine")throw new ApiError("--command and --latest only apply to an engine tab; pass --kind engine","BAD_FLAG",helpStep("tab-new"));let type=ctx.args.str("type");if(type!==void 0&&kind!=="shell")throw new ApiError("--type only applies to a shell tab; pass --kind shell","BAD_FLAG",helpStep("tab-new"));let{task}=await daemon.request("task.get",{taskId}),preferred=latest?resolvePreferredVendor(task.repo):void 0,command=ctx.args.str("command")||(preferred&&preferred!==task.vendor?preferred:void 0);if(kind==="editor"||kind==="pane"||kind==="viewport")return launchKind(ctx,daemon,task,kind);if(kind==="engine")return withFirstConversationLock(taskId,async()=>{let home=await conversationHome(daemon,ctx.runtime,task,command),target={id:home.task.id,worktreePath:taskCwd(home.task),kind:home.task.kind,vendor:home.task.vendor,command:home.task.command,modelEffort:home.task.modelEffort,model:home.task.model,repo:home.task.repo,...task.extraRepos?.length?{multiRepo:!0}:{}};if(!home.child){let opened=await ctx.runtime.openTab(daemon,target,kind),reply=await daemon.request("terminalTab.add",{taskId,tab:opened.tab});return{ok:!0,taskId,tabId:opened.tab.id,session:opened.session,kind,clients:reply.clients??0}}try{let opened=await ctx.runtime.openTab(daemon,target,kind);return{ok:!0,taskId,childTaskId:home.task.id,tabId:opened.tab.id,session:opened.session,kind}}catch(err){return failSharedChild(daemon,err,home.task.id,["api","tab-new","--task-id",taskId,"--kind","engine",...command?["--command",command]:[]])}});return openShellTab(ctx,daemon,task,type)}},TAB_SPLIT_VERB={name:"tab-split",group:"drive",summary:"Edit one Terminal Tab's split panes without a TUI: `open` splits a pane (the TUI's ctrl+\\ right / ctrl+= down) into a new login shell in the task's worktree, `close` ends a pane, `rename` names it (F2), `reset` kills a shell pane and spawns a fresh one (F5; engine panes refuse). The layout is the tab's persisted split tree and each pane beyond `leaf-1` is its own hosted session `<taskId>::<tabId>::<leaf>`, so an attached TUI absorbs the new row over tab.add and a later `tab-close` sweeps the panes. The shell is always the login shell: no command line is taken. Returns the pane acted on, whether the tree changed (`false` when the pane is too small to split), and the tab's `split` layout (null once back to one pane).",flags:[F.taskId(!0),{name:"tab",type:"string",required:!0,placeholder:"TAB",description:"Exact Terminal Tab id from get-task .tabs[].id (for example tab-2)."},{name:"action",type:"enum",values:["open","close","rename","reset"],required:!0,placeholder:"ACTION",description:"`open` a new pane, `close` one, `rename` one, or `reset` a shell pane."},{name:"direction",type:"enum",values:["right","down"],default:"right",description:"`open` only: split toward the right (side by side) or down (stacked)."},{name:"leaf",type:"string",placeholder:"LEAF",description:"The pane to act on (`leaf-1` is the tab's own process); for `open`, the pane being split. Default: the tree's active pane."},{name:"title",type:"string",placeholder:"TEXT",description:"`rename` only: the pane's name; empty clears it back to the default."},{name:"cols",type:"int",placeholder:"N",description:"Cells wide of the pane acted on; with --rows, `open` refuses a split that would leave a pane under 20x6."},{name:"rows",type:"int",placeholder:"N",description:"Cells high of the pane acted on (see --cols)."}],handler:async(ctx)=>{let daemon=daemonOf(ctx),taskId=ctx.args.require("task-id"),tabId=ctx.args.require("tab"),action=ctx.args.requireEnum("action"),{task}=await daemon.request("task.get",{taskId});if(!task.worktreePath)throw new ApiError(`task ${taskId} has no worktree yet`,"NO_WORKTREE");let cols=ctx.args.int("cols"),rows=ctx.args.int("rows");if(cols===void 0!==(rows===void 0))throw new ApiError("--cols and --rows go together","BAD_FLAG",helpStep("tab-split"));let size=cols!==void 0&&rows!==void 0?{cols,rows}:void 0,leaf=ctx.args.str("leaf")??"";if(action!=="open"&&!leaf)throw new ApiError(`--leaf is required to ${action}`,"BAD_FLAG",helpStep("tab-split"));let edit=action==="open"?{kind:"open",orientation:ctx.args.str("direction")==="down"?"column":"row",from:leaf,size}:action==="rename"?{kind:"rename",leaf,title:ctx.args.str("title")??""}:action==="reset"?{kind:"reset",leaf,size}:{kind:"close",leaf},out=await ctx.runtime.splitTab(daemon,taskId,tabId,taskCwd(task),edit),reply=out.changed?await daemon.request("terminalTab.add",{taskId,tab:out.tab}):{};return{ok:!0,taskId,tabId,leaf:out.leaf,changed:out.changed,split:wireSplit(out.tab.splitTree),clients:reply.clients??0}}};var VIEWER_SHAPE="{ viewer, surface: tui|web|phone, taskId, tabId?, panel (has a side panel: web desktop only), panelOpen?, narrow?, local (same machine as the daemon) }",VIEWERS_VERB={name:"viewers",group:"read",summary:`Every UI attached and showing a task right now, so an agent knows what the human can see before it drives anything. Returns { viewers: [${VIEWER_SHAPE}] }. A TUI is surface tui (no panel, local). \`panel: true\` = a web page with a side panel, the only screen \`panel-show\` can open; \`local: false\` = behind a tunnel, so its \`localhost\` is not yours. An empty list = nobody is looking. --task-id keeps one task's viewers.`,flags:[F.taskId(!1)],handler:(ctx)=>simpleRpc(ctx,"ui.viewers",{taskId:ctx.args.str("task-id")})},PANEL_SHOW_VERB={name:"panel-show",group:"drive",summary:"Bring up the web side panel on a view of a task: the Browser (--url), a file in the Files view or the Diff view (--path). Every web client remembers the view for the task; a web page currently on that task with a panel opens it (no focus steal, no task switch). A TUI has no panel: nothing happens there. Task defaults to $ROVE_TASK_ID, then the active task. Returns { shown (pages that open it now), remembered (web pages that keep it for later), viewers (as `viewers`, for the task), url? { reachable, frameable, reason? } }. `url.reachable: false` = a localhost URL and the viewer is remote; `url.frameable: false` = the site forbids framing (X-Frame-Options / CSP frame-ancestors) or was refused by the probe, so tell the human the link instead.",flags:[F.taskId(!1),{name:"view",type:"enum",values:["browser","files","diff"],required:!0,description:"Which panel view to show."},{name:"url",type:"string",placeholder:"URL",description:"Browser view: the http(s) page to load. Only localhost is framed over plain http."},{name:"path",type:"string",placeholder:"FILE",description:"Files / Diff view: a file relative to the task's worktree (no `..`)."}],handler:async(ctx)=>{let taskId=ctx.args.str("task-id")??process.env.ROVE_TASK_ID??await resolveActiveTaskId(daemonOf(ctx));if(!taskId)throw new ApiError("no target task: pass --task-id (no $ROVE_TASK_ID, no active task)","MISSING_TARGET");return simpleRpc(ctx,"panel.show",{taskId,view:ctx.args.str("view"),url:ctx.args.str("url"),path:ctx.args.str("path")})}};var DEFAULT_TTL_SECONDS=60;function sourceOf(env=process.env){return env.ROVE_PLUGIN_ID??"cli"}async function rowToken(ctx){let{args}=ctx,taskId=args.require("task-id"),clear=args.bool("clear")===!0,text=args.str("text");if(!clear&&!text)throw new ApiError("row-token needs --text, or --clear to remove one","MISSING_TEXT");let key=args.str("key"),tone=args.str("tone"),ttl=args.int("ttl");return simpleRpc(ctx,"task.rowToken",{taskId,source:sourceOf(),...key?{key}:{},...clear?{clear:!0}:{text,...tone?{tone}:{}},...ttl!==void 0?{ttlMs:ttl*1000}:{}})}var ROW_TOKEN_VERB={name:"row-token",group:"drive",summary:`Put one short label on a task's sidebar/board row, with a TTL \u2014 the surface a coordination plugin paints its own state on. Every token EXPIRES (default ${DEFAULT_TTL_SECONDS}s, max 1h): a plugin refreshes the label to keep it, and one that dies has its labels fade instead of leaving stale state on screen. A plugin writes into its own slot (\`ROVE_PLUGIN_ID\` + --key, 2 slots per plugin per task) and can say nothing else: the derived group, the activity badge, the PR chip, the title and the branch are host-owned. \`--tone\` names a ROLE, not a colour, so the active theme still decides how it looks. Text is trimmed to ${ROW_TOKEN_MAX_TEXT} characters. Returns { ok, token } (or { ok, cleared } with --clear).`,flags:[F.taskId(!0),{name:"text",type:"string",placeholder:"LABEL",description:`The label, trimmed to ${ROW_TOKEN_MAX_TEXT} characters. Required unless --clear.`},{name:"ttl",type:"int",default:String(DEFAULT_TTL_SECONDS),placeholder:"SECONDS",description:"How long the label survives without a refresh (1\u20263600, clamped)."},{name:"key",type:"string",default:"default",placeholder:"SLOT",description:"Which of your two slots on this row to write. Writing the same key again replaces the token."},{name:"tone",type:"enum",values:ROW_TOKEN_TONES,description:"Semantic role the theme colours. Omitted = the row's own muted tone."},{name:"clear",type:"bool",description:"Remove the token at --key now, or every token of yours on this row when --key is omitted."}],handler:rowToken};var TAB_REVIVE_VERB={name:"tab-revive",group:"drive",summary:"Reopen one Terminal Tab's own session when no PTY runs in it: a dead or freeze-restored tab, or one whose key the PTY host lost (a host restart that dropped its freeze records). A running tab is left alone (`revived: false, alive: true`). With --restart-exited, an engine tab whose session is alive but whose engine has exited (get-task .tabs[].engineAlive false: only the fallback shell is left) is closed and relaunched like a dead one; an engine still running, or a shell tab, is still left alone. An engine tab resumes its persisted conversation (`resumed: true`) or, with none recorded, starts a fresh one with no first message \u2014 the task's original prompt is never replayed; it respects engine.maxLiveSessions. A shell or command tab runs its recorded command in the task's worktree. Refused: a tab id the task does not hold (TAB_NOT_FOUND), a split leaf (`tab-split --action reset` is its reopen), and viewport or preview tabs (TAB_NOT_REVIVABLE). A task on another registered machine runs there, like every verb. Returns { taskId, tabId, revived, resumed, sessionId? }.",flags:[F.taskId(!0),{name:"tab",type:"string",required:!0,placeholder:"TAB",description:"Exact Terminal Tab id from get-task .tabs[].id (for example tab-2)."},{name:"restart-exited",type:"bool",placeholder:"BOOL",description:"Also relaunch a tab whose engine exited inside a still-running session (keepAlive's fallback shell); without it that tab counts as running."}],handler:async(ctx)=>{let daemon=daemonOf(ctx),taskId=ctx.args.require("task-id"),tabId=ctx.args.require("tab");if(tabId.includes("::")){let[tab,...leaf]=tabId.split("::");throw new ApiError(`${tabId} is a split pane, not a tab`,"BAD_FLAG",{taskId,hint:"reopen a split pane with `tab-split --action reset`",nextCommandArgs:["api","tab-split","--task-id",taskId,"--tab",tab,"--action","reset","--leaf",leaf.join("::")]})}let{task}=await daemon.request("task.get",{taskId}),scope=await ptyScopeOf(daemon,task);if(hostedKeyFor(taskId,tabId,scope)===null)throw new ApiError(`tab ${tabId} on task ${taskId} belongs to a child task's conversation`,"TAB_NOT_FOUND",{taskId,tabId,hint:"address the child itself with its own --task-id (`list` shows the lineage)",nextCommandArgs:["api","get-task","--task-id",taskId]});let alias=scope.alias;if(alias?.tabId===tabId)throw new ApiError(`tab ${tabId} of task ${taskId} runs under task ${alias.taskId}`,"TAB_NOT_REVIVABLE",{taskId,tabId,hint:"revive it under the task that hosts the session",nextCommandArgs:["api","tab-revive","--task-id",alias.taskId,"--tab",alias.tabId]});let worktree=taskCwd(task);if(!worktree)throw new ApiError(`task ${taskId} has no worktree yet`,"NO_WORKTREE");let exited=ctx.args.bool("restart-exited")===!0&&(await ctx.runtime.taskTabs(taskId,taskEngineArgv(task),scope)).tabs.some((t)=>t.id===tabId&&t.kind==="engine"&&t.alive===!0&&t.engineAlive===!1),revived=await ctx.runtime.reviveTab(daemon,{id:taskId,worktreePath:worktree,kind:task.kind,vendor:task.vendor,command:task.command,modelEffort:task.modelEffort,model:task.model,repo:task.repo},tabId,worktree,{replaceExited:exited});return{ok:!0,taskId,tabId,...revived}}};var DRIVE_VERBS=[{name:"send",group:"drive",summary:"Paste a follow-up prompt into a task's running engine (one full turn). Without --task-id, a task spawned from another Rove session replies to its dispatcher's tab (then that task's live canonical engine; nothing alive = DISPATCHER_UNREACHABLE, never a silent spawn); otherwise the active task. Sent from inside another Rove task ($ROVE_TASK_ID), the prompt is prefixed with [ROVE PEER] provenance \u2014 who sent it and how to reply (tab-precise) \u2014 so agent-to-agent messaging needs no coordinator. The prompt is pasted into the target composer and submitted; delivery is refused only when there is physically nothing to write to (no such tab, dead PTY, no engine process). A `succeeded:` report sent from a managed task whose branch has 0 commits is REFUSED (EMPTY_SUCCESS_REPORT) \u2014 commit first, or pass --allow-empty when the task genuinely produced no commits. `delivered: true` means the bytes landed, nothing more: when the target session was already in `error` or `dead`, the reply carries `targetState` (+ `targetDetail`) \u2014 read it before waiting on an answer.",flags:[F.taskId(!1),F.prompt(!1,"Text pasted + submitted into the engine pane. One of --prompt / --prompt-file is required, except with --handoff-from."),F.promptFile(),{name:"tab",type:"string",required:!1,placeholder:"TAB",description:'Tab addressing: "new" sends the prompt to a new conversation: `tab-1` of the target itself while it has none yet, else a shared child task of the target (same directory) \u2014 the reply then adds `childTaskId`, and `session` is the child\'s; "tab-N" delivers to that exact alive tab (error when dead/absent). Omitted = the canonical engine tab.'},{...F.command(),description:"Engine launch command for the child `--tab new` creates \u2014 the API twin of the TUI's ctrl+e pick. Lets one directory run two agents on the same files (e.g. hand the stuck work to codex without leaving the branch). An engine id from `engine-list` or a full command line; stored on the child, so it survives restarts. Only valid with --tab new."},{name:"handoff-from",type:"string",required:!1,placeholder:"TAB",description:"Continue another tab's conversation in the new one (`--tab new`, usually with --command naming the other engine): the prompt becomes a handoff brief \u2014 the task title, the source engine's transcript path, the user's last request and a bounded excerpt of the latest turns \u2014 followed by --prompt when given, so --prompt is optional. Same directory and branch; the source tab is only read. Refuses a tab with no conversation yet (NO_CONVERSATION) or an engine whose transcript Rove cannot name (NO_TRANSCRIPT)."},{name:"replace",type:"bool",required:!1,description:"With --handoff-from tab-N: put the new engine in that tab's own slot instead of a shared child task. Only for a source whose engine is dead or exited (`get-task` .tabs[]: `alive: false`, or `engineAlive: false` = only the fallback shell is left); a running engine is refused (TAB_ALIVE) and keeps the child. Same task, same tab id, same worktree: the tab is repointed at the --command engine with a fresh conversation (the old session is left untouched on disk), launched, and handed the brief. The reply adds `replacedTab`. The shipped UIs pass this exact rule on their own; a script chooses it."},{name:"respawn",type:"bool",required:!1,description:"Revive a FREEZE-RESTORED --tab tab-N before delivering. After a pty-host restart (reboot, crash) a tab keeps its scrollback and launch command but nothing runs in it; without this flag such a tab is refused (TAB_RESTORED) rather than silently re-run. With it, the tab is respawned in place and resumes its pinned conversation when it has one (`get-task` shows each tab's sessionId) \u2014 a tab with none replays its recorded launch command, which for claude carries the task's original first prompt. Only valid with --tab tab-N."},{name:"plain",type:"bool",required:!1,description:"Deliver the prompt verbatim \u2014 skip the [ROVE PEER] provenance prefix."},{name:"allow-empty",type:"bool",required:!1,description:"Report success from a task with zero commits (EMPTY_SUCCESS_REPORT is refused otherwise). For work that legitimately produces no commits \u2014 an investigation, a review, a question answered."},{name:"expect-report",type:"bool",required:!1,description:"Arm a delivery: the prompt gets a [ROVE] footer naming `report --delivery <id>`, the reply carries `deliveryId`, and a turn that ends without that report raises a `report_missing` Inbox row. Implied for a task added with --require-report. See `deliveries`."},{name:"if-runnable",type:"bool",required:!1,description:"Send only when `should-run` says the task can use a tick. The check, the delivery claim and the paste share one decision, so two ticks arriving together cannot both pass. When it refuses, nothing is pasted and the reply is `{skipped: {run:false, reason, detail?, until?, gateIds?}}` with exit 3 (a skip recorded as a `skipped` delivery when the send would have owed a report). A skip is dropped, never held for later. See `should-run`."},{name:"queue",type:"bool",required:!1,description:"Wait for a running turn instead of landing in it. A tab that is idle takes the prompt now, as without the flag; one that is working, waiting on a permission menu or rate-limited has it held by the daemon and pasted when its turn ends, one held prompt per turn, in order. The reply is then `{queued:true, queueId, position, tabId}`. Without it a prompt sent mid-turn lands in that turn: Codex drops the work in flight, Claude Code folds it in. A report (`--expect-report`) is armed when the prompt is pasted, not when it is held. See `queue-list`, `queue-edit`, `queue-cancel`; a daemon restart drops held prompts."}],handler:send},DISPATCH_VERB,INTERRUPT_VERB,QUEUE_EDIT_VERB,QUEUE_CANCEL_VERB,{name:"note",group:"drive",summary:"File a one-line field note \u2014 a resolved, repo-level gotcha worth sharing. Appended to the repo's durable note store (every future session on this repo starts with it) and forwarded to the dispatcher session for live relay (docs/design/dispatcher.md).",flags:[F.taskId(!0),{name:"text",type:"string",required:!0,placeholder:"TEXT",description:"One line: the verified conclusion another session could act on."}],handler:note},{name:"note-list",group:"drive",summary:"Read a repo's accumulated field notes, newest first. Returns { notes } \u2014 each with the `id` note-delete takes.",flags:[F.repo(!0)],handler:(ctx)=>simpleRpc(ctx,"note.list",{repo:ctx.args.requireRepo("repo")})},{name:"note-delete",group:"drive",summary:"Retire one field note by the id `note-list` reports. The note store is not an archive: its newest entries are injected into every fresh session on the repo, so a note whose fact has stopped being true keeps being handed to agents as if it still were. Returns { deleted } \u2014 false (not an error) when the id names nothing, which is also what an already-evicted note answers.",flags:[F.repo(!0),{name:"id",type:"int",required:!0,placeholder:"N",description:"Note id from `note-list`."}],handler:(ctx)=>simpleRpc(ctx,"note.delete",{repo:ctx.args.requireRepo("repo"),id:ctx.args.int("id")})},PANE_VERB,PANE_CLOSE_VERB,PANE_GRAPHICS_VERB,TAB_CLOSE_VERB,TAB_NEW_VERB,TAB_SPLIT_VERB,TAB_REVIVE_VERB,PANEL_SHOW_VERB,{name:"notify",group:"drive",summary:"Show a toast in every attached Rove UI \u2014 broadcast over the daemon's notice.event channel. Agents/scripts use it to surface 'done / needs input / error' moments without touching the task's session. Returns `clients` (attached connections; 0 = no UI showed the toast).",flags:[{name:"title",type:"string",required:!0,placeholder:"TEXT",description:"Toast text (one line)."},{name:"body",type:"string",placeholder:"TEXT",description:"Optional second line under the title \u2014 context, not a second message."},{name:"kind",type:"string",default:"done",placeholder:"KIND",description:`Free-form kind tag. "done", "needs_input" and "error" get the TUI's severity styling/unread mark; any other value renders neutrally.`},F.taskId(!1),{name:"source",type:"string",placeholder:"TAG",description:"Free-form origin tag (e.g. an agent name) recorded on the event."}],handler:async(ctx)=>{return simpleRpc(ctx,"notice.send",{title:ctx.args.str("title"),body:ctx.args.str("body"),kind:ctx.args.str("kind")??"done",taskId:ctx.args.str("task-id"),source:ctx.args.str("source")})}},{name:"prompt",group:"drive",summary:"Ask the human for a line of text through the attached TUI's input dialog (plugins' host-provided prompt). Blocks until answered, cancelled, or timed out; returns { value } or { cancelled, reason }. With no front-end attached it returns at once with reason `no-gui`.",flags:[{name:"title",type:"string",required:!0,placeholder:"TEXT",description:"Dialog title (shown verbatim)."},{name:"placeholder",type:"string",placeholder:"TEXT",description:"Input placeholder."},{name:"initial",type:"string",placeholder:"TEXT",description:"Pre-filled input value."},{name:"timeout",type:"string",placeholder:"MS",description:"Give up after this many milliseconds (default 120000, max 600000)."}],handler:async(ctx)=>{let timeoutRaw=ctx.args.str("timeout"),timeoutMs=timeoutRaw?Number.parseInt(timeoutRaw,10):void 0;return simpleRpc(ctx,"ui.prompt",{title:ctx.args.str("title"),placeholder:ctx.args.str("placeholder"),initial:ctx.args.str("initial"),...timeoutMs&&Number.isFinite(timeoutMs)?{timeoutMs}:{}})}},{name:"engine-report",group:"drive",summary:"Report a normalized engine-activity verb for a task \u2014 the public face of the same engine.reportEvent RPC the built-in hook adapters use. Lets a plugin-contributed engine (or any wrapper script) drive the sidebar badge, attention inbox, and plugin event stream without a built-in hook adapter. Kinds: session-start|turn-start|turn-complete|turn-failed|turn-interrupted|awaiting-input|session-end (state kinds) plus tool-pre|tool-post|tool-failed|pre-compact|post-compact|subagent-start|subagent-stop (plugin-only).",flags:[F.taskId(!1),{name:"kind",type:"enum",values:ENGINE_ACTIVITY_KINDS,required:!0,placeholder:"KIND",description:"Normalized activity verb (see summary). Unknown kinds are rejected."},{name:"engine",type:"string",placeholder:"ID",description:"Engine id producing the report (a plugin engine id, or a built-in vendor)."},{name:"tab",type:"string",placeholder:"TAB",description:"Terminal tab id the session runs in (defaults to $ROVE_TAB_ID / $ROVE_TAB_ID)."},{name:"detail",type:"string",placeholder:"JSON",description:`Optional detail JSON, e.g. '{"failure":"rate_limit"}' or '{"waiting":"input"}'.`}],handler:async(ctx)=>{let taskId=ctx.args.str("task-id")??process.env.ROVE_TASK_ID,tabId=ctx.args.str("tab")??process.env.ROVE_TAB_ID,detailRaw=ctx.args.str("detail"),detail;if(detailRaw!==void 0)try{detail=JSON.parse(detailRaw)}catch{throw new ApiError("--detail must be valid JSON","BAD_FLAG",helpStep("engine-report"))}if(!taskId&&ctx.caller.cwd===void 0)throw new ApiError("no target task: pass --task-id (this caller has no cwd to map)","MISSING_TARGET");return simpleRpc(ctx,"engine.reportEvent",{kind:ctx.args.str("kind"),...taskId?{taskId}:{cwd:ctx.caller.cwd},...ctx.args.str("engine")?{engine:ctx.args.str("engine")}:{},...tabId?{tabId}:{},...detail!==void 0?{detail}:{}})}},ROW_TOKEN_VERB,{name:"set-active",group:"drive",summary:"Set the shared active task (the focus every Tasks pane highlights). Pass --none to clear.",flags:[F.taskId(!1),{name:"none",type:"bool",description:"Clear the active task instead of setting one."}],handler:setActive}];async function collect(ctx){let daemon=daemonOf(ctx),{args,runtime}=ctx,idsFlag=args.str("task-ids"),repoFlag=args.path("repo"),groupFlag=args.str("group"),taskIds,unresolvableRepos=[];if(idsFlag)taskIds=idsFlag.split(",").map((s)=>s.trim()).filter(Boolean);else if(repoFlag||groupFlag){let{tasks}=await daemon.request("task.list"),filter=repoFlag?await repoFilter(runtime,repoFlag,tasks.map((t)=>t.repo)):null;unresolvableRepos=filter?.unresolvableRepos??[],taskIds=[];for(let t of tasks){if(groupFlag&&t.groupId!==groupFlag)continue;if(filter&&!filter.matches(t.repo))continue;taskIds.push(t.id)}}else throw new ApiError("collect needs --task-ids id1,id2, --group GROUPID, or --repo PATH","MISSING_TARGET");let registry=await readActivityRegistry(daemon),out=[];for(let taskId of taskIds){let{task}=await daemon.request("task.get",{taskId}),{tabs,running}=await runtime.taskTabs(taskId,taskEngineArgv(task),await ptyScopeOf(daemon,task)),repos=taskRepos(task),signals=await Promise.all(repos.map(async(r)=>({changes:r.worktreePath?await runtime.readWorktreeChanges(r.worktreePath):null,base:r.worktreePath?await runtime.readBranchSignals(r.worktreePath,r.baseRef):{baseRef:null,ahead:null,behind:null,diff:null}}))),{changes,base}=signals[0]??{changes:null,base:null},activity=await activityView(registry,task),runnability=await readRunnability(daemon,task.id);out.push({taskId:task.id,title:task.title,branch:task.branch,worktreePath:task.worktreePath,vendor:task.vendor,status:task.status,...task.groupId?{groupId:task.groupId}:{},...task.report?{report:task.report}:{},...task.dispatcher?{dispatcher:task.dispatcher}:{},running,activity,...runnability?{runnability}:{},tabs,changes,base,...task.extraRepos?.length?{repos:repos.map((r,i)=>({name:r.name,branch:r.branch,worktreePath:r.worktreePath,changes:signals[i]?.changes??null,base:signals[i]?.base??null}))}:{}})}return{tasks:out,...unresolvableRepos.length>0?{unresolvableRepos}:{}}}async function feedback(ctx){let kind=ctx.args.str("kind")??DEFAULT_FEEDBACK_KIND;if(!isFeedbackKind(kind))throw new ApiError(`--kind must be one of ${FEEDBACK_KINDS.join(", ")}`,"BAD_FLAG");return{ok:!0,...submitFeedback({title:ctx.args.require("title"),body:ctx.args.require("body"),kind})}}var FEEDBACK_VERBS=[{name:"feedback",group:"feedback",summary:"File a GitHub issue on the Rove repo through `gh`, with an environment block appended. Returns { filed, url, number? }; when `gh` is missing or signed out, `filed` is false and `url` is a prefilled new-issue link to open.",flags:[{name:"title",type:"string",required:!0,placeholder:"T",description:"Issue title."},{name:"body",type:"string",required:!0,placeholder:"TEXT",description:"Issue body."},{name:"kind",type:"string",default:DEFAULT_FEEDBACK_KIND,placeholder:FEEDBACK_KINDS.join("|"),description:"bug (label bug), idea (enhancement) or question."}],offline:!0,handler:feedback}];var GATE_VERBS=[{name:"gate-raise",group:"drive",summary:"Ask the OWNER a question only they can answer, instead of burying it in a MEMO or report. Opens a decision gate: it shows in their Inbox as `decision_needed` and in `gates`; their answer is pasted back into this tab as `[ROVE GATE g-N answered] \u2026`. Raising counts as ending your turn on a blocker \u2014 ask, then stop and wait for the answer. Returns { gate } with the `g-N` id.",flags:[F.taskId(!0),{name:"tab",type:"string",placeholder:"tab-N",description:"Tab the answer is pasted into. Default: the tab you are running in (when it is this task's), else the task's first engine tab."},{name:"question",type:"string",required:!0,placeholder:"TEXT",description:"The question, \u2264 500 chars. Make it answerable without opening the task."},{name:"option",type:"string",repeatable:!0,placeholder:"KEY=LABEL",description:"A choice the owner can pick, e.g. `--option a=ship-now --option b=wait`. Up to 6; omit for a free-text-only question."},{name:"recommend",type:"string",placeholder:"KEY",description:"The option key you recommend; shown marked in the owner's dialog."},{name:"context",type:"string",placeholder:"TEXT",description:"\u2264 4000 chars: why it matters and what each option costs."}],handler:gateRaise},{name:"gate-answer",group:"drive",summary:"Answer an open decision gate. The answer is saved first, then pasted into the gate's tab; with no live engine the gate is answered but `handoff: undelivered`, and the next `send` to that tab (or routine firing into it) carries it. A prompt typed into the TUI, or a TUI revive, does not. Pass --option (must be one of the gate's keys), --text, or both. Returns { gate, delivered }.",flags:[{name:"gate",type:"string",required:!0,placeholder:"g-N",description:"Gate id (from `gates`)."},{name:"option",type:"string",placeholder:"KEY",description:"The chosen option's key."},{name:"text",type:"string",placeholder:"TEXT",description:"Free-text answer, or a note to an option."}],handler:gateAnswer},{name:"gate-withdraw",group:"drive",summary:"Close a decision gate that no longer needs an answer (the question went moot), or an answer the engine never received and the owner no longer wants delivered. A delivered one cannot be withdrawn. Deleting a task closes its gates automatically.",flags:[{name:"gate",type:"string",required:!0,placeholder:"g-N",description:"Gate id."}],handler:gateWithdraw},{name:"gates",group:"read",summary:"List decision gates. Default: the open ones \u2014 questions waiting on the owner. `--status answered` shows answered gates, whose `.handoff` says whether the engine has the answer (`undelivered` = it rides the next `send` or routine firing into that tab).",flags:[F.repo(!1),F.taskId(!1),{name:"status",type:"enum",values:["open","answered","withdrawn","all"],default:"open",description:"Which gates to list."}],handler:gates}];var PROBE_MS=8000,VERSION_MS=4000;function parseVersion(tool,result){if(result.missing||result.code!==0)return null;return new RegExp(`${tool} version (\\S+)`).exec(result.stdout)?.[1]??null}function parseAuthStatus(stdout){let parsed;try{parsed=JSON.parse(stdout)}catch{return null}let hosts=parsed?.hosts;if(!hosts||typeof hosts!=="object")return null;let entries=hosts[GITHUB_HOST],list2=Array.isArray(entries)?entries:[],active=list2.find((e)=>e.active===!0)??list2[0];if(!active)return{account:null,protocol:null,scopes:[]};if(active.state!=="success"||typeof active.login!=="string")return{account:null,protocol:null,scopes:[],...typeof active.error==="string"?{detail:active.error.trim().slice(0,200)}:{}};let scopes=typeof active.scopes==="string"?active.scopes.split(",").map((s)=>s.trim()):[];return{account:active.login,protocol:active.gitProtocol==="ssh"||active.gitProtocol==="https"?active.gitProtocol:null,scopes:scopes.filter(Boolean)}}async function accountViaApi(run){let user=await run("gh",["api","user","-q",".login"],{timeoutMs:PROBE_MS,env:GH_ENV}),login=user.stdout.trim();if(user.code!==0||!login)return{account:null,protocol:null,scopes:[],...user.timedOut?{detail:"timed out"}:{}};let value=(await run("gh",["config","get","-h",GITHUB_HOST,"git_protocol"],{timeoutMs:VERSION_MS,env:GH_ENV})).stdout.trim();return{account:login,protocol:value==="ssh"||value==="https"?value:null,scopes:[]}}async function ghAccount(run){let res=await run("gh",["auth","status","--hostname",GITHUB_HOST,"--json","hosts"],{timeoutMs:PROBE_MS,env:GH_ENV});if(res.timedOut)return{account:null,protocol:null,scopes:[],detail:"timed out"};return parseAuthStatus(res.stdout)??await accountViaApi(run)}function parseGitUsesGh(configOutput){return configOutput.split(`
13
13
  `).some((line)=>/auth git-credential/.test(line))}function parseSshProbe(result){if(result.missing)return{ok:!1,detail:"ssh is not installed"};if(result.timedOut)return{ok:!1,detail:"timed out"};let text=`${result.stderr}
14
14
  ${result.stdout}`,hi=/Hi ([A-Za-z0-9-]+)! You've successfully authenticated/.exec(text);if(hi?.[1])return{ok:!0,user:hi[1]};let lines=text.split(`
15
15
  `).map((l)=>l.replace(/^git@github\.com: /,"").trim()).filter(Boolean);return{ok:!1,detail:(lines.find((l)=>/Permission denied|Host key verification failed|Could not resolve|Connection /.test(l))??lines[lines.length-1]??"ssh failed").slice(0,160)}}async function githubStatus(deps){let run=deps.run??runTool,[git,gh,helpers,ssh]=await Promise.all([run("git",["--version"],{timeoutMs:VERSION_MS}),run("gh",["--version"],{timeoutMs:VERSION_MS,env:GH_ENV}),run("git",["config","--get-regexp","^credential\\.(https://github\\.com\\.)?helper$"],{timeoutMs:VERSION_MS}),run("ssh",["-T","-o","BatchMode=yes","-o","StrictHostKeyChecking=yes","-o","ConnectTimeout=5",`git@${GITHUB_HOST}`],{timeoutMs:PROBE_MS})]),ghVersion=parseVersion("gh",gh),auth=ghVersion?await ghAccount(run):{account:null,protocol:null,scopes:[],detail:"gh is not installed"};return{git:parseVersion("git",git),gh:ghVersion,account:auth.account,...auth.account===null&&auth.detail&&ghVersion?{authDetail:auth.detail}:{},host:GITHUB_HOST,protocol:auth.protocol,scopes:auth.scopes,gitUsesGh:parseGitUsesGh(helpers.stdout),ssh:parseSshProbe(ssh),login:deps.login()}}var GH_MS=15000,TOKEN_SHAPE=/^[A-Za-z0-9_]{20,255}$/;async function setupGit(run){await run("gh",["auth","setup-git","--hostname",GITHUB_HOST],{timeoutMs:GH_MS,env:GH_ENV})}async function finishLogin(syncOthers,run=runTool){if(await setupGit(run),!syncOthers)return;let token=await readHubToken(run).catch(()=>null);if(!token)return;await autoSyncSignedOut(token)}var deviceLogin=new DeviceLogin({spawn:spawnGhLogin,finish:(syncOthers)=>finishLogin(syncOthers)}),localStatus=(run)=>githubStatus({...run?{run}:{},login:()=>deviceLogin.state()});async function readHubToken(run=runTool){let res=await run("gh",["auth","token","--hostname",GITHUB_HOST],{timeoutMs:GH_MS,env:GH_ENV}),token=res.stdout.trim();if(res.missing)throw new ApiError("gh is not installed on this machine","GITHUB_GH_MISSING");if(res.code!==0||!token)throw new ApiError("this machine is not signed in to GitHub","GITHUB_SIGNED_OUT",{hint:"connect GitHub here first (`rove api github-login`)"});return token}var scrub=(text,token)=>text.split(token).join("***").trim().slice(0,300);async function applyToken(token,run=runTool){if(!TOKEN_SHAPE.test(token))throw new ApiError("the token is not a GitHub token","BAD_FLAG");let res=await run("gh",["auth","login","--with-token","--hostname",GITHUB_HOST],{timeoutMs:GH_MS,env:GH_ENV,input:`${token}