@wjarka/cezarion 0.14.9-dev.898 → 0.14.9-dev.911

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +13 -0
  2. package/dist/ci-wait/controller.d.ts +12 -12
  3. package/dist/contract/index.js +50 -5
  4. package/dist/contract/projects.d.ts +40 -0
  5. package/dist/contract/runs.d.ts +43 -0
  6. package/dist/contract/skills.d.ts +5 -0
  7. package/dist/index.js +6 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/runs/store.d.ts +12 -0
  10. package/dist/runs/store.js +5 -0
  11. package/dist/runs/store.js.map +1 -1
  12. package/dist/runs/webhook.d.ts +125 -0
  13. package/dist/runs/webhook.js +256 -0
  14. package/dist/runs/webhook.js.map +1 -0
  15. package/dist/server/server.d.ts +3647 -2416
  16. package/dist/server/server.js +131 -7
  17. package/dist/server/server.js.map +1 -1
  18. package/dist/skills-remote.d.ts +30 -5
  19. package/dist/skills-remote.js +122 -32
  20. package/dist/skills-remote.js.map +1 -1
  21. package/dist/task-cli/cli.js +51 -4
  22. package/dist/task-cli/cli.js.map +1 -1
  23. package/dist/task-cli/discovery.js +6 -5
  24. package/dist/task-cli/discovery.js.map +1 -1
  25. package/dist/task-cli/http.d.ts +3 -0
  26. package/dist/task-cli/http.js.map +1 -1
  27. package/dist/workflows/run.d.ts +2 -0
  28. package/dist/workflows/run.js +1 -0
  29. package/dist/workflows/run.js.map +1 -1
  30. package/dist/workspace/config.d.ts +9 -0
  31. package/dist/workspace/config.js +13 -1
  32. package/dist/workspace/config.js.map +1 -1
  33. package/dist/workspace/projects.d.ts +24 -1
  34. package/dist/workspace/projects.js +14 -1
  35. package/dist/workspace/projects.js.map +1 -1
  36. package/package.json +3 -3
  37. package/web/dist/assets/agent-accounts-Bh1l3ZSb.js +1 -0
  38. package/web/dist/assets/centered-state-kOb8j_BL.js +43 -0
  39. package/web/dist/assets/{collapsible-D3Lh_nlG.js → collapsible-KSKVv9J9.js} +1 -1
  40. package/web/dist/assets/{commit-list-CgoIXaVd.js → commit-list-na9G1rHG.js} +1 -1
  41. package/web/dist/assets/{compare-variants-2AnPP6ez.js → compare-variants-CG8saweC.js} +1 -1
  42. package/web/dist/assets/{diff-CZcD4D1r2.js → diff-Jd8igxRb2.js} +2 -2
  43. package/web/dist/assets/{diff-controls-BCazakYu.js → diff-controls-D_kgpdoa.js} +1 -1
  44. package/web/dist/assets/{diff-stat-Cn7rzy1H.js → diff-stat-C3Id3W_C.js} +1 -1
  45. package/web/dist/assets/{diff-view-C46nHiP2.js → diff-view-BK1wJX0D.js} +1 -1
  46. package/web/dist/assets/{dropdown-menu-CyFz8H2A.js → dropdown-menu-78Ug-7x2.js} +1 -1
  47. package/web/dist/assets/{git-toolbar-DT4Q02mZ.js → git-toolbar-DFC6bS8a.js} +1 -1
  48. package/web/dist/assets/github-BiUjSaov.js +1 -0
  49. package/web/dist/assets/{image-preview-B618kDBH.js → image-preview-BEOsw8qX.js} +1 -1
  50. package/web/dist/assets/index-D-m3edXI.js +7 -0
  51. package/web/dist/assets/index-D5NkwqY7.css +1 -0
  52. package/web/dist/assets/{markdown-0VE-LMFx.js → markdown-DlvW2wEx.js} +1 -1
  53. package/web/dist/assets/pill-0JHCf1v8.js +1 -0
  54. package/web/dist/assets/{project-router-EEnXzYjj.js → project-router-CS_oeLqi.js} +1 -1
  55. package/web/dist/assets/{prompt-templates-D8lCwJds.js → prompt-templates-BgXzCOlY.js} +3 -3
  56. package/web/dist/assets/repo-git-CMFUWUJo.js +1 -0
  57. package/web/dist/assets/{run-diff-C-Ij6Y04.js → run-diff-xym-7QHm.js} +2 -2
  58. package/web/dist/assets/run-header-BSKELRnz.js +1 -0
  59. package/web/dist/assets/skills-7nEXnFFc.js +1 -0
  60. package/web/dist/assets/{tab-link-CQc65wEy.js → tab-link-D6eAdJez.js} +1 -1
  61. package/web/dist/assets/{task-changes-CEi-l2GZ.js → task-changes-CjsEtUjh.js} +1 -1
  62. package/web/dist/assets/{task-commits-BYdWmsx1.js → task-commits-CeALoVuD.js} +1 -1
  63. package/web/dist/assets/{task-files-DHUEF1Fh.js → task-files-Csb6CCIr.js} +2 -2
  64. package/web/dist/assets/task-thread-B-e9ZDoy.js +9 -0
  65. package/web/dist/assets/{textarea-BOXhPgVD.js → textarea-D2Gx4GDI.js} +1 -1
  66. package/web/dist/assets/thread-loading-hVlcCLdm.js +1 -0
  67. package/web/dist/assets/{token-metrics-DJeOZ5n-.js → token-metrics-BQvw6Yoj.js} +1 -1
  68. package/web/dist/assets/{utils-DKheTxGD.js → utils-BW1-Ibnd.js} +1 -1
  69. package/web/dist/assets/workflows-CGbU1HdX.js +11 -0
  70. package/web/dist/assets/{zoomable-image-YW3Qdrr_.js → zoomable-image-DVvXID3t.js} +1 -1
  71. package/web/dist/index.html +14 -14
  72. package/web/dist/assets/agent-accounts-tNk0wiKj.js +0 -1
  73. package/web/dist/assets/centered-state-DXxIJA15.js +0 -43
  74. package/web/dist/assets/github-vwoJvCTz.js +0 -1
  75. package/web/dist/assets/index-1YNk4P3I.js +0 -7
  76. package/web/dist/assets/index-D0WAgmTQ.css +0 -1
  77. package/web/dist/assets/pill-BU5y1CTd.js +0 -1
  78. package/web/dist/assets/repo-git-CYDGngEk.js +0 -1
  79. package/web/dist/assets/run-header-DYiqx0qE.js +0 -1
  80. package/web/dist/assets/skills-CH6TkMy9.js +0 -1
  81. package/web/dist/assets/task-thread-DAxvkDvr.js +0 -9
  82. package/web/dist/assets/thread-loading-D9dDLmql.js +0 -1
  83. package/web/dist/assets/workflows-CpHaJbUe.js +0 -11
@@ -17,9 +17,10 @@ import { serve } from '@hono/node-server';
17
17
  import { bodyLimit } from 'hono/body-limit';
18
18
  import { streamSSE } from 'hono/streaming';
19
19
  import { jsonZodValidator, paramZodValidator, queryZodValidator } from './validators.js';
20
+ import { deliverOnce, TaskWebhooks } from '../runs/webhook.js';
20
21
  import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
21
22
  import { z } from 'zod';
22
- import { repoPullInputSchema, pinRunInputSchema, githubSearchQuerySchema, setWorkspaceUiStateInputSchema, } from '../contract/index.js';
23
+ import { repoPullInputSchema, pinRunInputSchema, notifyRunInputSchema, githubSearchQuerySchema, setWorkspaceUiStateInputSchema, } from '../contract/index.js';
23
24
  // A contract VALUE, like `workspaceUiStateSchema` in workspace/migrations.ts — the request
24
25
  // schema this route validates with is the same one the client compiles against.
25
26
  import { MODEL_DISCOVERY_RUNNERS, attachmentInputSchema, attachmentFileParamSchema, modelDiscoveryRunnerSchema, openProjectInSchema, runnerDiscoversModels, updateProjectInputSchema, } from '../contract/index.js';
@@ -68,7 +69,7 @@ import { defaultAgentProfile, listAgentProfiles, profileDirState, resolveProfile
68
69
  import { PROFILE_CAPABLE_PROVIDERS, profileEnv, supportsProfiles } from '../core/agent-profiles.js';
69
70
  import { quoteExecutable, withEnvPrefix } from '../core/shell-env.js';
70
71
  import { resolveCursorExecutable } from '../core/cursor-model-catalog.js';
71
- import { allocateProjectSlug, listProjects, normalizeProjectTags, probeProjectStatus, registerProject, removeProject, shouldRegisterProject, } from '../workspace/projects.js';
72
+ import { allocateProjectSlug, listProjects, normalizeProjectTags, probeProjectStatus, toProjectListEntry, registerProject, removeProject, shouldRegisterProject, } from '../workspace/projects.js';
72
73
  import { mergeWriteWorkspaceUiState, readWorkspaceUiState } from '../workspace/ui-state.js';
73
74
  import { checkoutRepo } from './checkout.js';
74
75
  import { ProjectContextError, ProjectContexts } from './project-context.js';
@@ -312,6 +313,8 @@ const startRunSchema = z
312
313
  todoId: z.string().min(1).max(200, 'must be at most 200 characters').optional(),
313
314
  // Idempotent start (#504): the same id and payload answer 200 with the existing run.
314
315
  clientRequestId: z.string().uuid().optional(),
316
+ // Task webhook opt-in (#589). Absent = off; not part of the idempotency hash.
317
+ notify: z.boolean().optional(),
315
318
  })
316
319
  .refine((b) => Boolean(b.workflow) !== Boolean(b.steps), {
317
320
  message: 'provide either "workflow" or "steps", not both',
@@ -822,6 +825,11 @@ export function createApp(deps) {
822
825
  automationStore: deps.automationStore ?? AutomationStore.open(bootDataDir),
823
826
  launchKey: ensureLaunchKey(bootDataDir), // bookmarklet auto-start secret (spec 011)
824
827
  };
828
+ // The task webhook (#589): attached before any recovery — the boot store by `index.ts`, lazy
829
+ // projects through `prepareManager` below. Attaching is idempotent, so the calls here only
830
+ // cover an app built without boot's wiring (tests, embedded servers).
831
+ const taskWebhooks = deps.taskWebhooks ?? new TaskWebhooks({ fetch: deps.taskWebhookFetch, origin: '' });
832
+ taskWebhooks.attach(bootContext);
825
833
  // Non-boot projects build lazily on first scoped request; their managers
826
834
  // count against the same workspace semaphore as the boot manager (step 2.5).
827
835
  const contexts = deps.contexts ?? new ProjectContexts({
@@ -832,7 +840,10 @@ export function createApp(deps) {
832
840
  return listProjects(selector);
833
841
  },
834
842
  semaphore: deps.semaphore,
835
- prepareManager: project => deps.delegation?.attachProject(project),
843
+ prepareManager: (project) => {
844
+ taskWebhooks.attach(project);
845
+ return deps.delegation?.attachProject(project);
846
+ },
836
847
  });
837
848
  // Workspace-level SSE bus (step 2.8) — the registry mutators and the
838
849
  // checkout flow (Phase 4) emit here; /api/workspace/events relays.
@@ -856,6 +867,17 @@ export function createApp(deps) {
856
867
  }
857
868
  contexts.onStoreCreated((store) => providerRuntimeAuth.watch(store));
858
869
  contexts.onContextBuilt((ctx) => providerRuntimeAuth.watch(ctx.store));
870
+ // Injected contexts (tests) skip `prepareManager`; attach those when they are built.
871
+ for (const id of contexts.ids()) {
872
+ const ctx = contexts.peek(id);
873
+ if (ctx)
874
+ taskWebhooks.attach(ctx);
875
+ }
876
+ contexts.onContextBuilt((ctx) => taskWebhooks.attach(ctx));
877
+ const projectRegistryEntry = async (projectId) => {
878
+ const id = projectId === 'default' ? await resolveBootProject() : projectId;
879
+ return (await loadWorkspaceConfig()).projects.find((entry) => entry.id === id);
880
+ };
859
881
  const app = new Hono();
860
882
  // Reject oversized request bodies before they reach any handler (#429). GETs
861
883
  // and SSE carry no body, so this only ever gates the mutating routes.
@@ -2057,7 +2079,7 @@ export function createApp(deps) {
2057
2079
  const parsed = { data: c.req.valid('json') };
2058
2080
  // `default` is the boot alias the cockpit is allowed to use everywhere else.
2059
2081
  const id = raw === 'default' ? await resolveBootProject() : raw;
2060
- const { maxParallel, tags } = parsed.data;
2082
+ const { maxParallel, tags, webhook } = parsed.data;
2061
2083
  // Read-first (mirroring DELETE, server.ts:1252-1258): a well-formed but
2062
2084
  // unknown id must 404 WITHOUT rewriting the config — otherwise it would both
2063
2085
  // do a needless full-config tmp+rename and, on a read-only home, surface the
@@ -2099,6 +2121,16 @@ export function createApp(deps) {
2099
2121
  else
2100
2122
  entry.tags = normalized;
2101
2123
  }
2124
+ if (webhook !== undefined) {
2125
+ // The token is write-only (#589): the form never has it back, so an omitted token
2126
+ // keeps the stored one and only `''` clears it.
2127
+ if (webhook === null)
2128
+ delete entry.webhook;
2129
+ else {
2130
+ const token = webhook.token === undefined ? entry.webhook?.token : webhook.token || undefined;
2131
+ entry.webhook = { url: webhook.url, ...(token ? { token } : {}) };
2132
+ }
2133
+ }
2102
2134
  updated = entry;
2103
2135
  });
2104
2136
  }
@@ -2114,7 +2146,41 @@ export function createApp(deps) {
2114
2146
  // `PUT /api/workspace/config` fires for a workspace-cap change.
2115
2147
  await deps.semaphore?.refresh();
2116
2148
  const body = {
2117
- project: { ...updated, ...(await probeProjectStatus(updated.root)) },
2149
+ project: toProjectListEntry(updated, await probeProjectStatus(updated.root)),
2150
+ };
2151
+ return c.json(body);
2152
+ })
2153
+ // "Send test" (#589): one `task.test` delivery to the STORED webhook, now, with the timeout
2154
+ // the real deliveries use and no retries — the settings page wants the answer, not a queue.
2155
+ // Refused in single-project mode like every other registry edit route.
2156
+ .post('/projects/:projectId/webhook/test', async (c) => {
2157
+ if (capabilities().singleProject) {
2158
+ return c.json(singleProjectRefusal('editing projects'), 409);
2159
+ }
2160
+ const raw = c.req.param('projectId');
2161
+ if (!projectIdSchema.safeParse(raw).success) {
2162
+ return c.json({ error: `unknown project: ${raw}` }, 404);
2163
+ }
2164
+ const entry = await projectRegistryEntry(raw).catch(() => undefined);
2165
+ if (!entry)
2166
+ return c.json({ error: `unknown project: ${raw}` }, 404);
2167
+ if (!entry.webhook)
2168
+ return c.json({ error: NO_WEBHOOK_ERROR }, 400);
2169
+ const payload = {
2170
+ event: 'task.test',
2171
+ deliveryId: randomUUID(),
2172
+ projectId: entry.id,
2173
+ occurredAt: new Date().toISOString(),
2174
+ };
2175
+ if (process.env.CEZ_DRY_RUN === '1') {
2176
+ const body = { ok: true, dryRun: true };
2177
+ return c.json(body);
2178
+ }
2179
+ const result = await deliverOnce(entry.webhook, payload, { fetch: deps.taskWebhookFetch });
2180
+ const body = {
2181
+ ok: result.ok,
2182
+ ...(result.status === undefined ? {} : { status: result.status }),
2183
+ ...(result.error === undefined ? {} : { error: result.error }),
2118
2184
  };
2119
2185
  return c.json(body);
2120
2186
  })
@@ -2250,7 +2316,7 @@ export function createApp(deps) {
2250
2316
  let project;
2251
2317
  try {
2252
2318
  const entry = await registerProject(requested, source);
2253
- project = { ...entry, ...(await probeProjectStatus(entry.root)) };
2319
+ project = toProjectListEntry(entry, await probeProjectStatus(entry.root));
2254
2320
  }
2255
2321
  catch (err) {
2256
2322
  // e.g. a read-only home — nothing was persisted (atomic tmp+rename).
@@ -3149,6 +3215,34 @@ export function createApp(deps) {
3149
3215
  const { store } = c.get('project');
3150
3216
  const run = store.setPinned(c.req.valid('param').id, c.req.valid('json').pinned !== false);
3151
3217
  return run ? c.json(run) : c.json({ error: 'not found' }, 404);
3218
+ })
3219
+ // Task webhook opt-in for one run (#589), valid in every state — handing a finished task to a
3220
+ // bot for follow-up is a real case. Turning it on sends one `task.subscribed` delivery with
3221
+ // the note; the note never reaches the agent session. Either direction leaves a durable
3222
+ // `handoff` event so the thread shows when and why. Idempotent: repeating `true` with a
3223
+ // new note is how "Send a note…" works.
3224
+ .post('/runs/:id/notify', paramZodValidator(runIdParamSchema), jsonZodValidator(notifyRunInputSchema), async (c) => {
3225
+ const project = c.get('project');
3226
+ const { id } = c.req.valid('param');
3227
+ const { notify, message } = c.req.valid('json');
3228
+ if (!project.store.getRun(id))
3229
+ return c.json({ error: 'not found' }, 404);
3230
+ if (notify) {
3231
+ const entry = await projectRegistryEntry(project.id).catch(() => undefined);
3232
+ if (!entry?.webhook)
3233
+ return c.json({ error: NO_WEBHOOK_ERROR }, 400);
3234
+ }
3235
+ const run = project.store.updateRun(id, { notify: notify ? true : undefined });
3236
+ if (!run)
3237
+ return c.json({ error: 'not found' }, 404);
3238
+ project.store.appendEvent(id, {
3239
+ type: 'handoff',
3240
+ notify,
3241
+ ...(message === undefined ? {} : { message }),
3242
+ });
3243
+ if (notify)
3244
+ taskWebhooks.attach(project).subscribed(id, message);
3245
+ return c.json(run);
3152
3246
  })
3153
3247
  // The per-task off switch for that resume (the workspace setting is Settings → Resources).
3154
3248
  // Idempotent: a run with nothing pending answers 200 too, because "this task will not
@@ -3225,9 +3319,15 @@ export function createApp(deps) {
3225
3319
  if ('error' in account)
3226
3320
  return c.json({ error: account.error }, 400);
3227
3321
  }
3322
+ // Task webhook (#589): an explicit `true` on a project with no webhook is refused, so a
3323
+ // bot learns in one round trip. `cez task start`'s implicit default never sends it there.
3324
+ if (parsed.data.notify === true && !(await projectRegistryEntry(c.get('project').id).catch(() => undefined))?.webhook) {
3325
+ return c.json({ error: NO_WEBHOOK_ERROR }, 400);
3326
+ }
3228
3327
  const images = parsed.data.images?.map(toPastedContent);
3229
3328
  const input = {
3230
3329
  task: parsed.data.task,
3330
+ notify: parsed.data.notify,
3231
3331
  model: parsed.data.model,
3232
3332
  effort: parsed.data.effort,
3233
3333
  runner: parsed.data.runner,
@@ -5247,6 +5347,13 @@ export function createApp(deps) {
5247
5347
  routed.get('*', (c) => serveShell(c) ?? c.notFound());
5248
5348
  return routed;
5249
5349
  }
5350
+ /** The host a thread link names (#589): the bound one, or loopback for a wildcard bind. */
5351
+ function taskWebhookHost(bindHost) {
5352
+ if (!bindHost || bindHost === '0.0.0.0' || bindHost === '::')
5353
+ return '127.0.0.1';
5354
+ return bindHost.includes(':') ? `[${bindHost}]` : bindHost;
5355
+ }
5356
+ const NO_WEBHOOK_ERROR = 'this project has no task webhook — set one in Settings → General → Task webhook, or start without notify';
5250
5357
  export function startServer(deps, port) {
5251
5358
  const workspaceEvents = deps.workspaceEvents ?? new WorkspaceEventBus();
5252
5359
  const skillsUpdate = deps.skillsUpdate ?? new SkillsUpdateService({ invalidateCatalog: refreshTeamSkills });
@@ -5256,11 +5363,15 @@ export function startServer(deps, port) {
5256
5363
  const automationCoordinator = new AutomationCoordinator({ listProjects });
5257
5364
  const bootProjectId = deps.bootProjectId ?? 'default';
5258
5365
  const bootAutomationStore = automationCoordinator.store(bootProjectId, deps.repoRoot);
5366
+ const taskWebhooks = deps.taskWebhooks ?? new TaskWebhooks({ fetch: deps.taskWebhookFetch });
5259
5367
  const sharedContexts = deps.contexts ?? new ProjectContexts({
5260
5368
  listProjects,
5261
5369
  semaphore: deps.semaphore,
5262
5370
  automationStore: (projectId, root) => automationCoordinator.store(projectId, root),
5263
- prepareManager: project => deps.delegation?.attachProject(project),
5371
+ prepareManager: (project) => {
5372
+ taskWebhooks.attach(project);
5373
+ return deps.delegation?.attachProject(project);
5374
+ },
5264
5375
  });
5265
5376
  // #801: GitHub automations are opt-in. Off, the flag must remove the BEHAVIOR and not merely
5266
5377
  // the UI — no scheduler, no GitHub polling, no launched runs — so every entry point into the
@@ -5270,6 +5381,7 @@ export function startServer(deps, port) {
5270
5381
  let rescheduleAutomations = () => { };
5271
5382
  const app = createApp({
5272
5383
  ...deps,
5384
+ taskWebhooks,
5273
5385
  contexts: sharedContexts,
5274
5386
  automationStore: bootAutomationStore,
5275
5387
  workspaceEvents,
@@ -5408,6 +5520,18 @@ export function startServer(deps, port) {
5408
5520
  server.closeAllConnections();
5409
5521
  });
5410
5522
  };
5523
+ // The task webhook's thread links name the port the listener BOUND (#594 review): `--port 0`
5524
+ // asks for an ephemeral one, so the requested number is not an address. Deliveries queue
5525
+ // behind this, so none goes out before it is set.
5526
+ const setTaskWebhookOrigin = () => {
5527
+ const address = server.address();
5528
+ const bound = address && typeof address === 'object' ? address.port : port;
5529
+ taskWebhooks.setOrigin(`http://${taskWebhookHost(deps.bindHost)}:${bound}`);
5530
+ };
5531
+ if (server.listening)
5532
+ setTaskWebhookOrigin();
5533
+ else
5534
+ server.once('listening', setTaskWebhookOrigin);
5411
5535
  return Object.assign(server, { shutdownForRestart });
5412
5536
  }
5413
5537
  /**