Conversation
The CLI is the surface an agent has, so this is the whole cron API and not a read-only view: list, create, show, edit, pause, resume, delete, run, runs, and preview. Four things here carry meaning rather than plumbing: A job is addressed by NAME on the command line and by id in the API, so every verb resolves through a list-and-match the way resolve-service.ts does. Names are unique per project and branch, which is what makes that safe. Idempotency-Key is minted once per invocation, never per HTTP attempt. It is the whole reason a retried trigger cannot fire the job twice, and moving it inside a retry loop would silently undo that. If-Match carries the revision just read. A 409 means the schedule changed underneath you and is reported as that — re-reading and retrying would clobber whatever the other writer did. Times print as UTC with the marker visible. A cron pinned to UTC does not keep a fixed local time, so a localised column would quietly be wrong twice a year. `show` lists header NAMES and says values are not returned: they are encrypted at rest and the API never hands them back, so an empty value column would read as "no value set" rather than "not shown". `runs` reports status, trigger, attempts, the wake/request split and the HTTP status — the same facts the console shows, because this is where an operator looks when a schedule is misbehaving and they have no browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
4 issues found across 6 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/commands/cron.ts">
<violation number="1" location="src/commands/cron.ts:286">
P2: When editing a job that currently has a POST body, supplying only `--header` or `--method` silently replaces it with a GET or body-less request. Reject incomplete request replacements or require an explicit body/clear-body choice, and warn that the existing payload will be discarded.</violation>
<violation number="2" location="src/commands/cron.ts:522">
P3: When `cron run` targets a paused job, it falsely says the next scheduled run still happens. Report that the job remains paused instead of claiming a scheduled execution will occur.</violation>
</file>
<file name="test/cron.test.ts">
<violation number="1" location="test/cron.test.ts:263">
P1: This test does not exercise the replay guarantee named in its title. Simulate the real 401/refresh/retry path or assert every trigger POST uses the same key and occurs only once, otherwise a duplicate manual execution can regress unnoticed.</violation>
<violation number="2" location="test/cron.test.ts:326">
P2: The conflict test does not verify its no-re-read guarantee. Assert that the listing GET occurs exactly once, in addition to checking the single PATCH attempt.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
|
|
||
| describe('cron run (manual trigger)', () => { | ||
| it('mints ONE Idempotency-Key for the invocation, so a replayed request cannot fire the job twice', async () => { | ||
| const { ctx, calls } = stub((c) => (c.path.endsWith('/runs') ? { status: 202, body: { runId: 'run9' } } : listResponse)) |
There was a problem hiding this comment.
P1: This test does not exercise the replay guarantee named in its title. Simulate the real 401/refresh/retry path or assert every trigger POST uses the same key and occurs only once, otherwise a duplicate manual execution can regress unnoticed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/cron.test.ts, line 263:
<comment>This test does not exercise the replay guarantee named in its title. Simulate the real 401/refresh/retry path or assert every trigger POST uses the same key and occurs only once, otherwise a duplicate manual execution can regress unnoticed.</comment>
<file context>
@@ -0,0 +1,367 @@
+
+describe('cron run (manual trigger)', () => {
+ it('mints ONE Idempotency-Key for the invocation, so a replayed request cannot fire the job twice', async () => {
+ const { ctx, calls } = stub((c) => (c.path.endsWith('/runs') ? { status: 202, body: { runId: 'run9' } } : listResponse))
+ const log = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
+ try {
</file context>
| /** The stored request, from the flags alone. `undefined` when no flag shaped one. */ | ||
| export function buildRequest(o: RequestOpts): { method: CronMethod; headers?: Record<string, string>; body?: string } | undefined { | ||
| if (!namesRequest(o)) return undefined | ||
| const method = o.method ? parseMethod(o.method) : o.body !== undefined ? 'POST' : 'GET' |
There was a problem hiding this comment.
P2: When editing a job that currently has a POST body, supplying only --header or --method silently replaces it with a GET or body-less request. Reject incomplete request replacements or require an explicit body/clear-body choice, and warn that the existing payload will be discarded.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/commands/cron.ts, line 286:
<comment>When editing a job that currently has a POST body, supplying only `--header` or `--method` silently replaces it with a GET or body-less request. Reject incomplete request replacements or require an explicit body/clear-body choice, and warn that the existing payload will be discarded.</comment>
<file context>
@@ -0,0 +1,567 @@
+/** The stored request, from the flags alone. `undefined` when no flag shaped one. */
+export function buildRequest(o: RequestOpts): { method: CronMethod; headers?: Record<string, string>; body?: string } | undefined {
+ if (!namesRequest(o)) return undefined
+ const method = o.method ? parseMethod(o.method) : o.body !== undefined ? 'POST' : 'GET'
+ // A body on a GET is a typo with a plausible-looking outcome: the platform would send it and most
+ // targets would ignore it, so the job would run "fine" and do nothing. `--body` alone implies
</file context>
| expect(said).toContain('changed since revision 4') | ||
| expect(said).toContain('nothing was written') | ||
| // One PATCH, and no second read to "resolve" the conflict. | ||
| expect(calls.filter((c) => c.method === 'PATCH')).toHaveLength(1) |
There was a problem hiding this comment.
P2: The conflict test does not verify its no-re-read guarantee. Assert that the listing GET occurs exactly once, in addition to checking the single PATCH attempt.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/cron.test.ts, line 326:
<comment>The conflict test does not verify its no-re-read guarantee. Assert that the listing GET occurs exactly once, in addition to checking the single PATCH attempt.</comment>
<file context>
@@ -0,0 +1,367 @@
+ expect(said).toContain('changed since revision 4')
+ expect(said).toContain('nothing was written')
+ // One PATCH, and no second read to "resolve" the conflict.
+ expect(calls.filter((c) => c.method === 'PATCH')).toHaveLength(1)
+ } finally {
+ err.mockRestore()
</file context>
| info(`triggered cron job ${job.name} — run ${runId} accepted (read it with \`insta cron runs ${name}\`)`) | ||
| // A manual run is an EXTRA execution: the platform does not move next_run_at for it, and an | ||
| // operator firing a job by hand at 02:59 should not expect the 03:00 tick to have been consumed. | ||
| info(` the scheduled run at ${fmtUtc(job.next_run_at)} still happens — a manual run is an extra execution`) |
There was a problem hiding this comment.
P3: When cron run targets a paused job, it falsely says the next scheduled run still happens. Report that the job remains paused instead of claiming a scheduled execution will occur.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/commands/cron.ts, line 522:
<comment>When `cron run` targets a paused job, it falsely says the next scheduled run still happens. Report that the job remains paused instead of claiming a scheduled execution will occur.</comment>
<file context>
@@ -0,0 +1,567 @@
+ info(`triggered cron job ${job.name} — run ${runId} accepted (read it with \`insta cron runs ${name}\`)`)
+ // A manual run is an EXTRA execution: the platform does not move next_run_at for it, and an
+ // operator firing a job by hand at 02:59 should not expect the 03:00 tick to have been consumed.
+ info(` the scheduled run at ${fmtUtc(job.next_run_at)} still happens — a manual run is an extra execution`)
+}
+
</file context>
| info(` the scheduled run at ${fmtUtc(job.next_run_at)} still happens — a manual run is an extra execution`) | |
| info(job.enabled | |
| ? ` the scheduled run at ${fmtUtc(job.next_run_at)} still happens — a manual run is an extra execution` | |
| : ' the job is paused — this manual run does not resume its schedule') |
insta cron— list, create, show, edit, pause, resume, delete, run, runs, preview.Needs InsForge/instacloud-platform#480, which serves these endpoints.
The CLI is the surface an agent has, so this is the whole cron API rather than a read-only view.
The platform PR adds the governance entries that make agent mode work; without them every mutation
here returns 403.
Four things that carry meaning
A job is addressed by name here and by id in the API, so every verb resolves through a
list-and-match, the way
resolve-service.tsdoes. Names are unique per project and branch, which iswhat makes that safe.
Idempotency-Keyis minted once per invocation, never per HTTP attempt. It is the whole reasona retried trigger cannot fire the job twice; moving it inside a retry loop would silently undo that.
If-Matchcarries the revision just read, and a 409 is reported as "the schedule changedunderneath you" — re-reading and retrying would clobber whatever the other writer did.
Times print as UTC with the marker visible. A cron pinned to UTC does not keep a fixed local
time, so a localised column would quietly be wrong twice a year.
Smaller calls
showlists header NAMES and says values are not returned — they are encrypted at rest and the APInever hands them back, so an empty value column would read as "no value set" rather than "not
shown".
runsreports status, trigger, attempts, the wake/request split and the HTTP status: the same factsthe console shows, because this is where an operator looks when a schedule misbehaves and they have
no browser.
deleterefuses without--yesand says what deletion does and does not destroy (the schedulestops immediately; run history is retained).
Verified
tsc --noEmitandnpm run buildclean. Full suite 80 files / 1657 tests passing, including 45new ones. This repo has no lint config — CI is tsc + vitest, so there was nothing to run.
Driven end to end against a locally booted platform on the production entrypoint: preview, create,
list, run, runs, pause, resume, delete, with the run landing in Postgres and a real https target
answering 200. Agent mode exercised with a real enrolled session across three policy modes — every
verb executes under
full_access, returns an approval id underbranch_specific, and is denied byname under
read_only.One behaviour worth seeing in the output rather than the diff: pointing a job at
http://127.0.0.1:…is refused asblocked (scheme_not_allowed). The production entrypoint doesnot set
allowInsecure, so loopback and plain http are refused by design — the CLI reports thatrefusal rather than hiding it.
🤖 Generated with Claude Code
Summary by cubic
Adds
insta cron, the full cron API from the command line:list,create,show,edit,pause,resume,delete,run,runs, andpreview. Requires the platform endpoints inInsForge/instacloud-platform#480— without its governance entries every mutation returns 403.Behavior
runmints oneIdempotency-Keyper invocation, outside the retry loop, so a retried request cannot fire the job twice.If-Matchwith the revision just read; a 409 says the schedule changed and nothing was written, instead of re-reading and clobbering.showlists header names only — values are write-only;editreplaces the request and warns which headers it drops.deleterefuses without--yes; run history is retained, and a manualrunis an extra execution that leaves the next scheduled tick in place.httpand loopback URLs are refused by the platform asscheme_not_allowed; the CLI reports the refusal.Written for commit a3b2e2f. Summary will update on new commits.