Add mission schedules, triggers, and scheduler#9
Merged
Conversation
- Rename commander block to command_center to avoid confusion with mission commander - Add Schedule config type with three modes (at/every/cron) that compile to cron expressions - Add Trigger config type for webhook-based mission triggers - Add scheduler package with cron-based timer management and concurrency tracking - Wire scheduler into wsbridge client lifecycle (init, config update, stop) - Extract startMission helper in handlers for reuse by scheduler - Support schedule inputs for missions with input parameters - Connect to command center even with invalid config (partial config loading) - Include config state (ready/error) in register and variable result payloads
Update squadron-wire to v0.0.32. Register concurrency limits for all missions (not just scheduled ones) so max_parallel is enforced on manual and webhook-triggered runs. Fire schedule_skip events when missions are skipped due to capacity.
The wsbridge package is for WebSocket communication, not cron management. Scheduler creation, config updates, and shutdown now live in serve.go. Client receives concurrency tracking via a ConcurrencyTracker interface, with a no-op default for non-serve code paths.
Document schedule block modes (at/every/cron), trigger webhooks, max_parallel concurrency, and scheduler architecture.
GitHub Pages serves from /squadron/, so all internal markdown links need the prefix for navigation to work correctly.
New docs/pages/missions/schedules.md covering schedule modes (at/every/cron), triggers (webhooks), concurrency (max_parallel), and schedule inputs. Links added from index, missions overview, and serve pages.
mlund01
added a commit
that referenced
this pull request
Jun 6, 2026
Closes the bugs and refactor items the high-effort review flagged. The ones the user explicitly skipped (#2 load() bare-name semantic change, #3 load() .. now rejected, #6 inconsistent IsPacketSlot ordering, #8 JSON tag inconsistency) are left as-is. #1 — Packets-inside-packets are now dropped from allPackets Old: the Stage 1.4 packet loop ran over EVERY parsed file before the HCL-exclusion filter knew about packet roots, so a `packet "inner"` block declared in a .hcl file living inside another packet's path ended up registered alongside the legitimate packets. Other block types in the same file were correctly filtered later, but packets weren't. New: track each decoded packet with its source file, then drop any packet whose source file lies inside ANOTHER packet's path. Test: `packet "outer"` at top-level with `outer/inner.hcl` declaring `packet "inner"` — only outer survives. #4 — configDir is now the absolute -c argument, not Dir(files[0]) Discovered while writing the V1 test: filepath.WalkDir's lexical order can surface a subdirectory file before its parent, so `filepath.Dir(files[0])` could pick a child dir as configDir, breaking `@/foo` resolution. Fix: LoadDir/LoadFile absolutize the original input path and pass it through to loadFromFiles as an explicit configDir parameter. The CWD-independence promise now holds even when -c is `.` or `./relpath`. #5 — wsbridge file browser now exposes packet slots Old: collectMemoryInfos and resolveMemoryPath only walked cfg.Memories and cfg.Missions[i].Memory. cfg.Packets was never consulted, so the command-center file-browser sidebar showed zero packets and any attempt to browse `packet.<name>` returned "memory not found". New: both helpers iterate cfg.Packets. Each packet surfaces with Editable=false so any future write affordance the UI builds stays disabled. dedupStrings() keeps the Missions list clean when a packet is referenced by both a mission and one of its tasks. #7 — file_read peeks at 8KB before allocating the full file Old: file_read on a packet slot called io.ReadAll for the whole file (capped at 10MB), then ran looksBinary on the full content. A 9MB binary in a packet allocated 9MB to be rejected. New: for packet slots specifically, io.ReadFull a 8KB head, run looksBinary, and only then proceed to the full read. Same pattern file_grep already used. #9 — missionSnapshot/taskSnapshot now include slot references Old: neither snapshot helper emitted memories, packets, memory, or scratchpad keys, so the persisted ConfigJSON had no record that a mission or task referenced specific data bundles. New: missionSnapshot emits memories, packets, memory (description only — the path is runtime-derived), and scratchpad. taskSnapshot emits packets. Audit views and dashboards can now show what data each mission/task had access to without re-parsing HCL. #10 — Centralized containment via paths.IsInside Old: three independent containment implementations — ResolveConfigPath, ResolveProjectPath, and the new isInsidePacket — with subtly different forms. The packet filter used the loose `HasPrefix(rel, "..")` form that false-positives on a filename literally starting with `..` (e.g. `..keep`). New: `paths.IsInside(root, p string, requireStrictDescendant bool) bool` is the single source of truth. Strict-descendant mode rejects p == root. All three callers route through it; ResolveConfigPath keeps the helpful error message that distinguishes "is the root itself" from "outside the root".
mlund01
added a commit
that referenced
this pull request
Jun 6, 2026
* Add `context` block + unified project-root path resolution
Introduces a read-only `context "name" { path = ... }` HCL block for
attaching reference data bundles to missions, and unifies path resolution
for every config attribute that takes a path (context.path, plugin.source
local sources, load()) behind a single helper that anchors to the
project root rather than the process working directory.
## context block
- Top-level `context "name" { path = ..., description = ... }`.
- Mission and task opt in via `contexts = [contexts.X]`.
- Agents address contexts as `slot = "context.<name>"` on every file tool.
- `file_create` / `file_delete` rejected as read-only.
- `file_read` rejects binary content (NUL byte in first 8 KB).
- `file_grep` silently skips binary files.
- `.hcl` files inside a context path are excluded from config parsing —
the context folder is opaque reference data, not config.
Loaded in a new Stage 1.4 (between vars and storage) so the HCL-exclusion
filter runs before vault / storage / command_center / mcp_host iterate
allParsedBlocks. Per-file parse errors are deferred and only surfaced
when they don't belong to a context path.
## Path resolution
`paths.ResolveConfigPath(projectRoot, hclFileDir, rawPath)` is the new
single source of truth, used by context.path, plugin.source, and load():
./foo, ../foo, bare foo → HCL file's own directory
@/foo → project root (the -c argument)
/foo, absolute → rejected
.. escaping project root → rejected (post-Clean containment check)
Path equal to project root itself (e.g. @/) → rejected
The process working directory is never consulted, so `squadron -c <dir>`
behaves identically regardless of where it was invoked from.
`load()` has no per-callsite "HCL file dir", so relative forms collapse
to the project root for it specifically.
The `squadron plugin build` CLI subcommand still uses ResolveProjectPath
(CWD-based) — that's correct for a shell-typed path. ResolveProjectPath
is marked Deprecated for HCL-attribute use.
## Tests
Six new context-specific tests, four rewritten plugin tests, four
rewritten load() tests. Full `go test ./...` is green across 18 packages.
Live-verified against an external testground from an unrelated CWD:
context_smoke, memory_smoke, shell_smoke, pinger_smoke_go,
pinger_smoke_py all pass, and the four rejection paths surface clear
errors.
## Docs
contexts.mdx, plugins.mdx (Path Resolution section), functions.mdx
(load() rules), and CLAUDE.md (staged-evaluation list, Contexts section,
Path resolution for config attributes section) all updated with the
unified rule, failure-mode tables, and cross-links so the three surfaces
read consistently.
* Rename context → packet wholesale
The new HCL block, namespace, slot prefix, Go types, files, tests,
docs, and configuration attribute names all rename from "context" to
"packet." "Packet" is more pointed: it's a discrete, named bundle of
read-only reference data, distinct from the many overloaded senses of
"context" (HCL EvalContext, Go context.Context, "vars context," "agent
context" the LLM operates within, etc).
User-visible surface:
packet "name" { path = ..., description = ... } # HCL block
packets = [packets.x] # mission/task attr
slot = "packet.<name>" # tool slot parameter
Internal:
config.Packet (was config.Context)
config.PacketSlotPrefix = "packet." (was ContextSlotPrefix = "context.")
aitools.IsPacketSlot (was IsContextSlot)
Mission.Packets, Task.Packets (was .Contexts)
Config.Packets (was .Contexts)
Files:
config/packet.go (was context.go)
config/packet_test.go (was context_test.go)
aitools/memory_tools_packet_test.go (was _context_test.go)
mission/memory_store_packets_test.go (was _contexts_test.go)
docs/content/missions/packets.mdx (was contexts.mdx)
Carefully NOT renamed:
hcl.EvalContext, ctx context.Context, buildVarsContext etc — these
are HCL evaluation contexts and the Go runtime context type, unrelated
to our user-facing block.
All tests pass across 18 packages. The packet_smoke mission was rebuilt
against the new terminology and verified end-to-end on the testground.
* Catch leftover 'context' strings in user-visible errors and comments
Smoke run on the renamed feature surfaced five spots the bulk rename
missed because they hide inside string literals and prose:
- aitools/memory_tools.go: error strings for file_read binary rejection
and file_create/delete read-only rejection still said "context slots"
and "context bundles"; doc comments on PacketSlotPrefix/IsPacketSlot
still referenced "context bundle".
- agent/internal/prompts/prompts.go: the slot label in the agent system
prompt still said "(context bundle — read-only reference data ...)".
- internal/paths/paths.go: ResolveConfigPath docstring still listed
"context.path" in the example callers, and the rel=="." rejection
comment still said "for context blocks specifically".
- config/packet_test.go + mission/memory_store_packets_test.go: stray
prose in test descriptions/comments.
Verified by re-running packet_smoke from /tmp against the testground:
the agent's captured binary-rejection error now reads 'packet slots
accept UTF-8 text only' and the read-only error reads 'packet bundles
are immutable'.
* Drop last 'Context' references in comments and a local var
Three spots the wholesale rename and the leftover-strings patch both
missed:
- aitools/memory_tools.go: the doc comment above the binary-content
rejection said "Contexts are read-only reference data..." and the
file_grep local variable was still named isContext.
- config/packet_test.go: a leftover code comment referenced the old
helper name ResolveContextPath.
Genuine non-feature 'context' references remain on purpose: Go runtime
context.Context, HCL EvalContext, and generic prose like 'mission
context' (cancellation), 'item context' (iteration), 'agents context'
(HCL eval), 'context pruning' (commander feature). None of those are
the packet block.
* Address code-review findings #1, #4, #5, #7, #9, #10
Closes the bugs and refactor items the high-effort review flagged. The
ones the user explicitly skipped (#2 load() bare-name semantic change,
#3 load() .. now rejected, #6 inconsistent IsPacketSlot ordering, #8
JSON tag inconsistency) are left as-is.
#1 — Packets-inside-packets are now dropped from allPackets
Old: the Stage 1.4 packet loop ran over EVERY parsed file before the
HCL-exclusion filter knew about packet roots, so a `packet "inner"`
block declared in a .hcl file living inside another packet's path
ended up registered alongside the legitimate packets. Other block
types in the same file were correctly filtered later, but packets
weren't.
New: track each decoded packet with its source file, then drop any
packet whose source file lies inside ANOTHER packet's path. Test:
`packet "outer"` at top-level with `outer/inner.hcl` declaring `packet
"inner"` — only outer survives.
#4 — configDir is now the absolute -c argument, not Dir(files[0])
Discovered while writing the V1 test: filepath.WalkDir's lexical order
can surface a subdirectory file before its parent, so
`filepath.Dir(files[0])` could pick a child dir as configDir, breaking
`@/foo` resolution.
Fix: LoadDir/LoadFile absolutize the original input path and pass it
through to loadFromFiles as an explicit configDir parameter. The
CWD-independence promise now holds even when -c is `.` or `./relpath`.
#5 — wsbridge file browser now exposes packet slots
Old: collectMemoryInfos and resolveMemoryPath only walked cfg.Memories
and cfg.Missions[i].Memory. cfg.Packets was never consulted, so the
command-center file-browser sidebar showed zero packets and any
attempt to browse `packet.<name>` returned "memory not found".
New: both helpers iterate cfg.Packets. Each packet surfaces with
Editable=false so any future write affordance the UI builds stays
disabled. dedupStrings() keeps the Missions list clean when a packet
is referenced by both a mission and one of its tasks.
#7 — file_read peeks at 8KB before allocating the full file
Old: file_read on a packet slot called io.ReadAll for the whole file
(capped at 10MB), then ran looksBinary on the full content. A 9MB
binary in a packet allocated 9MB to be rejected.
New: for packet slots specifically, io.ReadFull a 8KB head, run
looksBinary, and only then proceed to the full read. Same pattern
file_grep already used.
#9 — missionSnapshot/taskSnapshot now include slot references
Old: neither snapshot helper emitted memories, packets, memory, or
scratchpad keys, so the persisted ConfigJSON had no record that a
mission or task referenced specific data bundles.
New: missionSnapshot emits memories, packets, memory (description
only — the path is runtime-derived), and scratchpad. taskSnapshot
emits packets. Audit views and dashboards can now show what data each
mission/task had access to without re-parsing HCL.
#10 — Centralized containment via paths.IsInside
Old: three independent containment implementations — ResolveConfigPath,
ResolveProjectPath, and the new isInsidePacket — with subtly different
forms. The packet filter used the loose `HasPrefix(rel, "..")` form
that false-positives on a filename literally starting with `..`
(e.g. `..keep`).
New: `paths.IsInside(root, p string, requireStrictDescendant bool)
bool` is the single source of truth. Strict-descendant mode rejects
p == root. All three callers route through it; ResolveConfigPath
keeps the helpful error message that distinguishes "is the root
itself" from "outside the root".
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
commanderconfig block tocommand_centerto avoid confusion with mission commanderScheduleconfig type with three mutually exclusive modes (at,every,cron) that compile to cron expressions viaToCron()Triggerconfig type for webhook-based mission triggers with optional secret validationschedulerpackage with cron-based timer management, concurrency tracking (max_parallel), and schedule input passthroughstartMissionhelper in handlers for reuse by both WebSocket requests and schedulerinputsblock for missions with input parametersLoadPartial(partial config loading)configReady/configError) in register and variable result payloadsDepends on: mlund01/squadron-wire#1
Test plan
go test ./config/...— schedule/trigger validation, HCL parsing (155 tests)go test ./scheduler/...— cron compilation, next-fire calculation, concurrency (25 tests)squadron serve, verify it fires on timemax_parallel = 1, trigger rapidly, verify excess runs are skippedreplacedirective in go.mod is updated to published squadron-wire version before merge🤖 Generated with Claude Code