CommunityWriting & Editinggithub.com

kingofwind/scheduler-workers

Concurrency-limited multi-task chain scheduler for CLI agents (opencode reference impl). Run long-running tasks (e.g. model training) in parallel under cap N, with task chains, exclusive/mutex segments, real-time human-in-the-loop, and resumable file-based state. Cross-agent file protocol.

What is scheduler-workers?

scheduler-workers is a OpenCode agent skill that concurrency-limited multi-task chain scheduler for CLI agents (opencode reference impl). Run long-running tasks (e.g. model training) in parallel under cap N, with task chains, exclusive/mutex segments, real-time human-in-the-loop, and resumable file-based state. Cross-agent file protocol.

Works with~Claude Code~Codex CLI~CursorOpenCode
npx skills add kingofwind/scheduler-workers

Installed? Explore more Writing & Editing skills: steipete/notion, affaan-m/seo, affaan-m/brand-voice · View all 6 →

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

scheduler-workers:并发上限调度的多任务链框架

概述与适用场景

主 agent 作为调度者,维护一个 while 事件循环:按并发上限 N 异步分离启动若干 worker(无头 opencode run 进程),worker 独立跑各自的任务链;主 agent 靠一个阻塞的"事件等待"bash 循环在"有 worker 提问 / 有 worker 结束"时唤醒自己,实时处理,再继续等。全部链完成后汇总退出。

关键点:worker 是分离进程,不是阻塞主 agent 的 Task 子会话。这是"实时转发 + 即时补位"能成立的前提(实测 opencode 的 Task 工具会把并行子任务结果整批返回,中途收不到单个完成通知;分离进程 + 文件事件彻底绕开)。

适用于:

  • 多个长时/独立任务需要并行调度(如多个模型训练脚本)。
  • 任务之间存在"改配置 + 启动"这类必须串行的互斥段。
  • 任务可能阻塞数小时(如训练 24h)。
  • 需要用户实时决策介入(人在环)。
  • 会话中断后需要断点续跑。

核心概念

  1. Chain(任务链):最小调度单位。一个有序步骤序列,由一个 worker 全权执行。有依赖关系的步骤打包进一条链;链之间才谈并行。
  2. Step(步骤):链内一个动作。类型:run / launch / wait / check / ask / report
  3. Exclusive Segment(互斥段):链内连续一段步骤,携带同一 exclusive 段名 key。worker 进入前必须持有该 key 的锁,直到段内最后一步完成才释放。锁的是"任务段"(如"改配置+启动"),不是单个资源文件。默认全项目用同一段名 → 同一时刻全局只有一个互斥段在执行;不同段名可并行。
  4. 并发上限调度 + 事件循环:主 agent 维护 pending 队列与 running 集合,running ≤ N 恒成立;靠事件循环在 worker 完成时即时补位、在 worker 提问时实时转发
  5. Worker(分离进程):默认是 Start-Process 启动的无头 agent workeropencode run --agent build --auto);主 agent 判断链为纯确定性步骤时可降级为 脚本 workerscripts/worker.ps1,省 token、更快)。两种 worker 遵守同一协议(写标记/事件、ask 走 messages 消息、互斥段走锁)。
  6. Marker(标记文件).task-runner/runs/<chain_id>.json,链的唯一权威状态,append-only 记录每步时间戳;续跑依据。
  7. Lock(锁).task-runner/locks/<段key>.lock,跨进程文件锁(原子创建)。
  8. Messages(消息).task-runner/messages/<chain_id>.<type>.json,worker 与主 agent 之间的统一信令通道。消息类型:question(worker→主,提问)、answer(主→worker,回答)、done/failed(worker→主,终态)。文件持久,不丢事件。

链定义格式

主 agent 调度前,把每条链的定义写成一个 JSON 文件(放在 .task-runner/defs/<chain_id>.json),启动 worker 时只传文件路径,避免命令串里夹带被权限拒绝的 token。字段规则:

  • chain_id:唯一标识,也用作标记/消息文件名。
  • timeout_sec(可选):整条链超时,超时记 failed
  • completion(可选,链级默认完成信号):process_exit(默认)| marker_file | log_keyword | 组合。wait / check 步骤可覆盖。
  • steps[]:有序执行。步骤类型:
    类型语义
    run前台执行命令并等待其返回(无互斥锁时可 N 路并行)
    launch后台启动进程,返回 PID 存为步骤结果,供 wait.pid_from 引用;可选 log 记录日志路径
    waitcompletion 规则阻塞等待(见"长阻塞与完成信号")
    checkcompletion 规则校验证据一次,不满足 → 链 failed 并释放锁
    ask人在环:写 question 消息 → 轮询 answer 消息(带超时)→ 读答案继续;超时 → 链 failed
    report更新标记为 done/failed、写入摘要,返回 report
  • exclusive: "<key>":该步骤属于互斥段 key连续且同 key 的步骤构成一个互斥段:进入首个步骤前加锁、最后一个步骤后释放。

示例:

{
  "chain_id": "train-a",
  "timeout_sec": 90000,
  "completion": { "type": "process_exit" },
  "steps": [
    { "name": "prepare", "run": "python prep.py --input raw" },
    { "name": "edit-cfg", "exclusive": "train",
      "run": "Set-Content -Path config.yaml -Value 'epochs: 100'" },
    { "name": "launch", "exclusive": "train",
      "launch": "python train.py --cfg config.yaml",
      "log": ".task-runner/runs/train-a.log" },
    { "name": "wait", "wait": { "pid_from": "launch", "timeout_sec": 86400 } },
    { "name": "confirm", "ask": "训练完成,是否继续执行评估?" },
    { "name": "verify", "check": { "type": "marker_file", "path": "runs/train-a/done.txt" } },
    { "name": "done", "report": true }
  ]
}

调度协议(主 agent 事件循环)

  1. 建队列:把所有链定义写成 .task-runner/defs/<chain_id>.json,标 pending
  2. 确定 N:执行前询问用户并行度(并发上限)。
  3. 分离启动前 N 条:每条链一个分离 worker(见下),记录 chain_id、PID、发起时刻,加入 running
  4. 事件等待:执行一次阻塞的"事件等待"bash(轮询 messages + 标记,事件出现即 break 返回,参考脚本见下)。
  5. 处理事件
    • QUESTION <id>:读 .question.json → 用 question 工具实时问用户 → 写 .answer.json → 回到第 4 步。
    • DONE/FAILED <id>:该链记 done/failed,从 running 移除 → 若 pending 非空,立即分离启动下一条(即时补位)→ 回到第 4 步。
    • DEAD <id>:worker 进程已死但链未终态 → 按中断续跑处理(见下)。
  6. 汇总退出pending 空 且 running 空 → 输出汇总表(chain_id / 状态 / 自测时长 / 墙钟时长 / 摘要)→ 退出。

分离启动 worker(命令串只含路径/ID,不含被权限拒绝的 token):

Start-Process -FilePath "opencode" -ArgumentList "run","--agent","build","--auto",
  "--dir",(Get-Location).Path,
  "读取链定义文件 .task-runner/defs/<chain_id>.json,严格按 scheduler-workers 协议执行该链,写标记,ask 步骤写 question 消息并等待 answer,完成后写 done/failed 消息并返回 report。" `
  -RedirectStandardOutput ".task-runner/logs/<chain_id>.out.log" `
  -RedirectStandardError  ".task-runner/logs/<chain_id>.err.log" `
  -WindowStyle Hidden -PassThru

事件等待参考脚本(单条阻塞 bash,事件出现即打印并退出;主 agent 处理后再次调用):

# wait-event.ps1 —— 打印待处理事件并退出(messages/ 目录统一消息类型)
$msg = ".task-runner\messages"; $runs = ".task-runner\runs"
while ($true) {
  $qs = @(); $ts = @(); $ds = @()
  # 1) 待答问题(有 question 无 answer)
  Get-ChildItem $msg -Filter *.question.json -ErrorAction SilentlyContinue | ForEach-Object {
    $id = $_.BaseName -replace '\.question$',''
    if (-not (Test-Path (Join-Path $msg ($id + '.answer.json')))) { $qs += $id }
  }
  # 2) 终态(done/failed 消息,文件名即类型)
  Get-ChildItem $msg -Filter *.done.json -ErrorAction SilentlyContinue | ForEach-Object { $ts += ($_.BaseName -replace '\.done$','') + ":done" }
  Get-ChildItem $msg -Filter *.failed.json -ErrorAction SilentlyContinue | ForEach-Object { $ts += ($_.BaseName -replace '\.failed$','') + ":failed" }
  # 3) 死亡 worker(running 但 pid 已死)
  Get-ChildItem $runs -Filter *.json -ErrorAction SilentlyContinue | ForEach-Object {
    $m = Get-Content $_.FullName -Raw -Encoding UTF8 | ConvertFrom-Json
    if ($m.status -eq 'running' -and $m.pid -and -not (Get-Process -Id $m.pid -ErrorAction SilentlyContinue)) { $ds += $m.chain_id }
  }
  if ($qs.Count -gt 0 -or $ts.Count -gt 0 -or $ds.Count -gt 0) {
    foreach ($q in $qs) { Write-Output ("QUESTION " + $q) }
    foreach ($t in $ts) { Write-Output ("TERMINAL " + $t) }
    foreach ($d in $ds) { Write-Output ("DEAD " + $d) }
    break
  }
  Start-Sleep -Seconds 2
}

说明:消息脚本只"发现"不"消费";终态消息(done/failed)由主 agent 在处理后删除对应文件去重。文件持久,事件不丢。

Worker 执行协议

Worker 有两种,主 agent 按链的性质选择,两者遵守同一协议:

  • agent worker(默认)Start-Process opencode -ArgumentList "run","--agent","build","--auto","<读 def 文件并按协议执行该链>"。适合含判断/决策的步骤(改配置怎么改、读日志怎么做决策)。
  • 脚本 worker(降级)Start-Process powershell -ArgumentList "-NoProfile","-File","<skill>/scripts/worker.ps1","-Def","<def>"。适合纯确定性步骤(固定命令 + sleep/ask),省 token、更快。
  1. .task-runner/defs/<chain_id>.json,写标记 status=runninglaunched_atpid(用 bash 内 $PID)。
  2. 顺序执行步骤;每步开始/结束追加一条 steps[](name + started_at + finished_at + result)→ append-only
  3. 互斥段:段内所有步骤 + acquire + release 放在同一条内联 bash 脚本里一次执行(因每次 bash 调用是全新 PowerShell 进程,函数定义不跨调用保留),release 放 try/finally 保证失败也释放。
  4. launch 记录 PID 与日志路径到标记。
  5. wait / check 按完成信号阻塞/校验。
  6. ask:写 .task-runner/messages/<chain_id>.question.json → bash 轮询 .answer.jsonStart-Sleep + Test-Path,带超时)→ 读答案继续。
  7. 收尾:更新标记 status=done/failedfinished_atsummary;写 .task-runner/messages/<chain_id>.done.json.failed.json;返回 report。

锁协议(L1,PowerShell 助手,内联使用)

锁文件 .task-runner/locks/<段key>.lock,内容 { "pid": <持有者PID>, "started_at": <时间> }。原子创建(CreateNew),已存在则等待;持有者进程已死则判陈旧锁、接管。用 .NET IO 实现,避免触碰全局 Remove-Item/New-Item 拒绝:

function Acquire-SegmentLock {
  param([string]$Key, [int]$TimeoutSec = 600)
  $lockDir = ".task-runner\locks"
  [System.IO.Directory]::CreateDirectory($lockDir) | Out-Null
  $lock = Join-Path $lockDir ($Key + ".lock")
  $deadline = (Get-Date).AddSeconds($TimeoutSec)
  while ((Get-Date) -lt $deadline) {
    try {
      $fs = [System.IO.File]::Open($lock, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write)
      $j = @{ pid = $PID; started_at = (Get-Date -Format o) } | ConvertTo-Json
      $b = [System.Text.Encoding]::UTF8.GetBytes($j)
      $fs.Write($b, 0, $b.Length); $fs.Close()
      return $true
    } catch [System.IO.IOException] {
      if (-not (Test-Path $lock)) { continue }
      $owner = Get-Content $lock -Raw | ConvertFrom-Json
      if (-not (Get-Process -Id $owner.pid -ErrorAction SilentlyContinue)) {
        [System.IO.File]::Delete($lock)   # 陈旧锁:持有者已死,接管
        continue
      }
      Start-Sleep -Seconds 1
    }
  }
  return $false                            # 超时未拿到 → 链 failed
}

function Release-SegmentLock {
  param([string]$Key)
  $lock = Join-Path ".task-runner\locks" ($Key + ".lock")
  if (Test-Path $lock) { [System.IO.File]::Delete($lock) }
}

锁语义:锁覆盖"改配置 → 启动"整段;A 持锁期间 B 的同 key 互斥段等待;wait/check/ask/report 等非互斥步骤不受影响。

标记文件约定

  • 路径:.task-runner/runs/<chain_id>.json
  • 结构:
    {
      "chain_id": "train-a",
      "status": "pending|running|done|failed",
      "launched_at": "ISO8601|null",
      "finished_at": "ISO8601|null",
      "pid": 12345,
      "log_path": ".task-runner/runs/train-a.log",
      "completion": { "type": "process_exit" },
      "steps": [
        { "name": "edit-cfg", "started_at": "...", "finished_at": "...", "result": "ok" }
      ],
      "summary": null
    }
    
  • append-only:每步完成追加一条 steps[];只更新 status / finished_at / pid,不覆盖步骤历史。

Messages 消息协议(人在环实时交互)

统一消息目录 .task-runner/messages/,文件名 <chain_id>.<type>.json

  • question:worker ask 步骤写({"chain_id","question"}),然后轮询 .answer.json
  • answer:主 agent 事件循环检测到 QUESTION → question 工具实时问用户 → 写({"answer":"..."})。
  • done / failed:worker 收尾时写(终态信号,文件名即类型)。
  • worker 读到 .answer.json 后清理 question/answer 两个文件(用 [System.IO.File]::Delete,避免 Remove-Item 拒绝);主 agent 处理完 done/failed 后删除对应消息。
  • 消息靠文件持久,worker 无论何时提问都能被主 agent 稍后检测到(不丢);主 agent 不在场时,用户也可直接改 .answer.json

长阻塞与完成信号

  • wait 步骤 = 单条 bash + 显式大 timeout(已验证 300s 调用可配 400000ms 不中断),阻塞期 0 模型推理。
  • completion.type
    • process_exitWait-Process -Id <pid>,或轮询 Get-Process -Id <pid>
    • marker_file:轮询 Test-Path <path>
    • log_keyword:轮询 Select-String -Path <log> -Pattern <kw>
    • 组合:全部满足才算完成。
    • 轮询用 bash 内 while + Start-Sleep 60,保持单次调用阻塞。
  • check = 校验一次,不满足 → 链 failed(释放锁);waittimeout_sec → 链 failed(释放锁)。

中断续跑

  • 主 agent 重启后扫描 .task-runner/runs/*.jsonstatus=runningpid 已死 → 读标记 → 重新分离启动一个 worker 从 steps[] 最后未完成步骤续跑(wait 步骤直接重新阻塞等原进程/信号)。
  • 锁与续跑:持锁进程已死时,新 worker 走 Acquire-SegmentLock 的陈旧锁逻辑自动接管。

权限清单(项目 opencode.jsonc,主 agent 与子/worker 统一生效)

一键复制的权限示例见 install/opencode.jsonc.permission.example

  • 放行(中风险,框架核心能力):Start-ProcessWait-ProcessSet-ContentAdd-ContentSystem.IO.File(子串匹配,兼容复合写法)。
  • 放行(低风险):Start-SleepGet-ProcessGet-DateConvertTo-JsonConvertFrom-JsonSystem.IO.Directorypowershell(主 agent 运行事件循环脚本)。
  • headless worker 用 --auto:自动批准未显式拒绝的权限,避免无头进程卡在权限确认上(deny 规则仍生效)。
  • 不额外放行(保持全局 deny):Remove-ItemClear-Contentrm -rfcurl/iwr/irmgit push 等;锁/清理统一走 [System.IO.File]::Delete
  • 命令串按字符串匹配:不要把被拒 token(如 Set-Content 字面量)写进主 agent 的启动命令里,指令放文件、命令串只引用文件名。

时长口径

  • worker 自测:launched_at → finished_at(纯执行)。
  • 主 agent 墙钟:分离启动时刻 → 收到终态事件时刻。
  • 报告输出:每链 chain_id / 状态 / 自测时长 / 墙钟时长 / 摘要

自检清单

可执行的验证用例见同目录 TEST.md(TC1 并行/并发上限、TC2 实时交互+补位+正确性、TC3 互斥段、TC4 中断续跑),含逐条通过标准与判定总表。

  1. 并行性:N 路 worker 并行窗口重叠,总墙钟 ≈ max + 初始化。
  2. 长阻塞:单条 bash 300s + 大 timeout 不被中断。
  3. 并发上限 + 即时补位:worker 结束即补下一条,running ≤ N
  4. 互斥段:两个 worker 同 key 互斥段不重叠。
  5. 人在环实时:worker ask 写 question → 主 agent 事件循环秒级唤醒 → question 实时转发 → 答案回流。
  6. headless worker:opencode run --agent build --auto 能读项目配置、写文件、返回结果。
  7. 中断续跑:读标记续跑、陈旧锁接管。

示例与自测

本 skill 附带可直接运行的参考实现与示例(scripts/examples/ 子目录,随 skill 一起分发):

  • scripts/worker.ps1:参考 worker(读链定义 → 执行 sleep/ask 步骤 → 写标记/事件)。
  • scripts/wait-event.ps1:主 agent 的事件等待脚本(阻塞,事件出现即打印并退出)。
  • examples/a.json ~ d.json:四条示例链定义(sleep + 可选 ask)。

冒烟测试流程(验证实时交互 / 补位 / 正确性,N=2):

  1. 清空 .task-runner/{runs,messages}
  2. Start-Process powershell -File scripts/worker.ps1 -Def examples/a.json 启动 a、b;主 agent 循环跑 powershell -File scripts/wait-event.ps1
  3. QUESTIONquestion 工具问用户 → 写 <id>.answer.json;遇 TERMINAL → 删除事件文件并补位下一条;直到全 done。
  4. 检查 runs/*.jsonsteps[] 时间戳与 result(答案回传)。

examples/ 里 ask 文案用英文,只为规避中文在不同编码下的显示/解析噪音;真实链可放心用中文(脚本已统一 -Encoding UTF8)。

实现注意(踩坑记录)

  1. PowerShell 变量大小写不敏感$def 与参数 $Def 是同一个变量,互相覆盖会让解析结果变回字符串。脚本内变量名避开与参数同名(用 $cfg)。
  2. UTF-8 无 BOM 文件读取write 工具/编辑器写的是 UTF-8 无 BOM;PowerShell 5.1 Get-Content 默认按系统 ANSI(中文 Windows 为 GBK)解码,中文会乱码、JSON 解析失败。链定义 / messages / 标记一律显式 -Encoding UTF8 读写。
  3. 权限按命令字符串匹配:复合单行(如 $p = Start-Process ...)不以放行 cmdlet 开头会被前缀规则误拒,故配置用子串匹配(*Start-Process* 等);主 agent 的启动命令里不要夹带被拒 token 字面量(指令放文件、命令串只引文件名)。
  4. opencode Task 工具结果批量交付:并行子任务结果等整批结束才返回,主 agent 中途收不到单个完成;子 agent 也无 question 工具 → 改用"分离进程 + 文件事件 + 事件循环"绕开(本 skill 的核心设计)。
  5. 每次 bash 调用是全新 PowerShell 进程:函数定义不跨调用保留,互斥段必须把"函数定义 + acquire + 段内步骤 + release"内联到同一条 bash 脚本执行。
  6. 主 agent 是单线程事件循环:一次只处理一个事件(问用户 或 补位);多事件同时到达按序处理、靠文件持久不丢,但补位/转发有串行延迟。若将来要"无人值守 + 完全即时补位",可把事件循环下沉为独立监督进程(方向 3),skill 协议不变。

Related Skills

steipete/notion

Notion CLI/API for pages, Markdown content, data sources, files, comments, search, Workers, and raw API calls.

community

affaan-m/seo

Audit, plan, and implement SEO improvements across technical SEO, on-page optimization, structured data, Core Web Vitals, and content strategy. Use when the user wants better search visibility, SEO remediation, schema markup, sitemap/robots work, or keyword mapping.

community

affaan-m/brand-voice

Build a source-derived writing style profile from real posts, essays, launch notes, docs, or site copy, then reuse that profile across content, outreach, and social workflows. Use when the user wants voice consistency without generic AI writing tropes.

community

affaan-m/crosspost

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

community

affaan-m/x-api

X/Twitter API integration for posting tweets, threads, reading timelines, search, and analytics. Covers OAuth auth patterns, rate limits, and platform-native content posting. Use when the user wants to interact with X programmatically.

community

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

community