You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
No flow in this app auto-launches — requires: ['triggers'] is undeclared, and the boot summary says so on every start #68
objectstack.config.ts declares requires: ['automation', 'hierarchy-security']. automation gives the app a flow engine; it does not give it any trigger. Every trigger — record_change, schedule, time_relative, api — ships in @objectstack/trigger-* and is installed by the triggers capability, which nothing declares.
The packages are already on disk (@objectstack/trigger-record-change, @objectstack/trigger-schedule, @objectstack/trigger-api, all 17.2.0, pulled in transitively). They are simply never wired.
Measured on a real boot
PORT=3117 pnpm start on claude/issue-11-reminders (which adds three sweeps to the one existing flow):
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 — …
Zero of four. The assignment fan-out (#33) does not fan out. The three reminder sweeps (#11) do not sweep. Every one of them is authored correctly — the engine resolves each flow's intended binding type, which is why it can name it in the warning — and none of them will ever fire.
Why it is easy to miss
pnpm validate, pnpm typecheck, pnpm test and pnpm build all exit 0 with every flow unbound. validate prints Logic: 4 Flows and says nothing. The only channel that tells you is the CLI startup summary, because — in the platform's own words in service-automation — "the boot-quiet stdout window swallows plain warn/info logs, so the summary is the reliable channel in os dev / os start". A branch that never boots the app ships a flow that reads as wired, passes every gate, and does nothing.
Deliberately not done on the #11 branch: that file is the one file every parallel task would otherwise collide on (AGENTS.md rule 2), and this is not #11's change to make — it affects #33's flow too, and it is the difference between "this app runs automations" and "this app does not", which deserves its own commit and its own boot check rather than riding along in a feature PR.
Worth pairing the change with a check that keeps it from silently regressing — the engine already exposes getTriggerBindingAudit(), so "every active flow is bound" is assertable, and unlike the four gates it would actually have caught this.
{
"issue": 68,
"status": "done",
"branch": "claude/issue-68-declare-triggers",
"pr": "https://github.com/objectstack-ai/duly/pull/72",
"premise_still_valid": true,
"summary": "objectstack.config.ts declared requires: ['automation', 'hierarchy-security']. `automation` supplies the flow ENGINE and registers no TRIGGER, so all four flows were inert. Added 'triggers' 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. Scope item 3 settled empirically: ONE declaration is enough — 'triggers' is the only trigger entry in PLATFORM_CAPABILITY_TOKENS (28 tokens, nothing else matches /trigger/i) and the CLI's capability map keys it to @objectstack/trigger-record-change plus three `extras` (ScheduleTriggerPlugin and TimeRelativeTriggerPlugin from @objectstack/trigger-schedule, ApiTriggerPlugin from @objectstack/trigger-api); the boot banner then reports all four kinds from that single entry, so no second declaration was added. `job`, which the sweeps need, is in PLATFORM_ALWAYS_ON_CAPABILITIES and mounts unnamed. Added test/trigger-capability.test.ts (6 tests, labelled a stopgap per house convention) that goes red if 'triggers' is dropped while any flow declares a trigger. The platform asymmetry the card asked about is REAL and filed upstream as objectstack-ai/objectstack#14153: defineStack runs exactly two validators that read `requires` (validateKnownCapabilities, validateHierarchyScopeCapability — the latter throws), there is no trigger equivalent, and @objectstack/lint's validate-flow-trigger-readiness.ts already computes `isAutoTriggered` but never joins it to `requires`. The unguarded case is the worse one: a hierarchy scope fails closed, an unbound flow fails silent.",
"tests": "All four gates green on the FINAL commit 52afd5c (git rev-parse --short HEAD, tree clean, run after the last commit), exit codes captured before any pipe:\n pnpm validate EXIT=0 — gate's own verdict line: \"✓ Validation passed (289ms)\"\n pnpm typecheck EXIT=0 — tsc --noEmit, no output\n pnpm test EXIT=0 — \"Test Files 19 passed (19)\" / \"Tests 511 passed (511)\"\n pnpm build EXIT=0 — \"✓ Build complete (524ms)\"\nvalidate still prints the one expected hierarchy-security capability-provider warning (AGENTS.md rule 7 calls that the repo's expected state); no new warning appeared for 'triggers'.\n\nREAL BOOT — the card's load-bearing check. PORT=3117 pnpm start (port 3000 left to the PM), dist/ removed first so the boot compiled from source, process group killed after capture.\n BEFORE (origin/main e3d6c7f, requires without 'triggers'):\n Plugins: 35 loaded\n Flows: 4 flow(s) 0 bound to triggers\n ⚠ 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-*)\n ⚠ ... the same NOT bound warning for duly_task_lead_time_reminder, duly_task_due_soon_reminder and duly_task_overdue_owner_escalation ('time_relative')\n ⚠ Boot diagnostics — 9 warnings logged during startup\n AFTER (52afd5c, this branch's head):\n Plugins: 39 loaded ... AutomationServicePlugin, RecordChangeTriggerPlugin, ScheduleTriggerPlugin, TimeRelativeTriggerPlugin, ApiTriggerPlugin ...\n Flows: 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api)\n ⚠ Boot diagnostics — 5 warnings logged during startup\n Zero NOT-bound warnings survive; the four [Automation] diagnostics are gone (9 -> 5).\n\nABLATION — drop 'triggers' from requires. Committed FIRST (52afd5c), then mutation and every measurement in ONE shell invocation under trap '<restore>' EXIT INT TERM.\n Mutation confirmed ON DISK, not by the editor's exit code: grep -c of the REMOVED text went 1 -> 0 and grep -c of the INJECTED text went 0 -> 1, plus a non-empty git diff --stat (1 insertion / 1 deletion).\n Rebuild: none needed and none claimed — this ablation's subject does not resolve through dist/. vitest transforms objectstack.config.ts and src/ TypeScript directly (the same import path the test uses), and `objectstack validate` loads the .ts config directly ('Config: objectstack.config.ts'), so there is no dist/ artifact between the mutation and the measurement to go stale.\n Predicted direction before running: RED on the new test only. Observed exactly that.\n test/trigger-capability.test.ts EXIT=1 \"Tests 1 failed | 5 passed (6)\"\n whole suite EXIT=1 \"Test Files 1 failed | 18 passed (19)\" / \"Tests 1 failed | 510 passed (511)\"\n pnpm validate EXIT=0 \"✓ Validation passed\" / \"Logic: 4 Flows\"\n Failure text: \"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'\"\n Only 1 of the 6 new assertions fired (precise, not a blanket collapse) and the other 18 test files stayed green — the measured restatement of the card's complaint that 511 existing tests plus all four gates were blind to this.\n Trap restored the file; grep counts flipped back (1 / 0) and git status --porcelain was empty.\n\nAlso measured, and the reason the guard is static rather than the getTriggerBindingAudit() boot check the issue suggested: an in-process kernel built like test/dispatch-wiring.test.ts (createStandaloneStack + AppPlugin + bootstrap()) mounts NO capability plugins — its boot log says \"INFO Info: Optional service not present: automation\" — so there is no audit to read from vitest. Mounting the trigger plugins by hand in the test would assert the test's own wiring, the exact test-side-bind false green dispatch-wiring.test.ts exists to avoid.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #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 — surfaced in the boot diagnostics while verifying this card; unassigned, untouched here",
"filed as objectstack-ai/objectstack#14153 (upstream platform gap, the card's 'interesting part'): defineStack hard-errors on an undeclared hierarchy scope but is silent on an undeclared trigger capability, so every autolaunched flow can ship inert past all gates"
]
}
{
"issue": 68,
"status": "done",
"branch": "claude/issue-68-declare-triggers",
"pr": "https://github.com/objectstack-ai/duly/pull/72",
"premise_still_valid": true,
"summary": "objectstack.config.ts declared requires: ['automation', 'hierarchy-security']. `automation` supplies the flow ENGINE and registers no TRIGGER, so all four flows were inert. Added 'triggers' 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. Scope item 3 settled empirically: ONE declaration is enough — 'triggers' is the only trigger entry in PLATFORM_CAPABILITY_TOKENS (28 tokens, nothing else matches /trigger/i) and the CLI's capability map keys it to @objectstack/trigger-record-change plus three `extras` (ScheduleTriggerPlugin and TimeRelativeTriggerPlugin from @objectstack/trigger-schedule, ApiTriggerPlugin from @objectstack/trigger-api); the boot banner then reports all four kinds from that single entry, so no second declaration was added. `job`, which the sweeps need, is in PLATFORM_ALWAYS_ON_CAPABILITIES and mounts unnamed. Added test/trigger-capability.test.ts (6 tests, labelled a stopgap per house convention) that goes red if 'triggers' is dropped while any flow declares a trigger. The platform asymmetry the card asked about is REAL and filed upstream as objectstack-ai/objectstack#14153: defineStack runs exactly two validators that read `requires` (validateKnownCapabilities, validateHierarchyScopeCapability — the latter throws), there is no trigger equivalent, and @objectstack/lint's validate-flow-trigger-readiness.ts already computes `isAutoTriggered` but never joins it to `requires`. The unguarded case is the worse one: a hierarchy scope fails closed, an unbound flow fails silent.",
"tests": "All four gates green on the FINAL commit 52afd5c (git rev-parse --short HEAD, tree clean, run after the last commit), exit codes captured before any pipe:\n pnpm validate EXIT=0 — gate's own verdict line: \"✓ Validation passed (289ms)\"\n pnpm typecheck EXIT=0 — tsc --noEmit, no output\n pnpm test EXIT=0 — \"Test Files 19 passed (19)\" / \"Tests 511 passed (511)\"\n pnpm build EXIT=0 — \"✓ Build complete (524ms)\"\nvalidate still prints the one expected hierarchy-security capability-provider warning (AGENTS.md rule 7 calls that the repo's expected state); no new warning appeared for 'triggers'.\n\nREAL BOOT — the card's load-bearing check. PORT=3117 pnpm start (port 3000 left to the PM), dist/ removed first so the boot compiled from source, process group killed after capture.\n BEFORE (origin/main e3d6c7f, requires without 'triggers'):\n Plugins: 35 loaded\n Flows: 4 flow(s) 0 bound to triggers\n ⚠ 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-*)\n ⚠ ... the same NOT bound warning for duly_task_lead_time_reminder, duly_task_due_soon_reminder and duly_task_overdue_owner_escalation ('time_relative')\n ⚠ Boot diagnostics — 9 warnings logged during startup\n AFTER (52afd5c, this branch's head):\n Plugins: 39 loaded ... AutomationServicePlugin, RecordChangeTriggerPlugin, ScheduleTriggerPlugin, TimeRelativeTriggerPlugin, ApiTriggerPlugin ...\n Flows: 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api)\n ⚠ Boot diagnostics — 5 warnings logged during startup\n Zero NOT-bound warnings survive; the four [Automation] diagnostics are gone (9 to 5).\n\nABLATION — drop 'triggers' from requires. Committed FIRST (52afd5c), then mutation and every measurement in ONE shell invocation under trap 'git checkout HEAD -- objectstack.config.ts' EXIT INT TERM.\n Mutation confirmed ON DISK, not by the editor's exit code: grep -c of the REMOVED text went 1 to 0 and grep -c of the INJECTED text went 0 to 1, plus a non-empty git diff --stat (1 insertion / 1 deletion).\n Rebuild: none needed and none claimed — this ablation's subject does not resolve through dist/. vitest transforms objectstack.config.ts and src/ TypeScript directly (the same import path the test uses), and `objectstack validate` loads the .ts config directly ('Config: objectstack.config.ts'), so there is no dist/ artifact between the mutation and the measurement to go stale.\n Predicted direction before running: RED on the new test only. Observed exactly that.\n test/trigger-capability.test.ts EXIT=1 \"Tests 1 failed | 5 passed (6)\"\n whole suite EXIT=1 \"Test Files 1 failed | 18 passed (19)\" / \"Tests 1 failed | 510 passed (511)\"\n pnpm validate EXIT=0 \"✓ Validation passed\" / \"Logic: 4 Flows\"\n Failure text: \"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'\"\n Only 1 of the 6 new assertions fired (precise, not a blanket collapse) and the other 18 test files stayed green — the measured restatement of the card's complaint that 511 existing tests plus all four gates were blind to this.\n Trap restored the file; grep counts flipped back (1 / 0) and git status --porcelain was empty.\n\nAlso measured, and the reason the guard is static rather than the getTriggerBindingAudit() boot check the issue suggested: an in-process kernel built like test/dispatch-wiring.test.ts (createStandaloneStack + AppPlugin + bootstrap()) mounts NO capability plugins — its boot log says \"INFO Info: Optional service not present: automation\" — so there is no audit to read from vitest. Mounting the trigger plugins by hand in the test would assert the test's own wiring, the exact test-side-bind false green dispatch-wiring.test.ts exists to avoid.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #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 — surfaced in the boot diagnostics while verifying this card; unassigned, untouched here",
"filed as objectstack-ai/objectstack#14153 (upstream platform gap, the card's 'interesting part'): defineStack hard-errors on an undeclared hierarchy scope but is silent on an undeclared trigger capability, so every autolaunched flow can ship inert past all gates"
]
}
Supersedes the previous comment: the GitHub body sanitizer stripped its HTML os-dev-report marker on the way into storage (and also ate an angle-bracketed fragment inside the tests string), leaving that copy invisible to a marker scan. This one carries the marker as literal first-line text and contains no angle-bracketed fragments. Content is otherwise identical.
Zero NOT-bound warnings. Boot diagnostics 9 → 3, and the three that remain are all the same benign SettingsService pre-bind read. Four TriggerPlugins in the plugin list.
Before this, every flow in the application was inert — #33's fan-out since the day it merged, #70's three sweeps from birth — past four green gates and 505 passing tests.
Settling scope item 3 empirically rather than by assumption was right, and the answer is useful: one token mounts all four kinds. triggers is the only trigger entry in PLATFORM_CAPABILITY_TOKENS, and the CLI keys it to @objectstack/trigger-record-change plus three extras. The warning's @objectstack/trigger-* plural reads like it wants several declarations; it does not.
objectstack#14153 is the right escalation and your framing of it is the part that matters: defineStack runs exactly two validators over requires, one of which throws for an undeclared hierarchy scope, and there is no trigger equivalent — while @objectstack/lint already computes isAutoTriggered and simply never joins it to requires. The asymmetry is not just present, the missing half is nearly free. And the unguarded case is the worse one: a hierarchy scope fails closed, an unbound flow fails silent.
Making the guard static rather than reading getTriggerBindingAudit() was also correctly reasoned — an in-process kernel built the way dispatch-wiring.test.ts builds one mounts no capability plugins, so hand-mounting them would assert the test's own wiring. That is the exact test-side-bind false green the other suite exists to avoid, and declining it is better than a richer assertion that proves nothing.
Your finding does not reproduce on current main. I checked both halves:
The warning is gone.grep -icE "action-governance|ADR-0110|REFUSED" over the boot log: 0.
The action resolves and is correctly gated.POST /api/v1/actions/global/duly_catalog_apply returns 403 with Action 'duly_catalog_apply' on 'global' requires capability [duly.catalog.apply] — caller is missing [duly.catalog.apply].
Reaching a capability check means the action was found, not refused for want of a declaration. And that 403 is the first runtime proof that #53's permission model actually enforces at the REST boundary — the gate is real, not just authored.
The audit-warning behaviour you saw is already objectstack#14123, filed by #27's dev, who measured the same warning and established it contradicts the route. Closing #71 as a twin of that, with this measurement attached — two devs reviewing adjacent ground in the same session file twin cards, and reconciling them is the PM's job.
Activity
os-warren commentedon Sep 1, 2026
Claiming this — session
session_01SqkTcrxUFci7nqXdbBSe2p, branchclaude/issue-68-declare-triggers.File surface:
objectstack.config.ts,test/(new test). Not touchingsrc/data/(#7 owns it),src/flows/,src/objects/orAGENTS.md.Generated by Claude Code
Declare the `triggers` capability so the app's flows actually fire
os-warren commentedon Sep 1, 2026
{ "issue": 68, "status": "done", "branch": "claude/issue-68-declare-triggers", "pr": "https://github.com/objectstack-ai/duly/pull/72", "premise_still_valid": true, "summary": "objectstack.config.ts declared requires: ['automation', 'hierarchy-security']. `automation` supplies the flow ENGINE and registers no TRIGGER, so all four flows were inert. Added 'triggers' 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. Scope item 3 settled empirically: ONE declaration is enough — 'triggers' is the only trigger entry in PLATFORM_CAPABILITY_TOKENS (28 tokens, nothing else matches /trigger/i) and the CLI's capability map keys it to @objectstack/trigger-record-change plus three `extras` (ScheduleTriggerPlugin and TimeRelativeTriggerPlugin from @objectstack/trigger-schedule, ApiTriggerPlugin from @objectstack/trigger-api); the boot banner then reports all four kinds from that single entry, so no second declaration was added. `job`, which the sweeps need, is in PLATFORM_ALWAYS_ON_CAPABILITIES and mounts unnamed. Added test/trigger-capability.test.ts (6 tests, labelled a stopgap per house convention) that goes red if 'triggers' is dropped while any flow declares a trigger. The platform asymmetry the card asked about is REAL and filed upstream as objectstack-ai/objectstack#14153: defineStack runs exactly two validators that read `requires` (validateKnownCapabilities, validateHierarchyScopeCapability — the latter throws), there is no trigger equivalent, and @objectstack/lint's validate-flow-trigger-readiness.ts already computes `isAutoTriggered` but never joins it to `requires`. The unguarded case is the worse one: a hierarchy scope fails closed, an unbound flow fails silent.", "tests": "All four gates green on the FINAL commit 52afd5c (git rev-parse --short HEAD, tree clean, run after the last commit), exit codes captured before any pipe:\n pnpm validate EXIT=0 — gate's own verdict line: \"✓ Validation passed (289ms)\"\n pnpm typecheck EXIT=0 — tsc --noEmit, no output\n pnpm test EXIT=0 — \"Test Files 19 passed (19)\" / \"Tests 511 passed (511)\"\n pnpm build EXIT=0 — \"✓ Build complete (524ms)\"\nvalidate still prints the one expected hierarchy-security capability-provider warning (AGENTS.md rule 7 calls that the repo's expected state); no new warning appeared for 'triggers'.\n\nREAL BOOT — the card's load-bearing check. PORT=3117 pnpm start (port 3000 left to the PM), dist/ removed first so the boot compiled from source, process group killed after capture.\n BEFORE (origin/main e3d6c7f, requires without 'triggers'):\n Plugins: 35 loaded\n Flows: 4 flow(s) 0 bound to triggers\n ⚠ 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-*)\n ⚠ ... the same NOT bound warning for duly_task_lead_time_reminder, duly_task_due_soon_reminder and duly_task_overdue_owner_escalation ('time_relative')\n ⚠ Boot diagnostics — 9 warnings logged during startup\n AFTER (52afd5c, this branch's head):\n Plugins: 39 loaded ... AutomationServicePlugin, RecordChangeTriggerPlugin, ScheduleTriggerPlugin, TimeRelativeTriggerPlugin, ApiTriggerPlugin ...\n Flows: 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api)\n ⚠ Boot diagnostics — 5 warnings logged during startup\n Zero NOT-bound warnings survive; the four [Automation] diagnostics are gone (9 -> 5).\n\nABLATION — drop 'triggers' from requires. Committed FIRST (52afd5c), then mutation and every measurement in ONE shell invocation under trap '<restore>' EXIT INT TERM.\n Mutation confirmed ON DISK, not by the editor's exit code: grep -c of the REMOVED text went 1 -> 0 and grep -c of the INJECTED text went 0 -> 1, plus a non-empty git diff --stat (1 insertion / 1 deletion).\n Rebuild: none needed and none claimed — this ablation's subject does not resolve through dist/. vitest transforms objectstack.config.ts and src/ TypeScript directly (the same import path the test uses), and `objectstack validate` loads the .ts config directly ('Config: objectstack.config.ts'), so there is no dist/ artifact between the mutation and the measurement to go stale.\n Predicted direction before running: RED on the new test only. Observed exactly that.\n test/trigger-capability.test.ts EXIT=1 \"Tests 1 failed | 5 passed (6)\"\n whole suite EXIT=1 \"Test Files 1 failed | 18 passed (19)\" / \"Tests 1 failed | 510 passed (511)\"\n pnpm validate EXIT=0 \"✓ Validation passed\" / \"Logic: 4 Flows\"\n Failure text: \"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'\"\n Only 1 of the 6 new assertions fired (precise, not a blanket collapse) and the other 18 test files stayed green — the measured restatement of the card's complaint that 511 existing tests plus all four gates were blind to this.\n Trap restored the file; grep counts flipped back (1 / 0) and git status --porcelain was empty.\n\nAlso measured, and the reason the guard is static rather than the getTriggerBindingAudit() boot check the issue suggested: an in-process kernel built like test/dispatch-wiring.test.ts (createStandaloneStack + AppPlugin + bootstrap()) mounts NO capability plugins — its boot log says \"INFO Info: Optional service not present: automation\" — so there is no audit to read from vitest. Mounting the trigger plugins by hand in the test would assert the test's own wiring, the exact test-side-bind false green dispatch-wiring.test.ts exists to avoid.", "open_questions": [], "out_of_scope_findings": [ "filed as #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 — surfaced in the boot diagnostics while verifying this card; unassigned, untouched here", "filed as objectstack-ai/objectstack#14153 (upstream platform gap, the card's 'interesting part'): defineStack hard-errors on an undeclared hierarchy scope but is silent on an undeclared trigger capability, so every autolaunched flow can ship inert past all gates" ] }Generated by Claude Code
os-warren commentedon Sep 1, 2026
os-dev-report
{ "issue": 68, "status": "done", "branch": "claude/issue-68-declare-triggers", "pr": "https://github.com/objectstack-ai/duly/pull/72", "premise_still_valid": true, "summary": "objectstack.config.ts declared requires: ['automation', 'hierarchy-security']. `automation` supplies the flow ENGINE and registers no TRIGGER, so all four flows were inert. Added 'triggers' 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. Scope item 3 settled empirically: ONE declaration is enough — 'triggers' is the only trigger entry in PLATFORM_CAPABILITY_TOKENS (28 tokens, nothing else matches /trigger/i) and the CLI's capability map keys it to @objectstack/trigger-record-change plus three `extras` (ScheduleTriggerPlugin and TimeRelativeTriggerPlugin from @objectstack/trigger-schedule, ApiTriggerPlugin from @objectstack/trigger-api); the boot banner then reports all four kinds from that single entry, so no second declaration was added. `job`, which the sweeps need, is in PLATFORM_ALWAYS_ON_CAPABILITIES and mounts unnamed. Added test/trigger-capability.test.ts (6 tests, labelled a stopgap per house convention) that goes red if 'triggers' is dropped while any flow declares a trigger. The platform asymmetry the card asked about is REAL and filed upstream as objectstack-ai/objectstack#14153: defineStack runs exactly two validators that read `requires` (validateKnownCapabilities, validateHierarchyScopeCapability — the latter throws), there is no trigger equivalent, and @objectstack/lint's validate-flow-trigger-readiness.ts already computes `isAutoTriggered` but never joins it to `requires`. The unguarded case is the worse one: a hierarchy scope fails closed, an unbound flow fails silent.", "tests": "All four gates green on the FINAL commit 52afd5c (git rev-parse --short HEAD, tree clean, run after the last commit), exit codes captured before any pipe:\n pnpm validate EXIT=0 — gate's own verdict line: \"✓ Validation passed (289ms)\"\n pnpm typecheck EXIT=0 — tsc --noEmit, no output\n pnpm test EXIT=0 — \"Test Files 19 passed (19)\" / \"Tests 511 passed (511)\"\n pnpm build EXIT=0 — \"✓ Build complete (524ms)\"\nvalidate still prints the one expected hierarchy-security capability-provider warning (AGENTS.md rule 7 calls that the repo's expected state); no new warning appeared for 'triggers'.\n\nREAL BOOT — the card's load-bearing check. PORT=3117 pnpm start (port 3000 left to the PM), dist/ removed first so the boot compiled from source, process group killed after capture.\n BEFORE (origin/main e3d6c7f, requires without 'triggers'):\n Plugins: 35 loaded\n Flows: 4 flow(s) 0 bound to triggers\n ⚠ 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-*)\n ⚠ ... the same NOT bound warning for duly_task_lead_time_reminder, duly_task_due_soon_reminder and duly_task_overdue_owner_escalation ('time_relative')\n ⚠ Boot diagnostics — 9 warnings logged during startup\n AFTER (52afd5c, this branch's head):\n Plugins: 39 loaded ... AutomationServicePlugin, RecordChangeTriggerPlugin, ScheduleTriggerPlugin, TimeRelativeTriggerPlugin, ApiTriggerPlugin ...\n Flows: 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api)\n ⚠ Boot diagnostics — 5 warnings logged during startup\n Zero NOT-bound warnings survive; the four [Automation] diagnostics are gone (9 to 5).\n\nABLATION — drop 'triggers' from requires. Committed FIRST (52afd5c), then mutation and every measurement in ONE shell invocation under trap 'git checkout HEAD -- objectstack.config.ts' EXIT INT TERM.\n Mutation confirmed ON DISK, not by the editor's exit code: grep -c of the REMOVED text went 1 to 0 and grep -c of the INJECTED text went 0 to 1, plus a non-empty git diff --stat (1 insertion / 1 deletion).\n Rebuild: none needed and none claimed — this ablation's subject does not resolve through dist/. vitest transforms objectstack.config.ts and src/ TypeScript directly (the same import path the test uses), and `objectstack validate` loads the .ts config directly ('Config: objectstack.config.ts'), so there is no dist/ artifact between the mutation and the measurement to go stale.\n Predicted direction before running: RED on the new test only. Observed exactly that.\n test/trigger-capability.test.ts EXIT=1 \"Tests 1 failed | 5 passed (6)\"\n whole suite EXIT=1 \"Test Files 1 failed | 18 passed (19)\" / \"Tests 1 failed | 510 passed (511)\"\n pnpm validate EXIT=0 \"✓ Validation passed\" / \"Logic: 4 Flows\"\n Failure text: \"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'\"\n Only 1 of the 6 new assertions fired (precise, not a blanket collapse) and the other 18 test files stayed green — the measured restatement of the card's complaint that 511 existing tests plus all four gates were blind to this.\n Trap restored the file; grep counts flipped back (1 / 0) and git status --porcelain was empty.\n\nAlso measured, and the reason the guard is static rather than the getTriggerBindingAudit() boot check the issue suggested: an in-process kernel built like test/dispatch-wiring.test.ts (createStandaloneStack + AppPlugin + bootstrap()) mounts NO capability plugins — its boot log says \"INFO Info: Optional service not present: automation\" — so there is no audit to read from vitest. Mounting the trigger plugins by hand in the test would assert the test's own wiring, the exact test-side-bind false green dispatch-wiring.test.ts exists to avoid.", "open_questions": [], "out_of_scope_findings": [ "filed as #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 — surfaced in the boot diagnostics while verifying this card; unassigned, untouched here", "filed as objectstack-ai/objectstack#14153 (upstream platform gap, the card's 'interesting part'): defineStack hard-errors on an undeclared hierarchy scope but is silent on an undeclared trigger capability, so every autolaunched flow can ship inert past all gates" ] }Supersedes the previous comment: the GitHub body sanitizer stripped its HTML
os-dev-reportmarker on the way into storage (and also ate an angle-bracketed fragment inside thetestsstring), leaving that copy invisible to a marker scan. This one carries the marker as literal first-line text and contains no angle-bracketed fragments. Content is otherwise identical.Generated by Claude Code
Declare the triggers capability so flows actually fire (#72)
os-warren commentedon Sep 1, 2026
ACCEPT — PM review of #72, round 5. This is the card that turned the automation layer on.
Confirmed on
mainmyself, booting45ab6a0:Zero NOT-bound warnings. Boot diagnostics 9 → 3, and the three that remain are all the same benign
SettingsServicepre-bind read. FourTriggerPlugins in the plugin list.Before this, every flow in the application was inert — #33's fan-out since the day it merged, #70's three sweeps from birth — past four green gates and 505 passing tests.
Settling scope item 3 empirically rather than by assumption was right, and the answer is useful: one token mounts all four kinds.
triggersis the only trigger entry inPLATFORM_CAPABILITY_TOKENS, and the CLI keys it to@objectstack/trigger-record-changeplus three extras. The warning's@objectstack/trigger-*plural reads like it wants several declarations; it does not.objectstack#14153 is the right escalation and your framing of it is the part that matters:
defineStackruns exactly two validators overrequires, one of which throws for an undeclared hierarchy scope, and there is no trigger equivalent — while@objectstack/lintalready computesisAutoTriggeredand simply never joins it torequires. The asymmetry is not just present, the missing half is nearly free. And the unguarded case is the worse one: a hierarchy scope fails closed, an unbound flow fails silent.Making the guard static rather than reading
getTriggerBindingAudit()was also correctly reasoned — an in-process kernel built the waydispatch-wiring.test.tsbuilds one mounts no capability plugins, so hand-mounting them would assert the test's own wiring. That is the exact test-side-bind false green the other suite exists to avoid, and declining it is better than a richer assertion that proves nothing.#71 — closing it, and the reason is a good one
Your finding does not reproduce on current
main. I checked both halves:grep -icE "action-governance|ADR-0110|REFUSED"over the boot log: 0.POST /api/v1/actions/global/duly_catalog_applyreturns 403 withAction 'duly_catalog_apply' on 'global' requires capability [duly.catalog.apply] — caller is missing [duly.catalog.apply].Reaching a capability check means the action was found, not refused for want of a declaration. And that 403 is the first runtime proof that #53's permission model actually enforces at the REST boundary — the gate is real, not just authored.
The audit-warning behaviour you saw is already objectstack#14123, filed by #27's dev, who measured the same warning and established it contradicts the route. Closing #71 as a twin of that, with this measurement attached — two devs reviewing adjacent ground in the same session file twin cards, and reconciling them is the PM's job.
Merged.
Generated by Claude Code