diff --git a/CHANGELOG.md b/CHANGELOG.md index 754eb9c..a0f7ac5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +### 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. + +### Compatibility + +- **Five archived Plans read as unfinished under the new rule, and are not rewritten.** `achieved/` is read-only in place, so nothing here changes them and no task is blocked by them. They are: `2026-09-07-repository-standards-and-doc-root` (three Steps say `done (v1)`), `2026-10-11-gate-host-coverage` (four say `completed`), `2026-09-08-token-efficiency` and `2026-09-14-personal-docs-git-boundary` (their Steps still say `pending` while the Plan header says `completed` — the same disagreement running the other way), and `2026-10-22-taskflow-drift-guard`, whose `## Verification / Review` section reuses `### Step 1` as an ordinary subheading for a table. Recorded rather than repaired: editing history so it passes a check that no longer applies to it would make the record say something untrue. Only two of the five change verdict when the real gate is run over all 91 — the other three were already refused on `main` for reasons that have nothing to do with Steps, so this release does not newly refuse them. + ## [1.1.9] — 2026-10-26 ### Added diff --git a/TaskFlowDocs/achieved/2026-10-27-close-step-status/plan.md b/TaskFlowDocs/achieved/2026-10-27-close-step-status/plan.md new file mode 100644 index 0000000..db0dde6 --- /dev/null +++ b/TaskFlowDocs/achieved/2026-10-27-close-step-status/plan.md @@ -0,0 +1,131 @@ +# Plan — 一个 Step 什么时候算「关闭」 +> Task version: v1 +> Status: completed + +No spec required — small, self-contained task: one `awk` condition in one hook, its assertions, and three doc lines. No cross-layer contract, no new file, no interface change. + +## Reference Pointers + +- `hooks/completion-gate:67` — 当前的黑名单:`/^- Status: (pending|in_progress|blocked)$/ { unfinished = 1 }`。 +- `hooks/completion-gate:59-75` — 一次 `awk` 读完整个 Plan 的既有形状,与 `:22-31` 记的 `pipefail` / SIGPIPE 原因;本任务沿用,不引入管道。 +- `hooks/task:449-460` — 写侧定位 Step 的方式(`### Step ` 前缀 + 纯数字 + 数字后不接字母数字下划线),R2 要镜像的就是这一段。 +- `hooks/task:1099-1101` — 写侧取值集合 `pending|in_progress|done|blocked`。 +- `hooks/smoke-test:33-60` — `cg_case` 辅助函数与 `cg_approved` 夹具,新断言按它加。 +- `skills/taskflow/SKILL.md:313` — 待改的黑名单措辞。 +- `skills/taskflow/references/runtime.md:244-245` — 同上,两行。 +- `TaskFlowDocs/todo.md` → `TF-20261027-131c2b` — 本任务的来源条目与三条已裁定决定。 + +## Related Tasks + +- `TF-20261027-e4ba77`(长实现里保持在场)——同源但不同层:那条改注入节奏,本条改门的判据。本条不依赖它。 +- `TF-20261027-e8c6dc`(先问结论是否依赖外部事实)——同批记下的另一条,无依赖。 +- `TF-20261027-131c2b` — 本任务。 + +## Skills / Tools Used + +- [PRD] Unaided — no capability applied to this phase; considered: requirements elicitation and framing +- [Plan] Unaided — no capability applied to this phase; considered: work breakdown and task decomposition +- [Test] Unaided — no capability applied to this phase; considered: testing strategy + +## Preconditions + +- 工作树 `.worktrees/close-step-status`,分支 `fix/close-step-status`,base `main` @ `b47ea3f`。 +- `TaskFlowDocs/todo.md` 的未提交改动(三条 2026-10-27 条目)已随本任务进入本工作树。 +- 用户已裁定三个岔口(PRD 的 Open Questions)。 + +## Approval + +- Status: approved +- Approved by: user +- Approved at: 2026-10-27 21:59 +0800 +- Approved version: v1 +- Approved scope: PRD / Plan + +## Steps + +### Step 1 — 把门的 unfinished 判定改成块内的白名单,并用断言钉住 + +- Goal: `hooks/completion-gate` 只在一个 `### Step N` 块里确实写着列首 `- Status: done` 时才认为该块关闭;其余一切算未完成。行为变化只发生在这一处。 +- Dependencies: 无(第一步)。 +- Files: `hooks/completion-gate`、`hooks/smoke-test`。 +- Implementation checklist: + - [x] 删除 `hooks/completion-gate:67` 的列首黑名单行,改为块内白名单:`### Step <数字>` 处开启块,`## ` 或下一个 `### Step <数字>` 或文件尾关闭块;`END` 时也要结算最后一个块。 + - [x] 块的开启判定镜像 `hooks/task:449-460`:`### Step ` 前缀、纯数字、数字后不接 `[A-Za-z0-9_]`。不按 `## Steps` 段落过滤(PRD 第 5 条)。 + - [x] 关闭的判据只认列首 `- Status: done`,行尾空白容忍(`[[:space:]]` 在 `LC_ALL=C` 下含 `\r`)。一个块里出现多行 `- Status:` 时以最后一行为准,并写进注释。 + - [x] 保持 `unfinished` 的既有输出:`Plan has unfinished Steps`、`exit 1`;`--approval-only` 分支不检查 Step(R4)。 + - [x] 在 `awk` 的注释里写明 R5 的残留:没有 `### Step` 的 Plan 此项通过,这是本规则明确留下的 fail-open,属于另一个问题。 + - [x] `hooks/smoke-test` 在既有 completion-gate 段加 `cg_case`:A1(`done`→0)、A2(`completed`→1)、A3(`complted`/`skipped`/`wip`/`DONE`/`done (v1)`/空值/缺行→1)、A4(三个旧词→1)、A5(一 done 一 completed→1)、A6(`## Approval` 段与 `## Verification / Review` 段下的块,`completed`→1 且 `done`→0)、A7(无 `### Step`→0)、A8(`--approval-only` 对 A2 的 Plan→0),外加补 `\r` 与行尾空白的容忍。 + - [x] 断言里带一句注释,说明 A7 是**故意断言 fail-open**,不是漏写。 +- Acceptance: A1–A8 全绿;A4(`pending`/`in_progress`/`blocked` 仍被拒)与既有 `cg_case unfinished` 无回归;A10(既有 completion-gate 断言不改)。 +- Verification: 5 次变异全部验证过(记录在 `## Verification / Review`),`bash hooks/smoke-test` `ALL SMOKE PASSED`,`diff` 确认每次改完都按字节还原。 +- Rollback: `git checkout -- hooks/completion-gate hooks/smoke-test`(本步不碰其它文件,回滚无连带)。 +- Status: done + +### Step 2 — 让文档说白名单,并全量验证 + +- Goal: 三处文档不再描述黑名单;整仓检查通过。 +- Dependencies: Step 1(措辞要描述已实现的规则)。 +- Files: `skills/taskflow/SKILL.md`、`skills/taskflow/references/runtime.md`、`CHANGELOG.md`。 +- Implementation checklist: + - [x] `SKILL.md:313`:把「no Step still `pending`, `in_progress`, or `blocked`」改成白名单措辞——每个 Step 必须写着 `- Status: done`,其余一律算未完成,并点明这与 `task progress` 只肯写 `done` 是同一条规则的两侧。 + - [x] `runtime.md:244-245`:同义修改,两行都要动(一行描述 `archive`,一行描述 `completion-gate`)。 + - [x] `CHANGELOG.md`:`## [Unreleased]` 下加 `### Fixed` 与 `### Compatibility` 两段——症状(`completed` 在内的七种写法都放行)、根因(黑名单 fail-open)、块定位与写侧一致的理由、R5 的已知残留,以及 5 份归档 Plan 逐条点名不重写。 + - [x] 扫一遍是否还有别处复述黑名单:`grep -rn 'in_progress\`, or \`blocked\|pending, in_progress, or blocked' skills/ hooks/README.md`。 +- Acceptance: 三处文档与实现一致;`grep` 不再命中旧措辞。 +- Verification: `bash hooks/smoke-test`;`bash hooks/repository-check .`;`bash hooks/release-check .`;`git diff --check`;`python3 /scripts/quick_validate.py skills/taskflow`。 +- Rollback: `git checkout -- skills/ CHANGELOG.md`。 +- Status: done + +## Checkpoints + +- Step 1 之后:`git diff` 只含 `hooks/completion-gate` 与 `hooks/smoke-test`;A1–A8 逐条驱动过一次;至少 A2、A6 各做过一次变异验证(改坏→断言红→改回)。 +- Step 2 之后:全量验证四条命令的记录写进 `## Verification / Review`。 + +## Verification / Review + +- **`Hooks` workflow, run [40519266846](https://github.com/hkwuks/TaskFlow/actions/runs/40519266846)** at `f03b806` — **`success`, all seven jobs**: `changes`, `evals`, `release`, `todo-merge-audit`, and `smoke` on `ubuntu-latest` (20s), `windows-latest` (39s) and `macos-latest` (1m7s). Run time 1m25s. CI is the test authority per `CONTRIBUTING.md`, so the new assertions ran on all three hosts rather than only on this one; the local run recorded below was made before CI existed for this branch and stands as the same tree's result. +- 2026-10-27 Step 2: bash hooks/smoke-test ALL SMOKE PASSED; 5 mutations each reddened a named assertion; release-check pass; quick_validate valid; git diff --check clean + +2026-10-27,本地(本工作树 `fix/close-step-status`)执行: + +- `bash hooks/smoke-test` — `ALL SMOKE PASSED`(含新增的 19 条 completion-gate 断言)。 +- `bash hooks/release-check .` — `STATUS: pass`;两侧 marketplace pin 仍是 `v1.1.9 -> 800e7f1…`。 +- `bash hooks/repository-check .` — `STATUS: needs-user-input`,三条原因都是本任务进行中的正常状态而不是缺陷:`Base: ambiguous`(新分支尚无 upstream)、`Working tree: has changes`、任务目录未跟踪。`Orphan task directories: none`、`Stale task worktrees: none`。 +- `git diff --check` — 干净。 +- `quick_validate.py skills/taskflow` — `Skill is valid!`。 +- 逐步驱动 `/tmp` 夹具跑过 A1–A8 与四个边界(`### Step 1b` 不算块、`### Step 12` 算块、` +` 尾、行尾空白)。 +- **五次变异验证,每次改坏后按字节还原**(`diff -q` 确认): + 1. 换回旧的黑名单行 → `FAIL completion-gate clean: rc 1 != 0 (Plan has unfinished Steps)`(白名单被删,`done` 的 Step 反而被拒)。 + 2. 把块定位限制在 `## Steps` 段内 → `FAIL completion-gate unfinished: rc 0 != 1`;并且单独证实新夹具 `block-outside-steps-section` 在变异下 return 0(应为 1),即它独立捕获了这一变异,不是搭了别的断言的便车。 + 3. 删掉 `END` 里的 `flush()` → `FAIL completion-gate unfinished: rc 0 != 1`(最后一个块不被结算)。 + 4. 把 `done` 判据收紧成整行精确相等 → 第一轮 `ALL SMOKE PASSED`,**断言没抓住**;补了 `printf` 生成的行尾空白与 `\r` 两条夹具后重跑,`FAIL completion-gate closed-done-trailing-space: rc 1 != 0`。 + 5. `done` 判据放宽成 `(done|completed)` → `FAIL completion-gate closed-completed: rc 0 != 1`(正是本任务要消除的那个词)。 + +第 4 次值得留档:第一版断言只写了字面上的 `done`,于是「容忍行尾空白」这条实现细节根本没有被钉住——变异验证发现了断言的漏洞,不是代码的漏洞。 + +**回归对照(`git show main:hooks/completion-gate` 对同一个 `achieved/` 跑一遍)**:91 份里 `main` 拒 17 份,改后拒 19 份,**新变化的只有 2 份**——`2026-10-11-gate-host-coverage` 与 `2026-10-22-taskflow-drift-guard`。另外 3 份(`repository-standards-and-doc-root`、`token-efficiency`、`personal-docs-git-boundary`)在 `main` 上就已经因为「current Task version is not approved」被拒,本次改动没有改变它们的判定。`achieved/` 不过门,所以这 2 份在运行时也没有后果;写进 CHANGELOG 的是事实而不是影响。 + +## Change Log + +- 2026-10-27 work revision — 变异验证发现第一版断言没钉住「行尾空白容忍」:实现里 `[[:space:]]` 允许 `done` 后面有空格与 `\r`,但夹具只写了字面的 `done`,把判据收紧成整行精确相等仍然全绿。补了两条用 `printf` 生成字节的夹具(行尾两空格、CRLF)后,同一变异变红。改的是断言,不是行为。 +- 2026-10-27 work revision — CHANGELOG 的 Compatibility 段补上回归对照的结论:91 份里 `main` 拒 17、改后拒 19,新变化只有 2 份,其余 3 份在 `main` 上已因别的条件被拒。原措辞只说「5 份算未完成」,会被读成「5 份的判定都变了」。 + +## Follow-ups + +- 「Plan 可以没有 `### Step`」这条残留口径(R5)不在本任务内,若要做需另立条目。 +- `2026-10-22-taskflow-drift-guard` 在 `## Verification / Review` 段下复用 `### Step 1` 作普通小标题——写侧本来就把它当 Step,本条只是让读侧与写侧一致;若要禁止这种复用,也另立条目。 + +## Version History + +- v1 — 初版:白名单 `done`、任意 `### Step N`、归档 5 份不动。 + +## Todo Record + +Moved verbatim from the TODO entry by `hooks/archive`, grouped by the field each block followed. + +### `- Notes:` + +- **2026-10-27 measured, and the hole is wider than the two vocabularies.** The two writers disagree — `hooks/task progress` accepts exactly `pending|in_progress|done|blocked` (`hooks/task:1100`) while `hooks/completion-gate:67` treats only `pending|in_progress|blocked` as unfinished — but the real defect is that the gate **fails open on every unrecognized value**. Driven through the gate directly, a Plan whose only Step reads `- Status: complted` / `skipped` / `wip` / empty / a bare `x` returns **exit 0, "finished"**. `completed`, `done (v1)` and `DONE` do the same, so the case that started this item is one instance of a general shape rather than a special one. + **Real Plans already carry the non-canonical spellings:** over 91 achieved Plans, Step statuses are 254 `done`, 6 `pending`, **4 `completed`** (`2026-10-11-gate-host-coverage`), **3 `done (v1)`** (`2026-09-07-repository-standards-and-doc-root`). Both spellings exist because `task progress` refused the word and the Step was hand-edited instead — the validator and the reader disagreeing is what produced them. + The choice is a user decision, not a tidy-up: a whitelist would refuse those archived Plans' shapes and is the fail-closed side, while the blacklist is what the gate already is. Whichever wins, `task progress` must move with it, or the next hand edit recreates the split. diff --git a/TaskFlowDocs/achieved/2026-10-27-close-step-status/prd.md b/TaskFlowDocs/achieved/2026-10-27-close-step-status/prd.md new file mode 100644 index 0000000..2a9cd68 --- /dev/null +++ b/TaskFlowDocs/achieved/2026-10-27-close-step-status/prd.md @@ -0,0 +1,74 @@ +# PRD — 一个 Step 什么时候算「关闭」 +> Task version: v1 +> Status: completed + +## Goal + +让 `hooks/completion-gate` 和 `hooks/task progress` 对「Step 关闭」给出同一个答案:`- Status: done` 是唯一表示关闭的写法,其余一律算未完成。今天门对任何它不认识的写法都**放行**,所以 `completed`、`done (v1)`、`complted`、空值、甚至没有这一行,全都能把一个没做完的 Plan 送到 `achieved/`。 + +## Background / Confirmed Facts + +2026-10-27 在本仓库直接驱动 `hooks/completion-gate` 实测(`/tmp` 一次性夹具,跑的是 `b47ea3f` 的码): + +1. **门是 fail-open 的,缺陷比「两套词表」说法更宽。** 一个只带 `- Status: complted` / `skipped` / `wip` / 空值 / 裸 `x` 的 Plan,门一律 `exit 0`「finished」。真正被拒的只有 `pending`、`in_progress`、`blocked` 三个词——因为 `hooks/completion-gate:67` 是一条黑名单:`/^- Status: (pending|in_progress|blocked)$/ { unfinished = 1 }`。任何没被列出来的值都落进「完成」。 +2. **写侧只认四个词。** `hooks/task:1099-1101` 把 `- Status:` 的取值限定在 `pending|in_progress|done|blocked`,别的直接 `fail`。所以 `completed` 这类写法**不可能**由 `task progress` 写出,它只能是手改的结果——两处对同一个字段的分歧,正是那些手改的成因。 +3. **真实 Plan 里已经有非规范写法。** 91 份 achieved Plan 的列首 `- Status:` 分布:254 `done`、78 `approved`(Approval 段)、6 `pending`、6 `completed`、4 `requested`(Approval 段)、3 `done (v1)`、1 `not required for read-only research`(Approval 段)。 +4. **按新规则重扫这 91 份,5 份会被判为未完成**,原因分三类:`2026-09-07-repository-standards-and-doc-root` 三个 Step 是 `done (v1)`;`2026-10-11-gate-host-coverage` 四个 Step 是 `completed`;`2026-09-08-token-efficiency`、`2026-09-14-personal-docs-git-boundary` 是真的 `pending`(这两个 `> Status: completed`,Step 却从没被改过——另一个方向的同一件事);`2026-10-22-taskflow-drift-guard` 的 `## Verification / Review` 段里有一个同名小节 `### Step 1 — per-host contract`,内容是表格不是计划步骤,没有 Status 行。 +5. **写侧定位 Step 的方式是「只看标题、不分段落」。** `hooks/task:449` 扫全文件找 `### Step <数字>`,块尾是下一个 `### Step ` 或 `## `。`setr` 又只认列首 `- Status:`。所以门若按「`## Steps` 段内」来找,会和写侧看到不同的块——`2026-09-10-repository-governed-development` 就有两个 Step 块挂在 `## Approval` 段下,写侧能改、门读不到。 +6. **`--approval-only` 是另一条路径。** `task state in_progress` 和 `task progress` 用它问「当前 Task version 批准了吗」,它们本来就不该要求 Plan 已完成。 + +## Requirements + +**R1 — 关闭的判据是白名单。** 一个 Step 块算关闭,当且仅当块内**列首**存在一行 `- Status: done`(行尾空白容忍)。其余一切——`completed`、`done (v1)`、拼写错误、空值、没有这一行——都算未完成。 + +**R2 — Step 块的定位与写侧一致。** 块由 `plan.md` 里任意位置的列首 `### Step <数字>` 开启(数字后不能紧跟字母数字或下划线),到下一个同类标题、或下一个列首 `## `、或文件尾结束。**不按 `## Steps` 段落过滤**:过滤会重新制造本任务要消除的那种分叉(第 5 条实测)。 + +**R3 — 门仍然只报一句话。** 有未关闭的 Step 时仍是 `Plan has unfinished Steps`、`exit 1`。不加 Step 编号,不改既有措辞——四个调用方和既有断言都靠这句话。 + +**R4 — `--approval-only` 不变。** 它继续只回答批准问题,不检查 Step。 + +**R5 — 没有 Step 块时此项通过,且写进注释。** 一个连 `### Step` 都没有的 Plan 不因本规则被拒。这是本规则明确留下的 fail-open 残留,属于另一个问题(「Plan 可以没有 Step」),不在本任务范围内,必须在 `hooks/completion-gate` 的注释里写清,免得下一个人以为这里是全封闭的。 + +**R6 — 不写兼容代码,不回填历史。** 不放宽到接受 `completed` / `done (v1)`,不改 `TaskFlowDocs/achieved/` 里的任何文件。已归档 Plan 不过门,新规则对它们没有运行时后果,第 4 条的 5 份只在 CHANGELOG 里记录事实。 + +**R7 — `hooks/task progress` 一个字都不用改。** 它已经是这条规则的写侧与校验侧;本任务只是让读侧与它对齐。 + +**R8 — 文档同步。** `skills/taskflow/SKILL.md:313` 与 `skills/taskflow/references/runtime.md:244-245` 现在写的是「no Step still `pending`, `in_progress`, or `blocked`」,那是黑名单的措辞,必须改成白名单;`CHANGELOG.md` 增一条,Compatibility 段声明「归档文档不重写,其中 5 份按新规则算未完成」。 + +## Acceptance Criteria + +- **A1** 一个 Step 写 `- Status: done` → 门 `exit 0`。 +- **A2** 一个 Step 写 `- Status: completed` → 门 `exit 1`,消息含 `unfinished Steps`。(这是本条缺陷的原始症状。) +- **A3** 一个 Step 写 `complted` / `skipped` / `wip` / `DONE` / 空值 / 整行缺失 → 六种都 `exit 1`。 +- **A4** 一个 Step 写 `pending` / `in_progress` / `blocked` → 仍 `exit 1`(不得回归)。 +- **A5** 两个 Step 一个 `done` 一个 `completed` → `exit 1`(不能因为有一个对就整体放行)。 +- **A6** Step 块写在 `## Approval` 段或 `## Verification / Review` 段下,`- Status: completed` → `exit 1`;同位置写 `done` → `exit 0`。证明定位不分段落。 +- **A7** 一个没有 `### Step` 的 Plan → `exit 0`(R5 的残留,被断言钉住,而不是默默存在)。 +- **A8** `--approval-only` 对 A2 的 Plan 仍 `exit 0`(批准就够了,这是 `task progress` 问的问题)。 +- **A9** `bash hooks/smoke-test` 全绿,新增断言覆盖 A1–A8。 +- **A10** `bash hooks/smoke-test` 的既有 completion-gate 断言不变(`cg_case unfinished` 等)——它们的 Step 用的是 `pending`/`done`,本来就在规则内。 + +## In Scope + +- `hooks/completion-gate`:`unfinished` 判定改写为白名单 + 块定位,附注释说明 R5 的残留。 +- `hooks/smoke-test`:新增 `cg_case`(A1–A8)。 +- `skills/taskflow/SKILL.md`、`skills/taskflow/references/runtime.md`:措辞同步。 +- `CHANGELOG.md`。 + +## Out of Scope + +- `TaskFlowDocs/achieved/` 里 5 份 Plan 的内容(R6)。 +- `hooks/task progress` 的取值集合(R7:已经是 `pending|in_progress|done|blocked`)。 +- 「Plan 可以没有 Step」这条残留口径(R5)——只记录,不关掉。 +- `TF-20261027-e4ba77`(长实现里不打扰)与本条同源,但那条要改的是注入节奏,不是门的判据。 +- `TF-20261027-e8c6dc`(先问结论是否依赖外部事实)——同一个 session 里记下的另一个问题。 + +## Risks / Deferred Items + +- **这是收紧,会让某些今天能归档的 Plan 被拒。** 缓解:被拒的只有「Step 没写 `done`」的 Plan,而 `task progress` 一直只肯写 `done`,所以正常流程产出的 Plan 不受影响;受影响的是手改过的,而它本来就该被拒。 +- **`### Step N` 这个形状被别处复用作普通小标题会误判。** 实测 91 份里有 1 份(`2026-10-22-taskflow-drift-guard`)。选「任何 `### Step N`」而不是「`## Steps` 段内」是为了和写侧一致(第 5 条);代价是这种复用会被算成未关闭的 Step。写侧本来也把它当成 Step 来改——所以这不是新分叉,是同一个已存在的约定。 +- **CRLF。** 仓库 `.gitattributes` 是 `* text=auto eol=lf`,实测归档 Plan 全是 LF。行尾仍按空白容忍处理,`[[:space:]]` 在 `LC_ALL=C` 下含 `\r`,不需要额外分支。 + +## Open Questions + +无。三个岔口已于 2026-10-27 由用户裁定:白名单只认 `done`;块定位用任意 `### Step N`;归档的 5 份不动、只记录。 diff --git a/TaskFlowDocs/todo.md b/TaskFlowDocs/todo.md index 850eeb1..8d1d014 100644 --- a/TaskFlowDocs/todo.md +++ b/TaskFlowDocs/todo.md @@ -26,6 +26,56 @@ 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 diff --git a/hooks/completion-gate b/hooks/completion-gate index e028a7e..010b8f6 100644 --- a/hooks/completion-gate +++ b/hooks/completion-gate @@ -56,16 +56,57 @@ plan="$1/plan.md" # The Approval fields are matched only inside the `## Approval` section, so a # `- Status: approved` written anywhere else cannot stand in for an approval. # An end-of-file section needs no special case: `inap` simply stays set. -result="$(awk -v approval_only="$approval_only" 'BEGIN { bad = 0 } +result="$(awk -v approval_only="$approval_only" ' + # A Step block is closed only when it says `- Status: done` in column 0. This + # is a whitelist, and that is the point: the previous shape listed the three + # words that meant unfinished, so every value it did not know about -- a typo, + # `completed`, `done (v1)`, an empty value -- counted as finished. Measured + # 2026-10-27: all of those archived. `hooks/task progress` has always accepted + # exactly `pending|in_progress|done|blocked`, so `done` is the only word that + # can mean closed, and reading anything else as closed is the disagreement + # this replaces rather than one it repeats. + # + # Block location mirrors the writer at `hooks/task` -- any `### Step ` with a + # number and no trailing word character opens one, and any `### Step ` or + # column-0 `## ` closes it. Deliberately NOT filtered to the `## Steps` + # section: two archived Plans carry Step blocks under other headings, and a + # reader that skipped them would see a different set of Steps than + # `task progress` writes to. That is the split this check exists to remove. + # + # What this still lets through, and knowingly: a Plan with no `### Step` at + # all has nothing to be unfinished, so this check passes it. Closing that + # needs a rule about whether a Plan must have Steps, which is a different + # question with its own arguments; it is not smuggled in here. + function flush() { + if (instep && !closed) unfinished = 1 + instep = 0; closed = 0 + } + BEGIN { bad = 0 } + /^## / { flush() } /^> Task version: v[0-9][0-9]*$/ { if (ver == "") { ver = $0; sub(/^> Task version: /, "", ver) } next } /^## Approval$/ { inap = 1; next } inap && /^## / { inap = 0 } inap && $0 == "- Status: approved" { st = 1 } inap && $0 == "- Approved by: pending" { pend = 1 } inap && index($0, "- Approved version: ") == 1 { av = substr($0, 21) } + /^### Step / { + flush() + rest = substr($0, 10) + if (match(rest, /^[0-9]+/)) { + if (substr(rest, RLENGTH + 1, 1) !~ /[A-Za-z0-9_]/) instep = 1 + } + next + } /^[[:space:]]*- \[ \]/ { unchecked = 1 } - /^- Status: (pending|in_progress|blocked)$/ { unfinished = 1 } + # Last one wins, which is what "the block says" means when a block carries + # two: `task progress` replaces the line it finds rather than adding one, so a + # second `- Status:` is a hand edit, and reading the last is the reading a + # person makes. + instep && /^- Status:/ { + closed = ($0 ~ /^- Status:[[:space:]]*done[[:space:]]*$/) + } END { + flush() if (ver == "") { print "missing Plan Task version"; bad = 1 } else if (!st || av != ver || pend) { print "current Task version is not approved"; bad = 1 } else if (!approval_only && unchecked) { print "Plan has unchecked items"; bad = 1 } diff --git a/hooks/smoke-test b/hooks/smoke-test index 9773d11..4a4d0c1 100755 --- a/hooks/smoke-test +++ b/hooks/smoke-test @@ -96,12 +96,81 @@ cg_case unchecked 1 'unchecked items' "${cg_approved}### Step 1 — a cg_case unfinished 1 'unfinished Steps' "${cg_approved}### Step 1 — a - Status: pending " +# Closed is a whitelist, so every word but `done` has to be refused. The list +# below is not decoration: `completed` was the one measured reaching `achieved/` +# (2026-10-27), the misspellings and the empty value are what the old blacklist +# let through for the same reason, and `done (v1)` is a shape two archived Plans +# still carry. One `cg_case` each, so deleting the whitelist reddens the one it +# actually removed rather than "something". +for cg_word in completed 'done (v1)' complted skipped wip DONE; do + cg_case "closed-$cg_word" 1 'unfinished Steps' "${cg_approved}### Step 1 — a +- Status: $cg_word +" +done +cg_case closed-empty 1 'unfinished Steps' "${cg_approved}### Step 1 — a +- Status: +" +# A block that never got a Status line is unfinished too: absent is not `done`. +cg_case closed-missing 1 'unfinished Steps' "${cg_approved}### Step 1 — a +- Goal: something +" +# ...and the words that already meant unfinished still do. `done` and `pending` +# are covered above; the other two are here. A whitelist that refused everything +# would pass all of these, which is why `closed-done` sits beside them. +cg_case open-in_progress 1 'unfinished Steps' "${cg_approved}### Step 1 — a +- Status: in_progress +" +cg_case open-blocked 1 'unfinished Steps' "${cg_approved}### Step 1 — a +- Status: blocked +" +cg_case closed-done 0 '' "${cg_approved}### Step 1 — a +- Status: done +" +# Trailing whitespace and a CRLF line ending are written with `printf` rather +# than typed, so the bytes are visible in the assertion instead of hiding at the +# end of a quoted line. Both have to count as closed: the repository's +# `.gitattributes` says `eol=lf`, but a Plan edited on Windows before it is +# committed carries the `\r`, and refusing it would report a finished Step as +# unfinished for a reason nobody could see. +cg_case closed-done-trailing-space 0 '' "$(printf '%s### Step 1 — a\n- Status: done \n' "$cg_approved")" +cg_case closed-done-crlf 0 '' "$(printf '%s### Step 1 — a\r\n- Status: done\r\n' "$cg_approved")" +# Two Steps, one closed and one not: the check is per block, not "does the file +# contain a `done` anywhere". +cg_case closed-but-sibling-open 1 'unfinished Steps' "${cg_approved}### Step 1 — a +- Status: done +### Step 2 — b +- Status: completed +" +# The block reader is not filtered to `## Steps`, on purpose: `hooks/task +# progress` locates a Step by its heading alone, so a reader that skipped the +# other sections would see a different set of Steps than the writer edits. Two +# archived Plans carry Step blocks under `## Approval` and `## Verification / +# Review`, which is why both sections are pinned here. +cg_case block-outside-steps-section 1 'unfinished Steps' "${cg_approved} +### Step 1 — a +- Status: completed +" +cg_case block-outside-steps-section-done 0 '' "${cg_approved} +### Step 1 — a +- Status: done +" +# Knowingly still fail-open, and pinned so it stays a decision rather than an +# oversight: a Plan with no `### Step` has nothing to be unfinished. Whether a +# Plan must have Steps is a different question and is not answered here. +cg_case no-step-blocks 0 '' "${cg_approved}## Steps + +## Checkpoints +" # `--approval-only` is the mid-task question: `state in_progress` and # `progress` must not demand a finished Plan, only an approved one. cg_case approval-only-unfinished 0 '' "${cg_approved}### Step 1 — a - [ ] still working - Status: in_progress " '--approval-only' +cg_case approval-only-not-done 0 '' "${cg_approved}## Steps +### Step 1 — a +- Status: completed +" '--approval-only' # A promotion that never got a Plan is the case `task complete` can reach, # because `task_paths` only requires the directory. The gate says so in words # rather than letting awk report a missing file. diff --git a/skills/taskflow/SKILL.md b/skills/taskflow/SKILL.md index f9e96e1..b502f5e 100644 --- a/skills/taskflow/SKILL.md +++ b/skills/taskflow/SKILL.md @@ -310,7 +310,7 @@ Run it as one command: `hooks/task complete --user-accepted`. It check Before that command, confirm all acceptance criteria pass, verification is recorded, the current Task version is consistent across existing core documents, unresolved items are explicit follow-ups, and no other agent is writing core documents. What the command performs: mark the task `completed`, move the entire directory, update the linked Todo item's `Task:` path to `TaskFlowDocs/achieved//`, close it, then verify the active path is absent, the achieved path exists, and the achieved root PRD and Plan both say `completed`. If any part fails, record the blocker and do not claim archival completion. -**`hooks/archive` refuses an unfinished task on its own.** It requires the Plan's `## Approval` to say `approved` for its current `> Task version:` with an approver other than `pending`, no unchecked `- [ ]` item, and no Step still `pending`, `in_progress`, or `blocked` — the same three gates `complete` applies, applied again where the move happens, because `archive` is also runnable by hand. All three are checked before the directory moves, so a refusal leaves the task active, the Todo untouched, and `achieved/` without it. +**`hooks/archive` refuses an unfinished task on its own.** It requires the Plan's `## Approval` to say `approved` for its current `> Task version:` with an approver other than `pending`, no unchecked `- [ ]` item, and every Step block closed — a Step is closed when it says `- Status: done`, and nothing else counts. That last one is a whitelist, and deliberately: it used to name the three words that meant unfinished, so any word it did not know (`completed`, a misspelling, an empty value, a missing line) read as finished and archived a Plan that was not. `hooks/task progress` has only ever accepted `pending|in_progress|done|blocked`, so `done` is the one word an author can write that means closed, and the two now read the same rule from both sides. The three gates are the same ones `complete` applies, applied again where the move happens, because `archive` is also runnable by hand. All of them are checked before the directory moves, so a refusal leaves the task active, the Todo untouched, and `achieved/` without it. **Closing the entry is part of the transaction, not a later tidying pass.** `hooks/archive` reduces the linked entry to the fields triage still needs — its heading plus `ID`, `Status`, `Goal`, `Task`, `Next action` — because a finished entry had become 78% of the file: eighty-five of them held 89 KB of the 114 KB `todo.md`. `Priority`, `Owner`, `Source`, `External`, `Added`, `Updated` and the free-form prose under any field go. A live entry keeps every field; only a closed one is reduced, and only `hooks/archive` does it, so `task next`, `task state` and `task reopen` never see a shortened entry they write to. An entry with no achieved plan.md to link — `- Task: Not promoted.`, or a `Task:` that is prose rather than a path — is left exactly as it is, because the prose has nowhere to go and cannot stay where it is: a `## Todo Record` heading at column 0 inside an entry is a new section to every hook that splits on `## `. diff --git a/skills/taskflow/references/runtime.md b/skills/taskflow/references/runtime.md index d161f37..dc68a91 100644 --- a/skills/taskflow/references/runtime.md +++ b/skills/taskflow/references/runtime.md @@ -241,8 +241,8 @@ A host with no hook wiring gets none of this, and that is not a gap to be filled | `hooks/task progress [verification] [--root ]` | Update one Plan Step and optionally append a verification line. | `done` rejects unchecked checklist items. | | `hooks/task unaided --considered "" [--root ]` | Record a deliberate unaided decision for a stage, which is what lets the pre-write gate release it and `approve` reconcile it. For `Test` it is also the stage's only carrier, so it spends the next unreleased evidence line when one is standing and reports which kind it wrote. | Exit `2` on a stage that is none of the four, a missing `--considered`, or a class outside the phase vocabulary, which it prints. It validates a class against the vocabulary and nothing else — it cannot judge whether the decision was right. | | `hooks/task complete --user-accepted [--root ]` | Run `completion-gate`, set core statuses, and invoke archive. | Requires explicit acceptance and leaves no active task on success. Refuses before the first write, so nothing has to be rolled back. | -| `hooks/archive ` | Run `completion-gate` — the Plan is approved for its current Task version, has no unchecked item and no unfinished Step — then move `TaskFlowDocs/` → `TaskFlowDocs/achieved/`, update the linked Todo item's `Task:` path and status to `done`, and verify the active path is absent, the achieved path exists, and the achieved root PRD and Plan both say `completed`. | Prints a check report; on any failure leaves the task active, the Todo unchanged, and exits nonzero. Both entry points into `achieved/` run the same file, so neither is a weaker way in. | -| `hooks/completion-gate ` | Decide whether the Plan may be treated as finished: approved for its current Task version, no unchecked item, no unfinished Step. `--approval-only` asks the narrower question `task state in_progress` and `task progress` need. | Exit `1` with one line naming the failing condition. Reads the Plan as a file in one `awk`; a `printf | grep -q` shape is silently disabled by `pipefail` on any Plan larger than the pipe buffer. | +| `hooks/archive ` | Run `completion-gate` — the Plan is approved for its current Task version, has no unchecked item and no unclosed Step — then move `TaskFlowDocs/` → `TaskFlowDocs/achieved/`, update the linked Todo item's `Task:` path and status to `done`, and verify the active path is absent, the achieved path exists, and the achieved root PRD and Plan both say `completed`. | Prints a check report; on any failure leaves the task active, the Todo unchanged, and exits nonzero. Both entry points into `achieved/` run the same file, so neither is a weaker way in. | +| `hooks/completion-gate ` | Decide whether the Plan may be treated as finished: approved for its current Task version, no unchecked item, no unclosed Step. A Step is closed only when it says `- Status: done` in column 0 — a whitelist, so an unrecognized or missing value counts as unfinished rather than as finished. A block is located by its `### Step ` heading anywhere in the Plan, not only under `## Steps`, because that is how `task progress` finds the block it edits. `--approval-only` asks the narrower question `task state in_progress` and `task progress` need. | Exit `1` with one line naming the failing condition. Reads the Plan as a file in one `awk`; a `printf | grep -q` shape is silently disabled by `pipefail` on any Plan larger than the pipe buffer. A Plan with no `### Step` block at all is not refused — that is a question about whether a Plan must have Steps, which this command does not answer. | | `hooks/reopen ` | Move `TaskFlowDocs/achieved/` → `TaskFlowDocs/`, record the Todo source/reopen reason in the Plan change log. | Prints a check report; exit nonzero on mismatch. | | `hooks/version ` | Copy only changed core documents plus `version.md` into `old/v/`, retain root docs, bump them to ``, and reset state/approval for review. | Prints archived paths + root version/state agreement; exit nonzero before mutation on invalid input. |