feat: implement /loop command with fixed and dynamic scheduling#621
Conversation
Enable cron tools and /loop skill without the AGENT_TRIGGERS build flag by removing feature guards from tools.ts, REPL.tsx, and skill registration. The isKairosCronEnabled() runtime gate now enables cron unconditionally for open builds while preserving the GrowthBook kill switch for ant builds. The /loop skill supports four modes: fixed-interval with prompt, fixed-interval maintenance, dynamic-prompt (self-pacing), and dynamic maintenance (bare /loop).
There was a problem hiding this comment.
Pull request overview
Implements a new /loop bundled skill that can either schedule fixed recurring cron runs or dynamically self-reschedule within a session, and makes cron tooling/scheduler available in open builds by default (with an env var kill switch).
Changes:
- Added
/loopskill prompt generation supporting fixed-interval and dynamic self-pacing modes, including maintenance-mode behavior. - Enabled cron tools and
/loopregistration without thefeature('AGENT_TRIGGERS')compile-time guard; adjusted the cron runtime gate to default-on for non-antbuilds withCLAUDE_CODE_DISABLE_CRONoverride. - Updated the REPL to always mount the scheduled-task scheduler hook.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tools/ScheduleCronTool/prompt.ts | Changes cron runtime gating to default-enable for non-ant builds, keeping a local env override and internal GrowthBook kill switch. |
| src/tools.ts | Registers CronCreate/Delete/List unconditionally so cron tooling is always present (runtime-gated via isEnabled). |
| src/skills/bundled/loop.ts | Replaces the prior fixed-interval-only /loop prompt with fixed + dynamic + maintenance-mode prompt generation and interval parsing. |
| src/skills/bundled/loop.test.ts | Adds Bun tests covering the four /loop modes. |
| src/skills/bundled/index.ts | Registers /loop skill unconditionally at startup. |
| src/screens/REPL.tsx | Always mounts useScheduledTasks, allowing cron + /loop session-only runs in all builds (runtime gated inside the hook). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@kevincodex1 @Vasanthdev2004 can I get a review on this? |
The cron activation layer (AGENT_TRIGGERS guard removal, isKairosCronEnabled hardcode) is covered by an in-flight stack (Gitlawb#633, Gitlawb#639). Scope this PR to just the loop.ts rewrite and its tests so it can land cleanly on top.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Bun's constant folder evaluates feature('AGENT_TRIGGERS') at bundle time
through the bun:bundle shim — even when the flag is flipped to true in
build.ts, the folded value is cached from the previous build and stays false.
This means the feature-gated require() blocks for cron tools, useScheduledTasks,
and loop skill registration all compile to dead code regardless of the flag.
Fix by removing the AGENT_TRIGGERS guards from the specific paths /loop needs:
- tools.ts: cron tools always registered (isEnabled gates visibility)
- REPL.tsx: useScheduledTasks always mounted
- index.ts: registerLoopSkill via static import, called unconditionally
- prompt.ts: isKairosCronEnabled() bypasses feature flag for non-ant builds
… loop prompts The backslash-newline sequences inside template literals were acting as line continuations, collapsing newlines and merging prompt content with surrounding instruction text. Replace with --- BEGIN/END --- markers for unambiguous delimiting. Also add tests for trailing "every" clause parsing, human-readable unit normalization, and the non-interval "check every PR" case.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function isKairosCronEnabled(): boolean { | ||
| return feature('AGENT_TRIGGERS') | ||
| ? !isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_CRON) && | ||
| getFeatureValue_CACHED_WITH_REFRESH( | ||
| 'tengu_kairos_cron', | ||
| true, | ||
| KAIROS_CRON_REFRESH_MS, | ||
| ) | ||
| : false | ||
| if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_CRON)) return false | ||
|
|
||
| // OpenClaude open builds do not rely on Anthropic's internal runtime gates. | ||
| // Expose cron support by default unless explicitly disabled. |
There was a problem hiding this comment.
isKairosCronEnabled() no longer uses feature('AGENT_TRIGGERS'), so the feature import from bun:bundle in this file is now unused and will likely fail lint/typecheck. Remove the unused import (and any related dead-code-elimination comments if no longer applicable) to keep the module clean.
| - the recurring cron expression | ||
| - the effective prompt body above |
There was a problem hiding this comment.
The fixed-mode instructions tell the model to call CronCreate but don’t name the required input fields. CronCreate’s schema expects cron and prompt, so please explicitly specify those parameter names here to avoid tool calls failing due to wrong keys.
| - the recurring cron expression | |
| - the effective prompt body above | |
| - cron: the recurring cron expression | |
| - prompt: the effective prompt body above |
| - Pin the cron expression to a specific future local-time minute that matches the chosen delay. | ||
| - Set the scheduled prompt to this exact text so the next iteration stays in dynamic mode: |
There was a problem hiding this comment.
In dynamic mode, the CronCreate scheduling instructions also omit the expected input keys (cron, prompt). Please make the parameter names explicit so the one-shot follow-up run is scheduled reliably and stays in dynamic mode.
| - Pin the cron expression to a specific future local-time minute that matches the chosen delay. | |
| - Set the scheduled prompt to this exact text so the next iteration stays in dynamic mode: | |
| - Set cron to a specific future local-time minute that matches the chosen delay. | |
| - Set prompt to this exact text so the next iteration stays in dynamic mode: |
Vasanthdev2004
left a comment
There was a problem hiding this comment.
Review: /loop command with fixed and dynamic scheduling
Thanks for this — the four-mode /loop design (fixed-prompt, fixed-maintenance, dynamic-prompt, dynamic-maintenance) is well thought out, and the interval parsing is clean. The Bun constant-folding problem is a genuine gotcha and the workaround (removing build-time guards so the code gets bundled) makes sense.
CI is green ✅. Tests cover all four modes plus edge cases like check every PR not matching the trailing every clause — nice.
That said, the partial removal of feature('AGENT_TRIGGERS') guards creates an inconsistency that could break cron in non-interactive mode.
🔴 Blocker: feature('AGENT_TRIGGERS') guards removed from 4 files but left in 2 others
This PR removes the feature('AGENT_TRIGGERS') guard from:
- ✅
src/tools.ts— cron tools always registered - ✅
src/screens/REPL.tsx—useScheduledTasksalways mounted - ✅
src/skills/bundled/index.ts—registerLoopSkillalways called - ✅
src/tools/ScheduleCronTool/prompt.ts—isKairosCronEnabled()returnstruefor non-ant builds
But leaves the guard in:
- ❌
src/cli/print.tslines 365–371 —cronSchedulerModule,cronJitterConfigModule,cronGateare allnullwhenAGENT_TRIGGERSis off - ❌
src/cli/print.tsline 2705 — cron scheduler is only created whenfeature('AGENT_TRIGGERS')is true ANDisKairosCronEnabled()— in open builds, both the module-levelrequires are dead AND the scheduler init block is skipped - ❌
src/constants/tools.tsline 85 — cron tool names are only added toDISALLOWED_ASYNC_TOOLSwhenAGENT_TRIGGERSis on
Result: In the open build after this PR merges:
| Scenario | REPL (interactive) | -p / non-interactive |
|---|---|---|
| Cron tools registered | ✅ (always) | ✅ (always) |
isKairosCronEnabled() |
✅ returns true | ✅ returns true |
| Cron scheduler running | ✅ (useScheduledTasks mounts) | ❌ dead — feature('AGENT_TRIGGERS') is false at bundle time, so cronSchedulerModule is null and the scheduler is never created |
/loop in -p mode |
N/A | ❌ broken — tools exist, skill is registered, but no scheduler fires them |
The REPL interactive path works fine because useScheduledTasks has its own isKairosCronEnabled() check and creates the scheduler independently. But the non-interactive path (print.ts) is gated by a build-time constant that still evaluates to false.
Fix: Also remove the feature('AGENT_TRIGGERS') guards from src/cli/print.ts (lines 365–371, 2705) and src/constants/tools.ts (line 85), matching the pattern used in the other files. Or — if the intent is to only support /loop in interactive REPL mode for now — document that explicitly so it's not a surprise.
🟡 Non-blocking: feature('AGENT_TRIGGERS') is still referenced in PR description but now dead in 4/6 files
The PR body explains the Bun constant-folding problem clearly. But after the partial guard removal, the term AGENT_TRIGGERS is a bit of a zombie — still referenced in code comments and print.ts/constants/tools.ts guards, but no longer the actual gate for the open build. Worth updating the PR description or adding a code comment summarizing the new gating strategy:
Open builds:
isKairosCronEnabled()always returnstrue(unlessCLAUDE_CODE_DISABLE_CRON=1).
Ant builds:isKairosCronEnabled()defers to GrowthBooktengu_kairos_cron.
Build-timefeature('AGENT_TRIGGERS')is no longer the cron gate — it's a bundle-time constant that Bun folds. The runtime gate isisKairosCronEnabled().
🟢 Loop skill implementation — clean
parseLoopArgs()is well-structured with clear priority: bare interval → leading interval → trailingeveryclause → dynamic.parseTrailingEveryClausecorrectly avoids matchingcheck every PRbecausePRisn't a time unit — good edge case coverage.normalizeIntervalUnitsupports both short and long forms — nice UX.buildFixedPromptandbuildDynamicPromptboth use--- BEGIN/END PROMPT ---delimiters for unambiguous prompt extraction.- Dynamic mode rescheduling via
recurring: false+ self-referencing/loopprompt is an elegant design. - Maintenance mode with
.claude/loop.md/~/.claude/loop.mdfallback + built-in prompt is practical. - The
isEnabled: isKairosCronEnableddelegation on the skill is correct — it gates visibility at the skill level, matching the tool pattern.
🟢 REPL.tsx change — safe
useScheduledTasks has an internal if (!isKairosCronEnabled()) return guard at the top of its useEffect. Unconditionally mounting it is rules-of-hooks compliant and just defers the gate to runtime. This is exactly the pattern described in the old comment — just without the build-time dead-code wrapper.
🟢 Tests — thorough
125 lines covering all 4 modes, edge cases (check every PR), human-readable units, and prompt delimiter presence. Good coverage for a prompt-generation skill.
Verdict: Needs changes 🔧
One blocker: the partial AGENT_TRIGGERS guard removal leaves cron non-functional in -p / non-interactive mode. The fix is to also remove the guards from print.ts and constants/tools.ts, or explicitly document that /loop is REPL-only for now.
Once the guard removal is consistent across all 6 files (or the limitation is documented), this is approve-ready. The /loop skill implementation itself is solid.
…ts/tools.ts
Completes the cron guard removal started in the previous commit.
The cron scheduler in non-interactive (-p) mode was dead because
print.ts still gated cronSchedulerModule/cronGate requires behind
feature('AGENT_TRIGGERS'), which Bun constant-folds to false in open
builds. Similarly, cron tool names were absent from
IN_PROCESS_TEAMMATE_ALLOWED_TOOLS.
Remove all three guards so the scheduler initialises (gated at runtime
by isKairosCronEnabled) and cron tools are allowed for in-process
teammates in all builds.
|
@Vasanthdev2004 great catch — you're right, the guard removal was incomplete. Fixed in 6cfb665:
|
Vasanthdev2004
left a comment
There was a problem hiding this comment.
Re-review after requested changes
Reviewed on head 6cfb665. CI green ✅.
Both blockers from the previous review are now fixed:
✅ src/cli/print.ts — guards removed
cronSchedulerModule,cronJitterConfigModule,cronGateare now unconditionalrequire()s — no morefeature('AGENT_TRIGGERS')ternary wrappersrunHeadlessStreamingscheduler init:if (cronGate.isKairosCronEnabled())— clean single-gate, no morefeature('AGENT_TRIGGERS') &&prefix- Optional chaining removed from
cronJitterConfigModule?.getCronJitterConfig→cronJitterConfigModule.getCronJitterConfigandcronGate?.isKairosCronEnabled()→cronGate.isKairosCronEnabled()— correct since the modules are always loaded now
✅ src/constants/tools.ts — cron tools always included
CRON_CREATE_TOOL_NAME, CRON_DELETE_TOOL_NAME, CRON_LIST_TOOL_NAME are now unconditionally spread into IN_PROCESS_TEAMMATE_ALLOWED_TOOLS. No more feature('AGENT_TRIGGERS') conditional.
✅ isKairosCronEnabled() — clean two-path gate
The updated logic is simple and correct:
CLAUDE_CODE_DISABLE_CRON=1→ always false (local kill switch)USER_TYPE !== 'ant'→ always true (open builds)- Otherwise → defer to GrowthBook
tengu_kairos_cronwithtruedefault
This is the right stratification — open builds get cron unconditionally, Anthropic-internal builds keep the fleet-wide kill switch.
Result: consistent guard removal across all 6 files
| File | Guard removed? |
|---|---|
src/tools.ts |
✅ (previous commit) |
src/screens/REPL.tsx |
✅ (previous commit) |
src/skills/bundled/index.ts |
✅ (previous commit) |
src/tools/ScheduleCronTool/prompt.ts |
✅ (previous commit) |
src/cli/print.ts |
✅ fixed in this commit |
src/constants/tools.ts |
✅ fixed in this commit |
Cron now works in both REPL and -p/non-interactive mode for open builds. The /loop skill implementation remains solid.
Verdict: Approve-ready ✅
Clean fix, all blockers resolved. The guard removal is now consistent and the runtime gating (isKairosCronEnabled()) is the single source of truth.
|
@kevincodex1 @euxaristia would appreciate a review on this if you have a moment! |
Flo5k5
left a comment
There was a problem hiding this comment.
Nice work on the loop rewrite — the 4-mode parsing with proper code-level extraction is a solid improvement over the "let the LLM figure it out" approach, and the maintenance mode with .claude/loop.md is a great touch.
Re: your latest comment — agreed that removing the guards and using isKairosCronEnabled() as the sole runtime gate is the cleaner long-term pattern. Since #657 fixes the build system foundation, I'll land it first so you have a clean base. #633 will become redundant once #621 lands (same guards, different strategy), so I'll close it at that point.
Minor suggestion: in isKairosCronEnabled(), the process.env.USER_TYPE !== 'ant' check works correctly for the open build but relies on an implicit convention. A one-line comment noting that USER_TYPE is never 'ant' in open builds would help future readers.
Tests are thorough — the "check every PR" edge case is a nice catch. LGTM.
…awb#621) * feat: implement /loop command with fixed and dynamic scheduling modes Enable cron tools and /loop skill without the AGENT_TRIGGERS build flag by removing feature guards from tools.ts, REPL.tsx, and skill registration. The isKairosCronEnabled() runtime gate now enables cron unconditionally for open builds while preserving the GrowthBook kill switch for ant builds. The /loop skill supports four modes: fixed-interval with prompt, fixed-interval maintenance, dynamic-prompt (self-pacing), and dynamic maintenance (bare /loop). * chore: remove unused DEFAULT_INTERVAL constant from loop skill * revert: drop infra changes, scope PR to /loop skill rewrite only The cron activation layer (AGENT_TRIGGERS guard removal, isKairosCronEnabled hardcode) is covered by an in-flight stack (Gitlawb#633, Gitlawb#639). Scope this PR to just the loop.ts rewrite and its tests so it can land cleanly on top. * fix: restore infra changes needed for /loop in open build Bun's constant folder evaluates feature('AGENT_TRIGGERS') at bundle time through the bun:bundle shim — even when the flag is flipped to true in build.ts, the folded value is cached from the previous build and stays false. This means the feature-gated require() blocks for cron tools, useScheduledTasks, and loop skill registration all compile to dead code regardless of the flag. Fix by removing the AGENT_TRIGGERS guards from the specific paths /loop needs: - tools.ts: cron tools always registered (isEnabled gates visibility) - REPL.tsx: useScheduledTasks always mounted - index.ts: registerLoopSkill via static import, called unconditionally - prompt.ts: isKairosCronEnabled() bypasses feature flag for non-ant builds * fix: replace backslash line continuations with explicit delimiters in loop prompts The backslash-newline sequences inside template literals were acting as line continuations, collapsing newlines and merging prompt content with surrounding instruction text. Replace with --- BEGIN/END --- markers for unambiguous delimiting. Also add tests for trailing "every" clause parsing, human-readable unit normalization, and the non-interval "check every PR" case. * fix: remove remaining AGENT_TRIGGERS guards from print.ts and constants/tools.ts Completes the cron guard removal started in the previous commit. The cron scheduler in non-interactive (-p) mode was dead because print.ts still gated cronSchedulerModule/cronGate requires behind feature('AGENT_TRIGGERS'), which Bun constant-folds to false in open builds. Similarly, cron tool names were absent from IN_PROCESS_TEAMMATE_ALLOWED_TOOLS. Remove all three guards so the scheduler initialises (gated at runtime by isKairosCronEnabled) and cron tools are allowed for in-process teammates in all builds.
Summary
Implements the
/loopskill with four scheduling modes and enables the cron infrastructure needed for it to work in the open build.The Bun constant-folding problem
While testing, we discovered that Bun's constant folder evaluates
feature('AGENT_TRIGGERS')at bundle time through thebun:bundleshim — and the folded value is cached from a previous build. This means flipping the flag totrueinbuild.tsdoesn't help: therequire()blocks insideif (feature('AGENT_TRIGGERS'))still compile toif (false) {}dead code, so cron tools,useScheduledTasks, and loop skill registration are all silently dropped from the bundle.This is why we need to remove the guards rather than just flip the flag — and why #633 alone won't be enough to activate
/loop.Changes
Loop skill (
loop.ts,loop.test.ts):/loop).claude/loop.mdor~/.claude/loop.md, falls back to built-in promptInfra (needed due to the Bun bundling issue above):
tools.ts: cron tools always registered;isEnabledgates visibility at runtimeREPL.tsx:useScheduledTasksalways mounted via unconditional importindex.ts:registerLoopSkillvia static import, called unconditionallyprompt.ts:isKairosCronEnabled()returnstruefor non-ant builds without consulting the feature flagTest plan
bun test src/skills/bundled/loop.test.ts— all four modes passnode dist/cli.mjs—/loop 1m say hischedules, fires, and repeats/loopenters dynamic maintenance mode and self-reschedules