Skip to content

Windows (Git Bash) support, plus this fork's own work - #2

Closed
notno wants to merge 31 commits into
mainfrom
windows
Closed

notno wants to merge 31 commits into
mainfrom
windows

Conversation

@notno

@notno notno commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Scope

This branch carries more than the Windows port.

main was reset to match upstream, so windows is now the only branch holding this fork's own work. Landing it brings two distinct bodies of change:

  1. The Windows port - native Git Bash / MSYS2 support, no WSL.
  2. Pre-existing fork-local work that predates the port (feat(bin): resolve harness identity from Windows process facts #1-docs: clarify live supervision constraints kunchenguid/firstmate#9): the status board generator, the Telegram process-event adapter, brief decision keys, backlog title parsing, crew-state rerun preference, and the AGENTS.md section 2 trim.

Review accordingly, or split the branch if only the port is wanted right now.

The Windows port

Firstmate previously assumed a POSIX host. Three classes of Windows-specific breakage are fixed here, each measured against a real Herdr session rather than inferred:

  • Pane PATH. A Git Bash pane opens a non-login shell, so /etc/profile never runs and /usr/bin is absent. Every #!/usr/bin/env bash shebang and the harness binary itself become unresolvable: the launch line dies at the prompt while the spawn still reports success. Firstmate now ships its own PATH in ahead of the launch line, keeping the pane's own entries behind it.
  • Argument path conversion. The Herdr client is a native Windows binary, so Git Bash rewrites arguments it reads as POSIX paths before the binary sees them. A harness /exit slash command arrived in the pane as C:/Program Files/Git/exit; the agent never stopped and the send still reported success. Conversion is now suppressed for exactly the calls whose payload is literal pane input, and left in place for calls that genuinely pass paths.
  • jq line endings. The Windows jq build writes in text mode. A single-value capture survives untouched because command substitution strips the whole trailing CRLF, but a multi-line capture keeps a carriage return inside every line but the last, and those then ride inside workspace, tab, and pane identifiers. Multi-line reads are now filtered.

Earlier commits on the branch cover path-form conversion, Windows-pid-space pane resolution, socket identity, mode-bit capability probing, and Windows installers for the Herdr, treehouse, and lint toolchains.

docs/windows.md is the operator manual for all of it.

Upstream merge

origin/main is merged in (11 commits). Two files conflicted:

  • AGENTS.md - kept the fork's trimmed section 2 layout block from feat(bin): resolve harness identity from Windows process facts #1. It is a deliberate change to pointers, not stale text, and docs/configuration.md remains the owner of every entry it no longer lists.
  • bin/fm-brief.sh - kept the fork's keyed needs-decision instructions, which are a superset of upstream's and consistent with the surrounding un-conflicted key language.

Three clean auto-merges were reviewed semantically rather than trusted: 52d20f1 (ask-user decoupled from yolo) and the Windows PATH fix both land intact in bin/fm-spawn.sh; 5b6d0fb (GitLab merge requests) merges without interaction.

Verification

Run on Windows 11, Git Bash, inside a real Herdr 0.8.2 session.

  • bin/fm-lint.sh - clean (ShellCheck 0.11.0, actionlint 1.7.12, 3 workflows valid).
  • bin/fm-doc-audience-check.sh - ok, 74 surfaces, 277 local links.
  • tests/fm-brief.test.sh - all pass.
  • tests/fm-backend-herdr.test.sh - 67 pass, unchanged by the merge.

Known issue

test_projection_close_emptying_after_focus_uses_pane_death_without_move fails on Windows. It was verified pre-existing against an untouched checkout at this branch's base, is unrelated to the carriage-return class, and is not addressed here. The suite stops at the first failure, so further Windows failures may sit behind it.

Note for reviewers

One upstream commit, 266fdb9, adds its own uname-conditional platform branch in bin/fm-busy-event.sh while this branch adds bin/fm-platform-lib.sh. The two layers do not conflict today but are growing independently and are worth consolidating before they diverge further.

nathan-rosquist and others added 30 commits August 24, 2026 07:31
Starting 40 shells costs about 1.5s under Git Bash against milliseconds on
Linux, so several fixed-second budgets in the watcher-lock suite expired
mid-race and reported failures that were really the clock.

tests/wake-helpers.sh gains FM_TEST_PROC_WAIT_SCALE, 8 on msys/mingw/cygwin
and 1 elsewhere, applied inside wait_for_exit so all nine call sites are
covered. Raising an upper bound is free on a fast host because each loop
breaks the moment the process is gone. bin/fm-watch-arm.sh already carries
the same platform split for the same reason.

The concurrency case no longer holds the lock for a fixed second. It now
barriers until all 40 contenders have finished their attempt, which is what
the case actually means. Under Git Bash a backgrounded shell can begin
executing well after it is launched, so the old winner exited before the last
contender started; a late contender then correctly reclaimed a dead-pid lock
and scored as a second winner for behaving exactly as designed.

The lock itself needed no change. Instrumented under 40 contenders, exactly
one process cleanly created the lock and the apparent second winner had
explicitly reclaimed the dead owner's pid through the intended stale-reclaim
path.

(cherry picked from commit 98d311f)
fm_harness_ancestry_pids walks up to 16 parents to find the harness process
that owns this home's session lock. On a Git Bash (MSYS) host that walk cannot
succeed, because the MSYS process tree is severed from the Windows one: an MSYS
shell launched by a native Windows parent reports ppid 1, ps -W lists claude.exe
but reports ppid 0 for every native process, kill -0 cannot see a Windows pid
because the two are different number spaces, and ps -o is unsupported. A ps
shim alone therefore cannot fix this.

The consequence is not cosmetic. With no ancestry, bin/fm-lock.sh refuses the
session lock and bin/fm-claude-stop-autoarm.sh then exits 0, silently disabling
the whole watcher and turn-end continuity layer. The home looks fine and
supervises nothing.

Adds bin/fm-winproc-lib.sh, a single owner for what the Windows kernel reports
about a given process, and teaches bin/fm-session-lock-lib.sh to consult it
before falling back to the existing POSIX walk. The bridge reports raw process
facts only; the harness decision stays with its existing owner.

Two independent evidence sources, because they fail for unrelated reasons and
the coding guidelines require that no single vendor-emitted string be
load-bearing:

  - the harness-exported pid, confirmed against one ps -W row, about 0.24s
  - a memoized Get-CimInstance Win32_Process snapshot walked up 16 hops,
    about 1.5s

Either source alone carries a positive verdict. With neither, the ancestry
refuses rather than guessing.

Liveness asks Windows first and falls through to the original kill -0 path when
Windows does not describe the pid, because a recorded pid can belong to either
number space: the native resolver writes a Windows pid while an MSYS-side
process writes an MSYS one. Answering only for the Windows space would call a
live MSYS process dead. The fall-through cannot invent a live process, since a
genuinely dead pid fails both checks.

Linux and macOS are unaffected. Every entry point gates on
fm_winproc_available, which probes /proc/$$/winpid, so off Windows the bridge
returns 1 and callers fall through untouched. Capability detection, never
uname. The new wait-budget multiplier is 1 on every non-MSYS host.

tests/fm-winproc-lib.test.sh adds 14 assertions that run on Linux CI rather
than skipping, which is the point, since this decides whether a home may hold
the session lock at all. It drives the two evidence sources apart through the
library's documented seams and asserts each isolation is not vacuous, plus the
inert path, the fail-closed path, and pid-reuse rejection. It also pins the
backslash conversion itself: a no-op conversion shipped during development and
no test noticed, because the harness regex is unanchored and matched the
unconverted string by luck.

tests/fm-session-lock-ancestry.test.sh sets FM_WINPROC_DISABLE=1, because every
case in it simulates a POSIX host behind a fake ps and would otherwise be
answered by the bridge before the fixtures were reached.

Verified on Windows 11, Git Bash MINGW64_NT-10.0-26200, bash 5.3.15:
fm_harness_ancestry_pids went from no output and exit 1 to the harness pid and
exit 0, and bin/fm-session-start.sh reports a lock acquired against a pid that
matches the live claude.exe exactly. The structural walk resolves the same pid
with the exported pid unset, so neither source is doing all the work.

(cherry picked from commit 36a2fec)
Sourcing bin/fm-winproc-lib.sh unconditionally broke three CI shards for
reasons that had nothing to do with the cases being run. Eighteen test fixtures
build a partial bin/ holding only the scripts under test, so
bin/fm-session-lock-lib.sh arrives there without its new sibling, the source
fails, and every function the fixture actually wanted is gone. It surfaced as
tests/fm-cursor-primary.test.sh, tests/fm-claude-stop-autoarm.test.sh, and
tests/fm-pi-watch-extension.test.sh reporting unrelated assertions with a
No such file or directory line embedded in the output.

The bridge is now sourced only when readable, with fm_winproc_available stubbed
to return 1 otherwise. That is the exact gate both call sites already check
first, so a home with no bridge behaves like the non-Windows home it resembles:
it has no Windows process facts to offer. It cannot mask a broken install on an
MSYS host either, because the callers fall through to the POSIX walk, which
still refuses with its original "cannot locate harness process in ancestry"
diagnostic rather than passing.

tests/fm-winproc-lib.test.sh builds that partial layout and requires the
library to load and keep answering. Confirmed by mutation: restoring the
unconditional source fails the new case with the identical
No such file or directory message CI reported.

Also corrects an earlier judgement. Two Windows failures were called
pre-existing on the evidence that they failed identically with
FM_WINPROC_DISABLE=1. That comparison could not distinguish this bug, because a
failing source runs before any seam is read. The two remaining Windows failures
were re-checked against pristine main and are genuinely pre-existing, but the
original reasoning was not sound.

(cherry picked from commit e83edb1)
One owner for POSIX-vs-native path conversion on Git Bash: cygpath -m for values handed to native binaries, cygpath -u for MSYS-side comparison, identity fallback off MSYS or when cygpath is absent or fails. Tests drive both branches through the FM_PLATFORM_UNAME seam and a fixture cygpath.

(cherry picked from commit d874a6a)
GOTMPDIR is exported in the native form so go.exe resolves it; TASK_TMP records stay POSIX. path_is_ancestor_of normalizes both operands so a Windows spelling of the same directory cannot slip past the prefix test, and the secondmate home guard now also refuses drive roots (/c), which canonicalize to /<letter> rather than / on MSYS.

(cherry picked from commit ddd7806)
The spawn now exports GOTMPDIR in the native form on Windows, so the two suites that pinned the literal /tmp form compute the expectation through fm_path_native instead. Identity off MSYS keeps Linux CI byte-identical.

(cherry picked from commit 9d36163)
0.8.2 is the earliest stable release with an official Windows asset; every stable tag through 0.8.0 ships Linux and macOS binaries only. All five SHA-256 pins come from the GitHub release asset digests, the stale ogulcancelik/herdr org moves to herdrdev/herdr, and the CI pin assertion follows. The Windows .zip installs herdr.exe beside its app-local ConPTY runtime, the layout Herdr's own Windows installer validates. Verified end to end on Git Bash: install rc 0, idempotent rerun byte-identical, herdr 0.8.2 protocol 20. Note: 0.8.2 is not yet in the verified real-Herdr backend matrix (docs/herdr-backend.md:4); the matrix decision is still open.

(cherry picked from commit 123db8c)
The fleet-state tripwire required a running default session before any lab work, which made the whole live lane unrunnable on a machine that never started herdr. Safety comes from the byte-identical after-comparison, not the starting state: snapshotting a stopped default and requiring it identical afterward also proves the lab never started the default server. New coverage: a stopped default provisions and snapshots running:false, and a default that changed running state hard-fails teardown with evidence retained. Verified live on Windows: 19 assertions now pass across the three real-herdr suites, up from zero, with the default session byte-identical before and after.

(cherry picked from commit 76c07e5)
win32 Python has no socket.AF_UNIX, so the attribute lookup raises AttributeError before any OSError can, and both helpers died with a traceback and exit 1 instead of their designed unavailable exit 2. The adapter already discarded the stderr and normalized the failure, so observable behavior is unchanged; the helpers now keep their own contract. Verified on Windows: both exit 2 cleanly where they crashed before; eventwait smoke unchanged at 1 ok / 1 not-ok.

(cherry picked from commit 1982d25)
…o the herdr lock

The probe moves from fm-pr-lib.sh into fm-platform-lib.sh as fm_platform_fs_honors_modes (env override renamed FM_PR_MODES_HONORED -> FM_FS_MODES_HONORED), fm-pr-lib delegates, and the herdr presentation lock namespace keeps its directory, symlink, and owning-uid checks always while requiring mode 700 only where the filesystem can store a mode. On a noacl Git Bash mount mkdir -m 700 is a silent no-op and the namespace reads 755 forever, which made the lock permanently unacquirable. Note: on Windows the lock still fails earlier today - herdr reports a native-form socket path that the socket canonicalizer refuses - so this lands as the second of two sequential fixes.

(cherry picked from commit 74605c9)
Herdr on Windows reports native socket paths (C:\Users\...\herdr.sock), which the canonicalizer's absolute-path test read as relative and refused, so the presentation session lock could never be acquired there. The incoming socket is folded through fm_path_posix first, exactly because this function is the single identity owner: C:\x, C:/x, and /c/x must all collapse to one lock identity; off MSYS the fold is the identity function. With this and the mode-probe change together, the live smoke lane's three presentation-lock warnings drop to zero, the lock namespace is created, and every lock taken is released. New seam-driven regression coverage collapses all three spellings and pins the off-MSYS refusal of drive-lettered paths.

(cherry picked from commit f72503f)
herdr's shell_pid is a native Windows pid on Windows, which MSYS ps cannot address (no -o at all, and its PID column carries MSYS pids - a naive shim would cross namespaces). The idle-shell proof's three ps reads become one fm_winproc_pid_census snapshot (both counts from one moment, so they cannot straddle an exit), the recognized-shell checks share one normalizer that also strips the .exe herdr reports for Git Bash panes, and signaling routes through /usr/bin/kill -W, which is blind to processes outside the MSYS runtime and so can never signal a native process (verified: HUP ends a real idle pane shell, KILL escalates past a HUP-ignoring one, and a foreign native pid reads as No such process). stat= has no Windows source and is relaxed there with the reasoning in place; the POSIX path is byte-identical. Verified live: the idle-shell proof passes and the pane-death close ends a pane where it previously refused, with zero lock warnings and the default session untouched.

(cherry picked from commit f6b95ad)
Herdr 0.8.2 protocol 20 is the earliest stable release carrying a Windows
x86_64 asset, so the installer pins it on every platform rather than keeping
a separate Windows pin. Record that choice, add 0.8.2 to the verified
version list and the floor table, and note that its row is the one entry not
produced by the macOS aarch64 sweep.

Add a Windows x86_64 verification section covering the measured live lane and
the four adapter paths that carry the platform's differences: the native
socket-path fold, the probe-and-accept lock mode gate, the bash.exe shell
normalizer, and the Windows-pid-space process reads.

Record the one open blocker plainly. foreground_cwd is always null on
Windows and a pane's own cwd freezes at creation, so the worktree-discovery
poll reads nothing and a crewmate or scout spawn refuses instead of
recording an unverified directory. The guard is behaving correctly, but an
ordinary Herdr spawn cannot complete on this platform until the directory is
read from inside the pane rather than off the terminal.

(cherry picked from commit c6d25d6)
Treehouse already publishes Windows assets at the pinned v2.0.1, so this
needed no version bump, unlike the Herdr installer port. The Windows arm
differs from the existing platforms in two ways only: the asset is a .zip
rather than a .tar.gz, and the binary inside it is treehouse.exe.

Add ARCHIVE_FORMAT and BINARY so the extract, install, and version-check
steps stay one code path across every platform instead of growing a parallel
Windows branch. Check for unzip before spending the download, matching the
Herdr installer's ordering. The sha256 pin comes from the GitHub API digest
field for treehouse-v2.0.1-windows-amd64.zip.

Verified live on Windows 11 x86_64 under Git Bash: the install completes with
a matching checksum and the installed binary reports the exact pin v2.0.1.
Linux CI is unaffected; it never selects the new arm.

(cherry picked from commit 3e784d2)
Spawn discovered a crewmate or scout worktree by sending `treehouse get` to
the endpoint and polling the pane's working directory until it left the
project. That required a backend able to report a live foreground path.
Herdr on Windows cannot: foreground_cwd is always null and a Git Bash pane's
own cwd freezes at creation, so no poll ever observed the pane moving and
every crewmate and scout spawn refused after sixty seconds. The refusal was
correct, but the platform could not spawn at all.

Acquire the worktree in fm-spawn.sh's own shell instead. `treehouse get
--lease` is non-interactive, opens no subshell, and prints only the
worktree's absolute path, so the path is known before the endpoint is told
anything. The endpoint is then sent a plain cd, and the pane's own position
is confirmed best-effort: a backend that reports a path must agree, while an
empty read means the platform cannot answer and the spawn proceeds. The ship
brief's own isolation assertion is the agent-side backstop there.

validate_spawn_worktree is unchanged and still gates every spawn. It reads
the filesystem rather than the terminal, so it is the isolation guarantee on
every platform regardless of where the path came from.

A lease is durable, so every failure between the lease and the published task
record hands it back through the existing exit trap or a pool slot is burned
permanently. The rollback disarms once state/<id>.meta names the worktree,
matching the boundary the Orca worktree cleanup already used: returning a
worktree a live record still points at would hard-reset it and hand it to
another task.

On Windows treehouse prints a native backslashed path, measured live, which
no POSIX comparison here can use and which teardown could not match against
its record. Fold it through fm_path_posix at the single point of capture, as
the Herdr canonical socket path is folded. The regression covering that runs
on every platform through the library's own uname and cygpath seams, and
fails without the fold.

Remaining changes are the test stub every spawn-shaped fixture now needs, and
comment and documentation corrections wherever the old mechanism was
described.

(cherry picked from commit 0b11112)
Windows support was spread across backend guides and a verification record,
so an operator had no single place that answers "what do I need, and what is
different here". Collect that into one operator-current page and route to it
from the README and the audience inventory.

The page leads with the symbolic-link setting because it is the one whose
absence fails silently: without it every lock quietly fails to be taken,
supervision never starts, and a checkpoint reports a quiet fleet it never
actually observed. It also covers the line-ending trap a fresh clone hits,
the pinned installers, the Herdr pane shell requirement, the current
platform limits and what each degrades to, and troubleshooting keyed by the
symptom rather than the cause.

The backend guides keep ownership of their own setup and safety boundaries,
and the verification record keeps the measured evidence; this page carries
only what Windows changes and points at those owners.

(cherry picked from commit 3ebb6a3)
The previous commit carried an unintended one-line edit: an `expect_code`
failure message in the trace-context spawn test had the captured spawn output
appended to it while a suspected regression was being chased. That regression
turned out not to exist, and the edit was never meant to ship.

It changed only what a failure prints, never whether the case passes, so this
restores the original message and nothing else. Fixed forward rather than by
amending, because the carrying commit is already pushed.

(cherry picked from commit 846b25a)
The page assumed a reader who already had firstmate running and only
needed the platform's differences. A first-time Windows operator has no
entry point before the symlink setting, so add three sections ahead of
it: what firstmate is, how to install it here, and how to run it.

Each new step points at the section that owns its detail rather than
restating it, and the context section points at the README and the
architecture doc. The install section also carries a copy-paste prompt
for handing the setup to Claude Code.

(cherry picked from commit 013f428)
The context section said there is no application to install. Firstmate
itself has no installer, but Herdr, Treehouse, ShellCheck, and actionlint
are all real installs the same page then walks you through, so the claim
read as false to anyone about to do that work.

Keep the accurate half - the clone is the distro - and say plainly that
the separately installed tools are what the install section is for.

(cherry picked from commit 58061d3)
The install step and the Claude Code prompt both told a reader to clone
the upstream repository. Upstream carries no Windows support at all - not
even this page - so following that step produces a checkout where nothing
else on the page applies, with no error to explain why.

Point both at the windows branch of the fork, and say plainly that
upstream is not a substitute.

(cherry picked from commit 638c39f)
A Herdr pane opens a non-login shell on Windows, so /etc/profile never runs and
the pane's PATH carries Git for Windows' `bin` directory, which holds only bash,
sh, and git. /usr/bin is absent there, so `env`, every `#!/usr/bin/env bash`
shebang, and the harness binary itself are unresolvable: the launch line died at
the prompt while the spawn still reported success, so no agent ever started and
nothing said so.

Export firstmate's own PATH into the pane ahead of the launch command, through
the same pre-launch channel GOTMPDIR already uses, keeping the pane's own
entries behind ours. Same root cause as the CLAUDE_CONFIG_DIR forward beside it:
the backend daemon does not inherit firstmate's environment.

Proven live on Windows: a real Claude Code agent now starts from
`fm-spawn.sh --scout --backend herdr --harness claude` in an isolated rig and
guarded lab. The new tests fail on pre-fix code and pass on fixed code.

(cherry picked from commit 1171e51)
… path

The Herdr client is a native Windows program, so under MSYS Git Bash rewrites
any argument it reads as a POSIX path before the client ever sees it. Every
call that sends literal input to a pane went through that conversion, so a
harness slash command such as `/exit` arrived as `C:/Program Files/Git/exit`.
Measured in a live pane: `fm-control.sh <id> exit` delivered that mangled text,
the agent never stopped, and the run ended at `exit=unconfirmed`.

Route the three literal-payload calls - `pane run`, `pane send-text`, and
`pane send-keys` - through a wrapper that suppresses argument conversion on
MSYS only. Every other herdr call keeps converting, because those do pass real
paths a native client must receive in native form.

Verified live on Windows 11 with herdr 0.8.2 and a real Claude agent:
`fm-control.sh exitproof-w1 exit` now reports `stopped`, where the same command
before this change reported `exit=unconfirmed; the agent did not stop within
30s`. Three portable regressions drive fm-platform-lib's FM_PLATFORM_UNAME seam
and read the fake client's opt-in environment log; disabling the MSYS branch
makes the first of them fail.

(cherry picked from commit 4c275f5)
…line reads

The Windows jq build writes its output in text mode, so every line it emits
ends CRLF. A single-value capture survives untouched, because command
substitution strips the whole trailing CRLF, but a multi-line capture keeps an
interior CR on every line but the last. Those CRs then ride inside workspace,
tab, and pane ids and into text joined from them.

Two measured consequences on Windows, both caught by this suite: the ambiguous
workspace refusal named its candidates as `w1<CR> w7`, and create_task read
every husk tab id but the last with a trailing CR, found no pane for it, and
refused with "already exists" instead of replacing it.

Add fm_backend_herdr_strip_cr and route the five multi-line jq reads through
it. It is a plain filter rather than an MSYS branch so the same bytes come back
on every platform, which is what lets one portable regression cover it. The
dup_tabs read strips in a second step instead of extending its pipeline,
because jq's exit status is load-bearing there and the filter's own success
would hide a parse failure.

The suite goes from 22 passing to 67 on Windows. It exits at the first failure,
so the cases after the ambiguity one had never run on this platform; the
remaining failure at the projection close path reproduces at HEAD without this
change and is untouched here.

(cherry picked from commit dd78578)
@notno

notno commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: wrong shape. This branch mixed the Windows port with six unrelated fork-local features. The branch has been rebuilt to carry only the Windows work, and each feature now sits on its own branch cut from clean main. The pre-split state is preserved on archive/windows-pre-split.

@notno notno closed this Aug 24, 2026
notno added a commit that referenced this pull request Sep 8, 2026
…r replies (#2)

* feat(procevent): add a Telegram process-event adapter

An operator reply from a phone now wakes firstmate through the existing
process-event runner instead of sitting unseen in a chat.

bin/fm-procevent-telegram.sh long-polls getUpdates from an offset one past
the recorded cursor and returns the first non-empty batch as a captured
result. The poll only reads the cursor: the handler advances it after fully
handling the messages, so a crash never advances past unhandled replies. A
restart before that advance re-captures the same updates, so the header
states plainly that handlers must treat any update id at or below the cursor
as already seen.

The bot token is read from a private file and passed to curl through a stdin
config, so it never reaches the argv, stdout, or stderr. Transient transport
and server errors retry with bounded backoff; HTTP 401 and 403 exit non-zero
so the runner surfaces a rejected token rather than spinning. A message
stream never self-terminates, so terminal always keeps the source armed and
retirement stays an explicit operator action.

* no-mistakes(review): add telegram cursor ack, webhook/rate-limit and batch guards

* no-mistakes(review): serialize telegram cursor ack, clarify 409 and handler steps

* no-mistakes(review): document ack-before-handled order, dedupe ack teardown

* no-mistakes(document): document telegram adapter env knobs and toolbelt row

* no-mistakes(lint): fix shellcheck SC1007 and SC2012 in telegram adapter

* no-mistakes(review): gate duplicate telegram emissions, keep failed child stderr

* no-mistakes(review): redirect child stderr to file, record telegram handoff after delivery

* no-mistakes(document): document kept failure diagnostic for process-event runner

* no-mistakes(document): anchor procevent help extraction to header end marker

* no-mistakes(review): drop emitted-marker dedupe for at-least-once polls, anchor help extraction

* no-mistakes(document): list telegram ack ordering in procevent skill description

* no-mistakes(document): split telegram ack skill guidance to one sentence per line

(cherry picked from commit 457c4d8)
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.

2 participants