Skip to content

feat: implement /loop command with fixed and dynamic scheduling#621

Merged
kevincodex1 merged 6 commits into
Gitlawb:mainfrom
khaledmoayad:feature/loop-command
Apr 13, 2026
Merged

feat: implement /loop command with fixed and dynamic scheduling#621
kevincodex1 merged 6 commits into
Gitlawb:mainfrom
khaledmoayad:feature/loop-command

Conversation

@khaledmoayad
Copy link
Copy Markdown
Contributor

@khaledmoayad khaledmoayad commented Apr 12, 2026

Summary

Implements the /loop skill 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 the bun:bundle shim — and the folded value is cached from a previous build. This means flipping the flag to true in build.ts doesn't help: the require() blocks inside if (feature('AGENT_TRIGGERS')) still compile to if (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):

  • Four modes: fixed-interval with prompt, fixed-interval maintenance, dynamic self-pacing, dynamic maintenance (bare /loop)
  • Interval parsing accepts leading tokens, trailing "every" clauses, and human-readable units
  • Maintenance mode reads .claude/loop.md or ~/.claude/loop.md, falls back to built-in prompt

Infra (needed due to the Bun bundling issue above):

  • tools.ts: cron tools always registered; isEnabled gates visibility at runtime
  • REPL.tsx: useScheduledTasks always mounted via unconditional import
  • index.ts: registerLoopSkill via static import, called unconditionally
  • prompt.ts: isKairosCronEnabled() returns true for non-ant builds without consulting the feature flag

Test plan

  • bun test src/skills/bundled/loop.test.ts — all four modes pass
  • node dist/cli.mjs/loop 1m say hi schedules, fires, and repeats
  • bare /loop enters dynamic maintenance mode and self-reschedules

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).
Copilot AI review requested due to automatic review settings April 12, 2026 15:23
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 /loop skill prompt generation supporting fixed-interval and dynamic self-pacing modes, including maintenance-mode behavior.
  • Enabled cron tools and /loop registration without the feature('AGENT_TRIGGERS') compile-time guard; adjusted the cron runtime gate to default-on for non-ant builds with CLAUDE_CODE_DISABLE_CRON override.
  • 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.

Comment thread src/tools.ts
Comment thread src/screens/REPL.tsx
Comment thread src/skills/bundled/loop.ts
Comment thread src/skills/bundled/loop.ts Outdated
@khaledmoayad
Copy link
Copy Markdown
Contributor Author

@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.
Copilot AI review requested due to automatic review settings April 12, 2026 23:07
@khaledmoayad khaledmoayad changed the title feat: implement /loop command with fixed and dynamic scheduling feat: rewrite /loop skill with dynamic and maintenance modes Apr 12, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/skills/bundled/loop.ts Outdated
Comment thread src/skills/bundled/loop.ts Outdated
Comment thread src/skills/bundled/loop.ts Outdated
Comment thread src/skills/bundled/loop.test.ts
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
@khaledmoayad khaledmoayad changed the title feat: rewrite /loop skill with dynamic and maintenance modes feat: implement /loop command with fixed and dynamic scheduling Apr 12, 2026
… 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.
Copilot AI review requested due to automatic review settings April 12, 2026 23:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 29 to +33
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.
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +142 to +143
- the recurring cron expression
- the effective prompt body above
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
- the recurring cron expression
- the effective prompt body above
- cron: the recurring cron expression
- prompt: the effective prompt body above

Copilot uses AI. Check for mistakes.
Comment on lines +192 to +193
- 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:
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
- 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:

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

@Vasanthdev2004 Vasanthdev2004 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.tsxuseScheduledTasks always mounted
  • src/skills/bundled/index.tsregisterLoopSkill always called
  • src/tools/ScheduleCronTool/prompt.tsisKairosCronEnabled() returns true for non-ant builds

But leaves the guard in:

  • src/cli/print.ts lines 365–371 — cronSchedulerModule, cronJitterConfigModule, cronGate are all null when AGENT_TRIGGERS is off
  • src/cli/print.ts line 2705 — cron scheduler is only created when feature('AGENT_TRIGGERS') is true AND isKairosCronEnabled() — in open builds, both the module-level requires are dead AND the scheduler init block is skipped
  • src/constants/tools.ts line 85 — cron tool names are only added to DISALLOWED_ASYNC_TOOLS when AGENT_TRIGGERS is 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) deadfeature('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 returns true (unless CLAUDE_CODE_DISABLE_CRON=1).
Ant builds: isKairosCronEnabled() defers to GrowthBook tengu_kairos_cron.
Build-time feature('AGENT_TRIGGERS') is no longer the cron gate — it's a bundle-time constant that Bun folds. The runtime gate is isKairosCronEnabled().


🟢 Loop skill implementation — clean

  • parseLoopArgs() is well-structured with clear priority: bare interval → leading interval → trailing every clause → dynamic.
  • parseTrailingEveryClause correctly avoids matching check every PR because PR isn't a time unit — good edge case coverage.
  • normalizeIntervalUnit supports both short and long forms — nice UX.
  • buildFixedPrompt and buildDynamicPrompt both use --- BEGIN/END PROMPT --- delimiters for unambiguous prompt extraction.
  • Dynamic mode rescheduling via recurring: false + self-referencing /loop prompt is an elegant design.
  • Maintenance mode with .claude/loop.md / ~/.claude/loop.md fallback + built-in prompt is practical.
  • The isEnabled: isKairosCronEnabled delegation 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.
@khaledmoayad
Copy link
Copy Markdown
Contributor Author

@Vasanthdev2004 great catch — you're right, the guard removal was incomplete.

Fixed in 6cfb665:

  • src/cli/print.ts: cronSchedulerModule, cronJitterConfigModule, and cronGate are now unconditional requires. The scheduler init block drops the feature('AGENT_TRIGGERS') check and delegates entirely to cronGate.isKairosCronEnabled() (which returns true for non-ant builds unless CLAUDE_CODE_DISABLE_CRON=1).
  • src/constants/tools.ts: Cron tool names are always included in IN_PROCESS_TEAMMATE_ALLOWED_TOOLS — no more feature-gated spread.

/loop should now work correctly in -p / non-interactive mode as well as the REPL.

Copy link
Copy Markdown
Collaborator

@Vasanthdev2004 Vasanthdev2004 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, cronGate are now unconditional require()s — no more feature('AGENT_TRIGGERS') ternary wrappers
  • runHeadlessStreaming scheduler init: if (cronGate.isKairosCronEnabled()) — clean single-gate, no more feature('AGENT_TRIGGERS') && prefix
  • Optional chaining removed from cronJitterConfigModule?.getCronJitterConfigcronJitterConfigModule.getCronJitterConfig and cronGate?.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_cron with true default

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.

@khaledmoayad
Copy link
Copy Markdown
Contributor Author

@kevincodex1 @euxaristia would appreciate a review on this if you have a moment!

Copy link
Copy Markdown
Contributor

@kevincodex1 kevincodex1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm thanks bro!

@kevincodex1 kevincodex1 merged commit 64298a6 into Gitlawb:main Apr 13, 2026
1 check passed
Copy link
Copy Markdown
Contributor

@Flo5k5 Flo5k5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

C1ph3r404 pushed a commit to C1ph3r404/openclaude that referenced this pull request Apr 29, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants