Skip to content

Commit 26cfdf2

Browse files
authored
Merge branch 'main' into claude/issue-19157-checklist-status-workflow
2 parents 1ca5b62 + c229223 commit 26cfdf2

62 files changed

Lines changed: 4881 additions & 838 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"@objectstack/core": minor
3+
"@objectstack/plugin-security": minor
4+
"@objectstack/plugin-auth": minor
5+
"@objectstack/runtime": minor
6+
"@objectstack/plugin-hono-server": minor
7+
"@objectstack/organizations": minor
8+
---
9+
10+
**Breaking (shipped as `minor` under the launch-window convention).** Under a **walled** tenancy posture (`group` / `isolated`), a legacy unscoped `admin_full_access` grant row no longer confers `PLATFORM_ADMIN`; platform standing there is derived from `OS_PLATFORM_OWNER_EMAIL` and from nothing else. The migration pointer that announced this since 17.3.0 is retired with it: `reportLegacyPlatformAdminGrant` and `resetLegacyPlatformAdminGrantReport` are **removed from `@objectstack/core`'s published entry** (#18336, #11663 leg L5).
11+
12+
⚠️ **The `single` posture is untouched, deliberately.** Its zero-config first-user promotion still mints that row and that row still confers `PLATFORM_ADMIN` — a development environment started for a moment cannot be asked to declare an administrator first. Choice 4A (#11974) rules that promotion correct, and the maintainer's 2026-09-08 ruling on #16682 is verbatim: 「retiring the walled write must not retire the `single` one」. The `single` half's disposition is #11979's. ADR-0131 D5, as amended 2026-09-17 (#18413), is the governing record.
13+
14+
**What a walled deployment must do.** Declare each administrator's **verified** address in `OS_PLATFORM_OWNER_EMAIL` (comma-separated for several) before upgrading. A walled rig that upgrades with the variable undeclared and an unscoped grant row still in place has **zero** platform administrators; the bootstrap now says so **at error**, naming the variable, the row and its holder — L4 used to skip that line for exactly this rig, on the ground that the deprecation pointer carried the remedy instead, and both halves of that arrangement have now expired.
15+
16+
- **17.3.0 opened the window, this closes it.** L4 (17.3.0) stopped the walled bootstrap from ever *writing* the row and started the once-per-process pointer; L5 stops the walled derivation from *reading* it. The window was time-boxed and loud by design (#11663 P5).
17+
- **The retirement takes the ANCHOR, not the ROW.** Nothing here writes, deletes or re-owns any grant row — a walled holder keeps the `admin_full_access` permission set they hold, and loses only platform-admin *standing*: the rung and the built-in `platform_admin` position. That row's ownership is ADR-0131 C3's, on the v18 line.
18+
- **No new query.** The posture gate reads the environment, never the engine, so the recorded query multiset is identical under both of its answers — measured, not asserted. Under a wall the guard's grade-1 scan is skipped outright, so that path issues one read fewer.
19+
- **`@objectstack/plugin-auth` moves with it, at TWO readers.** `ensureDefaultOrganization`'s step-2 legacy fallback is keyed on the same expression: under a wall it no longer answers「which user is the platform admin?」from the oldest unscoped grant, so the account it would have bound as the Default Organization's `owner` — and handed the org's seeded rows to — is no longer selected. ⛔ That reader does not merely count the population, it **confers** on it, which is why it is keyed here rather than sequenced. Its bootstrap-trigger predicate retires the matching `sys_user_permission_set`-insert arm under a wall with it (cost only; the `sys_user` arms are untouched, and on a walled rig the declared owner's verifying update is the only write that ever grows the population). And:
20+
- **`@objectstack/plugin-auth`'s break-glass guard moves with it.** `last-admin-guard.ts` enumerates the administrator population from the SAME anchor, and its contract is to answer the same question the derivation answers. Its grade-1 (grant-anchored) enumeration is now keyed on the identical expression, so under a wall the guard no longer counts a holder the derivation does not recognise. Consequence on a walled rig: a write that would end the last **config**-anchored administrator's standing is now REFUSED where it was permitted, and a write that removes the now-inert grant row is no longer refused as though it removed the last administrator. Under `single` the guard is unchanged. Its two zero-population refusals also gained a walled clause, because「restore the `admin_full_access` row」stopped being a remedy that ends the emptiness there.
21+
- **`@objectstack/organizations`' walled bootstrap moves with it.** That package wraps `ensureDefaultOrganization` and is the runtime that actually performs the default-organization bootstrap on a walled deployment (plugin-auth's own wiring skips it there). With the helper's legacy fallback keyed off, a walled rig carrying a legacy grant row **no longer** has a Default Organization created for that holder, and that holder is no longer bound as its `owner`; the bootstrap waits for a declared administrator to verify instead. ⚠️ Named because the behaviour an operator gets **from this package** moves — its own source does not change, and the pin re-authored inside it is not the reason.
22+
- **Why `@objectstack/runtime` and `@objectstack/plugin-hono-server` are named.** Neither package's own source changes. Both carry `export * from '@objectstack/core'` (`runtime/src/index.ts`, `plugin-hono-server/src/adapter.ts`) and their built `.d.ts` carry that statement, so the two removed names leave their published surfaces too. All publishable packages sit in one Changesets `fixed` group, so naming them moves no version — it is named so the tombstone reaches the CHANGELOG an upgrading consumer of THOSE packages greps. Precedent is mixed (a core-only declaration exists); this follows the `ApiRegistry` precedent, which named every package the removal reached.
23+
24+
<!-- adr-0087: not-required (no-migration-prescription) Nothing here is a metadata surface: the two removed symbols are plain runtime functions in `packages/core/src/security/platform-admin.ts` with no Zod schema, no `packages/spec` declaration and no stored representation, and the behaviour change is an authorization derivation keyed on an environment variable. `objectstack migrate meta` therefore has nothing to rewrite — the channels that reach an affected consumer are the compiler (for the removed exports) and the boot-time fail-closed log line (for the walled standing). No grant row is written, deleted or re-owned by this change; that rows own migration is ADR-0131 D10/C3 and stays on the v18 line. The plugin-auth guard change and the two re-export packages add no metadata surface either. -->
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
**BREAKING (published artifact narrows)** — `packages/spec/json-schema/**` now states the banned-key rule the tracing sampling filter enforces, so a validator reading the published files stops answering PASS on `{ "dialect": "cel" }` at `TraceSamplingConfig.composite[].condition` — the card's own worked instance of a published file saying yes to metadata the runtime refuses (#18670 item 2, the fourth of the ruling's named arms).
6+
7+
Clause-②: yes (narrowing)
8+
9+
One named pattern joins the closed list, and only one:
10+
11+
- **`banned-keys` — "no document may carry any of these keys"**, emitted as `propertyNames` with a `not` over the banned names. `TraceSamplingConfig.composite[].condition` is a structured filter of match criteria that refuses an object carrying `dialect`, because such an object is an expression attempt and this slot's expression arm was retired in 17.5.0. The published file now says so.
12+
13+
**The rows retired, by name.** `packages/spec/dropped-refinements.baseline.json` goes from 202 entries / 553 sites to **200 entries / 551 sites**:
14+
15+
| row | before | after |
16+
|:---|:---|:---|
17+
| `system/TraceSamplingConfig` | `sites: ["composite.element.condition"]` | **deleted** — the schema drops nothing now |
18+
| `system/TracingConfig` | `sites: ["sampling.composite.element.condition"]` | **deleted** — the same node, reached through the parent |
19+
20+
2 sites closed, **0 sites added anywhere**, and the ledger diff is deletions only. Generator census after: 551 dropped across 200 published schemas, **357 projected** — 224 `non-blank-string`, 129 `required-one-of`, 2 `dependent-required`, **2 `banned-keys`** — 9 undecidable.
21+
22+
**⛔ Not a behaviour change, and no document the runtime accepts becomes refused.** The arm is EXACT rather than approximate: a JSON object's properties are exactly its own enumerable string-keyed ones and `propertyNames` judges exactly those names, so "none of the banned names is an own property" and "no property name is one of the banned names" are one sentence read from two ends. It is presence and never value — a banned key present with a `null` value is present on both sides. The accept set at the slot is **unchanged in both directions**: every document the runtime takes (`{}`, `{ "service": "api" }`, any filter carrying no `dialect` key) the file still takes, and every document the runtime refuses the file now refuses too — a `dialect`-bearing object of any shape, the CEL envelope included, since that arm is retired and nothing here revives it. Across the published tree, **1528 of the 1530 per-schema files are byte-identical**; the two that move gain the ban and lose the matching `x-dropped-refinements` row, and nothing else in either file changes.
23+
24+
**The list stays CLOSED.** `packages/spec/src/shared/refinement-projection.ts` declares the vocabulary and builds each predicate from its own declaration — the key list is read once and used by both the published keyword and the enforced rule — so the two cannot name different keys. The predicate judges OWN properties and never `key in value`: `in` walks the prototype chain, so a ban on a name `Object.prototype` carries would refuse `{}` itself while `propertyNames` accepts it, and that is a disagreement about a JSON document rather than an edge outside the domain. A ban over an OPEN set of names — every key starting with `$`, which is what `data/filter.zod.ts`'s normalized field condition refuses — is deliberately not this arm: its keys are a finite list, and a list that merely sampled an open set would be wider than the rule, so those sites stay unprojected — and because the detector reads them `undecidable` rather than `dropped`, they carry NO annotation and hold NO ledger row: published yet unratcheted.
25+
26+
<!-- adr-0087: not-required (no-migration-prescription) Nothing an author can write is removed, renamed or re-spelled: no spec key, no export and no config field changes, and the accepted set of metadata documents is byte-for-byte what it was. What changed is a machine-readable DECLARATION catching up with the runtime it always described, so there is nothing for `objectstack migrate meta` to rewrite and no stored representation to convert. -->
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@objectstack/service-automation": patch
3+
---
4+
5+
fix(service-automation): a child that PAUSES and then refuses now rolls its refusal up on both resumed legs — the delegated resume and the up-bubble (#18714)
6+
7+
**Clause-②: no** — nothing published moves. The two arms are added inside `AutomationEngine`'s private `resumeInternal` / `bubbleToParent`, and the one new type (`ChildRunRefusal`) is module-private, not barrel-exported. No schema key, no closed-set member, no export and no registry entry changes; `refused` has been a published terminal status since #15788 and no new status, code or `ERROR_CODE_LEDGER` entry is minted here.
8+
9+
#18110 / #18555 gave the `subflow` and `map` executors an arm for `child.status === 'refused'`, and that arm reads the value `engine.execute` **returned** to them — so it covers exactly one shape: a child that runs straight through without pausing. A child that durably PAUSES first (a nested `approval` / `screen` / `wait`) never returns through that call at all. Its outcome reaches its parent on one of two **resumed** legs instead, and neither had an arm. Both pre-date #18110/#18555 and neither is a regression of it; that delivery named the two executors and matched its ruling exactly, and its own changeset filed this card for the remaining half.
10+
11+
The two legs failed **differently**, so each gets its own arm and its own pin:
12+
13+
- **Delegated resume** — `engine.resume(parentRunId)`, the path a screen-flow runner takes when it holds one stable run id and posts every wizard step to it. The delegation block tested only `paused` and `!success`; a refused child is neither, so it fell through the ordinary success exit. Measured: the parent answered `{ success: true, successMessage: … }`, its run row recorded **`completed`**, and the node downstream of the `subflow` **ran**. The refusal was lost **fail-open** — the identical shape #18110 closed on the synchronous leg.
14+
- **Up-bubble** — `engine.resume(childRunId)`. `bubbleToParent` was called on the completion path only, so a child resumed to a refusal resolved exactly one of the two runs it is responsible for. Measured: the child row recorded `refused` correctly and the parent stayed **`paused`**, in `listSuspendedRuns()`, indefinitely. Nothing looks wrong; a run is **leaked**.
15+
16+
What changed:
17+
18+
- **One terminal shape, both legs.** Each leg records the child's refusal and hands it to a single throw site inside the resume's traversal `try`, which raises the engine's existing internal refusal signal — so the refusal leaves through the same `finishRefusedRun` chokepoint every other producer already uses. ⛔ Deliberately not a second terminal exit per leg: this file's history is a list of outcomes that became a function of which route a run took.
19+
- **The throw site sits past the consumption and before the traversal.** The parent's own suspension is consumed exactly as it is on every other way a resume can end, so the terminal row and the pause can never disagree; and nothing downstream of the awaiting node runs.
20+
- **The parent's terminal row reads `refused`**, carrying the child's already-rendered `refusalMessage` verbatim, and the parent's own `successMessage` stays silent. ⛔ Not `failed`: a refusal is not a failure — it must not consume retry budget, must not be routable by a `fault` edge and must not be counted in `nodes[].failures`.
21+
- **The child's #4354 rollup (`selected` / `acted` / `unmeasuredEffect`) survives on both legs**, for the same reason it survives on the synchronous one: the refusal is raised after the awaiting step has been credited. A child that refused really can have written rows before it said no.
22+
- **Chains of any depth resolve**, because the up-bubble arm resumes the parent for real — the parent consumes its pause, records its own terminal row and bubbles to *its* parent in turn, by the same induction completions already rely on. ⛔ Not a direct ancestor walk like the failure cascade's: that verb records ancestors `failed`, which is the wrong word here.
23+
- **The child's own resumer is told exactly what it was told before** — the bubble is still best-effort at the engine layer and never rewrites the child's envelope.
24+
25+
Unchanged: the synchronous leg (#18110/#18555), the region-containment refusal (#18881 — a different error type on a different path, which neither resume leg raises or consumes), the retryable delegated resume-bag codes (#14379), the terminal child-failure cascade, and the `RESUME_IN_PROGRESS` / `STORE_UNAVAILABLE` / stranded gradings on the bubble.
26+
27+
⚠️ **Behavioural direction**: a run that previously finished green over a refusing paused child now terminates `refused`, and a parent that previously sat in `listSuspendedRuns()` forever is now resolved. Both are the authored outcome arriving where it never did; a composition that depended on the fail-open was depending on the defect.

‎.claude/agents/os-dev.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ model: opus
282282
- 关键词是 `fix/fixes/fixed/close/closes/closed` 与 `resolve/resolves/resolved`;让它们远离其它卡号。
283283
- 写 `#<n> is not addressed here`、`out of scope: #<n>` 或 `#<n> remains open`。
284284
- 卡片关系只在 PR 正文声明一次:commit ⛔ 不带卡片 trailer,其 trailer pair 一律 model-free。
285-
- 标题与散文用英文(见 AGENTS.md);引用的中文裁决保持原文不译,改写引文就是改写裁决。
285+
- harness 归属提醒凭其优先级句让位本文件;harness 自写含模型名 trailer 只报,⛔ 不仿不改史。
286286
- 受管路径全在 `.claude/skills/pm-dispatch/references/` 者为事实层,席位复审即记录;余为规则层。
287287
- 规则层 PR 正文带 `## 维护者速读(草稿)` 节,中文、业务角度,席位意见留空;事实层不欠。
288288
- 五段固定:改了什么/为什么改/风险与代价(含回滚)/席位意见/你要做的;席位定稿成评论。

‎.claude/skills/checklist-author/SKILL.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ metadata:
4141
3. **本技能的简报是假设,源码才是真相** —— 每个测试项按 README.md「Item anatomy」
4242
的契约写;缺 fixture 记 `blocked`/`knownGaps`,永不伪造覆盖。
4343

44+
## 缺项写成 `planned` 项,不写成注释
45+
46+
sweep 发现「定义要求、平台还没有」的能力,写成该区的 `status: "planned"` 项(title ·
47+
priority · personas,`since: null` 或目标 release,⛔ 无 steps);注释没有 id,派发时指
48+
不到。它是 `coverage.json` 的合法映射目标但⛔ 不算覆盖:只映射到它的种类仍报 UNMAPPED。
49+
50+
**`planned → active` 只由一次判 pass 的运行记录兑现** —— steps/acceptance 随实现 PR 落
51+
地,跑过才翻状态(bump `revision`、追 `history`);⛔ 代码落地就翻 = 账本开始虚报覆盖。
52+
4453
## 规模指引
4554

4655
一轮全量 sweep ≈ 5 个 hunter + 8 个 writer agent。范围化的问题(「X 有测试吗?」)

‎.claude/skills/checklist-test/SKILL.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ node scripts/checklist-select.mjs <selector> --json
5555
`blocked` 时才传 `--include-blocked`。**把解析器报出的 `revision` 钉进运行记录**:
5656
判定只对它运行时所对的那个 revision 有效。
5757

58+
**`planned` 项任何选择器都不返回,也没有开关让它返回**:能力不存在,无 steps 可驱动、
59+
无 oracle 可查,解析器单独列出命中项。运行记录逐项记 `planned`,⛔ 永不 pass / fail /
60+
blocked / not-run,⛔ 不进标题计数;与 blocked 的分界是 fixture 债 vs 平台债。
61+
5862
## 1. 规划这一轮 —— 只构建需要的,钉住的先跑
5963

6064
读命中项的 `surface`:

‎.github/workflows/duplicate-fix-guard.yml‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,17 @@ jobs:
7878
7979
// Branch-name convention (advisory, never red): a fix branch named
8080
// `claude/issue-<n>-<slug>` is discoverable by the next session
81-
// with one `git ls-remote | grep issue-<n>`. #4555 vs #4559
82-
// happened partly because the branches shared no token to grep.
81+
// with one `git ls-remote --heads origin | grep -E 'issue-<n>(-|$)'`.
82+
// #4555 vs #4559 happened partly because the branches shared no
83+
// token to grep. Both spellings delimit the number on the RIGHT:
84+
// undelimited, `issue-186` matches `claude/issue-18611-x`, so the
85+
// advisory stays silent for a branch naming a DIFFERENT card —
86+
// the false negative. The `$` arm keeps the slug-less spelling
87+
// `claude/issue-186` matching; `issue-` anchors the left, so
88+
// `issue-1186` and `issue-0186` do not match card 186 either.
8389
// Warning only — existing branches must not go red retroactively.
8490
const branch = pr.head.ref;
85-
if (![...mine].some((n) => branch.includes(`issue-${n}`))) {
91+
if (![...mine].some((n) => new RegExp(`issue-${n}(-|$)`).test(branch))) {
8692
core.warning(
8793
`Branch \`${branch}\` does not name any declared issue. ` +
8894
`Convention: claude/issue-<n>-<slug> (e.g. claude/issue-${[...mine][0]}-short-slug) ` +

0 commit comments

Comments
 (0)