Skip to content

[feat] Default workflows a team can override, starting with session-close #1388

Closed

Description

@SaulMoro

Example

teamai ships session-close. A team that writes workflows/session-close.yaml replaces it.

$ teamai list workflows

=== REPO WORKFLOWS ===
  review-and-fix  (team)
  session-close  (default)

# after the team adds workflows/session-close.yaml and runs teamai pull
$ teamai list workflows

=== REPO WORKFLOWS ===
  review-and-fix  (team)
  session-close  (team)

A workflow from the user-scope team also shows team. For one name the order is project workflow > user-scope workflow > teamai default.

The default definition. It parses with parseWorkflow as written, on a teamai that reads invoke (#1304):

schemaVersion: v1alpha1
name: session-close
description: Close out this session with the person - share what it taught as a team learning, publish its transcript to the team, score it with the team's scorers. Use when the person asks to close the session, share a learning, publish or archive the session, or score or judge it.
invoke: ask
inputs: { session: string, source: string, publish: boolean, scorers: [string], judge: here | driver }
steps:
  - id: learning
    interactive: true
    prompt: "Run `teamai skill get share` and follow it with the person for the {{ inputs.source }} session {{ inputs.session }}. If the person finds nothing worth sharing, return verdict skip. Otherwise write the learning to a file outside the repository and return verdict share, its path, its title, and the learnings namespace (an empty string for the shared root)."
    output: { verdict: share | skip, title: string, file: string, namespace: string }
  - id: contribute
    needs: [learning]
    if: steps.learning.outputs.verdict == 'share'
    approve: Share "{{ steps.learning.outputs.title }}" with the team?
    command: teamai contribute --file "$FILE" --title "$TITLE" --namespace "$NS" --session-id "$SESSION"
    env: { FILE: "{{ steps.learning.outputs.file }}", TITLE: "{{ steps.learning.outputs.title }}", NS: "{{ steps.learning.outputs.namespace }}", SESSION: "{{ inputs.session }}" }
  - id: publish
    needs: []
    if: inputs.publish
    approve: { message: "Publish this session, scrubbed, to the team?", required: true }
    command: teamai session push --scrub --session "$SESSION" --source "$SOURCE"
    env: { SESSION: "{{ inputs.session }}", SOURCE: "{{ inputs.source }}" }
  - id: score
    needs: []
    if: inputs.judge == 'here'
    for_each: inputs.scorers
    command: teamai score --session "$SESSION" --source "$SOURCE" --scorer "$SCORER"
    env: { SESSION: "{{ inputs.session }}", SOURCE: "{{ inputs.source }}", SCORER: "{{ item }}" }
  - id: score-driver
    needs: []
    if: inputs.judge == 'driver'
    for_each: inputs.scorers
    prompt: |
      Judge the {{ inputs.source }} session {{ inputs.session }} with the scorer {{ item }}. Take the judge work with
      teamai score --scorer {{ item }} --session {{ inputs.session }} --source {{ inputs.source }} --json '{"driver":{"kind":"{{ inputs.source }}","machine":"local","workspace":".","can":{"judge":{"isolation":"best-effort","access":"read","tools":["<your tool>"]}}}}'
      Read only the lease's prompt file and evidence directory, answer with the lease's answer schema, and send the answer with the lease's submit command and the same driver. Return the label it stored.
      In tools, declare the id of your own tool (claude, codex, cursor, …), not the session's platform. If teamai answers isolation_unmet, judge_model_unavailable or busy, return an empty label.
    output: { label: string }

judge picks how a session is scored, and the hook decides it when it offers the workflow:

a judge tool teamai runs is installed here (claude, codex, opencode, codebuddy)  → judge: here    → score: teamai's own judge, as today
none is installed (a member who only has Cursor, for example)                   → judge: driver  → score-driver: the step's agent takes
                                                                                                  #1386's lease and judges it, best-effort

publish and scorers are offers the person confirms in the start question. publish also asks once more, from the person's own terminal or the dashboard, because no one reviews a transcript after it is published. auto publishing and scoring never run here; they are #1385's.

The learning step ends in one of two answers. Only share reaches contribute:

person: nothing worth sharing   → { verdict: skip }                                              → contribute skipped
person: share it under k8s      → { verdict: share, title: "Pod startup timeouts",
                                    file: "<outside the repo>/pod-startup.md", namespace: "k8s" } → contribute asks, then
                                                                                                    teamai contribute … --namespace "k8s"
person: share it at the root    → { verdict: share, …, namespace: "" }                            → contribute … --namespace ""

What offers it, at the Stop hook of an attended session. The friction reminder decides; publishing and scoring only ride along with it:

launchedBy is set (#1384)                        → nothing   # a workflow step or a judge; also stops session-close offering itself
nestedStart(env, cwd) is set (#1304)             → nothing   # a child in a workflow worktree or on a teamai/<run>/… branch
runMode is unattended                            → nothing   # attended only
this session drives a skill-driven run not done  → nothing   # a not-done skill state on this machine has orchestrator.session = this session
adapter  = the session's platform has a session adapter (#1221 F `sessions`, today platform-facts.ts)
resolved = session-close as this checkout resolves it (project > user > default)
offer    = resolved.invoke is ask or allow ? sessionCloseOffer(config, {platform, sessionId}) : none
           # sessionCloseOffer:
           #   judge    = installedJudgeTools([]) is not empty ? here : driver
           #   publish  = adapter and sessions.publish.attended is ask (#1385)
           #   scorers  = adapter and scoring sessions.attended is ask (#1385) ? sampledScorers(config, {platform, sessionId}) : []
           #              with judge: driver, minus the scorers whose required isolation is verified: the strictest of
           #              scorers/scoring.yaml isolation, the scorer's isolation and config.yaml scoring.isolation, as the
           #              judge lease computes it (requiredIsolation, src/scorers/isolation.ts)
contributeCheckForSession(sessionId, …, offer)
    # today's friction reminder, unchanged: contributeHintAllowed, smartScore, thresholds,
    # sharing.contributeHint.enabled, contributeHintEnabled, share gate, TEAMAI_CONTRIBUTE_HINT_DISABLED, hinted, contributed
    no friction reminder                         → nothing   # no publish or score offer either
    reminder fires, offer is none                → today's reminder text (`skill get share`), no offered state
    reminder fires                               → the reminder names session-close and carries no inputs
                                                   one state write sets hinted, offered: {publish, scorers} and, for a tool
                                                   whose Stop cannot print, pendingHint with that same text

sampledScorers is the per-session rule the background session judge already applies in entries (src/session-flow/session-work.ts), moved into one function both call:

for each scorer teamai pull cached (sessionScorers)
  scorer has no `sessions: true`                                   → dropped
  scorer lives in a namespace the session was not delivered        → dropped
  isSampled(scorer, {platform, sessionId}, samplingRate) is false  → dropped   # a hash: same answer on every machine and every read
  a label or a settled attempt exists for (scorer, rubric, session) → dropped
  otherwise                                                        → returned with its state: open, or leased while a judge holds it
entries keeps open and leased; sessionCloseOffer offers only open
scorer           samplingRate   isSampled(…, cursor/7c1e…)   offered
review-quality   25             true                          yes
style            25             false                         no
secure-review    100            true                          yes

So with samplingRate: 25 a scorer is offered on about a quarter of the sessions that reach the reminder, always the same ones, and those are the sessions the background judge would pick under auto. The 6 hours idle rule of the background judge does not apply: the offer comes at Stop, while the person is there.

So a team sees the reminder on exactly the Stops it sees it on today, at most once per session, and a person can always start session-close by hand to publish or score a session the reminder never fired on.

The reminder text:

This session hit friction worth sharing. Offer the person the session-close workflow: run
teamai workflow start --describe session-close --driver skill
and start it with the inputs it proposes, once the person confirms them.

describe proposes the inputs, computed for the calling session by the same sessionCloseOffer:

$ teamai workflow start --describe session-close --driver skill
{"data":{"describe":{"workflow":{…},"judging":{"kind":"none"},"ask":{"kind":"ask","question":{"text":"…","inputs":[
  {"name":"session","type":{"type":"string"},"value":"7c1e…","why":"The session to close: this one."},
  {"name":"source","type":{"type":"string"},"value":"cursor","why":"This session's tool, as teamai session list names it."},
  {"name":"publish","type":{"type":"boolean"},"value":true,
   "why":"Archive this session's transcript, scrubbed, for the team. Proposed because the team's attended publish policy is ask. You confirm it again from your own terminal or the dashboard before it runs."},
  {"name":"scorers","type":{"type":"array","items":{"type":"string"}},"value":["review-quality"],
   "why":"Scorers that judge this session now. review-quality samples this session (samplingRate 25). [] skips scoring."},
  {"name":"judge","type":{"type":"enum","values":["here","driver"]},"value":"driver",
   "why":"No judge tool teamai runs is installed here, so this session's agent judges, best-effort."}],"options":[…]}}}}}

Each proposed input carries a why: what the input does, and why describe proposed that value or none. The agent shows the person every input with its value and why, and the person may change or empty any of them before confirming:

Input why with a value why with none, or false / []
publish the line above "…Not proposed: the attended publish policy (<file that sets it>) is off." / "…Not proposed: the attended publish policy is auto, so teamai publishes this session, scrubbed, once it has been idle 6 hours." / "…teamai reads no sessions from this tool."
scorers names each proposed scorer and its samplingRate; with judge: driver, names each left out and the file that asks for verified isolation: "secure-review is left out: scorers/secure-review.md asks for verified isolation, which this session's agent cannot give." "…None proposed: the attended scoring policy is off." / "…None proposed: the attended scoring policy is auto, so the background judge scores this session once it has been idle 6 hours." / "…no scorer samples this session." Each ends "Name any scorer to judge with it."
judge here: " is installed here, so teamai's own judge scores it." driver: the line above — (always proposed while the caller has an agent session)
session, source the lines above "No agent session found: give the session and its tool."
describe of the resolved session-close, called from an agent shell
  agentSessionFromEnv() finds no agent session       → no value on any input; the agent asks the person for each
  for each declared input
    name and type are in the contract below          → value and why from sessionCloseOffer(config, {platform, sessionId})
    otherwise                                        → no value and no why; the person gives it

The contract: session: string, source: string, publish: boolean, scorers: [string], judge: here | driver. A declared input matches when its parsed type (OutputSchema) equals the contract's: [string] is {"type":"array","items":{"type":"string"}}, and here | driver is that enum in any order. describe keeps today's type encoding. A team override that keeps these names and types gets them filled:

Team override Values describe proposes
Same five inputs, other steps or prompts all five
Adds ticket: string the five; ticket is the person's
Drops publish and judge (learnings and scoring only) session, source, scorers
Renames session to sessionId, or makes scorers a string all but that one
Sets invoke: deny not offered: the reminder is today's text

Starting it by hand

A person can ask their agent to close the session at any time, for example with the friction reminder off. The agent finds it through the workflow-session-close skill that teamai pull generates from the definition's description (#1304), which names those requests, and through skill-data/share, which points at it. It runs the same describe and gets the same proposal; the person can change any value before confirming, for example to name a scorer sampling did not pick. An explicit choice is not sampled: the run judges the scorers it was started with.

person: close this session and score it with style
agent:  teamai workflow start --describe session-close --driver skill
        proposal {"session":"7c1e…","source":"cursor","publish":false,"scorers":[],"judge":"driver"}
person: add style
agent:  starts with {"session":"7c1e…","source":"cursor","publish":false,"scorers":["style"],"judge":"driver"}

Sharing and publishing without scoring:

agent:  proposal {"session":"7c1e…","source":"cursor","publish":true,"scorers":["review-quality"],"judge":"driver"}
person: share the learning and publish it, but don't score it
agent:  starts with {"session":"7c1e…","source":"cursor","publish":true,"scorers":[],"judge":"driver"}
        learning → share → contribute; publish → required approval → session push; score-driver over [] succeeds at once

Behavior

Team today After
Friction reminder on, members share with skill get share Same reminder on the same Stop; it now offers session-close, whose learning step runs skill get share with the person. With ask policies (#1385) the same offer also proposes publishing and scoring
Friction reminder off No automatic offer at all. A person who wants to publish or score a session starts session-close by hand
A team file workflows/session-close.yaml Used instead of the default; pull validates it like any workflow. An invalid override with a last valid version keeps that version, as today. An invalid override with none leaves the default in effect, and pull says so
A session teamai launched (a workflow step, a judge, session-close's own steps), or a child in a workflow place Never offered session-close
A team override of session-close that sets invoke: deny The friction reminder as today (skill get share); session-close is not offered and no offered state is written
unattended run mode Never offered session-close. A hook there inherits the tool-session variables, so it counts as a start from a tool session under invoke, and nested starts are refused: nothing could start it lawfully
Step Kind Who decides it runs
learning interactive prompt: the current tool session works it with the person, so it has the session's context, and it is exempt from the read-only check. Returns verdict: share with the file, title and namespace, or verdict: skip the person, while working it
contribute command, optional approve, run only on verdict: share. --namespace passes the namespace the person chose ("" is the shared root) the person, when they picked "ask me at optional approvals" at start; under the recommended start the tool session, recorded as the agent's answer; the engine (auto_approve) when they picked "answer optional approvals automatically"
publish command, required: true only the person, from their own terminal or the dashboard. An answer from a tool session is refused
score command, no approval, one per scorer: teamai score --session … --scorer … runs teamai's own judge, as today. Only with judge: here. With no scorers it succeeds at once the scorers the person confirmed at start
score-driver prompt step, no approval, one per scorer: its agent takes #1386's lease for the session and scorer, judges from the lease's evidence only, and submits. Labels record isolation: best-effort; a scorer that requires verified answers isolation_unmet and is not judged. Only with judge: driver. With no scorers it succeeds at once as score

learning, publish and score / score-driver have no needs between them, so they run side by side as the start's parallel answer allows. publish and score read the session as it stands when they run: the close-out turns after that are not published or judged, as with #1385's drain from inside a session.

Gates live in the definition, not in prompts, so the engine records each answer (choice, channel, whether an agent sent it) and the dashboard shows it. contribute keeps an optional approval: a learning still reaches the team's review, so the tool session may answer it under the recommended start, and a person who wants to answer it picks "ask me at optional approvals". The scorers are an input the person confirms at start and the run state keeps, so no approval asks again. Publishing a transcript leaves nothing for the team to review, so it alone is required: the person answers it from their own terminal or the dashboard, and an answer relayed from the tool session is refused (agent_in_path). workflows.auto_approve does not answer these approvals: under the skill driver, #1304's next ignores it for a driver with HITL. The third start describe offers, "Same, but answer optional approvals automatically" (--auto-approve), answers contribute yes on channel auto_approve. It never answers publish, which is required.

Acceptance criteria

  • No team sees the friction reminder in more or fewer cases than today. Covered by tests that replay today's contributeCheck cases, also with sessions.publish.attended: ask: the reminder fires on the same Stops, and a Stop where it does not fire offers nothing.
  • session-close is offered at most once per session within 30 days, on the Stop where the friction reminder fires, with every artifact that applies on that Stop. A session resumed after its offer record expired (below) can be offered again; that is the accepted bound.
  • The offer is computed before contributeCheckForSession and passed into it; the function builds the reminder text and writes hinted, offered and pendingHint in its one state write (src/contribute-check.ts:611-650). A caller never post-processes its result: for a tool whose Stop hook cannot print (codex, codebuddy, workbuddy: the tools outside STOP_STDOUT_TOOLS in src/utils/tool-names.ts) it returns { hint: null } even when the reminder fires. Covered by a hook test with codex: the stashed reminder names the describe command and carries no inputs, and offered is in the write that sets hinted and pendingHint.
  • readContributeState, which today drops every field it does not know, reads the new offered: { publish: boolean, scorers: string[] }.
  • The offer facts (hinted, offered, contributed) outlive the 24 hours cleanup of the state file: cleanupStaleSessions replaces a stale file with a record of only those facts, removed after 30 days. Resuming a session more than 24 hours later, after a cleanup, never repeats an offer. [feat] Show scorer results in the dashboard #1182 needs the same record for its reminder table; whichever of the two issues lands first adds it, and the other reuses it.
  • Use only launchedBy ([feat] Run mode: tell attended runs from unattended ones #1384) and nestedStart(env, cwd) (feat(workflow): add session discovery and managed public leases #1304) for where the session was launched; add no other launch-marker reader. Covered by hook tests with TEAMAI_LAUNCHED_BY=workflow, with TEAMAI_WORKFLOW_ATTEMPT set, and with a teamai/<run>/… branch. A host child with neither a marker nor a workflow place is not recognized, [feat] Run mode: tell attended runs from unattended ones #1384's documented limitation. The unattended and driving-run guards below are separate.
  • Never offered while this session drives a skill-driven run that is not done, its own session-close included. A session drives a run when a not-done skill state in this machine's data homes has orchestrator.session equal to it. A state without orchestrator.session blocks nothing.
  • Never offered in an unattended session. An unattended --describe of session-close answers denied anyway, since invoke: ask needs a person (feat(workflow): add session discovery and managed public leases #1304).
  • The offered scorers come from sampledScorers, one function that SessionWork.entries also calls, so the offer and the background session judge pick the same (scorer, session) pairs. Covered by a test: a scorer with samplingRate: 0 is never offered, one with 100 is offered unless already labelled, one without sessions: true is never offered, and a scorer in a namespace the session was not delivered is not offered.
  • skill-data/share, which points at session-close (Definition of done), says to start it with describe's proposed inputs once the person confirms them.
  • publish and scorers are offered only for a session whose platform has a session adapter, since session push --session and score --session cannot read the others.
  • skill get share keeps working on its own and stays the single source of the learning instructions; the default has no prompt_file.
  • One function, sessionCloseOffer, computes the offer for the hook and the values describe proposes. Covered by a test: for one session, the reminder's offered and describe's proposed publish and scorers are equal.
  • describe adds value and why to each entry of ask.question.inputs, and of inputs on kind: start, whose name and type are in the contract, when the caller has an agent session (agentSessionFromEnv). Other inputs get neither. why follows the table above, also for publish: false and scorers: []; tests cover each row. Covered by tests: an override that adds ticket: string gets the five values and none for ticket; one that renames session gets no value for it; a call with no agent session variable gets no values.
  • The reminder names session-close only while the resolved definition's invoke is ask or allow. With deny it is today's text, and no offered state is written. Covered by a hook test with a project override that sets invoke: deny.
  • sessionCloseOffer sets judge: here when installedJudgeTools([]) finds a judge tool on this machine, and judge: driver otherwise. Covered by tests for both values: here runs score, driver runs score-driver, and the other step is skipped.
  • The default parses with parseWorkflow, with invoke: ask, interactive: true on learning, the learning output { verdict: share | skip, title, file, namespace }, contribute guarded by if: steps.learning.outputs.verdict == 'share', required: true on the publish approval, an optional approval on contribute, and none on score and score-driver.
  • A learning step that returns verdict: skip skips contribute and asks nothing more about the learning. Covered by a test. A run started with scorers: [] runs no judge, and its score or score-driver step succeeds with no results. A scorer whose required isolation is verified (from the scorer, scorers/scoring.yaml or config.yaml) is never proposed with judge: driver; covered by a test with isolation: verified in scorers/scoring.yaml only. A scorer whose lease teamai score refuses with isolation_unmet, judge_model_unavailable or busy ends its score-driver item with an empty label, and the run still succeeds; covered by a test with a model-pinned scorer whose alias has no mapping for the declared tool. The why rows for publish: false and scorers: [] cover off, auto and a tool teamai reads no sessions from, each with a test. learning always runs: a person who only wants to publish or score answers it with nothing to share.
  • contribute stays a read-only command step, in self mode too. It may call migrateSelfModeGitignore, which rewrites .teamai/.gitignore only when the file is out of date; teamai pull already runs it, so after a pull it writes nothing. Covered by a test: a self-mode session-close run, after teamai pull, shares a learning and ends without read_only_dirty.
  • Defaults resolve behind loadCachedWorkflow and inside listCachedWorkflows (feat(workflow): add session discovery and managed public leases #1304), the one seam feat(workflow): add session discovery and managed public leases #1304 keeps for listing and loading. For one name the order is project workflow > user-scope workflow > teamai default. Defaults are never written to the workflow cache or the team repo. So start, sub-workflows, bundles, handoff, review --branch and feat(workflow): add session discovery and managed public leases #1304's discovery see them, and invoke: ask gets a generated workflow-session-close skill.
  • readyStartConfig (src/workflow-commands.ts, feat(workflow): add session discovery and managed public leases #1304) picks the run's scope by asking the project scope's loadCachedWorkflow first. Its project probe must not return the teamai default before the user scope is checked: resolve scope and definition in project > user > default order, then use that scope's configuration. Covered by a test with a project install and a user-scope session-close override.
  • teamai list workflows, which lists through scanTeamForPull, and workflow start --describe (feat(workflow): add session discovery and managed public leases #1304) show each workflow's source (describe adds source: 'team' | 'default' to data.describe.workflow, WorkflowDescription in src/workflow/describe.ts): team for a valid cached team definition (project or user scope), default otherwise. No new command.
  • A run of a default records definition: { source: 'default', teamaiVersion: '<version>' } on the run state, the snapshot and the run records, and no teamRevision. A team definition records definition: { source: 'team' } beside teamRevision, as today. A default run continued on another machine whose teamai version differs refuses with definition_mismatch, naming both versions.
  • A session-close run is recorded and listed like any run. Its record names the closed session in inputs.session and inputs.source. contribute runs inside the attempt with --session-id, so [feat] Record what a session, step or run delivered #1387 records the learning twice: on the contribute pass, and as a learning outcome of the closed session, which is the member's own session and has no launchedBy. Covered by a test.
  • Learnings are shared with teamai contribute --session-id to teamai-learnings, as today.

Definition of done

  • docs/guide/workflow.md (default workflows and overriding them, with the session-close input contract), docs/guide/knowledge.md (sharing a learning through session-close, and who answers each offer) and their zh-CN pages, with the examples above.
  • skill-data/workflow/SKILL.md (feat(workflow): add session discovery and managed public leases #1304), the ask line: give the person each entry of ask.question.inputs with its proposed value and why, and say they may change or empty any of them.
  • skill-data/share: points at session-close, since the share workflow changes. skill-data/setup/references/setup-admin.md (the paragraph on the end-of-session prompt and the share workflow): the prompt now offers session-close. skills/teamai/SKILL.md keeps feat(workflow): add session discovery and managed public leases #1304's lines and nothing more.
  • The friction reminder text names session-close.
  • CHANGELOG.md, under ## [Unreleased] › ### Changed: the friction reminder offers the session-close workflow, which teams can override with workflows/session-close.yaml.
  • list help text says workflows show their source; skill-data/core/references/commands.md regenerated with npx vitest run commands-reference -u.
  • Real-CLI record: one attended session that triggers the reminder and shares a learning through session-close.

Out of scope

Notes

  • Reminder: src/contribute-check.ts (flow comment at the top), contributeCheckForSession (:509), handler contributeCheckHandler at src/hook-handlers.ts:486-511, contributeHintAllowed in src/skill-content.ts:180. The handler's early return on contributeHintAllowed (:494) stays: no offer goes out without the friction reminder.
  • Per-session state: the file getSessionPath returns (src/contribute-check.ts:120-128). readContributeState reads only the fields it knows (:138-160). ContributeState: src/types.ts:2156. Cleanup: cleanupStaleSessions deletes state files older than 24 hours (STALE_SESSION_MS, :183-208) whenever another session writes its state.
  • Config keys: sharing.contributeHint.enabled (team) and contributeHintEnabled (member), src/types.ts:124-132, :850, resolved by isContributeHintEnabled (:282-290).
  • Guards: readLaunch (src/launch-context.ts), which [feat] Run mode: tell attended runs from unattended ones #1384's launchedBy extends to any TEAMAI_LAUNCHED_BY value and to the skill driver's TEAMAI_WORKFLOW_ATTEMPT. nestedStart is on main since feat(workflow): add session discovery and managed public leases #1304 (src/workflow/attempt-marker.ts); a start inside a workflow place is refused nested_workflow anyway, so the reminder must not offer one. The driving-run lookup reads orchestrator.session in the skill state (src/workflow/drivers/skill/skill-state.ts, feat(workflow): add session discovery and managed public leases #1304).
  • invoke: the default deny refuses a start from any tool session, the Stop hook's included. ask folds the person's consent into the one question --describe asks, so a tool session never starts a close-out on its own from the generated skill. allow would. A team that wants that overrides with allow. The host also asks once before start, since feat(workflow): add session discovery and managed public leases #1304 pre-approves only start --describe, next, report and respond. A team file that sets invoke needs a teamai that reads it.
  • interactive: true steps are worked by the current tool session with the person (feat(workflow): add session discovery and managed public leases #1304). A child session would get only a brief, none of the closed session's context, and a read-only step that leaves a file fails read_only_dirty.
  • Approvals under the skill driver: feat(workflow): add session discovery and managed public leases #1304's HITL choices at start are person, recommend and auto-approve (src/workflow/drivers/skill/preflight.ts), asked only when a step has an optional gate (neededAnswers); with recommend, the tool session answers those gates. contribute's is session-close's only one. A required gate is handed to the person, who answers with teamai workflow respond in their own terminal or in the dashboard; respond from the tool session's shell is refused agent_in_path (feat(workflow): add session discovery and managed public leases #1304, src/workflow/run-controls.ts). next ignores workflows.auto_approve for a driver with HITL (src/workflow/engine.ts, the comment above answerOptionalGates); answerWithoutHitl applies it only to drivers without HITL.
  • Workflow resolution: loadCachedWorkflow (src/workflow/cache.ts), used by loadWorkflowTree, loadWorkflowBundle, the engine's startRun (src/workflow/engine.ts), review --branch (src/review-branch.ts:696) and readyStartConfig (src/workflow-commands.ts, feat(workflow): add session discovery and managed public leases #1304). dropUnsettled (src/workflow/cache.ts) would delete a default written into the cache. feat(workflow): add session discovery and managed public leases #1304 added listCachedWorkflows, aliases, advertisedWorkflows (src/workflow/discovery/workflow-skills.ts) and --describe. Precedent for package-served content: src/builtin-skills.ts.
  • Listing: teamai list workflows exists (workflows in RESOURCE_TYPES, src/types.ts:1649-1664). It lists the team repo's files through the handler's scanTeamForPull (src/status.ts:419-432), valid or not, so it needs the cache's view to tell team from default.
  • Run state fields: BodySchema (src/workflow/snapshot.ts) holds teamRevision today. A claimed run without a team revision: definitionsOf (src/workflow/handoff.ts) today tells the member to run teamai pull.
  • This issue lands after [feat] Add the session skill driver and invocable workflows #1086. A tool whose Stop hook cannot print gets the reminder stashed for its next prompt, as today (stopStdoutUnsupported, src/hook-handlers.ts:505-508).
  • session push --session <id> comes from [feat] Publish and score sessions by run mode #1385. teamai score --session --source --scorer exists today and runs teamai's own judge (src/session-flow/scoring.ts); --scorer is repeatable, and for_each gives one call per scorer. Its choice of judge tool is installedJudgeTools([]) and chooseJudgeModel (src/session-flow/scoring.ts:124-132); the hook's judge uses the same list. score --session with --json and can.judge, which score-driver runs, comes from [feat] Judge a session with the driver's own tool #1386.
  • Input types: TYPE_NAMES and parseOutput in src/workflow/parse.ts ([string] is an array of strings; a bare array is not a type; share | skip is an enum). approve without required is optional (parseHitl, src/workflow/parse-hitl.ts). A step that needs a skipped step is skipped (skipUnreachable, src/workflow/engine.ts), so contribute is skipped with learning. contribute is also skipped when its if is false, on verdict: skip. A for_each over an empty list succeeds at once with results [] (docs/guide/workflow-advanced.md), so scorers: [] needs no guard.
  • In self mode contribute calls migrateSelfModeGitignore (src/contribute.ts:219-225, src/init.ts:1057). It writes only when migrateSelfModeGitignoreContent reports a change, and teamai pull runs it first (src/pull.ts:187-188), so the contribute step leaves the checkout as it found it.
  • describe's input list: inputsOf (src/workflow/drivers/skill/preflight.ts, feat(workflow): add session discovery and managed public leases #1304), used for ask.question.inputs and start.inputs. This issue adds value and why, keyed by the resolved workflow's name. Today the question shows only each input's name and type (factsOf), and the workflow's description is not in it. The caller's session: agentSessionFromEnv (src/utils/session-id.ts) gives its id and agent family; source is the platform name teamai session list uses (src/session-flow/adapters/platform-facts.ts), mapped from that family.
  • No step uses choose: each offer is a yes or no on an artifact the hook already picked, so options would only restate the inputs.

Activity

  1. SaulMoro commented on Oct 25, 2026

    @SaulMoro
    CollaboratorAuthor

    Rewrote the body. There is no workflow list: defaults show in teamai list workflows and --describe (#1304), resolve behind loadCachedWorkflow and listCachedWorkflows, and are never written to the cache.
    The default sets invoke: ask, since the default deny refuses any start from an agent session. learning is interactive, because a child agent lacks the session's context; score is a command step that runs teamai's own judge; the publish approval is required: true.
    It runs attended only. The Stop hook offers each artifact at most once per session, never in a session teamai launched or inside a workflow place, and never while the session drives a run. The new flags go into readContributeState, which drops unknown fields (src/contribute-check.ts:138-160).
    The shipped YAML parses with #1304's parser. session-to-task moves here as a second slice, after #1180's task format.

  2. SaulMoro commented on Oct 25, 2026

    @SaulMoro
    CollaboratorAuthor
    • session-close is offered only on a Stop where the friction reminder already fires. Publishing and scoring ride along and are never offered alone, so no team sees a reminder it does not see today. A person can still start it by hand.
    • Dropped the claim that workflows.auto_approve answers these approvals: feat(workflow): add session discovery and managed public leases #1304's next ignores it for a driver with HITL. Optional approvals are answered by the person, or by the tool session when the person chose recommend.
    • The offer facts now outlive the 24 hours cleanup of the per-session file (src/contribute-check.ts:183-208), so a resumed session is not offered twice.
    • Named the readyStartConfig scope probe, the run's definition field, and the orchestrator.session lookup for a session that drives a run.
  3. SaulMoro commented on Oct 25, 2026

    @SaulMoro
    CollaboratorAuthor

    The learning step now returns { verdict: share | skip, title, file, namespace }. contribute runs only on share and passes --namespace, so the person can share nothing, or share under a namespace. The default still parses with parseWorkflow.
    Settled the self-mode question. contribute stays a read-only command step: migrateSelfModeGitignore writes only when .teamai/.gitignore is out of date, and pull already runs it. A test covers a self-mode run that ends without read_only_dirty.
    Noted why no step uses choose, and updated the src/types.ts line numbers.
    Offers are computed before contributeCheckForSession and written in its one state write, so tools whose Stop cannot print (Codex, CodeBuddy, WorkBuddy) get the same offer in the stashed reminder. The once-per-session promise now states its 30-day bound.

  4. SaulMoro commented on Oct 25, 2026

    @SaulMoro
    CollaboratorAuthor

    Updated: members without a local judge tool can now score through session-close.

    • New input judge: here | driver, set by the hook when it offers the workflow: here when installedJudgeTools([]) finds a judge tool on this machine, driver otherwise.
    • score runs only with judge: here, as before. New score-driver prompt step for judge: driver: its agent takes [feat] Judge a session with the driver's own tool #1386's lease for the session and scorer, judges from the lease's evidence only, and submits. Labels record best-effort; a scorer that requires verified answers isolation_unmet.
    • The reminder text carries judge. The "Scoring a session whose tool teamai cannot launch as a judge" out-of-scope item is removed.
    • The default still parses with parseWorkflow. The existing dependency on [feat] Judge a session with the driver's own tool #1386 now also covers score-driver.
  5. SaulMoro commented on Oct 25, 2026

    @SaulMoro
    CollaboratorAuthor

    Updated:

    • Which scorers the reminder offers is now spelled out: sampledScorers, the per-session rule the background session judge already applies (sessions: true, the session's namespaces, isSampled on samplingRate, not already labelled or tried), moved into one function both call. A samplingRate: 25 scorer is offered on about a quarter of the sessions that reach the reminder, always the same ones. The 6 hours idle rule does not apply at Stop.
    • New "Starting it by hand" section: where each input comes from when a person asks their agent to close the session. Sampling does not apply there; the person names the scorers.
  6. SaulMoro commented on Oct 25, 2026

    @SaulMoro
    CollaboratorAuthor

    session-to-task now tries the task's check before it opens the pull request: it must fail at startingCommit and pass at the new optional fix input, the commit with the session's fix. A check that goes the wrong way opens no pull request. Without fix, the pull request says the check was not tried on a fix. The task file stays in #1180's format, and the results go in the pull request body. Added acceptance for the slice.

  7. SaulMoro commented on Oct 26, 2026

    @SaulMoro
    CollaboratorAuthor

    Correction to the session-to-task checks: at fix, the check now runs after the protect paths are put back as startingCommit has them, as a trial's check runs. Without that, a check whose test only the fix added passes at fix, yet every trial fails it, since #1180 restores protected paths and deletes files the agent added there. Added criterion 5.

  8. SaulMoro commented on Oct 26, 2026

    @SaulMoro
    CollaboratorAuthor

    Updated:

    • The reminder no longer carries inputs. It names teamai workflow start --describe session-close --driver skill, and describe proposes the inputs for the calling session from sessionCloseOffer, the same function the hook uses for its offer. Starting it by hand gets the same proposal; the person can change any value before confirming.
    • Input contract for overrides: session, source, publish, scorers, judge. describe fills each input an override declares with one of those names and types, and leaves the rest to the person.
    • learning is no longer an input: the learning step always runs, and a person with nothing to share answers it with skip.
    • An override that sets invoke: deny keeps today's reminder (skill get share) and is not offered.
    • The session-to-task edits made since the last update are kept as they were.
  9. SaulMoro commented on Oct 26, 2026

    @SaulMoro
    CollaboratorAuthor

    Moved the "Second slice: session-to-task" to its own issue, #1402, which replaces it with its examples and acceptance. The design changed on the way: the session that fixed the bug drafts the task interactively, teamai push --file --path proposes it as a pull request, and the team's CI tries the check on that pull request with #1180's --try, since a nested benchmark start inside a step is refused nested_workflow. Out of scope now points at #1402.

  10. SaulMoro commented on Oct 26, 2026

    @SaulMoro
    CollaboratorAuthor

    Removed score-ok:

    -  - id: score-ok
    -    needs: []
    -    if: length(inputs.scorers) > 0
    -    approve: "Score this session with {{ inputs.scorers }}?"
       - id: score / score-driver
    -    needs: [score-ok]
    +    needs: []          # scorers: [] runs no judge: an empty for_each succeeds at once

    The scorers are an input the person confirms in the start question, and the run state keeps it, so score-ok asked the same question a second time.

    The gates that matter stay in the definition, where the engine records each answer and the dashboard shows it:

    • contribute keeps its optional approval. Under the recommended start the tool session answers it, and "ask me at optional approvals" makes it the person's.
    • publish stays required, because no one reviews a transcript after it is published.

    Updated the step table, the paragraph on who decides, the acceptance criteria and the Notes to match.

  11. SaulMoro commented on Oct 26, 2026

    @SaulMoro
    CollaboratorAuthor

    describe now explains what it proposes. Each input in the contract gets a why next to its value: what the input does, and why that value was proposed, or why none was (publish policy, sampled scorers with their rate, judge tool installed or not). The workflow skill shows each input with its value and why, and says the person may change or empty any of them. Until now the question showed only names and types.

    Also added:

    • learning, publish and score run side by side, and publish and score read the session as it stands when they run, so the close-out turns after that are not published or judged.
    • An example of sharing and publishing without scoring (scorers: []).
  12. SaulMoro commented on Oct 26, 2026

    @SaulMoro
    CollaboratorAuthor

    The default description now says when to use it: "Use when the person asks to close the session, share a learning, publish or archive the session, or score or judge it." teamai pull turns it into the description of the generated workflow-session-close skill, which is how an agent finds the workflow when the person asks by hand. "Starting it by hand" now says so.

  13. SaulMoro commented on Oct 26, 2026

    @SaulMoro
    CollaboratorAuthor

    Fixes:

    • Auto-approve. The third start option, "Same, but answer optional approvals automatically", does apply. It answers contribute yes on channel auto_approve, but never publish. The contribute row and the paragraph now say so.
    • The describe example. Its envelope is now the real one (data.describe), and each type is the parsed schema ({"type":"string"}, …). The contract match is structural, and describe keeps today's type encoding.
    • Scorers that require verified isolation. With judge: driver, such a scorer is no longer proposed, and its why says it was left out. If the person adds one by hand, its score-driver item returns an empty label on isolation_unmet, so the run does not fail after the learning was shared.
    • The score-driver prompt. It declares the agent's own tool id, not the session's platform.
    • sampledScorers. A live lease stays with the background judge and is not offered.
    • Line numbers are updated for main.
  14. SaulMoro commented on Oct 26, 2026

    @SaulMoro
    CollaboratorAuthor

    Updated the references to #1304, which is now merged: what it brought is on main.

  15. SaulMoro commented on Oct 26, 2026

    @SaulMoro
    CollaboratorAuthor

    Changes:

    • Scorers that require verified isolation are now excluded with judge: driver using the required isolation, which is the strictest of the scorer, scorers/scoring.yaml and config.yaml (requiredIsolation). The scorer's own field is not enough on its own. The why names the file that asks for it.
    • score-driver returns an empty label for isolation_unmet, judge_model_unavailable or busy, so a refused lease never fails the run after the learning was shared.
    • The why for an unproposed publish or scorers now covers auto: teamai publishes or scores the session after 6 hours idle. It no longer says "the team's" policy, since a member's config can set it.
    • Learnings in [feat] Record what a session, step or run delivered #1387. The learning is recorded on the contribute pass and as an outcome of the closed session, which has no launchedBy.
    • sampledScorers returns each scorer with its state, open or leased. The offer takes only open ones.
    • describe adds source: team | default to data.describe.workflow.
  16. added a commit that references this issue on Oct 28, 2026
    3b7f52d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions