Skip to content

feat(workflow): add session discovery and managed public leases - #1304

Merged
SaulMoro merged 245 commits into
Tencent:mainfrom
nateEc:feat/1086-session-workflows
Oct 26, 2026
Merged

SaulMoro merged 245 commits into
Tencent:mainfrom
nateEc:feat/1086-session-workflows

Conversation

@nateEc

@nateEc nateEc commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A coding session runs team workflows with --driver skill. The agent orchestrates. A file-backed state machine in the CLI drives the public lease loop and answers literal commands. The engine keeps the graph.

agent (workflow skill)                     teamai workflow ... --driver skill
  start --describe <name>          ->      describe: ask | start | local | denied
  start <name> ...                 ->      first turn
  loop: run turn.then              ->      tick: lease, dispatch, settle, gate, done
    dispatch -> child agent / inline       next --prepare (worktree or --adopt host place)
    command  -> detached runner            env scoped to requires.env, TEAMAI_MODE exported
    person   -> ask, respond               report: commit, push, clean up
    judging  -> score --next
src/workflow/
├── drivers/skill/   # state machine: rows R/D/C/K/H/S/J/X, runners, briefs, host profiles h1-h4
├── place/           # TeamAI owns the workspace contract, the host owns the place
├── discovery/       # generated workflow-<name> skills
├── protocol.ts      # one prepare path; the invoke check for start, resume, takeover
└── attempt-marker.ts# nested_workflow guard, agent evidence
src/launch-context.ts# resolveRunMode: TEAMAI_MODE > CI > attended
skill-data/workflow/ # the orchestrator guide the CLI serves

Behaviour changes a user can see:

  • invocable becomes invoke: allow | ask | deny, default deny, plus alias. invoke limits agents only:

    invoke Agent session (any driver; --driver skill always counts) A person's terminal, local driver CI without an agent session, local driver Sub-workflow
    allow Starts Starts Starts Starts
    ask Starts once the person agrees (describe, then --confirmed). Unattended: denied, also with --confirmed Starts Starts Starts
    deny, the default Refused Starts Starts Starts

    resume and next --takeover follow the same rule, except for a run the calling session started and drives.

  • pull generates one workflow-<name> skill per allow/ask workflow; push refuses generated and CLI-owned skills.

  • workflow start fails with nested_workflow inside an attempt or a workflow worktree; both drivers export the marker to every process they start.

  • Run modes: TEAMAI_MODE (attended/unattended) > CI > attended. An unattended describe answers the recommended start with --auto-approve, or denied; an unattended resume or takeover decides the same way from the steps the run has left, before it claims anything. Every process TeamAI starts for a step gets TEAMAI_MODE. An unattended local run under a terminal asks nothing.

  • A run published elsewhere continues from a session with teamai workflow resume <run> --driver skill [--claim].

  • The local driver honours --auto-approve in a terminal. respond from an agent session counts as the agent's answer, so required gates refuse it.

  • score --next {run} for an unknown run answers run_not_found. workflow report or next given a judge lease answers judge_lease and names teamai score. can.env: 'commands' declares scoped env for command steps and setup only.

Known limits, recorded in the docs:

  • The pre-approved teamai workflow next prefix in the generated skill also covers --takeover and --takeover --confirmed.
  • A takeover cannot continue workspace copies this machine's local driver opened; it hands them to the person.
  • A host place whose setup failed or was cut short is refused on re-adoption until the host makes it again.
  • A child that reports by hand through report --json instead of its brief's command still settles the step, but the session's journal shows that step released.

Type of Change

  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature causing existing behavior to change): a workflow without invoke now refuses a start, resume or takeover from an agent session
  • Bilingual guides, CLI-served skills and the generated command reference

Test Plan

  • npx tsc --noEmit passes
  • npm run lint passes
  • npx vitest run passes
  • Added/updated tests for the change: the transition table cell by cell, a model-based test of the state machine, places, run modes, invoke, claim

Head b2f115c02, built with npm run build, macOS, provider git.

Check Result
npx tsc --noEmit, npm run lint pass
npx vitest run 572 files, 16386 passed, 21 skipped
npm run test:e2e on the workflow files (session-1086, skill-claim, publish-resume, nested-start, conformance, prompt, workspaces, inline, command) 12 files, 223 passed
npm run test:e2e on workflow-signals, env-exec-signals, score-external-driver 6, 6 and 3 passed
workflow-skill-model (property test), 10 runs 10/10 passed

Real CLI, built dist. Members alice and bob use separate homes, sharing local bare team and business origins. Agent-session commands set only CLAUDE_CODE_SESSION_ID.

A session claims a run another install stopped mid-step and drives it to done
$ [alice] teamai workflow start relay --driver local       # Ctrl-C while build runs
$ [alice] teamai workflow publish <run>
  exit 0; publication published, inFlight [build, local]
$ [bob, agent session] teamai workflow resume <run> --driver skill --claim
  exit 0; turn driving, actions [choose host]
$ [bob, agent session] teamai workflow next <run> --driver skill --choice h1
  exit 0; events [started build, settled build]
$ [bob, agent session] teamai workflow next <run> --driver skill
  exit 0; actions [dispatch notes]
$ [bob, agent session] teamai workflow next <run> --driver skill --ack dispatch.<attempt>
$ [bob, agent session] teamai workflow report <run> --driver skill --attempt <attempt>
  exit 0; runner reported
$ [bob, agent session] teamai workflow next <run> --driver skill
  exit 0; turn done, events [settled notes]
$ [alice] teamai workflow resume <run>                      exit 1; not_owner
$ [alice, agent session] teamai workflow resume <run> --driver skill --parallel 9
  exit 1; invalid_arguments
invoke: deny on resume: an agent session is refused, the person's terminal claims
alice stopped the run with SIGINT and published it, build in flight
$ [bob, agent session] teamai workflow resume <run> --driver skill --claim
  exit 1; invoke_denied: Workflow "locked" has invoke: deny, so no coding agent continues it, with any driver, and this
  command runs inside an agent session (CLAUDE_CODE_SESSION_ID is set). The person can continue it from their own
  terminal with teamai workflow resume <run>. ...
  owner unchanged (alice); bob's run directory is empty
$ [bob] teamai workflow resume <run> --claim
  exit 0; driving; owner bob
Unattended loop (TEAMAI_MODE=unattended): describe recommends --auto-approve, the loop reaches done without asking
$ teamai workflow start --describe ship --driver skill
  ask: start, "teamai workflow start ship --driver skill --auto-approve"
$ teamai workflow start ship --driver skill --auto-approve
  events [started build, settled build success]
$ teamai workflow next <run> --driver skill --wait
  turn done succeeded, events [started ship, settled ship success]
skill.json options {"parallel":1,"hitl":"auto-approve","mode":"unattended"}
Unattended resume --claim with the skill driver: refused before the claim when a person is needed, counted from the steps left (this head)
A: alice stops a run of gated mid-build and publishes it; the required approval go-live is still ahead
$ [bob, TEAMAI_MODE=unattended] teamai workflow resume <run A> --driver skill --claim
  exit 1; unattended_denied: Workflow gated needs a person, and this run is unattended (TEAMAI_MODE or CI says so):
  only a person can answer the approval at go-live. ...
  owner after: alice, unchanged; bob's run directory: []
B: alice's session drives gated, alice answers go-live from her terminal, check is in flight; alice publishes
$ [bob, TEAMAI_MODE=unattended] teamai workflow resume <run B> --driver skill --claim
  exit 0; driving, events [started check, settled check]; owner after: bob
$ [bob, TEAMAI_MODE=unattended] teamai workflow next <run B> --driver skill --wait
  exit 0; done
  skill.json options {"parallel":1,"hitl":"auto-approve","mode":"unattended"}
The same script on the build before this change: A leaves bob owning the run; B is refused unattended_denied, and bob owns it.
Done runs: a retry during the end's publication, an end that failed to publish, a late PR receipt (at d373ea5; the done path has not changed since)
race-retry: a retry lands while the tick that ends the run publishes it
  session nexts after the retry: driving (poll), done succeeded, done succeeded; run file succeeded, revision 4
offline-end: the push to teamai-reports fails when the run ends
  online again: session next -> done succeeded, teamai-reports says succeeded
late-pr-recovery: the delivering process is killed after the host opened the PR, then the run is cancelled
  next skill call: done cancelled, prs [https://github.com/acme/business/pull/1]; receipt recorded, create resolved
done-poll: three nexts of an unchanged done run
  git calls against teamai-reports: 0, 0, 0
A user-scope run started in a project that joined another team: judge it from there (this head)
the app joins team acme (with review) in user scope, and team other (without it) in project scope
$ teamai workflow start review                 run in the user scope; driven to done by an external driver
$ teamai score --next {driver, run}            lease, cwd: the app
$ teamai score --lease <ref> --submit --stdin  from the lease's cwd: exit 0; label thorough (was lease_unknown)

workflow publish of such a run, from the same project, is covered by a unit test that publishes it to a bare origin (it answered run_not_found before).

Live agents, three workflows each (hm-flow: parallel prompt steps, a command step with a secret, a required and an optional approval, judging; hm-ask: invoke: ask; hm-cancel: cancelled by the person mid-step):

Host hm-flow hm-ask hm-cancel Notes
Claude Code, native subagents succeeded succeeded cancelled go-live answered by the person (channel=tty agent=false); no secret read, no git in a place, no step work by the orchestrator
Codex, spawned agents succeeded succeeded cancelled same; one child, whose teamai in a worktree resolved to an older install, reported through report --json by hand instead of its brief's command

Not covered here: GitLab and GitHub providers, CodeBuddy and OpenCode hosts (left to CI); Windows.

Related Issues

Fixes #1086

Notes for Reviewers

Merge danger, door: two-way

invoke and alias are new workflow keys: teamai before 0.28.0 drops a workflow that sets either, and review --branch warns. Nothing migrates stored state; skill.json is new.

Blast radius: workflows

Every workflow without invoke now refuses a start from an agent session. Local-driver runs from a terminal, CI and sub-workflows are unchanged. The shared place module now also routes the local driver's prepare, so a regression there reaches local runs.

Review suggestions left as they are, on purpose:

  • An unattended call that passes --hitl person keeps it: it is the caller's explicit choice, and refusing it would change the options start accepts.
  • An unattended resume of a cancelled or failed run still counts the steps it has left: a retry reopens such a run, and those steps would then ask a person nobody is.
  • score --lease resolves the scope from the directory it runs in, then the user scope. A lease names the cwd to submit from, its run's checkout, as --next's help and the lease_unknown message say.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Preserve interactive prompt write permissions — src/workflow/local-driver.ts:1041: Prepare an interactive: true prompt step without a workspace, edit a repository file, then report success. Preparation always retains a snapshot, and reporting unconditionally checks it, converting valid interactive edits into read_only_dirty failure. The existing local driver explicitly permits these edits when interactive && !readOnly. Retain those flags in the preparation record/schema and apply the same exemption.

The PR description documents sufficient testing, including a representative build-first real-CLI round trip. No testing-record finding. I only read the diff; I did not run, build, or install PR code.

@github-actions

Copy link
Copy Markdown

No new findings in the specified diff.

  • Resolved: The earlier interactive-permission finding is fixed: preparation retains interactive/readOnly, and reporting exempts interactive edits unless explicitly read-only.
  • Testing: The PR description documents sufficient testing, including a build-first representative real-CLI round trip. No testing-record finding.

I only read the diff and trusted base review criteria; I did not run, build, or install PR code.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

@nateEc Thanks. This branch conflicts with main in src/workflow/types.ts and src/__tests__/workflow-records.test.ts. Main caused it: #1234 (2401d047) added workflow_namespace_conflict and prompt_namespace_conflict to the WorkflowDiagnostic['code'] union and to the diagnostics list in the records test, on the same lines this PR extends. Keeping both sets of codes should resolve it. We'll review the rebased head. #1303, #1304 and #1306 also share most of their workflow files (types.ts, parse.ts, local-driver.ts, src/index.ts, the workflow guides), so whichever lands first will need the other two rebased again.

@SaulMoro

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

@nateEc an update to the request for changes: the conflict with main is now wider than the two files it named. #1280 (publish and resume workflow runs across machines) landed and touches the same engine. This head now conflicts in src/index.ts, src/workflow-commands.ts, src/workflow/types.ts, docs/dev/workflow-engine.md, commands.md and workflow-records.test.ts. Please rebase onto current main. Keep #1234's workflow_namespace_conflict and prompt_namespace_conflict codes and #1280's publish/resume state, then rerun the real-CLI record on the result.

- Add invocable workflow metadata, unambiguous aliases, bounded SessionStart discovery, a CLI-served workflow skill, and input-schema inspection without creating a run.
- Resolve run ids across configured local scopes independently of cwd, reject duplicate ids, and bind configuration and workspace preparation to the run's retained original checkout.
- Reuse local base-overlay, worktree, commit and push helpers through next --prepare and report; persist only preparation paths/fingerprints, retain edits on handback, and reject cancelled or mismatched reports before git delivery.
- Keep graph scheduling in the engine and required approvals on the existing direct-person channel; update bilingual docs and generated help, with workflow/hook/skill tests, lint/typecheck/build and a real-CLI discovery-to-push round trip.
- Retain interactive and explicit read-only flags in durable preparation data,
  then apply the local driver's interactive write exemption during reporting.
  Explicit read-only sessions continue to reject repository edits.
- Add prepared-session regressions for both interactive modes and extend the
  real CLI session round trip to preserve edits made in the active checkout.
- Update skill-serving catalog and recall-gate expectations for the new
  workflow skill, fixing the failed fork-safe CI shard without weakening checks.
- Verify all five session-driver tests, nine build-first real CLI/session/skill
  serving cases, typecheck and lint.
- 将会话驱动迁移到主分支共享 DriverSchema,保留发布与恢复的运行锁、命名空间诊断码及会话续接状态。
- 本机尚无运行文件时允许 next claim 和 resume 继续进入远程认领流程,避免本地发现逻辑提前拒绝它们。
- 新 next claim 回归在修复前失败、修复后通过;较大范围 1405 项 workflow 测试和最终命令文件 17 项通过。
- 6 个真实 CLI 会话、跨机器发布与恢复用例及构建、类型检查和 lint 通过;重新生成命令参考。
@nateEc
nateEc force-pushed the feat/1086-session-workflows branch from 1c5c63a to 73b8835 Compare October 24, 2026 17:37
@nateEc

nateEc commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review follow-up at 73b8835747b7cf5114dd279461cf08d3876becbc on current main.

  • Rebased onto main while keeping feat(push): support all namespaced resource types #1234 diagnostic codes and feat(workflow): publish and resume workflow runs across machines (#1084) #1280 publish/resume, driver locks, session continuation and current shared DriverSchema. Regenerated the CLI reference.
  • Preserved remote claim before a local run file exists for both workflow next with claim: true and workflow resume. A new next-claim regression failed before the compatibility fix and passes afterwards.
  • Broad workflow regression: 25 files / 1,405 tests passed. After the small claim fallback fix, the final command file passed 17/17.
  • Built-CLI session-workflow plus cross-machine publish/resume E2E files: 6 passed, covering outside-checkout preparation/reporting, direct HITL approval, remote ownership, detached bases, shallow clones and archived-session continuation.
  • Build, type checking and lint passed.

The PR description contains the updated verification record. Ready for another review.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Treat rejected pushes as delivery failures — src/workflow/local-driver.ts:1200: finishWorkspace expects a failed flag, but receives report.outcome === 'success'. If a prepared writer reports success and origin rejects its push, the helper returns unpushed instead of a failure; this wrapper discards that warning and reports success, potentially authorizing final-worktree cleanup despite undelivered commits. Pass report.outcome === 'failed' and preserve the delivery warning for already-failed reports.

  • Resolved: The earlier interactive-permission finding remains fixed.

  • Testing: The description documents sufficient build-first real-CLI verification. No testing-record finding.

I only read the specified diff and trusted base review criteria; I did not run, build, or install PR code.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nateEc Thanks for this work. We are taking over #1303, #1304 and #1306 to speed up delivery. We will push the remaining changes to your branches, bring them up to date with main, and keep you as co-author in the squash commit. #1303 and #1306 go first, and #1304 follows once both have merged.

Please hold off on pushing to these branches in the meantime. All three stay blocked until we finish.

- 按工作区完成接口的失败标志处理会话报告,推送被拒绝时保留分支、提交和原始执行错误。
- 恢复跨检出目录 resume 的当前目录回退,确保缺失基础提交和合并提交仍执行远端校验。
- 新增成功与失败报告的拒绝推送回归;24 项相关单测、7 项 Git CLI E2E、构建、类型检查和 lint 通过。
@nateEc

nateEc commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review follow-up at 9537a12f9ab64d314fb31335bbd7f7c454969c83 on current main.

  • Rebased onto main while keeping feat(push): support all namespaced resource types #1234 diagnostic codes and feat(workflow): publish and resume workflow runs across machines (#1084) #1280 publish/resume, driver locks, session continuation and current shared DriverSchema. Regenerated the CLI reference.
  • Preserved remote claim before a local run file exists for both workflow next with claim: true and workflow resume. A new next-claim regression failed before the compatibility fix and passes afterwards.
  • Broad workflow regression: 25 files / 1,405 tests passed. After the small claim fallback fix, the final command file passed 17/17.
  • Built-CLI session-workflow plus cross-machine publish/resume E2E files: 6 passed, covering outside-checkout preparation/reporting, direct HITL approval, remote ownership, detached bases, shallow clones and archived-session continuation.
  • Build, type checking and lint passed.
  • Addressed the new rejected-push finding: prepared session writers now pass the correct failure flag to workspace finalization, preserve original tool failures with an unpushed warning, and retain rejected commits. Both new success/failure report regressions failed before the correction.
  • Fixed the CI Git-conformance regression by restoring the caller checkout as the resume fallback when no same-machine driver workspace applies. Final validation: 24 focused unit tests, all 7 built-CLI Git-conformance cases (including both previously failing missing-commit checks), build, types and lint passed.

The PR description contains the updated verification record. Ready for another review.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nateEc Thanks for this work. We are taking over these three PRs to speed up delivery:

We will push the remaining changes to your branches, bring them up to date with main, and keep you as co-author in the squash commit. #1303 and #1306 go first, and #1304 follows once both have merged.

Please hold off on pushing to these branches in the meantime. All three stay blocked until we finish.

@github-actions

Copy link
Copy Markdown

No new findings in the specified diff.

  • Resolved: Interactive write permissions remain fixed; preparation retains interactive/readOnly, and reporting applies the existing exemption.
  • Resolved: Rejected pushes now fail successful reports; already-failed reports retain their original failure with an unpushed warning.
  • Testing: The PR description includes sufficient build-first real-CLI verification. No testing-record finding.

I only read the specified diff and trusted base review criteria; I did not run, build, or install PR code.

…ce helpers

The script git-mvs the local driver and tool adapter into src/workflow/drivers/local/ and setup and worktree-snapshot into src/workflow/place/, rewrites every relative specifier that names a moved file (static, type, export-from, dynamic import, vi.mock), and fails if any relative specifier then resolves to no file. A second run changes nothing.
Output of node scripts/codemods/1304-move-drivers.mjs: renames plus specifier rewrites, no code change.
A workflow now says whether an agent session may start it with
`invoke: allow | ask | deny`; a file that writes none is `deny`.
`bad_invoke` replaces `bad_invocable`, and an alias that is not a
token, or repeats the workflow's own name, is `bad_alias`.

Pull reports `alias_conflict` on each workflow whose alias another
workflow also uses or takes as its name. Those workflows stay cached
and start by name; only the alias starts nothing.
…eckout places

The run lock moves to store.ts as withRunLock, with a wait bound and a
RunLock value that proves a caller holds it. The local driver takes it
without waiting, as before.

A step without a workspace contract is now staged by place/place.ts:
preparePlace records the place and the checkout snapshot under the
attempt, and finishPlace runs the read-only check from whichever process
reports the attempt. The local driver goes through it for those steps.

protocol.ts holds WorkflowClient, moved from the conformance interpreter,
and inProcessClient, which the in-process conformance suite and plain
workflow start now use. Its report finishes a prepared place first.
protocolDescribe reads a workflow and the ones it runs into inputs,
invoke, gates, prompt steps, steps outside the checkout and run width.

StepModel is defined in step-model.ts, and the driver identity helper in
driver-identity.ts, so neither flat module imports the local driver.
- `workflow start|next|report --driver skill`: describe says how to start,
  start prints the first turn, and `next --wait` polls until the run is done.
  A detached runner per attempt runs its command in the checkout, masks the
  output and reports it.
- `pull` writes a `workflow-<name>` Claude skill for each `invoke: allow`
  workflow, signed with a marker so a later pull rewrites only an unedited
  copy and never a skill of the same name it did not write.
- The teamai stub pre-approves the skill driver calls that cannot start a run.
- Workflows with prompt steps or steps outside the checkout answer
  `needs_local_driver`; the person runs them with `--driver local`.
- The transition rows are documented in docs/dev/workflow-engine.md and a test
  checks them against the code both ways; a boundary test keeps the skill
  driver off the local driver.
- Both vitest configs seed an agent session id and a workflow attempt, which
  the setup file removes, so the scrub always has something to remove.
A driver can now declare can.env: 'commands'. negotiate grants command
steps and setup their envScope and holds a prompt step that requires env
with missing: ['env']. can.env true and false keep their meaning.
The skill driver declares can.env: 'commands'. Its runner builds the
command's environment from the lease's envScope, so a required key is
given and the run's other keys are kept from the step.
…ne unsupported_driver message

A resume without --driver skill refused --parallel, --hitl and
--no-judge with one combined message from a hand-written check, while
start refused each flag on its own through NonSkillStartOptionsSchema.
That schema, now NonSkillRunOptionsSchema, checks both, and both
commands build the unsupported_driver refusal from one helper.
An editor shows only the second of two adjacent JSDoc blocks, so the
older one-line block above runSkillCall was dead text.
…rals' option words, once

The skill state a start (R10) and a resume or takeover (R19) write was
built by the same newSkillState call in two places, and the resume
literal repeated the start literal's --parallel, HITL and --no-judge
words. plannedState and runOptionWords now hold each once. The start
literal keeps --confirmed between --parallel and the HITL word, so no
literal changes.
…he workflow has prompt steps

The guide said the first turn of a resume asks the host question when
prompt steps are left. It asks whenever the run's workflow has a prompt
step, settled or not, as R19 records.
…sumeRefusal

A --json next whose payload claims the run makes the resume's invoke
check too, and answers confirm_needed with the resume --claim
--confirmed literal, since a payload cannot carry the person's yes.
The dev doc's invoke rule named only resume and the takeover.
…t held it

A stopped attempt held its place until a report abandoned it, but a
driver that is gone never sends that report. A local driver killed
mid-step whose lease then expired or was cancelled left its place busy
for good, where the expiry had freed it before, and a skill session
continuing the run queued behind that attempt forever.

Only the stopped attempts of the preparing lease's own driver hold
their place now. That driver tracks and reports them, as the skill
driver does through its runner or the tick, and a --json driver can
report the attempt busy names. Another driver's stopped attempt holds
nothing, as before.
…tempt the run already stopped, when a session resumes
… end, and count its pull request receipts

A retry that landed while the engine answered done gave the old end the
retried run's version, so every later tick skipped the engine and the
retried step never ran. A receipt or an unresolved create changes no
revision, so a done run also stopped recovering a pull request opened
before its delivery process stopped.
…o reach teamai-reports

A done reply that carries notPublished is not the kept end, so the next
tick's engine next publishes it again, as the guide says. R19's row now
names the stopped attempts' groups a resume stops, and refuseOptions
gets its JSDoc back.
…e claiming the run

An unattended resume or takeover with the skill driver counted every
required approval, interactive step and open choice of the definition,
so a run whose approval already ran was still refused. It now counts only
the steps the run has not settled; invoke: ask still counts.

The refusal also came after the claim, so an unattended resume --claim
of a run that needs a person or the local driver left this install
owning it. Both are now read from the run as it stands, before the claim.
A dashboard answer is the person's, so it records agent: false.
@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Ignore human requirements inside settled containers — src/workflow/drivers/skill/preflight.ts:58: If a sub-workflow containing a required approval is skipped by if: false, settledSteps contains the container ID, but describe lists container.approve. The exact-ID check incorrectly rejects unattended resume with unattended_denied, although that approval will never run. Completed fan-outs have the same mismatch (items[0].approve versus items.approve). Account for settled ancestors and instantiated item paths.

  • [P2 non-blocking] Apply unattended checks to actual starts — src/workflow/drivers/skill/skill-driver.ts:334: attendedReason is checked only for describe. With TEAMAI_MODE=unattended, directly starting an invoke: allow workflow with a required approval creates a run that waits for a nonexistent person, although describe denies that same workflow. Apply the unattended refusal before creating the run.

Resolved: Both earlier findings remain fixed: interactive write permissions are preserved, and rejected pushes fail successful reports while retaining warnings for already-failed reports.

Testing: The description documents sufficient build-first real-CLI verification. No testing-record finding.

I only read the specified diff and trusted base criteria; I did not run, build, or install PR code.

…fuse an unattended start that needs a person

A sub-workflow skipped whole, or a for_each step done, settles the steps
inside it, whose paths the run never lists; an unattended resume counted
their approvals still. A start --driver skill in an unattended run now
answers unattended_denied as describe does, before any run is created.
@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Check unattended takeovers of existing skill runs — src/workflow/drivers/skill/tick.ts:266: An attended skill run with invoke: allow and a pending required approval can be taken over using TEAMAI_MODE=unattended ... next --takeover. Existing skill.json skips the unattended preflight, so the caller becomes orchestrator and waits for a nonexistent person. Check remaining human requirements before changing the orchestrator, regardless of whether skill state exists.

  • [P1 blocking] Check the published state before reclaiming a run — src/workflow/handoff.ts:582: Alice publishes before a required approval; Bob claims, completes that approval, and publishes while later commands remain. Alice’s unattended resume --claim checks her stale local snapshot and returns unattended_denied, although the published state she would claim needs nobody. Preflight must inspect the snapshot the claim would actually install.

  • [P1 blocking] Recognize settled approvals inside active fan-outs — src/workflow/drivers/skill/preflight.ts:60: If every item’s approval has succeeded but downstream item commands are still running, settled paths include items[0].approve and items[1].approve, while describe names items.approve. Neither that definition path nor its parent is settled, so unattended resume incorrectly returns unattended_denied. The ancestor fix handles settled containers, but not settled per-item requirements inside active ones.

Earlier findings: Interactive write permissions, rejected-push handling, and unattended actual starts are fixed. Skipped/completed containers are handled; the per-item case remains above.

Testing: The description includes sufficient build-first real-CLI verification. No testing-record finding; the older recorded head is not blocking.

I only read the diff and trusted base criteria; I did not run, build, or install PR code.

…ed run a claim would install

An unattended next --takeover of a run a session drives skipped the check
and made the caller orchestrator of a run that waits for a person. An
unattended resume --claim judged this machine's older copy instead of the
published run the claim installs. An approval every for_each item already
answered still counted, since the run names it per item.
@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Apply unattended options to retained skill state — src/workflow/drivers/skill/transitions.ts:373: Start an attended skill run with an optional approval and --hitl person, then resume with TEAMAI_MODE=unattended ... --driver skill --auto-approve. This early return preserves hitl: person and the attended mode, so preflight allows the resume but the run still asks a nonexistent person. Existing-state takeovers retain the same options. Apply the unattended policy to retained state before continuing.

  • [P1 blocking] Do not treat looped approvals as permanently settled — src/workflow/handoff.ts:570: Consider a required approval on review, followed by fix with loop: { back_to: review, max_iterations: 2 }. Publish after review succeeds while fix is still running. Unattended resume excludes review from human requirements and claims the run, but completing fix reopens that required approval. Account for steps that remaining loops can execute again before permitting the claim.

  • [P1 blocking] Recognize settled ancestors within each fan-out item — src/workflow/handoff.ts:578: If items[0].review is skipped while items[1].review.approve succeeds and its downstream work remains running, neither items.review nor items.review.approve is aggregated as settled. Unattended resume incorrectly returns unattended_denied, although neither item needs approval now. Check settled ancestors within each instantiated item, not only the item root and exact requirement path.

Resolved: The earlier concrete findings are fixed: interactive write permissions, rejected-push handling, unattended starts and existing-state preflight, published-state claim checks, skipped/completed containers, and direct per-item approval aggregation.

Testing: The description documents sufficient build-first real-CLI verification. The older recorded head and provider/host coverage deferred to CI are not blocking findings.

I only read the specified diff and trusted base review criteria; I did not run, build, or install PR code.

…ume, and count a settled step a loop can rerun

An unattended resume or takeover of a run this machine keeps skill state for now switches that state to unattended
options (row R26), so its optional gates no longer wait for a person nobody is. A run whose definitions loop settles
nothing for the unattended check, and a step inside a for_each item counts as settled when the item settled a step
that holds it.
@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Exclude only steps that remaining loops can reopen — src/workflow/handoff.ts:571: A looping sub-workflow finishes, a subsequent required approval succeeds, and a final command remains running. Because any loop anywhere returns an empty settled set, unattended resume rejects the already-answered approval with unattended_denied, although that completed inner loop cannot reopen it. Scope the exclusion to steps reachable by remaining loops.

  • [P2 non-blocking] Reject person HITL in unattended calls — src/workflow/drivers/skill/transitions.ts:382: TEAMAI_MODE=unattended ... resume <run> --driver skill --hitl person passes preflight for an optional approval, but retains hitl: person; the run then asks a nonexistent person and stalls. Starts and retained-state resumes also accept this combination. Reject it or enforce auto-approval consistently.

  • [P2 non-blocking] Skip human preflight for terminal runs — src/workflow/handoff.ts:608: Publish an invoke: allow run cancelled before its required approval. Unattended resume --claim rejects it because cancelled steps are not included in settledSteps, although claiming this terminal run executes nothing. Carry the run status into preflight and bypass remaining-human checks for terminal runs.

Resolved: The earlier concrete findings are fixed, including retained-state unattended options and per-item settled ancestors. Looped approvals are now guarded, but that guard introduces the first finding.

Testing: The description documents sufficient build-first real-CLI verification. No testing-record finding.

I only read the specified diff and trusted base criteria; I did not run, build, or install PR code.

…p that can still run reaches

A loop that ended, whose owner is final or whose workflow's step settled, no longer keeps an answered approval
counting as one a person must answer. A loop that can still run, or one whose owner a loop can run again, still does.
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] Reject person HITL in unattended calls — src/workflow/drivers/skill/transitions.ts:382: TEAMAI_MODE=unattended ... resume <run> --driver skill --hitl person passes preflight for an optional approval but stores hitl: person, so the run asks a nonexistent person and stalls. Starts and retained-state resumes also accept this combination. Reject it or consistently enforce auto-approval.

  • [P2 non-blocking] Skip human preflight for terminal runs — src/workflow/drivers/skill/tick.ts:275: Publish an invoke: allow run cancelled before its required approval. Unattended resume --claim still returns unattended_denied: cancelled steps are not settled, and preflight receives no run status. Bypass remaining-human checks for terminal runs, which execute no further steps.

Resolved: The overly broad loop exclusion is fixed; only loops that can reopen a step prevent it from being considered settled. Earlier interactive-permission, push-failure, and resume/takeover fixes remain intact.

Testing: The description documents sufficient build-first real-CLI verification. The older recorded head and coverage deferred to CI are not blocking.

I only read the specified diff and trusted base review criteria; I did not run, build, or install PR code.

The test setup now drops CI, so the pull a new step workspace runs draws its spinner in the terminal these tests
open. That terminal had no columns, and the spinner then clears lines without end, so the Ctrl-C tests timed out.
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] Reject person HITL in unattended calls — src/workflow/drivers/skill/transitions.ts:382: An unattended resume --driver skill --hitl person passes preflight for an optional approval but stores hitl: person, leaving the run waiting for a nonexistent person. Starts and retained-state resumes also accept this combination. Reject it or consistently enforce auto-approval.

  • [P2 non-blocking] Skip human preflight for terminal runs — src/workflow/drivers/skill/tick.ts:275: Unattended resume --claim rejects an invoke: allow run cancelled before its required approval because cancelled steps are not settled and preflight receives no run status. Terminal runs execute no further steps, so bypass their remaining-human checks.

  • [P2 non-blocking] Publish using the run’s owning scope — src/workflow-commands.ts:450: Starting a user-scope workflow from a project without that workflow stores the run in the user scope, but workflow publish <run> still loads the current project’s config. Publishing from the same directory therefore cannot find the newly created run. Resolve its owner with readyRunConfig, as the other run commands do.

  • [P2 non-blocking] Keep judge follow-ups in the scope that leased them — src/score-commands.ts:140: From a project directory, score --next targeting a user-scope run now leases from the user scope, but subsequent score --lease execution or submission still uses the project config. Those calls cannot find the issued lease. Resolve its owning scope before executing or submitting it.

Resolved: Earlier interactive-permission, push-failure, unattended-start/takeover, published-snapshot, settled-container/fan-out, and loop-scope findings remain fixed.

Testing: The description includes sufficient build-first real-CLI verification. The older recorded head and coverage deferred to CI are not blocking findings.

I only read the specified diff and trusted base review criteria; I did not run, build, or install PR code.

…the harness

A session's sleep sets the fake clock from the harness's own, so a vi.setSystemTime between it and the next call
could be undone: the lease then lapsed without the timeout, the step was leased again, and the runner reported
twice. The harness's advance moves both.
…e that holds it

A run of the user scope started in a project with its own scope lives in the user scope, as do the judge leases score
--next takes for it. workflow publish now finds the run's scope as the other run commands do, and score --lease falls
back to the user scope when this directory's scope does not hold the lease.
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] Reject person HITL in unattended calls — src/workflow/drivers/skill/transitions.ts:382: An unattended resume --driver skill --hitl person passes preflight for an optional approval but stores hitl: person, leaving the run waiting for a nonexistent person. Starts and retained-state resumes also accept this combination. Reject it or consistently enforce auto-approval.

  • [P2 non-blocking] Skip human preflight for terminal runs — src/workflow/drivers/skill/tick.ts:275: Unattended resume --claim rejects an invoke: allow run cancelled before its required approval because cancelled steps are not settled and preflight receives no run status. Terminal runs execute no further steps; bypass their remaining-human checks.

  • [P2 non-blocking] Resolve judge leases across project scopes too — src/score-commands.ts:114: From project B, score --next targeting a run in project A now finds A and issues its judge lease. Subsequent score --lease execution or submission searches only B and the user scope, so neither finds that lease. Resolve lease ownership across the same scopes used for run discovery.

Resolved: The earlier user-scope publish and judge-follow-up findings are fixed. Earlier interactive-permission, push-failure, unattended-start/takeover, published-snapshot, settled-container/fan-out, and loop-scope fixes remain intact.

Testing: The description documents sufficient build-first real-CLI verification. The older recorded head and coverage deferred to CI are not blocking findings.

I only read the specified diff and trusted base review criteria; I did not run, build, or install PR code.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified at b2f115c: CI green, local gates and the real-CLI records in the description pass. Approving.

@SaulMoro
SaulMoro merged commit fc8a229 into Tencent:main Oct 26, 2026
18 checks passed
@SaulMoro

Copy link
Copy Markdown
Collaborator

Merged as fc8a229. Thank you, @nateEc: this is a large piece of work, and the state machine held up well under review. On top of your branch I added the unattended rules from #1086 (steps left, refusal before any claim, kept skill state), the done-run cache fixes, run scope for publish and judge leases, and two test timing fixes; each is a separate commit in the branch history if you want to look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] Add the session skill driver and invocable workflows

3 participants