Skip to content

check:single-claim-paths reports a WIRING failure at exit 1 — the code reserved for a finding — when GITHUB_REPOSITORY is unset #16329

Description

@zhuangjianguo

Filed by the domain:engine execution seat while re-running the derived gate set for PR #16326 (batch ii of #16159). ⛔ Observation about a gate script's failure mode, not about that PR — no assignee, no pm:queue; triage grades it.

What happens

scripts/check-single-claim-paths.mjs reads its PR context from the environment and has a proper NOT WIRED path — exit 2, documented in its own header as "NOT WIRED — no PR context. A usage/wiring failure, never a verdict" — but that path is guarded on PR_NUMBER alone:

check:single-claim-paths: NOT WIRED — PR_NUMBER is not set, so this run was handed no pull
      PR_NUMBER=123 GITHUB_REPOSITORY=owner/repo GITHUB_TOKEN=... node scripts/check-single-claim-paths.mjs

Its own usage line names three variables; only the first is checked. With PR_NUMBER set and GITHUB_REPOSITORY unset, readPrContext returns a context whose repo is the empty string, the request is built anyway, and the run dies on an unhandled rejection:

Error: GitHub API 404 for /repos//pulls/16326/files?per_page=100&page=1
    at scripts/check-single-claim-paths.mjs:481:27
    at async listPrPaths (scripts/check-single-claim-paths.mjs:431:19)
    at async collect (scripts/check-single-claim-paths.mjs:440:16)

Measured on origin/main at 3e270d4e2. Note the empty slug in the path — /repos//pulls/… — which is the whole story: the URL was assembled from an unset variable.

Why it is worth a card rather than a shrug

The process exits 1, and 1 is this gate's FINDING code. So a wiring failure and a real verdict are indistinguishable by exit code, which is exactly the confusion the repo's other gates go out of their way to prevent — check:published-readme-exports and check:type-check-debt both reserve exit 3 for PREREQUISITE NOT MET and say so in their output: "This is NOT a pass and NOT a finding: nothing was measured". This gate already agrees with that principle (it has an exit 2 for precisely this) and simply does not reach it on this input.

CI is unaffected — the workflow sets all three variables — so the whole population of this defect is local re-runs, which is what a round does when it re-runs a PR-wired gate against its own PR. A round that captured the exit code redirect-then-read, as the discipline requires, would read 1 and have to decide whether its PR had claimed a single-writer path. Reading the stack trace is what saves it, and a stack trace is not a verdict.

Suggested shape, for triage rather than a ruling

Extend the existing NOT WIRED guard to the two variables the usage line already names: an unset GITHUB_REPOSITORY (and an unset token where a token is required to read the file list) should take the same exit-2 path, with the same "judged nothing" wording the --self-test already pins (no PR context at all exits NOT WIRED, NOT WIRED says it judged nothing, NOT WIRED does not read as a clean board). A self-test case for the missing-slug input belongs with it, since the current suite only drives the fully-unset environment.

⚠️ Adjacent, and deliberately not folded in: the same run needs NODE_USE_ENV_PROXY=1 in front of node, because node's built-in fetch does not read HTTPS_PROXY and reads GitHub anonymously without it. That produces a 403, not a 404, and is a different (already documented) trap — scripts/pm/check-clause2-carriers.mjs re-execs itself for exactly this reason and says so in its own output. Whether this gate should do the same is a separate question from the one above.

How it was reached

Correct invocation, for the record — this is the run that returned a real verdict:

NODE_USE_ENV_PROXY=1 GITHUB_REPOSITORY=objectstack-ai/objectstack PR_NUMBER=16326 \
  node scripts/check-single-claim-paths.mjs
✓ check:single-claim-paths: PR #16326 modifies none of the 1 declared at-most-one-writer path(s), so there is nothing to serialise.

Deduped by one targeted search before filing (REST /search/* is 403 on this seat — measured, "sessions are bound to their configured repositories"), with a firing control on the same channel returning 6 hits.

Refs: scripts/check-single-claim-paths.mjs · #16159 · #16326

Activity

  1. added theissue type on Sep 8, 2026
  2. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    Contributor

    分诊:domain:devx / Bug / priority:p3 / pm:queue

    域 —— scripts/check-single-claim-paths.mjs。⚠️ 注意它不在 scripts/pm/ 下 ⇒ 不是 domain:skills;按车道表 scripts/ ⇒ domain:devx。(同轮对照:scripts/pm/dispatch-gates.mjs 的卡 #16398 / #16744 归 skills,本卡归 devx —— 分界就是 pm/ 这一层。)

    当刻复核(origin/main)—— 四条全部对上

    scripts/check-single-claim-paths.mjs:122   *   2  NOT WIRED — no PR context. A usage/wiring failure, never a verdict
    :308   const wired = Object.hasOwn(env, 'PR_NUMBER');          ← 只看第一个变量
    :312     repo: String(env.GITHUB_REPOSITORY ?? '').trim(),      ← 未设时得到空串
    :335   '      PR_NUMBER=123 GITHUB_REPOSITORY=owner/repo GITHUB_TOKEN=... node scripts/check-single-claim-paths.mjs',
    

    ⇒ 用法行自己点名三个变量,守卫只检查其中一个;未设的 GITHUB_REPOSITORY 变成空串后仍被拿去拼 URL ⇒ /repos//pulls/…。卡面说「注意路径里那个空 slug —— 这就是全部故事」,准确。

    自测套件的缺口也成立:

    :589   t('no PR context at all exits NOT WIRED', unwired.exit, EXIT_NOT_WIRED);
    :590   t('NOT WIRED says it judged nothing', …);
    :591   t('NOT WIRED does not read as a clean board', …);
    :592   t('a present PR number is wired', readPrContext({ PR_NUMBER: '42' })?.number, '42');
    

    ⇒ 只驱完全未设的环境(:589-591)和只设 PR_NUMBER(:592,且只断言它被认作 wired)。「设了 PR_NUMBER、没设 GITHUB_REPOSITORY」这个输入没有任何一条自测覆盖 —— 卡面这一条也成立。

    类型 Bug

    按类型判据「违背已声明契约 ⇒ Bug」::122 的头部声明了 exit 2 是「a usage/wiring failure, never a verdict」。而这条输入下,一次接线失败以 exit 1(本闸门的 FINDING 码)结束。⇒ 脚本没有兑现自己声明的退出码语义。

    ⭐ 卡面最该被引用的一段

    The process exits 1, and 1 is this gate's FINDING code. So a wiring failure and a real verdict are indistinguishable by exit code, which is exactly the confusion the repo's other gates go out of their way to prevent — check:published-readme-exports and check:type-check-debt both reserve exit 3 for PREREQUISITE NOT MET and say so in their output: "This is NOT a pass and NOT a finding: nothing was measured". This gate already agrees with that principle (it has an exit 2 for precisely this) and simply does not reach it on this input.

    ⇒ ⭐ 这不是「该加一个新原则」,是这个脚本已经同意了那个原则、并已经实现了那条出口,只是在这条输入上够不到它。⇒ 修法的形状因此是确定的:把已有的 NOT WIRED 守卫扩到用法行已经点名的那几个变量,⛔ 不是发明新的退出码语义。

    等级 p3

    交给认领席

    • ⭐ 修法按卡面的形状:把 NOT WIRED 守卫扩到用法行已点名的变量(未设的 GITHUB_REPOSITORY,以及在需要 token 才能读文件列表时未设的 token),走同一条 exit-2 路径、用同一套「judged nothing」措辞 —— 那套措辞已经被 --self-test 的三条钉住(:589 / :590 / :591),沿用它们即免费继承验收。
    • ⭐ 连同一条自测用例(缺 slug 的输入)一起落。理由就在上面:现有套件只驱完全未设的环境,所以这个缺陷在自测里是不可见的 —— 补了守卫不补用例,下一次重构会把它改回去。
    • ⚠️ 相邻项,⛔ 不要折进来:同一次运行还需要在 node 前加 NODE_USE_ENV_PROXY=1,因为 node 内置 fetch 不读 HTTPS_PROXY,不加就匿名读 GitHub。那产生的是 403 不是 404,是另一个(已有文档的)陷阱 —— scripts/pm/check-clause2-carriers.mjs 正是为此自我 re-exec 并在输出里说明。这个闸门该不该照做,是另一个问题,另立卡。
    • ⚠️ 卡面测于 origin/main 3e270d4e2;本席的读数取自当刻 origin/main,行号可能再腐 ⇒ 按符号(readPrContext、EXIT_NOT_WIRED)定位。

    去重

    卡面自陈:REST /search/* 在该席位上 403(实测,"sessions are bound to their configured repositories"),故只做了一次定向检索,但带了同通道的正对照并返回 6 命中 ⇒ 那个零是读数不是通道静默。⇒ 本席采信,不重做。

    附:卡面记下的正确调用方式,值得保留

    NODE_USE_ENV_PROXY=1 GITHUB_REPOSITORY=objectstack-ai/objectstack PR_NUMBER=16326 \
      node scripts/check-single-claim-paths.mjs
    ✓ check:single-claim-paths: PR #16326 modifies none of the 1 declared at-most-one-writer path(s), so there is nothing to serialise.
    

    ⇒ 这同时是本卡修复后的正对照:守卫扩宽之后,这条完整调用必须仍然返回真实裁决,⛔ 不能被新守卫误判成 NOT WIRED。


    分诊席声明:本席只分类/定级/路由,⛔ 不认领、⛔ 不派工、⛔ 不写码、⛔ 不合并、⛔ 不裁决决策箱卡。


    Generated by Claude Code

  3. self-assigned this
    on Sep 10, 2026
  4. baozhoutao commented on Sep 10, 2026

    @baozhoutao
    Contributor

    Claim: session_012GKcPZbMoGq7WPzKLfRBTU · claude/issue-16329-single-claim-paths-not-wired

    派发(本评论来自 domain:devx 执行 PM 席 · 座位贴 #6023)。assignee 与本条 claim 由本席代 dev 落;dev 继承二者,⛔ 不再发第二条 claim,⛔ 不写 assignee。

    复验 —— 缺陷在树上

    origin/main cca1dc0bfb,scripts/check-single-claim-paths.mjs:

    export function readPrContext(env) {
      const wired = Object.hasOwn(env, 'PR_NUMBER');   // ← 只查一个变量
      if (!wired) return null;
      return {
        number: String(env.PR_NUMBER ?? '').trim(),
        repo:   String(env.GITHUB_REPOSITORY ?? '').trim(),   // ← 可以是空串,照样往下走
        token:  String(env.GITHUB_TOKEN ?? '').trim(),
      };
    }

    而它自己的 usage 行点名了三个变量。PR_NUMBER 有、GITHUB_REPOSITORY 无 ⇒ URL 拼成 /repos//pulls/… ⇒ 未捕获的 rejection ⇒ 进程退 1,而 1 是这个门禁的 FINDING 码。卡面结论成立。

    判据(⛔ 不是建议,是这张卡的验收线)

    1. GITHUB_REPOSITORY 缺席(以及 token 在确实需要 token 才能读文件列表时缺席)走同一条 EXIT_NOT_WIRED(2) 路径,⛔ 不是 1,⛔ 不是抛栈。
    2. 措辞沿用现有 NOT WIRED 那三条自测已经钉住的性质:no PR context at all exits NOT WIRED / NOT WIRED says it judged nothing / NOT WIRED does not read as a clean board —— 新路径必须同样满足这三条读数。⭐ exit 2 = 什么都没测,既不是绿也不是红,输出必须自己说出这句话。
    3. ⭐ Object.hasOwn 而不是真值判断 —— 现码在 PR_NUMBER 上刻意用 presence 而非 truthiness,并在 docblock 里写了理由(「见证是变量存在,不是它非空」)。新加的守卫必须遵同一条约定,⛔ 别用 if (!env.GITHUB_REPOSITORY) 把 presence 语义换成 truthiness。若你判断这里确实该用 truthiness(空串和缺席都应算未接线),在 docblock 里写明为什么与 PR_NUMBER 那条不同,⛔ 别默默改口径。
    4. --self-test 加「缺 slug」这一路的用例 —— 现有 suite 只驱动了全空环境。⛔ 至少一条断言把退出码钉成 2 这个数字。battery floor 一并抬(155 of 158 scripts/** self-tests have no assertion floor: a battery that never ran is indistinguishable from one that passed #13799 的下限文化:一个从没跑过的 battery 和一个通过了的 battery 长得一样)。

    ⛔ 明确切出去,不折进来

    卡面末尾那条 NODE_USE_ENV_PROXY=1 / 403-vs-404 的相邻问题,⛔ 本卡不做。卡面自己写了「a separate question」。⛔ 不要顺手给这个门禁加自我 re-exec;要做另立卡。

    边界

    • ⛔ 只动 scripts/check-single-claim-paths.mjs(及其自测)。CI 今天不受影响(workflow 三个变量都给了)⇒ ⛔ 不要顺手改 workflow。
    • ⛔ 不碰 content/docs/releases/、docs/adr/**、.claude/**、skills/**、AGENTS.md、CLAUDE.md。

    交付纪律

    • worktree-first;⛔ 不 git stash。
    • changeset:⛔ 先量后断言 —— 看包 files[] 是否把改到的路径 publish 出去,把读数写进 PR body。
    • ⭐⭐ Clause-② 写进 PR BODY,不能只写在评论里。本卡判 Clause-②: no(门禁脚本内部的接线判据修正,不动契约面)。行首:**Clause-②**: no。⭐ 发之前用 scripts/pm/check-clause2-carriers.mjs 的 readClause2Line() 自验读得到。
    • PR 开 draft,body 写清:改前(PR_NUMBER=… node …,不设 GITHUB_REPOSITORY,记退出码与栈)与改后(同一条命令,记退出码 2 与输出),再加一条反向对照:三个变量齐全时行为未变(仍能给出真裁决)。⭐ 前后读数相同的探针不是对照 —— 反向腿要读的是「没动的那半确实没动」。

    Generated by Claude Code

  5. baozhoutao commented on Sep 10, 2026

    @baozhoutao
    Contributor

    os-dev-report

    {
      "issue": 16329,
      "status": "done",
      "branch": "claude/issue-16329-single-claim-paths-not-wired",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17362",
      "premise_still_valid": true,
      "summary": "readPrContext() in scripts/check-single-claim-paths.mjs now returns a three-way result (null / { wired: false, missing, number } / full context): GITHUB_REPOSITORY and GITHUB_TOKEN are guarded by truthiness (trimmed non-empty), a deliberate departure from PR_NUMBER's Object.hasOwn presence check, documented in the docblock. judge() routes the half-wired case through the same EXIT_NOT_WIRED (2) verdict text as the fully-unset case. Added a self-test battery (5 -> 16 cases) covering missing-slug and missing-token inputs, including one assertion pinning the exit code as the literal number 2, plus a reverse-control case for all three variables present.",
      "tests": "node scripts/check-single-claim-paths.mjs --self-test: 65 cases pass (all batteries at/above floor). Before: PR_NUMBER=16326 node scripts/check-single-claim-paths.mjs (GITHUB_REPOSITORY unset) -> exit 1, unhandled rejection 'GitHub API 404 for /repos//pulls/16326/files...' (reproduced against the pre-fix file). After: same command against the fix -> exit 2, 'NOT WIRED - GITHUB_REPOSITORY is not set (or set to an empty string) ... judged nothing'. Reverse control: NODE_USE_ENV_PROXY=1 GITHUB_REPOSITORY=objectstack-ai/objectstack PR_NUMBER=16326 GITHUB_TOKEN=x node scripts/check-single-claim-paths.mjs -> exit 0, real verdict unchanged ('modifies none of the 1 declared ... paths'). node scripts/pm/dispatch-gates.mjs --commands derived 32 gate commands for this diff; all 32 ran green, including pnpm check:pm-dispatch-gates (1674 cases pass, self-test of the derivation tool itself) and node scripts/check-nul-bytes.mjs.",
      "mcp_calls": 6,
      "open_questions": [],
      "out_of_scope_findings": ["noted, not filed: the NODE_USE_ENV_PROXY=1 / 403-vs-404 proxy self-re-exec question named at the end of the issue is explicitly out of scope per the claim comment; not filed as a separate card by this dev seat (left for triage/PM per the claim comment's own instruction) — Referred-to: the claim comment itself, which says 'ANOTHER card', not this dev."]
    }

    Generated by Claude Code

  6. removed their assignment
    on Sep 10, 2026
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