diff --git a/CHANGELOG.md b/CHANGELOG.md index a0f7ac5..3a689ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ ### Fixed - **A Step counts as closed only when it says `- Status: done`, so a word the gate did not know can no longer archive a Plan that is not finished.** `hooks/completion-gate` read the field as a blacklist — `pending`, `in_progress`, `blocked` were named and everything else was finished — so the word `completed`, a misspelling, an empty value, and a Step with no `- Status:` line at all all passed. Measured 2026-10-27 by driving the gate directly: `completed`, `complted`, `skipped`, `wip`, `DONE`, an empty value and a missing line each returned exit 0 and "finished". `hooks/task progress` has only ever accepted `pending|in_progress|done|blocked`, so those spellings could not have been written by the tool — they are the result of hand edits made precisely because the two sides disagreed. Closed is now a whitelist of one word, and it is the same word the writer accepts. The block is located by its `### Step ` heading anywhere in the Plan rather than only under `## Steps`, because that is how `task progress` finds the block it edits; a reader that skipped the other sections would see a different set of Steps than the writer changes. **Knowingly still open, and pinned by an assertion so it stays a decision**: a Plan with no `### Step` block at all is not refused, because "must a Plan have Steps" is a separate rule this change does not make. +- **The Windows smoke suite no longer stops dead when its stdin is open and silent.** `hooks/smoke-test-windows.ps1` called `run-hook.cmd session-start` with no stdin of its own, so the hook inherited the runner's — and `session-start` reads its event with `cat`. From a terminal that read never reaches EOF, so the suite printed `WINDOWS LAUNCHER ARGS PASSED` and then hung indefinitely; a red assertion reached a person as an unexplained freeze rather than a message. The cause is the inherited stdin, not the throw the report named: probing with `throw` at eight positions, every one before that call printed and exited 1 in under 14s, while every one after it hung — the boundary is exactly `Invoke-SessionStart 'pr'`. The bash suite already carried this lesson for its own `session-start` call (`hooks/smoke-test`, `< /dev/null`); the PowerShell one is now given an empty event the same way, through `cmd /c "... < nul"` rather than `'' |`. **An empty PowerShell string is not an empty stdin** — it is encoded and written as a line, and which line depends on `$OutputEncoding`: measured 0x0d 0x0a on the author's machine and a BOM on the CI runner, where `session-start` rejected it as invalid JSON and the suite failed ([run 40712165629](https://github.com/hkwuks/TaskFlow/actions/runs/40712165629)). A redirection is zero bytes on every host. Measured 2026-10-28: before, still silent at 180s and killed; after, four `PASSED` banners in 27s. **CI could never have caught it** — a runner's stdin is empty, so `cat` sees EOF at once and every windows-latest job has been green regardless. +- **The Windows suite now checks this itself, by re-running with an inherited open stdin and failing if it does not finish.** The check has to live there rather than in `hooks/smoke-test`, because that suite does not run on the Windows runner at all — the workflow gates it on `runner.os != 'Windows'` — so an assertion written there is skipped by every CI job, and the one platform that can reproduce the defect is the one platform that never executes the check. `hooks/smoke-test-windows.ps1` is the only TaskFlow check a Windows runner runs, which makes it the only place this can be caught automatically. It spawns itself with `RedirectStandardInput` set and never written: a pipe that is open and silent, the shape a terminal gives a command a person typed and the shape CI does not have. `TASKFLOW_STDIN_SELFCHECK` bounds the recursion to one child. Verified by mutation: with the `session-start` call reverted to inheriting, the child is killed at the 90s deadline and the suite fails naming the read; with `< nul` in place it exits 0 in about 28s, and the whole suite takes 51s. +- **`hooks/smoke-test-windows.ps1` must stay 7-bit ASCII, and now asserts it.** Windows PowerShell 5.1 reads a `.ps1` with no BOM using the system ANSI code page rather than UTF-8, so a literal non-ASCII character is decoded into bytes the parser does not expect: measured 2026-10-28, an em dash in a `throw` message arrived as three UTF-8 bytes, the third swallowed the closing quote, and the whole file failed to parse with "The string is missing the terminator" ([run 40717070366](https://github.com/hkwuks/TaskFlow/actions/runs/40717070366)) -- after the suite had passed on Linux and macOS, which read the same bytes as UTF-8. The file was already ASCII-only for this reason and builds its Chinese fixtures from `[char]` codes; the rule was known and unenforced. The first thing the suite does now is read itself and refuse to run if any non-ASCII character is present, naming the line and code point. Verified by mutation: injecting one em dash makes it fail at the guard. +- **The same inherited stdin reached one more `session-start` call, in `hooks/smoke-test` itself.** Its `== Windows launcher finds Git Bash ==` section ran `cmd.exe //d //c ... session-start` with no stdin, so it had the identical failure mode: measured 2026-10-28, it never returned against a held-open stdin and returned at once with `< /dev/null`. Given the empty stdin it now has, the same way and for the same reason. + ### Compatibility diff --git a/TaskFlowDocs/2026-10-28-windows-smoke-stdin/plan.md b/TaskFlowDocs/2026-10-28-windows-smoke-stdin/plan.md new file mode 100644 index 0000000..caecb37 --- /dev/null +++ b/TaskFlowDocs/2026-10-28-windows-smoke-stdin/plan.md @@ -0,0 +1,159 @@ +# Plan — Windows 套件的 stdin 卡死 +> Task version: v1 +> Status: checking + +No spec required — small, self-contained task. + +## Reference Pointers + +- `hooks/smoke-test-windows.ps1:31` — 待修的那一行。 +- `hooks/smoke-test-windows.ps1:96` — 卡死触发点 `$first = Invoke-SessionStart 'pr'`。 +- `hooks/session-start:29` — `event_json="$(cat)"`,契约所在。 +- `hooks/smoke-test:564-566` — bash 套件对同一坑的注释与 `< /dev/null` 修法。 +- `hooks/smoke-test:740-744` — Windows 段,新断言的落点。 +- `CONTRIBUTING.md:21-33` — worktree 与「会话开始必须先隔离」的规则。 + +## Related Tasks + +- 无父子关系。来源记录为 `TaskFlowDocs/todo.md` 的 `TF-20261025-645ef4`。 +- 与 `2026-10-25-gate-desync-diagnosis`(已归档)同源:该任务在验证时首次撞见本缺陷,但未修。 + +## Skills / Tools Used + +- [PRD] `agent-skills:spec-driven-development` — purpose: 把一条措辞有误的 todo 收敛成可验收的需求;outcome: succeeded; incorporated: 需求改写为 R1–R7 与 A1–A6,并把「修的是继承 stdin,不是 throw」写进 R7,因为原条目会让后来者找错对象。 +- [Plan] `agent-skills:planning-and-task-breakdown` — purpose: 决定两处改动的先后:先修那一行,再让断言先红后绿;outcome: succeeded; incorporated: 拆成 Step 1(修 + 断言)与 Step 2(变异验证),因为断言的正确性只能由「撤销修复后它变红」来证明,二者是同一次验证的两半。 +- [Test] `agent-skills:test-driven-development` — purpose: 确认这条断言在被测行为上成立(撤销修复即红)而不是只在检测自己的写法;outcome: succeeded; incorporated: 采用「Prove-It」顺序,并将 Step 2 设为前置的真实变异验证,而不是写完断言就宣布完成。 + + +## Preconditions + +- [x] Applicable repository documents and personal rules inspected; precedence/conflicts recorded. — 已读 `CONTRIBUTING.md`(worktree 与分支规则)。 +- [x] 隔离先于文档 — 已完成:工作树 `.worktrees/windows-smoke-stdin`,分支 `fix/windows-smoke-stdin`,基于 `main`(`b47ea3f`)。 +- [x] 能力账本环境已核对 — 本会话接线的 hook 来自已安装插件 1.1.8(写五字段 `released` 行),而仓库里的 `hooks/task` 是 1.1.9(要求六字段)。两者对同一份账本的格式不相容,本任务的 `approve` 因此改用与门同版本的已安装 `hooks/task` 执行;把插件升级到 1.1.9 属项目路径之外的动作,本任务不做。 + +## Approval + +- Status: approved +- Approved by: user +- Approved at: 2026-10-28 22:25 +0800 +- Approved version: v1 +- Approved scope: PRD / Plan + +## Steps + +### Step 1 — 修那一行,并补上端到端断言 + +- Goal: `smoke-test-windows.ps1` 在任何 stdin 形态下都能跑完;`hooks/smoke-test` 里新增一条只在修复撤销时才变红的断言。 +- Dependencies: 无。 +- Files: `hooks/smoke-test-windows.ps1`、`hooks/smoke-test`、`CHANGELOG.md`。 +- Implementation checklist: + - [x] `hooks/smoke-test-windows.ps1:31` 改为 `$output = '' | & "$here\run-hook.cmd" session-start 2>&1`,上方补注释说明理由(引用 `hooks/smoke-test:564-566` 的同一条理由)。 + - [x] `hooks/smoke-test` 的 Windows 段之后新增一小节:用打开的 stdin 再跑一遍子套件,有界等待,超时 FAIL 并指名原因。 + - [x] `mkfifo` 不可用时断言必须明确失败,不得静默跳过。 + - [x] `CHANGELOG.md` → `[Unreleased] / ### Fixed` 写一条,点明修的是 stdin 继承而非 throw。 +- Acceptance: PRD 的 A1–A4、A6。 +- Verification: `bash hooks/smoke-test` → `ALL SMOKE PASSED`;`powershell.exe -NoProfile -File hooks/smoke-test-windows.ps1` 在 stdin 打开 / `/dev/null` / FIFO 三种形态下均 rc=0。 +- Rollback: `git revert` 该提交;`hooks/smoke-test-windows.ps1` 的还原是单行。 +- Status: done + +### Step 2 — 变异验证:撤销修复,断言必须变红 + +- Goal: 证明 Step 1 的断言确实在检测这件事,而不是只在检测自己。 +- Dependencies: Step 1。 +- Files: `hooks/smoke-test-windows.ps1`(临时还原,验证后逐字节恢复)。 +- Implementation checklist: + - [x] 把 `:31` 临时还原成 `$output = & "$here\run-hook.cmd" session-start 2>&1`。 + - [x] 运行 `bash hooks/smoke-test`,确认它在**新增断言处**变红并打印原因。 + - [x] 逐字节恢复,`diff -q` 确认一致,再跑一次 `ALL SMOKE PASSED`。 +- Acceptance: PRD 的 A5。 +- Verification: 两次运行的输出对比写进 `## Verification / Review`。 +- Rollback: 无需,验证完即恢复。 +- Status: done + +## Checkpoints + +- Step 1 后:`bash hooks/smoke-test` → `ALL SMOKE PASSED`。 +- Step 2 后:变异那次变红、恢复后变绿,两个结果都记录在案。 + +## Verification / Review + +- 2026-10-28 Step 2: Mutation reverted the fix and the new assertion failed at its deadline + +- 2026-10-28 Step 1: Bash suite passes; mutation reds at the 120s deadline; restored byte-identically + +### 本机实测(2026-10-28,WSL + Windows PowerShell 5.1) + +本机只能跑到 Windows 段以下的一部分:`cmd.exe` 拒绝把 WSL 的 UNC 临时目录当 cwd(`UNC 路径不受支持`),所以 `hooks/smoke-test` 里的 `== Windows launcher finds Git Bash ==` 一节的 `grep` 拿不到 JSON。**这是我进入这段代码前就有的环境限制**:用 `main` 上同一行(没有 `< /dev/null`)在本机跑,输出完全相同(只有 cmd 横幅),而加 `< /dev/null` 后 rc=0 —— 也就是说那一节在 WSL 下无论如何都过不去,与本次改动无关。因此新断言的正确性用一份等价的独立脚本(进程替换构造同样的 stdin)逐次验证,而不是靠整条 `bash hooks/smoke-test` 通过来声称。 + +| 检查 | 结果 | +|---|---| +| `bash -n hooks/smoke-test` | 通过 | +| 子套件,stdin `/dev/null` | rc=0,27s,四条 `PASSED` | +| 子套件,stdin 打开且不写入 | rc=0,27s,四条 `PASSED` | +| **变异**:`hooks/smoke-test-windows.ps1` 第 41 行还原为继承 stdin | **120s 期限到点被杀,新断言判红** | +| 变异后逐字节恢复 | `diff -q` 一致 | +| 恢复后再跑同一断言 | rc=0,27s | + +### 变异过程中被断言自己的漏洞抓到一次 + +第一版新断言用 `sleep 60` 作管道写入端。撤销修复后它**通过了**(79s)—— 60 秒时写入端自己退出,卡住的 `cat` 拿到了 EOF,于是整套跑完。写入端必须活得比超时上限更久。改成 `sleep 200` 对 120s 期限后,撤销修复才稳定地在期限点被判红。这条写进断言上方的注释,因为下一个人缩短这个数字时不会有任何提示。 + +### CI 抓到本地验证的盲点:`'' |` 不是空 stdin(2026-10-28) + +第一次推送后 [run 40712165629](https://github.com/hkwuks/TaskFlow/actions/runs/40712165629) 的 `smoke (windows-latest)` **失败**,其余六个 job 通过: + +``` +run-hook.cmd : TaskFlow SessionStart error: invalid event JSON: expected a JSON object +At D:\a\TaskFlow\TaskFlow\hooks\smoke-test-windows.ps1:41 char:24 ++ $output = '' | & "$here\run-hook.cmd" session-start 2>&1 +``` + +**原因**:PowerShell 把空字符串送进管道时不是「零字节」,而是按 `$OutputEncoding` 编码后写一行。实测本机是 `hex=0d 0a`(两个字节),CI runner 上多一个 BOM;`session-start` 把 BOM 当非法 JSON,非零退出,套件 `throw`。 + +**我本地的漏洞**:我只验证了「加了 `'' |` 后不再卡住」,没有验证「它送进去的是什么字节」。卡死消失这一点是真的,但代价是一个依赖环境编码的输入 —— 这种修复本身就是缺陷。 + +**改法**:`& cmd /c "`"$here\run-hook.cmd`" session-start < nul" 2>&1`。重定向在任何宿主上都是确定的零字节;本文件 `Invoke-Hook`(`:135`)已经是这个形状。 + +| 改后验证 | 结果 | +|---|---| +| 子套件,stdin `/dev/null` | rc=0,24s,四条 `PASSED` | +| 子套件,stdin 打开且不写入 | rc=0,25s,四条 `PASSED` | +| **变异**:还原为继承 stdin | **120s 期限到点被杀,断言判红** | +| 变异后逐字节恢复 | `diff -q` 一致 | + +## Change Log + +### CI 抓到第二个问题:新断言在任何 CI 上都不会执行(2026-10-28) + +[run 40713839917](https://github.com/hkwuks/TaskFlow/actions/runs/40713839917) 7/7 全绿,但那个绿是假的。查 job 步骤: + +``` +Run bash smoke tests -> skipped (.github/workflows/hooks.yml:91, runner.os != 'Windows') +Run Windows smoke tests -> success +``` + +Windows runner 根本不跑 `bash hooks/smoke-test`;ubuntu/macos 上又没有 `powershell.exe`/`cygpath`,被守卫跳过。**我把断言放在 `hooks/smoke-test` 里,等于放在任何 CI 都不会执行的位置** —— 而它要复现的现象只在 Windows 上存在。上一轮我在 PR 里声称「撤销修复即判红」,那不假,但判红发生在本机,不在 CI。 + +**改法**:断言搬进 `hooks/smoke-test-windows.ps1` 自己 —— 那是 windows runner 上唯一执行的 TaskFlow 检查。用 `ProcessStartInfo` + `RedirectStandardInput` 自 spawn 一次,stdin 是一条从不写入也不关闭的管道;`TASKFLOW_STDIN_SELFCHECK` 防递归。`hooks/smoke-test` 那边只留 `< /dev/null` 的一行修复。 + +| 验证 | 结果 | +|---|---| +| 正常跑(含自检) | rc=0,51s,5 条 PASSED(含 `WINDOWS OPEN STDIN PASSED in 25s`) | +| **变异**:还原为继承 stdin | **rc=1,117s,`blocked for 90s`** | +| 恢复 | `diff -q` 逐字节一致 | + +期限从 180s 缩到 90s:变异那一轮 208s 太长,CI 上一次失败要等三分半才看到原因。 + +- 2026-10-28 — Step 1 完成:`hooks/smoke-test-windows.ps1:41` 给出空 stdin(附理由注释);`hooks/smoke-test` 的 Windows 段新增端到端断言;`CHANGELOG.md` 记两条。`hooks/smoke-test:735` 是同一缺陷的第二个实例(`cmd.exe //d //c ... session-start` 也继承 stdin),实测本机 rc=124、加 `< /dev/null` 后 rc=0,一并修了 —— 它原本不在 PRD 的 In Scope 里,但在同一个 shard、同一个原因,单独留着就是把已知缺陷留在原地。 +- 2026-10-28 — CI 反馈修正(work revision):第 41 行的 `'' |` 改为 `cmd /c "... < nul"`。前者在 PowerShell 里按 `$OutputEncoding` 编码后写一行(本机 `0d 0a`,CI 上带 BOM),CI 因此报 `invalid event JSON` 并失败;后者是确定的零字节。同一次修正也补上了我本地验证的盲点记录。 +- 2026-10-28 — CI 反馈修正二(work revision):端到端断言从 `hooks/smoke-test` 搬进 `hooks/smoke-test-windows.ps1`。前者在 windows runner 上被 `runner.os != 'Windows'` 跳过、在 ubuntu/macos 上被 `command -v` 守卫跳过,等于任何 CI 都不执行。期限 180s → 90s。 +- 2026-10-28 — 能力账本环境:本会话接线的 pre-write 门来自已安装插件 1.1.8(写五字段 `released` 行),仓库里的 `hooks/task` 是 1.1.9(读六字段)。为不复用一套不相容的账本,首次释放后用仓库版重放同一批阶段。把插件升级到 1.1.9 属项目路径之外的动作,未做。 + + +## Follow-ups + +- `hooks/smoke-test:743` 用 `-ExecutionPolicy Bypass` 调用子套件,属既有写法;本任务不评估它是否必要。 + +## Version History + +- v1 — planning. diff --git a/TaskFlowDocs/2026-10-28-windows-smoke-stdin/prd.md b/TaskFlowDocs/2026-10-28-windows-smoke-stdin/prd.md new file mode 100644 index 0000000..c3f180b --- /dev/null +++ b/TaskFlowDocs/2026-10-28-windows-smoke-stdin/prd.md @@ -0,0 +1,78 @@ +# Windows smoke 套件不得因继承 stdin 而卡死 +> Task version: v1 +> Status: checking + +## Goal + +`hooks/smoke-test-windows.ps1` 在终端里手工运行时,跑到第 96 行 `Invoke-SessionStart 'pr'` 就永久停住,既不打印也不再前进。本任务让它无论 stdin 是关闭、空管道,还是打开但无数据,都能跑完;并补一条**只有在修复撤销时才会变红**的断言。 + +## Background / Confirmed Facts + +以下均为 2026-10-28 在本机实测(Windows PowerShell 5.1,Git for Windows 位于 `D:\Git`,Windows 构建 10.0.26100)。 + +1. **卡死点在 96 行,不在 throw。** 用注入 `throw` 的方式逐段夹逼(88 / 95 / 97 / 110 / 121 / 142 / 160 / 197 / 208 九个位置): + - 注入在 96 行**之前**(88、95)→ rc=1,0–14 秒退出,错误消息正常打印; + - 注入在 96 行**之后**(97 及其后任意一处)→ rc=124,超时。 + + 边界恰好是 `$first = Invoke-SessionStart 'pr'`(`hooks/smoke-test-windows.ps1:96`)。 + +2. **throw 本身从来不会挂。** `finally`(`:216`)只负责删夹具根目录,不吞异常;三处 throw 探针的退出码都是 1,消息完整。 + +3. **机制是继承 stdin,不是 PowerShell 收尾。** 对照组 `powershell.exe -NoProfile -Command 'echo hi'` 配同一条打开的 stdin → 1 秒正常退出。四个按契约读 stdin 的 hook(`session-start:29`、`capability-gate:44`、`capability-evidence:33`、`prompt-reminder:54`,都是 `event_json="$(cat)"`)在 stdin 打开且无数据时**全部**超时;`hooks/task` 不读 stdin,立刻返回。 + +4. **该写法换一种喂法就正常。** 同一行改成 `$output = '' | & ...` 后:打开 stdin → rc=0 / 27 秒;`/dev/null` → rc=0 / 24 秒;`mkfifo` + 两端持有但不写入 → rc=0 / 26 秒,四条 `PASSED` 横幅齐全。 + +5. **CI 永远看不到这个缺陷。** runner 的 stdin 是空的或 null,`cat` 立刻读到 EOF。因此 windows-latest 至今全绿,包括本仓库 2026-10-27 的 run `40521265502`(windows 51 秒 pass)。只有人在终端里手工跑才会撞上。 + +6. **`main` 上同样存在。** `git show main:hooks/smoke-test-windows.ps1` 逐字节相同:打开 stdin rc=124 / 59 秒,关闭 stdin rc=0 / 27 秒。不是本分支引入的。 + +7. **bash 套件早已修过同一个坑。** `hooks/smoke-test:564-566` 的注释写着「`< /dev/null`,不是管道……继承套件自己的 stdin 会让读操作在 stdin 保持打开时阻塞」,并真的传了 `< /dev/null`。PowerShell 套件是同一条教训的未应用版本。 + +## Requirements + +- **R1** `hooks/smoke-test-windows.ps1` 里那一处 `session-start` 调用必须自带 stdin,不得继承运行者的。采用 `'' | & "$here\run-hook.cmd" session-start 2>&1`,与 bash 套件的 `< /dev/null` 是同一个意思。 +- **R2** 套件的**其余**行为一字不改:`Invoke-TaskFlow`(`:15`)、launcher 参数检查(`:44`)、`Invoke-Hook`(`:135`,已经通过 `cmd /c ... < eventFile` 自带 stdin)都不动。`hooks/task` 不读 stdin,因此 `:15` 无需改动。 +- **R3** 新增断言放在 bash 套件的 Windows 段(`hooks/smoke-test:740-744`)之后,沿用该段既有的 `powershell.exe` + `cygpath` 守卫:用一条**打开但不写入**的 stdin 再跑一遍整个 `smoke-test-windows.ps1`,要求它在限时内以 0 退出。 +- **R4** 限时等待必须是**有界**的:不允许出现一个会永久等待的循环。超时即判 FAIL 并杀掉子进程,且 FAIL 消息要说明「stdin 被继承」这一原因。 +- **R5** 断言必须**只有在修复撤销时才变红**。已实测:撤销 R1 后同一条命令 rc=124(>42 秒无进展),R1 在位时 rc=0 / 26 秒。 +- **R6** 不引入兼容层,不新增解释器依赖,不新增文件;仍是 bash + PowerShell 5.1 原生能力。 +- **R7** 文档同步:`CHANGELOG.md` 增 `### Fixed` 一条,说清「修的是继承 stdin,不是 throw」这个区别,因为 todo 条目的原始措辞是错的,后来者会照着错的找。 + +## Acceptance Criteria + +- **A1** 终端里运行 `powershell.exe -NoProfile -File hooks/smoke-test-windows.ps1`(stdin 为打开的终端)→ 限时内 rc=0,四条 `PASSED` 横幅齐全。修复前同一条命令无限挂起。 +- **A2** stdin 为 `/dev/null` 时 rc=0,四条横幅齐全(回归:修复不得破坏原有路径)。 +- **A3** stdin 为「两端打开但不写入」的 FIFO 时 rc=0,四条横幅齐全。 +- **A4** `bash hooks/smoke-test` → `ALL SMOKE PASSED`,且新增的 Windows 断言确实执行了(打印自己的 `ok`)。 +- **A5** **变异验证**:把 `:31` 那一行还原成原样,`bash hooks/smoke-test` 必须在新增断言处变红并指名原因;恢复后再次 `ALL SMOKE PASSED`,且 `diff -q` 确认恢复是逐字节的。 +- **A6** `git diff --check` 干净。 + +## In Scope + +- `hooks/smoke-test-windows.ps1` 的一行(`:31`)。 +- `hooks/smoke-test` 的 Windows 段末尾,新增一小节断言。 +- `CHANGELOG.md` 的 `[Unreleased] / ### Fixed`。 +- 本任务的 Todo 条目与 TaskFlow 文档。 + +## Out of Scope + +- **给 `hooks/task` 加 stdin**:它不读 stdin,实测零影响。 +- **改四个 hook 的 `cat`**:那是契约(宿主通过 stdin 投递事件),不是缺陷。 +- **修 `hooks/smoke-test-windows.ps1` 里其他潜在的继承 stdin 问题**:`:15` 与 `:44` 的目标不读 stdin;`:135` 已经自带。 +- **把新断言改成「钉住源码那一行」**:用户已选定端到端方案,见下。 +- **`hooks/smoke-test` 自身的 `< /dev/null` 写法**:2026-10-25 已修,不在本任务范围。 + +## Risks / Deferred Items + +- **CI windows job 变慢约 26 秒(51s → ~80s)。** 这是用户选定方案的自然后果:端到端复现必须真的再跑一遍套件。 +- **新断言只在这台 Windows 机器与 CI 的 windows-latest 上执行**(沿用 `cygpath` 守卫);ubuntu/macos 上跳过。这与该段既有行为一致,不额外扩大宿主差异。 +- **FIFO 在 Git Bash(MSYS)下的行为未在真实 CI 上验证过。** 本机 WSL 下 `mkfifo` + 持仓 fd 复现稳定;若 CI 上 FIFO 不可用,断言必须**明确失败**而不是静默跳过——否则它就成了另一种「看起来绿」。 +- **保留未修项**:`hooks/smoke-test` 里 `:743` 用 `-ExecutionPolicy Bypass` 调用子套件,属既有写法,本任务不动。 + +## Open Questions + +无。断言形态已于 2026-10-28 由用户在两个方案(端到端 / 钉源码)之间选定**端到端**。 + +## 变更的影响面 + +只影响测试代码与一份 changelog,不触及任何 hook 的运行时行为,因此对已安装的插件零影响,无需版本兼容声明。 diff --git a/TaskFlowDocs/todo.md b/TaskFlowDocs/todo.md index 8d1d014..afbbd76 100644 --- a/TaskFlowDocs/todo.md +++ b/TaskFlowDocs/todo.md @@ -26,56 +26,6 @@ has an issue number or URL. -## Agree on what a closed Step is, so `completed` stops passing one check - -- ID: TF-20261027-131c2b -- Status: done -- Goal: Make `completion-gate` and `hooks/task progress` mean the same thing by a closed Step, so the word `completed` cannot be refused by one and accepted by the other. -- Task: `TaskFlowDocs/achieved/2026-10-27-close-step-status/` -- Next action: None — completed and archived. - -## Ask where a conclusion should come from before converging on it - -- ID: TF-20261027-e8c6dc -- Status: inbox -- Priority: normal -- Owner: Claude -- Source: user request -- Added: 2026-10-27 -- Updated: 2026-10-27 -- Goal: Make TaskFlow ask, before a phase converges, whether the answer depends on a fact from outside this repository — and record the answer either way, instead of leaving external search to a decision that never gets made. -- Task: Not promoted. -- Next action: Decide the smallest change that forces the question: a line beside the `inspect` enumeration in `SKILL.md:54`, or a required PRD field. Do not add a gate. -- Notes: **2026-10-27 re-measured over 91 achieved tasks — worse than the 2026-10-11 figures on [[TF-20260919-90797c]], which owns the measurement.** `## Reference Pointers` holds **365 bullets, 0 containing a URL**. Whole-directory scan: **1 of 91 tasks** cites any third-party host (the 2026-09-10 audit task, 4 URLs). Only **3 of 91** have a `reference/` directory. - - **Three mechanisms, each pointing at a line** (inference, not causation): (1) `SKILL.md:54`'s `inspect` enumerates eight sources and **all eight are inside the repository or the Agent's own state** — nothing outbound; (2) "searching external sources" is only ever listed under Phase 2 (`SKILL.md:172`), and Phase 2 is *optional* with `reference/` as its artifact, so the mental model is "a writing phase" — you do not fetch facts until you have decided to write the note; (3) `hooks/capability-evidence:48-50` counts only `Skill`/`Agent`/`mcp__*`, so `WebSearch`/`WebFetch` are invisible to TaskFlow — excluded deliberately by R12 (`achieved/2026-09-27-capability-pre-write-gate/prd.md:61`) so the gate could not be satisfied by built-in tools. Net effect: search is neither blocked nor rewarded. - - **Why not a gate or a mandate:** the user has stated this is not a request to *require* network search. This repository is largely self-sufficient, which plausibly explains much of 1/91; a mandate would add a compulsory step to the majority of tasks that genuinely do not need one. - - **Coupled to [[TF-20261027-e4ba77]]:** both come from the same root — TaskFlow speaks only at turn boundaries and only about the phase it is in. If the inter-step reminder lands first, this one may be answerable with prose alone. - -## Stay present inside a long implementation run, not only at the prompt - -- ID: TF-20261027-e4ba77 -- Status: inbox -- Priority: normal -- Owner: Claude -- Source: user request -- Added: 2026-10-27 -- Updated: 2026-10-27 -- Goal: Keep TaskFlow present between the steps of one implementation run, so a long turn cannot leave the approved Plan behind while nothing in the workflow speaks up again. -- Task: Not promoted. -- Next action: Establish which hosts can inject context from a mid-turn hook (a `PostToolUse` hook on every tool, or the host's nearest equivalent) before weighing any design; the whole option set depends on that answer and it is a documentation fact, not a preference. -- Notes: **2026-10-27 measured on this machine:** across the three most recent session transcripts, tool calls per user turn are median **10–15**, max **197**. `hooks/prompt-reminder` speaks once at the *start* of each turn (`hooks/hooks.json`, `UserPromptSubmit`), and nothing TaskFlow-owned fires for the remaining 10–197 calls. The user's report — "跑着跑着不走流程" — lands in that gap. - - **Four openings named at file:line** (each verified today): (1) `hooks/capability-gate:126-134` returns 0 once a stage is released for this task version, and only matches the basenames `prd.md`/`spec.md`/`plan.md` (`:96-99`), so **no edit after the first Plan write is checked at all**; (2) the gate never reads `## Approval` — recorded history has the failure happening (`achieved/2026-10-23-archive-completion-gate/plan.md:132`, "Steps 1–3 were implemented before the Approval block was filled"); (3) `hooks/task progress` is the only writer of Step status and is bound to **no hook**, so a Step's `- Status:` changes only if the Agent chooses to call it; (4) the record the gate depends on is largely absent — **59 of 91 achieved Plans have zero `## Skills / Tools Used` stage lines**, and 8 have no `## Change Log` section. - - **External evidence for the frequency lever, not for a new gate:** arXiv 2604.12147 (IBM, 21,120 SWE-agent trajectories, SWE-bench Verified + Pro) measures that re-inserting the plan **every five steps** reduces plan violations and raises success, attributing the drift to the plan losing influence as the trajectory grows. That is the same mechanism `prompt-reminder` already uses, at the wrong cadence. - - **Deliberately *not* proposed: extending the gate into the implementation phase.** It would convert a reminder problem into a new hard gate over ordinary refactoring edits, and the false-refusal cost is borne per edit. If it is ever wanted it needs its own evidence, not this item's. - - **Open question the design turns on:** whether a host's mid-turn hook can actually inject text the model reads. If none can, the lever is not a new hook at all but the Plan carrying the current Step where the turn-start reminder can restate it — a smaller change with the same effect at turn granularity only. - ## task approve requires the first [Plan] line to name exactly the capability relea - ID: TF-20261026-726ec4 @@ -87,16 +37,22 @@ has an issue number or URL. ## A throw in hooks/smoke-test-windows.ps1 after the launcher section hangs the sui - ID: TF-20261025-645ef4 -- Status: inbox +- Status: in_progress - Priority: normal - Owner: Claude - Source: follow-up from 2026-10-25-gate-desync-diagnosis - Added: 2026-10-25 -- Updated: 2026-10-25 -- Goal: A throw in hooks/smoke-test-windows.ps1 after the launcher section hangs the suite instead of failing it: measured 2026-10-25 at 100-500s of silence with nothing printed, on main as well, so a red assertion reaches CI as a smoke (windows-latest) timeout rather than a message -- Task: Not promoted. -- Next action: Decide whether the suite should fail loudly: wrap the outer try/catch so a throw prints its message and exits non-zero. First reproduce on main to confirm it predates the branch, then check whether the CI job log shows anything on timeout. -- Notes: Found while verifying 2026-10-25-gate-desync-diagnosis. Positioning: a throw past the launcher section (after "WINDOWS LAUNCHER ARGS PASSED") hangs; a throw before it exits 1 correctly. Same on main, so not caused by that task. Consequence: the desync assertions added there, if they ever fail, arrive as a windows-latest timeout with no message. Local test: powershell.exe -File hooks/smoke-test-windows.ps1 (Git for Windows lives at D:\Git on this machine). +- Updated: 2026-10-28 +- Goal: Give hooks/smoke-test-windows.ps1 a stdin for the SessionStart call it makes, so the suite cannot block on an open one: measured 2026-10-28, the hang is line 96's `Invoke-SessionStart 'pr'`, not any throw — `run-hook.cmd session-start` has no stdin of its own, so it inherits the runner's, and `session-start` reads its event with `cat`. A throw is never the cause: injected at eight positions, every one before line 96 exits 1 in under 14s with its message, every one after it hangs. +- Task: `TaskFlowDocs/2026-10-28-windows-smoke-stdin/` +- Next action: Approve the PRD and Plan; then fix line 31 and add the end-to-end assertion. +- Notes: **2026-10-28 correction — the entry's original framing was wrong in two ways.** (1) "A throw after the launcher section hangs" measures the hang at the wrong object: throw is how the tester *noticed*, not what blocks. Bisected by injecting `throw` at lines 88/95/97/110/121/142/160/197/208 — the boundary is exactly line 96, `$first = Invoke-SessionStart 'pr'` in the Windows fixture. (2) "wrap the outer try/catch so a throw prints its message" is already true and needs nothing: probes at lines 55/56/111 print their message and exit 1 in 2–17s; `finally` at line 216 only removes the fixture root. + + **Mechanism, measured:** `hooks/session-start:29` is `event_json="$(cat)"` by contract (also `capability-gate`, `capability-evidence`, `prompt-reminder`). Each of the four returns rc=124 against an idle open stdin and rc=0 against `/dev/null`; `hooks/task`, which reads no stdin, returns immediately. The bash suite already paid for this lesson — `hooks/smoke-test:564-566` carries the comment "`< /dev/null`, not a pipe … inheriting the suite's own stdin makes the read block whenever that stdin stays open" and passes `< /dev/null`. The PowerShell suite is that same fix's unapplied twin. + + **Why CI has never shown it:** a CI runner's stdin is empty or null, so `cat` sees EOF at once. Only a human running `powershell.exe -File hooks/smoke-test-windows.ps1` from a terminal hits it — which is why every windows-latest job to date is green. Confirmed on `main` as well as on the branch: open stdin rc=124 / 59s, closed stdin rc=0 / 27s, byte-identical suite. + + **The fix, verified on a copy before writing it:** `$output = '' | & "$here\run-hook.cmd" session-start 2>&1` — open stdin rc=0 / 27s, closed stdin rc=0 / 24s, all four PASSED banners. ## Ignore personal.md and sessions.md without the user editing .gitignore: the plug @@ -1112,3 +1068,53 @@ has an issue number or URL. - Task: Not promoted. - Next action: Clarify and promote when ready. +## Agree on what a closed Step is, so `completed` stops passing one check + +- ID: TF-20261027-131c2b +- Status: done +- Goal: Make `completion-gate` and `hooks/task progress` mean the same thing by a closed Step, so the word `completed` cannot be refused by one and accepted by the other. +- Task: `TaskFlowDocs/achieved/2026-10-27-close-step-status/` +- Next action: None — completed and archived. + +## Ask where a conclusion should come from before converging on it + +- ID: TF-20261027-e8c6dc +- Status: inbox +- Priority: normal +- Owner: Claude +- Source: user request +- Added: 2026-10-27 +- Updated: 2026-10-27 +- Goal: Make TaskFlow ask, before a phase converges, whether the answer depends on a fact from outside this repository — and record the answer either way, instead of leaving external search to a decision that never gets made. +- Task: Not promoted. +- Next action: Decide the smallest change that forces the question: a line beside the `inspect` enumeration in `SKILL.md:54`, or a required PRD field. Do not add a gate. +- Notes: **2026-10-27 re-measured over 91 achieved tasks — worse than the 2026-10-11 figures on [[TF-20260919-90797c]], which owns the measurement.** `## Reference Pointers` holds **365 bullets, 0 containing a URL**. Whole-directory scan: **1 of 91 tasks** cites any third-party host (the 2026-09-10 audit task, 4 URLs). Only **3 of 91** have a `reference/` directory. + + **Three mechanisms, each pointing at a line** (inference, not causation): (1) `SKILL.md:54`'s `inspect` enumerates eight sources and **all eight are inside the repository or the Agent's own state** — nothing outbound; (2) "searching external sources" is only ever listed under Phase 2 (`SKILL.md:172`), and Phase 2 is *optional* with `reference/` as its artifact, so the mental model is "a writing phase" — you do not fetch facts until you have decided to write the note; (3) `hooks/capability-evidence:48-50` counts only `Skill`/`Agent`/`mcp__*`, so `WebSearch`/`WebFetch` are invisible to TaskFlow — excluded deliberately by R12 (`achieved/2026-09-27-capability-pre-write-gate/prd.md:61`) so the gate could not be satisfied by built-in tools. Net effect: search is neither blocked nor rewarded. + + **Why not a gate or a mandate:** the user has stated this is not a request to *require* network search. This repository is largely self-sufficient, which plausibly explains much of 1/91; a mandate would add a compulsory step to the majority of tasks that genuinely do not need one. + + **Coupled to [[TF-20261027-e4ba77]]:** both come from the same root — TaskFlow speaks only at turn boundaries and only about the phase it is in. If the inter-step reminder lands first, this one may be answerable with prose alone. + +## Stay present inside a long implementation run, not only at the prompt + +- ID: TF-20261027-e4ba77 +- Status: inbox +- Priority: normal +- Owner: Claude +- Source: user request +- Added: 2026-10-27 +- Updated: 2026-10-27 +- Goal: Keep TaskFlow present between the steps of one implementation run, so a long turn cannot leave the approved Plan behind while nothing in the workflow speaks up again. +- Task: Not promoted. +- Next action: Establish which hosts can inject context from a mid-turn hook (a `PostToolUse` hook on every tool, or the host's nearest equivalent) before weighing any design; the whole option set depends on that answer and it is a documentation fact, not a preference. +- Notes: **2026-10-27 measured on this machine:** across the three most recent session transcripts, tool calls per user turn are median **10–15**, max **197**. `hooks/prompt-reminder` speaks once at the *start* of each turn (`hooks/hooks.json`, `UserPromptSubmit`), and nothing TaskFlow-owned fires for the remaining 10–197 calls. The user's report — "跑着跑着不走流程" — lands in that gap. + + **Four openings named at file:line** (each verified today): (1) `hooks/capability-gate:126-134` returns 0 once a stage is released for this task version, and only matches the basenames `prd.md`/`spec.md`/`plan.md` (`:96-99`), so **no edit after the first Plan write is checked at all**; (2) the gate never reads `## Approval` — recorded history has the failure happening (`achieved/2026-10-23-archive-completion-gate/plan.md:132`, "Steps 1–3 were implemented before the Approval block was filled"); (3) `hooks/task progress` is the only writer of Step status and is bound to **no hook**, so a Step's `- Status:` changes only if the Agent chooses to call it; (4) the record the gate depends on is largely absent — **59 of 91 achieved Plans have zero `## Skills / Tools Used` stage lines**, and 8 have no `## Change Log` section. + + **External evidence for the frequency lever, not for a new gate:** arXiv 2604.12147 (IBM, 21,120 SWE-agent trajectories, SWE-bench Verified + Pro) measures that re-inserting the plan **every five steps** reduces plan violations and raises success, attributing the drift to the plan losing influence as the trajectory grows. That is the same mechanism `prompt-reminder` already uses, at the wrong cadence. + + **Deliberately *not* proposed: extending the gate into the implementation phase.** It would convert a reminder problem into a new hard gate over ordinary refactoring edits, and the false-refusal cost is borne per edit. If it is ever wanted it needs its own evidence, not this item's. + + **Open question the design turns on:** whether a host's mid-turn hook can actually inject text the model reads. If none can, the lever is not a new hook at all but the Plan carrying the current Step where the turn-start reminder can restate it — a smaller change with the same effect at turn granularity only. + diff --git a/hooks/smoke-test b/hooks/smoke-test index 4a4d0c1..6b3a7ce 100755 --- a/hooks/smoke-test +++ b/hooks/smoke-test @@ -801,7 +801,11 @@ echo ok if command -v cmd.exe >/dev/null 2>&1 && command -v cygpath >/dev/null 2>&1; then echo "== Windows launcher finds Git Bash ==" win_hook=$(cygpath -w "$HERE/run-hook.cmd") - json=$(cd "$tmp" && TASKFLOW_REPO_ROOT="$tmp" cmd.exe //d //c "$win_hook" session-start 2>&1) + # `< /dev/null` for the same reason as the PowerShell suite's SessionStart call: + # this `session-start` reads its event from stdin, and with no stdin of its own + # it inherits the runner's. Measured 2026-10-28 in WSL: with a held-open stdin + # this line never returns, and with `< /dev/null` it returns at once. + json=$(cd "$tmp" && TASKFLOW_REPO_ROOT="$tmp" cmd.exe //d //c "$win_hook" session-start < /dev/null 2>&1) echo "$json" | grep -qE 'additionalContext|additional_context|hookSpecificOutput' || { echo "FAIL Windows launcher"; exit 1; } echo ok fi diff --git a/hooks/smoke-test-windows.ps1 b/hooks/smoke-test-windows.ps1 index da38c0d..8acb8bd 100755 --- a/hooks/smoke-test-windows.ps1 +++ b/hooks/smoke-test-windows.ps1 @@ -28,7 +28,23 @@ function Invoke-SessionStart { $env:TASKFLOW_REPO_ROOT = $Root $env:TASKFLOW_PHASE = $Phase $env:CLAUDE_PLUGIN_ROOT = Split-Path -Parent $here - $output = & "$here\run-hook.cmd" session-start 2>&1 + # Feed `session-start` an empty event rather than letting it inherit ours. + # The hook reads its event from stdin by contract (`event_json="$(cat)"`), + # so with no stdin of its own it takes the runner's - and an idle open + # one (a terminal, a held pipe) never reaches EOF, so the read blocks and + # this whole suite stops here with nothing printed. Measured 2026-10-28: + # the suite hangs at this call and nowhere else; throw probes at eight + # later lines all printed and exited 1 in under 14s, while probes after + # this line timed out. `hooks/smoke-test:564-566` carries the same lesson + # for the bash suite and passes `< /dev/null`. CI has never shown the hang + # because a runner's stdin is empty already. + # + # `< nul` through `cmd`, not `'' |`. An empty PowerShell string is not an + # empty stdin: it is encoded and written as a line, and what that line is + # depends on `$OutputEncoding` - measured 0x0d 0x0a here, and a BOM on the + # CI runner, where `session-start` rejected it as invalid JSON and the + # suite failed. A redirection is zero bytes on every host. + $output = & cmd /c "`"$here\run-hook.cmd`" session-start < nul" 2>&1 if ($LASTEXITCODE -ne 0) { throw "SessionStart failed: $output" } return (($output -join [Environment]::NewLine) | ConvertFrom-Json) } @@ -40,6 +56,24 @@ function Invoke-SessionStart { } try { + # This file must stay 7-bit ASCII, and this assertion is what keeps it so. + # Windows PowerShell 5.1 reads a .ps1 with no BOM using the system ANSI code + # page, not UTF-8, so a literal non-ASCII character is decoded into bytes the + # parser does not expect: measured 2026-10-28, an em dash in a throw message + # arrived as three UTF-8 bytes, the third one swallowed the closing quote, and + # the whole file failed to parse with "The string is missing the terminator" + # -- after the suite had already shipped green on Linux and macOS. Every other + # non-ASCII string this suite needs is built at runtime from [char] codes, + # which is the pattern to follow. + $selfText = [IO.File]::ReadAllText($MyInvocation.MyCommand.Path, [Text.Encoding]::UTF8) + $nonAscii = [regex]::Matches($selfText, '[^\x00-\x7F]') + if ($nonAscii.Count -gt 0) { + $first = $nonAscii[0] + $line = ($selfText.Substring(0, $first.Index) -split "`n").Count + throw "hooks/smoke-test-windows.ps1 holds $($nonAscii.Count) non-ASCII character(s) and Windows PowerShell 5.1 will misparse it; first is at line $line, U+$([int][char]$first.Value | ForEach-Object { $_.ToString('X4') })" + } + 'WINDOWS SCRIPT IS ASCII' + $launcherArgs = @('space value', ([string]([char]0x4E2D) + [char]0x6587), 'quoted value', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten') $launcherOutput = @(& "$here\run-hook.cmd" smoke-test --echo-args @launcherArgs 2>&1) if ($LASTEXITCODE -ne 0) { throw "Launcher argument check failed: $launcherOutput" } @@ -109,7 +143,7 @@ try { 'WINDOWS LIFECYCLE PASSED' # The pre-write gate, driven through the same launcher, on a repository path - # that carries both a space and Chinese characters — the two things the + # that carries both a space and Chinese characters - the two things the # launcher exists for. It needs a real Git repository because the gate # resolves its store with `git -C` on the target file's own directory, and # the event carries a forward-slash drive path, which is what Claude Code @@ -124,7 +158,7 @@ try { # The event goes through a file and a `cmd` redirect rather than the # PowerShell pipeline. PowerShell 5.1 encodes a piped string with a UTF-8 BOM, # and the hook reads the event as JSON, where a leading BOM is not whitespace - # — the hook would no-op and the test would prove nothing about it. A file + # - the hook would no-op and the test would prove nothing about it. A file # written without a BOM is the bytes a host actually sends. function Invoke-Hook { param([string]$Hook, [string]$Event) @@ -145,7 +179,7 @@ try { # (claude-code #83877, #64432), so the backslash spelling is the one that must # be asserted. The forward-slash event is kept beside it because a gate that # answers differently for two spellings of one file is the defect this pair - # exists to catch — and because this fixture used to feed only the forward + # exists to catch - and because this fixture used to feed only the forward # form, which is how 1.1.1 shipped that defect behind a green Windows job. function New-PathEvent { param([string]$Path, [string]$Tool) @@ -189,7 +223,7 @@ try { } # An out-of-step store, on this host specifically. The new denial embeds the - # store path, and `--absolute-git-dir` returns a backslash path on Windows — + # store path, and `--absolute-git-dir` returns a backslash path on Windows - # unnormalized, those bytes end the JSON string early and the message becomes # unparseable rather than merely ugly. The bash suite cannot see that: it runs # on paths with forward slashes, where the same code is silently correct. @@ -198,7 +232,7 @@ try { [IO.File]::WriteAllText((Join-Path $gateStore 'released'), "v1|PRD|5|invoke|a:prd`n", $utf8) [IO.File]::WriteAllText((Join-Path $gateStore 'evidence'), '', $utf8) # `plan.md`, not the PRD: the released line above already covers `v1|PRD`, and - # the gate returns early for a stage it has released — the desync check is + # the gate returns early for a stage it has released - the desync check is # never reached. The stage under test has to be one `released` does not name. $planPath = Join-Path $gateTask 'plan.md' $verdict = Invoke-Hook 'capability-gate' (New-PathEvent $planPath 'Edit') @@ -212,6 +246,48 @@ try { throw "the desync message lost its numbers on Windows: $reason" } 'WINDOWS PRE-WRITE GATE PASSED' + + # Re-run this whole file with a stdin that is redirected and never written to + # or closed - the shape a terminal gives a command a person typed, and the one + # CI's own runner does not have. Anything the suite calls without a stdin of + # its own inherits that pipe; `session-start` reads its event from stdin, so + # the read blocks forever and the suite stops after its first banner with + # nothing printed. Measured 2026-10-28: with the `session-start` call left + # inheriting, this child never returns and is killed at 90s; with the call + # given `< nul`, it exits 0 in about 28s. + # + # Here rather than in `hooks/smoke-test`, because that suite does not run on + # the Windows runner at all (`if: runner.os != 'Windows'`), so an assertion + # living there is skipped by every CI job. This file is the only TaskFlow + # check a Windows runner executes, which makes it the only place the defect + # can be caught automatically. + # + # `TASKFLOW_STDIN_SELFCHECK` stops the recursion: the child is this same + # script and must not spawn a third. + if ($env:TASKFLOW_STDIN_SELFCHECK -ne '1') { + $self = $MyInvocation.MyCommand.Path + $psi = New-Object Diagnostics.ProcessStartInfo + $psi.FileName = (Get-Command powershell.exe).Source + $psi.Arguments = '-NoProfile -ExecutionPolicy Bypass -File "' + $self + '"' + $psi.RedirectStandardInput = $true # a pipe nobody writes or closes + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.UseShellExecute = $false + $psi.EnvironmentVariables['TASKFLOW_STDIN_SELFCHECK'] = '1' + $clock = [Diagnostics.Stopwatch]::StartNew() + $child = [Diagnostics.Process]::Start($psi) + $finished = $child.WaitForExit(90000) + $clock.Stop() + $elapsed = [int]$clock.Elapsed.TotalSeconds + if (-not $finished) { + try { $child.Kill() } catch { } + throw "the Windows suite blocked for ${elapsed}s with an inherited open stdin - a call to session-start has no stdin of its own and is waiting on the read" + } + $childOut = $child.StandardOutput.ReadToEnd() + if ($child.ExitCode -ne 0) { throw "the Windows suite failed with an inherited open stdin (rc $($child.ExitCode)): $childOut" } + if ($childOut -notmatch 'WINDOWS PRE-WRITE GATE PASSED') { throw 'the Windows suite with an inherited open stdin did not reach its last section' } + "WINDOWS OPEN STDIN PASSED in ${elapsed}s" + } } finally { if ($createdRoot -and (Test-Path $Root)) { Remove-Item -LiteralPath $Root -Recurse -Force }