defineStack hard-errors on an undeclared hierarchy scope but is silent on an undeclared trigger capability — every autolaunched flow ships inert #14153

Description

@os-warren

defineStack refuses to load a stack that grants an ADR-0057 hierarchy scope without requires: ['hierarchy-security']. It accepts, without a word, a stack full of flows that declare triggers while requires omits triggers. The second failure mode is the worse of the two, and it is the one that is unguarded.

The asymmetry, measured

@objectstack/spec 17.2.0 — defineStack runs exactly two validators that read requires:

validatorwhat it does
validateKnownCapabilitiesrejects an unknown token (typo guard)
validateHierarchyScopeCapabilitythrows when a permission-set grant uses readScope/writeScope of unit / unit_and_below / own_and_reports and requires omits hierarchy-security

There is no third. Grepping dist/index.js for function validate* bodies that mention requires returns those two and nothing else. A flow that declares a record_change, schedule, time_relative or api trigger while requires omits triggers loads clean, validates clean, builds clean, and never fires.

What it cost downstream

objectstack-ai/duly shipped requires: ['automation', 'hierarchy-security']. automation gives a flow engine; it registers no trigger. Measured on @objectstack/cli 17.2.0, PORT=3117 pnpm start:

 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 — …

Zero of four. On that exact tree pnpm validate, pnpm typecheck, pnpm test and pnpm buildall exit 0, and validate prints Logic: 4 Flows and says nothing further. The app's entire automation layer was dark across five merged rounds — a record-change fan-out and three reminder sweeps, every one of them authored correctly — and no gate in the project could see it. Adding the one token flips the same boot to 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api).

The runtime already knows, at boot

The banner names the flow, resolves and names the intended trigger type, and prints the exact remedy including the token to add. Everything needed for an author-time diagnostic already exists as a string; it is simply emitted after deploy instead of at authoring. service-automation's own note explains why even that channel is fragile: "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 sees nothing at all.

The predicate is already computed at author time — it is just never joined to requires

@objectstack/lint's validate-flow-trigger-readiness.ts already derives exactly the needed fact:

constisAutoTriggered=isRecordTriggered2||triggerType==="api"||config.schedule!=null||isTimeRelative||flow.type==="schedule"||flow.type==="api";

It uses it for one finding only — flow-draft-status-ambiguous. The stack object handed to the rule carries requires right there. The two are never joined.

That file already reasons about trigger availabilityFLOW_TRIGGER_UNROUTABLE's message argues that "a plugin can supply the record-change trigger itself, and it still would not help, because the flow never reaches the point of asking for one." It considers whether a trigger could be supplied, and never asks whether one is declared.

Why the unguarded case is the worse one

A hierarchy scope without its capability fails closed: visibility narrows to owner-only. Wrong, but conservative, and a user notices missing rows. An autolaunched flow without its capability fails silent: the automation simply does not happen. Nothing narrows, nothing errors, nobody notices — the tasks that should have been created just do not exist. The platform guards the fail-closed case at authoring time and leaves the fail-silent one to a boot line.

Proposed

A sibling to validateHierarchyScopeCapability in defineStack:

defineStack trigger capability validation failed (1 issue):
✗ flow 'duly_assignment_fanout' declares a 'record_change' trigger but `requires` does not
include 'triggers'. No trigger would be registered, so the flow would never auto-launch.
Add requires: ['triggers'] (record_change/schedule/time_relative/api ship in
@objectstack/trigger-*).

The wording already exists — it is what the boot banner prints today.

Two notes on scope:

  1. triggers is a single token covering all four kinds (PLATFORM_CAPABILITY_PROVIDERS.triggers@objectstack/trigger-record-change plus extras for ScheduleTriggerPlugin, TimeRelativeTriggerPlugin and ApiTriggerPlugin), so the check is one membership test, not a per-kind table.
  2. If a hard error is judged too strong — an app could legitimately ship a type: 'record_change' flow it only ever launches by hand — the smaller version is an error-severity lint rule in validate-flow-trigger-readiness.ts, where the predicate already lives and where pnpm validate would surface it. Either lands the diagnostic before deploy; today neither does.

Repro

git clone https://github.com/objectstack-ai/duly &&cd duly && pnpm install
# edit objectstack.config.ts: requires: ['automation', 'hierarchy-security']
pnpm validate && pnpm typecheck && pnpm test&& pnpm build # all exit 0
PORT=3117 pnpm start # "4 flow(s) 0 bound to triggers"

Found while implementing objectstack-ai/duly#68, which adds the missing token plus a repo-local test/trigger-capability.test.ts stopgap. That stopgap is written to be deleted when this lands.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , '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

    defineStack hard-errors on an undeclared hierarchy scope but is silent on an undeclared trigger capability — every autolaunched flow ships inert #14153

    Description

    @os-warren

    defineStack refuses to load a stack that grants an ADR-0057 hierarchy scope without requires: ['hierarchy-security']. It accepts, without a word, a stack full of flows that declare triggers while requires omits triggers. The second failure mode is the worse of the two, and it is the one that is unguarded.

    The asymmetry, measured

    @objectstack/spec 17.2.0 — defineStack runs exactly two validators that read requires:

    validatorwhat it does
    validateKnownCapabilitiesrejects an unknown token (typo guard)
    validateHierarchyScopeCapabilitythrows when a permission-set grant uses readScope/writeScope of unit / unit_and_below / own_and_reports and requires omits hierarchy-security

    There is no third. Grepping dist/index.js for function validate* bodies that mention requires returns those two and nothing else. A flow that declares a record_change, schedule, time_relative or api trigger while requires omits triggers loads clean, validates clean, builds clean, and never fires.

    What it cost downstream

    objectstack-ai/duly shipped requires: ['automation', 'hierarchy-security']. automation gives a flow engine; it registers no trigger. Measured on @objectstack/cli 17.2.0, PORT=3117 pnpm start:

     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 — …
    

    Zero of four. On that exact tree pnpm validate, pnpm typecheck, pnpm test and pnpm buildall exit 0, and validate prints Logic: 4 Flows and says nothing further. The app's entire automation layer was dark across five merged rounds — a record-change fan-out and three reminder sweeps, every one of them authored correctly — and no gate in the project could see it. Adding the one token flips the same boot to 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api).

    The runtime already knows, at boot

    The banner names the flow, resolves and names the intended trigger type, and prints the exact remedy including the token to add. Everything needed for an author-time diagnostic already exists as a string; it is simply emitted after deploy instead of at authoring. service-automation's own note explains why even that channel is fragile: "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 sees nothing at all.

    The predicate is already computed at author time — it is just never joined to requires

    @objectstack/lint's validate-flow-trigger-readiness.ts already derives exactly the needed fact:

    constisAutoTriggered=isRecordTriggered2||triggerType==="api"||config.schedule!=null||isTimeRelative||flow.type==="schedule"||flow.type==="api";

    It uses it for one finding only — flow-draft-status-ambiguous. The stack object handed to the rule carries requires right there. The two are never joined.

    That file already reasons about trigger availabilityFLOW_TRIGGER_UNROUTABLE's message argues that "a plugin can supply the record-change trigger itself, and it still would not help, because the flow never reaches the point of asking for one." It considers whether a trigger could be supplied, and never asks whether one is declared.

    Why the unguarded case is the worse one

    A hierarchy scope without its capability fails closed: visibility narrows to owner-only. Wrong, but conservative, and a user notices missing rows. An autolaunched flow without its capability fails silent: the automation simply does not happen. Nothing narrows, nothing errors, nobody notices — the tasks that should have been created just do not exist. The platform guards the fail-closed case at authoring time and leaves the fail-silent one to a boot line.

    Proposed

    A sibling to validateHierarchyScopeCapability in defineStack:

    defineStack trigger capability validation failed (1 issue):
    ✗ flow 'duly_assignment_fanout' declares a 'record_change' trigger but `requires` does not
    include 'triggers'. No trigger would be registered, so the flow would never auto-launch.
    Add requires: ['triggers'] (record_change/schedule/time_relative/api ship in
    @objectstack/trigger-*).
    

    The wording already exists — it is what the boot banner prints today.

    Two notes on scope:

    1. triggers is a single token covering all four kinds (PLATFORM_CAPABILITY_PROVIDERS.triggers@objectstack/trigger-record-change plus extras for ScheduleTriggerPlugin, TimeRelativeTriggerPlugin and ApiTriggerPlugin), so the check is one membership test, not a per-kind table.
    2. If a hard error is judged too strong — an app could legitimately ship a type: 'record_change' flow it only ever launches by hand — the smaller version is an error-severity lint rule in validate-flow-trigger-readiness.ts, where the predicate already lives and where pnpm validate would surface it. Either lands the diagnostic before deploy; today neither does.

    Repro

    git clone https://github.com/objectstack-ai/duly &&cd duly && pnpm install
    # edit objectstack.config.ts: requires: ['automation', 'hierarchy-security']
    pnpm validate && pnpm typecheck && pnpm test&& pnpm build # all exit 0
    PORT=3117 pnpm start # "4 flow(s) 0 bound to triggers"

    Found while implementing objectstack-ai/duly#68, which adds the missing token plus a repo-local test/trigger-capability.test.ts stopgap. That stopgap is written to be deleted when this lands.


    Generated by Claude Code

    Metadata

    Metadata

    Assignees

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , '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

      defineStack hard-errors on an undeclared hierarchy scope but is silent on an undeclared trigger capability — every autolaunched flow ships inert #14153

      Description

      @os-warren

      defineStack refuses to load a stack that grants an ADR-0057 hierarchy scope without requires: ['hierarchy-security']. It accepts, without a word, a stack full of flows that declare triggers while requires omits triggers. The second failure mode is the worse of the two, and it is the one that is unguarded.

      The asymmetry, measured

      @objectstack/spec 17.2.0 — defineStack runs exactly two validators that read requires:

      validatorwhat it does
      validateKnownCapabilitiesrejects an unknown token (typo guard)
      validateHierarchyScopeCapabilitythrows when a permission-set grant uses readScope/writeScope of unit / unit_and_below / own_and_reports and requires omits hierarchy-security

      There is no third. Grepping dist/index.js for function validate* bodies that mention requires returns those two and nothing else. A flow that declares a record_change, schedule, time_relative or api trigger while requires omits triggers loads clean, validates clean, builds clean, and never fires.

      What it cost downstream

      objectstack-ai/duly shipped requires: ['automation', 'hierarchy-security']. automation gives a flow engine; it registers no trigger. Measured on @objectstack/cli 17.2.0, PORT=3117 pnpm start:

       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 — …
      

      Zero of four. On that exact tree pnpm validate, pnpm typecheck, pnpm test and pnpm buildall exit 0, and validate prints Logic: 4 Flows and says nothing further. The app's entire automation layer was dark across five merged rounds — a record-change fan-out and three reminder sweeps, every one of them authored correctly — and no gate in the project could see it. Adding the one token flips the same boot to 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api).

      The runtime already knows, at boot

      The banner names the flow, resolves and names the intended trigger type, and prints the exact remedy including the token to add. Everything needed for an author-time diagnostic already exists as a string; it is simply emitted after deploy instead of at authoring. service-automation's own note explains why even that channel is fragile: "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 sees nothing at all.

      The predicate is already computed at author time — it is just never joined to requires

      @objectstack/lint's validate-flow-trigger-readiness.ts already derives exactly the needed fact:

      constisAutoTriggered=isRecordTriggered2||triggerType==="api"||config.schedule!=null||isTimeRelative||flow.type==="schedule"||flow.type==="api";

      It uses it for one finding only — flow-draft-status-ambiguous. The stack object handed to the rule carries requires right there. The two are never joined.

      That file already reasons about trigger availabilityFLOW_TRIGGER_UNROUTABLE's message argues that "a plugin can supply the record-change trigger itself, and it still would not help, because the flow never reaches the point of asking for one." It considers whether a trigger could be supplied, and never asks whether one is declared.

      Why the unguarded case is the worse one

      A hierarchy scope without its capability fails closed: visibility narrows to owner-only. Wrong, but conservative, and a user notices missing rows. An autolaunched flow without its capability fails silent: the automation simply does not happen. Nothing narrows, nothing errors, nobody notices — the tasks that should have been created just do not exist. The platform guards the fail-closed case at authoring time and leaves the fail-silent one to a boot line.

      Proposed

      A sibling to validateHierarchyScopeCapability in defineStack:

      defineStack trigger capability validation failed (1 issue):
      ✗ flow 'duly_assignment_fanout' declares a 'record_change' trigger but `requires` does not
      include 'triggers'. No trigger would be registered, so the flow would never auto-launch.
      Add requires: ['triggers'] (record_change/schedule/time_relative/api ship in
      @objectstack/trigger-*).
      

      The wording already exists — it is what the boot banner prints today.

      Two notes on scope:

      1. triggers is a single token covering all four kinds (PLATFORM_CAPABILITY_PROVIDERS.triggers@objectstack/trigger-record-change plus extras for ScheduleTriggerPlugin, TimeRelativeTriggerPlugin and ApiTriggerPlugin), so the check is one membership test, not a per-kind table.
      2. If a hard error is judged too strong — an app could legitimately ship a type: 'record_change' flow it only ever launches by hand — the smaller version is an error-severity lint rule in validate-flow-trigger-readiness.ts, where the predicate already lives and where pnpm validate would surface it. Either lands the diagnostic before deploy; today neither does.

      Repro

      git clone https://github.com/objectstack-ai/duly &&cd duly && pnpm install
      # edit objectstack.config.ts: requires: ['automation', 'hierarchy-security']
      pnpm validate && pnpm typecheck && pnpm test&& pnpm build # all exit 0
      PORT=3117 pnpm start # "4 flow(s) 0 bound to triggers"

      Found while implementing objectstack-ai/duly#68, which adds the missing token plus a repo-local test/trigger-capability.test.ts stopgap. That stopgap is written to be deleted when this lands.


      Generated by Claude Code

      Metadata

      Metadata

      Assignees

      Type

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , '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

        defineStack hard-errors on an undeclared hierarchy scope but is silent on an undeclared trigger capability — every autolaunched flow ships inert #14153

        Description

        @os-warren

        defineStack refuses to load a stack that grants an ADR-0057 hierarchy scope without requires: ['hierarchy-security']. It accepts, without a word, a stack full of flows that declare triggers while requires omits triggers. The second failure mode is the worse of the two, and it is the one that is unguarded.

        The asymmetry, measured

        @objectstack/spec 17.2.0 — defineStack runs exactly two validators that read requires:

        validatorwhat it does
        validateKnownCapabilitiesrejects an unknown token (typo guard)
        validateHierarchyScopeCapabilitythrows when a permission-set grant uses readScope/writeScope of unit / unit_and_below / own_and_reports and requires omits hierarchy-security

        There is no third. Grepping dist/index.js for function validate* bodies that mention requires returns those two and nothing else. A flow that declares a record_change, schedule, time_relative or api trigger while requires omits triggers loads clean, validates clean, builds clean, and never fires.

        What it cost downstream

        objectstack-ai/duly shipped requires: ['automation', 'hierarchy-security']. automation gives a flow engine; it registers no trigger. Measured on @objectstack/cli 17.2.0, PORT=3117 pnpm start:

         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 — …
        

        Zero of four. On that exact tree pnpm validate, pnpm typecheck, pnpm test and pnpm buildall exit 0, and validate prints Logic: 4 Flows and says nothing further. The app's entire automation layer was dark across five merged rounds — a record-change fan-out and three reminder sweeps, every one of them authored correctly — and no gate in the project could see it. Adding the one token flips the same boot to 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api).

        The runtime already knows, at boot

        The banner names the flow, resolves and names the intended trigger type, and prints the exact remedy including the token to add. Everything needed for an author-time diagnostic already exists as a string; it is simply emitted after deploy instead of at authoring. service-automation's own note explains why even that channel is fragile: "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 sees nothing at all.

        The predicate is already computed at author time — it is just never joined to requires

        @objectstack/lint's validate-flow-trigger-readiness.ts already derives exactly the needed fact:

        constisAutoTriggered=isRecordTriggered2||triggerType==="api"||config.schedule!=null||isTimeRelative||flow.type==="schedule"||flow.type==="api";

        It uses it for one finding only — flow-draft-status-ambiguous. The stack object handed to the rule carries requires right there. The two are never joined.

        That file already reasons about trigger availabilityFLOW_TRIGGER_UNROUTABLE's message argues that "a plugin can supply the record-change trigger itself, and it still would not help, because the flow never reaches the point of asking for one." It considers whether a trigger could be supplied, and never asks whether one is declared.

        Why the unguarded case is the worse one

        A hierarchy scope without its capability fails closed: visibility narrows to owner-only. Wrong, but conservative, and a user notices missing rows. An autolaunched flow without its capability fails silent: the automation simply does not happen. Nothing narrows, nothing errors, nobody notices — the tasks that should have been created just do not exist. The platform guards the fail-closed case at authoring time and leaves the fail-silent one to a boot line.

        Proposed

        A sibling to validateHierarchyScopeCapability in defineStack:

        defineStack trigger capability validation failed (1 issue):
        ✗ flow 'duly_assignment_fanout' declares a 'record_change' trigger but `requires` does not
        include 'triggers'. No trigger would be registered, so the flow would never auto-launch.
        Add requires: ['triggers'] (record_change/schedule/time_relative/api ship in
        @objectstack/trigger-*).
        

        The wording already exists — it is what the boot banner prints today.

        Two notes on scope:

        1. triggers is a single token covering all four kinds (PLATFORM_CAPABILITY_PROVIDERS.triggers@objectstack/trigger-record-change plus extras for ScheduleTriggerPlugin, TimeRelativeTriggerPlugin and ApiTriggerPlugin), so the check is one membership test, not a per-kind table.
        2. If a hard error is judged too strong — an app could legitimately ship a type: 'record_change' flow it only ever launches by hand — the smaller version is an error-severity lint rule in validate-flow-trigger-readiness.ts, where the predicate already lives and where pnpm validate would surface it. Either lands the diagnostic before deploy; today neither does.

        Repro

        git clone https://github.com/objectstack-ai/duly &&cd duly && pnpm install
        # edit objectstack.config.ts: requires: ['automation', 'hierarchy-security']
        pnpm validate && pnpm typecheck && pnpm test&& pnpm build # all exit 0
        PORT=3117 pnpm start # "4 flow(s) 0 bound to triggers"

        Found while implementing objectstack-ai/duly#68, which adds the missing token plus a repo-local test/trigger-capability.test.ts stopgap. That stopgap is written to be deleted when this lands.


        Generated by Claude Code

        Metadata

        Metadata

        Assignees

        Type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , '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

          defineStack hard-errors on an undeclared hierarchy scope but is silent on an undeclared trigger capability — every autolaunched flow ships inert #14153

          Description

          @os-warren

          defineStack refuses to load a stack that grants an ADR-0057 hierarchy scope without requires: ['hierarchy-security']. It accepts, without a word, a stack full of flows that declare triggers while requires omits triggers. The second failure mode is the worse of the two, and it is the one that is unguarded.

          The asymmetry, measured

          @objectstack/spec 17.2.0 — defineStack runs exactly two validators that read requires:

          validatorwhat it does
          validateKnownCapabilitiesrejects an unknown token (typo guard)
          validateHierarchyScopeCapabilitythrows when a permission-set grant uses readScope/writeScope of unit / unit_and_below / own_and_reports and requires omits hierarchy-security

          There is no third. Grepping dist/index.js for function validate* bodies that mention requires returns those two and nothing else. A flow that declares a record_change, schedule, time_relative or api trigger while requires omits triggers loads clean, validates clean, builds clean, and never fires.

          What it cost downstream

          objectstack-ai/duly shipped requires: ['automation', 'hierarchy-security']. automation gives a flow engine; it registers no trigger. Measured on @objectstack/cli 17.2.0, PORT=3117 pnpm start:

           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 — …
          

          Zero of four. On that exact tree pnpm validate, pnpm typecheck, pnpm test and pnpm buildall exit 0, and validate prints Logic: 4 Flows and says nothing further. The app's entire automation layer was dark across five merged rounds — a record-change fan-out and three reminder sweeps, every one of them authored correctly — and no gate in the project could see it. Adding the one token flips the same boot to 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api).

          The runtime already knows, at boot

          The banner names the flow, resolves and names the intended trigger type, and prints the exact remedy including the token to add. Everything needed for an author-time diagnostic already exists as a string; it is simply emitted after deploy instead of at authoring. service-automation's own note explains why even that channel is fragile: "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 sees nothing at all.

          The predicate is already computed at author time — it is just never joined to requires

          @objectstack/lint's validate-flow-trigger-readiness.ts already derives exactly the needed fact:

          constisAutoTriggered=isRecordTriggered2||triggerType==="api"||config.schedule!=null||isTimeRelative||flow.type==="schedule"||flow.type==="api";

          It uses it for one finding only — flow-draft-status-ambiguous. The stack object handed to the rule carries requires right there. The two are never joined.

          That file already reasons about trigger availabilityFLOW_TRIGGER_UNROUTABLE's message argues that "a plugin can supply the record-change trigger itself, and it still would not help, because the flow never reaches the point of asking for one." It considers whether a trigger could be supplied, and never asks whether one is declared.

          Why the unguarded case is the worse one

          A hierarchy scope without its capability fails closed: visibility narrows to owner-only. Wrong, but conservative, and a user notices missing rows. An autolaunched flow without its capability fails silent: the automation simply does not happen. Nothing narrows, nothing errors, nobody notices — the tasks that should have been created just do not exist. The platform guards the fail-closed case at authoring time and leaves the fail-silent one to a boot line.

          Proposed

          A sibling to validateHierarchyScopeCapability in defineStack:

          defineStack trigger capability validation failed (1 issue):
          ✗ flow 'duly_assignment_fanout' declares a 'record_change' trigger but `requires` does not
          include 'triggers'. No trigger would be registered, so the flow would never auto-launch.
          Add requires: ['triggers'] (record_change/schedule/time_relative/api ship in
          @objectstack/trigger-*).
          

          The wording already exists — it is what the boot banner prints today.

          Two notes on scope:

          1. triggers is a single token covering all four kinds (PLATFORM_CAPABILITY_PROVIDERS.triggers@objectstack/trigger-record-change plus extras for ScheduleTriggerPlugin, TimeRelativeTriggerPlugin and ApiTriggerPlugin), so the check is one membership test, not a per-kind table.
          2. If a hard error is judged too strong — an app could legitimately ship a type: 'record_change' flow it only ever launches by hand — the smaller version is an error-severity lint rule in validate-flow-trigger-readiness.ts, where the predicate already lives and where pnpm validate would surface it. Either lands the diagnostic before deploy; today neither does.

          Repro

          git clone https://github.com/objectstack-ai/duly &&cd duly && pnpm install
          # edit objectstack.config.ts: requires: ['automation', 'hierarchy-security']
          pnpm validate && pnpm typecheck && pnpm test&& pnpm build # all exit 0
          PORT=3117 pnpm start # "4 flow(s) 0 bound to triggers"

          Found while implementing objectstack-ai/duly#68, which adds the missing token plus a repo-local test/trigger-capability.test.ts stopgap. That stopgap is written to be deleted when this lands.


          Generated by Claude Code

          Metadata

          Metadata

          Assignees

          Type

          Projects

          No projects

            Milestone

            No milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , '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

            defineStack hard-errors on an undeclared hierarchy scope but is silent on an undeclared trigger capability — every autolaunched flow ships inert #14153

            Description

            @os-warren

            defineStack refuses to load a stack that grants an ADR-0057 hierarchy scope without requires: ['hierarchy-security']. It accepts, without a word, a stack full of flows that declare triggers while requires omits triggers. The second failure mode is the worse of the two, and it is the one that is unguarded.

            The asymmetry, measured

            @objectstack/spec 17.2.0 — defineStack runs exactly two validators that read requires:

            validatorwhat it does
            validateKnownCapabilitiesrejects an unknown token (typo guard)
            validateHierarchyScopeCapabilitythrows when a permission-set grant uses readScope/writeScope of unit / unit_and_below / own_and_reports and requires omits hierarchy-security

            There is no third. Grepping dist/index.js for function validate* bodies that mention requires returns those two and nothing else. A flow that declares a record_change, schedule, time_relative or api trigger while requires omits triggers loads clean, validates clean, builds clean, and never fires.

            What it cost downstream

            objectstack-ai/duly shipped requires: ['automation', 'hierarchy-security']. automation gives a flow engine; it registers no trigger. Measured on @objectstack/cli 17.2.0, PORT=3117 pnpm start:

             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 — …
            

            Zero of four. On that exact tree pnpm validate, pnpm typecheck, pnpm test and pnpm buildall exit 0, and validate prints Logic: 4 Flows and says nothing further. The app's entire automation layer was dark across five merged rounds — a record-change fan-out and three reminder sweeps, every one of them authored correctly — and no gate in the project could see it. Adding the one token flips the same boot to 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api).

            The runtime already knows, at boot

            The banner names the flow, resolves and names the intended trigger type, and prints the exact remedy including the token to add. Everything needed for an author-time diagnostic already exists as a string; it is simply emitted after deploy instead of at authoring. service-automation's own note explains why even that channel is fragile: "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 sees nothing at all.

            The predicate is already computed at author time — it is just never joined to requires

            @objectstack/lint's validate-flow-trigger-readiness.ts already derives exactly the needed fact:

            constisAutoTriggered=isRecordTriggered2||triggerType==="api"||config.schedule!=null||isTimeRelative||flow.type==="schedule"||flow.type==="api";

            It uses it for one finding only — flow-draft-status-ambiguous. The stack object handed to the rule carries requires right there. The two are never joined.

            That file already reasons about trigger availabilityFLOW_TRIGGER_UNROUTABLE's message argues that "a plugin can supply the record-change trigger itself, and it still would not help, because the flow never reaches the point of asking for one." It considers whether a trigger could be supplied, and never asks whether one is declared.

            Why the unguarded case is the worse one

            A hierarchy scope without its capability fails closed: visibility narrows to owner-only. Wrong, but conservative, and a user notices missing rows. An autolaunched flow without its capability fails silent: the automation simply does not happen. Nothing narrows, nothing errors, nobody notices — the tasks that should have been created just do not exist. The platform guards the fail-closed case at authoring time and leaves the fail-silent one to a boot line.

            Proposed

            A sibling to validateHierarchyScopeCapability in defineStack:

            defineStack trigger capability validation failed (1 issue):
            ✗ flow 'duly_assignment_fanout' declares a 'record_change' trigger but `requires` does not
            include 'triggers'. No trigger would be registered, so the flow would never auto-launch.
            Add requires: ['triggers'] (record_change/schedule/time_relative/api ship in
            @objectstack/trigger-*).
            

            The wording already exists — it is what the boot banner prints today.

            Two notes on scope:

            1. triggers is a single token covering all four kinds (PLATFORM_CAPABILITY_PROVIDERS.triggers@objectstack/trigger-record-change plus extras for ScheduleTriggerPlugin, TimeRelativeTriggerPlugin and ApiTriggerPlugin), so the check is one membership test, not a per-kind table.
            2. If a hard error is judged too strong — an app could legitimately ship a type: 'record_change' flow it only ever launches by hand — the smaller version is an error-severity lint rule in validate-flow-trigger-readiness.ts, where the predicate already lives and where pnpm validate would surface it. Either lands the diagnostic before deploy; today neither does.

            Repro

            git clone https://github.com/objectstack-ai/duly &&cd duly && pnpm install
            # edit objectstack.config.ts: requires: ['automation', 'hierarchy-security']
            pnpm validate && pnpm typecheck && pnpm test&& pnpm build # all exit 0
            PORT=3117 pnpm start # "4 flow(s) 0 bound to triggers"

            Found while implementing objectstack-ai/duly#68, which adds the missing token plus a repo-local test/trigger-capability.test.ts stopgap. That stopgap is written to be deleted when this lands.


            Generated by Claude Code

            Metadata

            Metadata

            Assignees

            Type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , '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

              defineStack hard-errors on an undeclared hierarchy scope but is silent on an undeclared trigger capability — every autolaunched flow ships inert #14153

              Description

              @os-warren

              defineStack refuses to load a stack that grants an ADR-0057 hierarchy scope without requires: ['hierarchy-security']. It accepts, without a word, a stack full of flows that declare triggers while requires omits triggers. The second failure mode is the worse of the two, and it is the one that is unguarded.

              The asymmetry, measured

              @objectstack/spec 17.2.0 — defineStack runs exactly two validators that read requires:

              validatorwhat it does
              validateKnownCapabilitiesrejects an unknown token (typo guard)
              validateHierarchyScopeCapabilitythrows when a permission-set grant uses readScope/writeScope of unit / unit_and_below / own_and_reports and requires omits hierarchy-security

              There is no third. Grepping dist/index.js for function validate* bodies that mention requires returns those two and nothing else. A flow that declares a record_change, schedule, time_relative or api trigger while requires omits triggers loads clean, validates clean, builds clean, and never fires.

              What it cost downstream

              objectstack-ai/duly shipped requires: ['automation', 'hierarchy-security']. automation gives a flow engine; it registers no trigger. Measured on @objectstack/cli 17.2.0, PORT=3117 pnpm start:

               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 — …
              

              Zero of four. On that exact tree pnpm validate, pnpm typecheck, pnpm test and pnpm buildall exit 0, and validate prints Logic: 4 Flows and says nothing further. The app's entire automation layer was dark across five merged rounds — a record-change fan-out and three reminder sweeps, every one of them authored correctly — and no gate in the project could see it. Adding the one token flips the same boot to 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api).

              The runtime already knows, at boot

              The banner names the flow, resolves and names the intended trigger type, and prints the exact remedy including the token to add. Everything needed for an author-time diagnostic already exists as a string; it is simply emitted after deploy instead of at authoring. service-automation's own note explains why even that channel is fragile: "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 sees nothing at all.

              The predicate is already computed at author time — it is just never joined to requires

              @objectstack/lint's validate-flow-trigger-readiness.ts already derives exactly the needed fact:

              constisAutoTriggered=isRecordTriggered2||triggerType==="api"||config.schedule!=null||isTimeRelative||flow.type==="schedule"||flow.type==="api";

              It uses it for one finding only — flow-draft-status-ambiguous. The stack object handed to the rule carries requires right there. The two are never joined.

              That file already reasons about trigger availabilityFLOW_TRIGGER_UNROUTABLE's message argues that "a plugin can supply the record-change trigger itself, and it still would not help, because the flow never reaches the point of asking for one." It considers whether a trigger could be supplied, and never asks whether one is declared.

              Why the unguarded case is the worse one

              A hierarchy scope without its capability fails closed: visibility narrows to owner-only. Wrong, but conservative, and a user notices missing rows. An autolaunched flow without its capability fails silent: the automation simply does not happen. Nothing narrows, nothing errors, nobody notices — the tasks that should have been created just do not exist. The platform guards the fail-closed case at authoring time and leaves the fail-silent one to a boot line.

              Proposed

              A sibling to validateHierarchyScopeCapability in defineStack:

              defineStack trigger capability validation failed (1 issue):
              ✗ flow 'duly_assignment_fanout' declares a 'record_change' trigger but `requires` does not
              include 'triggers'. No trigger would be registered, so the flow would never auto-launch.
              Add requires: ['triggers'] (record_change/schedule/time_relative/api ship in
              @objectstack/trigger-*).
              

              The wording already exists — it is what the boot banner prints today.

              Two notes on scope:

              1. triggers is a single token covering all four kinds (PLATFORM_CAPABILITY_PROVIDERS.triggers@objectstack/trigger-record-change plus extras for ScheduleTriggerPlugin, TimeRelativeTriggerPlugin and ApiTriggerPlugin), so the check is one membership test, not a per-kind table.
              2. If a hard error is judged too strong — an app could legitimately ship a type: 'record_change' flow it only ever launches by hand — the smaller version is an error-severity lint rule in validate-flow-trigger-readiness.ts, where the predicate already lives and where pnpm validate would surface it. Either lands the diagnostic before deploy; today neither does.

              Repro

              git clone https://github.com/objectstack-ai/duly &&cd duly && pnpm install
              # edit objectstack.config.ts: requires: ['automation', 'hierarchy-security']
              pnpm validate && pnpm typecheck && pnpm test&& pnpm build # all exit 0
              PORT=3117 pnpm start # "4 flow(s) 0 bound to triggers"

              Found while implementing objectstack-ai/duly#68, which adds the missing token plus a repo-local test/trigger-capability.test.ts stopgap. That stopgap is written to be deleted when this lands.


              Generated by Claude Code

              Metadata

              Metadata

              Assignees

              Type

              Projects

              No projects

                Milestone

                No milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , '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

                defineStack hard-errors on an undeclared hierarchy scope but is silent on an undeclared trigger capability — every autolaunched flow ships inert #14153

                Description

                @os-warren

                defineStack refuses to load a stack that grants an ADR-0057 hierarchy scope without requires: ['hierarchy-security']. It accepts, without a word, a stack full of flows that declare triggers while requires omits triggers. The second failure mode is the worse of the two, and it is the one that is unguarded.

                The asymmetry, measured

                @objectstack/spec 17.2.0 — defineStack runs exactly two validators that read requires:

                validatorwhat it does
                validateKnownCapabilitiesrejects an unknown token (typo guard)
                validateHierarchyScopeCapabilitythrows when a permission-set grant uses readScope/writeScope of unit / unit_and_below / own_and_reports and requires omits hierarchy-security

                There is no third. Grepping dist/index.js for function validate* bodies that mention requires returns those two and nothing else. A flow that declares a record_change, schedule, time_relative or api trigger while requires omits triggers loads clean, validates clean, builds clean, and never fires.

                What it cost downstream

                objectstack-ai/duly shipped requires: ['automation', 'hierarchy-security']. automation gives a flow engine; it registers no trigger. Measured on @objectstack/cli 17.2.0, PORT=3117 pnpm start:

                 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 — …
                

                Zero of four. On that exact tree pnpm validate, pnpm typecheck, pnpm test and pnpm buildall exit 0, and validate prints Logic: 4 Flows and says nothing further. The app's entire automation layer was dark across five merged rounds — a record-change fan-out and three reminder sweeps, every one of them authored correctly — and no gate in the project could see it. Adding the one token flips the same boot to 4 flow(s) 4 bound to triggers (record_change, schedule, time_relative, api).

                The runtime already knows, at boot

                The banner names the flow, resolves and names the intended trigger type, and prints the exact remedy including the token to add. Everything needed for an author-time diagnostic already exists as a string; it is simply emitted after deploy instead of at authoring. service-automation's own note explains why even that channel is fragile: "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 sees nothing at all.

                The predicate is already computed at author time — it is just never joined to requires

                @objectstack/lint's validate-flow-trigger-readiness.ts already derives exactly the needed fact:

                constisAutoTriggered=isRecordTriggered2||triggerType==="api"||config.schedule!=null||isTimeRelative||flow.type==="schedule"||flow.type==="api";

                It uses it for one finding only — flow-draft-status-ambiguous. The stack object handed to the rule carries requires right there. The two are never joined.

                That file already reasons about trigger availabilityFLOW_TRIGGER_UNROUTABLE's message argues that "a plugin can supply the record-change trigger itself, and it still would not help, because the flow never reaches the point of asking for one." It considers whether a trigger could be supplied, and never asks whether one is declared.

                Why the unguarded case is the worse one

                A hierarchy scope without its capability fails closed: visibility narrows to owner-only. Wrong, but conservative, and a user notices missing rows. An autolaunched flow without its capability fails silent: the automation simply does not happen. Nothing narrows, nothing errors, nobody notices — the tasks that should have been created just do not exist. The platform guards the fail-closed case at authoring time and leaves the fail-silent one to a boot line.

                Proposed

                A sibling to validateHierarchyScopeCapability in defineStack:

                defineStack trigger capability validation failed (1 issue):
                ✗ flow 'duly_assignment_fanout' declares a 'record_change' trigger but `requires` does not
                include 'triggers'. No trigger would be registered, so the flow would never auto-launch.
                Add requires: ['triggers'] (record_change/schedule/time_relative/api ship in
                @objectstack/trigger-*).
                

                The wording already exists — it is what the boot banner prints today.

                Two notes on scope:

                1. triggers is a single token covering all four kinds (PLATFORM_CAPABILITY_PROVIDERS.triggers@objectstack/trigger-record-change plus extras for ScheduleTriggerPlugin, TimeRelativeTriggerPlugin and ApiTriggerPlugin), so the check is one membership test, not a per-kind table.
                2. If a hard error is judged too strong — an app could legitimately ship a type: 'record_change' flow it only ever launches by hand — the smaller version is an error-severity lint rule in validate-flow-trigger-readiness.ts, where the predicate already lives and where pnpm validate would surface it. Either lands the diagnostic before deploy; today neither does.

                Repro

                git clone https://github.com/objectstack-ai/duly &&cd duly && pnpm install
                # edit objectstack.config.ts: requires: ['automation', 'hierarchy-security']
                pnpm validate && pnpm typecheck && pnpm test&& pnpm build # all exit 0
                PORT=3117 pnpm start # "4 flow(s) 0 bound to triggers"

                Found while implementing objectstack-ai/duly#68, which adds the missing token plus a repo-local test/trigger-capability.test.ts stopgap. That stopgap is written to be deleted when this lands.


                Generated by Claude Code

                Metadata

                Metadata

                Assignees

                Type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions