Skip to content

fix(pm): post-stamped refuses a token it cannot render - #18286

Merged
claude[bot] merged 2 commits into
mainfrom
claude/issue-18284-post-stamped-refuses-malformed-tokens
Sep 15, 2026
Merged

claude[bot] merged 2 commits into
mainfrom
claude/issue-18284-post-stamped-refuses-malformed-tokens

Conversation

@claude

@claude claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #18284

The defect

scripts/pm/post-stamped.mjs recognised its two tokens with QUOTED_TOKEN_RE
({{WAS:([^{}]*)}}) and caught the rest with a post-substitution leftover scan
over ANY_TOKEN_RE ({{[^{}]*}}). Both spell their payload [^{}]*, so a
token-shaped opener whose payload carries a brace, or one that never closes
at all, matched neither: it was not substituted, because nothing recognised it,
and it was not refused, because the leftover scan could not see it. It was
copied to the board verbatim.

The filed reading is the seat's own artefact: a quoted slot was filled from a
shell variable, the read failed, and the variable held a multi-line Node dump —
which carries braces. Reproduced here on the unpatched tool at fe0ae5c1:

$ node scripts/pm/post-stamped.mjs --dry-run --comment=18284 --file=dump.md ; echo "EXIT=$?"
post-stamped: DRY RUN — nothing was written. 0 token(s) substituted with
`2026-09-15T08:18Z`, 0 quoted stamp(s) rendered verbatim. Target would be
objectstack-ai/objectstack#18284 (comment).
EXIT=0

…and its stdout was the body it would have written, opener, dump and closer
intact. Same fixture after the patch, EXIT=2, nothing on stdout:

post-stamped: REFUSED — 1 double-brace opener(s) in this body are not tokens this tool
  can render. Nothing was written.
  1. [brace-in-payload] a brace inside the payload, so no token ends here — `{{WAS:[eval]:1\nJSON.parse(process.env.LANDED).mergedAt\n     …`

What changed

The check moved ahead of substitution and changed what it walks: not the
tokens a regex happens to match, but every {{ in the body, positionally —
because the defect is precisely an opener no regex in the file matches, so a
scan built out of those same regexes would walk past it again. Each opener must
be {{NOW}} or a {{WAS:…}} whose payload is brace-free and closed. Four
shapes are refused and named: unknown-token-name, unclosed-opener,
brace-in-payload, nested-opener. The refusal prints the offending span
clipped to 60 bytes with control characters escaped, so a payload that arrived
from a shell read gone wrong stays readable; the same renderer is now applied to
the existing quoted-not-a-stamp detail, which used to inline a raw multi-line
dump into a numbered row.

The scan judges shape and stops. Whether an admitted payload is an instant,
and one the clock has reached, stays with stampRefusals — two places deciding
"what is a quoted stamp" is two spellings of one decision. What the scan gives
that rule is the thing it could not previously assume: that maskQuotedStamps
consumed every opener, so the bare-stamp scan underneath is reading prose and
not the inside of a token nobody could parse. That ordering is pinned: an
unclosed opener on the opening line is reported as the opener it is, not as the
positional refusal the unmasked payload would otherwise raise.

The post-substitution leftover check is gone rather than kept beside the
scan: after the scan every opener is consumed and the two substituted stamps
carry no braces, so it could never fire, and a check that cannot fire is a check
nobody maintains. Its kind (unknown-token), its exit code and the two
sentences callers read are preserved on the new refusal.

No accepted form is narrowed. The forms the tool took before — {{NOW}}
exactly; {{WAS:…}} with a payload that is one protocolStamps match and
nothing else, minute- or seconds-grained, surrounding whitespace tolerated —
all still clear the scan, each pinned by a case. A bare stamp in prose is still
not an opener's business, and a token inside backticks is still substituted:
there has never been an escape form and the scan does not invent one.

--dry-run and the live write take the same refusal, because both run through
renderBody. The DRY RUN line, the read-back delta note, the --ack-through
rule and the footer policy are untouched.

Verification

  • Self-test 117 → 152 cases, 8 → 9 batteries; SELF_TEST_BATTERY_FLOOR
    raised 8 → 9 so a deleted battery is still caught. New battery: the opener
    scan: every {{ is a token this tool renders, or the body is refused
    (35
    cases) — the filed repro, a multi-line payload, an unknown token name, an
    unclosed opener, nested and doubled openers, the valid-pair control, the
    no-narrowing controls, the no-escape-hatch controls, and the span renderer.
  • Reverse verification by ablation from the committed state: the base blob was
    checked out over the file, its presence proved by git hash-object against
    fe0ae5c1:scripts/pm/post-stamped.mjs (and grep -c unrecognisedOpeners → 0),
    the fixture run, then git checkout HEAD -- restored and git diff HEAD
    proved empty. Both readings are quoted above.
  • All 32 commands from node scripts/pm/dispatch-gates.mjs --commands exit 0,
    plus node scripts/pm/post-stamped.mjs --self-test,
    node scripts/check-self-test-wired.mjs and ESLint on the file.
  • No changeset: nothing published moves. scripts/pm/** is not in any package's
    files[].

Acceptance notes

Noted, not filed — nothing in this PR's path touches them and no queued card
covers the file:

  • The tool's usage text and header describe the quoted payload as
    YYYY-MM-DDThh:mmZ, while the shape actually accepted is
    PROTOCOL_STAMP_RE, whose seconds group is optional. The seconds grain is
    deliberate and pinned by an existing case; only the prose is narrower than the
    contract. Documentation nit, left alone rather than widened in a fix PR.

One reproducible defect found while reading the payload rules is reported in the
terminal report for the seat to file, with dedupe words: a quoted payload that
matches the stamp shape but names an impossible date is accepted and rendered
verbatim, because stampSpan returns null for it and stampIsFuture reads
null as "not future". It is a different failure mode from this card's —
rendered-but-unvalidated rather than neither-rendered-nor-refused — so it is not
fixed here.


Generated by Claude Code

Every double-brace opener in a body must now be `{{NOW}}` or a `{{WAS:...}}`
whose payload is brace-free and closed; anything else is REFUSED with the
offending span, on `--dry-run` as well as on a live write.

The UNKNOWN refusal used to run AFTER substitution, over whatever the two
payload regexes had left behind. Both spell their payload `[^{}]*`, so a
token-shaped opener carrying a BRACE, or one that never closes, matched
neither: not substituted, because nothing recognised it, and not refused,
because the leftover scan could not see it. A seat filled a quoted slot from
a shell variable, the read failed and the variable held a multi-line Node
dump; `--dry-run` reported "0 token(s) substituted, 0 quoted stamp(s)
rendered verbatim" and the stored comment carried the opener, the dump and
the closer verbatim.

So the check moved ahead of substitution and walks every `{{` positionally
rather than whatever a regex matches -- the defect is precisely an opener no
regex here matches. Four shapes are named: an unknown token NAME, an opener
with no closer, a brace inside the payload, a second opener before the first
closes. Spans print clipped to 60 bytes with control characters escaped, so
a payload that arrived from a shell read gone wrong stays readable; the same
renderer is applied to the existing quoted-shape refusal.

The scan judges shape and stops -- whether an admitted payload is an instant
the clock has reached stays with `stampRefusals`, and what the scan gives
that rule is the thing it could not previously assume: that masking consumed
every opener. The post-substitution leftover check is gone rather than kept
beside it; after the scan it could never fire.

No accepted form is narrowed: the whitespace-tolerant declaration, the
seconds grain, a bare stamp in prose and a token inside backticks all read
exactly as before, each pinned. Self-test 117 -> 152 cases, 8 -> 9 batteries.

Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr
Co-authored-by: Claude <noreply@anthropic.com>
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9ec6e4051cca47873f754dd9d5e23693dbfabd62

Reviewing seat: domain:skills, in-seat review at the contract-review tier — get_session read at 2026-09-15T07:15Z: configured_model = session_context.model = last_served_model, the constant's tier, no fallback notice this round. Card #18284, claim 5676868959, report 5677361795 (written by the patched tool itself — the act-clock token in it was substituted by the code under review); Clause-②: no on the claim; default-tier build, seat review at tier.

① Derived judgments

  • Accept set / public surface: none moves — scripts/pm/post-stamped.mjs (+241/−19) against merge-base 9ed3f167 (the branch merged origin/main before the PR opened; the three-dot diff is one file); nothing published; skip-changeset read back (size/m). Non-governed ⇒ in-seat landing.
  • The change: an opener scan — every double-brace opener in a body must be a token the tool renders (the act-clock token, or the quoted-stamp token with a payload in the accepted shape and not later than the clock); anything else is REFUSED on --dry-run and on a live write with the reason and the clipped, escaped span, exit 2, nothing rendered. The seat reproduced the defect and the fix on the same fixture — a quoted slot holding a two-line Node dump WITH braces inside it (the shape of the filed artefact; the braces are what hid the payload from both payload regexes): the tool on origin/main prints its DRY RUN line with the opener intact (「0 quoted stamp(s) rendered verbatim」 — passed through), the tool on this head prints post-stamped: REFUSED — 1 double-brace opener(s) in this body are not tokens this tool can render. Also seat-run on the head: an unknown token name REFUSED; an opener with no closer REFUSED; the control (one act-clock token + one valid quoted stamp) renders. Self-test 117 cases / 8 batteries on main → 152 / 9 on the head, the new battery floored at 35 and the battery floor raised to 9 so deleting the battery is caught; the six cases the card asked for are present, (f) pinned as 「the tool has no entity form — an entity spelling is prose」 rather than a control.
  • Gates 32 derived (the seat re-derived 32 on the head) / all run, exits 0 by --ran (a first --ran with bare lines was refused and rewritten — both readings reported); the battery 1730 cases pass, 443.9 s detached, exit captured to a file by the detached shell. Seat re-run on a detached worktree of the head: self-test 152, ESLint 0/0, check-self-test-wired OK, check-watch-hint-literal OK. Reverse verification by the dev: ablation from the committed state with hash proofs (base blob e90e4a1b → the scan absent, the fixture passes; HEAD blob 26e7f6ce restored, git diff HEAD empty).

② Semver level

Not applicable — nothing published moves; skip-changeset measured.

③ Boundary flags

open_questions: none. Deviations, each answered: (1) origin/main merged before the PR — accepted, the derivation had gone stale on check-skill-line-ratchet.mjs; the diff stays one file. (2) the first battery killed by recorded PID after the merge decision, the second instrumented run reported — accepted. (3) the first --ran refused for bare lines, rewritten — accepted, both readings kept. (4) a four_axis field and the standing template's fields added to the report — accepted, the seat's field list was the narrower one. (5) the PR-body footer follows AGENTS.md's session-URL form over the harness reminder — accepted, the repo file outranks the reminder. Out of scope, routed: a quoted stamp in the digit shape naming an impossible instant (month 13, day 45, 99:99) is rendered verbatim because the direction rule reads a NaN span as 「not future」 — a different failure mode, filed as #18289 (serial behind this PR on the same file); the header prose says minute grain while the contract also accepts seconds — rides #18289. Landing: in-seat, every check on this head green (24 success / 11 skipped at 2026-09-15T08:47Z) — ready through the CCR route, auto-merge SQUASH, the enqueue event read, provenance below.

Implemented-by: claude/issue-18284-post-stamped-refuses-malformed-tokens
Reviewed-by: session_01HZfg2AwVX191qCizp88gQr

VERDICT: PASS


Generated by Claude Code

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Landing provenance — skills seat, session session_01HZfg2AwVX191qCizp88gQr, 2026-09-15T08:49Z. Non-governed landing (scripts/pm/**): review of record 5677397822 PASS on head 9ec6e4051cca47873f754dd9d5e23693dbfabd62, ACCEPT on #18284; every check on that head read green at 2026-09-15T08:49Z (24 success / 11 skipped / running none / failed none). The seat flipped the PR ready through POST …/pulls/18286/ccr/ready_for_review and armed auto-merge SQUASH through PUT …/pulls/18286/ccr/auto_merge; the added_to_merge_queue event reads 2026-09-15T08:49Z. Queue-ref reading taken right after the enqueue under the register's spelling refs/heads/gh-readonly-queue/*: 0 ref(s) naming this PR (the seat's earlier reads used another spelling; the timeline event stays the reading of record). Landing is confirmed by the two readings (merged_at + git log origin/main carrying (#18286) single-parent) at the next wake; then #18284's residue is stripped and the landing record posted, and #18289 (same file) becomes dispatchable.


Generated by Claude Code

Merged via the queue into main with commit 499f7f9 Sep 15, 2026
37 checks passed
@claude
claude Bot deleted the claude/issue-18284-post-stamped-refuses-malformed-tokens branch September 15, 2026 09:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant