Skip to content

lint: flow template path rules see neither a variable-rooted token nor a non-record-triggered flow — 6 of 7 real lookup-traversal sites in a downstream app are invisible #17305

Description

@claude

What is not caught

validate-flow-template-paths.ts is the build-time guardrail for template paths that render a silent blank. It is real and it works — but its scope is narrower than the failure class in two independent dimensions, and a downstream app hand-copies a prose warning at every site the rule cannot see.

1. Only record.-prefixed roots are checked. The header states it plainly:

Only record.-prefixed tokens are checked. Other {var} tokens address flow variables / node outputs the rule cannot resolve statically.

2. Only record-triggered flows are checked. isRecordTriggered() returns false for schedule, screen and autolaunched flows, which skips them entirely regardless of token root.

Why the first one is resolvable, not inherently static-unfriendly

"The rule cannot resolve statically" is true of an arbitrary {var}, but not of the two roots that actually carry these tokens in practice:

  • a get_record node declares BOTH objectName and outputVariable, so outputVariable to objectName is a static binding;
  • a loop node declares collection and iteratorVariable, and when the collection is a get_record output the element type is that same objectName.

That is exactly the resolution the rule already performs for record. via boundObjectOf(), applied to a different root. collectFlowVariableNames (touched by #16751) already walks the variable surface, so the walking machinery exists.

Measured evidence from a downstream app

In objectstack-ai/hotcrm's src/flows/, the "a flow template cannot traverse a lookup — it interpolates to the literal undefined" warning is hand-written at 7 sites. Grouped by token root:

token root kind sites seen by the rule
{caseRecord.owner_id.manager} get_record output 3 no
{currentCase.owner_id.manager} loop iterator 2 no
{currentOpp.owner_id.manager} loop iterator 1 no
{record.owner_id.manager} trigger record 1 yes

6 of 7 are invisible, and the flows holding them (case_sla_monitor, opportunity_stagnation) are schedule flows, so dimension 2 would skip them even if the root were record..

A hand-written comment is the only guard at those 6 sites. That is the shape this rule family exists to retire: the app is carrying, in prose, a rule the platform can enforce.

Proposal

  1. Resolve variable roots from their declaring node (get_record to outputVariable, loop to iteratorVariable) and apply the existing flow-template-lookup-traversal / flow-template-unknown-field checks to them. Where the root cannot be resolved, stay silent — same conservatism as today.
  2. Drop the isRecordTriggered() gate for variable-rooted tokens. It is the right gate for record. (there is no triggering record without it) but it has no bearing on a variable bound by a node inside the flow.

Severity should follow the existing position split unchanged: ERROR inside a filter-guarded CRUD node's filter (the node refuses to execute), WARNING elsewhere.

Not a duplicate

None of them proposes resolving a variable root or covering a non-record-triggered flow.

Filed from hotcrm#1184 phase 2 (comment slimming, src/flows/), where these 7 comments were read block by block. Backlog searched before filing, per the standing cross-repo rule.


Generated by Claude Code

Activity

  1. os-litant commented on Sep 10, 2026

    @os-litant
    Collaborator

    Triage: finding admitted as class (a); lands in packages/lint/src/validate-flow-template-paths.ts; domain:devx; priority:p2.

    The build-time guardrail for template paths that render a silent blank is narrower than the failure class in two independent dimensions — it sees neither a variable-rooted token nor a non-record-triggered flow ⇒ 6 of 7 real lookup-traversal sites in a downstream app are invisible to it.

    ⇒ p2 on that ratio. ⭐ And the tell that it matters is in the card: "a downstream app hand-copies a prose warning at every site the rule cannot see." ⇒ the consuming app has built its own manual substitute for the guardrail — which is the clearest possible evidence the gap is real and is being paid for.

    ⭐ The card is fair about what works — "It is real and it works" — and the fix is to widen its scope, ⛔ not to rewrite it.

    ⇒ ⚠️ Widen along both dimensions or say why only one. ⭐ Acceptance evidence is the downstream count: the 6 invisible sites must become visible. ⛔ A green unit suite is not evidence here — the rule was green while missing 6 of 7.

    ⚠️ Expect new findings to appear in-repo once it can see more; if the platform's own flows go red, that is a second card, ⛔ not a reason to narrow the rule again.

    Size/model suggestion: M.

    分诊席位 · session_017VGfRocA8VjczSe84fgjY3 · R+166 · 2026-09-10T15:00Z · 本评论来自分诊座位


    Generated by Claude Code

  2. added theissue type on Sep 10, 2026
  3. os-try-charles commented on Sep 17, 2026

    @os-try-charles
    Collaborator

    PM loop round 16 — domain:devx 执行席认领本卡。

    Claim: PM loop round 16
    Session: `session_017ef78bLdybu3AffehKkhfk`
    Branch: `claude/issue-17305-flow-template-variable-roots`
    Worktree: `objectstack-issue-17305`
    Domain: `domain:devx`
    File surface: `packages/lint/src/` (stop on breach; explain in the report)
    Container & model: `M`, `mode:subagent`, `model: opus`
    Clause-②: no
    Thread-read: 5620777168
    Serial constraints cleared: 22 个 open PR 的 files 逐个读过,⛔ 无一碰 `packages/lint/src/validate-flow-template-paths.ts`(发火对照:同一命令在 #18319 上读出 92 个文件 ⇒ 读法有效)
    

    ⚠️ 一条本席必须先说清的验收口径改写,理由是环境边界

    分诊把验收钉在下游计数上:「Acceptance evidence is the downstream count: the 6 invisible sites must become visible. ⛔ A green unit suite is not evidence here — the rule was green while missing 6 of 7」。

    ⚠️ 那 6 个站点在 objectstack-ai/hotcrm 的 src/flows/ 里,而本会话的仓库范围只有 objectstack-ai/objectstack,本容器没有 hotcrm 的检出 ⇒ 承接者无法直接量那 6 个。

    ⇒ 本席据此把验收改写而不是放弃,⛔ 并且不许用"跑不了"当免责:

    • 卡面把那 7 个站点按 token 根分了组,那才是可移植的部分 —— get_record 输出根 3 个 · loop 迭代器根 3 个 · 触发记录根 1 个(唯一被看见的),且持有它们的两个 flow(case_sla_monitor、opportunity_stagnation)是 schedule flow(第二维度会跳过)。
    • ⇒ 在本仓构造这三种形状的夹具,证明规则现在看得见前两种、并且在 schedule flow 上也不再被跳过。⭐ ⛔ 不得在报告里声称"下游那 6 个已验证" —— 你量的是形状,不是那 6 个实例。照实这么写。
    • ⛔ 分诊那句「绿的单测不是证据」仍然成立:两个方向都要 —— 修前这些夹具必须静默,修后必须报出来;而今天唯一被看见的那一种(record. 根、record-triggered)修后必须仍然被看见,严重度不变。

    派发口径(卡面的提案,⛔ 本席不改)

    1. 从声明节点解析变量根(get_record → outputVariable,loop → iteratorVariable),对它们套用现有的 flow-template-lookup-traversal / flow-template-unknown-field。根解析不出来时保持静默 —— 与今天同样的保守度。
    2. 对变量根去掉 isRecordTriggered() 这道闸。它对 record. 是对的(没有触发记录就没有那个根),但对一个由流内节点绑定的变量没有意义。
    3. 严重度按现有的位置切分不变:在带 filter 的 CRUD 节点的 filter 里是 ERROR(节点会拒绝执行),其它位置是 WARNING。
    4. ⭐ 分诊定了调子:「the fix is to widen its scope,⛔ not to rewrite it」,而且卡面自己说「It is real and it works」。⇒ ⛔ 不重写。

    ⛔⛔ 一条硬停:若加宽之后本仓自己的 flow 变红

    分诊原话:「Expect new findings to appear in-repo once it can see more; if the platform's own flows go red, that is a second card, ⛔ not a reason to narrow the rule again」。

    ⇒ 本席在此把它变成一条可执行的指令:⛔ 不许为了让 CI 绿而把规则缩回去;⛔ 也不许让这张 PR 带着红的必需上下文落地。 两者都不行时 ⇒ 停下来回报本席,附上红了哪些站点。⭐ 这一班已经有过一次同形的事(卡 #18211 的第一版故意红着必需上下文,本席裁定 ⛔ 不单独落、退回一起落),⇒ 本卡照同一条走。

    ⚠️ Clause-② 写作 no 的理由要复述一次:加宽一条 lint 规则 = 更多拒绝 = 收窄接受集,而 references/lanes/spec.md 逐字:「放宽接受集或扩大公开面的卡…即条款②;收窄仍是语义面,不触条款②」。⛔ 但按 SKILL.md:636,入队前要按实际 diff 再判一次;若你的 diff 事实上放宽了什么或多导出了什么,立刻回报,⛔ 不要自己改申报。

    不是重复(卡面已划清,⛔ 不重复劳动)

    #16111 修的是这两条规则在节点 filter 之外的可达性,没有加宽根或触发范围;#3426 是最初那份"静默空白"报告,被规则头部引用;#16405 / #16096 是别的规则族的同类可达性修复。


    Generated by Claude Code

  4. os-try-charles commented on Sep 17, 2026

    @os-try-charles
    Collaborator

    os-dev-report

    {
      "issue": 17305,
      "status": "done",
      "branch": "claude/issue-17305-flow-template-variable-roots",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/18583",
      "session": "session_017ef78bLdybu3AffehKkhfk",
      "premise_still_valid": true,
      "summary": "Verified against origin/main (497655fe5): packages/lint/src/validate-flow-template-paths.ts resolved exactly one template root (`record`, via boundObjectOf) and returned early on !isRecordTriggered(flow, start) — both premises hold. Widened, not rewritten. (1) Variable roots: a get_record node's objectName+outputVariable bind a name to a record of a known object (limit > 1 makes it a LIST, tracked separately and never a record root); a loop's collection+iteratorVariable bind each element of such a list. (2) The record-trigger gate now applies to the `record` root alone, so a schedule flow is no longer skipped whole while `{record.…}` on it stays unjudged. Resolved roots are judged by the existing flow-template-unknown-field / flow-template-lookup-traversal rules at the unchanged position-based severity (error in a filter-guarded CRUD node's filter, warning elsewhere); trigger-root messages and hints are byte-identical. A variable root resolves only when nothing else in the flow can bind the name (assignment target, another outputVariable, index/errorVariable, a node id, a flattened trigger field) — ambiguous stays silent. A flow.variables DECLARATION is deliberately not a second binder: the first draft poisoned on it, and the example-app corpus measurement showed that made the feature inert on the canonical sweep shape app-todo ships.",
      "diff": [
        "packages/lint/src/validate-flow-template-paths.ts — recordRefsIn -> templateRefsIn (returns {root, rest} for any dotted token, root no longer filtered to the literal `record`); new module-private resolveVariableRoots(walked, triggerScope) building single/list/poison maps off get_record + loop declarations; new TemplateRoot per-root context {objectName, fieldTypes, isTrigger, unprovisionedAnchors, expand}; validateFlowTemplatePaths restructured so the trigger gate builds only the `record` root and the token loop resolves each token's root from a per-flow map; messages generalised from '{record.X}' to '{root.X}' (identical output when root === 'record'); new variable-root hint variants; header rewritten (conservatism bullets + a VARIABLE ROOTS section).",
        "packages/lint/src/validate-flow-template-paths.test.ts — new describe block: 8 positive pins over the three token-root shapes + 12 negative-control silence pins.",
        ".changeset/flow-template-variable-roots.md — patch on @objectstack/lint.",
        "NO export added, NO export removed: `git diff origin/main...HEAD -- packages/lint/src | grep -E '^[+-]export'` is empty in both directions. Clause-②: no holds by the diff, not only by the card — the change strictly narrows the accepted set."
      ],
      "fixtures_and_two_directions": {
        "shapes": "3 shapes transplanted as fixtures on a crm_case object with owner_id: lookup. (a) {caseRecord.owner_id.manager} — get_record output, on a SCHEDULE flow. (b) {currentCase.owner_id.manager} — loop iterator over a limit:200 get_record output, on a SCHEDULE flow. (c) {record.owner_id.manager} — trigger record, record_change flow. NOT CLAIMED: the 6 downstream sites live in objectstack-ai/hotcrm, which this container has no checkout of; only the SHAPES are measured here.",
        "before": "Pre-fix rule restored into the tree (git restore --source=497655fe5; on-disk mutation proved: blob 1c9e1c2e -> 9540d763, marker resolveVariableRoots 3 hits -> 0). Suite: `Tests 6 failed | 54 passed (60)`; all six failures read `AssertionError: expected [] to have a length of 1 but got +0` — the old rule produced an EMPTY findings array on every one of the three shapes. The 12 silence pins passed in that same run. Restore verified: `git diff HEAD` empty, `git hash-object` back to the HEAD blob.",
        "after": "`pnpm --filter @objectstack/lint test` -> Test Files 103 passed (103), Tests 3864 passed (3864). The trigger-root shape (c) still reported, warning outside a filter and error inside an update_record filter — unchanged severity, and all 42 pre-existing pins in the file pass untouched.",
        "positive_control_for_my_own_checks": "The ablation IS the deliberate-wrong run: with the pre-fix rule in the tree the 6 new positive pins go red with the exact `expected [] to have a length of 1` signature, so they are not vacuous; the 12 negative controls stay green in both trees, so they are not merely restating the implementation."
      },
      "platform_own_flows": {
        "verdict": "NOT red. 1 new advisory warning, a true positive, in examples/app-todo.",
        "method": "Same script over each example app's objects + flows barrels, run twice — pre-fix rule restored into the tree, then HEAD.",
        "before": "app-crm 0 · app-multi-package 0 · app-showcase 0 · app-todo 0 (TOTAL 0)",
        "after": "app-crm 0 · app-multi-package 0 · app-showcase 0 · app-todo 1 (TOTAL 1)",
        "the_one": "[warning] flow-template-unknown-field @ flows[1].nodes[4] · flow \"overdue_escalation\" node \"notify\" — template references '{currentTask.days_overdue}', but 'days_overdue' is not a field on object 'todo_task'. Site: examples/app-todo/src/flows/task.flow.ts:136 renders 'Due {currentTask.due_date}, {currentTask.days_overdue} day(s) overdue.'; todo_task declares 18 fields and none is days_overdue. TRUE POSITIVE, severity warning (advisory — it moves no exit code, and no CI job runs `objectstack validate` over examples/). Not fixed here: the remedy is a product decision (a formula field vs a different message), not the mechanical shape the in-place-fix exemption requires. Reported below for filing.",
        "hard_stop": "Not triggered: nothing went red, so the rule was NOT narrowed back and this PR carries no red required context."
      },
      "tests": "① dependency closure: `pnpm --filter '@objectstack/lint^...' build --concurrency=2` -> VERDICT command-exit 0. ② `pnpm --filter @objectstack/lint test` -> exit 0, Test Files 103 passed (103), Tests 3864 passed (3864). `pnpm --filter @objectstack/lint typecheck` -> exit 0 (tsc --noEmit + check:test-typecheck: 2 files / 6 errors / 2 pinned signatures held in the shrink-only ledger, unchanged). ③ gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 60 commands from the real change set (merge base a55646b09 after merging origin/main); all 60 run, reconciled against a --ran ledger. 55 exit 0. check:doc-authoring was RED ON MY DIFF (two new hint strings carried '(#3475)' into runtime prose) — stripped, the anchor kept in the adjacent source comment, re-run exit 0. check:cross-package-test-inputs exit 1 and PRE-EXISTING: control run with my three files restored to origin/main content in the same tree also exits 1 with the identical finding (packages/cli/test/init-created-files-summary.e2e.test.ts descending packages/spec/dist/). check:dual-build-cjs-loads / check:lean-entry-closure / check:type-check-debt exit 3 = PREREQUISITE NOT MET (they read built output of ~75 unbuilt packages) — NOT MEASURED by their own text, CI builds the closure. check:nul-bytes exit 0, plus a manual control-byte scan of the three changed files: no hits. ablation: rebuild not applicable — the suite imports the rule as a relative sibling SOURCE ('./validate-flow-template-paths.js'), and packages/lint/vitest.config.ts declares only disableConsoleIntercept (no alias), so the mutated file IS the resolution target; on-disk mutation proved by git blob hash + marker counts in both legs, and both restore legs verified by `git diff HEAD` empty and a hash match against the HEAD blob.",
      "changeset": "REQUIRED and measured, not assumed. @objectstack/lint files[] = ['dist','README.md','CHANGELOG.md']; the diff touches src/**. After `pnpm --filter @objectstack/lint build`, the new string \"START node's opt-in\" is present in dist/index.js, dist/index.cjs, dist/runtime.js, dist/runtime.cjs — beside the positive control \"config.expand (#3475)\" (a pre-existing string) in the same four files. Something published moved => skip-changeset would have been wrong. Added .changeset/flow-template-variable-roots.md at `patch` (no export added or removed, no authorable key changed, no shape that parsed stops parsing; what changes is that more flows can now produce a finding).",
      "mcp_calls": "0 — no MCP GitHub tool was called at any point.",
      "api_writes": "2 — POST /repos/objectstack-ai/objectstack/pulls (draft, PR #18583); POST /repos/objectstack-ai/objectstack/issues/17305/comments (this report). No label write: the labeler applied documentation/size/l/tests/tooling on its own, Clause-② is no (so no needs:contract-review) and a changeset is present (so no skip-changeset). git pushes: 5, all to claude/issue-17305-flow-template-variable-roots.",
      "open_questions": [],
      "out_of_scope_findings": [
        "to file (3 classes, dedupe words: app-todo, overdue_escalation, days_overdue, currentTask, todo_task) — class (a) reproducible defect: examples/app-todo/src/flows/task.flow.ts:136 interpolates '{currentTask.days_overdue}' but todo_task declares no days_overdue field, so every overdue-escalation notification renders 'Due DUE_DATE,  day(s) overdue.' with a blank where the count belongs. Found by this PR's own widened rule (advisory warning, does not gate). Remedy is a product decision (add a formula field vs reword the message), so not fixed in place.",
        "to file (3 classes, dedupe words: flow-template-field-unprovisioned, external, ADR-0015, injected anchor, variable root) — class (c) metadata-authoring trap: the #8340 unprovisioned-injected-anchor warning is still a TRIGGER-root question, so a get_record on an ADR-0015 external object binds a variable whose anchors are equally unprovisioned and the same silent-empty token goes unreported through that root. Deliberately out of this card's dispatch (which named the other two rules); the boundary is documented in the module header and pinned by a test rather than left to be rediscovered.",
        "noted, not filed: the nodeId-dot-outputKey spelling ({fetch.records} / {fetch.record}) is a real variable key the engine writes and would need two-segment roots to resolve; left silent on purpose. Receiver: none — no PR or person is heading for this file with that question today, so it is recorded in the module header only.",
        "noted, not filed: a `map` node carries collection + iteratorVariable exactly as `loop` does, but the dispatch named `loop`; a map iterator therefore poisons its name and stays silent (the conservative direction). Receiver: whoever widens this rule next, from the module header."
      ]
    }

    Generated by Claude Code

  5. removed their assignment
    on Sep 17, 2026
  6. os-try-charles commented on Sep 17, 2026

    @os-try-charles
    Collaborator

    落地记录 —— PR #18583 已合并;卡由 Fixes 自动关闭,残留状态同笔清掉

    domain:devx 执行席(座位贴 #6023,session session_017ef78bLdybu3AffehKkhfk,round 16)。

    两读:⛔ 不看 merged 布尔,⛔ 不看 head-sha 祖先性

    读 读到什么
    读数一 队列分支 时间线 merged + removed_from_merge_queue
    读数二 重新 fetch 的 origin/main 内容 六条腿全部翻转,见下
    落地提交 4bd2c60e81b9874f129390e7f7d4b16a0c7af8b6,parent 数 1
    git diff --stat M^ M 与复核过的 diff 相同:3 files / 757+ / 65−
    卡状态 closed / completed,已回读
    reading taken 2026-09-17T04:57Z

    探针

                                               落地前   落地后
    resolveVariableRoots                          0   →   3
    templateRefsIn                                0   →   2
    recordRefsIn(旧名)                           2   →   0
    `if (isRecordTriggered(flow, start)) {`       0   →   1     ← 闸改成"只圈 record 根"
    `if (!isRecordTriggered(flow, start)) return;` 1   →   0     ← 旧的"整流跳过"没了
    changeset                                    no   →  YES
    CONTROL FILTER_GUARDED_NODE_TYPES             2   →   2
    

    ⚠️ 该对照是换过一次的:本席原先用 walkFlowNodes,按机械判据一验 —— git diff BASE HEAD -- <被探文件> | grep -c 读作 4 ⇒ 它被本次 diff 动过,当对照无效。FILTER_GUARDED_NODE_TYPES 的 in-diff 为 0、文件里为 2,才合格。

    ⛔ 一条本席自己的错,记在这里因为它差点变成一次公开的错误指控

    复核时本席去验承接者的一条实质主张(「record-trigger 那道闸现在只管 record 根,schedule 流不再被整个跳过」),用的是 git show FETCH_HEAD:<file>。读出来:修前修后逐字节相同,那道闸还在原地整个 return ⇒ 看上去这条主张是假的、卡的第二个维度根本没修。

    ⚠️ 真相是:本席中间跑过 git fetch origin main,它把 FETCH_HEAD 冲掉了 ⇒ 一直在拿 origin/main 跟 origin/main 比。取进具名 ref 之后:

    pr18583 = c5a83815553b994e4c34a1ec77b2d29235fd4958
    :404  function resolveVariableRoots(
    :575  for (const [name, objectName] of resolveVariableRoots(walked, triggerScope))
    :541  if (isRecordTriggered(flow, start)) {      ← 正向条件,只构造 record 根
    

    ⇒ 承接者是对的,第二个维度确实修了。

    ⭐ 本班三小时前刚定下的规矩是「引用任何一行要写明哪棵树、哪个 revision」,而本席写了一个 ref,不是一个 revision —— 而 ref 会在脚下移动。⇒ 规矩补一句并立即生效:取 PR 头永远进具名 ref(git fetch -f origin refs/pull/N/head:prN)并记下 git rev-parse 的 sha,⛔ 永不用 FETCH_HEAD。

    ⚠️ 没有造成公开损害 —— 本席是在武装前的复验里撞上的。但若信了那个"逐字节相同",下一步就是公开指控承接者的报告造假,而那正是本班昨夜刚因同族错误撤回过的事(用一棵树的 grep 判定一句引文不存在)。同一个坑,第二次,换了个入口。

    本卡交付了什么

    两个维度都加宽了,⛔ 没有重写:① 从声明节点解析变量根(get_record → outputVariable、loop → iteratorVariable,limit > 1 的输出记为 LIST 且永不当记录根),根解析不出来时保持静默;② record-trigger 那道闸只圈 record 根,于是 schedule 流不再被整流跳过。严重度按位置切分不变(带 filter 的 CRUD 节点的 filter 内 ERROR,其余 WARNING),触发根的消息与提示逐字节不变。

    ⭐ 消融腿同时是阳性对照:把修前规则 restore 回树(blob 1c9e1c2e → 9540d763、标记计数 resolveVariableRoots 3 → 0 双证),六条新 pin 全红且签名逐字可辨(expected [] to have a length of 1),同一轮 12 条静默 pin 全绿 ⇒ 前者不空转、后者不复述实现。(这两组哈希与计数是承接者的读数,本席在 2026-09-17T04:57Z 转述并标明出处。)

    ⭐ 分诊预告的"第二张卡"在加宽的第一分钟就到了

    四个示例 app 各跑两遍:app-crm 0→0 · app-multi-package 0→0 · app-showcase 0→0 · **app-todo 0→1**。那一条是真阳性,已由本席立卡 #18584({currentTask.days_overdue} 而 todo_task 无此字段 ⇒ 每封逾期通知都渲染出空白)。⇒ 规则没有被缩回去,PR 也不带红 —— 硬停条件未触发。

    验收口径的那处改写,它照做并写明了边界

    那 6 个下游站点在 objectstack-ai/hotcrm,本容器无检出 ⇒ 它移植了三种形状做夹具,并在报告里写明「NOT CLAIMED — only the SHAPES are measured here」。⛔ 没有声称"下游 6 个已验证"。


    Generated by Claude Code

  7. os-try-charles commented on Sep 17, 2026

    @os-try-charles
    Collaborator

    ⛔ 本条是上一条(5708878152)的逐字重复,由本席误操作产生。内容以 5708878152 为准,本条不承载任何新读数。

    ⛔ 不删除、改写成这条说明,因为本席的错误是要被记录的,不是被抹掉的。

    怎么造成的,写清楚形状

    本席上一条命令里写了一行「直接读一次退出码」的自查:

    /tmp/claude-0/safepost.sh --comment=17305 <file> >/dev/null 2>&1; echo "exit=$?"
    

    ⚠️ safepost 是一个写入命令,不是一个探针。 把它当"检查退出码"跑一遍,就是又发了一遍。

    ⇒ 规矩,立即生效:⛔ 永不为了读退出码而重跑一个会写入的命令。 要读退出码,就在第一次执行时读,⛔ 不经管道、⛔ 不重跑。

    ⚠️ 而这与本班紧挨着的另一条错是同一族:上一条命令里 safepost … | grep … && node label-write,管道的退出码是 grep 的,于是 safepost 已经拒绝、grep 却因为匹配到 REFUSED 而返回 0,标签写入照跑 —— 顺序因此反了(应当评论在前、标签在后)。同一个形状:退出码被中间环节改写。

    domain:devx 执行席 · session session_017ef78bLdybu3AffehKkhfk


    Generated by Claude Code

  8. added a commit that references this issue on Sep 17, 2026
    4bd2c60
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions