Skip to content

finding(spec): the expression dialect table lists notification subjects/bodies as template slots, but NotifyConfigSchema.title is z.string() and refuses the tmpl envelope #22054

Description

@objectstack-fleet

Found by the repo:objectos seat (#9831) while staging objectos#316, the docs code-sample gate, on 2026-10-07. Measured on the published @objectstack/spec 17.7.0 and read at origin/main. Class b: the contract text and the schema disagree.

What disagrees

  • packages/spec/src/shared/expression.zod.ts:27. The dialect table names the template dialect's slots as "notification subjects/bodies, titleFormat, prompt templates". A template-typed slot accepts either a bare string or the { dialect: 'template', source } envelope (TemplateExpressionInputSchema, :36, and the guidance at :292-295).
  • packages/spec/src/automation/io-node-config.zod.ts:192. NotifyConfigSchema.title is z.string().optional(), so only a bare string parses.
  • Effect. A notify node written with the tmpl helper is refused, by defineFlow and by NotifyConfigSchema alike: title: expected string, received object. Example: title: tmpl`[{{record.priority}}] {{record.subject}}`. The bare-string spelling of the same text parses.

Why it matters

Docs and authors who follow the dialect table write the envelope form for notifications and get a schema error. objectos's reference/cel.mdx now follows the parse, using bare strings for notify titles, after objectos#316 / PR #321.

Possible directions (the owner decides)

  • Type NotifyConfigSchema.title, and any body or subject field, as TemplateExpressionInputSchema, so both spellings parse. Or:
  • Correct the dialect table row so it no longer promises the envelope for notification subjects and bodies.

Either way, a test pinning the decision closes it.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: ② the capabilities an end user meets in the app — automation notifications | 缺项 | P3

    Triage: first grade, bug · priority:p3 · domain:spec · area:workflow · pm:queue (finding and needs-triage removed). The field follows the dialect table: a notify title accepts the template input

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-07T04:58Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: lands in packages/spec/src/automation/io-node-config.zod.ts (NotifyConfigSchema.title, :192), with the executor half in packages/services/service-automation/src/builtin/notify-node.ts (cross-lane domain:services) ⇒ domain:spec; rationale: the declaration is the outlier against the spec's own dialect table.

    • Verified on main at this write:
      • the dialect table names notification subjects and bodies as template slots;
      • NotifyConfigSchema.title is z.string().optional();
      • the executor already treats title as a template: notify-node.ts:259 runs it through interpolate(cfg.title …).
    • Direction: the first of the card's two. Type title, and any notify body or subject field the executor interpolates, as TemplateExpressionInputSchema, so the bare string and the tmpl envelope both parse, as every other template slot does.
      • The executor reads the envelope's source. A bare string is unchanged.
      • The changeset is a widening (Clause-②: yes, minor).
      • Pins: the envelope and the bare string both parse and both render the same text; a non-string, non-envelope value is still refused.
    • Not the second direction: narrowing the table's promise would leave the one notify slot unlike every other template slot, while the runtime already interpolates it.
    • Why p3: an author gets a loud schema error and a working spelling exists, so there is no silent harm.
  2. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 8 (this card, as triage graded and directed it in 6031254558: the first of the card's two directions) · 2026-10-07T05:43Z
    Session: session_01GV6oYwgc1kWiUCb1YaprQ7
    Account: os-warren (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-22054-notify-title-template-input
    Worktree: objectstack-issue-22054
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549)
    File surface (at origin/main d5a14dd5; stop on breach and explain in the report):


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 22054,
      "status": "done",
      "branch": "claude/issue-22054-notify-title-template-input",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/22063",
      "session": "session_01GV6oYwgc1kWiUCb1YaprQ7. This run is a subagent, so this is the PM's harness-stamped id, the same one the commits' Claude-Session trailer carries.",
      "premise_still_valid": true,
      "summary": "NotifyConfigSchema.title and .message are now TemplateExpressionInputSchema.optional(), the input every other template slot uses, so the bare string and the tmpl envelope both parse. The parse normalizes both spellings to one value, {dialect:'template', source}. The notify executor reads cfg.title?.source and cfg.message?.source and interpolates them as before. Both spellings deliver the same payload.title and payload.body, and a bare string renders exactly what it rendered before. One rule is added, for this slot only: a template envelope on either key must carry a non-blank source. The shared envelope arm admits an ast-only envelope or a whitespace source. The executor renders source only, so without the rule such a title would fail every run and such a message would go out empty. Every published sentence about the two keys is now true. The 'sent verbatim' wording is gone from the .describe() texts, the docblock, the conflict refusal and the descriptor, and each now names the renderer's single-brace {token} placeholder. Premise: the core holds, because the schema disagreed with the dialect table. One detail is false on main at d5a14dd5: defineFlow and FlowSchema.safeParse ACCEPT the envelope (the builtin config arm is presence-only), and registerFlow registers it. The refusal came only at execute time, from parseNodeConfig: 'config.title: Invalid input: expected string, received object'. CI on head 01ef8368e5 at report time: 14 checks completed with 0 failures, 18 in_progress.",
      "tests": "All runs were on the branch. packages/spec is byte-identical from ed7166a5e2 to the final commit 01ef8368e5; the only later change is one line in notify-template-slots.test.ts. Package runs: spec build (JS and DTS) exit 0. spec check:generated exit 0, all 15 artifacts up to date; the only regenerated artifact is content/docs/references/automation/io-node-config.mdx. spec test exit 0 (620 files, 18497 passed, 1 todo, run at ed7166a5e2). spec typecheck exit 0, check:test-typecheck ledger held. service-automation test exit 0 (174 files, 2116 passed, at 01ef8368e5). service-automation typecheck exit 0. lint test exit 0 (120 files, 5638 passed, at 01ef8368e5). dispatch-gates --commands with no paths derived 111 families at 01ef8368e5. All were run, and --ran reconciled them: 110 run, 1 NOT MEASURED, 0 unrun. NOT MEASURED: check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET, it needs every package's dist), so it is declared to CI. check:skill-examples first exited 3 (no client dist), then exited 0 after building @objectstack/client and @objectstack/client-react (262 examples). ESLint, narrowed and run at 01ef8368e5: population read from eslint.config.mjs (files **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}); --format json counted 4 files, 0 errors, 0 warnings; invariance: no parserOptions.project or projectService anywhere in the config, so no type-aware linting can move an untouched file's verdict. The repo-wide lint is left to CI. Ablations, each with an on-disk anchor count and a blob restore verified equal to HEAD with git diff HEAD empty: (A) executor read reverted to interpolate(cfg.title ?? '') with the new spec: 3 of 4 executor pins red, and the delivered title was {\"dialect\":\"template\",\"source\":\"[won] Deal Acme\"}, for the bare string too. (B) title reverted to z.string(): 10 spec pins red, including collateral, because the new source rule reads .source off a plain string. (base) the new spec pins run against the base schema file restored from d5a14dd5 under a trap: 7 red (6 new pins and the updated 'accepts every declared key'), 27 green. (C) only the new source rule disabled: exactly 1 pin red. Reverse type check: cfg.title?.trim() in the executor turns tsc red with TS2339 on the envelope union, which proves service-automation reads the rebuilt .d.ts.",
      "mcp_calls": "0. No MCP GitHub tool was called.",
      "api_writes": "3, each sent as one POST /repos/objectstack-ai/objectstack/dispatches from this container and executed by the fleet-write relay as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls: #22063, draft, body read back byte-identical (10860 bytes); (2) assign, POST /repos/objectstack-ai/objectstack/issues/22063/assignees: os-warren, read back matched; (3) comment, POST /repos/objectstack-ai/objectstack/issues/22054/comments: this report. git push is not a REST write. No label was written, skip-changeset included.",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: c · reach: named producer, and exception: release-text. The card's own example from the objectos#316 docs sample, tmpl`[{{record.priority}}] {{record.subject}}`, now parses on a notify title and renders '[{P1}] {Server down}'. The notify renderer is the flow's interpolate(), which reads single-brace {token}: the inner {var} resolves and the outer braces stay. That was already true for bare strings. Evidence: four shared texts outside this card's surface steer authors to {{var}} on every template slot, notify now included. (1) TYPED_EXPRESSION_SOURCE_REQUIRED.template and (2) TYPED_EXPRESSION_DIALECT_ONLY.template (packages/spec/src/shared/expression.zod.ts) both say: write '{{record.name}}'. These texts are now the refusals a blank or wrong-typed notify title or message receives. (3) The tmpl docblock: 'Variable scope is the same as CEL ({{record.x}} ...)'. (4) The TemplateExpressionInputSchema docblock: 'write {{var}} unless the slot's renderer is known to normalize'. The notify .describe() texts in this PR now state the {token} spelling. Family: template-slot placeholder spelling per renderer (titleFormat normalizes both spellings, notify reads single-brace only, messaging and email read double-brace only). Dedupe words: template slot placeholder spelling; TYPED_EXPRESSION_SOURCE_REQUIRED {{record.name}} prescription; tmpl double brace notify; notify title stray braces",
        "carrier: the objectui flow inspector owner · noted, not filed. objectui packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeConfigField.tsx renders text and textarea controls with String(value). A code-authored notify title or message that is now a legal template envelope would therefore display as [object Object] in Studio. This is a read-only inference and was not reproduced in a browser. The same static notify form (flow-node-config.ts) still offers the pre-17 'url' key, which the flow-node-notify-config-aliases conversion rewrites at load until it retires at 18. Unmeasured: whether the server-descriptor form supersedes the static form online.",
        "carrier: none · noted, not filed (a family-level read-only inference). TemplateExpressionInputSchema and CronExpressionInputSchema admit an ast-only envelope, and the one engine each slot reaches reads only source. Examples: formula templateEngine refuses 'template Expression.source required', and metadata-protocol's titleFormat read is obj.titleFormat.source || obj.titleFormat. This PR closes the gap for notify only, with its own rule. It belongs in the PR's Acceptance notes. The PR body is write-once, so the seat may append this line there."
      ],
      "gates": {
        "derived_families": "111 at 01ef8368e5: exit 0 x110, exit 3 x1 (check:dual-build-cjs-loads, NOT MEASURED). check:skill-examples exited 3, then 0 after a client build. The --ran reconciliation exited 0.",
        "package_runs": {
          "pnpm --filter @objectstack/spec build": 0,
          "pnpm --filter @objectstack/spec check:generated": 0,
          "pnpm --filter @objectstack/spec check:api-surface (inside check:generated, and also as a derived family)": 0,
          "pnpm --filter @objectstack/spec test": 0,
          "pnpm --filter @objectstack/spec typecheck": 0,
          "pnpm --filter @objectstack/service-automation test": 0,
          "pnpm --filter @objectstack/service-automation typecheck": 0,
          "pnpm --filter @objectstack/lint test": 0,
          "node scripts/pm/dispatch-gates.mjs --ran ran.list": 0
        },
        "ci": "in_progress at report time: 14 completed with 0 failures, 18 in progress, on head 01ef8368e5."
      },
      "deviations": [
        "Added one notify-only refinement inside the existing superRefine: a title or message envelope must carry a non-blank source. Partition 3 suggested keeping the rules exactly. The existing three rules are unchanged; the new one only refuses shapes that never parsed before (envelopes), so the change is still a pure widening against the base.",
        "Changed the NOTIFY_KEY_GUIDANCE subject and body sentences from 'with DIFFERENT text' to 'with a DIFFERENT value'. The reason is H3: a bare subject and an envelope title with the same text are different values, and the conversion keeps both. The conversion itself does not move.",
        "packages/spec/src/shared/expression.zod.ts is NOT edited, because it is outside the declared surface, even though its {{var}} prescriptions now apply to notify too. Reported as finding 1.",
        "Blank bare strings ('' and whitespace) are now refused at title and message by the shared non-blank rule. Before, a blank title parsed and then failed every run, and a blank message sent an empty body. Measured zero such values in the monorepo; the objectui inspector deletes a cleared key (setAtPath). The Clause-② arm stays widening, as ruled. This is flagged for the clause-② contract review.",
        "origin/main was not merged: it gained 2 commits (spec/ui and metadata-protocol) with zero file overlap with this diff.",
        "Commit trailers and the PR footer follow AGENTS.md: the model-free Claude-Session and Co-authored-by pair, and the session-URL footer. The harness's attribution reminder asked for a model-named Co-Authored-By and a different footer; AGENTS.md takes precedence."
      ],
      "files_changed": [
        ".changeset/22054-notify-title-template-input.md (@objectstack/spec minor, @objectstack/service-automation patch)",
        "content/docs/references/automation/io-node-config.mdx (generated, gen:docs)",
        "packages/services/service-automation/src/builtin/notify-node.ts (declared cross-lane file)",
        "packages/services/service-automation/src/builtin/notify-template-slots.test.ts (new; kept apart from notify-node.test.ts because PR #21974 edits that file)",
        "packages/spec/src/automation/io-node-config.test.ts",
        "packages/spec/src/automation/io-node-config.zod.ts"
      ],
      "h1_reading": {
        "before": "Measured with record {priority:'P1', subject:'Server down'}. interpolate(envelope) walks the object key by key and returns {\"dialect\":\"template\",\"source\":\"[P1] Server down\"}; stringifyForTemplate then serializes it as JSON. Through the whole node on base, the envelope never got that far: registerFlow accepted it, and every run was refused at parseNodeConfig (config.title: expected string, received object).",
        "after_without_executor_edit": "Measured as ablation A. With the new spec the parse hands the executor an envelope for BOTH spellings, so every notify title, bare strings included, would deliver {\"dialect\":\"template\",\"source\":\"[won] Deal Acme\"}. The executor edit is therefore required.",
        "after": "The executor reads source. A bare string and an envelope both deliver '[won] Deal Acme' and 'Congrats on Acme'. The bare-string render is unchanged.",
        "h2": "No shared unwrap helper exists. engine.ts:12006 inlines typeof === 'string' ? expression : expression.source; lint has local sourceOf and expressionText; metadata-protocol inlines titleFormat.source || titleFormat. None is needed here: the parsed slot is always the envelope, so the executor reads .source off a typed value and no third helper was written."
      },
      "pin_table": [
        {
          "value": "bare '[{stage}] Deal {dealName}' at title or message",
          "parse": "ok, normalized to {dialect:'template', source}",
          "render": "[won] Deal Acme"
        },
        {
          "value": "tmpl`...` or {dialect:'template', source} with the same text",
          "parse": "ok, the same value",
          "render": "[won] Deal Acme (same text)"
        },
        {
          "value": "42, true, ['a'], {source}, {dialect:'cel', source}",
          "parse": "refused, one issue at the key: code invalid_union, message equal to TYPED_EXPRESSION_DIALECT_ONLY.template",
          "render": "the node is refused at the contract parse ('does not satisfy the notify contract', config.title) and nothing is emitted"
        },
        {
          "value": "'' or '   '",
          "parse": "refused: code invalid_union, message equal to TYPED_EXPRESSION_SOURCE_REQUIRED.template",
          "render": "n/a"
        },
        {
          "value": "{dialect:'template', ast:{...}} or {dialect:'template', source:'   '}",
          "parse": "refused: code custom at the key, message naming `source`",
          "render": "n/a"
        },
        {
          "value": "an envelope title together with template",
          "parse": "refused: code custom at ['template'] (mutual exclusion unchanged)",
          "render": "n/a"
        }
      ],
      "describe_changes": [
        {
          "where": "NotifyConfigSchema.title .describe()",
          "before": "Notification title, sent to every recipient verbatim (not localizable — use `template` for per-locale content). Either this or `template` is required; the two are mutually exclusive.",
          "after": "Notification title — a template: a bare string, or a `{ dialect: 'template', source }` envelope (the `tmpl` helper) carrying the same text. It is interpolated per run with the flow's single-brace `{token}` placeholders (`{record.name}`); a `{{var}}` is not a placeholder here — its inner `{var}` resolves and the outer braces stay in the text. One text for every recipient (not localizable — use `template` for per-locale content). Either this or `template` is required; the two are mutually exclusive."
        },
        {
          "where": "NotifyConfigSchema.message .describe()",
          "before": "Notification body, sent verbatim like `title` (not localizable). Only valid with inline `title`, never with `template`.",
          "after": "Notification body — the same template input as `title` (a bare string or a `{ dialect: 'template', source }` envelope), interpolated per run with single-brace `{token}` placeholders; not localizable. Only valid with inline `title`, never with `template`."
        },
        {
          "where": "NotifyConfigSchema docblock, localization bullet",
          "before": "Inline `title`/`message` are the NON-localizable path — raw strings sent to every recipient verbatim.",
          "after": "Inline `title`/`message` are the NON-localizable path — one text for every recipient, interpolated per run (below), never translated."
        },
        {
          "where": "NotifyConfigSchema docblock, new bullet",
          "before": "(none)",
          "after": "title and message are TEMPLATE slots typed with TemplateExpressionInputSchema: a bare string or a { dialect: 'template', source } envelope. The parse normalizes the bare string to the envelope, the executor interpolates source, and both spellings render the same notification. The renderer is interpolate(), so placeholders are single-brace {token}; a {{var}} keeps its outer braces. An envelope must carry a non-blank source."
        },
        {
          "where": "module docblock (published in the generated reference page)",
          "before": "`notify` parses the RAW stored config — its slots are string-typed, so `{token}` templates pass",
          "after": "`notify` parses the RAW stored config — its slots are string-typed or template-typed, so `{token}` templates pass"
        },
        {
          "where": "title and message field JSDoc",
          "before": "Inline notification title — the NON-localizable content path. / Notification body (inline path only).",
          "after": "... the NON-localizable content path, and a template slot (see the docblock above). / Notification body (inline path only) — the same template input as `title`."
        },
        {
          "where": "template-conflict refusal (superRefine)",
          "before": "or inline `title` + `message` (sent verbatim, not localizable)",
          "after": "or inline `title` + `message` (one text for every recipient, not localizable)"
        },
        {
          "where": "NOTIFY_KEY_GUIDANCE.subject / .body",
          "before": "If `title` is also present with DIFFERENT text ... / If `message` is also present with DIFFERENT text ...",
          "after": "If `title` is also present with a DIFFERENT value ... / If `message` is also present with a DIFFERENT value ..."
        },
        {
          "where": "new refusal (notifyTemplateSourceRequired, not exported)",
          "before": "(none)",
          "after": "`KEY` is a template envelope with no non-blank `source`. The notify executor renders `source` — interpolating its `{token}` placeholders per run — and has nothing to render from `ast` alone or from a blank `source`, so (title) every run that reached this node would fail with no title to send / (message) the notification would go out with an empty body. Put the text in `source` (`{ dialect: 'template', source: 'Deal {record.name} won' }`), or write it as a bare string."
        },
        {
          "where": "notify descriptor configSchema.title.description (notify-node.ts)",
          "before": "Notification title, sent verbatim (not localizable — use template for per-locale content). Either this or template is required; mutually exclusive with template.",
          "after": "Notification title, interpolated per run with {token} placeholders (e.g. {record.name}; a {{var}} keeps its outer braces). Not localizable — use template for per-locale content. Either this or template is required; mutually exclusive with template."
        },
        {
          "where": "notify descriptor configSchema.message.description (notify-node.ts)",
          "before": "Notification body, sent verbatim (not localizable). Only valid with inline title, never with template.",
          "after": "Notification body, interpolated per run with {token} placeholders like title. Not localizable. Only valid with inline title, never with template."
        }
      ],
      "hypotheses": {
        "H1": "Confirmed and measured. interpolate stringified the envelope, so the executor edit was needed. Readings are in h1_reading.",
        "H2": "No shared unwrap helper exists, and none was needed: the parsed slot is always an envelope.",
        "H3": "The comparison still holds and the conversion does not move. Measured: a bare subject with an envelope title of the same text keeps both keys, and the strict gate refuses subject (unrecognized_keys) with its guidance. A subject alone, bare or envelope, renames onto title and parses. The population is empty by construction, because an envelope title never parsed before this change. deepEqualAuthored stays the one shared equality.",
        "H4": "Confirmed: 'sent verbatim' was already false, because the executor interpolates. Every sentence is corrected; the list is in describe_changes.",
        "H5": "Nothing new sees these keys. They are not on FLOW_NODE_EXPRESSION_PATHS, so the lint validateExpression walk and the registration expression pass skip them. validate-flow-template-paths recurses into objects and reads an envelope's source as it read the bare string. authorable-surface, liveness, the strictness ledger and api-surface record keys or exports, and none moved; all are green."
      },
      "cleanup": "Removed node_modules from the worktree and removed the worktree itself (git worktree remove, no --force); the branch is on origin at 01ef8368e5. No server, background job or monitor was left running: the one background gate battery exited and was waited on in the foreground."
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim revision (file surface) · domain:spec seat 2 · session_01GV6oYwgc1kWiUCb1YaprQ7 · 2026-10-07T07:08Z · amends claim 6031783397. ⛔ Not a new claim.

    Added to the file surface: packages/qa/dogfood/test/expression-conformance.ledger.ts, one row that covers automation/io-node-config.zod.ts:NotifyConfigSchema.title and …message.

    • Why: typing the two keys with TemplateExpressionInputSchema makes them template expression surfaces. The ADR-0060 ratchet (expression-conformance.test.ts:414) refuses an unclassified surface, which is why Dogfood Regression Gate (3/3) is red on PR feat(spec): notify title/message are template slots — bare string or tmpl envelope #22063 at 01ef8368e5 (note 6032851265).
    • Why it rides this PR: the ledger is the gate's own reader of the surface this card changes. The row moves with the change, at the same strength, and no other claim holds the file.
    • Bounds: that one row, measured, not recalled: its dialect, mode, state, fail policy and enforcement cell are read from the notify executor at the PR head. No other ledger row and no other dogfood file. The dev's next patch round carries it, after the at-tier contract review now running on 01ef8368e5.

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 22054,
      "status": "done",
      "branch": "claude/issue-22054-notify-title-template-input",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/22063",
      "session": "session_01GV6oYwgc1kWiUCb1YaprQ7. This run is a subagent, so this is the PM's harness-stamped id, the same one the commits' Claude-Session trailer carries.",
      "premise_still_valid": true,
      "round": "patch round 1, answering contract review 6033083158 (FAIL on ② only) and the red Dogfood Regression Gate (3/3)",
      "head_sha": "9bfb746a3594437fa36860bba393156ae47818a7 (parent 01ef8368e5; pushed as a fast-forward, the remote was still at 01ef8368e5)",
      "summary": "Two items, nothing else. (1) The changeset is re-graded to what the diff publishes. The summary is now a feat(spec)! line, the Clause-② line reads 'yes (narrowing)', and there is exactly one ADR-0087 marker, not-required (no-migration-prescription), carrying its census. A **BREAKING** accept-set narrowing line says it ships as minor under the launch-window convention. A consumer-side note covers the parse output: string to {dialect:'template', source}, read .source. The two false sentences are corrected: blank bare strings are now NEWLY refused, and only title '' failed every run before (a whitespace-only title was delivered). The levels stay: spec minor, service-automation patch. (2) One ledger row, template-notify-content, in packages/qa/dogfood/test/expression-conformance.ledger.ts covers NotifyConfigSchema.title and .message. Its cells were measured from notify-node.ts and template.ts at head and confirmed by a runtime probe through AutomationEngine. Schema, executor and tests are untouched. For the seat's PR-body update: the body's Clause-② line still reads 'yes (widening: …)'. The level axis of check-changeset-no-major passes offline with either line, so no gate will flag a body that keeps the old arm.",
      "tests": "At head 9bfb746a35. The dogfood test 'pnpm --filter @objectstack/dogfood exec vitest run test/expression-conformance.test.ts' exited 0 (1 file, 7 passed) after building the @objectstack/verify closure. Control leg: deleting only the title cover from the new row turned the ratchet red, 'UNCLASSIFIED surface — add a ledger row (ADR-0060): automation/io-node-config.zod.ts:NotifyConfigSchema.title' (1 failed, 6 passed). The restore was verified: blob equal to HEAD, git diff HEAD empty. Ledger types: tsc --ignoreConfig --strict with nodenext over the ledger file alone exited 0. Its control (failPolicy 'bogus') gave TS2322 'not assignable to type FailPolicy', and was restored and verified. The package's own typecheck (tsc --noEmit over every dogfood test) was not run, because it needs every dependency's dist: NOT MEASURED, declared to CI. ESLint on the ledger file: --no-inline-config --format json counted 1 file, 0 errors, 0 warnings. No type-aware linting is configured, so untouched files' verdicts cannot move. Runtime probe of the fault cells, AutomationEngine plus registerNotifyNode at head: title 42 or '   ' fails the run at the contract parse; {round(1,2,3)} fails with 'round() takes exactly 1 argument, got 3'; {ROUND(2)} fails with 'unknown function'; a title of '{missing}' fails with 'notify: title is required'; message 'Hi {record.missing} end' succeeds with body 'Hi  end' and no template log (the run's only two warn lines are the unsealed-vocabulary and no-organization notices); message 'A {dealName +* 2} B' succeeds with body 'A  B'; title '[{{dealName}}]' delivers '[{Acme}]'.",
      "mcp_calls": "0. No MCP GitHub tool was called.",
      "api_writes": "1, sent as one POST /repos/objectstack-ai/objectstack/dispatches from this container and executed by the fleet-write relay as objectstack-fleet[bot]: comment, POST /repos/objectstack-ai/objectstack/issues/22054/comments (this os-dev-report). git push (one, fast-forward 01ef8368e5 to 9bfb746a35) is not a REST write. The PR body, labels and assignee were not touched.",
      "open_questions": [],
      "out_of_scope_findings": [],
      "changeset_before": "---\n\"@objectstack/spec\": minor\n\"@objectstack/service-automation\": patch\n---\n\n`NotifyConfigSchema.title` and `NotifyConfigSchema.message` are template slots: each takes a bare string or a `{ dialect: 'template', source }` envelope (the `tmpl` helper), as the expression dialect table already listed notification subjects and bodies among the `template` slots\n\nClause-②: yes (widening: a published notify slot now accepts the template envelope as well as the bare string)\n\n- Both keys are typed with `TemplateExpressionInputSchema`, the input every other `template` slot uses. Before, both were `z.string()`, so a notify node written with `` tmpl`…` `` passed `defineFlow` and registration and then failed every run at the execute-time contract parse (`expected string, received object`).\n- The parse normalizes a bare string to `{ dialect: 'template', source }`, so `NotifyConfigSchema.parse(...)` now returns the envelope for both spellings. The `notify` executor reads the envelope's `source` and interpolates it as before, so both spellings of one text deliver the same `payload.title` and `payload.body`. A bare string renders exactly what it rendered before.\n- The placeholder spelling these two slots read is the flow's single-brace `{token}` (`{record.name}`). A `{{var}}` is not a placeholder here: the inner `{var}` resolves and the outer braces stay in the text, for a bare string and an envelope alike. The `.describe()` on both keys now says so, and no longer says the text is \"sent verbatim\".\n- Still refused at each key: a value that is neither a string nor a template envelope (a number, an array, a `cel` envelope), and a blank bare string (`''` or whitespace), both by the shared template input. A blank `title` used to parse and then fail every run (\"title is required\"). A blank `message` used to send an empty body, the same as leaving `message` out.\n- New, notify-only: a template envelope on either key must carry a non-blank `source`. The executor renders `source` and has nothing to render from `ast` alone, so such an envelope is refused at the key instead of failing every run (`title`) or sending an empty body (`message`).\n- `@objectstack/service-automation`: the `notify` executor reads `source` from the two template slots, and the descriptor's `title` / `message` descriptions state the `{token}` interpolation in place of \"sent verbatim\".\n",
      "changeset_after": "---\n\"@objectstack/spec\": minor\n\"@objectstack/service-automation\": patch\n---\n\nfeat(spec)!: `NotifyConfigSchema.title` and `NotifyConfigSchema.message` are template slots: each takes a bare string or a `{ dialect: 'template', source }` envelope (the `tmpl` helper), as the expression dialect table already listed notification subjects and bodies among the `template` slots, and a blank bare string is now refused there\n\nClause-②: yes (narrowing)\n\n[HTML-comment opener] adr-0087: not-required (no-migration-prescription) No authorable key is renamed, retired or re-shaped for an author: `title` and `message` still take every non-blank bare string they took before, and now also the `template` envelope. The one newly refused input is a blank bare string (`''` or whitespace-only) at either key, and it was measured, not assumed. Census: the 31 files in this repo that author a `notify` node at the merge base (examples, docs, skills, the ADR, tests and fixtures) carry zero blank `title` / `message` values. The objectui pin (`a58626c8`) has 9 files that name a `notify` type and zero blank `title` / `message` literals. Not covered by either count: objectui's flow inspector deletes a cleared key only when the committed value is `''` (`setAtPath`), so a whitespace-only entry typed into the Studio form IS stored, and a stored flow carrying one is refused at its next run; hosted tenants' stored flows were not measured. No conversion can repair such a value, because an empty title or body has no intended text to recover: the remedy is authoring intent (write the text, or delete the key), so the ledger has nothing to rewrite. [HTML-comment closer]\n\n**BREAKING** accept-set narrowing at two authorable keys (`automation/NotifyConfig:title`, `automation/NotifyConfig:message`), shipped as `minor` under the repo's launch-window convention for breaking changes. It is the grade `TemplateExpressionInputSchema`'s blank-string rule shipped with when it reached the first twelve typed keys.\n\n- **What widens.** Both keys are typed with `TemplateExpressionInputSchema`, the input every other `template` slot uses. Before, both were `z.string()`, so a notify node written with `` tmpl`…` `` passed `defineFlow` and registration and then failed every run at the execute-time contract parse (`expected string, received object`). It now parses and runs.\n- **What narrows.** A blank bare string (`''` or whitespace-only) at either key is newly refused, by the shared template input's non-blank rule (`invalid_union`, with the `TYPED_EXPRESSION_SOURCE_REQUIRED.template` sentence). Before, every blank value parsed:\n  - `title: ''` then failed every run at the executor's guard (\"notify: title is required\"), so it fails either way, now earlier;\n  - a whitespace-only `title` passed that guard and was delivered as the notification title, and it is now refused;\n  - `message: ''` or a whitespace-only `message` was delivered as an empty or blank body, and it is now refused.\n\n  The fix is to write the text, or to delete the key (`message` is optional).\n- **Parse output.** `NotifyConfigSchema.parse(...).title` and `.message` go from `string` to `{ dialect: 'template', source }`, for both spellings, because the parse normalizes a bare string to that envelope. The exported `NotifyConfigParsed` type changes with them. Code that reads parse output reads `.source`. The `notify` executor, the one reader in this repo, now does, so both spellings of one text deliver the same `payload.title` and `payload.body`, and a bare string renders exactly what it rendered before.\n- **Still refused, with a new sentence.** A value that is neither a string nor a template envelope (a number, an array, a `cel` envelope) was refused before (`invalid_type`). It is refused now as `invalid_union`, with the `TYPED_EXPRESSION_DIALECT_ONLY.template` sentence.\n- **New, notify-only.** A template envelope on either key must carry a non-blank `source`. The executor renders `source` and has nothing to render from `ast` alone, so such an envelope is refused at the key instead of failing every run (`title`) or sending an empty body (`message`). An envelope never parsed at these keys before, so this refuses nothing that used to parse.\n- **Placeholder spelling.** These two slots read the flow's single-brace `{token}` (`{record.name}`). A `{{var}}` is not a placeholder here: the inner `{var}` resolves and the outer braces stay in the text, for a bare string and an envelope alike. The `.describe()` on both keys now says so, and no longer says the text is \"sent verbatim\".\n- `@objectstack/service-automation`: the `notify` executor reads `source` from the two template slots, and the descriptor's `title` / `message` descriptions state the `{token}` interpolation in place of \"sent verbatim\".\n",
      "changeset_after_note": "In changeset_after the ADR-0087 marker is spelled in words, '[HTML-comment opener]' and '[HTML-comment closer]', because this text is posted to GitHub. The file at 9bfb746a35 carries the real HTML-comment delimiters, and check-adr-0087-registration reads them (see gates).",
      "census_measured": {
        "in_repo": "Files naming a notify type at the merge base d5a14dd5: 32. Minus notify-node.ts, which registers the executor and authors no node, that leaves 31 authoring files. A blank-value scan of them for title, message, subject or body set to '', \"\" or whitespace found zero hits. Its positive control matched 3 of 3 blank lines and 0 of 1 non-blank. At head the count is 32, the extra file being notify-template-slots.test.ts, which has no blank values.",
        "objectui_pin": "a58626c8 (.objectui-sha): 9 files name a notify type, with zero blank title or message literals (git grep exited 1).",
        "studio": "FlowNodeInspector.tsx setAtPath at the pin (line 150) deletes a leaf only for undefined, null or '', so a whitespace-only Studio entry is stored."
      },
      "ledger_row_as_written": "  {\n    id: 'template-notify-content',\n    summary: 'flow `notify` node inline content (NotifyConfig.title, .message) — single-brace `{token}` interpolation per run',\n    dialect: 'template', mode: 'interpret', state: 'enforced', failPolicy: 'throw',\n    enforcement:\n      'service-automation/builtin/notify-node.ts `execute`: `parseNodeConfig` parses the RAW config against `NotifyConfigSchema` first, and the parse normalizes a bare string to `{dialect:\"template\",source}`; then `stringifyForTemplate(interpolate(cfg.title?.source ?? \"\", …))` and the same for `message` — builtin/template.ts `interpolate` → `interpolateString`, which substitutes single-brace `{token}` only (`/\\\\{([^{}]+)\\\\}/g` → `resolveToken`), so a `{{var}}` keeps its outer braces. The rendered text goes out as `payload.title` / `payload.body` through the messaging service `emit`. On a fault: a malformed value (not a string or a template envelope, a blank bare string, a foreign-dialect envelope, an envelope with no non-blank `source`) is refused by that parse as a guard (`refuseNode`), so the run fails at the node and no `fault` edge routes it; a function-shaped defect in a token (unknown function, wrong arity, argument out of domain) THROWS `FlowExpressionFunctionError`, a marked guard refusal, failing the run the same way; and a `title` that renders empty fails the node (`notify: title is required`). NOT a throw: a token whose path resolves to nothing, or whose arithmetic does not evaluate, renders as empty text with no log, so a `message` goes out with the gap. Neither `FlowSchema.parse` nor registration judges these values (the builtin config arm reports only an ABSENT required key); `os validate` warns on a `{{var}}` (lint `flow-double-brace-interpolation`) and on an unknown `{record.x}` head (`validate-flow-template-paths`)',\n    covers: [\n      'automation/io-node-config.zod.ts:NotifyConfigSchema.title',\n      'automation/io-node-config.zod.ts:NotifyConfigSchema.message',\n    ],\n    proof: 'packages/services/service-automation/src/builtin/notify-template-slots.test.ts',\n    note: 'The renderer is the flow template interpolator, not `@objectstack/formula` templateEngine: the `{{var}}` spelling that engine and the messaging/email renderers read is NOT a placeholder here. `throw` is the ADR-0058 D5 flow tier and describes the faults the cell names as refusals; the silent half (an unresolved token rendering empty) is stated in the cell rather than rounded into the tier.',\n  },",
      "ledger_cell_readings": {
        "id": "template-notify-content: a new id, unique (checkLedger duplicate check green).",
        "dialect": "template. Both keys are typed TemplateExpressionInputSchema (io-node-config.zod.ts), and the parse normalizes a bare string to {dialect:'template', source}.",
        "mode": "interpret. notify-node.ts:274-275 interpolates per run, per execution; nothing compiles it to a filter.",
        "state": "enforced. A runtime evaluator exists: notify-node.ts execute, then interpolate over .source, then payload.title / payload.body into messaging.emit. Proven by the executor pins in notify-template-slots.test.ts.",
        "failPolicy": "throw: the ADR-0058 D5 flow tier ('flow → throw (author bug)'), the same as the cel-flow row. Measured faults that fail the run: the contract parse refusal (notify-node.ts:256 parseNodeConfig, then refuseNode guard), FlowExpressionFunctionError for a function-shaped token defect (template.ts, marked guard refusal), and an empty title (notify-node.ts:308). The silent half is stated in the enforcement cell, not rounded into the tier: an unresolved path or a non-evaluating arithmetic token renders empty with no log. That is why the tier is not fail-soft-log (there is no log) and not fail-closed (not a security deny).",
        "enforcement": "Names the evaluator (notify-node.ts execute, parseNodeConfig, interpolate over cfg.title?.source / cfg.message?.source, template.ts interpolateString with the single-brace regex and resolveToken) and every fault path measured above. It adds two facts: registration and FlowSchema.parse do not judge these values (flow-node-config-refusals forwards only ABSENT keys for builtins), and os validate warns (advisory, no severity: 'error') on {{var}} through lint flow-double-brace-interpolation (collectTemplateStrings recurses into the envelope, so its source is scanned) and on unknown {record.x} heads through validate-flow-template-paths.",
        "covers": "automation/io-node-config.zod.ts:NotifyConfigSchema.title and :NotifyConfigSchema.message, exactly the two keys the red ratchet named.",
        "proof": "packages/services/service-automation/src/builtin/notify-template-slots.test.ts. It exists, and checkLedger checks existence only (the conformance test passes no attribution option). This field is beyond the template-title-format shape; it is the shape of the enforced cron-job-schedule row.",
        "note": "States that the renderer is the flow interpolator, not formula's templateEngine, so {{var}} is not a placeholder here, and why the tier is throw while the silent half is stated in the cell. It carries no tracker ids (check:doc-authoring exited 0)."
      },
      "gates": {
        "check-adr-0087-registration (no args)": "exit 0. '✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.' and '.changeset/22054-notify-title-template-input.md [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription)', with the marker reason echoed and a ::notice of the exemption.",
        "check-adr-0087-registration --base origin/main (5cfd8661c4)": "exit 0, the same reading.",
        "check-changeset-no-major --base origin/main": "exit 0. 'Diffing HEAD from d5a14dd5c (merge base with origin/main). ✓ This diff introduces no `major` bump.' Then 'ℹ️ LEVEL AXIS: NOT APPLICABLE — this run has no `pull_request` to read a declaration from'.",
        "check-changeset-no-major --base origin/main --event (offline PR payloads)": "With the CURRENT PR body (Clause-② yes (widening: …)): exit 0, '✓ LEVEL AXIS: this PR declares clause-② `yes (widening)` … @objectstack/spec: minor'. With the body's Clause-② line replaced by 'yes (narrowing)': exit 0, 'direction arm: `narrowing` — a BREAKING change; during the launch window it ships `minor`'. Both pass, so the gate does not catch a body that keeps the old arm.",
        "clause2 reader": "readClause2Line on the changeset reads kind=declared, value=yes, arm=narrowing.",
        "dogfood expression-conformance": "exit 0 (7 passed); the control leg was red as expected.",
        "derived families": "dispatch-gates --commands at 9bfb746a35: 111 families, the same set as round 1 (no family added or dropped). All were run, and --ran reconciled them: 110 run, 1 NOT MEASURED, 0 unrun. NOT MEASURED: check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET, not every package has a dist), so it is declared to CI. check:skill-examples exited 0 this round after building @objectstack/client and @objectstack/client-react. check:doc-authoring exited 0, check:nul-bytes exited 0.",
        "builds (under the verify lock)": "pnpm --filter '@objectstack/verify...' build: exit 0 (306 s held). Client packages: exit 0.",
        "ci": "At report time on 9bfb746a35: 1 completed, 7 in_progress, 9 queued. Not waited on."
      },
      "files_changed": [
        ".changeset/22054-notify-title-template-input.md (re-graded; levels unchanged: @objectstack/spec minor, @objectstack/service-automation patch)",
        "packages/qa/dogfood/test/expression-conformance.ledger.ts (one row: template-notify-content)"
      ],
      "deviations": [
        "The new row carries a 'proof' field (the executor pin file) and a 'note', beyond the template-title-format shape. 'proof' is the shape of the enforced cron-job-schedule row; it is optional, and checkLedger only checks that the file exists. No other row or dogfood file was touched.",
        "failPolicy is 'throw' by measurement, with the non-throwing half (an unresolved token renders empty, unlogged) written into the enforcement cell. The dispatch named no tier; this is the measured reading, stated so the seat can re-grade it.",
        "The dogfood package's whole-package typecheck was not run (it needs every dependency built). The narrow tsc over the ledger file, with its control, stands in; the full typecheck is declared to CI.",
        "In this report the changeset's HTML-comment marker is spelled in words (see changeset_after_note)."
      ],
      "cleanup": "Removed the worktree's node_modules and the worktree itself (git worktree remove, no --force), after the push. No server, background job or monitor was left running: the background build and the gate battery both exited and were waited on in the foreground."
    }

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR #22063 at 9bfb746a35 (a notify node's title / message are template slots). ⛔ Not enqueued: it is a declared BREAKING narrowing, held on the release-line question

    domain:spec seat 2 · os-warren · session session_01GV6oYwgc1kWiUCb1YaprQ7 · 2026-10-07T08:36Z · holder of claim 6031783397 (file surface revised in 6032860972); the review of record for the reports 6032736906 and 6033625039 (patch round 1).

    Checklist (read on GitHub and on origin/main, not from the reports):

    • Form: draft, base main, first line Fixes #22054, and no other closing keyword in the body.
      • The PR body, the changeset and the claim (edited in place) each carry Clause-②: yes (narrowing…).
      • PR assignee os-warren.
    • Scope: 7 files, all inside the revised claim:
    • What the diff does, as graded:
      • It widens: the tmpl envelope now parses and renders the same text as the bare string.
      • It narrows: a blank or whitespace-only bare string is newly refused. A whitespace-only title used to be delivered.
      • It re-shapes the parse output: NotifyConfigParsed.title / .message become the envelope. The executor, the one reader in either repo, reads .source.
      • The changeset grades this as the repo graded the same rule in f81afe3: feat(spec)!, a BREAKING line, and one ADR-0087 not-required (no-migration-prescription) marker with a measured census (zero blank values in the repo and at the objectui pin; the whitespace gap in Studio named).
    • Contract review: FAIL on 01ef8368e5 (6033083158, on ② only: the changeset declared a pure widening). PASS on this head at CONTRACT_REVIEW_TIER (6033880025, Local-runs: none, identity pair present). It found the re-grade, the corrected sentences and the ledger row true.
    • The CI red on 01ef8368e5 is fixed: the ADR-0060 ratchet's two unclassified surfaces are covered by the new template-notify-content row, and all three Dogfood Regression Gate shards are green on this head.
    • The seat chose the shape: the record's four-axis input favoured the shape as built (uniform with every other template slot, as triage directed), declared honestly. The unwrap-to-string alternative was not taken. This is a p3 card with a working spelling (the bare string), so there is no case for landing it before the closing 17.x release.

    Checks on 9bfb746a35: 38 success and 4 skipped. check-expected-skips reads all 4 as on the roster (Auto Label, Check PR Size, Console Pin Gate, Packed-tarball smoke (opt-in)). Lint & Repo Gates (it carries check:adr-0087-registration and the LEVEL axis of check-changeset-no-major) and TypeScript Type Check are success on this head.

    ⛔ Landing held. Ruling 6020116360 keeps a narrowing breaking change out of the closing 17.x release ("it lands after the opening, in 18.0"), and both review records read this PR as that class. Narrowings on other doors have landed on main since that ruling, so the seat puts this PR with #22032's PR #22041 under the one question already with the maintainer: land it now in the closing 17.x release, or hold it for the v18 opening. The PR stays draft, with no auto-merge, until that answer.

    Acceptance notes (follow-ups whose trigger is this PR's merge; the seat files them then):

    • The shared refusal sentences TYPED_EXPRESSION_SOURCE_REQUIRED.template / TYPED_EXPRESSION_DIALECT_ONLY.template (expression.zod.ts:295, :311) and the tmpl docblock prescribe {{record.name}}. After this merge they become the refusal a notify slot returns, though the notify renderer reads single-brace {token} and packages/lint refuses {{…}} on flow node values. A card on expression.zod.ts follows the merge (contract review ③, escalation A).
    • objectui's flow inspector (FlowNodeConfigField.tsx) renders a text control with String(value), so a code-authored envelope would show as [object Object] once envelopes are legal. An objectui card follows the merge (escalation B).
    • Not filed, no measured reach: typed slots (TemplateExpressionInputSchema, CronExpressionInputSchema) admit an ast-only envelope that no engine renders. This PR closes it for notify alone, with its own rule (escalation C).

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    The release-line hold is lifted: PR #22063 lands through the queue now

    domain:spec seat 2 · os-warren · session session_01GV6oYwgc1kWiUCb1YaprQ7 · 2026-10-07T12:36Z · holder of the claim on this card.

    Provenance (who, verbatim, where): the maintainer, in this seat's session chat, answering the release-line question this seat put for PR #22041 and PR #22063. Verbatim: 「v18 已经启动,这两个可以合并了」.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #22063 → d4680d2820. Fixes #22054 closed the card; pm:dispatched is removed

    domain:spec seat 2 · os-warren · session session_01GV6oYwgc1kWiUCb1YaprQ7 · 2026-10-07T13:23Z · holder of claim 6031783397 (file surface revised in 6032860972), which this act releases.

    • Landed: PR feat(spec): notify title/message are template slots — bare string or tmpl envelope #22063 merged through the merge queue at 2026-10-07T13:17Z as d4680d2820, after the maintainer's word that v18 had started (recorded in 6038044985). It has one parent (e67ba80049) and is an ancestor of origin/main. The queued head 9bfb746a35 is not an ancestor, as a squash leaves it.
    • Content check: all 7 files on origin/main are blob-equal to the accepted head 9bfb746a35 (ACCEPT 6034196850; contract review PASS 6033880025).
    • What now holds:
      • A notify node's title and message accept a bare string or the { dialect: 'template', source } envelope, and the executor renders both the same way.
      • A blank or whitespace-only bare string is refused (the declared narrowing).
      • NotifyConfigParsed.title / .message are the envelope.
      • It ships as feat(spec)!: minor with its BREAKING banner for @objectstack/spec, a patch for @objectstack/service-automation.
    • The card closed completed at 2026-10-07T13:17Z, by Fixes #22054.

    The ACCEPT's three acceptance notes:

    • Escalation A, filed as spec: a notify node's refused title / message is told to write '{{record.name}}', the spelling the build's flow-double-brace-interpolation rule then flags on the same node and the notify renderer does not resolve #22081. A refused notify slot is told to write '{{record.name}}'. Measured on d4680d2820: that spelling, bare or as an envelope, draws flow-double-brace-interpolation from the build's flow rule on the same node, and the notify renderer reads single-brace {token} only.
    • Escalation B, not filed. objectui's flow inspector would show a notify envelope as [object Object] (FlowNodeConfigField.tsx:444, :465, String(value), on objectui main 179f6fe9dc). It fails the filing gate's reach: test: nothing in either repo writes an envelope into a notify title today, and no screen reproduction was taken. Carrier: none, and the seat records it here. Read from the same source, and not measured: objectui's notify field labels (metadata-admin/i18n.ts near :6405) still localize url, which 17 renamed to actionUrl.
    • Escalation C, not filed (as the ACCEPT said): typed slots that admit an ast-only envelope no engine renders. There is no measured reach.

    Unblocks: nothing names #22054 in a Blocked-by: line. Seat 1's PR #21974 shares notify-node.test.ts and merges main at its own landing.

    Release: session_01GV6oYwgc1kWiUCb1YaprQ7 · why: the card's change landed and closed it · to: closed, pm:dispatched removed.


    Generated by Claude Code

  9. added a commit that references this issue on Oct 9, 2026
    d4680d2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:workflowApprovals and automation — the work that runs without a person driving itbugSomething isn't workingdomain:specpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions