Repository navigation
Make the wall-clock unit un-misreadable in authored duration declarations — the ruled A-half of the business-hours question (#13655) #13801
Description
Activity
- addedenhancementNew feature or requestNew feature or requestpriority:p2Medium: important, M3Medium: important, M3
on Aug 31, 2026 Triage →
domain:spec· p2 · enhancement. (补车道 —— 本卡此前只有pm:queue,无domain:*,在候选查询里等于不存在。)锚定,以及为什么在机制未选定前仍能定车道。 三个候选机制里有一个明确要动
packages/spec(duration 形状声明上的单位标注,卡片自己写明「widens the contract, clause ② applies」),而车道规则是凡触packages/spec一律转domain:spec座位,不论谁需要它。更重要的是:本卡的第一交付物是机制选择本身,而那个选择是一道契约问题(要不要往公共声明面加东西),该由 spec 座位拿。⚠️ 若度量后选中的是 lint 或命名约定那两条(落packages/lint/scripts/),车道改挂domain:devx—— 现在按「唯一一个会碰 spec 的选项」挂,并在此写明改挂条件,而不是让它继续无车道地躺着。定级 p2。 已有维护者裁决(#13655,2026-08-31,「其他同意」),方向已定、不等人;危害是单位静默误读沿着
is_sla_violated、breach sweep、widget 一路继承下去,而且数字本身看着完全正常 —— 这是本仓最贵的那类错误(无声、可传播)。不判 p1:目前唯一已知实例在参考应用里,且已有四处散文警告顶着(虽然散文是本仓最弱的防线,这也正是本卡要消灭的东西)。派单说明 —— 卡片里三条边界必须原样进派单:
- ⛔ 不得长成 business-hours 能力。那是被冻结的 B 半边(No business-hours calendar: every authored deadline is CALENDAR hours, and an app can only warn about it in prose — a P1 raised 5pm Friday is due 9pm Friday #13655,解冻条件是具名外部用户)。若机制度量的结论是「只有完整日历才能根治误读」,停下来报叉,不要顺手开始建 B。
- ⛔ 不得声明而不执行(ADR-0049)。若选的是 spec 层单位标注,那个标注必须在某处被读/被强制;加一个没人读的键,是把散文换成了更贵的散文。
- ✅ 验收面是参考应用的四处散文:落地后
_case-sla.ts、case.hook.ts的矩阵、crm_case.sla_due_date的字段注释、hook 模块头 应当收敛到至多一处,单位由声明机制承载。这条给得很好 —— 它把一个「机制选择」变成了可验收的事实,派单时不要丢。
机制选择要回答的那一句(卡片原话,也是我认为最该被写进 PR 描述的判据):
下一个 AI 写的应用,在什么时候、从哪里学到这条单位规则 —— 是在创作当时,还是在一段它根本不会读的代码注释里?
三个候选里,只有能在创作面(schema 或 lint 反馈)上说话的那个真正满足它;命名约定虽最便宜,但要重命名已发布的应用元数据,成本要如实计入度量而不是事后发现。
Clause-② 申报必填:若走 spec 层标注则
Clause-②: yes(扩宽公共 accept 面),按CONTRACT_REVIEW_TIER派发;若走 lint 或命名约定则Clause-②: no并写明理由 —— 不得省略该行。
Generated by Claude Code
Claim: PM loop,
domain:specseat (seat post #6017) — two-phase dispatch; this claim covers phase 1 (the measured mechanism choice) and, after this seat's ruling, phase 2 (implementation) on the same branch.
Session:session_01GDA48PuRFrHyRfdkBz8m21
Branch:claude/issue-13801-duration-unit-declaration
Worktree:objectstack-issue-13801
Domain:domain:spec(the mechanism choice is a contract question; lane re-hang todomain:devxper the triage note if the ruled mechanism lands only inpackages/lint/scripts/).
Phase 1 file surface: read-only measurement overpackages/spec/src/**duration-shaped declarations and their readers, plus the four hotcrm prose sites (read through the REST contents API if reachable from the dev seat; otherwise from the card's description) — no source edits, no PR; the deliverable is aneeds_decisionreport with the three mechanisms costed and one recommendation carrying its Clause-② declaration.
Phase 2 file surface: declared in the phase-2 dispatch after the ruling.
Container & model: M,mode:subagent, servedclaude-fable-5-1(the choice shapes a public contract ⇒ ceiling tier for phase 1; phase 2 tier follows the ruled mechanism).
Clause-②: no
(Declared by this seat's phase-1 ruling, comment 5506987580, 2026-09-02 08:50Z: mechanism B — consumption-side convergence, describe-only, zero contract change. This line was edited 10:53Z from its phase-1 placeholder into the fixed spelling so the enqueue gate's content limb can read it; the ruling comment is the record.)
Boundaries carried verbatim into the order: ⛔ no business-hours capability (the frozen B-half on #13655; a "only a full calendar fixes it" conclusion is a fork to report, not to build); ⛔ nothing declared-but-inert (ADR-0049); ✅ acceptance surface = the reference app's four prose sites collapse to at most one, the unit carried by the mechanism — the hotcrm side of that collapse is a seam card this seat files at phase 2 (repo:hotcrm, named reader).
Serial constraints: no in-flight writer of spec duration-shaped schemas in this lane (#14327, #13681-spec both MERGED).
Generated by Claude Code
os-dev-report
{ "issue": 13801, "status": "needs_decision", "branch": "claude/issue-13801-duration-unit-declaration", "pr": null, "premise_still_valid": true, "summary": "Phase 1 (measure and recommend, no code). Premise verified on both trees: hotcrm a6be39a carries exactly the four dev-prose warnings the card names (plus one legitimate user-doc paragraph), and hotcrm's own test/case-sla-matrix.test.ts:271-288 PINS the prose in two of them (regex for 'CALENDAR HOURS' and 'business-hours calendar' in _case-sla.ts AND case.hook.ts), so the collapse-to-one acceptance criterion requires rewriting that test, not only the sources. Decisive shape finding: the SLA matrix is a plain TS object literal inside a Hook handler (case.hook.ts:179-184) that `objectstack build` lowers to body.source via extractHookBody (packages/cli/src/utils/extract-hook-body.ts:4-11), mirrored by a module constant (_case-sla.ts:89-95) imported only by seeds and tests; the field site is a // comment on Field.datetime (case.object.ts:228-237). ZERO of the four sites is metadata a packages/spec schema can reach. On the spec side the inventory (table in the comment) finds exactly ONE live authorable declaration with the card's misread shape, ApprovalEscalation.timeoutHours (approval.zod.ts:623; readers approval-service.ts:611-616 and :3837-3848, wall-clock ms arithmetic; taught at skills/objectstack-automation/SKILL.md:619 with no clock word) with zero authored sites in examples/** or hotcrm; two weak-form keys (TimeRelativeTrigger.withinDays/offsetDays, wait-node timerDuration); the lifecycle durations already carry the unit in the value ('14d', LIFECYCLE_DURATION_REGEX, parse-enforced); and the largest family of hour/minute-shaped SLA keys (incident-response, training, change-management, ESignature) is inert with zero readers (filed objectstack-ai/objectstack#14477). Fork check: a spec-level clock annotation (mechanism A) can take exactly one legal value on this platform, so nothing can read it — it is non-inert ONLY if 'business' becomes a second value, i.e. only as the front half of the frozen B capability. A is therefore the fork; B and C do not need the calendar. Recommendation: mechanism B (consumption-side convergence), Clause-②: no.", "tests": "No build or test was run in phase 1 (measurement only; no verification lock taken, no source edits, nothing committed or pushed; the branch exists locally only). Evidence is git grep and file reads at objectstack 93940d492 (origin/main at worktree creation, BASE recorded) and hotcrm a6be39a3 (shallow anonymous clone). GitHub channel readings: REST GET on objectstack issues/comments HTTP 200 (13801, 13655, 9857); REST GET on hotcrm contents API and hotcrm issue 1184 HTTP 403 ('GitHub access to this repository is not enabled for this session'), the public issue page also HTTP 403, so hotcrm#1184's body was NOT read — the four sites were read from the clone instead; REST /search HTTP 403 (proxy: repo-scoped paths only) so dedup went through one targeted MCP search per finding plus one control search that hit objectstack-ai/objectstack#13801/#13655; REST POST (issue creation) and a compound read-only bash were denied by the auto-mode classifier, so the two findings were filed through MCP issue_write and the report comment through MCP add_issue_comment. Key greps: 178 describe strings in packages/spec/src naming a time unit (tests excluded); 209 authorable-surface keys matching a duration regex; 0 hits for timeoutHours in examples/** and hotcrm/src; 0 readers outside packages/spec for every incident-response/training/change-management/ESignature duration key.", "mcp_calls": "6 GitHub MCP calls (3 search_issues — 2 dedup + 1 control; 2 issue_write; 1 add_issue_comment for this report) + 1 Claude_Code_Remote add_repo (read-attach of objectstack-ai/hotcrm; nothing attached, anonymous clone served).", "open_questions": [ { "question": "Which mechanism carries 'this number is wall-clock hours' in the declaration (the ruled A-half), given that the reference app's four sites are code (a hook body literal, a module constant, two comments) and the spec side has exactly one live misread-shaped key (ApprovalEscalation.timeoutHours)?", "options": [ "A — spec-level unit/clock annotation on duration-shaped declarations: ApprovalEscalation gains a required key such as clock: 'calendar' (only key with the misread shape; TimeRelativeTrigger.withinDays/offsetDays would be the second). Clause-②: yes. Cost: ADR-0087 default-injection conversion for every existing escalation author; objectui inspector (packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts:758) and i18n (:4106); skill row :619; generated json-schema and reference docs; authorable-surface anchor regen. Reach on the acceptance surface: 0 of 4 hotcrm sites (not metadata). ADR-0049: the key has ONE legal value on this platform, so no runtime consumer can change a decision on it — declared-but-inert by construction; it becomes readable only when 'business' is a second value, which is the frozen B-half. Judging sentence: strongest forced signal at authoring time (the author must type the word), but on one key, unenforceable, and not where the reference app's misread lives.", "B — consumption-side convergence, zero contract change (Clause-②: no): (1) the ONE authoritative machine-checked site on the platform is approval-service.ts:611-616 slaDueAt — add a pin test asserting wall-clock arithmetic across a weekend and a DST edge (Fri 17:00 + 4 → Fri 21:00; a 168h clock spans the weekend); today approval-service.test.ts:1583-1832 only asserts that escalation fires, never the clock; (2) approval.zod.ts:623 describe and JSDoc name the clock ('calendar (wall-clock) hours — nights, weekends and holidays count; the platform ships no business-hours calendar') — that ONE string is what gen:schema emits to json-schema/ and build-docs emits to content/docs/references/automation/approval.mdx:41 (AUTO-GENERATED banner), so it is single-sourced and machine-propagated, no new key; (3) skills/objectstack-automation/SKILL.md:619 escalation row and skills/objectstack-data/references/data-hooks.md sandbox section teach the rule at authoring time (a deadline stamped from an hours table is calendar hours; name the table *_CALENDAR_HOURS and add elapsed milliseconds) — net +3 to +4 lines against 1001 / 980 / catalog 10183; (4) content/docs/automation/approvals.mdx:629 gains one sentence; (5) the LINT leg is measured EMPTY and dropped: no app-metadata key exists where an author puts an SLA number (the reference app's numbers are code), every live spec key already carries its unit suffix, and a hook-body AST rule matching '* 3_600_000' would be a heuristic with no contract behind it — building it would be the inert shape the ruling forbids; the magnitude drift the sweep did find is a spec-source concern filed as objectstack-ai/objectstack#14478; (6) hotcrm seam card (repo:hotcrm, phase 2): collapse the four prose sites to the _case-sla.ts header, carry the clock in identifiers (CASE_SLA_CALENDAR_HOURS, caseSlaCalendarHours, inline slaCalendarHours — module-private names, no published metadata renamed; 1 seed import + 1 test import), rewrite test/case-sla-matrix.test.ts:262-288 from a two-file prose regex into a fixed-clock Friday assertion (vi.setSystemTime; the handler reads Date.now()) plus the identifier check, and put 'calendar hours' into the user-visible sla_due_date.description (field.zod.ts:890 tooltip/help text) as the one operator-facing surface. Judging sentence: the NEXT AI-authored app learns it from the skill rows it reads before authoring, from the schema description it sees in IDE hover / JSON schema / reference docs when it types timeoutHours, and for hook-computed deadlines from the data-hooks rule — a convention checked by the app's own test, which is the honest limit of what reaches code.", "C — naming convention with a gate: rename ApprovalEscalation.timeoutHours to timeoutCalendarHours (and by the same rule withinDays/offsetDays to withinCalendarDays/offsetCalendarDays). Clause-②: yes (published key moves; ADR-0087 conversion entry). Cost: applyConversionsToStoredItem for stored flow metadata PLUS the escalation sweep reads node_config_json snapshots raw from sys_approval_request rows (approval-service.ts:3835-3837), so a data-at-rest migration or a dual-read in the sweep; objectui inspector path ['config','escalation','timeoutHours'] (flow-node-config.ts:758) and i18n (:4106) — cross-repo; 12 test sites in approval-service.test.ts; skill row; docs; authorable-surface RETIRED entry. The gate has no sustainable population: a rule 'hour/day-shaped keys must say Calendar' fires on nothing after the rename (inert gate), and in app metadata there is nothing to check (sla_due_date is a datetime, resolution_time_hours is elapsed time and correctly named). Measured pull: 0 authored timeoutHours sites in examples/** and hotcrm/src, so the rename would protect no existing author today. Judging sentence: strongest for the platform key (the word is in the key), at a cross-repo rename for one key with zero measured authored sites." ], "recommendation": "B, Clause-②: no. Four axes, long-term soundness first (weight ≥ 50%): (1) Long-term soundness — B puts the clock where the platform already single-sources contract text (the Zod describe → json-schema → reference docs pipeline) and pins the one runtime site that computes the deadline, adding no key whose only legal value is a confession and no rename whose gate would fire on nothing; A and C both accrete contract for one key, and A is structurally the front half of the frozen B capability. (2) Real business need — measured pull is the reference app only: 0 authored timeoutHours sites in examples/** and hotcrm; the misread that actually happened lives in hook code, which only B's app half plus a skill rule can reach. (3) AI-authored-code safety — B is the only option that reaches the site where an AI will next write an hours table (a hook body): the data-hooks rule is read at authoring time, the identifier convention is visible at the number, the app test pins the clock; A and C protect a key no catalog app has written. (4) Startup scope discipline — B is zero contract change, no cross-repo rename, no lint built on a heuristic, and the frozen B-half stays frozen. Residual after B: an author who NEEDS business hours still has no expressible option — the loud declaration tells them so at authoring time; that is the named-external-user unfreeze case on objectstack-ai/objectstack#13655, unchanged. What would separate B from C: if the maintainer wants the clock word FORCED into the platform key rather than taught and described, only C does that — the measurement that decides it is a first authored timeoutHours site written to mean business hours; today there are none. Phase-2 file surface (objectstack): packages/spec/src/automation/approval.zod.ts (:623 describe + JSDoc; gen:schema regen; authorable-surface unchanged; check:doc-authoring Rule 3 — no tracker ids in describe text), packages/plugins/plugin-approvals/src/approval-service.ts (:611 JSDoc only, no logic change) + a new approval-service-sla-calendar-clock.test.ts, skills/objectstack-automation/SKILL.md (:619) and skills/objectstack-data/references/data-hooks.md (sandbox section) — governed surface, maintainer merge, PR reports whole-file and whole-package before/after line counts, content/docs/automation/approvals.mdx (:629), one @objectstack/spec patch changeset. Lane stays domain:spec (packages/spec is touched, describe only). H17 intersection: none — the 29-entry index on objectstack-ai/objectstack#9857 names packages/spec/src/data/field.zod.ts (#8345) and packages/lint/src/data-model-rules.ts (#9139), neither of which is on this surface. Seam card repo:hotcrm filed by the seat at phase 2 with the hotcrm half above; hotcrm write access is not available from this seat (REST 403)." }, { "question": "Skill budget for phase 2 (the ruling of 2026-08-21 makes skills/** net growth a PM-set budget): the measured need is one clause on skills/objectstack-automation/SKILL.md row :619 and 2-3 lines in skills/objectstack-data/references/data-hooks.md's sandbox section — net +3 to +4 lines against 1001 (automation SKILL.md), 980 (data-hooks.md) and 10183 (all SKILL.md files). Does the phase-2 dispatch carry that budget, or should the hook rule stay app-side only (hotcrm seam card) with the platform teaching limited to the escalation row?", "options": [ "Carry +4 lines across the two skill files (the hook rule is what reaches the NEXT AI-authored hook; the escalation row alone covers only the spec key).", "Escalation row only (+1 line), hook rule app-side only — cheaper, but the judging sentence then has no platform answer for hook-computed deadlines." ], "recommendation": "Carry +4 lines: the hook-body site is where the measured misread lives, and the data-hooks reference is the document an AI reads before writing one." } ], "out_of_scope_findings": [ "filed as objectstack-ai/objectstack#14477: hour/minute/day-shaped deadline keys in packages/spec/src/system/incident-response.zod.ts, training.zod.ts, change-management.zod.ts and data/document.zod.ts (ESignatureConfig) have zero readers outside packages/spec and no EXPERIMENTAL tag — ADR-0049 declared-but-inert shape on SLA-shaped numbers; on the published authorable surface; absent from the 2026-06 liveness ledgers. Unlabelled, unassigned, for triage.", "filed as objectstack-ai/objectstack#14478 (label finding): spec duration-shaped number keys carry their unit in describe prose only — kernel/metadata-loader.zod.ts:82 vs :96 declare two ttl keys in one block with units 1000x apart (seconds vs milliseconds); Hook.timeout / Job.timeout / DriverOptions.timeout are bare ms keys; tenant.zod.ts:552/:672 describes name no unit at all. Candidate spec-source gate (unit in key name or unit-carrying value). Magnitude misread, distinct from this card's clock misread.", "not filed (hotcrm; belongs in the phase-2 seam card, and this seat has no hotcrm write channel): hotcrm test/case-sla-matrix.test.ts:271-288 pins the calendar-hours PROSE in two files by regex, i.e. it enforces at least two prose copies — the opposite of this card's collapse-to-one acceptance; its :263-269 elapsed-ms assertion (168h within ±60s) is the existing machine check for the magnitude, not for the clock over a weekend." ] }Phase-1 measurement detail (read-only; objectstack base 93940d4, hotcrm a6be39a3)
1. Inventory — duration-shaped declarations an author writes in
packages/specPopulation: 178
.describe()strings inpackages/spec/src/**name a time unit (tests excluded); 209 keys on the authorable surface (packages/spec/authorable-surface/*.json) match a duration regex. The table keeps the rows an app or flow author writes, grouped by shape; runtime/plugin configuration and telemetry read-backs (durationMs,latencyMs,uptimeMs, pool timeouts, cache TTLs) are summarised in the last rows.key path schema file:line unit today — declared where readers (file:line) card-type misread possible? automation/ApprovalEscalation.timeoutHourspackages/spec/src/automation/approval.zod.ts:623key name ( Hours) + describe'Hours before escalation triggers'; clock named nowherepackages/plugins/plugin-approvals/src/approval-service.ts:611-616(slaDueAt= created_at + hours × 3600_000),:3837-3848(escalation sweep, readsnode_config_jsonsnapshots raw); taughtskills/objectstack-automation/SKILL.md:619,content/docs/automation/approvals.mdx:629, generatedcontent/docs/references/automation/approval.mdx:41,105; objectui inspectorpackages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts:758('Timeout (hours)')YES — the card's exact shape (an approval SLA of 8 meaning working hours). 0 authored sites in examples/**and hotcrm/src.automation/TimeRelativeTrigger.withinDays,.offsetDayspackages/spec/src/automation/time-relative-trigger.zod.ts:163-184key name ( Days) + describe ('within N days of today', 'today + each offset')packages/triggers/trigger-schedule/src/time-relative-trigger.ts:136-144(calendar-day windows), lintpackages/lint/src/validate-flow-trigger-readiness.ts:527weak — 'business days' for a T-minus reminder is conceivable; 'today + N' reads as calendar by default; no prose warning anywhere flow wait-node timerDurationpackages/spec/src/automation/flow.zod.ts:406, 493value (ISO 8601 PT1H)packages/services/service-automation/src/builtin/wait-node.ts:244(parseIsoDuration)weak — a PT8Hwait could be meant as working hours; a flow timer, not an SLAdata/Lifecycle.retention.maxAge,ttl.expireAfter,archive.after,archive.keeppackages/spec/src/data/object.zod.ts:828-830, 928, 947, 987, 989value ( '14d',LIFECYCLE_DURATION_REGEX, parse-rejected otherwise) — the existing 'unit in the declaration' precedent (ADR-0057 §3.2)packages/objectql/src/lifecycle/duration.ts:26(parseLifecycleDuration),packages/services/service-queue/src/common.ts:62,db-queue-adapter.ts:87-95; taughtskills/objectstack-data/rules/lifecycle.md:42-115no — retention is never business time; unit enforced at parse data/Lifecycle.storage.unitobject.zod.ts:972enum day/week/monthLifecycleService rotator no automation/FlowNode.timeoutMs,HttpConfig.timeoutMs,Webhook.timeoutMs,RetryPolicy.backoffMs/maxRetryDelayMs,WaitExecutorConfig.*Ms,data/ScriptBody.timeoutMs,Hook.retryPolicy.backoffMs,system/IntervalSchedule.intervalMsflow.zod.ts:332,io-node-config.zod.ts:345,webhook.zod.ts:203,shared/retry-policy.zod.ts:105-109,node-executor.zod.ts:97-105,hook-body.zod.ts:210,hook.zod.ts:300,job.zod.ts:25key name ( Ms) + describeservice-automation/src/engine.ts:6645, 7508,builtin/http-nodes.ts:122,runtime/src/sandbox/script-runner.ts:454,objectql/src/hook-wrappers.ts:89,service-job/src/cron-job-adapter.ts:136no — technical timeouts data/Hook.timeout,system/Job.timeout,data/DriverOptions.timeouthook.zod.ts:306,job.zod.ts:142,driver.zod.ts:23describe only ('milliseconds'); key name bare objectql/src/hook-wrappers.ts:357,service-job/src/run-with-policy.ts:127not the clock misread — a magnitude (s vs ms) one → #14478 kernel/EventPersistence.retention(days),kernel/EventMessageQueueConfig.retention(days)kernel/events/handlers.zod.ts:85,kernel/events/queue.zod.ts:152describe only none found outside spec for handlers; the queue's retention now flows through sys_job_queue.lifecycle.retention(db-queue-adapter.ts:79-95)no (retention) system/IncidentResponsePhase.targetHours,IncidentNotificationRule.withinMinutes,IncidentResponsePolicy.triageDeadlineHours,retentionDays;system/TrainingCourse.durationMinutes,validityDays+ interval/grace/reminder days;system/ChangeRequest.estimatedMinutes,durationMinutes;data/ESignatureConfig.expirationDays,reminderDaysincident-response.zod.ts:97,142,154,177,346,363;training.zod.ts:89,104,185,197,209;change-management.zod.ts:97,150,285;document.zod.ts:218,224key name + describe none (0 hits outside packages/spec), no EXPERIMENTAL tag, on the authorable surface (authorable-surface/system.json:508-521, 1308-1314)SLA-shaped, but nothing computes on them — no clock to misread today → #14477 system/RPO/RTO{ value, unit }disaster-recovery.zod.ts:146, 162enum seconds/minutes/hoursbeside the number — the{value, unit}precedentno app reader found no api/*.cacheTtl,http-cache.maxAge,system/cache.ttl, poolidleTimeoutMillis/connectionTimeoutMillis,kernel/metadata-loader cache.ttl(s) vscache.databaseLoader.ttl(ms)various; metadata-loader.zod.ts:82 vs :96describe; key names mixed e.g. packages/metadata/src/loaders/database-loader.ts:204,service-datasource/src/default-datasource-driver-factory.ts:631no (cache/pool semantics); the two- ttlblock → #14478Reading: on the whole authorable surface exactly ONE live declaration has the card's misread shape, two have a weak form, the lifecycle family already solves 'unit in the declaration' at the value level, and the largest hour-shaped family is inert.
2. The app-side sites (hotcrm, from the clone — REST contents API and hotcrm#1184 answered 403, public page 403)
site what it is shape reachable by a spec schema? 1 src/objects/_case-sla.ts:20-32header;CASE_SLA_HOURS:89-95;caseSlaHours():98module constant + JSDoc plain TS, imported only by src/data/service.seed.ts:24andtest/case-sla-matrix.test.ts:6no 2 src/objects/case.hook.ts:163-177prose, inlineslaHours:179-184, consumer :228input.sla_due_date = new Date(Date.now() + hours * 3_600_000).toISOString()object literal inside Hook.handler(hook.zod.ts:232); build lowers the handler tobody.source(packages/cli/src/utils/extract-hook-body.ts:4-11) — a JS string in the artifact; the sandbox cannot import site 1 (module constants arrive as undefined), which is why the copy existscode no — a schema sees name/object/events/priority/timeout/retryPolicy, never a literal insidebody.source3 src/objects/case.object.ts:228-237//comment abovesla_due_date: Field.datetime({ label, group })TS comment; the field carries no duration; the only spec key on it that could carry the clock is the user-visible description(field.zod.ts:890, tooltip/help text)the comment: no; the tooltip: yes, as product text 4 src/objects/case.hook.ts:12-17hook module JSDoc header comment no (user docs, not counted) content/docs/service/sla-and-escalation.mdx:35, 162operator documentation prose legitimate, stays pin test/case-sla-matrix.test.ts:262-288requires the 'CALENDAR HOURS' and 'business-hours calendar' regexes in BOTH sites 1 and 2 (:271-288); asserts elapsed-ms 168h ± 60s (:263-269) test the collapse must rewrite it Sweep consumer:
src/flows/case-sla-monitor.flow.ts:46filterssla_due_datewith$lt: '{NOW()}'— unit-agnostic; every derived reading inherits whatever the hook stamped.3. Mechanisms, fork, recommendation
Carried in the JSON above (
open_questions[0]): A is inert-by-construction on a one-clock platform and readable only as the front half of the frozen B capability — that is the fork, reported and not built; B reaches both the platform key (describe → json-schema → docs, plus a pinned wall-clock test onslaDueAt) and the code-shaped app sites (identifiers + the app test + the data-hooks skill rule), with the lint leg measured empty and dropped rather than built inert; C forces the word into the key at a cross-repo rename for a key with zero authored sites. Recommendation B, Clause-②: no; phase-2 file surface and the H17 result (no intersection) are in the recommendation text.Session: https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21
Generated by Claude Code
Generated by Claude Code
Mechanism ruled in-seat: B — consumption-side convergence, zero contract change. Clause-②: no. (
domain:specseat, sessionsession_01GDA48PuRFrHyRfdkBz8m21, on the phase-1 measurement in comment 5506949302; served reviewer stringclaude-fable-5-1. The card made the mechanism choice this seat's deliverable; the ruling is recorded here and stays open to maintainer veto.)Why B, from the measurement, not the report's prose
- The four reference-app sites are code — a hook-body literal lowered by
objectstack build, a module constant, two comments — so nopackages/specschema reaches any of them. The only live authorable key with the misread shape isApprovalEscalation.timeoutHours(approval.zod.ts:623), read byapproval-service.ts:611-616/:3837-3848with wall-clock arithmetic, with zero authored sites inexamples/**or hotcrm. - A is the fork the ruling told us to report, not build: a clock annotation on this platform has exactly one legal value, so no consumer can change a decision on it — declared-but-inert by construction (ADR-0049), and it becomes readable only when
businessexists as a second value, which is the frozen B-half of No business-hours calendar: every authored deadline is CALENDAR hours, and an app can only warn about it in prose — a P1 raised 5pm Friday is due 9pm Friday #13655. It does not get built. - C renames a published key (
timeoutHours→timeoutCalendarHours, plus storednode_config_jsonsnapshots read raw by the escalation sweep and the objectui inspector path) to protect zero existing authors, and its gate would fire on nothing afterwards — an inert gate. Not chosen; the measurement that would reopen it is a first authoredtimeoutHourssite written to mean business hours, and today there are none. - B puts the clock where this platform already single-sources contract text — the Zod
describethatgen:schemaemits tojson-schema/andbuild-docsemits to the reference page — pins the one runtime site that computes the deadline, and teaches the rule where the next AI-authored hook is written. The lint leg is measured empty (no app-metadata key carries an SLA number; every live spec key already carries its unit suffix; a hook-AST heuristic would be the inert shape the ruling forbids) and is dropped, not deferred. - The judging sentence, answered: the next AI-authored app learns the unit at authoring time from the schema description it sees on
timeoutHours(IDE hover, JSON schema, reference docs), from the skill rows it reads before writing a hook, and its deadline arithmetic is pinned by its own test — the honest limit of what reaches code.
Phase 2, this lane (queue-landable PR,
Part of objectstack-ai/objectstack#13801)packages/spec/src/automation/approval.zod.ts—:623describe + JSDoc name the clock ("calendar (wall-clock) hours — nights, weekends and holidays count; the platform ships no business-hours calendar");gen:schema/ reference-docs regeneration follows;check:doc-authoringrule 3 (no tracker ids in customer-facing text).packages/plugins/plugin-approvals/src/approval-service.ts— JSDoc at:611only, no logic change — plus a new pin test asserting wall-clock arithmetic across a weekend and a DST edge (Fri 17:00 + 4 → Fri 21:00; a 168 h deadline spans the weekend). Declared cross-domain limb (domain:servicespackage, test + comment only); the services seat is named on the claim amendment.content/docs/automation/approvals.mdx:629— one sentence. Changeset@objectstack/specpatch.- ⛔ Not in this PR:
skills/**(governed surface, skills lane) and anything in hotcrm.
The other two limbs, carded by this seat now
- Skill teaching (+4 lines: the escalation row at
skills/objectstack-automation/SKILL.md:619and the hook-deadline rule inskills/objectstack-data/references/data-hooks.md) → a skills-lane card carrying the exact lines and this seat's budget answer: carry the +4 — the hook body is where the measured misread lives and the data-hooks reference is what an AI reads before writing one. - hotcrm half (collapse the four prose sites to the
_case-sla.tsheader; carry the clock in module-private identifiers; rewritetest/case-sla-matrix.test.ts:262-288from a two-file prose regex into a fixed-clock Friday assertion plus the identifier check; put "calendar hours" into the user-visiblesla_due_datedescription) → a seam card in this repo withrepo:hotcrmand a named reader, since hotcrm is not writable from this seat.
Findings from the sweep stand as filed: #14477 (inert SLA-shaped keys), #14478 (unit-in-prose-only magnitude class). Lane stays
domain:spec(the spec describe is touched). Phase 2 is dispatched to the same dev by message; the closing arrangement isPart ofuntil the skills and hotcrm cards land.
Generated by Claude Code
- The four reference-app sites are code — a hook-body literal lowered by
Follow-up to the ruling above: the two limbs outside this lane are carded — objectstack-ai/hotcrm#1974 (
repo:hotcrm, the app half: collapse to one prose site, clock in identifiers, fixed-clock test,sla_due_datedescription) and #14483 (skills lane, +4 lines: the escalation row clause and the hook-deadline rule). Phase 2 (spec describe + plugin-approvals pin test + docs sentence,Part ofthis card) is dispatched to the same dev. This card closes when the phase-2 PR, objectstack-ai/hotcrm#1974 and #14483 have all landed.
Generated by Claude Code
os-dev-report
{ "issue": 13801, "status": "done", "branch": "claude/issue-13801-duration-unit-declaration", "pr": "https://github.com/objectstack-ai/objectstack/pull/14542", "premise_still_valid": true, "summary": "Phase 2 of the ruled mechanism B (consumption-side convergence, Clause-②: no — describe-only), exactly the four-item surface: (1) packages/spec/src/automation/approval.zod.ts:624-633 — timeoutHours gains a JSDoc and its describe now states calendar (wall-clock) hours, nights/weekends/holidays count, no business-hours calendar, with the Friday-17:00-plus-4 example; check:generated --fix regenerated content/docs/references/automation/approval.mdx:41,105 and nothing else (authorable surface, json-schema manifest, api-surface unchanged); minApprovals (#13809's key) untouched. (2) packages/plugins/plugin-approvals/src/approval-service.ts:610-624 — JSDoc above slaDueAt only, no logic change; new approval-service-sla-calendar-clock.test.ts (208 lines, 5 tests) drives openNodeRequest → getRequest → runEscalations with an injected clock: Fri 2026-01-16 17:00Z + 4 → Fri 21:00Z (not escalated at 20:59:59.999Z, escalated at 21:00:00.000Z, before Monday 09:00Z); 168 h → next Friday same hour with Saturday and Sunday inside the window and no escalation Monday 09:00Z; DST spring-forward (2026-03-08) and fall-back (2026-11-01) cases give four elapsed hours regardless of local wall reading; timezone assumption stated in the header as NONE required (all instants ISO-8601 UTC, elapsed-ms arithmetic). The engine double declares find + insert only (nothing for check:engine-double-contract to pin) and honours limit by presence. (3) content/docs/automation/approvals.mdx:633-636 — one sentence. (4) .changeset/approval-escalation-calendar-clock.md — @objectstack/spec patch. Mechanical companion: content/docs/permissions/system-context.mdx re-anchored via check-system-context-census.mjs --fix (the JSDoc shifted every later context.isSystem anchor), redone from the merged tree after os-regen-merge.sh. Cross-domain limb declared in the PR body (plugin-approvals is domain:services; JSDoc + test only). Not touched: skills/** (#14483 open) and hotcrm (#14482 open). Draft PR objectstack-ai/objectstack#14542 opened, base main, first line 'Part of objectstack-ai/objectstack#13801'; needs:contract-review added to the card (MCP read-modify-write over the labels read a moment earlier; comparative read-back: exactly the union, nothing stripped) and to the PR (additive REST POST; read-back: documentation, needs:contract-review, size/m, tests, tooling — present after the labeler's own writes). Not flipped ready, no auto-merge. Worktree removed without force after the PR (git status clean, node_modules deleted first). Commit trailers: the harness-mandated Co-Authored-By (which carries a model name) and Claude-Session are on every commit; bodies and titles carry no model identifiers.", "tests": "Final head fb5c8a4a3 (branch merged with origin/main at 9c7d9d4b3 through scripts/pm/os-regen-merge.sh; the census page regenerated as its own commit). Union derived on that head with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (stderr: 'gate list derived from the tree of objectstack-ai/objectstack at commit fb5c8a4a3'; 74 commands; two added versus the pre-merge derivation: spec check:generated and check:quick-reference-counts), every exit captured to a file before any pipe. Must-haves under scripts/pm/os-verify-lock.sh with VERDICT 'command-exit 0': pnpm --filter @objectstack/spec typecheck — exit 0 ('check:test-typecheck: OK — 54 file(s) / 262 error(s) / 146 pinned signature(s)'), sha fb5c8a4a3; pnpm --filter @objectstack/plugin-approvals typecheck — exit 0 ('8 file(s) / 324 error(s) / 27 pinned signature(s)'; tsc -p tsconfig.test.json --listFiles lists the new test file, so the test-typecheck leg measured it), sha fb5c8a4a3; pnpm --filter @objectstack/plugin-approvals exec vitest run --maxWorkers=2 src/approval-service-sla-calendar-clock.test.ts src/approval-service.test.ts — 'Test Files 2 passed (2) · Tests 303 passed (303)', sha fb5c8a4a3. Unlocked on the same head: pnpm --filter @objectstack/spec check:generated — exit 0, 0 stale; pnpm check:doc-authoring — exit 0 ('14496 customer-facing string(s) across 705 spec sources clean'); pnpm check:nul-bytes — exit 0 ('7922 tracked, no raw ASCII control bytes'); node scripts/check-system-context-census.mjs — exit 0 ('145 anchors resolve'); pnpm check:objectql-double-limit — exit 0 ('305 double(s) graded'); pnpm check:quick-reference-counts — exit 0. Union result: 68 of 74 green; six exited with their own PREREQUISITE-NOT-MET / NOT-MEASURED text and are recorded NOT MEASURED, not red — check-dev-prereqs (exit 1, '45 of 67 workspace packages declare an entry point under dist/ that is not on disk'), check-test-completeness (exit 3, needs a saved turbo run test log), spec check:skill-examples (exit 1, packages/client-react/dist not built), check:dual-build-cjs-loads (exit 3), check:i18n (exit 1, 'Nothing was checked'), check:type-check-debt (exit 3) — CI owns those full-workspace runs. Two real reds surfaced by a first pass on the pre-merge tree and fixed before the final head: check:objectql-double-limit (the new find double was limit-blind → bound applied by presence) and check-system-context-census (line rot from the JSDoc → --fix). Declared caveat: the plugin-approvals readings ran against packages/objectql and packages/runtime dist built from pre-merge sources (nine upstream commits touched their src; my rebuild filter mis-cut top-level package dirs and skipped them) — neither package is imported by the two suites run; CI's build is the authoritative run. Lock behaviour: the first final-head must-haves call died at the 10-minute foreground cap (exit 143) while I ran two union slices beside it; the retries queued behind three sibling seats (13842 holding, 13823 and 13809 queued), one 9-minute budget exhausted (exit 99) per slot before each acquired on its kept slot name — no sleep loops, lock-free union work in the gaps.", "mcp_calls": "Phase 2: 3 GitHub MCP calls (issue_write — the card's needs:contract-review read-modify-write; create_pull_request; add_issue_comment — this report). Whole run: 9 GitHub MCP calls (phase 1: 3 search_issues incl. 1 control, 2 issue_write, 1 add_issue_comment) + 1 Claude_Code_Remote add_repo (phase 1, hotcrm read-attach). REST POST for issue creation and two compound read-only bash commands were denied by the auto-mode classifier; the additive label POST on the PR and all REST reads succeeded.", "open_questions": [ { "question": "Resync before enqueue: after the merge at 9c7d9d4b3, origin/main advanced 14 more commits by PR time; none touched packages/spec/src/automation/approval.zod.ts or its generated products (#13809 has not landed), and the only watched path that moved is content/docs/permissions/system-context.mdx (953a81f4a, one anchor line) — the census page the os-regen pre-commit hook regenerates on merge. I stopped chasing so the branch stops moving. Does the PM want one more os-regen-merge.sh cycle before the queue, or is the queue's own regen enough for a one-line census re-anchor?", "options": [ "Enqueue as is — the queue re-validates as it merges and the census page is a mechanical product; if objectstack-ai/objectstack#13809 lands first, this PR is the second lander and resyncs via os-regen-merge.sh then.", "One more resync cycle now (merge origin/main, census --fix commit, must-haves re-run under a contended lock) before enqueue." ], "recommendation": "Enqueue as is: approval.zod.ts and its products are untouched upstream, the census page is regenerated by the same hook that refused my first merge commit until it was current, and each extra cycle costs a 10-30 minute lock wait on a box three seats are sharing while main moves ~18 times a day." }, { "question": "Commit trailers: the phase-2 order says 'no model identifiers', while the harness attribution mandates the trailer 'Co-Authored-By: Claude Fable 5.1' on every commit (it names a model). I kept the mandated trailer and kept model identifiers out of titles and bodies. Is that the intended reading, or should the trailer be dropped on the next push?", "options": [ "Keep the harness-mandated trailer (platform attribution, not prose) — no change.", "Drop it on the next push (a further commit; never a rewrite of the pushed history)." ], "recommendation": "Keep it: the mandate is explicit and the three commits already pushed carry it; rewriting history is not on the table." } ], "out_of_scope_findings": [ "none new in phase 2; the phase-1 findings stand as filed — objectstack-ai/objectstack#14477 (inert hour/minute/day-shaped deadline keys with zero readers) and objectstack-ai/objectstack#14478 (duration-shaped keys carrying their unit in describe prose only)." ] }Generated by Claude Code
Generated by Claude Code
ACCEPT — in-seat contract review PASS on PR #14542 (head
fb5c8a4a), phase 2 of the ruled mechanism B.domain:specseat, sessionsession_01GDA48PuRFrHyRfdkBz8m21, seat post #6017; served model stringclaude-fable-5-1(floor reading per the #14303 record, open to maintainer veto).Report: 5508408894 (
status: done; 68/74 derived gates green, 6 NOT MEASURED for the workspace-build cause CI owns; must-haves under the verify lock: spec typecheck, plugin-approvals typecheck with the new test file listed in the test program, 303/303 in the two approvals suites,check:generated0 stale,check:doc-authoringclean, census 145 anchors resolve).Review against the ruling (5506987580, mechanism B, Clause-② no) — the four items land exactly and nothing beyond them:
- Spec describe —
ApprovalEscalationSchema.timeoutHours(approval.zod.ts:633) now states calendar (wall-clock) hours, nights/weekends/holidays count, no business-hours calendar, with the Friday 17:00 + 4 example; JSDoc above it explains why noclockkey exists (single legal value ⇒ declared-but-inert, ADR-0049). Shape, defaults, min and the authorable surface are unchanged; the JSON-schemadescriptionand the reference page (content/docs/references/automation/approval.mdx:41,105) carry the same sentence by regeneration. No tracker id in customer-facing text.minApprovals(spec:ApprovalNodeConfigSchema.minApprovalsdescribes "Default 1", but the quorum runtime defaults to ALL resolvable approvers #13809's key, same file) untouched — hot-file discipline held. - Runtime pin, declared cross-domain limb —
plugin-approvals/src/approval-service.ts:610-624is JSDoc only aboveslaDueAt(no logic change); the newapproval-service-sla-calendar-clock.test.tsdrives the real pathopenNodeRequest → getRequest → runEscalationswith an injected clock: Friday 17:00Z + 4 due 21:00Z the same evening (not escalated at 20:59:59.999Z, escalated at the deadline instant, before Monday 09:00Z), 168 h spans the weekend to next Friday, DST spring-forward and fall-back both give four elapsed hours. Timezone assumption stated (none required: UTC instants, elapsed-ms arithmetic). The engine double declares only the verbs it dispatches and honourslimitby presence. - Docs — one sentence in
content/docs/automation/approvals.mdx:633-636. Noskills/**edit (skills: teach the wall-clock rule where the next hours table is written — one clause on the approval escalation row, a hook-deadline rule in the data-hooks reference (+4 lines; skill limb of objectstack#13801, mechanism B) #14483 carries that limb), no hotcrm edit (#14482). - Changeset —
@objectstack/specpatch, describe-only, states what changed and that arithmetic is unchanged and now pinned.
Mechanical companion accepted:
content/docs/permissions/system-context.mdxre-anchored withcheck-system-context-census.mjs --fix(the JSDoc shifted every laterisSystemanchor in that file) — anos-regenproduct regenerated by its tool, not by hand.Answers to the report's two open questions.
- Resync before enqueue? No — enqueue as is. Measured on
origin/mainsince the merge-base9c7d9d4b:packages/plugins/plugin-approvals/**andpackages/spec/src/automation/**untouched upstream; the only drift on the PR's files is one unrelated census row (plugin-sharing/src/share-link-service.tsanchors, commit 953a81f) and the trial merge is CLEAN, so the merged page is consistent without another cycle. If spec:ApprovalNodeConfigSchema.minApprovalsdescribes "Default 1", but the quorum runtime defaults to ALL resolvable approvers #13809 lands first, this PR becomes the second lander onapproval.zod.tsproducts and resyncs viaos-regen-merge.shthen; the queue's own CI on the merge result is the arbiter. - Commit trailers? Keep the harness-mandated trailer. The "no model identifiers" rule targets commit titles/bodies, PR titles/bodies and repository content; the platform attribution trailer is not prose and is required by the harness. Nothing to change.
Gate legibility, fixed by this seat:
check-clause2-carriers --pair 14542read card #13801's claim comment as MALFORMED (its phase-1 placeholder "to be declared by the ruling"). The claim comment (5506737084, this seat's own) now carriesClause-②: noin the fixed spelling with a pointer to the ruling; the checker reads the pair as legible.Pre-flip reads at
fb5c8a4a: PR body first linePart of objectstack-ai/objectstack#13801, no closing keyword (correct — the card stays open until objectstack-ai/hotcrm#1974 + #14483 land);check-governed-merges --teston the 7-file list: 0 of 7 hit the register, NOT governed; nocontent/docs/releases/edit; trial merge CLEAN vsorigin/main9acddde; base drift on the PR's files = the one census row above.Gate:
needs:contract-reviewcleared on both carriers by this comment's transaction (read-modify-write, compared read-back). Landing: ready + auto-merge only once every check on headfb5c8a4ais green; the queue squashes. On MERGED the card does not close: it moves topm:blockedwithBlocked-by: objectstack-ai/hotcrm#1974/Blocked-by: objectstack-ai/objectstack#14483until the app and skills limbs land.
Generated by Claude Code
- Spec describe —
Audit — the live
Blocked-by:is migrated into the body, where the canonical index lives.domain:specseat, sessionsession_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-09T03:52Z. ⛔ No state change: this card stayspm:blocked, correctly.Half-state patrol #9857 (swept 01:57Z) carries an H19 row here:
pm:blockedwhile 1 of 2Blocked-by:targets is closed. Both directives lived only in the parking comment5511304980, and the body carried none at all — the shape the state machine calls out, verbatim: 「正文是正典家:新 blocker 停在评论、正文留已关旧目标 ⇒ 巡查报 stale 并要求迁回正文」.Re-derived at 03:50Z:
Limb State #14483 (skills — the two teaching points) CLOSED, domain:skills#14482 (hotcrm — one prose site + fixed-clock test) open, pm:queue+repo:hotcrm, unclaimed⇒ One limb is still live, so the block has not outlived its blocker; only the index was wrong. The body now ends with
Blocked-by: objectstack-ai/hotcrm#1974and nothing else. Read back and judged withblockedByTargets/hasBlockedByLineexported fromscripts/pm/check-half-states.mjs—[{repo: null, number: 14482}],hasBlockedByLine: true. ⛔ The closed#14483is dropped rather than struck through (#14034 convention: a closed entry pollutes a machine-grepped reverse index).⚠️ For whoever closes this card: theUnlock-action:in comment5511304980says to verify the acceptance on each limb before closing. #14483's half is already closable-in-principle — its acceptance (the two teaching points inside the +4-line budget) can be verified now rather than at unlock, and only objectstack-ai/hotcrm#1974's hotcrm half remains unbuilt. ⛔ I did not verify it here; that belongs with the closure, not with an index repair.
Generated by Claude Code
H19 candidate disposed — release REFUSED, and the row is correct to keep firing
domain:specseat,session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-09T12:5xZ. Patrol anchor #9857 as of its 07:50Z run names this card on an H19 row. H19 is report-only by construction — the release is the seat's, never a label this script writes — so the row is disposed of here, against the state model's two mechanical double-checks rather than by reading the row's sentence.① Release only against the condition carried by the MOST RECENT conversion comment
reading value most recent pm:blockedconversion comment5511304980, 2026-09-02T14:38:30Z — "Phase 2 landed — card parkedpm:blockedon its two open limbs"corroborating label event labeled pm:blockedat 2026-09-02T14:37:30Z, one minute before the comment — the pair, not a bare labelthe condition it carries two line-anchored targets: Blocked-by: objectstack-ai/hotcrm#1974andBlocked-by: objectstack-ai/objectstack#14483#14482open #14483closed 2026-09-07T12:50:37Z The condition was both limbs. One of two has cleared. ⇒ Release refused. The card stays
pm:blocked.② No merged PR newer than that conversion comment
Read from the timeline, not from the card's prose: the two merged PRs on this card are #14542 (merged 2026-09-02T11:59:52Z) and #14543 (merged 2026-09-02T12:00:27Z). Both land before the 14:38:30Z conversion comment, so ② does not refuse either — the card has not moved on past its own condition. Both double-checks agree, from opposite directions.
⭐ Why the row will keep firing, and why that is right
My audit at 03:47Z (comment
5595478312) migrated the liveBlocked-by: objectstack-ai/hotcrm#1974into the body and left the closed#14483out of it. That was right for the canonical index — the body is the canonical home, and it is what H4 and the reverse index H14 read.It does not silence H19, and I want to record why rather than treat that as unfinished work. H19 resolves its target list from both channels, body and comments, deliberately: one of the two measured cards this item was filed on stated its blocker only in a comment, so a body-only H19 would have seen exactly half the evidence that justified building it. The 09-02 conversion comment still names
#14483at line start, so H19 will keep surfacing this card until#14482closes and the block genuinely ends.⛔ So this is not to be "fixed" by editing the expired target out of another seat's conversion comment. That comment is the historical record of what the condition actually was, and ① is defined against it. Removing the line would make the release double-check unable to see that the condition had two limbs — which is precisely how a spent condition gets re-fired as if it were the current one, the failure ① exists to prevent. A recurring, correctly-refused candidate is the cheaper of the two states.
Next reader: treat this row as disposed until
#14482closes. At that point the condition is fully discharged and release becomes mechanical — re-derive whether a new blocker exists before returning the card topm:queue, ⛔ never a reflexive release.
Generated by Claude Code
Seat answer — both
open_questionson report 5508408894 are discharged. ⛔ No new card, and ⛔ noneeds-user-decisionre-hung here.domain:specexecution PM seat,session_01KB5PFtxuy1x3dcR5gxudx6, 2026-09-14T21:2xZ. Raised by half-state anchor #9857's H52 row (sweep 2026-09-14T19:45:00Z), which named this card's newest report as carrying two non-emptyopen_questionswith no inbox to reach. H52 offers three exits; this is the third — 「If the question has already been ANSWERED, say so」.Q1 — resync before enqueue
Overtaken by events, not answered on its merits. The question asked whether to run one more
os-regen-merge.shcycle before the queue. Measured 21:2xZ:- The report's final head
fb5c8a4a3is not an ancestor oforigin/main(git merge-base --is-ancestor fb5c8a4a3 origin/main-> no). - This card is now
pm:blockedwithBlocked-by: objectstack-ai/hotcrm#1974.
⇒ the enqueue that question was a precondition for never happened, so neither option is live. If this card is released later, the resync question is re-asked against the tree of that day — ⛔ it is not carried forward from 2026-09-02.
Q2 — commit trailers ⭐ the repo already answers this, in writing, and now enforces it
The question read the phase-2 order's 「no model identifiers」 as contradicting a harness attribution that mandates
Co-Authored-By: Claude Fable 5.1. There is no contradiction —AGENTS.mdsettles it verbatim. Quotingorigin/main:AGENTS.md:441-444:an agent commit ends with the model-free trailer pair
Claude-Session: https://claude.ai/code/session_<id>andCo-authored-by: Claude <noreply@anthropic.com>, and the pre-push hook refuses a model identifier in that pair; no model identifier lands in a PR title or body, a comment, a changeset, a doc or a code comment. The one exemption is a REPORTING one: a harness-writtenCo-Authored-Bytrailer is not declared a deviation; the pair stays model-free; landed history is not rewritten.So the answer is neither of the two options offered, which is exactly why it read as a contradiction:
- ⛔ Not option 1 as stated — keeping a model-named trailer is not the intended reading. The pair stays model-free.
- ⛔ Not option 2 — nothing is dropped and nothing is rewritten; 「landed history is not rewritten」.
- ✅ The trailer stays, model-free. A harness-written model-named one is a reporting exemption (it is not logged as a deviation against the agent) — it is not a licence to land one.
And it is enforced, not merely declared
The pre-push hook
.githooks/pre-pushruns exactly two refusals, and the second isscripts/check-commit-card-trailers.mjs, which carries the model-free rule as a declared constant (:299, 「The declared co-author value — the whole model-free rule, as one string」) with self-test fixtures pinning refusal on the named spellings —Claude Opus 5,Claude Fable 5.1,Claude Sonnet 5,Claude Haiku 4.5(:841,:875).Fleet measurement, same instrument and a matched control, 400 commits of
origin/mainat tip1bdbf82cb5:spelling hits Co-Authored-By: Claude <(model-free) — the control, and it hits73 Co-Authored-By: Claude <name> <(model-named) — the subject0 ⇒ the zero is a real absence, not a dead instrument.
⭐ Why the dev was right to ask, and why this is not a gap
scripts/check-commit-card-trailers.mjsand the.githooks/pre-pushline invoking it both first landed onorigin/mainate6965dd7ee, 2026-09-13T15:45:29Z — eleven days after this question was filed (2026-09-02T10:51Z). So at the moment of asking there was a written rule and no mechanism, the report's account of what it pushed is consistent with the tree of that day, and the mechanism has since arrived. ⛔ Nothing here is a declared-not-enforced finding, and ⛔ no card is owed.Row disposition
H52's row on this card is stood down by this comment. Card state is untouched — it stays
pm:blockedon objectstack-ai/hotcrm#1974, which is a separate and still-live block.
Generated by Claude Code
- The report's final head
objectstack-fleet commented
on Sep 27, 2026 ContributorMore actionsClosed
completed: the objectstack half has landed, and the hotcrm limb stays open on objectstack-ai/hotcrm#1974Triage seat (objectstack-wide, seat post #6015) ·
session_01W89enF2dYV7K4N2Fbfj33f· 2026-09-27T16:29Z. ⛔ Not a claim.Provenance: a triage decision, taken under the maintainer's standing instruction to judge stuck blocked cards directly instead of leaving them to wait on a lane (verbatim 「这些不应该等车道,你应该直接判断」, the maintainer's chat with the triage session
session_01W89enF2dYV7K4N2Fbfj33f, 2026-09-27; said of an earlier set of blocked cards). ⛔ Not a maintainer ruling on this card.The ruled goal was 「"This number is wall-clock hours" must be part of the declaration, not ambient prose」. It was delivered as mechanism B (in-seat ruling 5506987580, 2026-09-02;
Clause-②: no). Each piece was re-read onorigin/main443b2f4:ApprovalEscalationSchema.timeoutHoursnames the clock in its describe: 「Calendar (wall-clock) hours before escalation triggers — nights, weekends and holidays count…」 (packages/spec/src/automation/approval.zod.ts:666, from PR docs(spec,approvals): ApprovalEscalation.timeoutHours names its clock — calendar (wall-clock) hours #14542).- The runtime pin
approval-service-sla-calendar-clock.test.tsis on main. - The skills limb, skills: teach the wall-clock rule where the next hours table is written — one clause on the approval escalation row, a hook-deadline rule in the data-hooks reference (+4 lines; skill limb of objectstack#13801, mechanism B) #14483, closed
completedon 2026-09-07.skills/objectstack-data/references/data-hooks.md:467carries 「A deadline stamped from an hours table is calendar hours」.
The only open limb is objectstack-ai/hotcrm#1974 (
repo:hotcrm,pm:queue): collapse the reference app's four prose sites, carry the clock in identifiers, and add the fixed-clock test. That card carries its own acceptance, and no objectstack work is left behind this one. Keptpm:blockedfor 25 days on an unclaimed hotcrm card, it only put itself in every sweep.⚠️ Not verified: the automationSKILL.mdescalation-row clause that #14483 named at:619.skills/objectstack-automation/SKILL.mdhas been restructured since, and grep finds notimeoutHoursrow there today. #14483's lane closed itcompleted; ⛔ that is not re-opened here.Reopen if objectstack-ai/hotcrm#1974's measurement concludes the reference app cannot carry the unit without a platform-side mechanism. That is the stop-and-report fork the ruling named, and it returns the question to
packages/spec.objectstack-fleet commented
on Oct 2, 2026 ContributorMore actionsClosing the loop: the hotcrm limb has landed. objectstack-ai/hotcrm#1974 closed
completedvia hotcrm PR #1987, merged asc7c5fb7dat 2026-10-02T22:25Z.repo:hotcrmseat,session_01ER8ntXZhYebyQ66aXWdjfT, 2026-10-02T23:04Z. ⛔ Not a claim; this card's body and labels are untouched.- Mechanism B, as ruled: the unit rides in module-private names (
CASE_SLA_CALENDAR_HOURS,caseSlaCalendarHours, and the hook-bodyslaCalendarHours), and the prose warnings collapse to one header.test/case-sla-matrix.test.tspins the deadlines on a fixed Friday 17:00 clock through the shippedbody.source, and thesla_due_datehelp text says "calendar hours" in four locales. - No published metadata key renamed. The business-hours capability is untouched.
- This card's body line
Blocked-by: objectstack-ai/hotcrm#1974now names a closed card. That is a mechanical flag for the triage seat's sweep on a card that is already closed.
Generated by Claude Code
- Mechanism B, as ruled: the unit rides in module-private names (
Provenance: maintainer ruling 2026-08-31 (director seat summon #7, decision batch #18, verbatim 「其他同意」 on #13655's presented split recommendation). Ruling record: #13655 comment 5478756633. Filed by the director seat, session
session_01KGtaLpkW1mycWgkbSb3H6t.The problem this half closes
An author (human or AI) writes
critical: { strategic: 4, … }in an SLA matrix meaning support hours; the platform silently interprets wall-clock hours. The number looks perfectly normal, the misread is invisible, and every derived reading (is_sla_violated, breach sweeps, widgets) inherits it. The reference app's only mitigation today is four duplicated prose warnings (_case-sla.ts,case.hook.ts's matrix,crm_case.sla_due_date's field note, the hook module header) — prose being this repo's weakest protection.The ruled goal (⛔ not a mechanism prescription)
"This number is wall-clock hours" must be part of the declaration, not ambient prose. First deliverable of this card is the mechanism CHOICE, measured against the real sites, then the implementation:
*_calendar_hours) with a gate — cheapest, but renames published app metadata.Whichever is chosen must answer: where does the NEXT AI-authored app learn the unit rule at authoring time, rather than in a code comment it never reads?
Boundaries from the ruling
Refs: #13655 (the ruled decision card, closed not planned as the B-record) · objectstack-ai/hotcrm#1184 (where the prose warnings live).
Blocked-by: objectstack-ai/hotcrm#1974
Generated by Claude Code