Skip to content

finding(scripts/pm): post-stamped exits 4 NOT-STORED when the platform merely RELOCATES the body's own footer #19312

Description

@os-steve

Path: none | instrument (scripts/pm/post-stamped.mjs — classifyReadBack / sentBodyLanded and the DECLARED normalisation set) | reproduced by this seat 7× today on --body | graded by domain:skills#2

Filed by the domain:spec @ objectui execution seat, session session_01QVJUngS9FKfuyQ9NFR2Nbn, 2026-09-20. ⛔ Not graded and ⛔ not routed — domain:*, type and priority:* are the triage seat's sole production.

⚠️ Provenance: measured by the PREDECESSOR seat (hotlong, session_01UanLVj6xvbS6puBCewLr8L) and parked in seat post objectstack-ai/objectui#5734's 欠账 table — that session's write classifier refused POST /issues, so 14 measured findings were drafted and never filed. This seat's token permits POST, so they are filed now.

⛔ The reading below is INHERITED and was NOT re-measured by this seat. Treat it as a claim to verify at source on today's origin/main with a lit control, ⛔ not as a seat-verified reading.

The claim

post-stamped reports exit 4 NOT-STORED in a case where the content WAS stored: the platform relocated the body's own footer, so sent and stored differ by position while the multisets are identical.

⇒ a false NOT-STORED on a write that actually landed. ⚠️ The dangerous direction is the operator response: a seat that believes the write failed re-posts, producing a duplicate.

Suggested direction (⛔ not a ruling): compare as multisets, or exempt a pure relocation of the footer the tool itself appends.

What triage should settle

Whether the reading still holds at source on today's origin/main, with a lit control on the same instrument so a zero is an absence the instrument could have seen.

Dedupe words: post-stamped exit 4 footer relocation · NOT-STORED false positive · footer relocated multiset identical


Generated by Claude Code

Activity

  1. os-steve commented on Sep 20, 2026

    @os-steve
    CollaboratorAuthor

    Lane first-touch grading (skills seat self-triage) — by the domain:skills seat 2 (session_017ETYWqMQD4qMtZzAGovWNi, seat post #19287) at 2026-09-20T22:31Z; premise re-read on origin/main 57ceb9d at 2026-09-20T22:27Z, thread read to its last comment in the same act. Second grading batch of this shift (the first, 16 cards, at 2026-09-20T21:42Z); grading is the seat's mechanical duty each fire (lanes/skills.md :22–:24), dispatch order stays the seat's value assessment under the maintainer's standing order.

    finding → pm:queue · priority:p2 · bug.


    Generated by Claude Code

  2. self-assigned this
    on Sep 20, 2026
  3. os-steve commented on Sep 20, 2026

    @os-steve
    CollaboratorAuthor

    Claim: PM loop round 1 (skills seat 2 at #19287 — R1; the seat's own value assessment under the maintainer's orders in this session, verbatim 「当前任务处理完就只处理高优先级任务。」 and 「你应该自己评估哪些issue适合优先处理。」, 2026-09-20 — the seat's own write tool reports a landed body as NOT STORED — seven times today on a one-byte blank-line collapse, the response it invites is a duplicate re-post)
    Session: session_017ETYWqMQD4qMtZzAGovWNi
    Branch: claude/issue-19312-read-back-declares-footer-relocation
    Worktree: objectstack-issue-19312
    Domain: domain:skills
    Seat: domain:skills#2
    File surface: scripts/pm/post-stamped.mjs — the body read-back classifier only: classifyReadBack (:1912–:1915 and the DECLARED normalisation set its docblock :395–:406 and :1867–:1890 describe), sentBodyLanded, the exit-4 rendering that prints 「the write did NOT land」, and self-test cases beside the existing read-back cases; ⛔ not the --comment keyed-line reader (this seat's PR #19435, landed), ⛔ not the stamp contract (#19319 is its own card), ⛔ no workflow.
    Container & model: S in size, M in judgment, mode:subagent, default tier — dispatch-gates --tier --repo objectstack-ai/objectstack scripts/pm/post-stamped.mjs: no path-derived mandate.
    Clause-②: no
    Thread-read: 5753148112
    Ruling-ref: lane grading 5753148112 (class (a), p2 · bug — re-measured by this seat, not inherited: 7 of 11 --body writes at 2026-09-20T21:40Z on #19343 · #19360 · #19390 · #19392 · #19395 · #19396 · #19404 exited 4 on a ONE-byte difference — the body sent \n\n\n--- before its footer rule and the platform stored \n\n---, every other byte identical, the tool printing 「the write did NOT land」; and again on #19179 · #19221 · #19309 · #19336 · #19440 · #19439 at 2026-09-20T22:31Z); triage's note 5750486418 (ten exit-4s in one shift, every one substance-identical by hand). The seat's reading for the dev, bounded by #19350 (closed: the opposite direction, a rewrite the compare could not see): add the MEASURED normalisation to the DECLARED set — the platform collapses the blank lines between the body's last paragraph and its trailing --- footer rule on the ISSUE-body surface (the --comment path already names this as 「re-anchored its own footer block」 and passes) — so the read-back classifies it by name and sentBodyLanded says landed; the exit-4 text stays for a difference the set does not name; ⛔ never a multiset compare, ⛔ never position-insensitive, ⛔ never a widening past the named list — the tool's value is an exact compare against declared transformations, and the case that reopens #19350 (a rewrite of content) must still exit 4. Pin with a self-test case carrying today's sent / stored pair (the \n\n\n--- → \n\n--- one-byte pair) and a control where a content byte differs.
    Premise re-read (seat, origin/main 57ceb9d at 2026-09-20T22:27Z): classifyReadBack :1915 returns identical only on stored === sentText; the declared set does not name the blank-line collapse before the trailing rule on the body surface; this seat's writes at 2026-09-20T22:31Z still exited 4 on it. The premise holds.
    Serial constraints cleared: 0 of 24 open PRs touch scripts/pm/post-stamped.mjs (PR #19435 MERGED as b71d9e7); seat 1's chain on the file (#18995 → #19238) has nothing in flight and seat 1 is vacant; #19319 (graded p3, the fenced-stamp MIXED scan) is a different region of the same file — region-parallel under 甲 if both move; git ls-remote --heads origin | grep issue-19312 → 0 heads; H17 index: 0 rows.

    Net-line budget (a PM figure, ⛔ not the dev's to raise): net ≤ +60 lines in post-stamped.mjs including the self-test cases. Label on the PR: skip-changeset.


    Generated by Claude Code

  4. os-steve commented on Sep 20, 2026

    @os-steve
    CollaboratorAuthor

    os-dev-report
    {
    "issue": 19312,
    "status": "done",
    "branch": "claude/issue-19312-read-back-declares-footer-relocation",
    "pr": "#19456",
    "head_sha": "8cc37b7a57b71f023fd445ea44e4f22adc7adcc7",
    "session": "session_017ETYWqMQD4qMtZzAGovWNi",
    "reported_at": "2026-09-20T23:26Z",
    "premise_still_valid": true,
    "summary": "The false NOT-STORED is one predicate. footerReAnchoring's collapse arm required strippedNewlines === 0 and read the raw sent text, so it forgave the platform collapsing the footer block's blank line ONLY when the act had sent no trailing newline of its own. A seat post's --body write sends both: the file ends in a newline AND its last paragraph stands a blank line above the rule, so three newlines go out before the rule and the platform stores the block's own two. That pair reached the classifier as 'mutated' and exit 4. The arm now reads the TRIMMED body, so ONE arm covers the collapse alone and the collapse over the strip; the comparison stays an exact === against a candidate BUILT from the sent bytes, the head is still compared literally, and no class was added. Reproduced offline on 57ceb9d before the fix and after it; reverse-verified by ablation. The card was filed unassigned by the triage seat and carried an assignee set by the PM at dispatch; nothing on the card or the PR was set by this run beyond the one label named in the dispatch.",
    "files_changed": [
    "scripts/pm/post-stamped.mjs"
    ],
    "net_lines": "+73 / -24 = net +49 in 1 file (PM budget: net at most +60, including the self-test cases)",
    "before_after": {
    "predicate": "BEFORE: if (strippedNewlines === 0 && sentText.endsWith(PLATFORM_COMMENT_FOOTER)) { head = sentText.slice(...); if (stored === head + COLLAPSED) return { strippedNewlines: 0, shape: 'collapsed' } } || AFTER: if (trimmed.endsWith(PLATFORM_COMMENT_FOOTER)) { head = trimmed.slice(...); if (stored === head + COLLAPSED) return { strippedNewlines, shape: 'collapsed' } }",
    "declared_class_footer_blank_collapsed": "BEFORE: 'the stored body is that body with the blank line immediately before the footer block's rule collapsed - one newline of the block's own separator gone, and no content byte touched' || AFTER: the same, with ', over any trailing newline(s) the platform does not keep' added after 'collapsed'",
    "rendered_line": "BEFORE: '... was COLLAPSED, so the one byte short is that separator newline and every CONTENT byte sent IS stored (sent N, stored N-1)' || AFTER: '... was COLLAPSED, over 1 stripped trailing newline(s), so the byte(s) short are that separator newline and the newline(s) the platform does not keep, and every CONTENT byte sent IS stored (sent N, stored N-2)'. The old sentence was FALSE of a body two bytes short, which is why it moved with the predicate.",
    "vocabulary": "unchanged - six classes, no class added, no class removed. identical / trailing-newline-stripped / footer-appended / footer-re-anchored / mutated are byte-for-byte untouched.",
    "exit_4_text": "unchanged - notStoredText still names the three measured footer shapes and still says 'something this act sent is not there'."
    },
    "tests": "REPRO (offline, no live write, on 57ceb9d): four shapes through classifyReadBack. sent = HEAD + newline + block, stored = HEAD + block -> footer-blank-collapsed, exit 0, ALREADY clean before the fix. sent = HEAD + newline + block + newline, stored = HEAD + block -> BEFORE: class=mutated, landed=false, offset=54, exit 4; AFTER: class=footer-blank-collapsed, strippedNewlines=1, landed=true, exit 0. sent = HEAD + block + newline, stored = HEAD + newline + block -> footer-re-anchored, exit 0, unchanged. sent = HEAD + two newlines + block + newline, stored = HEAD + block (a whitespace truncation in the CONTENT) -> mutated, exit 4, unchanged in both directions. || SELF-TEST: node scripts/pm/post-stamped.mjs --self-test :: exit 0 - 544 cases across 18 batteries; the same command on the merge base 57ceb9d (a temp copy, removed, tree verified clean) :: exit 0 - 530 cases. +14 cases, 0 removed. || ABLATION (reverse verification, through scripts/ablation-replace.mjs so the mutation is proven ON DISK, never by an exit code): re-adding the single predicate 'strippedNewlines === 0 &&' - anchor hit 1 time as declared, blob 46734b107fcd -> b24615b0ab9a - turns the self-test red in the predicted direction: 9 of 544 cases fail, 0 floor problems, and every failing case is one of the 14 added. Restore leg verified against HEAD's blob, not an exit code: blob after restore == blob at HEAD (46734b107fcd) and git diff HEAD empty; re-run after restore :: exit 0, 544 pass. The ablation helper carries its own EXIT/INT/TERM restore trap. No dist preflight: this script is run directly by node, it has no build output. || REPO-WIDE LINT, not narrowed: pnpm lint (eslint . --no-inline-config) :: exit 0 in 91s at head 8cc37b7. || PR body read-back after creation: sent 9172 bytes, stored 9171, first difference at byte 9171 - the trailing newline, the platform's own declared strip; every load-bearing substring verified present including the fenced diff, the four-axis section and the footer.",
    "gates": [
    "node scripts/check-ci-filter-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
    "node scripts/check-comment-mask-corpus.mjs :: exit 0",
    "node scripts/check-declaration-mirrors.mjs :: exit 0",
    "node scripts/check-declaration-mirrors.mjs --self-test :: exit 0",
    "node scripts/check-scripts-symbol-anchors.mjs :: exit 0",
    "node scripts/check-scripts-symbol-anchors.mjs --self-test :: exit 0",
    "node scripts/check-self-test-wired.mjs :: exit 0",
    "node scripts/check-self-test-wired.mjs --self-test :: exit 0",
    "node scripts/check-self-test-workflow-commands.mjs :: exit 0",
    "node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0",
    "node scripts/check-whole-set-label-write.mjs :: exit 0",
    "node scripts/check-whole-set-label-write.mjs --self-test :: exit 0",
    "node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0",
    "pnpm check:agent-test-spelling :: exit 0",
    "pnpm check:bash32-floor :: exit 0",
    "pnpm check:cli-command-ids :: exit 0",
    "pnpm check:cross-package-test-inputs :: exit 0",
    "pnpm check:driver-memory-census :: exit 0",
    "pnpm check:entry-guard :: exit 0",
    "pnpm check:gitlink-declared :: exit 0",
    "pnpm check:nul-bytes :: exit 0",
    "pnpm check:parse-guard :: exit 0",
    "pnpm check:pm-post-stamped :: exit 0",
    "pnpm check:pnpm-filter-targets :: exit 0",
    "pnpm check:ratchet-remedy-authority :: exit 0",
    "pnpm check:refd-timer-probe :: exit 0",
    "pnpm check:watch-hint-literal :: exit 0",
    "pnpm check:pm-dispatch-gates :: exit 0"
    ],
    "gates_reconciliation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths, change set derived by the script from the merge base 57ceb9d, three-dot) derived 30 families; every one run with its exit captured BEFORE any pipe; node scripts/pm/dispatch-gates.mjs --ran RANFILE (each line spelled 'command :: exit N') --repo objectstack-ai/objectstack :: exit 0 - '30 derived, 30 run, 0 NOT-MEASURED (a DERIVED zero - all 30 recorded an exit code and none of them is 3), 0 UNRUN'. Outside that derived total and NOT MEASURED here, as the tool itself prints: 54 artifact-roster families, 11 declared-wide-population families, 14 pending-changeset families, 1 path-scheduled CI job and the always-runs tail. CI owns those.",
    "governed": "node scripts/pm/check-governed-merges.mjs --pr 19456 :: exit 0 - 'NOT governed - ordinary queue landing applies to a PR with exactly this file list', 0 of 1 path hit the register (6 surfaces). size 97 changed lines, under the 5000 human-merge threshold. The seat lands it; this run did not flip ready and did not arm auto-merge.",
    "mcp_calls": "0 - no MCP GitHub tool was called, read or write. Every GitHub read was curl against the REST proxy; every write is listed under api_writes.",
    "api_writes": "5 total, all inside the dispatched budget: (1) git push - the empty branch, before the first edit, as the write-routing probe; (2) git push - the fix commit 8cc37b7; (3) POST /repos/objectstack-ai/objectstack/pulls - the draft PR #19456, body written ONCE and never PATCHed; (4) POST /repos//issues/19456/labels via node scripts/pm/label-write.mjs --add skip-changeset - HTTP 200, read back and MATCHES the target ('size/s', 'skip-changeset'); (5) POST /repos//issues/19312/comments via node scripts/pm/post-stamped.mjs --comment=19312 - this report, posted with the tool AS BUILT on this branch. No write to the card body, no label on the card, no ready flip, no auto-merge.",
    "pm_assumptions": {
    "1_classifier_and_declared_set": "CONFIRMED, with one refinement that matters to the fix. classifyReadBack returns 'identical' only on stored === sentText, and the declared set did not carry the pair. But the extra blank line ALONE was already clean before this change: sent = HEAD + newline + block against stored = HEAD + block classifies footer-blank-collapsed and exits 0 on 57ceb9d. The exit 4 needs BOTH acts - the extra blank line AND a trailing newline the platform strips - and the byte that refused it was the guard 'strippedNewlines === 0' on the collapse arm, documented in-source as a deliberate refusal of an unmeasured cell. That cell is what the seat measured 13 times today, so the fix is that guard's removal and the comment that justified it, not a new class.",
    "2_comment_path_classifier": "REFUTED AS STATED, and the refutation is the answer to the 'can it be SHARED' question. There is no --comment classifier to share from: since #18693 classifyReadBack takes no mode parameter (pinned in the self-test) and footerReAnchoring is the ONE predicate both acts are judged by. The --comment path would have exited 4 on the identical bytes. What made the seat's comment writes pass is their BYTES - a comment body ending in the block with one trailing newline is the footer-re-anchored shape, already declared - not a different reader. So no second parser was written and none was needed: the fix is one arm inside the one shared predicate, and the comment path inherits it.",
    "3_serial": "CONFIRMED at dispatch and still true at push: the only open PR touching scripts/pm/post-stamped.mjs is this one. NOTE, not a deviation: origin/main advanced under this run from 57ceb9d to 2951c0f (a docs/skills change to .claude/skills/spec-property-retirement/SKILL.md, #19446) - it does not touch this file. The merge base of this branch is unchanged at 57ceb9d and every derivation above is three-dot against it, so nothing in the gate accounting is stale; a plain 'git diff origin/main' now shows that sibling file as a reverse diff and is NOT this branch's change set.",
    "4_governed": "CONFIRMED - see the 'governed' field: NOT governed, printed by the tool on PR 19456's own file list."
    },
    "deviations": [
    "check:pm-dispatch-gates was run twice. The first run was detached as the dispatch directs, but the wrapper did not leave a readable exit file, so the run was repeated in the foreground with its exit captured before any pipe (16m17s wall, exit 0). The recorded reading is the second one; the first is discarded rather than claimed.",
    "pnpm lint was run repo-wide rather than narrowed. It is CI's population, not a family this card derives, but it finished in 91s inside the foreground ceiling, so it is reported as a measurement instead of a declared narrowing."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: the domain:skills seat, alongside PR #19456 - .claude/skills/pm-dispatch/references/platform-readings.md :360 records the tool's PRE-FIX behaviour as a governed fact and is stale the moment this lands. It is a governed Tier S path and outside this card's declared file surface, so it was not touched. Line :359 is unaffected and is in fact the reading this change acts on.",
    "noted, not filed: the three newlines reach the platform because the body FILE carries the extra blank line, not because the tool adds one. Whether a seat's body composition should normalise its own tail before the write is a different question from whether the read-back should name what the platform does with it, and the instrument is the only thing this card names. Carrier if it is ever wanted: the same seat's body-composition step; no PR and no person is currently blocked on it, so it is a note and not a card.",
    "noted, not filed: carrier: none - the collapse is forgiven for exactly one newline of the block's own separator. A body sending TWO extra blank lines before the rule still exits 4 (pinned as a control), because nobody has measured what the platform stores for it. That is a deliberately unmeasured cell, not a gap: it becomes a reading the day a write produces one."
    ]
    }


    Generated by Claude Code

  5. os-steve commented on Sep 20, 2026

    @os-steve
    CollaboratorAuthor

    ACCEPT — domain:skills#2 (session_017ETYWqMQD4qMtZzAGovWNi, seat post #19287) at 2026-09-20T23:29Z; PR #19456 (draft, head 8cc37b7, not governed — the seat lands it through the queue on green).

    Checklist, verified on GitHub and on the tree (⛔ not on the report's self-description):

    • Shape: draft · base main · first line Fixes #19312 · Clause-②: no line-initial · labels skip-changeset + size/s.
    • Scope: 1 file, scripts/pm/post-stamped.mjs +73 / −24 (net +49, within ≤ +60) — one arm of the shared read-back predicate reads the trimmed body; the class text, the rendered line, docblocks, 14 self-test cases; no class added; exit-4 text unchanged; the keyed-line reader and the stamp contract untouched. check-governed-merges --pr 19456: NOT governed.
    • Contract: ## Contract review PASS on the PR at this head (5753524133, in-seat at CONTRACT_REVIEW_TIER); --pair 19456 after the record: exit 0.
    • Tests, seat-run on the head: --self-test exit 0, 544 cases; the seat's offline probes (today's pair lands; a content byte differing, two extra blank lines and a relocated footer still exit 4); merge-tree vs origin/main 8e368dc clean. The dev's ablation (9 of 544 red on the restored guard), the three filed byte counts pinned, the union 30 / 30 with --ran reconciled, lint repo-wide clean, check:pm-dispatch-gates exit 0.
    • Gates / CI on 8cc37b7 at 2026-09-20T23:28Z: 34 check runs, 2 still running, 0 red — landing waits for them.
    • Report: mcp_calls 0 · api_writes 5 (+2 pushes). Two assumptions refined by measurement (the collapse alone was already clean; one shared predicate, no second parser), two confirmed; deviations read and accepted. The stale platform-readings.md :360 row goes to the family PR (removed_from_merge_queue fires on a SUCCESSFUL merge too — 3 of 5 on this board today were merges, so the event is not a failure signal #19343 entry) — the seat's carrier; two notes stay notes.

    Landing: not governed ⇒ ready + auto-merge once every check is green; pm:dispatched comes off at MERGED (the Fixes line closes this card). The seat's next --body write reads back landed instead of 「NOT STORED」.


    Generated by Claude Code

  6. added a commit that references this issue on Sep 28, 2026
    3d6f0aa
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions