Declare the triggers capability so the app's flows actually fire - #72

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-68-declare-triggers
Sep 1, 2026
Merged

Declare the triggers capability so the app's flows actually fire#72
os-warren merged 1 commit into
mainfrom
claude/issue-68-declare-triggers

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#68

objectstack.config.ts declared requires: ['automation', 'hierarchy-security']. automation gives the app a flow engine; it registers no trigger. Every flow in the app was inert — #33's assignment fan-out never fanned out, #70's three reminder sweeps never swept — while all four gates stayed green.

Verified on a real boot

PORT=3117 pnpm start, @objectstack/cli 17.2.0, both runs on this branch's tree.

Before (at e3d6c7f, requires without triggers):

 Plugins: 35 loaded
Flows: 4 flow(s) 0 bound to triggers
⚠ flow 'duly_assignment_fanout' declares a 'record_change' trigger but is NOT bound —
no 'record_change' trigger is registered — add requires: ['triggers']
(record_change/schedule/time_relative/api ship in @objectstack/trigger-*)
⚠ flow 'duly_task_lead_time_reminder' declares a 'time_relative' trigger but is NOT bound — …
⚠ flow 'duly_task_due_soon_reminder' declares a 'time_relative' trigger but is NOT bound — …
⚠ flow 'duly_task_overdue_owner_escalation' declares a 'time_relative' trigger but is NOT bound — …
⚠ Boot diagnostics — 9 warnings logged during startup:

After (at 52afd5c, this branch's head):

 Plugins: 39 loaded
… AutomationServicePlugin, RecordChangeTriggerPlugin, ScheduleTriggerPlugin,
TimeRelativeTriggerPlugin, ApiTriggerPlugin, …
Flows: 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api)
⚠ Boot diagnostics — 5 warnings logged during startup:

Zero of four → four of four. No NOT bound warning survives; the four [Automation] boot diagnostics are gone (9 → 5), and the four trigger plugins now appear in the loaded list.

One token covers all four kinds — no second declaration

The warning names @objectstack/trigger-* plural, so this was worth establishing rather than assuming. It is a single declaration:

  • triggers is the only trigger entry in PLATFORM_CAPABILITY_TOKENS (28 tokens; nothing else matches /trigger/i).
  • The CLI's capability map keys it to @objectstack/trigger-record-change plus three extrasScheduleTriggerPluginandTimeRelativeTriggerPlugin (both from @objectstack/trigger-schedule) and ApiTriggerPlugin (from @objectstack/trigger-api).
  • PLATFORM_CAPABILITY_PROVIDERS.triggers.edition is open and the packages are already on disk transitively, so nothing needs installing.
  • The sweeps also need the job service; job is in PLATFORM_ALWAYS_ON_CAPABILITIES and mounts whether or not it is named.

The boot line confirms it empirically: one token, (record_change, schedule, time_relative, api).

The regression guard

test/trigger-capability.test.ts (6 tests) goes red if triggers is ever dropped while any flow declares a trigger. It does not restate platform facts — it re-derives them each run from the platform's own tables, so it reports a change instead of going stale:

assertionbreaks when
requires contains triggersthe token is dropped — the #68 defect
requires contains automationthe engine is dropped — same silence, one layer down
non-vacuity: flows exist and one is detected as trigger-launchedthe detector stops matching how flows are authored, so the guard would pass by finding nothing
isKnownPlatformCapability('triggers')the vocabulary renames the token
exactly one /trigger/i token, edition: 'open'the vocabulary is split — the app would then need the new token(s) too
every flow type enum member is classifiedthe enum grows a member nobody has judged trigger-launched or not

Trigger detection ORs the two routes the engine and @objectstack/lint both use — the flow's type, or a start node carrying triggerType / timeRelative / schedule. The type set is stated as its complement (screen, autolaunched are the non-trigger ones), so a future enum member demands the capability rather than being waved through.

Ablation — the guard was proven to fail

Committed first, then mutated and measured in one shell invocation under a restoring trap; the mutation was confirmed on disk by grep counts on both the injected and the removed text (1/0 → flipped) plus a non-empty git diff --stat, not by the editor's exit code.

Dropping 'triggers' from requires:

test/trigger-capability.test.ts 1 failed | 5 passed (6)
whole suite 1 failed | 18 passed (19) ← 511 tests, only the new one catches it
pnpm validate EXIT=0 "✓ Validation passed" · "Logic: 4 Flows"
AssertionError: 4 flow(s) declare a trigger — duly_assignment_fanout,
duly_task_lead_time_reminder, duly_task_due_soon_reminder,
duly_task_overdue_owner_escalation — but objectstack.config.ts does not declare
'triggers' in `requires`. … expected [ 'automation', 'hierarchy-security' ] to
include 'triggers'

Exactly one of the six failed, and the other 18 test files stayed green — which is the measured restatement of #68's complaint: the existing suite is blind to this, and validate still exits 0 while every flow is dark. No rebuild step is involved: vitest transforms the config and src/ TypeScript directly, so there is no dist/ to go stale between the mutation and the measurement.

Why nothing caught this — filed upstream as objectstack-ai/objectstack#14153

The card asked whether the asymmetry is real. It is, and it is one predicate wide:

  • defineStack runs exactly two validators that read requires: validateKnownCapabilities (typo guard) and validateHierarchyScopeCapability, which throws on a hierarchy scope declared without its capability. There is no third.
  • @objectstack/lint's validate-flow-trigger-readiness.tsalready computesisAutoTriggered — and uses it for one finding only (flow-draft-status-ambiguous). The stack it receives carries requires right there. The two are never joined.
  • The runtime already knows: the boot banner resolves the flow's intended trigger type and prints the exact remedy. The diagnostic text exists; it just fires after deploy.

And the unguarded case is the worse one: a hierarchy scope without its capability fails closed (visibility narrows — wrong but conservative, and someone notices missing rows). An unbound flow fails silent — the automation simply does not happen and nothing anywhere says so.

test/trigger-capability.test.ts is therefore labelled a stopgap in the house convention (test/flow-predicates.test.ts, test/metadata-bindings.test.ts): it points at #14153 and is written to be deleted when that lands, not maintained.

Why the guard is static rather than the getTriggerBindingAudit() boot check #68 suggested

Measured, not assumed: a kernel built the way test/dispatch-wiring.test.ts builds one (createStandaloneStack + AppPlugin + bootstrap()) mounts no capability plugins at all. Its own boot log says INFO Info: Optional service not present: automation, so getService('automation') yields nothing and there is no audit to read — requires is resolved by the CLI serve host, not by the kernel. Mounting the trigger plugins by hand inside the test would assert the test's wiring rather than the config's, which is the test-side-bind false green test/dispatch-wiring.test.ts was written to avoid. So the assertion is made where the fact lives — the declaration the host reads — and the real-boot half is evidenced above.

Gates

All four green on 52afd5c (the head of this branch), tree clean, run after the final commit:

pnpm validate EXIT=0 ✓ Validation passed (289ms)
pnpm typecheck EXIT=0 tsc --noEmit
pnpm test EXIT=0 Test Files 19 passed (19) · Tests 511 passed (511)
pnpm build EXIT=0 ✓ Build complete (524ms)

validate still prints the one expected hierarchy-security capability-provider warning — AGENTS.md rule 7 says that warning is this repo's expected state. No new warning appeared for triggers: its provider packages resolve.

Files changed

  • objectstack.config.tstriggers added to requires, with a comment recording what it turns on, that one token covers all four kinds, and that omitting it is caught by nothing at author time.
  • test/trigger-capability.test.ts — new.

Out of scope, filed unassigned: #71 — two catalog action handlers (duly_catalog_apply, duly_catalog_sync) are registered without a defineAction declaration, so ADR-0110 D3 refuses both at dispatch on every boot. Visible in the boot diagnostics above; not touched here.

src/data/, src/flows/, src/objects/ and AGENTS.md untouched.


Generated by Claude Code

`objectstack.config.ts` declared `requires: ['automation',
'hierarchy-security']`. `automation` gives the app a flow ENGINE; it
registers no TRIGGER. Every flow in the app was therefore inert: the
assignment fan-out (#33) never fanned out and the three reminder sweeps
(#70) never swept.
Measured on @objectstack/cli 17.2.0, `PORT=3117 pnpm start`:
before: Plugins: 35 loaded
Flows: 4 flow(s) 0 bound to triggers
+ one "declares a '<type>' trigger but is NOT bound" warning
per flow
after: Plugins: 39 loaded (RecordChangeTriggerPlugin,
ScheduleTriggerPlugin, TimeRelativeTriggerPlugin,
ApiTriggerPlugin)
Flows: 4 flow(s) 4 bound to triggers
(record_change, schedule, time_relative, api)
no unbound warnings; boot diagnostics 9 -> 5
One token covers all four kinds: `triggers` is the only trigger entry in
`PLATFORM_CAPABILITY_TOKENS`, and the CLI keys it to
@objectstack/trigger-record-change plus extras for the schedule,
time-relative and api plugins. No second declaration is needed.
`validate`, `typecheck`, `test` and `build` all exited 0 with every flow
unbound, so `test/trigger-capability.test.ts` pins the invariant: it goes
red if `triggers` is dropped while any flow declares a trigger, and it
re-derives its assumptions (token spelling, one-token coverage, the flow
`type` vocabulary) from the platform's own tables rather than restating
them. It is a labelled stopgap over an author-time platform gap, filed as
objectstack-ai/objectstack#14153, and is meant to be deleted when that
lands.
Part of #68
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:49
@os-warren
os-warren merged commit 45ab6a0 into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-warren added a commit that referenced this pull request Sep 1, 2026
* Seed the demo: the product working on first boot
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Spread in-flight touch ages by how long a task has been open
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No flow in this app auto-launches — requires: ['triggers'] is undeclared, and the boot summary says so on every start

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Declare the triggers capability so the app's flows actually fire - #72

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-68-declare-triggers
Sep 1, 2026
Merged

Declare the triggers capability so the app's flows actually fire#72
os-warren merged 1 commit into
mainfrom
claude/issue-68-declare-triggers

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#68

objectstack.config.ts declared requires: ['automation', 'hierarchy-security']. automation gives the app a flow engine; it registers no trigger. Every flow in the app was inert — #33's assignment fan-out never fanned out, #70's three reminder sweeps never swept — while all four gates stayed green.

Verified on a real boot

PORT=3117 pnpm start, @objectstack/cli 17.2.0, both runs on this branch's tree.

Before (at e3d6c7f, requires without triggers):

 Plugins: 35 loaded
Flows: 4 flow(s) 0 bound to triggers
⚠ flow 'duly_assignment_fanout' declares a 'record_change' trigger but is NOT bound —
no 'record_change' trigger is registered — add requires: ['triggers']
(record_change/schedule/time_relative/api ship in @objectstack/trigger-*)
⚠ flow 'duly_task_lead_time_reminder' declares a 'time_relative' trigger but is NOT bound — …
⚠ flow 'duly_task_due_soon_reminder' declares a 'time_relative' trigger but is NOT bound — …
⚠ flow 'duly_task_overdue_owner_escalation' declares a 'time_relative' trigger but is NOT bound — …
⚠ Boot diagnostics — 9 warnings logged during startup:

After (at 52afd5c, this branch's head):

 Plugins: 39 loaded
… AutomationServicePlugin, RecordChangeTriggerPlugin, ScheduleTriggerPlugin,
TimeRelativeTriggerPlugin, ApiTriggerPlugin, …
Flows: 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api)
⚠ Boot diagnostics — 5 warnings logged during startup:

Zero of four → four of four. No NOT bound warning survives; the four [Automation] boot diagnostics are gone (9 → 5), and the four trigger plugins now appear in the loaded list.

One token covers all four kinds — no second declaration

The warning names @objectstack/trigger-* plural, so this was worth establishing rather than assuming. It is a single declaration:

  • triggers is the only trigger entry in PLATFORM_CAPABILITY_TOKENS (28 tokens; nothing else matches /trigger/i).
  • The CLI's capability map keys it to @objectstack/trigger-record-change plus three extrasScheduleTriggerPluginandTimeRelativeTriggerPlugin (both from @objectstack/trigger-schedule) and ApiTriggerPlugin (from @objectstack/trigger-api).
  • PLATFORM_CAPABILITY_PROVIDERS.triggers.edition is open and the packages are already on disk transitively, so nothing needs installing.
  • The sweeps also need the job service; job is in PLATFORM_ALWAYS_ON_CAPABILITIES and mounts whether or not it is named.

The boot line confirms it empirically: one token, (record_change, schedule, time_relative, api).

The regression guard

test/trigger-capability.test.ts (6 tests) goes red if triggers is ever dropped while any flow declares a trigger. It does not restate platform facts — it re-derives them each run from the platform's own tables, so it reports a change instead of going stale:

assertionbreaks when
requires contains triggersthe token is dropped — the #68 defect
requires contains automationthe engine is dropped — same silence, one layer down
non-vacuity: flows exist and one is detected as trigger-launchedthe detector stops matching how flows are authored, so the guard would pass by finding nothing
isKnownPlatformCapability('triggers')the vocabulary renames the token
exactly one /trigger/i token, edition: 'open'the vocabulary is split — the app would then need the new token(s) too
every flow type enum member is classifiedthe enum grows a member nobody has judged trigger-launched or not

Trigger detection ORs the two routes the engine and @objectstack/lint both use — the flow's type, or a start node carrying triggerType / timeRelative / schedule. The type set is stated as its complement (screen, autolaunched are the non-trigger ones), so a future enum member demands the capability rather than being waved through.

Ablation — the guard was proven to fail

Committed first, then mutated and measured in one shell invocation under a restoring trap; the mutation was confirmed on disk by grep counts on both the injected and the removed text (1/0 → flipped) plus a non-empty git diff --stat, not by the editor's exit code.

Dropping 'triggers' from requires:

test/trigger-capability.test.ts 1 failed | 5 passed (6)
whole suite 1 failed | 18 passed (19) ← 511 tests, only the new one catches it
pnpm validate EXIT=0 "✓ Validation passed" · "Logic: 4 Flows"
AssertionError: 4 flow(s) declare a trigger — duly_assignment_fanout,
duly_task_lead_time_reminder, duly_task_due_soon_reminder,
duly_task_overdue_owner_escalation — but objectstack.config.ts does not declare
'triggers' in `requires`. … expected [ 'automation', 'hierarchy-security' ] to
include 'triggers'

Exactly one of the six failed, and the other 18 test files stayed green — which is the measured restatement of #68's complaint: the existing suite is blind to this, and validate still exits 0 while every flow is dark. No rebuild step is involved: vitest transforms the config and src/ TypeScript directly, so there is no dist/ to go stale between the mutation and the measurement.

Why nothing caught this — filed upstream as objectstack-ai/objectstack#14153

The card asked whether the asymmetry is real. It is, and it is one predicate wide:

  • defineStack runs exactly two validators that read requires: validateKnownCapabilities (typo guard) and validateHierarchyScopeCapability, which throws on a hierarchy scope declared without its capability. There is no third.
  • @objectstack/lint's validate-flow-trigger-readiness.tsalready computesisAutoTriggered — and uses it for one finding only (flow-draft-status-ambiguous). The stack it receives carries requires right there. The two are never joined.
  • The runtime already knows: the boot banner resolves the flow's intended trigger type and prints the exact remedy. The diagnostic text exists; it just fires after deploy.

And the unguarded case is the worse one: a hierarchy scope without its capability fails closed (visibility narrows — wrong but conservative, and someone notices missing rows). An unbound flow fails silent — the automation simply does not happen and nothing anywhere says so.

test/trigger-capability.test.ts is therefore labelled a stopgap in the house convention (test/flow-predicates.test.ts, test/metadata-bindings.test.ts): it points at #14153 and is written to be deleted when that lands, not maintained.

Why the guard is static rather than the getTriggerBindingAudit() boot check #68 suggested

Measured, not assumed: a kernel built the way test/dispatch-wiring.test.ts builds one (createStandaloneStack + AppPlugin + bootstrap()) mounts no capability plugins at all. Its own boot log says INFO Info: Optional service not present: automation, so getService('automation') yields nothing and there is no audit to read — requires is resolved by the CLI serve host, not by the kernel. Mounting the trigger plugins by hand inside the test would assert the test's wiring rather than the config's, which is the test-side-bind false green test/dispatch-wiring.test.ts was written to avoid. So the assertion is made where the fact lives — the declaration the host reads — and the real-boot half is evidenced above.

Gates

All four green on 52afd5c (the head of this branch), tree clean, run after the final commit:

pnpm validate EXIT=0 ✓ Validation passed (289ms)
pnpm typecheck EXIT=0 tsc --noEmit
pnpm test EXIT=0 Test Files 19 passed (19) · Tests 511 passed (511)
pnpm build EXIT=0 ✓ Build complete (524ms)

validate still prints the one expected hierarchy-security capability-provider warning — AGENTS.md rule 7 says that warning is this repo's expected state. No new warning appeared for triggers: its provider packages resolve.

Files changed

  • objectstack.config.tstriggers added to requires, with a comment recording what it turns on, that one token covers all four kinds, and that omitting it is caught by nothing at author time.
  • test/trigger-capability.test.ts — new.

Out of scope, filed unassigned: #71 — two catalog action handlers (duly_catalog_apply, duly_catalog_sync) are registered without a defineAction declaration, so ADR-0110 D3 refuses both at dispatch on every boot. Visible in the boot diagnostics above; not touched here.

src/data/, src/flows/, src/objects/ and AGENTS.md untouched.


Generated by Claude Code

`objectstack.config.ts` declared `requires: ['automation',
'hierarchy-security']`. `automation` gives the app a flow ENGINE; it
registers no TRIGGER. Every flow in the app was therefore inert: the
assignment fan-out (#33) never fanned out and the three reminder sweeps
(#70) never swept.
Measured on @objectstack/cli 17.2.0, `PORT=3117 pnpm start`:
before: Plugins: 35 loaded
Flows: 4 flow(s) 0 bound to triggers
+ one "declares a '<type>' trigger but is NOT bound" warning
per flow
after: Plugins: 39 loaded (RecordChangeTriggerPlugin,
ScheduleTriggerPlugin, TimeRelativeTriggerPlugin,
ApiTriggerPlugin)
Flows: 4 flow(s) 4 bound to triggers
(record_change, schedule, time_relative, api)
no unbound warnings; boot diagnostics 9 -> 5
One token covers all four kinds: `triggers` is the only trigger entry in
`PLATFORM_CAPABILITY_TOKENS`, and the CLI keys it to
@objectstack/trigger-record-change plus extras for the schedule,
time-relative and api plugins. No second declaration is needed.
`validate`, `typecheck`, `test` and `build` all exited 0 with every flow
unbound, so `test/trigger-capability.test.ts` pins the invariant: it goes
red if `triggers` is dropped while any flow declares a trigger, and it
re-derives its assumptions (token spelling, one-token coverage, the flow
`type` vocabulary) from the platform's own tables rather than restating
them. It is a labelled stopgap over an author-time platform gap, filed as
objectstack-ai/objectstack#14153, and is meant to be deleted when that
lands.
Part of #68
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:49
@os-warren
os-warren merged commit 45ab6a0 into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-warren added a commit that referenced this pull request Sep 1, 2026
* Seed the demo: the product working on first boot
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Spread in-flight touch ages by how long a task has been open
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No flow in this app auto-launches — requires: ['triggers'] is undeclared, and the boot summary says so on every start

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Declare the triggers capability so the app's flows actually fire - #72

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-68-declare-triggers
Sep 1, 2026
Merged

Declare the triggers capability so the app's flows actually fire#72
os-warren merged 1 commit into
mainfrom
claude/issue-68-declare-triggers

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#68

objectstack.config.ts declared requires: ['automation', 'hierarchy-security']. automation gives the app a flow engine; it registers no trigger. Every flow in the app was inert — #33's assignment fan-out never fanned out, #70's three reminder sweeps never swept — while all four gates stayed green.

Verified on a real boot

PORT=3117 pnpm start, @objectstack/cli 17.2.0, both runs on this branch's tree.

Before (at e3d6c7f, requires without triggers):

 Plugins: 35 loaded
Flows: 4 flow(s) 0 bound to triggers
⚠ flow 'duly_assignment_fanout' declares a 'record_change' trigger but is NOT bound —
no 'record_change' trigger is registered — add requires: ['triggers']
(record_change/schedule/time_relative/api ship in @objectstack/trigger-*)
⚠ flow 'duly_task_lead_time_reminder' declares a 'time_relative' trigger but is NOT bound — …
⚠ flow 'duly_task_due_soon_reminder' declares a 'time_relative' trigger but is NOT bound — …
⚠ flow 'duly_task_overdue_owner_escalation' declares a 'time_relative' trigger but is NOT bound — …
⚠ Boot diagnostics — 9 warnings logged during startup:

After (at 52afd5c, this branch's head):

 Plugins: 39 loaded
… AutomationServicePlugin, RecordChangeTriggerPlugin, ScheduleTriggerPlugin,
TimeRelativeTriggerPlugin, ApiTriggerPlugin, …
Flows: 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api)
⚠ Boot diagnostics — 5 warnings logged during startup:

Zero of four → four of four. No NOT bound warning survives; the four [Automation] boot diagnostics are gone (9 → 5), and the four trigger plugins now appear in the loaded list.

One token covers all four kinds — no second declaration

The warning names @objectstack/trigger-* plural, so this was worth establishing rather than assuming. It is a single declaration:

  • triggers is the only trigger entry in PLATFORM_CAPABILITY_TOKENS (28 tokens; nothing else matches /trigger/i).
  • The CLI's capability map keys it to @objectstack/trigger-record-change plus three extrasScheduleTriggerPluginandTimeRelativeTriggerPlugin (both from @objectstack/trigger-schedule) and ApiTriggerPlugin (from @objectstack/trigger-api).
  • PLATFORM_CAPABILITY_PROVIDERS.triggers.edition is open and the packages are already on disk transitively, so nothing needs installing.
  • The sweeps also need the job service; job is in PLATFORM_ALWAYS_ON_CAPABILITIES and mounts whether or not it is named.

The boot line confirms it empirically: one token, (record_change, schedule, time_relative, api).

The regression guard

test/trigger-capability.test.ts (6 tests) goes red if triggers is ever dropped while any flow declares a trigger. It does not restate platform facts — it re-derives them each run from the platform's own tables, so it reports a change instead of going stale:

assertionbreaks when
requires contains triggersthe token is dropped — the #68 defect
requires contains automationthe engine is dropped — same silence, one layer down
non-vacuity: flows exist and one is detected as trigger-launchedthe detector stops matching how flows are authored, so the guard would pass by finding nothing
isKnownPlatformCapability('triggers')the vocabulary renames the token
exactly one /trigger/i token, edition: 'open'the vocabulary is split — the app would then need the new token(s) too
every flow type enum member is classifiedthe enum grows a member nobody has judged trigger-launched or not

Trigger detection ORs the two routes the engine and @objectstack/lint both use — the flow's type, or a start node carrying triggerType / timeRelative / schedule. The type set is stated as its complement (screen, autolaunched are the non-trigger ones), so a future enum member demands the capability rather than being waved through.

Ablation — the guard was proven to fail

Committed first, then mutated and measured in one shell invocation under a restoring trap; the mutation was confirmed on disk by grep counts on both the injected and the removed text (1/0 → flipped) plus a non-empty git diff --stat, not by the editor's exit code.

Dropping 'triggers' from requires:

test/trigger-capability.test.ts 1 failed | 5 passed (6)
whole suite 1 failed | 18 passed (19) ← 511 tests, only the new one catches it
pnpm validate EXIT=0 "✓ Validation passed" · "Logic: 4 Flows"
AssertionError: 4 flow(s) declare a trigger — duly_assignment_fanout,
duly_task_lead_time_reminder, duly_task_due_soon_reminder,
duly_task_overdue_owner_escalation — but objectstack.config.ts does not declare
'triggers' in `requires`. … expected [ 'automation', 'hierarchy-security' ] to
include 'triggers'

Exactly one of the six failed, and the other 18 test files stayed green — which is the measured restatement of #68's complaint: the existing suite is blind to this, and validate still exits 0 while every flow is dark. No rebuild step is involved: vitest transforms the config and src/ TypeScript directly, so there is no dist/ to go stale between the mutation and the measurement.

Why nothing caught this — filed upstream as objectstack-ai/objectstack#14153

The card asked whether the asymmetry is real. It is, and it is one predicate wide:

  • defineStack runs exactly two validators that read requires: validateKnownCapabilities (typo guard) and validateHierarchyScopeCapability, which throws on a hierarchy scope declared without its capability. There is no third.
  • @objectstack/lint's validate-flow-trigger-readiness.tsalready computesisAutoTriggered — and uses it for one finding only (flow-draft-status-ambiguous). The stack it receives carries requires right there. The two are never joined.
  • The runtime already knows: the boot banner resolves the flow's intended trigger type and prints the exact remedy. The diagnostic text exists; it just fires after deploy.

And the unguarded case is the worse one: a hierarchy scope without its capability fails closed (visibility narrows — wrong but conservative, and someone notices missing rows). An unbound flow fails silent — the automation simply does not happen and nothing anywhere says so.

test/trigger-capability.test.ts is therefore labelled a stopgap in the house convention (test/flow-predicates.test.ts, test/metadata-bindings.test.ts): it points at #14153 and is written to be deleted when that lands, not maintained.

Why the guard is static rather than the getTriggerBindingAudit() boot check #68 suggested

Measured, not assumed: a kernel built the way test/dispatch-wiring.test.ts builds one (createStandaloneStack + AppPlugin + bootstrap()) mounts no capability plugins at all. Its own boot log says INFO Info: Optional service not present: automation, so getService('automation') yields nothing and there is no audit to read — requires is resolved by the CLI serve host, not by the kernel. Mounting the trigger plugins by hand inside the test would assert the test's wiring rather than the config's, which is the test-side-bind false green test/dispatch-wiring.test.ts was written to avoid. So the assertion is made where the fact lives — the declaration the host reads — and the real-boot half is evidenced above.

Gates

All four green on 52afd5c (the head of this branch), tree clean, run after the final commit:

pnpm validate EXIT=0 ✓ Validation passed (289ms)
pnpm typecheck EXIT=0 tsc --noEmit
pnpm test EXIT=0 Test Files 19 passed (19) · Tests 511 passed (511)
pnpm build EXIT=0 ✓ Build complete (524ms)

validate still prints the one expected hierarchy-security capability-provider warning — AGENTS.md rule 7 says that warning is this repo's expected state. No new warning appeared for triggers: its provider packages resolve.

Files changed

  • objectstack.config.tstriggers added to requires, with a comment recording what it turns on, that one token covers all four kinds, and that omitting it is caught by nothing at author time.
  • test/trigger-capability.test.ts — new.

Out of scope, filed unassigned: #71 — two catalog action handlers (duly_catalog_apply, duly_catalog_sync) are registered without a defineAction declaration, so ADR-0110 D3 refuses both at dispatch on every boot. Visible in the boot diagnostics above; not touched here.

src/data/, src/flows/, src/objects/ and AGENTS.md untouched.


Generated by Claude Code

`objectstack.config.ts` declared `requires: ['automation',
'hierarchy-security']`. `automation` gives the app a flow ENGINE; it
registers no TRIGGER. Every flow in the app was therefore inert: the
assignment fan-out (#33) never fanned out and the three reminder sweeps
(#70) never swept.
Measured on @objectstack/cli 17.2.0, `PORT=3117 pnpm start`:
before: Plugins: 35 loaded
Flows: 4 flow(s) 0 bound to triggers
+ one "declares a '<type>' trigger but is NOT bound" warning
per flow
after: Plugins: 39 loaded (RecordChangeTriggerPlugin,
ScheduleTriggerPlugin, TimeRelativeTriggerPlugin,
ApiTriggerPlugin)
Flows: 4 flow(s) 4 bound to triggers
(record_change, schedule, time_relative, api)
no unbound warnings; boot diagnostics 9 -> 5
One token covers all four kinds: `triggers` is the only trigger entry in
`PLATFORM_CAPABILITY_TOKENS`, and the CLI keys it to
@objectstack/trigger-record-change plus extras for the schedule,
time-relative and api plugins. No second declaration is needed.
`validate`, `typecheck`, `test` and `build` all exited 0 with every flow
unbound, so `test/trigger-capability.test.ts` pins the invariant: it goes
red if `triggers` is dropped while any flow declares a trigger, and it
re-derives its assumptions (token spelling, one-token coverage, the flow
`type` vocabulary) from the platform's own tables rather than restating
them. It is a labelled stopgap over an author-time platform gap, filed as
objectstack-ai/objectstack#14153, and is meant to be deleted when that
lands.
Part of #68
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:49
@os-warren
os-warren merged commit 45ab6a0 into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-warren added a commit that referenced this pull request Sep 1, 2026
* Seed the demo: the product working on first boot
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Spread in-flight touch ages by how long a task has been open
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No flow in this app auto-launches — requires: ['triggers'] is undeclared, and the boot summary says so on every start

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Declare the triggers capability so the app's flows actually fire - #72

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-68-declare-triggers
Sep 1, 2026
Merged

Declare the triggers capability so the app's flows actually fire#72
os-warren merged 1 commit into
mainfrom
claude/issue-68-declare-triggers

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#68

objectstack.config.ts declared requires: ['automation', 'hierarchy-security']. automation gives the app a flow engine; it registers no trigger. Every flow in the app was inert — #33's assignment fan-out never fanned out, #70's three reminder sweeps never swept — while all four gates stayed green.

Verified on a real boot

PORT=3117 pnpm start, @objectstack/cli 17.2.0, both runs on this branch's tree.

Before (at e3d6c7f, requires without triggers):

 Plugins: 35 loaded
Flows: 4 flow(s) 0 bound to triggers
⚠ flow 'duly_assignment_fanout' declares a 'record_change' trigger but is NOT bound —
no 'record_change' trigger is registered — add requires: ['triggers']
(record_change/schedule/time_relative/api ship in @objectstack/trigger-*)
⚠ flow 'duly_task_lead_time_reminder' declares a 'time_relative' trigger but is NOT bound — …
⚠ flow 'duly_task_due_soon_reminder' declares a 'time_relative' trigger but is NOT bound — …
⚠ flow 'duly_task_overdue_owner_escalation' declares a 'time_relative' trigger but is NOT bound — …
⚠ Boot diagnostics — 9 warnings logged during startup:

After (at 52afd5c, this branch's head):

 Plugins: 39 loaded
… AutomationServicePlugin, RecordChangeTriggerPlugin, ScheduleTriggerPlugin,
TimeRelativeTriggerPlugin, ApiTriggerPlugin, …
Flows: 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api)
⚠ Boot diagnostics — 5 warnings logged during startup:

Zero of four → four of four. No NOT bound warning survives; the four [Automation] boot diagnostics are gone (9 → 5), and the four trigger plugins now appear in the loaded list.

One token covers all four kinds — no second declaration

The warning names @objectstack/trigger-* plural, so this was worth establishing rather than assuming. It is a single declaration:

  • triggers is the only trigger entry in PLATFORM_CAPABILITY_TOKENS (28 tokens; nothing else matches /trigger/i).
  • The CLI's capability map keys it to @objectstack/trigger-record-change plus three extrasScheduleTriggerPluginandTimeRelativeTriggerPlugin (both from @objectstack/trigger-schedule) and ApiTriggerPlugin (from @objectstack/trigger-api).
  • PLATFORM_CAPABILITY_PROVIDERS.triggers.edition is open and the packages are already on disk transitively, so nothing needs installing.
  • The sweeps also need the job service; job is in PLATFORM_ALWAYS_ON_CAPABILITIES and mounts whether or not it is named.

The boot line confirms it empirically: one token, (record_change, schedule, time_relative, api).

The regression guard

test/trigger-capability.test.ts (6 tests) goes red if triggers is ever dropped while any flow declares a trigger. It does not restate platform facts — it re-derives them each run from the platform's own tables, so it reports a change instead of going stale:

assertionbreaks when
requires contains triggersthe token is dropped — the #68 defect
requires contains automationthe engine is dropped — same silence, one layer down
non-vacuity: flows exist and one is detected as trigger-launchedthe detector stops matching how flows are authored, so the guard would pass by finding nothing
isKnownPlatformCapability('triggers')the vocabulary renames the token
exactly one /trigger/i token, edition: 'open'the vocabulary is split — the app would then need the new token(s) too
every flow type enum member is classifiedthe enum grows a member nobody has judged trigger-launched or not

Trigger detection ORs the two routes the engine and @objectstack/lint both use — the flow's type, or a start node carrying triggerType / timeRelative / schedule. The type set is stated as its complement (screen, autolaunched are the non-trigger ones), so a future enum member demands the capability rather than being waved through.

Ablation — the guard was proven to fail

Committed first, then mutated and measured in one shell invocation under a restoring trap; the mutation was confirmed on disk by grep counts on both the injected and the removed text (1/0 → flipped) plus a non-empty git diff --stat, not by the editor's exit code.

Dropping 'triggers' from requires:

test/trigger-capability.test.ts 1 failed | 5 passed (6)
whole suite 1 failed | 18 passed (19) ← 511 tests, only the new one catches it
pnpm validate EXIT=0 "✓ Validation passed" · "Logic: 4 Flows"
AssertionError: 4 flow(s) declare a trigger — duly_assignment_fanout,
duly_task_lead_time_reminder, duly_task_due_soon_reminder,
duly_task_overdue_owner_escalation — but objectstack.config.ts does not declare
'triggers' in `requires`. … expected [ 'automation', 'hierarchy-security' ] to
include 'triggers'

Exactly one of the six failed, and the other 18 test files stayed green — which is the measured restatement of #68's complaint: the existing suite is blind to this, and validate still exits 0 while every flow is dark. No rebuild step is involved: vitest transforms the config and src/ TypeScript directly, so there is no dist/ to go stale between the mutation and the measurement.

Why nothing caught this — filed upstream as objectstack-ai/objectstack#14153

The card asked whether the asymmetry is real. It is, and it is one predicate wide:

  • defineStack runs exactly two validators that read requires: validateKnownCapabilities (typo guard) and validateHierarchyScopeCapability, which throws on a hierarchy scope declared without its capability. There is no third.
  • @objectstack/lint's validate-flow-trigger-readiness.tsalready computesisAutoTriggered — and uses it for one finding only (flow-draft-status-ambiguous). The stack it receives carries requires right there. The two are never joined.
  • The runtime already knows: the boot banner resolves the flow's intended trigger type and prints the exact remedy. The diagnostic text exists; it just fires after deploy.

And the unguarded case is the worse one: a hierarchy scope without its capability fails closed (visibility narrows — wrong but conservative, and someone notices missing rows). An unbound flow fails silent — the automation simply does not happen and nothing anywhere says so.

test/trigger-capability.test.ts is therefore labelled a stopgap in the house convention (test/flow-predicates.test.ts, test/metadata-bindings.test.ts): it points at #14153 and is written to be deleted when that lands, not maintained.

Why the guard is static rather than the getTriggerBindingAudit() boot check #68 suggested

Measured, not assumed: a kernel built the way test/dispatch-wiring.test.ts builds one (createStandaloneStack + AppPlugin + bootstrap()) mounts no capability plugins at all. Its own boot log says INFO Info: Optional service not present: automation, so getService('automation') yields nothing and there is no audit to read — requires is resolved by the CLI serve host, not by the kernel. Mounting the trigger plugins by hand inside the test would assert the test's wiring rather than the config's, which is the test-side-bind false green test/dispatch-wiring.test.ts was written to avoid. So the assertion is made where the fact lives — the declaration the host reads — and the real-boot half is evidenced above.

Gates

All four green on 52afd5c (the head of this branch), tree clean, run after the final commit:

pnpm validate EXIT=0 ✓ Validation passed (289ms)
pnpm typecheck EXIT=0 tsc --noEmit
pnpm test EXIT=0 Test Files 19 passed (19) · Tests 511 passed (511)
pnpm build EXIT=0 ✓ Build complete (524ms)

validate still prints the one expected hierarchy-security capability-provider warning — AGENTS.md rule 7 says that warning is this repo's expected state. No new warning appeared for triggers: its provider packages resolve.

Files changed

  • objectstack.config.tstriggers added to requires, with a comment recording what it turns on, that one token covers all four kinds, and that omitting it is caught by nothing at author time.
  • test/trigger-capability.test.ts — new.

Out of scope, filed unassigned: #71 — two catalog action handlers (duly_catalog_apply, duly_catalog_sync) are registered without a defineAction declaration, so ADR-0110 D3 refuses both at dispatch on every boot. Visible in the boot diagnostics above; not touched here.

src/data/, src/flows/, src/objects/ and AGENTS.md untouched.


Generated by Claude Code

`objectstack.config.ts` declared `requires: ['automation',
'hierarchy-security']`. `automation` gives the app a flow ENGINE; it
registers no TRIGGER. Every flow in the app was therefore inert: the
assignment fan-out (#33) never fanned out and the three reminder sweeps
(#70) never swept.
Measured on @objectstack/cli 17.2.0, `PORT=3117 pnpm start`:
before: Plugins: 35 loaded
Flows: 4 flow(s) 0 bound to triggers
+ one "declares a '<type>' trigger but is NOT bound" warning
per flow
after: Plugins: 39 loaded (RecordChangeTriggerPlugin,
ScheduleTriggerPlugin, TimeRelativeTriggerPlugin,
ApiTriggerPlugin)
Flows: 4 flow(s) 4 bound to triggers
(record_change, schedule, time_relative, api)
no unbound warnings; boot diagnostics 9 -> 5
One token covers all four kinds: `triggers` is the only trigger entry in
`PLATFORM_CAPABILITY_TOKENS`, and the CLI keys it to
@objectstack/trigger-record-change plus extras for the schedule,
time-relative and api plugins. No second declaration is needed.
`validate`, `typecheck`, `test` and `build` all exited 0 with every flow
unbound, so `test/trigger-capability.test.ts` pins the invariant: it goes
red if `triggers` is dropped while any flow declares a trigger, and it
re-derives its assumptions (token spelling, one-token coverage, the flow
`type` vocabulary) from the platform's own tables rather than restating
them. It is a labelled stopgap over an author-time platform gap, filed as
objectstack-ai/objectstack#14153, and is meant to be deleted when that
lands.
Part of #68
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:49
@os-warren
os-warren merged commit 45ab6a0 into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-warren added a commit that referenced this pull request Sep 1, 2026
* Seed the demo: the product working on first boot
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Spread in-flight touch ages by how long a task has been open
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No flow in this app auto-launches — requires: ['triggers'] is undeclared, and the boot summary says so on every start

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Declare the triggers capability so the app's flows actually fire - #72

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-68-declare-triggers
Sep 1, 2026
Merged

Declare the triggers capability so the app's flows actually fire#72
os-warren merged 1 commit into
mainfrom
claude/issue-68-declare-triggers

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#68

objectstack.config.ts declared requires: ['automation', 'hierarchy-security']. automation gives the app a flow engine; it registers no trigger. Every flow in the app was inert — #33's assignment fan-out never fanned out, #70's three reminder sweeps never swept — while all four gates stayed green.

Verified on a real boot

PORT=3117 pnpm start, @objectstack/cli 17.2.0, both runs on this branch's tree.

Before (at e3d6c7f, requires without triggers):

 Plugins: 35 loaded
Flows: 4 flow(s) 0 bound to triggers
⚠ flow 'duly_assignment_fanout' declares a 'record_change' trigger but is NOT bound —
no 'record_change' trigger is registered — add requires: ['triggers']
(record_change/schedule/time_relative/api ship in @objectstack/trigger-*)
⚠ flow 'duly_task_lead_time_reminder' declares a 'time_relative' trigger but is NOT bound — …
⚠ flow 'duly_task_due_soon_reminder' declares a 'time_relative' trigger but is NOT bound — …
⚠ flow 'duly_task_overdue_owner_escalation' declares a 'time_relative' trigger but is NOT bound — …
⚠ Boot diagnostics — 9 warnings logged during startup:

After (at 52afd5c, this branch's head):

 Plugins: 39 loaded
… AutomationServicePlugin, RecordChangeTriggerPlugin, ScheduleTriggerPlugin,
TimeRelativeTriggerPlugin, ApiTriggerPlugin, …
Flows: 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api)
⚠ Boot diagnostics — 5 warnings logged during startup:

Zero of four → four of four. No NOT bound warning survives; the four [Automation] boot diagnostics are gone (9 → 5), and the four trigger plugins now appear in the loaded list.

One token covers all four kinds — no second declaration

The warning names @objectstack/trigger-* plural, so this was worth establishing rather than assuming. It is a single declaration:

  • triggers is the only trigger entry in PLATFORM_CAPABILITY_TOKENS (28 tokens; nothing else matches /trigger/i).
  • The CLI's capability map keys it to @objectstack/trigger-record-change plus three extrasScheduleTriggerPluginandTimeRelativeTriggerPlugin (both from @objectstack/trigger-schedule) and ApiTriggerPlugin (from @objectstack/trigger-api).
  • PLATFORM_CAPABILITY_PROVIDERS.triggers.edition is open and the packages are already on disk transitively, so nothing needs installing.
  • The sweeps also need the job service; job is in PLATFORM_ALWAYS_ON_CAPABILITIES and mounts whether or not it is named.

The boot line confirms it empirically: one token, (record_change, schedule, time_relative, api).

The regression guard

test/trigger-capability.test.ts (6 tests) goes red if triggers is ever dropped while any flow declares a trigger. It does not restate platform facts — it re-derives them each run from the platform's own tables, so it reports a change instead of going stale:

assertionbreaks when
requires contains triggersthe token is dropped — the #68 defect
requires contains automationthe engine is dropped — same silence, one layer down
non-vacuity: flows exist and one is detected as trigger-launchedthe detector stops matching how flows are authored, so the guard would pass by finding nothing
isKnownPlatformCapability('triggers')the vocabulary renames the token
exactly one /trigger/i token, edition: 'open'the vocabulary is split — the app would then need the new token(s) too
every flow type enum member is classifiedthe enum grows a member nobody has judged trigger-launched or not

Trigger detection ORs the two routes the engine and @objectstack/lint both use — the flow's type, or a start node carrying triggerType / timeRelative / schedule. The type set is stated as its complement (screen, autolaunched are the non-trigger ones), so a future enum member demands the capability rather than being waved through.

Ablation — the guard was proven to fail

Committed first, then mutated and measured in one shell invocation under a restoring trap; the mutation was confirmed on disk by grep counts on both the injected and the removed text (1/0 → flipped) plus a non-empty git diff --stat, not by the editor's exit code.

Dropping 'triggers' from requires:

test/trigger-capability.test.ts 1 failed | 5 passed (6)
whole suite 1 failed | 18 passed (19) ← 511 tests, only the new one catches it
pnpm validate EXIT=0 "✓ Validation passed" · "Logic: 4 Flows"
AssertionError: 4 flow(s) declare a trigger — duly_assignment_fanout,
duly_task_lead_time_reminder, duly_task_due_soon_reminder,
duly_task_overdue_owner_escalation — but objectstack.config.ts does not declare
'triggers' in `requires`. … expected [ 'automation', 'hierarchy-security' ] to
include 'triggers'

Exactly one of the six failed, and the other 18 test files stayed green — which is the measured restatement of #68's complaint: the existing suite is blind to this, and validate still exits 0 while every flow is dark. No rebuild step is involved: vitest transforms the config and src/ TypeScript directly, so there is no dist/ to go stale between the mutation and the measurement.

Why nothing caught this — filed upstream as objectstack-ai/objectstack#14153

The card asked whether the asymmetry is real. It is, and it is one predicate wide:

  • defineStack runs exactly two validators that read requires: validateKnownCapabilities (typo guard) and validateHierarchyScopeCapability, which throws on a hierarchy scope declared without its capability. There is no third.
  • @objectstack/lint's validate-flow-trigger-readiness.tsalready computesisAutoTriggered — and uses it for one finding only (flow-draft-status-ambiguous). The stack it receives carries requires right there. The two are never joined.
  • The runtime already knows: the boot banner resolves the flow's intended trigger type and prints the exact remedy. The diagnostic text exists; it just fires after deploy.

And the unguarded case is the worse one: a hierarchy scope without its capability fails closed (visibility narrows — wrong but conservative, and someone notices missing rows). An unbound flow fails silent — the automation simply does not happen and nothing anywhere says so.

test/trigger-capability.test.ts is therefore labelled a stopgap in the house convention (test/flow-predicates.test.ts, test/metadata-bindings.test.ts): it points at #14153 and is written to be deleted when that lands, not maintained.

Why the guard is static rather than the getTriggerBindingAudit() boot check #68 suggested

Measured, not assumed: a kernel built the way test/dispatch-wiring.test.ts builds one (createStandaloneStack + AppPlugin + bootstrap()) mounts no capability plugins at all. Its own boot log says INFO Info: Optional service not present: automation, so getService('automation') yields nothing and there is no audit to read — requires is resolved by the CLI serve host, not by the kernel. Mounting the trigger plugins by hand inside the test would assert the test's wiring rather than the config's, which is the test-side-bind false green test/dispatch-wiring.test.ts was written to avoid. So the assertion is made where the fact lives — the declaration the host reads — and the real-boot half is evidenced above.

Gates

All four green on 52afd5c (the head of this branch), tree clean, run after the final commit:

pnpm validate EXIT=0 ✓ Validation passed (289ms)
pnpm typecheck EXIT=0 tsc --noEmit
pnpm test EXIT=0 Test Files 19 passed (19) · Tests 511 passed (511)
pnpm build EXIT=0 ✓ Build complete (524ms)

validate still prints the one expected hierarchy-security capability-provider warning — AGENTS.md rule 7 says that warning is this repo's expected state. No new warning appeared for triggers: its provider packages resolve.

Files changed

  • objectstack.config.tstriggers added to requires, with a comment recording what it turns on, that one token covers all four kinds, and that omitting it is caught by nothing at author time.
  • test/trigger-capability.test.ts — new.

Out of scope, filed unassigned: #71 — two catalog action handlers (duly_catalog_apply, duly_catalog_sync) are registered without a defineAction declaration, so ADR-0110 D3 refuses both at dispatch on every boot. Visible in the boot diagnostics above; not touched here.

src/data/, src/flows/, src/objects/ and AGENTS.md untouched.


Generated by Claude Code

`objectstack.config.ts` declared `requires: ['automation',
'hierarchy-security']`. `automation` gives the app a flow ENGINE; it
registers no TRIGGER. Every flow in the app was therefore inert: the
assignment fan-out (#33) never fanned out and the three reminder sweeps
(#70) never swept.
Measured on @objectstack/cli 17.2.0, `PORT=3117 pnpm start`:
before: Plugins: 35 loaded
Flows: 4 flow(s) 0 bound to triggers
+ one "declares a '<type>' trigger but is NOT bound" warning
per flow
after: Plugins: 39 loaded (RecordChangeTriggerPlugin,
ScheduleTriggerPlugin, TimeRelativeTriggerPlugin,
ApiTriggerPlugin)
Flows: 4 flow(s) 4 bound to triggers
(record_change, schedule, time_relative, api)
no unbound warnings; boot diagnostics 9 -> 5
One token covers all four kinds: `triggers` is the only trigger entry in
`PLATFORM_CAPABILITY_TOKENS`, and the CLI keys it to
@objectstack/trigger-record-change plus extras for the schedule,
time-relative and api plugins. No second declaration is needed.
`validate`, `typecheck`, `test` and `build` all exited 0 with every flow
unbound, so `test/trigger-capability.test.ts` pins the invariant: it goes
red if `triggers` is dropped while any flow declares a trigger, and it
re-derives its assumptions (token spelling, one-token coverage, the flow
`type` vocabulary) from the platform's own tables rather than restating
them. It is a labelled stopgap over an author-time platform gap, filed as
objectstack-ai/objectstack#14153, and is meant to be deleted when that
lands.
Part of #68
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:49
@os-warren
os-warren merged commit 45ab6a0 into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-warren added a commit that referenced this pull request Sep 1, 2026
* Seed the demo: the product working on first boot
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Spread in-flight touch ages by how long a task has been open
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No flow in this app auto-launches — requires: ['triggers'] is undeclared, and the boot summary says so on every start

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Declare the triggers capability so the app's flows actually fire - #72

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-68-declare-triggers
Sep 1, 2026
Merged

Declare the triggers capability so the app's flows actually fire#72
os-warren merged 1 commit into
mainfrom
claude/issue-68-declare-triggers

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#68

objectstack.config.ts declared requires: ['automation', 'hierarchy-security']. automation gives the app a flow engine; it registers no trigger. Every flow in the app was inert — #33's assignment fan-out never fanned out, #70's three reminder sweeps never swept — while all four gates stayed green.

Verified on a real boot

PORT=3117 pnpm start, @objectstack/cli 17.2.0, both runs on this branch's tree.

Before (at e3d6c7f, requires without triggers):

 Plugins: 35 loaded
Flows: 4 flow(s) 0 bound to triggers
⚠ flow 'duly_assignment_fanout' declares a 'record_change' trigger but is NOT bound —
no 'record_change' trigger is registered — add requires: ['triggers']
(record_change/schedule/time_relative/api ship in @objectstack/trigger-*)
⚠ flow 'duly_task_lead_time_reminder' declares a 'time_relative' trigger but is NOT bound — …
⚠ flow 'duly_task_due_soon_reminder' declares a 'time_relative' trigger but is NOT bound — …
⚠ flow 'duly_task_overdue_owner_escalation' declares a 'time_relative' trigger but is NOT bound — …
⚠ Boot diagnostics — 9 warnings logged during startup:

After (at 52afd5c, this branch's head):

 Plugins: 39 loaded
… AutomationServicePlugin, RecordChangeTriggerPlugin, ScheduleTriggerPlugin,
TimeRelativeTriggerPlugin, ApiTriggerPlugin, …
Flows: 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api)
⚠ Boot diagnostics — 5 warnings logged during startup:

Zero of four → four of four. No NOT bound warning survives; the four [Automation] boot diagnostics are gone (9 → 5), and the four trigger plugins now appear in the loaded list.

One token covers all four kinds — no second declaration

The warning names @objectstack/trigger-* plural, so this was worth establishing rather than assuming. It is a single declaration:

  • triggers is the only trigger entry in PLATFORM_CAPABILITY_TOKENS (28 tokens; nothing else matches /trigger/i).
  • The CLI's capability map keys it to @objectstack/trigger-record-change plus three extrasScheduleTriggerPluginandTimeRelativeTriggerPlugin (both from @objectstack/trigger-schedule) and ApiTriggerPlugin (from @objectstack/trigger-api).
  • PLATFORM_CAPABILITY_PROVIDERS.triggers.edition is open and the packages are already on disk transitively, so nothing needs installing.
  • The sweeps also need the job service; job is in PLATFORM_ALWAYS_ON_CAPABILITIES and mounts whether or not it is named.

The boot line confirms it empirically: one token, (record_change, schedule, time_relative, api).

The regression guard

test/trigger-capability.test.ts (6 tests) goes red if triggers is ever dropped while any flow declares a trigger. It does not restate platform facts — it re-derives them each run from the platform's own tables, so it reports a change instead of going stale:

assertionbreaks when
requires contains triggersthe token is dropped — the #68 defect
requires contains automationthe engine is dropped — same silence, one layer down
non-vacuity: flows exist and one is detected as trigger-launchedthe detector stops matching how flows are authored, so the guard would pass by finding nothing
isKnownPlatformCapability('triggers')the vocabulary renames the token
exactly one /trigger/i token, edition: 'open'the vocabulary is split — the app would then need the new token(s) too
every flow type enum member is classifiedthe enum grows a member nobody has judged trigger-launched or not

Trigger detection ORs the two routes the engine and @objectstack/lint both use — the flow's type, or a start node carrying triggerType / timeRelative / schedule. The type set is stated as its complement (screen, autolaunched are the non-trigger ones), so a future enum member demands the capability rather than being waved through.

Ablation — the guard was proven to fail

Committed first, then mutated and measured in one shell invocation under a restoring trap; the mutation was confirmed on disk by grep counts on both the injected and the removed text (1/0 → flipped) plus a non-empty git diff --stat, not by the editor's exit code.

Dropping 'triggers' from requires:

test/trigger-capability.test.ts 1 failed | 5 passed (6)
whole suite 1 failed | 18 passed (19) ← 511 tests, only the new one catches it
pnpm validate EXIT=0 "✓ Validation passed" · "Logic: 4 Flows"
AssertionError: 4 flow(s) declare a trigger — duly_assignment_fanout,
duly_task_lead_time_reminder, duly_task_due_soon_reminder,
duly_task_overdue_owner_escalation — but objectstack.config.ts does not declare
'triggers' in `requires`. … expected [ 'automation', 'hierarchy-security' ] to
include 'triggers'

Exactly one of the six failed, and the other 18 test files stayed green — which is the measured restatement of #68's complaint: the existing suite is blind to this, and validate still exits 0 while every flow is dark. No rebuild step is involved: vitest transforms the config and src/ TypeScript directly, so there is no dist/ to go stale between the mutation and the measurement.

Why nothing caught this — filed upstream as objectstack-ai/objectstack#14153

The card asked whether the asymmetry is real. It is, and it is one predicate wide:

  • defineStack runs exactly two validators that read requires: validateKnownCapabilities (typo guard) and validateHierarchyScopeCapability, which throws on a hierarchy scope declared without its capability. There is no third.
  • @objectstack/lint's validate-flow-trigger-readiness.tsalready computesisAutoTriggered — and uses it for one finding only (flow-draft-status-ambiguous). The stack it receives carries requires right there. The two are never joined.
  • The runtime already knows: the boot banner resolves the flow's intended trigger type and prints the exact remedy. The diagnostic text exists; it just fires after deploy.

And the unguarded case is the worse one: a hierarchy scope without its capability fails closed (visibility narrows — wrong but conservative, and someone notices missing rows). An unbound flow fails silent — the automation simply does not happen and nothing anywhere says so.

test/trigger-capability.test.ts is therefore labelled a stopgap in the house convention (test/flow-predicates.test.ts, test/metadata-bindings.test.ts): it points at #14153 and is written to be deleted when that lands, not maintained.

Why the guard is static rather than the getTriggerBindingAudit() boot check #68 suggested

Measured, not assumed: a kernel built the way test/dispatch-wiring.test.ts builds one (createStandaloneStack + AppPlugin + bootstrap()) mounts no capability plugins at all. Its own boot log says INFO Info: Optional service not present: automation, so getService('automation') yields nothing and there is no audit to read — requires is resolved by the CLI serve host, not by the kernel. Mounting the trigger plugins by hand inside the test would assert the test's wiring rather than the config's, which is the test-side-bind false green test/dispatch-wiring.test.ts was written to avoid. So the assertion is made where the fact lives — the declaration the host reads — and the real-boot half is evidenced above.

Gates

All four green on 52afd5c (the head of this branch), tree clean, run after the final commit:

pnpm validate EXIT=0 ✓ Validation passed (289ms)
pnpm typecheck EXIT=0 tsc --noEmit
pnpm test EXIT=0 Test Files 19 passed (19) · Tests 511 passed (511)
pnpm build EXIT=0 ✓ Build complete (524ms)

validate still prints the one expected hierarchy-security capability-provider warning — AGENTS.md rule 7 says that warning is this repo's expected state. No new warning appeared for triggers: its provider packages resolve.

Files changed

  • objectstack.config.tstriggers added to requires, with a comment recording what it turns on, that one token covers all four kinds, and that omitting it is caught by nothing at author time.
  • test/trigger-capability.test.ts — new.

Out of scope, filed unassigned: #71 — two catalog action handlers (duly_catalog_apply, duly_catalog_sync) are registered without a defineAction declaration, so ADR-0110 D3 refuses both at dispatch on every boot. Visible in the boot diagnostics above; not touched here.

src/data/, src/flows/, src/objects/ and AGENTS.md untouched.


Generated by Claude Code

`objectstack.config.ts` declared `requires: ['automation',
'hierarchy-security']`. `automation` gives the app a flow ENGINE; it
registers no TRIGGER. Every flow in the app was therefore inert: the
assignment fan-out (#33) never fanned out and the three reminder sweeps
(#70) never swept.
Measured on @objectstack/cli 17.2.0, `PORT=3117 pnpm start`:
before: Plugins: 35 loaded
Flows: 4 flow(s) 0 bound to triggers
+ one "declares a '<type>' trigger but is NOT bound" warning
per flow
after: Plugins: 39 loaded (RecordChangeTriggerPlugin,
ScheduleTriggerPlugin, TimeRelativeTriggerPlugin,
ApiTriggerPlugin)
Flows: 4 flow(s) 4 bound to triggers
(record_change, schedule, time_relative, api)
no unbound warnings; boot diagnostics 9 -> 5
One token covers all four kinds: `triggers` is the only trigger entry in
`PLATFORM_CAPABILITY_TOKENS`, and the CLI keys it to
@objectstack/trigger-record-change plus extras for the schedule,
time-relative and api plugins. No second declaration is needed.
`validate`, `typecheck`, `test` and `build` all exited 0 with every flow
unbound, so `test/trigger-capability.test.ts` pins the invariant: it goes
red if `triggers` is dropped while any flow declares a trigger, and it
re-derives its assumptions (token spelling, one-token coverage, the flow
`type` vocabulary) from the platform's own tables rather than restating
them. It is a labelled stopgap over an author-time platform gap, filed as
objectstack-ai/objectstack#14153, and is meant to be deleted when that
lands.
Part of #68
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:49
@os-warren
os-warren merged commit 45ab6a0 into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-warren added a commit that referenced this pull request Sep 1, 2026
* Seed the demo: the product working on first boot
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Spread in-flight touch ages by how long a task has been open
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No flow in this app auto-launches — requires: ['triggers'] is undeclared, and the boot summary says so on every start

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Declare the triggers capability so the app's flows actually fire - #72

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-68-declare-triggers
Sep 1, 2026
Merged

Declare the triggers capability so the app's flows actually fire#72
os-warren merged 1 commit into
mainfrom
claude/issue-68-declare-triggers

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#68

objectstack.config.ts declared requires: ['automation', 'hierarchy-security']. automation gives the app a flow engine; it registers no trigger. Every flow in the app was inert — #33's assignment fan-out never fanned out, #70's three reminder sweeps never swept — while all four gates stayed green.

Verified on a real boot

PORT=3117 pnpm start, @objectstack/cli 17.2.0, both runs on this branch's tree.

Before (at e3d6c7f, requires without triggers):

 Plugins: 35 loaded
Flows: 4 flow(s) 0 bound to triggers
⚠ flow 'duly_assignment_fanout' declares a 'record_change' trigger but is NOT bound —
no 'record_change' trigger is registered — add requires: ['triggers']
(record_change/schedule/time_relative/api ship in @objectstack/trigger-*)
⚠ flow 'duly_task_lead_time_reminder' declares a 'time_relative' trigger but is NOT bound — …
⚠ flow 'duly_task_due_soon_reminder' declares a 'time_relative' trigger but is NOT bound — …
⚠ flow 'duly_task_overdue_owner_escalation' declares a 'time_relative' trigger but is NOT bound — …
⚠ Boot diagnostics — 9 warnings logged during startup:

After (at 52afd5c, this branch's head):

 Plugins: 39 loaded
… AutomationServicePlugin, RecordChangeTriggerPlugin, ScheduleTriggerPlugin,
TimeRelativeTriggerPlugin, ApiTriggerPlugin, …
Flows: 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api)
⚠ Boot diagnostics — 5 warnings logged during startup:

Zero of four → four of four. No NOT bound warning survives; the four [Automation] boot diagnostics are gone (9 → 5), and the four trigger plugins now appear in the loaded list.

One token covers all four kinds — no second declaration

The warning names @objectstack/trigger-* plural, so this was worth establishing rather than assuming. It is a single declaration:

  • triggers is the only trigger entry in PLATFORM_CAPABILITY_TOKENS (28 tokens; nothing else matches /trigger/i).
  • The CLI's capability map keys it to @objectstack/trigger-record-change plus three extrasScheduleTriggerPluginandTimeRelativeTriggerPlugin (both from @objectstack/trigger-schedule) and ApiTriggerPlugin (from @objectstack/trigger-api).
  • PLATFORM_CAPABILITY_PROVIDERS.triggers.edition is open and the packages are already on disk transitively, so nothing needs installing.
  • The sweeps also need the job service; job is in PLATFORM_ALWAYS_ON_CAPABILITIES and mounts whether or not it is named.

The boot line confirms it empirically: one token, (record_change, schedule, time_relative, api).

The regression guard

test/trigger-capability.test.ts (6 tests) goes red if triggers is ever dropped while any flow declares a trigger. It does not restate platform facts — it re-derives them each run from the platform's own tables, so it reports a change instead of going stale:

assertionbreaks when
requires contains triggersthe token is dropped — the #68 defect
requires contains automationthe engine is dropped — same silence, one layer down
non-vacuity: flows exist and one is detected as trigger-launchedthe detector stops matching how flows are authored, so the guard would pass by finding nothing
isKnownPlatformCapability('triggers')the vocabulary renames the token
exactly one /trigger/i token, edition: 'open'the vocabulary is split — the app would then need the new token(s) too
every flow type enum member is classifiedthe enum grows a member nobody has judged trigger-launched or not

Trigger detection ORs the two routes the engine and @objectstack/lint both use — the flow's type, or a start node carrying triggerType / timeRelative / schedule. The type set is stated as its complement (screen, autolaunched are the non-trigger ones), so a future enum member demands the capability rather than being waved through.

Ablation — the guard was proven to fail

Committed first, then mutated and measured in one shell invocation under a restoring trap; the mutation was confirmed on disk by grep counts on both the injected and the removed text (1/0 → flipped) plus a non-empty git diff --stat, not by the editor's exit code.

Dropping 'triggers' from requires:

test/trigger-capability.test.ts 1 failed | 5 passed (6)
whole suite 1 failed | 18 passed (19) ← 511 tests, only the new one catches it
pnpm validate EXIT=0 "✓ Validation passed" · "Logic: 4 Flows"
AssertionError: 4 flow(s) declare a trigger — duly_assignment_fanout,
duly_task_lead_time_reminder, duly_task_due_soon_reminder,
duly_task_overdue_owner_escalation — but objectstack.config.ts does not declare
'triggers' in `requires`. … expected [ 'automation', 'hierarchy-security' ] to
include 'triggers'

Exactly one of the six failed, and the other 18 test files stayed green — which is the measured restatement of #68's complaint: the existing suite is blind to this, and validate still exits 0 while every flow is dark. No rebuild step is involved: vitest transforms the config and src/ TypeScript directly, so there is no dist/ to go stale between the mutation and the measurement.

Why nothing caught this — filed upstream as objectstack-ai/objectstack#14153

The card asked whether the asymmetry is real. It is, and it is one predicate wide:

  • defineStack runs exactly two validators that read requires: validateKnownCapabilities (typo guard) and validateHierarchyScopeCapability, which throws on a hierarchy scope declared without its capability. There is no third.
  • @objectstack/lint's validate-flow-trigger-readiness.tsalready computesisAutoTriggered — and uses it for one finding only (flow-draft-status-ambiguous). The stack it receives carries requires right there. The two are never joined.
  • The runtime already knows: the boot banner resolves the flow's intended trigger type and prints the exact remedy. The diagnostic text exists; it just fires after deploy.

And the unguarded case is the worse one: a hierarchy scope without its capability fails closed (visibility narrows — wrong but conservative, and someone notices missing rows). An unbound flow fails silent — the automation simply does not happen and nothing anywhere says so.

test/trigger-capability.test.ts is therefore labelled a stopgap in the house convention (test/flow-predicates.test.ts, test/metadata-bindings.test.ts): it points at #14153 and is written to be deleted when that lands, not maintained.

Why the guard is static rather than the getTriggerBindingAudit() boot check #68 suggested

Measured, not assumed: a kernel built the way test/dispatch-wiring.test.ts builds one (createStandaloneStack + AppPlugin + bootstrap()) mounts no capability plugins at all. Its own boot log says INFO Info: Optional service not present: automation, so getService('automation') yields nothing and there is no audit to read — requires is resolved by the CLI serve host, not by the kernel. Mounting the trigger plugins by hand inside the test would assert the test's wiring rather than the config's, which is the test-side-bind false green test/dispatch-wiring.test.ts was written to avoid. So the assertion is made where the fact lives — the declaration the host reads — and the real-boot half is evidenced above.

Gates

All four green on 52afd5c (the head of this branch), tree clean, run after the final commit:

pnpm validate EXIT=0 ✓ Validation passed (289ms)
pnpm typecheck EXIT=0 tsc --noEmit
pnpm test EXIT=0 Test Files 19 passed (19) · Tests 511 passed (511)
pnpm build EXIT=0 ✓ Build complete (524ms)

validate still prints the one expected hierarchy-security capability-provider warning — AGENTS.md rule 7 says that warning is this repo's expected state. No new warning appeared for triggers: its provider packages resolve.

Files changed

  • objectstack.config.tstriggers added to requires, with a comment recording what it turns on, that one token covers all four kinds, and that omitting it is caught by nothing at author time.
  • test/trigger-capability.test.ts — new.

Out of scope, filed unassigned: #71 — two catalog action handlers (duly_catalog_apply, duly_catalog_sync) are registered without a defineAction declaration, so ADR-0110 D3 refuses both at dispatch on every boot. Visible in the boot diagnostics above; not touched here.

src/data/, src/flows/, src/objects/ and AGENTS.md untouched.


Generated by Claude Code

`objectstack.config.ts` declared `requires: ['automation',
'hierarchy-security']`. `automation` gives the app a flow ENGINE; it
registers no TRIGGER. Every flow in the app was therefore inert: the
assignment fan-out (#33) never fanned out and the three reminder sweeps
(#70) never swept.
Measured on @objectstack/cli 17.2.0, `PORT=3117 pnpm start`:
before: Plugins: 35 loaded
Flows: 4 flow(s) 0 bound to triggers
+ one "declares a '<type>' trigger but is NOT bound" warning
per flow
after: Plugins: 39 loaded (RecordChangeTriggerPlugin,
ScheduleTriggerPlugin, TimeRelativeTriggerPlugin,
ApiTriggerPlugin)
Flows: 4 flow(s) 4 bound to triggers
(record_change, schedule, time_relative, api)
no unbound warnings; boot diagnostics 9 -> 5
One token covers all four kinds: `triggers` is the only trigger entry in
`PLATFORM_CAPABILITY_TOKENS`, and the CLI keys it to
@objectstack/trigger-record-change plus extras for the schedule,
time-relative and api plugins. No second declaration is needed.
`validate`, `typecheck`, `test` and `build` all exited 0 with every flow
unbound, so `test/trigger-capability.test.ts` pins the invariant: it goes
red if `triggers` is dropped while any flow declares a trigger, and it
re-derives its assumptions (token spelling, one-token coverage, the flow
`type` vocabulary) from the platform's own tables rather than restating
them. It is a labelled stopgap over an author-time platform gap, filed as
objectstack-ai/objectstack#14153, and is meant to be deleted when that
lands.
Part of #68
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:49
@os-warren
os-warren merged commit 45ab6a0 into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-warren added a commit that referenced this pull request Sep 1, 2026
* Seed the demo: the product working on first boot
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Spread in-flight touch ages by how long a task has been open
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No flow in this app auto-launches — requires: ['triggers'] is undeclared, and the boot summary says so on every start

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Declare the triggers capability so the app's flows actually fire - #72

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-68-declare-triggers
Sep 1, 2026
Merged

Declare the triggers capability so the app's flows actually fire#72
os-warren merged 1 commit into
mainfrom
claude/issue-68-declare-triggers

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#68

objectstack.config.ts declared requires: ['automation', 'hierarchy-security']. automation gives the app a flow engine; it registers no trigger. Every flow in the app was inert — #33's assignment fan-out never fanned out, #70's three reminder sweeps never swept — while all four gates stayed green.

Verified on a real boot

PORT=3117 pnpm start, @objectstack/cli 17.2.0, both runs on this branch's tree.

Before (at e3d6c7f, requires without triggers):

 Plugins: 35 loaded
Flows: 4 flow(s) 0 bound to triggers
⚠ flow 'duly_assignment_fanout' declares a 'record_change' trigger but is NOT bound —
no 'record_change' trigger is registered — add requires: ['triggers']
(record_change/schedule/time_relative/api ship in @objectstack/trigger-*)
⚠ flow 'duly_task_lead_time_reminder' declares a 'time_relative' trigger but is NOT bound — …
⚠ flow 'duly_task_due_soon_reminder' declares a 'time_relative' trigger but is NOT bound — …
⚠ flow 'duly_task_overdue_owner_escalation' declares a 'time_relative' trigger but is NOT bound — …
⚠ Boot diagnostics — 9 warnings logged during startup:

After (at 52afd5c, this branch's head):

 Plugins: 39 loaded
… AutomationServicePlugin, RecordChangeTriggerPlugin, ScheduleTriggerPlugin,
TimeRelativeTriggerPlugin, ApiTriggerPlugin, …
Flows: 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api)
⚠ Boot diagnostics — 5 warnings logged during startup:

Zero of four → four of four. No NOT bound warning survives; the four [Automation] boot diagnostics are gone (9 → 5), and the four trigger plugins now appear in the loaded list.

One token covers all four kinds — no second declaration

The warning names @objectstack/trigger-* plural, so this was worth establishing rather than assuming. It is a single declaration:

  • triggers is the only trigger entry in PLATFORM_CAPABILITY_TOKENS (28 tokens; nothing else matches /trigger/i).
  • The CLI's capability map keys it to @objectstack/trigger-record-change plus three extrasScheduleTriggerPluginandTimeRelativeTriggerPlugin (both from @objectstack/trigger-schedule) and ApiTriggerPlugin (from @objectstack/trigger-api).
  • PLATFORM_CAPABILITY_PROVIDERS.triggers.edition is open and the packages are already on disk transitively, so nothing needs installing.
  • The sweeps also need the job service; job is in PLATFORM_ALWAYS_ON_CAPABILITIES and mounts whether or not it is named.

The boot line confirms it empirically: one token, (record_change, schedule, time_relative, api).

The regression guard

test/trigger-capability.test.ts (6 tests) goes red if triggers is ever dropped while any flow declares a trigger. It does not restate platform facts — it re-derives them each run from the platform's own tables, so it reports a change instead of going stale:

assertionbreaks when
requires contains triggersthe token is dropped — the #68 defect
requires contains automationthe engine is dropped — same silence, one layer down
non-vacuity: flows exist and one is detected as trigger-launchedthe detector stops matching how flows are authored, so the guard would pass by finding nothing
isKnownPlatformCapability('triggers')the vocabulary renames the token
exactly one /trigger/i token, edition: 'open'the vocabulary is split — the app would then need the new token(s) too
every flow type enum member is classifiedthe enum grows a member nobody has judged trigger-launched or not

Trigger detection ORs the two routes the engine and @objectstack/lint both use — the flow's type, or a start node carrying triggerType / timeRelative / schedule. The type set is stated as its complement (screen, autolaunched are the non-trigger ones), so a future enum member demands the capability rather than being waved through.

Ablation — the guard was proven to fail

Committed first, then mutated and measured in one shell invocation under a restoring trap; the mutation was confirmed on disk by grep counts on both the injected and the removed text (1/0 → flipped) plus a non-empty git diff --stat, not by the editor's exit code.

Dropping 'triggers' from requires:

test/trigger-capability.test.ts 1 failed | 5 passed (6)
whole suite 1 failed | 18 passed (19) ← 511 tests, only the new one catches it
pnpm validate EXIT=0 "✓ Validation passed" · "Logic: 4 Flows"
AssertionError: 4 flow(s) declare a trigger — duly_assignment_fanout,
duly_task_lead_time_reminder, duly_task_due_soon_reminder,
duly_task_overdue_owner_escalation — but objectstack.config.ts does not declare
'triggers' in `requires`. … expected [ 'automation', 'hierarchy-security' ] to
include 'triggers'

Exactly one of the six failed, and the other 18 test files stayed green — which is the measured restatement of #68's complaint: the existing suite is blind to this, and validate still exits 0 while every flow is dark. No rebuild step is involved: vitest transforms the config and src/ TypeScript directly, so there is no dist/ to go stale between the mutation and the measurement.

Why nothing caught this — filed upstream as objectstack-ai/objectstack#14153

The card asked whether the asymmetry is real. It is, and it is one predicate wide:

  • defineStack runs exactly two validators that read requires: validateKnownCapabilities (typo guard) and validateHierarchyScopeCapability, which throws on a hierarchy scope declared without its capability. There is no third.
  • @objectstack/lint's validate-flow-trigger-readiness.tsalready computesisAutoTriggered — and uses it for one finding only (flow-draft-status-ambiguous). The stack it receives carries requires right there. The two are never joined.
  • The runtime already knows: the boot banner resolves the flow's intended trigger type and prints the exact remedy. The diagnostic text exists; it just fires after deploy.

And the unguarded case is the worse one: a hierarchy scope without its capability fails closed (visibility narrows — wrong but conservative, and someone notices missing rows). An unbound flow fails silent — the automation simply does not happen and nothing anywhere says so.

test/trigger-capability.test.ts is therefore labelled a stopgap in the house convention (test/flow-predicates.test.ts, test/metadata-bindings.test.ts): it points at #14153 and is written to be deleted when that lands, not maintained.

Why the guard is static rather than the getTriggerBindingAudit() boot check #68 suggested

Measured, not assumed: a kernel built the way test/dispatch-wiring.test.ts builds one (createStandaloneStack + AppPlugin + bootstrap()) mounts no capability plugins at all. Its own boot log says INFO Info: Optional service not present: automation, so getService('automation') yields nothing and there is no audit to read — requires is resolved by the CLI serve host, not by the kernel. Mounting the trigger plugins by hand inside the test would assert the test's wiring rather than the config's, which is the test-side-bind false green test/dispatch-wiring.test.ts was written to avoid. So the assertion is made where the fact lives — the declaration the host reads — and the real-boot half is evidenced above.

Gates

All four green on 52afd5c (the head of this branch), tree clean, run after the final commit:

pnpm validate EXIT=0 ✓ Validation passed (289ms)
pnpm typecheck EXIT=0 tsc --noEmit
pnpm test EXIT=0 Test Files 19 passed (19) · Tests 511 passed (511)
pnpm build EXIT=0 ✓ Build complete (524ms)

validate still prints the one expected hierarchy-security capability-provider warning — AGENTS.md rule 7 says that warning is this repo's expected state. No new warning appeared for triggers: its provider packages resolve.

Files changed

  • objectstack.config.tstriggers added to requires, with a comment recording what it turns on, that one token covers all four kinds, and that omitting it is caught by nothing at author time.
  • test/trigger-capability.test.ts — new.

Out of scope, filed unassigned: #71 — two catalog action handlers (duly_catalog_apply, duly_catalog_sync) are registered without a defineAction declaration, so ADR-0110 D3 refuses both at dispatch on every boot. Visible in the boot diagnostics above; not touched here.

src/data/, src/flows/, src/objects/ and AGENTS.md untouched.


Generated by Claude Code

`objectstack.config.ts` declared `requires: ['automation',
'hierarchy-security']`. `automation` gives the app a flow ENGINE; it
registers no TRIGGER. Every flow in the app was therefore inert: the
assignment fan-out (#33) never fanned out and the three reminder sweeps
(#70) never swept.
Measured on @objectstack/cli 17.2.0, `PORT=3117 pnpm start`:
before: Plugins: 35 loaded
Flows: 4 flow(s) 0 bound to triggers
+ one "declares a '<type>' trigger but is NOT bound" warning
per flow
after: Plugins: 39 loaded (RecordChangeTriggerPlugin,
ScheduleTriggerPlugin, TimeRelativeTriggerPlugin,
ApiTriggerPlugin)
Flows: 4 flow(s) 4 bound to triggers
(record_change, schedule, time_relative, api)
no unbound warnings; boot diagnostics 9 -> 5
One token covers all four kinds: `triggers` is the only trigger entry in
`PLATFORM_CAPABILITY_TOKENS`, and the CLI keys it to
@objectstack/trigger-record-change plus extras for the schedule,
time-relative and api plugins. No second declaration is needed.
`validate`, `typecheck`, `test` and `build` all exited 0 with every flow
unbound, so `test/trigger-capability.test.ts` pins the invariant: it goes
red if `triggers` is dropped while any flow declares a trigger, and it
re-derives its assumptions (token spelling, one-token coverage, the flow
`type` vocabulary) from the platform's own tables rather than restating
them. It is a labelled stopgap over an author-time platform gap, filed as
objectstack-ai/objectstack#14153, and is meant to be deleted when that
lands.
Part of #68
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:49
@os-warren
os-warren merged commit 45ab6a0 into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-warren added a commit that referenced this pull request Sep 1, 2026
* Seed the demo: the product working on first boot
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Spread in-flight touch ages by how long a task has been open
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No flow in this app auto-launches — requires: ['triggers'] is undeclared, and the boot summary says so on every start

2 participants

@os-warren@claude