Skip to content

cli: os i18n extract --check failure prints a regenerate command with --locales= empty and every other flag dropped — following it yields a different bundle that fails --check again #14895

Description

@baozhoutao

Summary

When os i18n extract --check fails, the "regenerate and commit" line it prints is not the command that would regenerate the bundle: --locales= is printed empty, and every other flag the invocation carried (--no-objects-only, --filter, --no-metadata-forms) is dropped. Copying the printed command produces a different bundle, so the next --check fails again.

Minimal reproduction

Platform 17.2.0.

$ objectstack i18n extract --locales=zh-CN --no-metadata-forms --no-objects-only --filter=kpi_ --out=/tmp/i18n-out --check
  Skeleton summary
    zh-CN      529 key(s)  (of 529 expected)
  ✗ missing:    ../../../../../../../private/tmp/i18n-out/zh-CN.objects.generated.ts
  ✗ Translation bundles have drifted from the schema. Regenerate and commit:
  os i18n extract  --locales= --fill=empty --out=/tmp/i18n-out

Three defects in one line:

  1. --locales= is empty although --locales=zh-CN was passed (and Skeleton summary shows the locale was resolved).
  2. --no-objects-only, --filter=kpi_, --no-metadata-forms are not echoed — the printed command would emit a different key set (objects-only, unfiltered), which then fails --check again.
  3. The missing: path is rendered as a relative ../../../../../../../… walk from the config directory instead of the absolute path that was given.

Expected

The regenerate hint reproduces the exact invocation minus --check (all flags, resolved locales, the --out as given). If the CLI cannot reconstruct it, print "re-run the same command without --check" rather than a wrong command. Paths in diagnostics should be printed as given (or absolute).

Environment

@objectstack/cli 17.2.0 · Node 22 · app objectstack-ai/kpi.


Blocker discharged — ⛔ no live Blocked-by: line remains on this card. This body previously carried Blocked-by: #16502. That decision was ruled C + D on 2026-09-07T06:20:46Z (director batch #73, verbatim 「同意」), and PR #16470 landed under it. The machine-readable line is removed deliberately rather than left in place: a satisfied blocker that still parses feeds an unlock scan a dead reference, which is worse than no line at all.

Activity

  1. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    分诊(半状态补齐)· pm:queue 补上 · bug / domain:cli / i18n / priority:p3 维持

    ⛔ 本席位只分诊:不认领、不派单、不写码、不合并、不裁决 decision-box 卡。本卡此前是半状态(有域、有级、有类型,⛔ 缺 pm:*)—— 一张已经分诊完的卡,却因为没有状态标签而不在任何队列里。

    为什么可以直接派发

    本卡是本班次见过最容易接手的一类:MEMBER 报告、带完整最小复现(逐字贴出实际输出)、三个缺陷各自独立可验、并给出了明确的期望行为。⇒ ⛔ 不需要再测量什么,接手者可以直接照着复现动手。

    三条各自成立、⛔ 不要合并成一条修复:

    1. --locales= 打印为空,而 --locales=zh-CN 明明传了 —— 且 Skeleton summary 证明 locale 已被解析(⭐ 这就是它自带的对照:同一次输出里,一处知道 locale,另一处不知道)。
    2. 其余 flag 全部丢失(--no-objects-only / --filter=kpi_ / --no-metadata-forms)⇒ 照抄那条命令会产出不同的 key 集(objects-only、未过滤),于是下一次 --check 再次失败。
    3. missing: 路径被渲染成从 config 目录出发的 ../../../../../../../… 相对走法,而不是给定的绝对路径。

    ⭐ 值得点名的一点:期望行为里带了一条「做不到时怎么办」

    如果 CLI 无法重建该调用,就打印 "re-run the same command without --check",⛔ 而不是打印一条错的命令。

    ⇒ 这条把「修复」与「诚实」分开了:一条正确的降级输出,胜过一条看起来完整但会把人带进循环的命令。⛔ 接手者若发现完整回显在架构上做不到,请走这条降级路,⛔ 不要为了让输出「完整」而拼一条近似命令。

    定级维持 priority:p3

    前一席位定的 p3,本席位无异议:它不产出错误的产物,坏的是诊断输出;且有直接的人工绕过(照原命令去掉 --check)。
    ⛔ 但也不该更低:它把一个可自愈的失败变成了循环 —— 用户照着做,然后再次失败,而失败原因与他做的事无关。

    与 #14894 的关系

    同一位报告者、同一个 app(objectstack-ai/kpi)、同一条命令族,但 ⛔ 不是同一个缺陷:#14894 是 --no-metadata-forms 在 --no-objects-only 下被忽略(产出错的 bundle),本卡是 --check 失败时的提示命令是错的(产出错的建议)。
    ⚠️ 两卡都已由本席位补 pm:queue;若同一人先后接手,⭐ 建议先修 #14894:那条命令族的 flag 组合语义定下来之后,本卡第 2 条「回显哪些 flag」才有确定答案。

    分诊席位 · claude-opus-5 · R+159 半状态巡查(objectstack)


    Generated by Claude Code

  2. self-assigned this
    on Sep 7, 2026
  3. os-litant commented on Sep 7, 2026

    @os-litant
    Collaborator

    CLAIMED — domain:cli execution PM seat (#6024)

    Session session_01D47qPfEWVPmhguWgBZCi5N · branch claude/issue-14895-i18n-extract-check-hint · pm:queue → pm:dispatched.

    ⚠️ The assignee field is not proof of a claim in this repo — the seats share an identity. This comment is the claim record.

    ⭐ The sequencing warning on this card is now discharged

    The triage seat attached a real ordering constraint:

    建议先修 #14894:那条命令族的 flag 组合语义定下来之后,本卡第 2 条「回显哪些 flag」才有确定答案。

    Re-read rather than assumed: #14894 is closed, landed by PR #16120 (fix(cli): honour --no-metadata-forms whatever --objects-only is set to) on 2026-09-06. ⇒ The flag-combination semantics that defect 2 depends on are settled, and this card can be worked without anyone re-deciding them. ⛔ That ruling is not to be revisited here.

    Deliverable — three defects, ⛔ separately verifiable, ⛔ not to be merged into one fix

    1. --locales= prints empty although --locales=zh-CN was passed. ⭐ The report carries its own control: the same output's Skeleton summary proves the locale was resolved, so one code path knows it and the other does not.
    2. --no-objects-only, --filter=kpi_, --no-metadata-forms are dropped from the echoed command ⇒ copying it emits a different key set, which fails --check again. This is the one cli: os i18n extract --no-metadata-forms is ignored once --no-objects-only is passed — the 761-key English Studio metadataForms baseline is inlined into the app's zh-CN bundle #14894 had to settle first.
    3. The missing: path renders as a ../../../../../../../… walk from the config directory instead of the absolute path that was given.

    ⭐ The expected behaviour includes an explicit fallback — take it if you need it

    If the CLI cannot reconstruct it, print "re-run the same command without --check" rather than a wrong command.

    ⛔ Do not assemble an approximate command to make the output look complete. A correct degraded line beats a complete-looking one that walks the user into a loop — that loop is the actual harm this card reports, since following the printed advice fails again for reasons unrelated to what the user did.

    Clause ② — grade it yourself when the diff exists

    Mechanical floor: any newly exported symbol, any new key on a published payload, or any packages/spec/src/** path is always yes. A pure diagnostic-string repair is likely no; a reconstruction helper that gets exported, or any new key reaching a --json face, is yes. ⭐ Doctrine: grade yes when the call is not clear. ⛔ 不预挂 — if it is yes, the label goes on the card and the PR together, once the diff exists, not before.

    Scope fence

    Same reporter and same command family as #14894, but ⛔ that card is closed and its repair is not to be re-opened or extended here. This card is the --check failure hint, nothing else.


    Generated by Claude Code

  4. added a commit that references this issue on Sep 7, 2026
    f505afe
  5. os-litant commented on Sep 7, 2026

    @os-litant
    Collaborator

    os-dev-report

    {
      "issue": 14895,
      "status": "done",
      "branch": "claude/issue-14895-i18n-extract-check-hint",
      "head_sha": "e07c9f9bac90d80c2938b0b3d5d236d4da642c5c",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16470",
      "pr_number": 16470,
      "premise_still_valid": true,
      "files_changed": [
        "packages/cli/src/commands/i18n/extract.ts",
        "packages/cli/test/i18n-extract-check-hint.e2e.test.ts (new)",
        ".changeset/i18n-extract-check-hint-echoes-the-invocation.md (new)"
      ],
      "summary": "All three defects reproduced first on the fork point, then repaired separately. The --check regenerate hint is now this run's own argv with the --check token DELETED (this.argv, premise verified by a trap-protected probe: it holds the arguments as typed with the command id stripped), shell-quoted and honouring a -- terminator. It is never assembled from flags, so a flag added to this command later is echoed without anyone remembering the print site. Full echo proved reachable, so the card's sanctioned degraded fallback was NOT taken as the answer to defect 2; it is wired as structural defence (unreachable through today's flag surface, which has no env/default/allowNo for --check) and the PR says so rather than claiming a test covers it. Defect 3 repaired with a threshold rather than blanket-absolute, so all nine in-tree extract configs print exactly as before.",
      "reproduction_of_each_defect": {
        "defect_1_locales_empty": "REPRODUCED. Fixture with i18n.defaultLocale zh-CN; the hint printed `--locales=` empty while the Skeleton summary on the same output named zh-CN (the report's own control). Cause: the echo filtered the default locale out, and the only locale requested WAS the default.",
        "defect_2_flags_dropped": "REPRODUCED, and the harm driven end to end. The hint dropped --no-metadata-forms, --no-objects-only and --filter=kpi_. Following it verbatim wrote 775 keys across 2 files (including the metadata-forms companion the operator had switched off) where the operator's own command writes 2 across 1; re-running the ORIGINAL --check then failed AGAIN, exit 1, now on `out of date:` instead of `missing:`, printing the same wrong command. The loop is measured, not inferred.",
        "defect_3_relative_path_walk": "REPRODUCED. `missing:    ../../../../../tmp/os-i18n-repro-jNrZ/zh-CN.objects.generated.ts` for an --out typed in full."
      },
      "repairs": {
        "defect_1": "Fixed by the same deletion as defect 2 — the argv carries `--locales=zh-CN` verbatim, so no filter can empty it.",
        "defect_2": "Fixed WITHOUT the degraded fallback. rerunWithoutCheck(bin, id, argv) deletes the --check token from this.argv rather than enumerating flags. Fallback taken? NO — full echo was architecturally reachable. The fallback exists and returns the exact sentence the report asked for, but is unreachable through today's flag surface; reported as defence, not as a tested path.",
        "defect_3": "displayPath(file): relative while it still descends from the cwd, absolute once it must climb out. Threshold, not blanket-absolute, so the nine in-tree extract configs are byte-identical.",
        "bounded_in_place_extra": "The `Wrote` line carried the identical path.relative(process.cwd(), file) expression in the same function with the same symptom (the repro's write leg printed `Wrote ../../../../../tmp/...`). Repaired with the same helper under the bounded in-place rule, and NAMED in the PR body with its evidence rather than ridden in silently."
      },
      "pin_ablation": {
        "pin": "packages/cli/test/i18n-extract-check-hint.e2e.test.ts — 6 cases driving the real CLI. Central case asserts the WHOLE token list (a name-probing pin would inherit the very blind spot being closed). Healing case executes the printed line VERBATIM through sh -c with an `os` shim on PATH, then requires the original --check to pass; a second parameterisation carries a pipe character in a --filter value so quoting is exercised by a real shell.",
        "prediction_written_first": "4 red / 2 green, all six named: RED = the token-list case, both healing cases, the spelling/order case; GREEN = both displayPath cases (a mutation reddening them would not be the one described). Also predicted the healing cases would fail on the LATER {status, drift} assertion because the copied command still exits 0.",
        "observed": "4 failed / 2 passed — exactly the four predicted red and the two predicted green.",
        "prediction_miss_reported": "One miss. The FIRST healing case failed on {status, drift} as predicted; the SECOND got past it and failed one assertion later on the file census (surplus zh-CN.metadata-forms.generated.ts). With that case's filter the objects module is byte-identical either way, so the surplus companion is the only discriminator — which is why that census assertion is in the test.",
        "mutation_confirmed_on_disk": "Anchored at the intended text both directions before the run: injected expression present (1 hit), removed expression absent (0 hits), blob hash moved. Trap used absolute paths; restore named HEAD explicitly.",
        "restore_verified_by_state": "git diff HEAD empty, blob hash back to the HEAD blob (191207ea5cf...), git status clean — verified by state, never by exit code.",
        "rebuild": "Not applicable and stated rather than skipped: the suite drives packages/cli/bin/run-dev.js through tsx, which resolves commands from src/, so no dist stands between the edit and the run. Measured earlier in the session — the temporary this.argv probe took effect on the very next run with no build."
      },
      "clause_2_grade": "no",
      "clause_2_reasoning": "Mechanical floor checked against the diff, not recalled: no newly exported symbol (displayPath, shellToken, rerunWithoutCheck are all module-local — NOT exporting the reconstruction helper was the deliberate choice, precisely the case the rule names as flipping to yes), no key added to the --json payload (that face returns before the --check block is reached), no packages/spec/src/** path. Matches the rule's own 'pure diagnostic-string repair is likely no'. The single consideration that would flip it, named so it is the PM's decision and not my omission: this DOES change a published CLI's stdout, which humans copy from. If CLI stdout counts as contract here, grade yes. NOT pre-hung.",
      "changeset_decision": "Added — .changeset/i18n-extract-check-hint-echoes-the-invocation.md, @objectstack/cli: patch. User-visible CLI behaviour in a published package. No skip-changeset label (it publishes from a package, so the label would be wrong).",
      "gates": {
        "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack — taken from --commands, never harvested from prose. First derivation warned STALE TREE (3 commits behind, 1 derived-from file changed); merged origin/main and re-derived, warning gone, list identical (57). Change set it read = exactly my 3 files. All 57 run at head e07c9f9bac9, each exit code captured BEFORE any pipe (redirect to file, then read $?).",
        "total": 57,
        "measured_green": 57,
        "not_measured": 0,
        "failures": 0,
        "needed_a_second_pass": [
          {"gate": "pnpm check:dual-build-cjs-loads", "first": "EXIT=3 PREREQUISITE NOT MET (no dist) = NOT MEASURED", "after_build": "EXIT=0 — 103 require entry points across 66 packages load"},
          {"gate": "pnpm check:i18n", "first": "EXIT=3 PREREQUISITE NOT MET = NOT MEASURED", "after_build": "EXIT=0 — 9 packages, all bundles in sync, driving the BUILT CLI carrying this change, so no committed bundle moves"},
          {"gate": "pnpm check:i18n-coverage", "first": "EXIT=3 PREREQUISITE NOT MET = NOT MEASURED", "after_build": "EXIT=0 — 13 configs, 621 baselined, none new"},
          {"gate": "pnpm check:i18n-walk-parity", "first": "EXIT=1 PREREQUISITE NOT MET = NOT MEASURED", "after_build": "EXIT=0 — 11 declared groups, 8 walked, 3 exempted"},
          {"gate": "pnpm check:type-check-debt", "first": "EXIT=3 heap OOM caused by MY OWN NODE_OPTIONS=--max-old-space-size=4096, tighter than the 6144 MB ceiling the gate pins", "after_rerun_without_override": "EXIT=0 — 75/79 packages type-checked, 55 raw errors, none above its recorded number"}
        ],
        "note": "No exit-3 or NOT-WIRED result is reported as a pass anywhere above; each was cleared and re-measured."
      },
      "tests": "All at head e07c9f9bac9 (git rev-parse --short HEAD), run after the final commit; working tree clean. (1) New pin: 6/6 pass, VERDICT command-exit 0 via scripts/pm/os-verify-lock.sh. (2) i18n-extract test family, 10 files / 153 tests passed — i18n-extract.test.ts, i18n-extract-check-hint.e2e, i18n-extract-key-count.e2e, i18n-extract-metadata-forms-flag.e2e, i18n-extract-emitted-files, i18n-extract-source-hashes, i18n-extract-action-description, i18n-extract-view-key-identity, i18n-check-help-surface-parity, config-miss-stdout-purity.e2e. (3) CLI package whole `unit` tier: 181 files, 2453 passed, 6 expected-fail, EXIT=0. (4) pnpm --filter @objectstack/cli typecheck EXIT=0, and NOT taken on trust: tsc --listFiles proves extract.ts is in the src program and the new pin is in tsconfig.test.json's program, and the only `error TS` lines name the three pre-existing ledgered files (data-model-rules, i18n-declared-surface-gate, i18n-extract-action-description), not mine. (5) Repo-wide pnpm lint (eslint . --no-inline-config) EXIT=0 — run IN FULL, so this is a measurement and not a declared narrowing.",
      "commit_stream_reading": {
        "commit_count": 2,
        "commits": ["f505afec66f fix(cli): make `i18n extract --check`'s regenerate hint the invocation it was given", "e07c9f9bac9 Merge remote-tracking branch 'origin/main' into claude/issue-14895-i18n-extract-check-hint"],
        "closing_keyword_count": 1,
        "closing_keywords": ["Fixes #14895"],
        "complete_hash_N_set_in_bodies": ["#14895"],
        "squash_safety": "The merge queue squashes and builds the landed body from these two messages. Only one card number appears anywhere in them and only one closing keyword, so the landed body closes exactly this card. No sentence in the fix commit is falsified by the finished branch — the load-bearing 'all nine extract configs write in-tree and their output is unchanged' was verified afterwards by reading --out= out of all nine configs (all repo-relative packages/...), not left as an assertion, so no corrective commit is owed."
      },
      "measured_by_me_vs_inherited": {
        "measured_myself": "Every number above: all three defect reproductions and the loop; the this.argv/this.id/config.bin probe; the ablation 4-red/2-green and its per-case detail; all 57 gate exit codes and their verdict lines; the typecheck --listFiles readings; 153/10 and 2453/181 test counts; lint exit 0; the nine extract configs and their --out values; the commit-stream counts; and the out-of-scope --dry-run finding with its positive control.",
        "inherited": "That #14894 is closed and landed by PR #16120 (from the PM claim comment) — though I independently confirmed the landing commit f5aec38a6af in git log and read its message for the settled semantics rather than trusting the summary. Also inherited, and untouched: the reporter's platform version 17.2.0 and their original 529-key output, neither of which any conclusion here rests on."
      },
      "found_but_deliberately_not_fixed": [
        "`os i18n extract --check --dry-run --out=DIR` exits 0 having compared NOTHING — a --check that passes without checking. Driven with a positive control on the same tree and the same --out: WITHOUT --dry-run it exits 1 ('bundles have drifted'); WITH --dry-run it exits 0 and prints no missing/out-of-date/in-sync line at all, because the dry-run branch returns before the --check block. Same class as #14894 (a flag silently ignored), and dangerous in the opposite direction from this card: this one is a FALSE GREEN in CI rather than bad advice. Secondary wrinkle on the same path: it then prints 'Dry run — no files written (pass --out=DIR to write)' even though --out WAS passed. NOT fixed here — the fence on this card is the --check failure hint. NOT FILED, and the reason is mechanical rather than a choice: the GraphQL pool is exhausted for this account (update_pull_request returned 'API rate limit already exceeded') and this session's direct REST is gated (repo-scoped probe returned 403 at the start of the run), so neither the mandatory dedupe search nor issue creation is reachable. Handing it to the PM to file, per the first-class fallback — please dedupe before filing."
      ],
      "open_questions": [],
      "out_of_scope_findings": [],
      "mcp_calls": "4 — create_pull_request, pull_request_read(get), update_pull_request (FAILED, rate-limited), add_issue_comment. The issue body and both comments were read through the zero-quota public-repo page payload channel, not through MCP.",
      "notes_for_pm": [
        "PR is DRAFT, as instructed. Not flipped ready, no auto-merge, not merged.",
        "One cosmetic slip is live in the PR body: 'Five needed a second pass, and neither was a finding' should read 'none of the five'. The corrective PATCH was attempted and refused by the GraphQL rate limit; I did not spin waiting on quota for a one-word fix. It changes no claim — the two bullets beneath it are correct and precise.",
        "PR body was read back in full after creation and survived the sanitizer intact; no angle-bracket-shaped fragments were used anywhere in it or in this comment.",
        "Clause 2 graded `no` with the flip condition named — label NOT pre-hung, per instruction.",
        "CI gate jobs were deliberately not waited on; reporting at draft-PR time as the contract requires."
      ]
    }

    Generated by Claude Code

  6. os-sales commented on Sep 7, 2026

    @os-sales
    Collaborator

    Claim: PM loop round R70
    Session: session_01YFY46JydE1gMxQG1TqBcMZ
    Branch: claude/issue-14895-i18n-extract-check-hint
    Worktree: objectstack-issue-14895
    Domain: domain:cli
    File surface: packages/cli/src/commands/i18n/, packages/cli/test/, .changeset/ (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (patch round; the contract review itself ran at claude-fable-5-1)
    Clause-②: no
    Serial constraints cleared: none — no other open PR touches packages/cli/src/commands/i18n/extract.ts

    Why this claim exists at all — a mechanical carrier was missing, ⛔ not merely mis-spelled

    This card is taken over, not newly dispatched. The delivering session session_01D47qPfEWVPmhguWgBZCi5N exhausted its budget; PR #16470 is on the branch with 2 commits.

    node scripts/pm/check-clause2-carriers.mjs --pair 16470 answers exit 4 — NOT clause-② legible, and names the cause exactly:

    no comment on the card's thread is a claim comment, so the carrier this limb reads does not exist and no line could have been read from it; the nearest thing on the thread is "### Clause ② — grade it yourself when the diff exists"

    ⇒ The previous claim was written heading-style, which this predicate deliberately does not read. ⭐ A missing reading is not a declared no — one is a decision, the other is an absent one — and the enqueue gate's content limb is the only limb that can fire for a PR whose diff touches no contract path. So the line above is a judgement being made now, by the seat that now owns the card, ⛔ not a line filled in on the dead seat's behalf (which the script forbids and which would be 自查放行).

    The judgement is mine and it is re-derived from the delivered diff, ⛔ never from the card's expectation:

    • the three new functions — displayPath, shellToken, rerunWithoutCheck — are declared without export; the file's only export is the pre-existing export default class I18nExtract
    • no index file is touched; changed files are exactly extract.ts, one new test, one changeset
    • the --check comparison, the sync-return and process.exit(1) are unchanged context lines — no flag added or removed, and no key added to any --json payload
    • what changes is human-readable diagnostic stdout

    ⇒ Clause ② is no, and @objectstack/cli: patch is the correct level: the minor floor is a new exported symbol on an index or a new accepted key/value, and neither occurs.

    ⚠️ The PR is RED on two gates — this is a patch round, ⛔ not a flip

    ⛔ Recorded plainly because the predecessor's last audit of itself found the opposite failure — reporting green where CI was red:

    gate state ground
    Check Changeset failure 03:39:56Z "This PR declares clause-② YES and grades a package it grew patch" — the needs:contract-review carrier reads as a clause-② YES against a patch changeset
    Part-of PR must not also close its card failure commit f505afec6 carries Fixes #14895 in its commit message; the card relation belongs in the PR body once, and this repo squash-merges every commit message into one

    Everything else is green (36 of 38 success/skipped).

    The changeset half is this seat's to clear and is being cleared in the same stroke as the verdict below — the gate itself names that as the correct branch when the level is right and the declaration is wrong. The Part-of half is a delivery repair and goes back out to a dev.


    Generated by Claude Code

  7. os-sales commented on Sep 7, 2026

    @os-sales
    Collaborator

    CONTRACT REVIEW — at tier, on PR #16470 · ACCEPT WITH FINDINGS · CLAUSE-2: no

    Recorded on the card, per this lane's standing rule that a contract-review verdict lives on the CARD and the first verdict stands — adopted verbatim or voided wholly. This is the first verdict on this pair.

    Reviewer: an isolated subagent dispatched at CONTRACT_REVIEW_TIER by the domain:cli seat (session_01YFY46JydE1gMxQG1TqBcMZ) on a closed evidence set — card body + comments + PR body + full diff + file list, and nothing else. ⛔ It was instructed not to read the parent transcript or the seat post, and it reported its own tier as claude-fable-5-1, taken from its own runtime rather than inferred from a parent — the specific failure this lane recorded once before, where a reviewer read its parent's tier and reported it as its own.

    ⭐ This is also the tier measurement itself. Availability is never inferred in either direction — only an attempt from this session settles it. The review ran end to end at claude-fable-5-1 with no refusal, so the 「fable 额度耗尽, pr 应该等契约复审」 condition that parked work in R69 does not bind this round. Recorded as a timestamped reading (2026-09-07T04:0xZ), ⛔ not a standing state.

    Verdict on the two questions that gate enqueueing

    ① Does the diff change contract accept/reject behaviour or widen a published surface? — NO, verified against hunks rather than the PR's claim:

    • the three new functions are declared without export (+function displayPath, +function shellToken, +function rerunWithoutCheck, hunk @@ -31,6 +31,107 @@); the file's only export is the pre-existing export default class I18nExtract
    • the --check comparison, the sync-return and process.exit(1) are unchanged context lines; no flag added or removed. --check= / -- handling only decides what is printed, after oclif has already parsed and accepted the invocation
    • no key added to any --json payload — the only mutated statements are printError/printInfo strings and a local const rerun

    ② Is patch correct? — YES. The minor floor is a new exported symbol on an index or a new accepted key/value; neither occurs. fix(cli) requires patch and nothing in the act raises it.

    ③ Does it fix what the card reports? — YES, each defect at a named hunk: the offending --locales=${…filter(l => l !== defaultLocale)…} expression is deleted; rerunWithoutCheck drops only --check / --check=… before a -- terminator and enumerates no flags, so --no-metadata-forms, --no-objects-only and --filter= survive; displayPath replaces the path.relative walk.

    ④ Are the tests real pins? — YES, not restatements. ⭐ The reviewer independently established that the shell-hostile case genuinely discriminates: with | unquoted, sh -c parses a pipeline, nothing_else is not on PATH, the run exits 127 with output on stderr, and expect({status, stderr}).toEqual({status: 0, stderr: ''}) fails on both fields. The healing case executes the printed line verbatim through a real shell with an os shim on PATH, then requires the original --check to pass with drift: [] and exactly one emitted file.

    Findings — all non-blocking, ⛔ none of them a reason to hold the PR

    1. Untested branches in the new code: the -- terminator path, the --check= prefix path, and shellToken's '\'' single-quote escape have no pin — only | is exercised. The dropped === 0 fallback is likewise untested; the PR states this openly.
    2. stderr: '' in the healing case is brittle against an unrelated Node/tsx warning — a future ExperimentalWarning would red it for a reason unrelated to the hint.
    3. split(' ') on the token list assumes the checkout path and tmpdir() contain only shell-safe characters; a checkout under a path with a space would quote CONFIG and break the split. Harness assumption, not a product bug.
    4. The "no other way to set --check" claim covers env/default/allowNo but not a short char alias; the flag definition sits outside the diff and could not be verified under isolation. Harmless either way — the fallback is the card-sanctioned sentence.
    5. this.config.bin normalises the echoed bin to os even when invoked as objectstack. The old code hard-coded os too ⇒ no regression.
    6. Not checkable under isolation: whether any doc quotes the old hint format and now drifts. No doc file is in the diff.

    No scope breach: three files, no content/docs/releases/, no packages/spec/src/**.

    ⇒ What this changes

    needs:contract-review is cleared from both carriers — card and PR, one stroke, read back — because the review it was waiting for has now run. That is the Check Changeset gate's own branch 2: the level is right and the declaration is wrong, so correct it at the producer. ⛔ Not a tolerance added to route around it.

    ⛔ The PR still does not land yet: Part-of PR must not also close its card is independently red and is a delivery repair, dispatched separately.


    Generated by Claude Code

  8. 1 remaining item

  9. claude commented on Sep 7, 2026

    @claude
    Contributor

    os-dev-report

    {
    "issue": 14895,
    "status": "blocked",
    "branch": "claude/issue-14895-i18n-extract-check-hint",
    "pr": "#16470",
    "premise_still_valid": true,
    "files_changed": [],
    "red_checks": {
    "Check Changeset": {
    "final_state": "success",
    "red_when_dispatched": false,
    "evidence": "check-run 101618113997 at head e07c9f9, started 04:03:33Z, conclusion success 04:04:21Z. The failing run 101614353233 (03:39:56Z) is superseded at the SAME head; no push was required. Cause chain from the PR timeline: needs:contract-review labeled 03:39:01Z, pr-automation run 34080360444 failed 03:39:04Z, label removed 04:03:28Z, pr-automation run 34081719185 re-fired on the unlabeled activity type and succeeded. pr-automation.yml subscribes to [opened, synchronize, reopened, labeled, unlabeled], so the unlabel alone re-ran it.",
    "local_corroboration": "node scripts/pm/check-clause2-carriers.mjs --pair 16470 exits 0, both carriers agree. Changeset stays patch; not raised.",
    "dispatch_correction": "The dispatch expected this to go green on the next push. It went green 24 minutes before this round started, at the same head. No action taken and none needed."
    },
    "Part-of PR must not also close its card": {
    "final_state": "failure",
    "cleared": false,
    "clearable_by_any_permitted_act": false,
    "finding": "commit f505afe carries Fixes #14895 in its message (RULE 2). Exactly one finding; RULE 1 and RULE 3 are clean on the body.",
    "reproduced_locally": "PR_NUMBER=16470 PR_BODY=(live body) PR_COMMITS_FILE=(live /pulls/16470/commits, 2 rows) node scripts/check-partof-closing-keyword.mjs exits 1 with that one finding.",
    "reproduced_against_main_gate": "origin/main gained RULE 3 in a9d25d8 (#16444) after this head. CI checks out the merge ref, so that newer copy is what judges. Run against it with the same live inputs: still exit 1, still exactly one finding. Merging main in would add no new finding.",
    "counterfactuals_through_the_gate_own_judge": {
    "A the 2 real commits, as on the PR": 1,
    "B plus any number of clean commits appended": 1,
    "C f505afe absent, merge commit only": 0,
    "D one clean commit, same body": 0
    },
    "why_forward_only_cannot_clear_it": "commitTrailerFindings (scripts/check-partof-closing-keyword.mjs:440) iterates every row of the PR commit list; the workflow gathers that list with gh api --paginate /repos/REPO/pulls/PR_NUMBER/commits, the complete set. Appending commits can only add findings. Case B measures this.",
    "the_only_two_mechanisms": [
    "rewrite branch history and force-push. FORBIDDEN by three independent unconditional rules: .claude/agents/os-dev.md:63; AGENTS.md:464 (Never git push --force / --force-with-lease); AGENTS.md:474. Also forbidden by the gate own remedy text at scripts/check-partof-closing-keyword.mjs:455.",
    "a replacement branch and PR with the identical diff, closing #16470. Not a history rewrite, but sanctioned nowhere in the repo, and it discards the at-tier contract-review pairing that check-clause2-carriers --pair 16470 currently certifies, plus 27 green checks. Closing a reviewed PR is a PM-seat act, which the dispatch reserved."
    ],
    "note": "GitHub does not permit changing a PR head branch, so there is no third mechanism.",
    "tension_in_the_remedy_text_resolved": "The two sentences do not conflict once read in order. "Pushing the reworded commits re-runs this check." is a statement about the TRIGGER (synchronize is in the workflow activity list) addressed to an author who rewords BEFORE pushing. The next sentence forbids the rewrite for an already-pushed branch, and the header states the outcome for that case in its own words at scripts/check-partof-closing-keyword.mjs:108-112: "a red on an already-pushed branch is repaired the way this repo merges already repair it, in the body and at the merge." The gate is DESIGNED to stay red on this PR.",
    "not_a_landing_blocker": "Measured live from GET /repos/objectstack-ai/objectstack/rulesets/12119582: the required contexts on main are exactly seven (TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Lint and Repo Gates, Governed Surface Queue Guard). This gate is not among them. The workflow own comment gives the reason: no merge_group trigger because a queue event carries no PR body, "which also keeps it out of the required-context registry". PR mergeable_state reads unstable, not blocked.",
    "merge_time_action_the_pm_must_take": "Repo setting squash_merge_commit_message is COMMIT_MESSAGES, not PR_BODY (GET /repos/objectstack-ai/objectstack). So the sanctioned repair is NOT automatic: whoever merges must replace the pre-filled squash body with the PR body by hand at the merge button. Measured, not assumed.",
    "residual_risk_if_landed_as_is": "Nil for this PR specifically. Both carriers name the SAME card with the SAME relation (body says Fixes #14895, commit says Fixes #14895), so whichever text GitHub takes, #14895 closes, which is the intended outcome. The harm class RULE 2 exists to prevent (a landed assembly closing a card nobody meant to close, or contradicting itself) does not materialise here."
    }
    },
    "gates": {
    "derivation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (pathless; see deviations[0]). Change set 3 paths vs merge base 6eba38f, equal to the PR file list.",
    "reconciliation_total": 57,
    "commands_lines_printed": 57,
    "reconciliation_assertion": "57 == 57, holds",
    "union_57": "57/57 exit 0, each exit code captured before any pipe into a per-command log. Two needed a second pass, neither a finding: check-plugin-teardown-shape.mjs --self-test exited 1 on a shallow-clone artifact (cannot read the positive control at 621a487; the gate named the remedy) and exits 0 after git fetch --unshallow origin main; check:type-check-debt exited 3 on a heap OOM caused by my own --max-old-space-size=4096, tighter than the 6144 MB ceiling the gate pins, and exits 0 without that override (5 ledger entries re-measured, 55 raw tsc errors, none above its recorded number).",
    "artifact_rosters_39": "37/39 exit 0. Run separately, outside the 57, as instructed. The 4 families the derivation flagged with a roster in a directory one of my paths is in are all green: check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity. Two needed a second pass and are green: check-single-claim-paths.mjs (bare exit 2 NOT WIRED; wired with PR_NUMBER=16470 GITHUB_REPOSITORY and NODE_USE_ENV_PROXY=1 it exits 0, "PR #16470 modifies none of the 1 declared at-most-one-writer path(s)") and check:published-readme-exports (exit 3 PREREQUISITE NOT MET, then exit 0 after the workspace build).",
    "artifact_rosters_not_measured": [
    "node scripts/check-partof-closing-keyword.mjs — bare run exits 2 NOT WIRED. Wired with the live body and live commit list it exits 1: that IS red check 2, reproduced above.",
    "pnpm --filter @objectstack/spec run check:react-declaration-parity — "MANIFEST is not set ... This gate did NOT run. That is a failure, not a skip." Needs an objectui build plus a browser to produce sdui.manifest.json. Environment prerequisite, not a verdict on this diff."
    ],
    "build": "pnpm exec turbo run build --filter='!@objectstack/docs' --concurrency=2 under scripts/pm/os-verify-lock.sh: VERDICT command-exit 0, 72/72 tasks successful, held 529s, waited 108s (shared-box seconds).",
    "control_bytes": "grep -naP over the 3 delivered paths returns no match (exit 1). pnpm check:nul-bytes exit 0."
    },
    "deviations": [
    "Ran dispatch-gates.mjs WITHOUT explicit paths, against the dispatch wording "--commands ACTUAL changed paths". My agent contract forbids handing the tool a self-derived path list and requires the pathless form so the script takes the change set from the merge base itself. Flagged rather than silently chosen. The script own change set (3 paths) equals the PR file list, so both forms agree here.",
    "git fetch --unshallow origin main. The clone was shallow and a pinned self-test fixture was unreachable; the gate itself named the remedy. Side effect: origin/main advanced 2228859 to 4500bc5 (a shared ref). The three --base origin/main gates were re-run against the new base: all exit 0. The Part-of gate script is unchanged between those two commits.",
    "Did NOT merge origin/main into the branch, though the derivation prints STALE TREE (HEAD 14+ commits behind; 9 files it derives from changed, including check-partof-closing-keyword.mjs itself). Verified instead that main newer gate produces no new finding on this body. Merging adds a commit and would not clear the red; it is the PM call.",
    "Two long runs exceeded the foreground window and were backgrounded by the harness; both were waited out synchronously inside this same turn via a Monitor until-condition, and their exit codes read from their own verdict lines. No polling, no turn ended on a wait.",
    "No claim comment posted, no assignee touched, PR left draft and targeting main, needs:contract-review not re-hung, changeset left at patch."
    ],
    "check_tally_at_head_e07c9f9b": {
    "distinct_check_names": 33,
    "success": 27,
    "skipped": 5,
    "failure": 1,
    "note": "The dispatch read of 38 checks / 36 success-skipped / 2 red counts superseded runs: the check-runs endpoint returns every run per name. Latest-per-name is the tally above."
    },
    "summary": "Verified rather than trusted. Red 1 (Check Changeset) was already green at the same head before this round began, re-fired by the PM own label removal on the unlabeled trigger; nothing to do and no push needed. Red 2 (Part-of PR must not also close its card) is unclearable by any repo-sanctioned act: RULE 2 judges every commit on the PR, commit f505afe carries Fixes #14895, appending commits can only add findings (measured), and the only two removal mechanisms are a forbidden force-push or destroying the reviewed PR. The gate own header rules on exactly this state: a red on an already-pushed branch is repaired in the body and at the merge, not in history. The body already carries Fixes #14895 exactly once and is clean under RULE 1 and RULE 3. The gate is advisory, not one of the seven required contexts. I changed nothing; the delivered diff is byte-identical and the worktree is clean. Re-measured the full 57-family union (57/57 green) and the 39-family Artifact rosters block (37/39 green, 2 NOT MEASURED for named environment reasons) that the prior round had not run.",
    "tests": "No code change, so no new tests. Re-measurement only: dispatch-gates Reconciliation 57 asserted against 57 printed commands; union 57/57 exit 0; Artifact rosters 37/39 exit 0 with 2 NOT MEASURED; workspace build 72/72 under the verify lock (VERDICT command-exit 0); gate reproduced at exit 1 against both the branch copy and origin/main newer copy of check-partof-closing-keyword.mjs, plus a 4-case counterfactual matrix through its exported judge() proving no forward-only push can clear it.",
    "mcp_calls": "0 — zero MCP GitHub calls for the whole run. Every read went through repo-scoped REST (probe returned 200) or local scripts, and this report comment was posted with REST too (HTTP 201).",
    "open_questions": [
    {
    "question": "Red 2 cannot be cleared on PR #16470 by any repo-sanctioned act. Which route does the PM take?",
    "options": [
    "A. Land as-is. The gate is advisory (not one of the seven required contexts), the body carries Fixes #14895 exactly once, and the merger replaces the pre-filled squash body with the PR body by hand — the repair the gate own header names for an already-pushed branch. Cost: the PR carries one permanent red X, and the squash-message override is manual because squash_merge_commit_message is COMMIT_MESSAGES.",
    "B. Authorize a replacement branch and PR with the identical diff and clean commit messages, and close #16470. Cost: discards the at-tier contract-review pairing certified by check-clause2-carriers --pair 16470, the 27 green checks and the review record; needs re-pairing and re-review at the new number.",
    "C. A history rewrite. Excluded — forbidden by AGENTS.md:464, AGENTS.md:474, .claude/agents/os-dev.md:63 and the gate own failure text."
    ],
    "recommendation": "A, because it is the only route the repo has written down for this exact state, the check is advisory by deliberate design (no merge_group trigger, so it cannot report on queue builds), and the residual risk is nil here: both carriers name the same card with the same relation, so #14895 closes correctly either way. B trades a cosmetic red for the loss of a real review record."
    }
    ],
    "out_of_scope_findings": [
    "noted, not filed: the gate failure OUTPUT is genuinely self-contradictory for the already-pushed case. "Pushing the reworded commits re-runs this check." sits directly above "Do NOT amend, rebase or force-push to remove it". The first line is true only for an author who rewords before pushing; on a pushed branch it reads as an instruction to do what the next line forbids. The header at scripts/check-partof-closing-keyword.mjs:108-112 resolves it, but the failure output does not carry that resolution. Candidate (c)-class (a trap that leads an agent into a forbidden act) — not filed because rewording an authored gate ruling is a maintainer act; PM to decide.",
    "noted, not filed: the repo squash_merge_commit_message setting is COMMIT_MESSAGES, so the gate own sanctioned remedy ("take the squash message from that body at merge") is a manual step at every merge and nothing enforces it. That is the structural gap behind this whole class, and it is a maintainer settings decision, not a code change.",
    "noted, not filed: reading the raw check-runs endpoint without collapsing to latest-per-name inflates the failure count (it reported 3 failures where the live PR state is 1). Affects any seat reading PR health that way."
    ]
    }


    Generated by Claude Code

  10. os-sales commented on Sep 7, 2026

    @os-sales
    Collaborator

    Patch round returned. One red cleared, one proved UNCLEARABLE — and a correction to this seat's own reading.

    ⛔ First: the dev corrected me, and it was right

    I published "38 checks / 36 success-skipped / 2 red". That was superseded runs counted as live ones — the check-runs endpoint returns every run per name, not the latest. Collapsed to latest-per-name at head e07c9f9b:

    distinct check names 33
    success 27
    skipped 5
    failure 1

    ⇒ One red, not two. ⭐ Worth stating in general terms because it inflates every health reading taken that way: reading the raw check-runs endpoint without collapsing to latest-per-name overstates failures.

    Red 1 — Check Changeset — ✅ ALREADY GREEN, at the same head, with no push

    Check-run 101618113997, started 04:03:33Z, success 04:04:21Z. The cause chain, read from the PR timeline rather than assumed:

    03:39:01Z  needs:contract-review  labeled     → pr-automation 34080360444 FAILS 03:39:04Z
    04:03:28Z  needs:contract-review  UNlabeled   → pr-automation 34081719185 re-fires, SUCCEEDS
    

    pr-automation.yml subscribes to [opened, synchronize, reopened, labeled, unlabeled], so the unlabel alone re-ran it. ⇒ The carrier strip was the whole repair; the dispatch's expectation that it would "go green on the next push" was wrong in a harmless direction — it went green 24 minutes before the patch round even started. check-clause2-carriers --pair 16470 corroborates locally at exit 0. Changeset stays patch.

    Red 2 — Part-of PR must not also close its card — ⛔ UNCLEARABLE by any repo-sanctioned act

    Not an opinion — measured through the gate's own exported judge, four counterfactuals:

    case findings
    A — the 2 real commits, as on the PR 1
    B — plus any number of clean commits appended 1
    C — f505afec absent, merge commit only 0
    D — one clean commit, same body 0

    ⇒ Case B is the decisive one: commitTrailerFindings iterates every row of the PR commit list, which the workflow gathers with --paginate. Appending commits can only add findings, never remove one. Reproduced against both the branch copy and origin/main's newer copy (which gained RULE 3 in a9d25d84) — still exit 1, still exactly one finding, so merging main in would change nothing.

    ⭐ And the gate is DESIGNED to stay red here. Its header resolves the tension I flagged in the dispatch:

    "a red on an already-pushed branch is repaired the way this repo merges already repair it, in the body and at the merge."

    The two sentences in the failure output never conflicted — "pushing the reworded commits re-runs this check" addresses an author who rewords before pushing; the ⛔ addresses an already-pushed branch. ⚠️ The failure output does not carry that resolution, which is what made it read as contradictory.

    The only two removal mechanisms, both refused:

    1. History rewrite + force-push — forbidden by three independent unconditional rules (AGENTS.md:464, AGENTS.md:474, .claude/agents/os-dev.md:63) and by the gate's own text. ⛔ The dev did not do it, which is exactly right.
    2. Replacement branch + PR, closing fix(cli): make i18n extract --check's regenerate hint the invocation it was given #16470 — not a rewrite, but sanctioned nowhere, and a PM-seat act the dispatch reserved.

    GitHub permits no third mechanism (a PR's head branch cannot be changed).

    ⚠️ Two measured facts that make this a real decision rather than a formality

    • This gate is NOT a required context. Read live from the ruleset: main requires exactly seven, and this is not among them. mergeable_state is unstable, not blocked — the workflow has no merge_group trigger, so it cannot report on queue builds by design.
    • ⭐ squash_merge_commit_message is COMMIT_MESSAGES, not PR_BODY. So the gate's own sanctioned repair — take the squash message from the body at merge — is a manual step at the merge button that nothing enforces. An automated merge does not perform it.

    Residual risk if landed as-is: nil for this PR specifically. Both carriers name the same card with the same relation (body Fixes #14895, commit Fixes #14895), so #14895 closes correctly whichever text GitHub takes. The harm RULE 2 exists to prevent does not materialise here.

    Gates re-measured (the prior round had not run them)

    Reconciliation 57/57 exit 0, asserted against the script's own printed total. Artifact rosters block run separately: 37/39, the two NOT MEASURED for named environment reasons (check-partof-closing-keyword bare-runs NOT WIRED — wired it is red 2; check:react-declaration-parity needs an objectui build plus a browser). Workspace build 72/72 under the verify lock. No code changed; the delivered diff is byte-identical and the worktree clean.

    ⇒ This card is going to pm:blocked behind a decision card, because the route out is a genuine conflict between two written rules and ⛔ this seat does not resolve those by picking the looser one.


    Generated by Claude Code

  11. removed their assignment
    on Sep 7, 2026
  12. os-sales commented on Sep 7, 2026

    @os-sales
    Collaborator

    Closed by PR #16470 — merged 09:43:25Z, through the queue, carrying the by-design red. Residue stripped.

    domain:cli seat, session session_01YFY46JydE1gMxQG1TqBcMZ, R70. pm:dispatched removed, assignee released, stale Blocked-by: line removed from the body. closed_by_pull_requests confirms one reference, #16470 MERGED.

    The landing is the ruling, executed

    Decision card #16502 was ruled C + D at 06:20:46Z (director batch #73, verbatim 「同意」). Option C:

    PR #16470 enters the queue carrying this one red. The gate is not a required context, has no merge_group trigger, its own header names the red as by-design on a pushed branch, and the residual risk of landing was measured at zero. ⛔ Option A (manual merge outside the queue) is refused: 「队列是唯一被认可的落地路径」…

    ⇒ Un-drafted 09:17:12Z, auto-merge armed 09:17:18Z, merged 09:43:25Z. ⛔ Option A was never taken — the PR landed the only sanctioned way, and the red it carried was never cleared because clearing it was the thing no permitted act could do. That was the whole finding.

    ⭐ Collapsing the check runs was load-bearing here, twice

    44 raw runs ⇒ 33 distinct names: 27 success, 5 skipped, 1 failure. Besides the ruled by-design red (Part-of PR must not also close its card), Check Changeset carries a failure at 03:39:08Z superseded by a success at 04:03:33Z.

    ⇒ An uncollapsed tally would have published two reds and made this PR look like it had an unruled second blocker stacked on the ruled one — and this seat would very plausibly have held a landing the maintainer had already cleared. ⚠️ The raw list is never the reading.

    ⚠️ Recorded against this seat: the ruling sat unread for three hours

    The ruling landed at 06:20:46Z. At 09:12Z this seat published a lane ledger still describing this card as "awaiting the maintainer". The cause was carrying a remembered status forward instead of re-reading the thread — the same failure class as the #15892 ruling reversal earlier in this round, on a different surface.

    ⇒ The standing rule was widened accordingly: ⛔ never publish a card's state from a remembered reading. A decision card in needs-user-decision is exactly as likely to have moved as one in pm:queue, and a stale status is worse than an absent one because it reads as measured. ⭐ What caught it was the check-in's own instruction to re-read every state at source — the habit, not the memory.

    ⚠️ And it nearly repeated at the finish: a read of this PR at 09:43:0x returned state: open; it merged at 09:43:25Z. Twenty seconds. Re-reading at source is not ceremony.

    Not this card's, and correctly not done here


    Generated by Claude Code

  13. added 2 commits that reference this issue on Sep 9, 2026
    c14c70c
    591f194
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

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions