@compilr-dev/sdk 0.18.10 → 0.19.0

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 (34) hide show
  1. package/dist/canvas/assets.d.ts +99 -0
  2. package/dist/canvas/assets.js +111 -0
  3. package/dist/canvas/color.d.ts +85 -0
  4. package/dist/canvas/color.js +216 -0
  5. package/dist/canvas/index.d.ts +8 -0
  6. package/dist/canvas/index.js +16 -0
  7. package/dist/canvas/quality-checks.d.ts +75 -0
  8. package/dist/canvas/quality-checks.js +217 -0
  9. package/dist/canvas/ramp.d.ts +45 -0
  10. package/dist/canvas/ramp.js +98 -0
  11. package/dist/capabilities/packs.js +16 -2
  12. package/dist/platform/context.d.ts +16 -2
  13. package/dist/platform/context.js +13 -1
  14. package/dist/platform/repositories.d.ts +4 -0
  15. package/dist/platform/sqlite/canvas-repository.d.ts +9 -0
  16. package/dist/platform/sqlite/canvas-repository.js +52 -0
  17. package/dist/platform/sqlite/db.js +29 -0
  18. package/dist/platform/sqlite/schema.d.ts +1 -1
  19. package/dist/platform/sqlite/schema.js +1 -1
  20. package/dist/platform/tools/canvas-tools.d.ts +12 -2
  21. package/dist/platform/tools/canvas-tools.js +316 -6
  22. package/dist/platform/tools/image-tools.d.ts +9 -2
  23. package/dist/platform/tools/image-tools.js +4 -2
  24. package/dist/platform/tools/index.js +10 -2
  25. package/dist/platform/tools/project-tools.js +3 -2
  26. package/dist/skills/canvas-exemplars.d.ts +8 -0
  27. package/dist/skills/canvas-exemplars.js +36 -0
  28. package/dist/skills/canvas-icons.d.ts +6 -0
  29. package/dist/skills/canvas-icons.js +84 -0
  30. package/dist/skills/canvas-skills.js +207 -20
  31. package/dist/skills/platform-skills.d.ts +2 -0
  32. package/dist/skills/platform-skills.js +8 -0
  33. package/dist/team/tool-config.js +4 -0
  34. package/package.json +1 -1
@@ -12,9 +12,17 @@
12
12
  */
13
13
  import { defineTool, createSuccessResult, createErrorResult } from '@compilr-dev/agents';
14
14
  import { truncateContent } from './truncate.js';
15
+ import { runQualityChecks } from '../../canvas/quality-checks.js';
16
+ import { resolveCanvasAssets, isValidAssetRef, missingAssetRefs, assetPayloadBytes, referencedAssets, ASSET_BUDGET_BYTES, DEFAULT_ASSET_WIDTH, } from '../../canvas/assets.js';
17
+ import { resolveCwd } from '../context.js';
18
+ import * as fsNode from 'node:fs';
19
+ import * as pathNode from 'node:path';
15
20
  const PAGE_SIZES = ['16:9', '4:3', '1:1', '4:5', '9:16'];
16
21
  import { seedValues } from '../../canvas/types.js';
17
22
  import { validateControlManifest } from '../../canvas/validate.js';
23
+ import { canvasSkill, infographicSkill, carouselSkill, boardSkill, canvasStylesSkill, } from '../../skills/canvas-skills.js';
24
+ import { canvasExemplarInfographicSkill, canvasExemplarAppScreenSkill, } from '../../skills/canvas-exemplars.js';
25
+ import { canvasIconsSkill } from '../../skills/canvas-icons.js';
18
26
  const CANVAS_TYPES = ['infographic', 'carousel', 'board'];
19
27
  /** Render content as numbered lines (1-based), optionally a slice. */
20
28
  function numberedLines(content, startLine, maxLines) {
@@ -133,7 +141,7 @@ function offCanvasNodes(src, bounds) {
133
141
  return out;
134
142
  }
135
143
  /** Run deterministic static checks on canvas content. No rendering. */
136
- export function runCanvasChecks(html, type) {
144
+ export function runCanvasChecks(html, type, manifest) {
137
145
  const issues = [];
138
146
  const src = html.trim();
139
147
  if (!src) {
@@ -200,10 +208,18 @@ export function runCanvasChecks(html, type) {
200
208
  });
201
209
  }
202
210
  }
211
+ /*
212
+ The quality checks — surfaces, contrast, hue count, dead Tweaks. A separate module
213
+ because they are pure arithmetic on declared values and want testing without a
214
+ platform context; see `canvas/quality-checks.ts` for what each one deliberately
215
+ cannot know.
216
+ */
217
+ for (const issue of runQualityChecks(src, manifest))
218
+ issues.push(issue);
203
219
  return issues;
204
220
  }
205
221
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
206
- export function createCanvasTools(config) {
222
+ export function createCanvasTools(config, imageConfig) {
207
223
  const ctx = config.context;
208
224
  const canvases = ctx.canvases;
209
225
  if (!canvases)
@@ -294,7 +310,19 @@ export function createCanvasTools(config) {
294
310
  });
295
311
  if (!updated)
296
312
  return createErrorResult(`Canvas ${String(input.canvas_id)} not found.`);
297
- return createSuccessResult(`Updated canvas "${updated.title}" (${updated.type})\nID: ${String(updated.id)}`);
313
+ /*
314
+ ⚠️ The prose is UNCHANGED, deliberately. Desktop's auto-split parses the id out
315
+ of this exact wording (`renderer/src/lib/canvas-authored.ts`), and the model has
316
+ been reading it for every canvas it has ever written. The id now also travels as
317
+ a structured sibling so the host can stop regex-parsing prose — but both must
318
+ ship together for one version, or a Desktop that has not updated silently stops
319
+ splitting on author.
320
+ */
321
+ return {
322
+ success: true,
323
+ result: `Updated canvas "${updated.title}" (${updated.type})\nID: ${String(updated.id)}`,
324
+ canvas: { id: updated.id, title: updated.title, type: updated.type, action: 'updated' },
325
+ };
298
326
  }
299
327
  const projectId = input.project_id ?? ctx.currentProjectId;
300
328
  if (!projectId) {
@@ -310,7 +338,11 @@ export function createCanvasTools(config) {
310
338
  values: seedValues(controls),
311
339
  pageSize: input.page_size,
312
340
  });
313
- return createSuccessResult(`Created canvas "${created.title}" (${created.type})\nID: ${String(created.id)}`);
341
+ return {
342
+ success: true,
343
+ result: `Created canvas "${created.title}" (${created.type})\nID: ${String(created.id)}`,
344
+ canvas: { id: created.id, title: created.title, type: created.type, action: 'created' },
345
+ };
314
346
  }
315
347
  catch (error) {
316
348
  return createErrorResult(`Failed to write canvas: ${error instanceof Error ? error.message : String(error)}`);
@@ -537,12 +569,17 @@ export function createCanvasTools(config) {
537
569
  try {
538
570
  let html;
539
571
  let type;
572
+ // ⚠️ The manifest travels with a STORED canvas only. Validating raw html has no
573
+ // manifest to check against, so the dead-Tweak check is silent there rather than
574
+ // reporting every control as unbound — a canvas is written before it is stored.
575
+ let manifest;
540
576
  if (input.canvas_id !== undefined) {
541
577
  const c = await canvases.getById(input.canvas_id);
542
578
  if (!c)
543
579
  return createErrorResult(`Canvas ${String(input.canvas_id)} not found.`);
544
580
  html = c.content;
545
581
  type = c.type;
582
+ manifest = c.controls;
546
583
  }
547
584
  else if (input.html !== undefined) {
548
585
  html = input.html;
@@ -551,7 +588,23 @@ export function createCanvasTools(config) {
551
588
  else {
552
589
  return createErrorResult('Provide canvas_id (a stored canvas) or html (+ type) to validate.');
553
590
  }
554
- const issues = runCanvasChecks(html, type);
591
+ const issues = runCanvasChecks(html, type, manifest);
592
+ /*
593
+ ⚠️ The check that makes the asset indirection safe. `asset:typo` is not a real
594
+ scheme — the CSP drops it — so without this a mistyped reference renders a hole
595
+ in the canvas and reports nothing anywhere.
596
+ */
597
+ if (input.canvas_id !== undefined && canvases.listAssets) {
598
+ const assets = await canvases.listAssets(input.canvas_id);
599
+ for (const ref of missingAssetRefs(html, assets)) {
600
+ issues.push({
601
+ level: 'error',
602
+ message: `The markup references asset:${ref} but no such image is stored on this ` +
603
+ 'canvas, so it will not render. Add it with canvas_asset_add, or check the ' +
604
+ 'handle with canvas_asset_list.',
605
+ });
606
+ }
607
+ }
555
608
  if (issues.length === 0) {
556
609
  return createSuccessResult('✓ Passed all static checks — no structural/technical issues. (Visual/aesthetic review is a separate pass.)');
557
610
  }
@@ -603,7 +656,18 @@ export function createCanvasTools(config) {
603
656
  const c = await canvases.getById(input.canvas_id);
604
657
  if (!c)
605
658
  return createErrorResult(`Canvas ${String(input.canvas_id)} not found.`);
606
- const img = await renderer.renderToImage({ content: c.content, type: c.type });
659
+ /*
660
+ ⚠️ Resolve assets HERE, not in the host renderer. This tool has the canvas id and
661
+ the repository; `renderToImage` receives only html, so pushing substitution into
662
+ the host would mean every host reimplementing it — and the screenshot the review
663
+ gate judges would differ from the canvas the user sees the moment one of them
664
+ drifted.
665
+ */
666
+ const shotAssets = canvases.listAssets ? await canvases.listAssets(c.id) : [];
667
+ const img = await renderer.renderToImage({
668
+ content: resolveCanvasAssets(c.content, shotAssets),
669
+ type: c.type,
670
+ });
607
671
  return {
608
672
  success: true,
609
673
  result: `Rendered "${c.title}" (${c.type}). Review the image below — check for overflow / cut-off content, ` +
@@ -625,6 +689,248 @@ export function createCanvasTools(config) {
625
689
  }
626
690
  },
627
691
  });
692
+ /*
693
+ ⚠️ THIS TOOL EXISTS BECAUSE THE CRAFT GUIDANCE WAS UNREACHABLE.
694
+
695
+ Skills are delivered by the HOST when a user types a slash command — Desktop fetches
696
+ the prompt and sends it as a user message. Nothing else delivers them: there is no
697
+ path from the agent to a skill, and the capability pack's snippet covers mechanics
698
+ only (canvas_write, the CSP, editing). So "make me an infographic" produced a canvas
699
+ authored with no design guidance at all, and every rule about hierarchy, restraint and
700
+ the elevation ramp reached the model only if the user happened to know to type
701
+ `/canvas` first.
702
+
703
+ That is the wrong shape: the agent, not the user, is the one who knows it is about to
704
+ author a canvas. So the guides become readable on demand — lazy, like the pack itself,
705
+ costing nothing until a canvas is actually being made.
706
+ */
707
+ const canvasGuideTool = defineTool({
708
+ name: 'canvas_guide',
709
+ description: 'Read the design guidance for a canvas BEFORE authoring one. Call with topic "canvas" ' +
710
+ 'first — it covers the steps, the sandbox rules and the theme tokens, and routes you to ' +
711
+ 'the right craft guide. Then read the guide for the type you picked ("infographic", ' +
712
+ '"carousel" or "board"), "styles" when choosing a look, and an "exemplar-*" topic for a ' +
713
+ 'worked example to adapt. Skipping this produces a canvas that renders but reads as ' +
714
+ 'generic — the exemplars in particular are worth more than any amount of description.',
715
+ inputSchema: {
716
+ type: 'object',
717
+ properties: {
718
+ topic: {
719
+ type: 'string',
720
+ enum: [
721
+ 'canvas',
722
+ 'infographic',
723
+ 'carousel',
724
+ 'board',
725
+ 'styles',
726
+ 'exemplar-infographic',
727
+ 'exemplar-app-screen',
728
+ 'icons',
729
+ ],
730
+ description: 'canvas = mechanics + steps (read this first) · infographic/carousel/board = the ' +
731
+ 'craft guide for that type · styles = the aesthetic catalogue · ' +
732
+ 'exemplar-infographic / exemplar-app-screen = a worked example to adapt, which is ' +
733
+ 'worth more than any amount of description.',
734
+ },
735
+ },
736
+ required: ['topic'],
737
+ },
738
+ execute: (input) => {
739
+ /*
740
+ ⚠️ `| undefined` is load-bearing. Without it `Record<string, T>` indexing claims a
741
+ value for every key, so the linter calls the guard below dead code — while at
742
+ runtime an unknown topic is exactly what it catches (asserted in canvas-guide
743
+ tests). The type was lying, not the guard.
744
+ */
745
+ const guides = {
746
+ canvas: canvasSkill,
747
+ infographic: infographicSkill,
748
+ carousel: carouselSkill,
749
+ board: boardSkill,
750
+ styles: canvasStylesSkill,
751
+ 'exemplar-infographic': canvasExemplarInfographicSkill,
752
+ 'exemplar-app-screen': canvasExemplarAppScreenSkill,
753
+ icons: canvasIconsSkill,
754
+ };
755
+ const guide = guides[input.topic];
756
+ if (!guide) {
757
+ return Promise.resolve(createErrorResult(`Unknown topic "${input.topic}". One of: ${Object.keys(guides).join(', ')}.`));
758
+ }
759
+ return Promise.resolve(createSuccessResult(guide.prompt));
760
+ },
761
+ });
762
+ // ---------------------------------------------------------------------------
763
+ // Assets — real images, stored beside the canvas
764
+ // ---------------------------------------------------------------------------
765
+ /*
766
+ ⚠️ THIS EXISTS BECAUSE THE AGENT COULD SEE IMAGES AND NOT USE THEM. `view_image` gives
767
+ vision; nothing returned bytes, so an agent asked to mock a page with the user's own
768
+ product photos had no path from "I can see it" to "it is in the canvas" — and said so,
769
+ blaming a sandbox that was not the real obstacle.
770
+
771
+ The bytes do NOT go into the markup. The canvas references `asset:<ref>` and the host
772
+ substitutes a data URI when it renders and when it exports; see `canvas/assets.ts` for
773
+ why inlining would break `canvas_edit`.
774
+ */
775
+ const canvasAssetAddTool = defineTool({
776
+ name: 'canvas_asset_add',
777
+ description: 'Add a real image to a canvas so its markup can show it. Reads the file, resizes it ' +
778
+ 'to display size, stores it against the canvas, and returns the reference to use: ' +
779
+ 'write <img src="asset:REF" alt="..."> in the HTML. Use this for a user\'s own ' +
780
+ 'photos, logos or screenshots — do NOT paste base64 into the canvas, and do not draw ' +
781
+ 'a grey placeholder box when a real image was provided.',
782
+ inputSchema: {
783
+ type: 'object',
784
+ properties: {
785
+ canvas_id: { type: 'number', description: 'The canvas to attach the image to.' },
786
+ path: {
787
+ type: 'string',
788
+ description: 'Image file path (absolute, or relative to the project directory).',
789
+ },
790
+ ref: {
791
+ type: 'string',
792
+ description: 'Short handle used as asset:REF in the markup — lowercase letters, digits and ' +
793
+ 'dashes (e.g. "hero", "product-1"). Re-using a handle replaces that image.',
794
+ },
795
+ max_width: {
796
+ type: 'number',
797
+ description: `Resize target in px (default ${String(DEFAULT_ASSET_WIDTH)}). Use the size it ` +
798
+ 'is DISPLAYED at — a product card needs ~480, a hero ~1200. Bigger costs the ' +
799
+ 'canvas and every export.',
800
+ },
801
+ },
802
+ required: ['canvas_id', 'path', 'ref'],
803
+ },
804
+ execute: async (input) => {
805
+ if (!canvases.addAsset || !canvases.listAssets) {
806
+ return createErrorResult('This host does not support canvas assets (its canvas storage predates them). ' +
807
+ 'Draw the layout without the image and tell the user why.');
808
+ }
809
+ if (!isValidAssetRef(input.ref)) {
810
+ return createErrorResult(`Invalid ref "${input.ref}". Use lowercase letters, digits and dashes, e.g. "hero".`);
811
+ }
812
+ const canvas = await canvases.getById(input.canvas_id);
813
+ if (!canvas)
814
+ return createErrorResult(`Canvas ${String(input.canvas_id)} not found.`);
815
+ const filePath = pathNode.isAbsolute(input.path)
816
+ ? input.path
817
+ : pathNode.resolve(resolveCwd(config.cwd), input.path);
818
+ if (!fsNode.existsSync(filePath)) {
819
+ return createErrorResult(`File not found: ${input.path} (looked in ${pathNode.dirname(filePath)}). ` +
820
+ 'Pass an absolute path if the file is outside the project.');
821
+ }
822
+ const ext = pathNode.extname(filePath).toLowerCase();
823
+ const mediaTypes = {
824
+ '.png': 'image/png',
825
+ '.jpg': 'image/jpeg',
826
+ '.jpeg': 'image/jpeg',
827
+ '.gif': 'image/gif',
828
+ '.webp': 'image/webp',
829
+ };
830
+ const sourceType = mediaTypes[ext];
831
+ if (!sourceType) {
832
+ return createErrorResult(`Unsupported image type "${ext}". Supported: ${Object.keys(mediaTypes).join(', ')}.`);
833
+ }
834
+ let bytes = fsNode.readFileSync(filePath);
835
+ let mediaType = sourceType;
836
+ let width;
837
+ let height;
838
+ /*
839
+ ⚠️ Resize where a resizer exists, and say so when it does not. A phone photo is
840
+ several MB; stored raw it is paid again in every export and in the model's own
841
+ reading of the canvas. Without a host resizer the file is stored as-is, and the
842
+ budget below is what stops that being silent.
843
+ */
844
+ if (imageConfig?.resizer) {
845
+ try {
846
+ const out = await imageConfig.resizer.resize(bytes, input.max_width ?? DEFAULT_ASSET_WIDTH);
847
+ bytes = out.data;
848
+ width = out.width;
849
+ height = out.height;
850
+ mediaType = out.mediaType;
851
+ }
852
+ catch {
853
+ // Keep the original — a failed resize must not lose the image.
854
+ }
855
+ }
856
+ const data = Buffer.from(bytes).toString('base64');
857
+ const existing = await canvases.listAssets(input.canvas_id);
858
+ const others = existing.filter((a) => a.ref.toLowerCase() !== input.ref.toLowerCase());
859
+ const projected = others.reduce((n, a) => n + a.bytes, 0) + data.length;
860
+ if (projected > ASSET_BUDGET_BYTES) {
861
+ return createErrorResult(`That would put this canvas at ${String(Math.round(projected / 1024))}KB of images, ` +
862
+ `over the ${String(Math.round(ASSET_BUDGET_BYTES / 1024))}KB budget. Re-add with a ` +
863
+ 'smaller max_width, or remove an image you are not using with canvas_asset_delete.');
864
+ }
865
+ const asset = await canvases.addAsset({
866
+ canvasId: input.canvas_id,
867
+ ref: input.ref.toLowerCase(),
868
+ mediaType,
869
+ data,
870
+ width,
871
+ height,
872
+ bytes: data.length,
873
+ sourcePath: filePath,
874
+ });
875
+ const dims = width && height ? ` ${String(width)}x${String(height)}` : '';
876
+ return createSuccessResult(`Stored "${asset.ref}"${dims} (${String(Math.round(asset.bytes / 1024))}KB, ` +
877
+ `${asset.mediaType}). Reference it in the canvas as: ` +
878
+ `<img src="asset:${asset.ref}" alt="describe what it shows">`);
879
+ },
880
+ });
881
+ const canvasAssetListTool = defineTool({
882
+ name: 'canvas_asset_list',
883
+ description: 'List the images stored on a canvas, with the reference to use for each and whether ' +
884
+ 'the markup currently uses it.',
885
+ inputSchema: {
886
+ type: 'object',
887
+ properties: { canvas_id: { type: 'number', description: 'The canvas.' } },
888
+ required: ['canvas_id'],
889
+ },
890
+ execute: async (input) => {
891
+ if (!canvases.listAssets) {
892
+ return createErrorResult('This host does not support canvas assets.');
893
+ }
894
+ const canvas = await canvases.getById(input.canvas_id);
895
+ if (!canvas)
896
+ return createErrorResult(`Canvas ${String(input.canvas_id)} not found.`);
897
+ const assets = await canvases.listAssets(input.canvas_id);
898
+ if (assets.length === 0) {
899
+ return createSuccessResult('No images on this canvas. Add one with canvas_asset_add.');
900
+ }
901
+ const used = new Set(referencedAssets(canvas.content));
902
+ const lines = assets.map((a) => {
903
+ const dims = a.width && a.height ? `${String(a.width)}x${String(a.height)}` : '—';
904
+ const where = used.has(a.ref) ? 'used' : 'NOT referenced';
905
+ return ` asset:${a.ref} ${dims} ${String(Math.round(a.bytes / 1024))}KB ${where}`;
906
+ });
907
+ const total = assetPayloadBytes(canvas.content, assets);
908
+ return createSuccessResult(`${String(assets.length)} image(s):\n${lines.join('\n')}\n` +
909
+ `In use: ${String(Math.round(total / 1024))}KB of the ` +
910
+ `${String(Math.round(ASSET_BUDGET_BYTES / 1024))}KB budget.`);
911
+ },
912
+ });
913
+ const canvasAssetDeleteTool = defineTool({
914
+ name: 'canvas_asset_delete',
915
+ description: 'Remove an image from a canvas.',
916
+ inputSchema: {
917
+ type: 'object',
918
+ properties: {
919
+ canvas_id: { type: 'number', description: 'The canvas.' },
920
+ ref: { type: 'string', description: 'The asset reference to remove.' },
921
+ },
922
+ required: ['canvas_id', 'ref'],
923
+ },
924
+ execute: async (input) => {
925
+ if (!canvases.deleteAsset) {
926
+ return createErrorResult('This host does not support canvas assets.');
927
+ }
928
+ const gone = await canvases.deleteAsset(input.canvas_id, input.ref.toLowerCase());
929
+ return gone
930
+ ? createSuccessResult(`Removed "${input.ref}".`)
931
+ : createErrorResult(`No asset "${input.ref}" on canvas ${String(input.canvas_id)}.`);
932
+ },
933
+ });
628
934
  return [
629
935
  canvasWriteTool,
630
936
  canvasEditTool,
@@ -633,5 +939,9 @@ export function createCanvasTools(config) {
633
939
  canvasDeleteTool,
634
940
  canvasValidateTool,
635
941
  canvasScreenshotTool,
942
+ canvasGuideTool,
943
+ canvasAssetAddTool,
944
+ canvasAssetListTool,
945
+ canvasAssetDeleteTool,
636
946
  ];
637
947
  }
@@ -25,8 +25,15 @@ export interface ImageResizer {
25
25
  }>;
26
26
  }
27
27
  export interface ImageToolsConfig {
28
- /** Working directory for resolving relative paths */
29
- cwd?: string;
28
+ /**
29
+ * Working directory for resolving relative paths.
30
+ *
31
+ * ⚠️ A FUNCTION, when the host's working directory can change. Platform tools are
32
+ * built once and memoised, so a string is captured at construction — which in Desktop
33
+ * meant every relative path resolved against whichever project happened to be open
34
+ * first, for the rest of the session. Pass a getter and it is resolved per call.
35
+ */
36
+ cwd?: string | (() => string | undefined | null);
30
37
  /** Optional image resizer (e.g., nativeImage in Electron) */
31
38
  resizer?: ImageResizer;
32
39
  /** Max dimension for resize (default: 1568) */
@@ -10,6 +10,7 @@
10
10
  import * as fs from 'node:fs';
11
11
  import * as path from 'node:path';
12
12
  import { defineTool, createErrorResult } from '@compilr-dev/agents';
13
+ import { resolveCwd } from '../context.js';
13
14
  /** Supported image MIME types */
14
15
  const IMAGE_EXTENSIONS = {
15
16
  '.png': 'image/png',
@@ -27,7 +28,6 @@ const MAX_FILE_SIZE = 10 * 1024 * 1024;
27
28
  export const DEFAULT_MAX_DIMENSION = 1568;
28
29
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
29
30
  export function createImageTools(config = {}) {
30
- const cwd = config.cwd ?? process.cwd();
31
31
  const maxDimension = config.maxDimension ?? DEFAULT_MAX_DIMENSION;
32
32
  const viewImageTool = defineTool({
33
33
  name: 'view_image',
@@ -45,7 +45,9 @@ export function createImageTools(config = {}) {
45
45
  },
46
46
  execute: async (input) => {
47
47
  // Resolve path
48
- const filePath = path.isAbsolute(input.path) ? input.path : path.resolve(cwd, input.path);
48
+ const filePath = path.isAbsolute(input.path)
49
+ ? input.path
50
+ : path.resolve(resolveCwd(config.cwd), input.path);
49
51
  // Validate file exists
50
52
  if (!fs.existsSync(filePath)) {
51
53
  return createErrorResult(`File not found: ${input.path}`);
@@ -29,7 +29,14 @@ export function createPlatformTools(config, options) {
29
29
  // Backwards compat: options can be ImageToolsConfig (old) or PlatformToolsOptions (new)
30
30
  let imageConfig;
31
31
  let planCallbacks;
32
- if (options && 'planModeCallbacks' in options) {
32
+ /*
33
+ ⚠️ DISCRIMINATE ON EITHER KEY. This used to test only `'planModeCallbacks' in options`,
34
+ so a caller passing `{ imageConfig }` alone — the documented new shape, without plan
35
+ mode — fell through to the legacy branch and was read as an ImageToolsConfig whose only
36
+ property was a stray `imageConfig`. The resizer silently went missing, with no type
37
+ error, because every ImageToolsConfig field is optional.
38
+ */
39
+ if (options && ('planModeCallbacks' in options || 'imageConfig' in options)) {
33
40
  imageConfig = options.imageConfig;
34
41
  planCallbacks = options.planModeCallbacks;
35
42
  }
@@ -56,7 +63,8 @@ export function createPlatformTools(config, options) {
56
63
  tools.push(...createEpisodeTools(config));
57
64
  }
58
65
  if (config.context.canvases) {
59
- tools.push(...createCanvasTools(config));
66
+ // The resizer is shared with view_image — canvas_asset_add resizes to display size.
67
+ tools.push(...createCanvasTools(config, imageConfig));
60
68
  }
61
69
  return tools;
62
70
  }
@@ -11,6 +11,7 @@
11
11
  * - All repo calls are properly await-ed
12
12
  */
13
13
  import { defineTool, createSuccessResult, createErrorResult } from '@compilr-dev/agents';
14
+ import { resolveCwd } from '../context.js';
14
15
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
15
16
  export function createProjectTools(config) {
16
17
  const { context: ctx, hooks } = config;
@@ -55,7 +56,7 @@ export function createProjectTools(config) {
55
56
  project = await ctx.projects.findByPath(input.path);
56
57
  }
57
58
  else {
58
- const cwd = config.cwd ?? process.cwd();
59
+ const cwd = resolveCwd(config.cwd);
59
60
  project = await ctx.projects.findByPath(cwd);
60
61
  }
61
62
  if (!project) {
@@ -166,7 +167,7 @@ export function createProjectTools(config) {
166
167
  return createErrorResult(`Project with name "${input.name}" already exists`);
167
168
  }
168
169
  // Check if a project exists at this path
169
- const projectPath = input.path ?? config.cwd ?? process.cwd();
170
+ const projectPath = input.path ?? resolveCwd(config.cwd);
170
171
  const existingByPath = await ctx.projects.findByPath(projectPath);
171
172
  if (existingByPath) {
172
173
  return createErrorResult(`A project already exists at path "${projectPath}": ${existingByPath.name}`);
@@ -0,0 +1,8 @@
1
+ /** The light theme-flat exemplar, ready to adapt. */
2
+ export declare const THEME_FLAT_EXEMPLAR = "<div style=\"--canvas-bg: #E9EAEE; --canvas-fg: #1B1C1F; --canvas-muted: #63656D; --canvas-accent: #C2410C; --canvas-card: #FFFFFF; --canvas-border: #D5D7DE; --canvas-surface-1: #FFFFFF; --canvas-surface-2: #F2F3F6; --canvas-hairline: #D5D7DE; width: 1000px; background: var(--canvas-bg); font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; color: var(--canvas-fg); padding: 40px;\">\n\n <div style=\"display: flex; align-items: baseline; justify-content: space-between; gap: 24px; margin-bottom: 24px;\">\n <div style=\"font-size: 11px; letter-spacing: 0.14em; font-weight: 600; color: var(--canvas-muted);\">Q3 DELIVERY REVIEW &nbsp;&middot;&nbsp; PLATFORM</div>\n <div style=\"font-size: 11px; letter-spacing: 0.08em; color: var(--canvas-muted);\">as of 6 Sep 2026</div>\n </div>\n\n <div style=\"display: grid; grid-template-columns: 1.35fr 1fr; gap: 16px; margin-bottom: 16px;\">\n <div style=\"background: var(--canvas-surface-1, var(--canvas-card)); border: 1px solid var(--canvas-hairline, var(--canvas-border)); padding: 30px 32px;\">\n <div style=\"font-size: 11px; letter-spacing: 0.12em; font-weight: 600; color: var(--canvas-accent); margin-bottom: 16px;\">THE QUARTER IN ONE NUMBER</div>\n <div style=\"font-size: 84px; line-height: 0.94; font-weight: 700; letter-spacing: -0.035em; font-variant-numeric: tabular-nums; margin-bottom: 12px;\">41<span style=\"font-size: 40px; font-weight: 600; color: var(--canvas-muted);\"> of 47</span></div>\n <div style=\"font-size: 20px; line-height: 1.35; font-weight: 600; letter-spacing: -0.01em; margin-bottom: 14px; max-width: 22em;\">commitments shipped, and the six that slipped share one cause.</div>\n <p style=\"font-size: 14px; line-height: 1.65; color: color-mix(in oklab, var(--canvas-fg) 78%, var(--canvas-bg)); margin: 0;\">Every delayed item was blocked on the migration, not on capacity. Team throughput rose 12% over Q2 while the queue behind a single dependency grew, which is why headcount is the wrong lever here.</p>\n </div>\n\n <div style=\"display: grid; grid-template-rows: repeat(3, 1fr); gap: 16px;\">\n <div style=\"background: var(--canvas-surface-1, var(--canvas-card)); border: 1px solid var(--canvas-hairline, var(--canvas-border)); padding: 18px 20px; display: flex; align-items: baseline; justify-content: space-between; gap: 16px;\">\n <div>\n <div style=\"font-size: 10px; letter-spacing: 0.12em; font-weight: 600; color: var(--canvas-muted); margin-bottom: 6px;\">CYCLE TIME</div>\n <div style=\"font-size: 34px; line-height: 1; font-weight: 700; letter-spacing: -0.02em; font-variant-numeric: tabular-nums;\">6.2<span style=\"font-size: 15px; font-weight: 500; color: var(--canvas-muted);\"> days</span></div>\n </div>\n <div style=\"font-size: 13px; font-weight: 600; color: var(--canvas-accent); flex: none;\">&#9660; 1.8</div>\n </div>\n <div style=\"background: var(--canvas-surface-1, var(--canvas-card)); border: 1px solid var(--canvas-hairline, var(--canvas-border)); padding: 18px 20px; display: flex; align-items: baseline; justify-content: space-between; gap: 16px;\">\n <div>\n <div style=\"font-size: 10px; letter-spacing: 0.12em; font-weight: 600; color: var(--canvas-muted); margin-bottom: 6px;\">ESCAPED DEFECTS</div>\n <div style=\"font-size: 34px; line-height: 1; font-weight: 700; letter-spacing: -0.02em; font-variant-numeric: tabular-nums;\">3</div>\n </div>\n <div style=\"font-size: 13px; font-weight: 600; color: var(--canvas-muted); flex: none;\">&#9660; 5</div>\n </div>\n <div style=\"background: var(--canvas-surface-1, var(--canvas-card)); border: 1px solid var(--canvas-hairline, var(--canvas-border)); padding: 18px 20px; display: flex; align-items: baseline; justify-content: space-between; gap: 16px;\">\n <div>\n <div style=\"font-size: 10px; letter-spacing: 0.12em; font-weight: 600; color: var(--canvas-muted); margin-bottom: 6px;\">BLOCKED, NOW</div>\n <div style=\"font-size: 34px; line-height: 1; font-weight: 700; letter-spacing: -0.02em; font-variant-numeric: tabular-nums;\">6</div>\n </div>\n <div style=\"font-size: 13px; font-weight: 600; color: var(--canvas-muted); flex: none;\">&#9650; 4</div>\n </div>\n </div>\n </div>\n\n <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: 16px;\">\n\n <div style=\"background: var(--canvas-surface-1, var(--canvas-card)); border: 1px solid var(--canvas-hairline, var(--canvas-border)); padding: 22px 24px 20px;\">\n <div style=\"font-size: 10px; letter-spacing: 0.12em; font-weight: 600; color: var(--canvas-muted); margin-bottom: 18px;\">SHIPPED PER SPRINT</div>\n <svg width=\"100%\" height=\"150\" viewBox=\"0 0 420 150\" style=\"display: block; overflow: visible;\">\n <line x1=\"0\" y1=\"126\" x2=\"420\" y2=\"126\" style=\"stroke: var(--canvas-hairline, var(--canvas-border));\" stroke-width=\"1\"></line>\n <rect x=\"10\" y=\"72\" width=\"46\" height=\"54\" style=\"fill: color-mix(in oklab, var(--canvas-fg) 22%, var(--canvas-bg));\"></rect>\n <text x=\"33\" y=\"62\" text-anchor=\"middle\" font-size=\"13\" font-weight=\"700\" style=\"fill: var(--canvas-fg); font-family: system-ui, sans-serif;\">6</text>\n <text x=\"33\" y=\"142\" text-anchor=\"middle\" font-size=\"11\" style=\"fill: var(--canvas-muted); font-family: system-ui, sans-serif;\">S1</text>\n <rect x=\"76\" y=\"45\" width=\"46\" height=\"81\" style=\"fill: color-mix(in oklab, var(--canvas-fg) 22%, var(--canvas-bg));\"></rect>\n <text x=\"99\" y=\"35\" text-anchor=\"middle\" font-size=\"13\" font-weight=\"700\" style=\"fill: var(--canvas-fg); font-family: system-ui, sans-serif;\">9</text>\n <text x=\"99\" y=\"142\" text-anchor=\"middle\" font-size=\"11\" style=\"fill: var(--canvas-muted); font-family: system-ui, sans-serif;\">S2</text>\n <rect x=\"142\" y=\"54\" width=\"46\" height=\"72\" style=\"fill: color-mix(in oklab, var(--canvas-fg) 22%, var(--canvas-bg));\"></rect>\n <text x=\"165\" y=\"44\" text-anchor=\"middle\" font-size=\"13\" font-weight=\"700\" style=\"fill: var(--canvas-fg); font-family: system-ui, sans-serif;\">8</text>\n <text x=\"165\" y=\"142\" text-anchor=\"middle\" font-size=\"11\" style=\"fill: var(--canvas-muted); font-family: system-ui, sans-serif;\">S3</text>\n <rect x=\"208\" y=\"27\" width=\"46\" height=\"99\" style=\"fill: color-mix(in oklab, var(--canvas-fg) 22%, var(--canvas-bg));\"></rect>\n <text x=\"231\" y=\"17\" text-anchor=\"middle\" font-size=\"13\" font-weight=\"700\" style=\"fill: var(--canvas-fg); font-family: system-ui, sans-serif;\">11</text>\n <text x=\"231\" y=\"142\" text-anchor=\"middle\" font-size=\"11\" style=\"fill: var(--canvas-muted); font-family: system-ui, sans-serif;\">S4</text>\n <rect x=\"274\" y=\"99\" width=\"46\" height=\"27\" style=\"fill: var(--canvas-accent);\"></rect>\n <text x=\"297\" y=\"89\" text-anchor=\"middle\" font-size=\"13\" font-weight=\"700\" style=\"fill: var(--canvas-accent); font-family: system-ui, sans-serif;\">3</text>\n <text x=\"297\" y=\"142\" text-anchor=\"middle\" font-size=\"11\" style=\"fill: var(--canvas-muted); font-family: system-ui, sans-serif;\">S5</text>\n <rect x=\"340\" y=\"90\" width=\"46\" height=\"36\" style=\"fill: color-mix(in oklab, var(--canvas-fg) 22%, var(--canvas-bg));\"></rect>\n <text x=\"363\" y=\"80\" text-anchor=\"middle\" font-size=\"13\" font-weight=\"700\" style=\"fill: var(--canvas-fg); font-family: system-ui, sans-serif;\">4</text>\n <text x=\"363\" y=\"142\" text-anchor=\"middle\" font-size=\"11\" style=\"fill: var(--canvas-muted); font-family: system-ui, sans-serif;\">S6</text>\n </svg>\n <div style=\"font-size: 13px; line-height: 1.55; color: color-mix(in oklab, var(--canvas-fg) 78%, var(--canvas-bg)); margin-top: 20px; border-top: 1px solid var(--canvas-hairline, var(--canvas-border)); padding-top: 12px;\">Sprint 5 is when the migration landed in the critical path.</div>\n </div>\n\n <div style=\"background: var(--canvas-surface-1, var(--canvas-card)); border: 1px solid var(--canvas-hairline, var(--canvas-border)); padding: 22px 24px 20px;\">\n <div style=\"font-size: 10px; letter-spacing: 0.12em; font-weight: 600; color: var(--canvas-muted); margin-bottom: 14px;\">THE SIX THAT SLIPPED</div>\n <div style=\"background: var(--canvas-surface-2, var(--canvas-bg)); border: 1px solid var(--canvas-hairline, var(--canvas-border));\">\n <div style=\"display: grid; grid-template-columns: 1fr 88px 74px; gap: 12px; padding: 8px 14px; border-bottom: 1px solid var(--canvas-hairline, var(--canvas-border)); font-size: 10px; letter-spacing: 0.1em; font-weight: 600; color: var(--canvas-muted);\"><span>COMMITMENT</span><span>BLOCKED BY</span><span style=\"text-align: right;\">DAYS</span></div>\n <div style=\"display: grid; grid-template-columns: 1fr 88px 74px; gap: 12px; padding: 10px 14px; border-bottom: 1px solid var(--canvas-hairline, var(--canvas-border)); font-size: 13px; align-items: baseline;\"><span style=\"font-weight: 600;\">Billing export</span><span style=\"color: color-mix(in oklab, var(--canvas-fg) 78%, var(--canvas-bg));\">migration</span><span style=\"text-align: right; font-variant-numeric: tabular-nums;\">21</span></div>\n <div style=\"display: grid; grid-template-columns: 1fr 88px 74px; gap: 12px; padding: 10px 14px; border-bottom: 1px solid var(--canvas-hairline, var(--canvas-border)); font-size: 13px; align-items: baseline;\"><span style=\"font-weight: 600;\">Audit trail</span><span style=\"color: color-mix(in oklab, var(--canvas-fg) 78%, var(--canvas-bg));\">migration</span><span style=\"text-align: right; font-variant-numeric: tabular-nums;\">18</span></div>\n <div style=\"display: grid; grid-template-columns: 1fr 88px 74px; gap: 12px; padding: 10px 14px; border-bottom: 1px solid var(--canvas-hairline, var(--canvas-border)); font-size: 13px; align-items: baseline;\"><span style=\"font-weight: 600;\">SSO for teams</span><span style=\"color: color-mix(in oklab, var(--canvas-fg) 78%, var(--canvas-bg));\">migration</span><span style=\"text-align: right; font-variant-numeric: tabular-nums;\">14</span></div>\n <div style=\"display: grid; grid-template-columns: 1fr 88px 74px; gap: 12px; padding: 10px 14px; border-bottom: 1px solid var(--canvas-hairline, var(--canvas-border)); font-size: 13px; align-items: baseline;\"><span style=\"font-weight: 600;\">Usage alerts</span><span style=\"color: color-mix(in oklab, var(--canvas-fg) 78%, var(--canvas-bg));\">migration</span><span style=\"text-align: right; font-variant-numeric: tabular-nums;\">11</span></div>\n <div style=\"display: grid; grid-template-columns: 1fr 88px 74px; gap: 12px; padding: 10px 14px; border-bottom: 1px solid var(--canvas-hairline, var(--canvas-border)); font-size: 13px; align-items: baseline;\"><span style=\"font-weight: 600;\">Retention job</span><span style=\"color: color-mix(in oklab, var(--canvas-fg) 78%, var(--canvas-bg));\">migration</span><span style=\"text-align: right; font-variant-numeric: tabular-nums;\">9</span></div>\n <div style=\"display: grid; grid-template-columns: 1fr 88px 74px; gap: 12px; padding: 10px 14px; font-size: 13px; align-items: baseline;\"><span style=\"font-weight: 600;\">Region failover</span><span style=\"color: color-mix(in oklab, var(--canvas-fg) 78%, var(--canvas-bg));\">migration</span><span style=\"text-align: right; font-variant-numeric: tabular-nums;\">6</span></div>\n </div>\n <div style=\"font-size: 13px; line-height: 1.55; color: color-mix(in oklab, var(--canvas-fg) 78%, var(--canvas-bg)); margin-top: 16px;\">One dependency, 79 days of accumulated delay.</div>\n </div>\n </div>\n\n <div style=\"border-top: 1px solid var(--canvas-hairline, var(--canvas-border)); margin-top: 26px; padding-top: 14px; display: flex; align-items: baseline; justify-content: space-between; gap: 24px; font-size: 11px; letter-spacing: 0.06em; color: var(--canvas-muted);\">\n <span>PLATFORM &nbsp;&middot;&nbsp; QUARTERLY REVIEW</span>\n <span>Source: delivery board, sprints 1&ndash;6</span>\n </div>\n </div>";
3
+ /** The same markup in dark: only these nine values change. */
4
+ export declare const THEME_FLAT_DARK_TOKENS = "--canvas-bg: #17181C; --canvas-fg: #ECEDF1; --canvas-muted: #9A9CA6; --canvas-accent: #FB7B4B; --canvas-card: #22242A; --canvas-border: #31333B; --canvas-surface-1: #22242A; --canvas-surface-2: #1B1D22; --canvas-hairline: #31333B";
5
+ /** The app-screen exemplar — a single UI screen, four surfaces. */
6
+ export declare const APP_SCREEN_EXEMPLAR = "<div style=\"--canvas-bg: #E9EAEE; --canvas-fg: #1B1C1F; --canvas-muted: #63656D; --canvas-accent: #C2410C; --canvas-card: #FFFFFF; --canvas-border: #D5D7DE; --canvas-surface-1: #FFFFFF; --canvas-surface-2: #F2F3F6; --canvas-hairline: #DCDEE4; width: 1000px; background: var(--canvas-surface-1, var(--canvas-card)); font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; color: var(--canvas-fg); display: grid; grid-template-columns: 232px 1fr;\">\n\n <div style=\"border-right: 1px solid var(--canvas-hairline, var(--canvas-border)); display: flex; flex-direction: column; min-height: 640px;\">\n <div style=\"padding: 18px 18px 16px; display: flex; align-items: center; gap: 10px;\">\n <span style=\"width: 28px; height: 28px; background: var(--canvas-fg); color: var(--canvas-surface-1, var(--canvas-card)); font-size: 11px; font-weight: 700; letter-spacing: 0.02em; display: flex; align-items: center; justify-content: center; flex: none;\">CO</span>\n <span style=\"font-size: 15px; font-weight: 700; letter-spacing: -0.01em;\">Cohort</span>\n </div>\n\n <div style=\"padding: 6px 10px 0; display: flex; flex-direction: column; gap: 1px;\">\n <div style=\"display: flex; align-items: center; gap: 10px; padding: 10px 12px; background: color-mix(in oklab, var(--canvas-accent) 10%, var(--canvas-surface-1, var(--canvas-card))); box-shadow: inset 2px 0 0 var(--canvas-accent); font-size: 13.5px; font-weight: 600;\">\n <span style=\"font-size: 13px; flex: none; color: var(--canvas-accent);\">&#9632;</span>Overview\n </div>\n <div style=\"display: flex; align-items: center; gap: 10px; padding: 10px 12px; font-size: 13.5px; color: color-mix(in oklab, var(--canvas-fg) 82%, var(--canvas-bg));\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"flex: none; color: var(--canvas-muted);\"><circle cx=\"6\" cy=\"6\" r=\"2.5\"></circle><path d=\"M2 13.5c.8-2.2 2.2-3.3 4-3.3s3.2 1.1 4 3.3\"></path><path d=\"M11 4.2a2.5 2.5 0 0 1 0 4.6\"></path></svg>Applicants\n <span style=\"margin-left: auto; font-size: 12px; color: var(--canvas-muted); font-variant-numeric: tabular-nums;\">248</span>\n </div>\n <div style=\"display: flex; align-items: center; gap: 10px; padding: 10px 12px; font-size: 13.5px; color: color-mix(in oklab, var(--canvas-fg) 82%, var(--canvas-bg));\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"flex: none; color: var(--canvas-muted);\"><rect x=\"2.5\" y=\"3.5\" width=\"11\" height=\"10\" rx=\"1\"></rect><path d=\"M2.5 6.5h11M5.5 2v2.5M10.5 2v2.5\"></path></svg>Interviews\n <span style=\"margin-left: auto; font-size: 12px; color: var(--canvas-muted); font-variant-numeric: tabular-nums;\">67</span>\n </div>\n <div style=\"display: flex; align-items: center; gap: 10px; padding: 10px 12px; font-size: 13.5px; color: color-mix(in oklab, var(--canvas-fg) 82%, var(--canvas-bg));\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"flex: none; color: var(--canvas-muted);\"><path d=\"M9.5 2H4.5A1.5 1.5 0 0 0 3 3.5v9A1.5 1.5 0 0 0 4.5 14h7a1.5 1.5 0 0 0 1.5-1.5V5.5z\"></path><path d=\"M9.5 2v3.5H13\"></path></svg>Documents\n </div>\n <div style=\"display: flex; align-items: center; gap: 10px; padding: 10px 12px; font-size: 13.5px; color: color-mix(in oklab, var(--canvas-fg) 82%, var(--canvas-bg));\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"flex: none; color: var(--canvas-muted);\"><path d=\"M2 14h12\"></path><path d=\"M4.5 14V9M8 14V4.5M11.5 14V7\"></path></svg>Reports\n </div>\n <div style=\"display: flex; align-items: center; gap: 10px; padding: 10px 12px; font-size: 13.5px; color: color-mix(in oklab, var(--canvas-fg) 82%, var(--canvas-bg));\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"flex: none; color: var(--canvas-muted);\"><path d=\"M2 5.5A1.5 1.5 0 0 1 3.5 4h2.2l1.3 1.6h5.5A1.5 1.5 0 0 1 14 7.1v5.4A1.5 1.5 0 0 1 12.5 14h-9A1.5 1.5 0 0 1 2 12.5z\"></path></svg>Programmes\n <span style=\"margin-left: auto; font-size: 12px; color: var(--canvas-muted); font-variant-numeric: tabular-nums;\">12</span>\n </div>\n </div>\n\n <div style=\"margin-top: auto; border-top: 1px solid var(--canvas-hairline, var(--canvas-border)); padding: 12px 22px; font-size: 13.5px; color: color-mix(in oklab, var(--canvas-fg) 82%, var(--canvas-bg));\">Settings</div>\n <div style=\"border-top: 1px solid var(--canvas-hairline, var(--canvas-border)); padding: 14px 18px; display: flex; align-items: center; gap: 10px;\">\n <span style=\"width: 30px; height: 30px; background: var(--canvas-surface-2, var(--canvas-bg)); border: 1px solid var(--canvas-hairline, var(--canvas-border)); font-size: 11px; font-weight: 600; color: var(--canvas-muted); display: flex; align-items: center; justify-content: center; flex: none;\">RM</span>\n <span>\n <span style=\"display: block; font-size: 13px; font-weight: 600;\">Rita Marchetti</span>\n <span style=\"display: block; font-size: 11px; color: var(--canvas-muted);\">Admissions lead</span>\n </span>\n </div>\n </div>\n\n <div style=\"display: flex; flex-direction: column;\">\n <div style=\"padding: 20px 28px 18px; border-bottom: 1px solid var(--canvas-hairline, var(--canvas-border)); display: flex; align-items: flex-end; justify-content: space-between; gap: 24px;\">\n <div>\n <div style=\"font-size: 11px; letter-spacing: 0.06em; color: var(--canvas-muted); margin-bottom: 6px;\">Autumn 2027 &nbsp;/&nbsp; Admissions</div>\n <div style=\"font-size: 26px; font-weight: 700; letter-spacing: -0.022em;\">Overview</div>\n </div>\n <div style=\"display: flex; align-items: center; gap: 8px; flex: none;\">\n <span style=\"font-size: 13px; padding: 8px 14px; border: 1px solid var(--canvas-hairline, var(--canvas-border)); color: color-mix(in oklab, var(--canvas-fg) 82%, var(--canvas-bg));\">Export</span>\n <span style=\"font-size: 13px; font-weight: 600; padding: 9px 15px; background: var(--canvas-fg); color: var(--canvas-surface-1, var(--canvas-card));\">New applicant</span>\n </div>\n </div>\n <div style=\"background: var(--canvas-surface-2, var(--canvas-bg)); padding: 20px 28px 26px; flex: 1; display: flex; flex-direction: column; gap: 16px;\">\n\n <div style=\"display: grid; grid-template-columns: 1.45fr 1fr; gap: 16px;\">\n <div style=\"background: var(--canvas-surface-1, var(--canvas-card)); border: 1px solid var(--canvas-hairline, var(--canvas-border)); padding: 22px 24px;\">\n <div style=\"display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin-bottom: 14px;\">\n <div style=\"font-size: 10px; letter-spacing: 0.12em; font-weight: 600; color: var(--canvas-accent);\">THIS WEEK</div>\n <div style=\"font-size: 11px; color: var(--canvas-muted);\">updated 4 min ago</div>\n </div>\n <div style=\"font-size: 27px; line-height: 1.2; font-weight: 700; letter-spacing: -0.022em; margin-bottom: 10px; max-width: 20em;\">Interviews are keeping pace. Document review is not.</div>\n <p style=\"font-size: 13.5px; line-height: 1.65; color: color-mix(in oklab, var(--canvas-fg) 78%, var(--canvas-bg)); margin: 0 0 16px;\">248 applicants across 12 programmes, and 67 have reached interview. The queue sits at document review, where 41 files have waited more than five days &mdash; two programmes account for 30 of them.</p>\n <span style=\"display: inline-block; font-size: 12.5px; font-weight: 600; color: var(--canvas-accent); border-bottom: 1px solid var(--canvas-accent); padding-bottom: 1px;\">Review the 41 waiting files</span>\n </div>\n\n <div style=\"background: var(--canvas-fg); color: var(--canvas-surface-1, var(--canvas-card)); padding: 20px 22px; display: flex; flex-direction: column; gap: 14px;\">\n <div style=\"display: flex; align-items: baseline; justify-content: space-between; gap: 12px;\">\n <span style=\"font-size: 10px; letter-spacing: 0.12em; font-weight: 600;\">PIPELINE</span>\n <span style=\"font-size: 10px; letter-spacing: 0.06em; opacity: 0.62;\">stage by stage</span>\n </div>\n <div style=\"display: flex; flex-direction: column; gap: 11px;\">\n <div>\n <div style=\"display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-size: 12px;\"><span style=\"opacity: 0.75;\">Applied</span><span style=\"font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums;\">248</span></div>\n <div style=\"height: 4px; background: rgba(255,255,255,0.16); margin-top: 5px;\"><div style=\"height: 4px; width: 100%; background: rgba(255,255,255,0.62);\"></div></div>\n </div>\n <div>\n <div style=\"display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-size: 12px;\"><span style=\"opacity: 0.75;\">Documents in</span><span style=\"font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums;\">193</span></div>\n <div style=\"height: 4px; background: rgba(255,255,255,0.16); margin-top: 5px;\"><div style=\"height: 4px; width: 78%; background: rgba(255,255,255,0.62);\"></div></div>\n </div>\n <div>\n <div style=\"display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-size: 12px;\"><span style=\"opacity: 0.75;\">Reviewed</span><span style=\"font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums;\">152</span></div>\n <div style=\"height: 4px; background: rgba(255,255,255,0.16); margin-top: 5px;\"><div style=\"height: 4px; width: 61%; background: var(--canvas-accent);\"></div></div>\n </div>\n <div>\n <div style=\"display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-size: 12px;\"><span style=\"opacity: 0.75;\">Interviewed</span><span style=\"font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums;\">67</span></div>\n <div style=\"height: 4px; background: rgba(255,255,255,0.16); margin-top: 5px;\"><div style=\"height: 4px; width: 27%; background: rgba(255,255,255,0.62);\"></div></div>\n </div>\n </div>\n <div style=\"margin-top: auto; border-top: 1px solid rgba(255,255,255,0.18); padding-top: 11px; font-size: 12px; opacity: 0.82;\">41 files stalled at review</div>\n </div>\n </div>\n\n <div style=\"display: flex; align-items: center; gap: 8px;\">\n <span style=\"font-size: 10px; letter-spacing: 0.12em; font-weight: 600; color: var(--canvas-muted); margin-right: 4px;\">FACULTY</span>\n <span style=\"font-size: 12.5px; font-weight: 600; padding: 6px 12px; background: var(--canvas-surface-1, var(--canvas-card)); border: 1px solid var(--canvas-fg);\">All <span style=\"color: var(--canvas-muted); font-weight: 400;\">12</span></span>\n <span style=\"font-size: 12.5px; padding: 6px 12px; background: var(--canvas-surface-1, var(--canvas-card)); border: 1px solid var(--canvas-hairline, var(--canvas-border)); color: color-mix(in oklab, var(--canvas-fg) 82%, var(--canvas-bg));\">Design <span style=\"color: var(--canvas-muted);\">4</span></span>\n <span style=\"font-size: 12.5px; padding: 6px 12px; background: var(--canvas-surface-1, var(--canvas-card)); border: 1px solid var(--canvas-hairline, var(--canvas-border)); color: color-mix(in oklab, var(--canvas-fg) 82%, var(--canvas-bg));\">Business <span style=\"color: var(--canvas-muted);\">5</span></span>\n <span style=\"font-size: 12.5px; padding: 6px 12px; background: var(--canvas-surface-1, var(--canvas-card)); border: 1px solid var(--canvas-hairline, var(--canvas-border)); color: color-mix(in oklab, var(--canvas-fg) 82%, var(--canvas-bg));\">Engineering <span style=\"color: var(--canvas-muted);\">3</span></span>\n </div>\n\n <div style=\"background: var(--canvas-surface-1, var(--canvas-card)); border: 1px solid var(--canvas-hairline, var(--canvas-border));\">\n <div style=\"padding: 13px 18px; border-bottom: 1px solid var(--canvas-hairline, var(--canvas-border)); display: flex; align-items: baseline; justify-content: space-between; gap: 16px;\">\n <span style=\"font-size: 14px; font-weight: 600;\">Programmes needing attention</span>\n <span style=\"font-size: 12px; color: var(--canvas-muted);\">4 of 12</span>\n </div>\n <div style=\"display: grid; grid-template-columns: 1.6fr 120px 84px 84px 92px; gap: 14px; padding: 9px 18px; background: var(--canvas-surface-2, var(--canvas-bg)); border-bottom: 1px solid var(--canvas-hairline, var(--canvas-border)); font-size: 10px; letter-spacing: 0.1em; font-weight: 600; color: var(--canvas-muted);\">\n <span>PROGRAMME</span><span>REVIEW</span><span style=\"text-align: right;\">APPLIED</span><span style=\"text-align: right;\">OFFERS</span><span style=\"text-align: right;\">WAITING</span>\n </div>\n <div style=\"display: grid; grid-template-columns: 1.6fr 120px 84px 84px 92px; gap: 14px; padding: 12px 18px; border-bottom: 1px solid var(--canvas-hairline, var(--canvas-border)); font-size: 13px; align-items: center;\">\n <span style=\"font-weight: 600;\">MSc Product Design</span>\n <span style=\"height: 5px; background: var(--canvas-surface-2, var(--canvas-bg));\"><span style=\"display: block; height: 5px; width: 38%; background: var(--canvas-accent);\"></span></span>\n <span style=\"text-align: right; font-variant-numeric: tabular-nums;\">31</span>\n <span style=\"text-align: right; font-variant-numeric: tabular-nums;\">4</span>\n <span style=\"text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; color: var(--canvas-accent);\">17</span>\n </div>\n <div style=\"display: grid; grid-template-columns: 1.6fr 120px 84px 84px 92px; gap: 14px; padding: 12px 18px; border-bottom: 1px solid var(--canvas-hairline, var(--canvas-border)); font-size: 13px; align-items: center;\">\n <span style=\"font-weight: 600;\">MBA Sustainable Business</span>\n <span style=\"height: 5px; background: var(--canvas-surface-2, var(--canvas-bg));\"><span style=\"display: block; height: 5px; width: 46%; background: var(--canvas-accent);\"></span></span>\n <span style=\"text-align: right; font-variant-numeric: tabular-nums;\">28</span>\n <span style=\"text-align: right; font-variant-numeric: tabular-nums;\">6</span>\n <span style=\"text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; color: var(--canvas-accent);\">13</span>\n </div>\n <div style=\"display: grid; grid-template-columns: 1.6fr 120px 84px 84px 92px; gap: 14px; padding: 12px 18px; border-bottom: 1px solid var(--canvas-hairline, var(--canvas-border)); font-size: 13px; align-items: center;\">\n <span style=\"font-weight: 600;\">MA Curation</span>\n <span style=\"height: 5px; background: var(--canvas-surface-2, var(--canvas-bg));\"><span style=\"display: block; height: 5px; width: 71%; background: color-mix(in oklab, var(--canvas-fg) 32%, var(--canvas-bg));\"></span></span>\n <span style=\"text-align: right; font-variant-numeric: tabular-nums;\">19</span>\n <span style=\"text-align: right; font-variant-numeric: tabular-nums;\">5</span>\n <span style=\"text-align: right; font-variant-numeric: tabular-nums;\">7</span>\n </div>\n <div style=\"display: grid; grid-template-columns: 1.6fr 120px 84px 84px 92px; gap: 14px; padding: 12px 18px; font-size: 13px; align-items: center;\">\n <span style=\"font-weight: 600;\">MEng Robotics</span>\n <span style=\"height: 5px; background: var(--canvas-surface-2, var(--canvas-bg));\"><span style=\"display: block; height: 5px; width: 83%; background: color-mix(in oklab, var(--canvas-fg) 32%, var(--canvas-bg));\"></span></span>\n <span style=\"text-align: right; font-variant-numeric: tabular-nums;\">24</span>\n <span style=\"text-align: right; font-variant-numeric: tabular-nums;\">9</span>\n <span style=\"text-align: right; font-variant-numeric: tabular-nums;\">4</span>\n </div>\n </div>\n </div>\n </div>\n </div>";
7
+ export declare const canvasExemplarInfographicSkill: import("@compilr-dev/agents").Skill;
8
+ export declare const canvasExemplarAppScreenSkill: import("@compilr-dev/agents").Skill;