Skip to content

agenttalk 0.101.0

Choose a tag to compare

@zoolok17 zoolok17 released this 12 Oct 09:57
· 426 commits to master since this release
7385c7c

In short: the checks that decide whether a team's work is finished are stricter and say more
clearly what is missing. A seat that runs through the paid gateway can no longer satisfy any
required review, and an acknowledgement counts only while its author is still an active,
trusted seat: a finished result that rested on a reviewer who has since left now needs a
current reviewer to acknowledge it again (an acceptance close whose runner has left needs a
new attempt instead, as described below). The check before an irreversible step names
exactly what holds a request back and also stops on an outdated request. When agenttalk gateway start fails before the task-start request is accepted, the gateway keeps its stop
file; after that point the command says the gateway may be starting and how to check and
stop it. agenttalk scratch store sets up a throwaway store that commands in that shell
cannot leave (an ordinary folder alone does not: set it up with that command). The
documentation set gained a license, a technical overview, a page on roles, skills and
procedures, and operator procedures that put each safeguard before the step it protects; the
source package now ships the documents its own tests read. After upgrading, refresh the
installed skills as described under "Lead skills" below.

Added

  • The repository now carries its license. agenttalk has always been offered under the
    MIT license (the package says so), but the repository had no license file, so GitHub
    showed "no license", which by default means nobody may reuse the code.

    What you will notice: a LICENSE file with the MIT text and the line "Copyright (c) 2026
    the agenttalk authors". GitHub now shows the MIT license, and the installed package
    carries the same file. Nothing to do.

  • A technical overview of how agenttalk is built (docs/OVERVIEW.md). One new page
    that explains how agenttalk is put together, for someone who will run or extend it.

    What you will notice: a new page, docs/OVERVIEW.md. It shows the parts and how they talk
    to each other, where the data lives under .agenttalk/ (and what a reset keeps), a
    message's life from send to reply, how the supervisor, wrapper and agent processes relate,
    how the console and the paid gateway work, and which promises hold and which do not. It has
    five diagrams, a short list of the words it uses, and an appendix of interfaces. Nothing in
    the program changed, and no other page was changed.

    What you need to do: nothing.

    Technical details: the page was checked against the code of release 0.100.0. It states the
    Python versions the project's tests run on (3.10 to 3.13 on Linux, Windows and macOS). Where
    an older page and the code disagreed, the page follows the code.

  • One page says who does what in a team, which skill to use, and the steps the team follows.
    This information was spread over several older pages, some of them out of date. It is
    the first page of the smaller documentation set the operator asked for on 2026-10-10.

    What you will notice: docs/ROLES-SKILLS-PROCEDURES.md describes each role, with what it
    does, what it must never do and how it hands work on:

    • the operator and the liaison;
    • the lead, developer, reviewer, tester, architect and challenger;
    • a seat that works through the paid gateway.

    It has one entry for every shipped skill, saying when to use it. It also gives current
    step lists for handing out work, the review cycle, the merge gate, the release ritual, the
    challenge, the acceptance pass, the lessons lookup, capturing lessons and scratch hygiene.
    It states the merge rule the team now follows: one recorded GO on the exact head commit, from
    a reviewer who did not build the change, with green CI and every automated P0, P1 and P2
    comment triaged. It retires the older rule of two reviewers both re-approving the final commit. Its
    facts were checked against the code of this version.

    What you need to do: nothing. The older pages stay until a later change moves them to an
    archive.

    Technical details: tests/test_roles_skills_doc.py fails when a shipped skill has no
    entry on the page, or the page names a skill that does not ship. A shipped skill is a
    Claude Code file in src/agenttalk/skills/claude/ or a folder with a SKILL.md in
    src/agenttalk/skills/codex/ or src/agenttalk/skills/devkit/.

  • An operations guide brings runtime upgrades and rollback, supervisor hosting,
    paid-gateway setup and spending limits, backups, recovery, logs and attended
    cleanup together in docs/OPERATIONS.md. It distinguishes shipped commands
    from steps that still need an operator or maintainer.

Fixed

  • The check before an irreversible step now says what is holding a request back. With both
    --epoch and --gates, a request that was still current but held only by a gate at HOLD was
    reported as "pre-epoch", and the agent was told to ask for the request again. It now says
    hold, names the gate and says not to act until it is GO (agenttalk gate check); a request
    from before the latest epoch barrier is still reported as one, and when both apply, both are
    named. The exit codes are unchanged (cmd_check in src/agenttalk/cli.py).

  • The source package now holds the documents its own tests read. The tests shipped in the
    source package read the new roles page, two pages under docs/ops/ and the staging example
    under docs/examples/, but the package left those files out, so those tests failed when run
    from it. The files are now included, with the new overview page, and a new test
    (tests/test_sdist_docs.py) fails when a test names a file under docs/ that the package
    leaves out.

  • The check before an irreversible step now also stops on an outdated request (#448).
    Before a merge, release, deploy or deletion, every seat is told to run agenttalk check on
    the request it is acting on. That check looked only for a withdrawn request and for gates on
    HOLD. When the team had since moved to a new epoch (someone ran agenttalk barrier bump),
    a request sent before that still passed, so a seat could act on instructions the team had
    already set aside.

    What you will notice: the wrapped-turn prompt, the lead, listen and handoff skills (Claude
    and Codex copies), the README and the agent, user and new-user manuals now give the check
    with --epoch. A request sent before the latest epoch
    barrier now makes the check exit 3, and the seat stops instead of going ahead. The seat needs
    no epoch value of its own: the check compares the epoch stamped on the request when it was
    sent with the current one.

    What you need to do: wrapped seats get the new prompt once their wrapper runs this version.
    A seat run by hand keeps its older installed skill files, because the installer leaves a
    changed file alone: preview with agenttalk install-skills --no-devkit --dry-run, back up
    local edits, refresh on purpose with agenttalk install-skills --no-devkit --force, then
    start a new session.

    Technical details: src/agenttalk/wrapper/prompt.py (_DEFAULT_RULES, the "Before any
    IRREVERSIBLE action" line), the six skill files, README.md, docs/AGENT-MANUAL.md,
    docs/USER-MANUAL.md and docs/AGENTTALK-NEW-USER-MANUAL.md; check --epoch is evaluated
    by _epoch_verdict in src/agenttalk/cli.py. The new tests/test_irreversible_check_text.py
    pins the full command in the prompt (both shells) and in every skill, that each Claude and
    Codex pair says the same thing, and that every check example on those four pages names
    --epoch; tests/test_skill_lint.py now pins --epoch --gates for the lead skills.

  • A seat that runs through the paid gateway can no longer give a required sign-off of a sign-off set (#447).
    Such a seat (an "external worker", a cheaper outside model with a narrower level of trust) was
    kept out of sign-off lists only at the moment the roster changed. If the sign-off policy or the
    domain lists were edited by hand afterwards, a close could list that seat as a candidate and
    count its sign-off.

    What you will notice:

    • A close now leaves a paid gateway seat out when it works out who may sign, so its sign-off
      is refused when given (close ack says why and records nothing, even with --override, and
      close signoffs override is refused for such a seat too) and is never counted, even if the
      record was edited to contain one.
    • An override recorded earlier now counts only while the seat that recorded it is still on the active
      roster and is not a paid gateway seat, the same test a signature already passes. If that seat was
      since reclassified as a paid gateway seat, retired, removed or renamed, the override is ignored as if
      it had not been recorded: the close goes back to HOLD (and says so) until a trusted seat signs, which
      then lets it through, or a current trusted lead records the override again. This deliberately turns
      an older GO that rested on such an override (for example one by a lead who later retired) into a HOLD.
    • Other candidates are not affected: a trusted seat in the same role or group still satisfies
      the sign-off. If the paid seat was the only candidate, the close stays on HOLD with the plain
      reason "its only candidate(s) run through the paid gateway (external workers), which cannot
      give a sign-off; add a trusted seat to the candidates (or override to escape)".
    • close check (text and --json) and close signoffs plan name the seats that were left out
      (signoff_exclusions and excluded_external_workers in the JSON); they show seat names only.
    • A close record whose stored sign-off state has the wrong shape (an override list that is not a table,
      a sign-off set without a text id, an ack whose author is not text) now gives the normal "malformed
      close record" HOLD instead of a crash.
    • The roster still refuses to name a paid seat as a candidate when you change it; that check
      is kept.

    What you need to do: nothing. To let the close pass, name a trusted seat as a candidate, or use
    the existing close signoffs override.

  • A failed agenttalk gateway start no longer leaves the gateway without its stop file (#456).
    The command used to delete gateway.kill first and only then check the ports and ask the task to run. If a
    port was busy, or Windows or systemd refused to run the task (for example because it was disabled), the
    command failed but the stop file was already gone, so a later logon or restart-on-failure retry could start a
    gateway you had stopped. Now the ports are checked before anything is deleted. The stop file is deleted only
    while gateway start holds the gateway's startup lock, so a gateway launched at the same moment by another
    trigger waits, and after a failed start finds the stop file back and refuses. If the task cannot be started
    (or the command is interrupted), the stop file is put back exactly as it was; if you had none, none is
    created. If putting it back fails (for example a permission error), the command says so plainly, shows both
    errors and tells you to run agenttalk gateway stop to write it again; until you do, no stop file protects
    the gateway. A gateway launched by that start waits for the startup lock up to 45 s (it used to wait 10 s), so a
    slow but successful start request still succeeds. Once the task start request has succeeded, the stop file stays
    removed: when the gateway then takes too long to become ready, when the command is interrupted, and when the
    startup lock then fails to let go (the command exits with an error saying the start was requested and the
    gateway may be starting, and to run agenttalk gateway status and then agenttalk gateway stop if it should
    not run). If a failed start could not
    put the stop file back and the lock also failed to let go, you still see that the gateway is not protected and
    that agenttalk gateway stop writes the file again.

  • A seat that runs through the paid gateway can satisfy no required close lens, and a malformed close record is reported before anything reads it.
    The earlier fix (#447) kept such a seat (an "external worker") from giving a sign-off that belongs to a
    sign-off set. A required lens you declare by hand (close open --lens security --allow security:gw) and the
    lenses of an acceptance close were not covered, so the seat could still record, and have counted, an ack on them.

    What you will notice:

    • close ack from a paid gateway seat on any required lens (a sign-off set's, one you declared, an acceptance
      lens) is refused with a plain message and records nothing, also with --override. Optional lenses are not
      affected, because they feed no verdict.
    • An ack already stored on a required lens by such a seat (for example a hand-edited record) is ignored when
      the close is counted, and close check names the lens and says why. A trusted seat's ack still satisfies it.
    • The acceptance checks name an acceptance lens ack from such a seat and do not accept it as independent. Only the
      required acceptance lenses are checked; an optional note from such a seat is left alone.
    • An ack stored on a required lens now counts only while its author is, at check time, on the active roster and is
      not a paid gateway seat: the same rule a sign-off signature and a stored override already follow. An ack from a
      retired, removed, renamed-away or unknown author is ignored and close check names it. This deliberately turns
      an older GO that rested on a since-retired trusted reviewer's ack into a HOLD, until a current trusted seat acks
      the lens (or a current trusted lead records an override ack). A sign-off set's HOLD names the author whose
      signature was ignored. Nothing about a seat's past is looked up, so a damaged or older roster cannot bring a paid
      seat's ack back.
    • An acceptance close is the exception to that recovery: its frozen runner must give the accept, so if that runner
      leaves the team before publication the close stays on HOLD, a lead's override ack does not help, and the close
      needs a new attempt (a new close id with a plan that names a current runner; a published one continues with
      close reopen --successor). A recovery command is tracked in issue #463.
    • close check no longer reads a malformed close record (for example lens_acks that is a string) before
      reporting it: it gives the normal "malformed close record" HOLD instead of a crash. close publish still
      refuses a malformed record with its existing error (exit 2); it never publishes one.

    What you need to do: nothing, except that an older close that rested on a paid seat's ack, or on the ack of a
    seat that has since left the roster, is now on HOLD until a current trusted seat acks that lens.

  • Trying a command no longer risks the live message store. A seat's shell inherits the
    live store's location, and that outranks the folder it runs in. On 2026-10-08 a reviewer
    who meant to try three setup commands in a throwaway store ran them against the live one;
    nothing changed only by luck.

    What you will notice:

    • agenttalk scratch store makes a throwaway store in your scratch folder and prints the
      line that points a shell at it. The line clears the agenttalk settings the shell
      inherited, and agenttalk's own commands in that shell then refuse every other store,
      even by name. They also refuse a store whose .agenttalk folder links elsewhere, cannot
      be fully listed or holds a file with a second name outside that folder (a hard link),
      also when a link there leads to such a file, and scratch, janitor, comprehension
      and the assurance scan refuse to list, read, write or remove the folders they work on when
      those lie outside, janitor --release and --release-report included. Those two also
      refuse a git remote they would read that is not a folder inside the fence, a network
      address included, before git contacts it, and they hand git origin's folder exactly as
      it was checked. They refuse outright a partial clone and a repository, here or origin,
      that borrows objects from another folder through an alternates file, since git might
      read anywhere for those. Origin is also refused when anything in it could send git to
      another folder or address: a file in its place that names the repository, a commondir
      file (which shares another repository's objects and refs), a worktrees folder, an
      http-alternates file, a link or junction anywhere inside it, or settings that include
      another settings file, fetch from a promisor remote or name a pack's web address. None
      of these is read or followed: origin's settings file is only searched for those setting
      names, so one that merely mentions them is refused too. A file in origin with a second
      name (a hard link) is refused unless all its names lie inside the fence; the object
      files a local clone shares with this repository pass, as both their names are inside.
      Origin's address is read exactly as git gives it, spaces included, so a name with a
      space at either end is not taken for another repository. A windowless gateway run
      (the gateway's scheduled task) refuses a log outside the fence, or one with a second
      name outside it: it stops with exit 2 before making the log's folder, opening the log,
      setting its permissions or rotating it.
      A command can still check whether a path outside exists,
      reading nothing from it: janitor for the extra paths in its own settings, and any
      command while it looks for the store in the folders above the current one. Git still
      reads its own settings files and the metadata of the repository it runs in wherever
      they live, and the programs git starts, such as hooks that repository installs, run as
      they are.
      It guards against reaching a real store by accident, not against a deliberate attacker:
      it is a check inside agenttalk, not an operating-system sandbox. It assumes the files and
      settings it checked stay as they are until it has used them: other programs, plain file
      commands, links or hard links made after a command has checked, and remote or git
      settings changed in that time are not stopped.
    • agenttalk init refuses to re-init a store the shell only inherited, even one that has
      lost its settings file, and changes nothing. Before, it quietly showed that store's
      settings. To re-init it on purpose, name it with --root.
    • A roster change other than add first prints one line naming the store, when the store
      came only from the inherited setting.
    • The test suite can no longer open a store outside pytest's temporary folder: not
      through a folder that links outside, not while test modules are loaded, and not through
      the programs it starts, even ones given an environment of their own or several started
      at once. A test that tries fails, even when it expected an error, and a refusal outside
      any test fails the run. A test run that a test starts stays inside the fence it was
      started under: if its temporary folder would lie outside, also through a link in the
      folders pytest picks for itself, it stops before making or emptying anything, and its
      refusals are added to the report of the run that started it, so that run fails too.
      The fence starts once pytest has loaded the suite's own tests/conftest.py, so what
      that file and the modules it imports do while loading comes before it; today they only
      load code.

    What you need to do: nothing for your own bus commands (reply, send, progress, threads,
    knowledge); they work exactly as before. To try commands, run agenttalk scratch store
    first.

    Technical details:

    • src/agenttalk/store.py: AGENTTALK_STORE_FENCE names a folder. check_store_fence
      (called by Store.__init__) refuses a root outside it, or one whose .agenttalk folder or
      a link inside it resolves outside, or that has a folder os.scandir cannot list (only a
      missing path or a file counts as having nothing below it), or a file whose os.lstat
      st_nlink is above 1 (a hard link) unless every one of its names is found in that folder,
      matched by st_dev and st_ino, looking again up to three times 50 ms apart so that
      a lock another process is taking (_publish_text_no_replace links a private name to the
      lock's name) is not refused; a symlink there to a file adds that file to the count with
      no name of its own (_walk_names, comment 4236579569). check_second_names applies the
      same rule to the files it is given, links followed: a file with several names passes only
      when a walk of the whole fence finds all of them (known_only, which passes over places
      that lead outside or cannot be listed). A path that cannot exist because a plain file
      stands where one of its folders would be (NotADirectoryError on Linux and macOS, a
      missing path on Windows) counts as missing there, in the walk's look at the file a link
      leads to, and in worktree_release._refuse_alternates. Under the fence
      recovery.create_backup copies instead of hard-linking (#423); check_folder_fence refuses a folder outside it. All three
      raise StoreFenceError (a ValueError, so the command exits 2) and append the refused
      place to the file named by AGENTTALK_STORE_FENCE_REPORT, when that is set.
    • src/agenttalk/checkpoint.py: log_hook_error writes nothing into a refused store.
    • src/agenttalk/assurance.py: main checks the root, the run folder and the summary's final
      place (a relative summary is placed in the run folder, as write_artifact does) before any
      scan.
    • src/agenttalk/cli.py:
      • scratch store (--for, --task, --agents, --shell); its line keeps
        AGENTTALK_PY and, when a test run set it, AGENTTALK_STORE_FENCE_REPORT;
      • _inherited_root: the root came only from AGENTTALK_ROOT, with no --root and no
        fence;
      • the init refusal (any .agenttalk folder there, with or without config.json), and
        _note_inherited_root in roster;
      • scratch root/store, janitor (and, in every mode, --release and
        --release-report included, its scratch, temp and .worktrees folders, every
        registered worktree and the checkout --release names, before anything is listed,
        read, removed or asked of git) and _comprehension_root check the fence first.
      • src/agenttalk/worktree_release.py: check_fence, called by cmd_janitor in the
        release modes under the fence before release runs. Origin must name a local folder
        inside the fence: the address after url.<base>.insteadOf (git ls-remote --get-url),
        relative to the repository, links followed, in each form git looks it up
        (path/.git, path, path.git/.git, path.git), both as configured and as resolved.
        A partial clone (a remote.<name>.promisor remote or extensions.partialClone) is
        refused, and so is a non-empty objects/info/alternates file here or in origin
        (_refuse_alternates, which never opens it; origin's is located first, links on the
        way followed). Each form of origin that exists then goes through _refuse_elsewhere,
        from gitrepository-layout(5) and git-config(1): it must be a folder (a file there is
        a gitfile), with no non-empty objects/info/http-alternates, no commondir and no
        worktrees (_ELSEWHERE_FILES), nothing but plain files and folders at any depth
        (_refuse_links, lstat only: a symlink or any Windows reparse point is refused), and
        none of include, promisor, partialclone or blobpackfileuri, in any case, in the
        bytes of its config or config.worktree (_ELSEWHERE_SETTINGS). Every plain file
        _refuse_links accepts goes through store.check_second_names. The objects folder
        and origin's address are read with _read_git_path, which takes Git's output as
        bytes (_git(..., exact=True)) and removes only the final line end. A network or helper
        address, a path starting with ~, a file URL with a host or an escape, a .git file,
        or settings git cannot give are refused with store.refuse_unplaced. check_fence
        returns origin's folder, resolved, and release passes it to ls-remote and fetch
        in place of the name origin (so remote.origin.* settings such as uploadpack no
        longer apply); a url.<base>.insteadOf rule that would rewrite it is refused.
    • src/agenttalk/gateway_run_log.py: route_missing_output_to_log calls
      _check_store_fence before it makes the log's folder. Under AGENTTALK_STORE_FENCE that
      imports store and runs check_folder_fence and then check_second_names on
      gateway.log and its rotated copies (gateway.log.1, gateway.log.2); a refusal is reported and ends the run with
      SystemExit(2), as there is no output stream to explain it on. Without the fence it
      returns at once and store is not imported.
    • Tests: tests/_store_fence.py, loaded by tests/conftest.py: configure (from a
      trylast pytest_configure, before collection) sets the fence and the report and binds
      subprocess.Popen's own arguments, so that every child, whatever its env (inherited,
      keyword, positional, text or byte keys, empty or different), gets this run's fence and
      report, always in an environment of its own; a fence that environment already holds is
      kept only when it lies inside the one the child would get, so a narrower fence Popen
      captured stays narrow through os.posix_spawn. os.spawnve, os.spawnvpe and
      os.posix_spawn are wrapped the same way. os.spawnv, os.spawnvp, os.system and
      multiprocessing.process.BaseProcess.start inherit: they put the settings into
      os.environ for the launch, one at a time under a shared lock (_inheriting). A
      per-test fixture fails a test that
      added a refusal, and pytest_sessionfinish fails the run on any refusal, also from xdist
      workers. A run started under a fence (a nested run or an xdist worker) checks its
      --basetemp (without one, pytest's temporary root and its pytest-of-<user> and
      pytest-of-unknown folders, links followed) against that fence before getbasetemp
      makes or empties it (_keep_inside), and checks the folder getbasetemp made again
      before it becomes the fence: a folder outside it, the fence itself or one that holds the
      given report stops the run with exit code 4 and one report line. A
      nested run that is not an xdist worker adds its refusals to the given report at its end.
      tests/test_store_fence.py and tests/test_probe_store.py.
    • The listen and lead skills, in the Claude and Codex copies, say how to try commands, and
      so do the new-user manual and the agent manual.
  • The operations guide puts safeguards before the steps they protect, and three pages say
    what the code does.
    Before a runtime switch, a rollback or a resume refreshes the
    supervisor's scripts, the guide now has you disable its scheduled task and enable it again
    only as the last step before the host starts, so it cannot start half-refreshed or before a
    rolled-back gateway is in place; a restore disables it, and the paid gateway's task, from
    the start and enables each only just before it starts; after a restore, the paid gateway
    starts before any agent resumes; and a protected agent (the liaison or a lead) is started by
    the protected restart request, because the supervisor never starts a stopped one on its own.
    The roles page says what --work-head does on a review task (no stage requires it) and that
    a published HOLD exits with code 3; the overview says the emergency release comes from the
    liaison when there is one, and that a close can require captured knowledge (#444).

Changed

  • The README no longer lists a fixed problem as a known gap. It still said that the reply
    instructions the wrapper gives a seat leave out the seat's name and that a team must work
    around it. Since 0.100.0 the reply command the wrapper prints for each message carries
    --from <seat>, and every other agenttalk command a seat runs takes its name from
    AGENTTALK_SELF, which the wrapper sets, gateway-backed seats included (#354). Only the
    missing completion record (#178) is still listed. The roadmap's rows for the lessons
    look-up, the windowless gateway task, the worktree release, the domain-registry fix and the
    dependency batch now say they are done. No behavior changed.

  • Lead skills: renew timers before they expire, and watch named work without a fast timer.
    The lead skills already told a lead to set a check-in timer (about every 30 minutes) and a
    handover-note timer (about every 2 hours), but only to recreate them after a restart. In
    Claude Code these timers belong to the session and lapse after some days, and nothing says so.

    What you will notice in both lead skills (Claude and Codex):

    • The handover note now has one line per standing timer: its name, when it is next due, when
      it expires (unverified if that cannot be read), the latest time to renew it and when it
      last ran for certain. At each check-in and each restart the lead compares those lines with
      what is really scheduled and renews a timer before it expires, by replacing it, never by
      adding a second one. The text does not promise that a timer never lapses; a lead that was
      paused for long should assume the timers are gone and rebuild them from the note.
    • A new short section, "Watching named work", for work that needs close attention. The lead
      first agrees the next decision point, the signal that needs action, the longest acceptable
      delay, and the watch's expiry and wake budget. It prefers the seat's own message, then a
      background wait that ends when a message about that work arrives (best effort: the host
      can end it, so the check-in stays the backstop), and uses a short
      repeating timer only when there is nothing to wait for, as a temporary one that is
      cancelled when the work ends or the budget is used up. A wrapped lead keeps its turns short,
      never starts a second reader of its mailbox and never keeps a turn alive with a background
      wait. A watch lets the lead notice sooner; it cannot steer a seat in the middle of a turn,
      because a wrapped seat reads new messages between turns.

    What you need to do: a copy of the skill that was installed earlier is not replaced by
    agenttalk install-skills alone; it reports such a file as skipped. There is no option
    for a single skill, so the narrowest update is every agenttalk skill of one agent, without
    the development skills. For Claude Code: agenttalk install-skills --claude-only --no-devkit --dry-run lists the files that differ (marked would-skip), then the same command with
    --force replaces them. For Codex use --codex-only instead of --claude-only. --force
    replaces every listed file with the bundled one, including a skill you edited yourself (not
    only the lead skill), so copy any edit you want to keep first. Without --no-devkit, a
    forced run also replaces the edited development skills, and it writes them for both Claude
    Code and Codex even with --claude-only (the run uses the default skill folders unless you
    name others). Then restart or re-open the lead so it reads the new text.