@crowdedkingdoms/crowdyjs 11.0.0 → 12.0.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 (81) hide show
  1. package/MIGRATION.md +118 -0
  2. package/README.md +208 -0
  3. package/dist/crowdy-agent/browser-dispatcher.d.ts +41 -0
  4. package/dist/crowdy-agent/browser-dispatcher.d.ts.map +1 -0
  5. package/dist/crowdy-agent/browser-dispatcher.js +264 -0
  6. package/dist/crowdy-agent/controller.d.ts +144 -0
  7. package/dist/crowdy-agent/controller.d.ts.map +1 -0
  8. package/dist/crowdy-agent/controller.js +1239 -0
  9. package/dist/crowdy-agent/errors.d.ts +37 -0
  10. package/dist/crowdy-agent/errors.d.ts.map +1 -0
  11. package/dist/crowdy-agent/errors.js +107 -0
  12. package/dist/crowdy-agent/graphql-transport.d.ts +154 -0
  13. package/dist/crowdy-agent/graphql-transport.d.ts.map +1 -0
  14. package/dist/crowdy-agent/graphql-transport.js +854 -0
  15. package/dist/crowdy-agent/index.d.ts +16 -0
  16. package/dist/crowdy-agent/index.d.ts.map +1 -0
  17. package/dist/crowdy-agent/index.js +15 -0
  18. package/dist/crowdy-agent/registry.d.ts +29 -0
  19. package/dist/crowdy-agent/registry.d.ts.map +1 -0
  20. package/dist/crowdy-agent/registry.js +281 -0
  21. package/dist/crowdy-agent/schema.d.ts +72 -0
  22. package/dist/crowdy-agent/schema.d.ts.map +1 -0
  23. package/dist/crowdy-agent/schema.js +467 -0
  24. package/dist/crowdy-agent/studio-tools.d.ts +15 -0
  25. package/dist/crowdy-agent/studio-tools.d.ts.map +1 -0
  26. package/dist/crowdy-agent/studio-tools.js +280 -0
  27. package/dist/crowdy-agent/tool-descriptors.d.ts +4 -0
  28. package/dist/crowdy-agent/tool-descriptors.d.ts.map +1 -0
  29. package/dist/crowdy-agent/tool-descriptors.js +1049 -0
  30. package/dist/crowdy-agent/transport.d.ts +152 -0
  31. package/dist/crowdy-agent/transport.d.ts.map +1 -0
  32. package/dist/crowdy-agent/transport.js +27 -0
  33. package/dist/crowdy-agent/types.d.ts +328 -0
  34. package/dist/crowdy-agent/types.d.ts.map +1 -0
  35. package/dist/crowdy-agent/types.js +37 -0
  36. package/dist/crowdy-client.d.ts +3 -0
  37. package/dist/crowdy-client.d.ts.map +1 -1
  38. package/dist/crowdy-client.js +6 -0
  39. package/dist/crowdy-studio/agent-dom-shell.d.ts +40 -0
  40. package/dist/crowdy-studio/agent-dom-shell.d.ts.map +1 -0
  41. package/dist/crowdy-studio/agent-dom-shell.js +354 -0
  42. package/dist/crowdy-studio/controller.d.ts +74 -4
  43. package/dist/crowdy-studio/controller.d.ts.map +1 -1
  44. package/dist/crowdy-studio/controller.js +450 -17
  45. package/dist/crowdy-studio/dom-shell.d.ts +4 -1
  46. package/dist/crowdy-studio/dom-shell.d.ts.map +1 -1
  47. package/dist/crowdy-studio/dom-shell.js +6 -1
  48. package/dist/crowdy-studio/index.d.ts +6 -3
  49. package/dist/crowdy-studio/index.d.ts.map +1 -1
  50. package/dist/crowdy-studio/index.js +3 -0
  51. package/dist/crowdy-studio/models.d.ts +67 -0
  52. package/dist/crowdy-studio/models.d.ts.map +1 -1
  53. package/dist/crowdy-studio/mount.d.ts +22 -1
  54. package/dist/crowdy-studio/mount.d.ts.map +1 -1
  55. package/dist/crowdy-studio/mount.js +163 -2
  56. package/dist/crowdy-studio/styles.d.ts +1 -1
  57. package/dist/crowdy-studio/styles.d.ts.map +1 -1
  58. package/dist/crowdy-studio/styles.js +5 -3
  59. package/dist/generated/graphql.d.ts +3753 -1
  60. package/dist/generated/graphql.d.ts.map +1 -1
  61. package/dist/generated/graphql.js +296 -0
  62. package/dist/index.d.ts +1 -1
  63. package/dist/index.js +1 -1
  64. package/dist/kit/npcs.d.ts.map +1 -1
  65. package/dist/kit/social.d.ts.map +1 -1
  66. package/dist/player-host/index.d.ts +5 -0
  67. package/dist/player-host/index.d.ts.map +1 -0
  68. package/dist/player-host/index.js +3 -0
  69. package/dist/player-host/lease-manager.d.ts +66 -0
  70. package/dist/player-host/lease-manager.d.ts.map +1 -0
  71. package/dist/player-host/lease-manager.js +428 -0
  72. package/dist/player-host/schemas.d.ts +9 -0
  73. package/dist/player-host/schemas.d.ts.map +1 -0
  74. package/dist/player-host/schemas.js +347 -0
  75. package/dist/player-host/tools.d.ts +13 -0
  76. package/dist/player-host/tools.d.ts.map +1 -0
  77. package/dist/player-host/tools.js +79 -0
  78. package/dist/player-host/types.d.ts +203 -0
  79. package/dist/player-host/types.d.ts.map +1 -0
  80. package/dist/player-host/types.js +1 -0
  81. package/package.json +12 -2
@@ -1,4 +1,5 @@
1
1
  import { PlayerCodeBroker, } from '../player-runtime/player-code-broker.js';
2
+ import { digestCanonicalJson, sha256Digest, } from '../crowdy-agent/schema.js';
2
3
  import { parseRustcDiagnostics } from './diagnostics.js';
3
4
  import { cloneCrowdyStudioProject, crowdyStudioFileKey, normalizeCrowdyStudioPath, projectTargets, CrowdyStudioOfflineError, CrowdyStudioRevisionConflictError, } from './models.js';
4
5
  import { createCrowdyStudioStarterProject, } from './starter-projects.js';
@@ -21,6 +22,9 @@ export class CrowdyStudioController {
21
22
  activeFile: null,
22
23
  saveState: 'SAVED',
23
24
  runtime: { phase: 'IDLE' },
25
+ runtimeSync: { state: 'NEVER_RUN' },
26
+ agentActivity: 'IDLE',
27
+ checkpoints: [],
24
28
  buildOutput: '',
25
29
  authoritativeDiagnostics: [],
26
30
  localDiagnostics: [],
@@ -31,6 +35,7 @@ export class CrowdyStudioController {
31
35
  invokeResult: null,
32
36
  };
33
37
  this.listeners = new Set();
38
+ this.humanEditListeners = new Set();
34
39
  this.autosaveTimer = null;
35
40
  this.retryTimer = null;
36
41
  this.savePromise = null;
@@ -39,6 +44,7 @@ export class CrowdyStudioController {
39
44
  this.conflictRemote = null;
40
45
  this.broker = null;
41
46
  this.operationGeneration = 0;
47
+ this.agentOperationGeneration = 0;
42
48
  this.visibleSurfaces = new Set();
43
49
  this.surfaceTimers = new Map();
44
50
  this.pageVisible = true;
@@ -54,6 +60,51 @@ export class CrowdyStudioController {
54
60
  listener(this.state);
55
61
  return () => this.listeners.delete(listener);
56
62
  }
63
+ /** Subscribe to synchronous human-edit preemption signals. */
64
+ onHumanEdit(listener) {
65
+ this.humanEditListeners.add(listener);
66
+ return () => this.humanEditListeners.delete(listener);
67
+ }
68
+ /**
69
+ * Flush autosave before a durable agent turn. Conflict/offline state fails
70
+ * closed so Build never starts from an uncommitted browser snapshot.
71
+ */
72
+ async prepareForAgentWork() {
73
+ this.ensureAlive();
74
+ this.update({ agentActivity: 'PREPARING' });
75
+ const saved = await this.saveNow();
76
+ if (!saved || this.state.saveState !== 'SAVED') {
77
+ this.update({ agentActivity: 'PAUSED' });
78
+ throw new Error('Resolve the project save before starting agent work');
79
+ }
80
+ const project = this.state.project;
81
+ this.update({ agentActivity: 'WORKING' });
82
+ return {
83
+ ...(project
84
+ ? {
85
+ projectId: project.projectId,
86
+ projectRevisionId: project.revision.id,
87
+ }
88
+ : {}),
89
+ saveState: 'SAVED',
90
+ runtimeSync: { ...this.state.runtimeSync },
91
+ };
92
+ }
93
+ finishAgentWork(paused = false) {
94
+ this.update({ agentActivity: paused ? 'PAUSED' : 'IDLE' });
95
+ }
96
+ beginAgentOperation() {
97
+ return ++this.agentOperationGeneration;
98
+ }
99
+ /** Synchronously fence an in-flight agent compile/deploy/invoke operation. */
100
+ cancelAgentOperation(message = 'Agent operation cancelled') {
101
+ ++this.agentOperationGeneration;
102
+ ++this.operationGeneration;
103
+ this.update({
104
+ agentActivity: 'PAUSED',
105
+ runtime: { phase: 'IDLE', message },
106
+ });
107
+ }
57
108
  canTarget(target, action) {
58
109
  const permission = this.options.targetPermissions?.[target];
59
110
  return permission
@@ -62,6 +113,32 @@ export class CrowdyStudioController {
62
113
  : permission.canRun
63
114
  : true;
64
115
  }
116
+ /** Credential-free context projection used by exact browser agent tools. */
117
+ getAgentContext() {
118
+ const project = this.state.project;
119
+ return {
120
+ appRef: this.options.appId,
121
+ ...(project ? { projectRef: project.projectId } : {}),
122
+ gridRef: this.options.gridId,
123
+ contextVersion: digestCanonicalJson({
124
+ contract: 'crowdy.studio-context/1',
125
+ appRef: this.options.appId,
126
+ gridRef: this.options.gridId,
127
+ ...(project
128
+ ? {
129
+ projectRef: project.projectId,
130
+ projectRevisionId: project.revision.id,
131
+ projectContentHash: projectContentHash(project),
132
+ }
133
+ : {}),
134
+ saveState: this.state.saveState,
135
+ runtimeSync: this.state.runtimeSync,
136
+ }),
137
+ ...(project
138
+ ? { projectContentHash: projectContentHash(project) }
139
+ : {}),
140
+ };
141
+ }
65
142
  async initialize() {
66
143
  this.ensureAlive();
67
144
  const scope = this.scope();
@@ -128,6 +205,9 @@ export class CrowdyStudioController {
128
205
  projectId,
129
206
  });
130
207
  this.installProject(project);
208
+ if (this.options.synchronizationProvider) {
209
+ await this.refreshCheckpoints();
210
+ }
131
211
  }
132
212
  installProject(project) {
133
213
  ++this.operationGeneration;
@@ -148,6 +228,12 @@ export class CrowdyStudioController {
148
228
  saveState: 'SAVED',
149
229
  saveMessage: undefined,
150
230
  runtime: { phase: 'IDLE' },
231
+ runtimeSync: {
232
+ state: 'NEVER_RUN',
233
+ savedRevisionId: clone.revision.id,
234
+ },
235
+ agentActivity: 'IDLE',
236
+ checkpoints: [],
151
237
  buildOutput: '',
152
238
  authoritativeDiagnostics: [],
153
239
  localDiagnostics: [],
@@ -352,19 +438,183 @@ export class CrowdyStudioController {
352
438
  this.update({ saveState: 'SAVING', saveMessage: undefined });
353
439
  return this.saveNow();
354
440
  }
355
- async testDraft() {
356
- await this.deployProject(true);
441
+ async refreshCheckpoints() {
442
+ const project = this.requireProject();
443
+ const checkpoints = this.options.synchronizationProvider
444
+ ? await this.options.synchronizationProvider.listCheckpoints({
445
+ ...this.scope(),
446
+ projectId: project.projectId,
447
+ })
448
+ : this.state.checkpoints;
449
+ this.update({ checkpoints: [...checkpoints] });
450
+ return checkpoints;
451
+ }
452
+ /**
453
+ * Validate every change against one immutable baseline, then persist and
454
+ * synchronize all files or none. Routine agent patches cannot delete/rename.
455
+ */
456
+ async applyAtomicPatch(input) {
457
+ if (!(await this.saveNow())) {
458
+ throw new Error('Resolve the current project save before applying an agent patch');
459
+ }
460
+ const baseline = cloneCrowdyStudioProject(this.requireProject());
461
+ if (baseline.revision.id !== input.expectedRevisionId) {
462
+ throw new CrowdyStudioRevisionConflictError(`Expected revision ${input.expectedRevisionId}, found ${baseline.revision.id}`, baseline);
463
+ }
464
+ applyValidatedPatch(baseline, input);
465
+ if (!this.options.synchronizationProvider) {
466
+ throw new Error('Atomic agent patches require a durable synchronization provider');
467
+ }
468
+ const result = await this.options.synchronizationProvider.applyAtomicPatch({
469
+ ...this.scope(),
470
+ projectId: baseline.projectId,
471
+ expectedRevisionId: input.expectedRevisionId,
472
+ changes: input.changes,
473
+ });
474
+ if (result.project.projectId !== baseline.projectId) {
475
+ throw new Error('Atomic patch returned a different project');
476
+ }
477
+ if (result.project.revision.id === baseline.revision.id ||
478
+ result.checkpoint.projectRevisionId !== baseline.revision.id) {
479
+ throw new Error('Atomic patch returned invalid revision/checkpoint metadata');
480
+ }
481
+ for (const change of input.changes) {
482
+ const persisted = result.project.files.find((file) => file.target === change.target &&
483
+ file.path === normalizeCrowdyStudioPath(change.path));
484
+ if (!persisted || persisted.content !== change.content) {
485
+ throw new Error(`Atomic patch did not synchronize ${change.target}:${change.path}`);
486
+ }
487
+ }
488
+ this.synchronizeProject(result.project, {
489
+ source: 'AGENT',
490
+ expectedPreviousRevisionId: baseline.revision.id,
491
+ checkpoint: result.checkpoint,
492
+ });
493
+ return result;
494
+ }
495
+ /**
496
+ * Apply a server-published project revision to Monaco/kernel state. Pending
497
+ * human edits win and turn the update into an explicit conflict.
498
+ */
499
+ synchronizeProject(project, synchronization) {
500
+ const current = this.requireProject();
501
+ if (project.projectId !== current.projectId) {
502
+ throw new Error('Project synchronization target does not match the open project');
503
+ }
504
+ if (synchronization.expectedPreviousRevisionId &&
505
+ synchronization.expectedPreviousRevisionId !== current.revision.id) {
506
+ throw new CrowdyStudioRevisionConflictError('Project synchronization started from a stale revision', project);
507
+ }
508
+ if (this.persistedGeneration !== this.editGeneration) {
509
+ this.conflictRemote = cloneCrowdyStudioProject(project);
510
+ this.update({
511
+ saveState: 'CONFLICT',
512
+ saveMessage: 'Human edits preempted an incoming agent project revision',
513
+ agentActivity: 'PAUSED',
514
+ });
515
+ throw new CrowdyStudioRevisionConflictError('Human edits preempted the agent project synchronization', project);
516
+ }
517
+ this.clearSaveTimers();
518
+ const clone = cloneCrowdyStudioProject(project);
519
+ this.editGeneration = 0;
520
+ this.persistedGeneration = 0;
521
+ this.conflictRemote = null;
522
+ const openFiles = this.state.openFiles.filter((ref) => fileRefExists(clone, this.state, ref));
523
+ const activeFile = this.state.activeFile &&
524
+ openFiles.some((ref) => sameFileRef(ref, this.state.activeFile))
525
+ ? this.state.activeFile
526
+ : openFiles.at(-1) ?? null;
527
+ const checkpoint = synchronization.checkpoint;
528
+ this.update({
529
+ project: clone,
530
+ projects: upsertSummary(this.state.projects, summaryOf(clone)),
531
+ openFiles,
532
+ activeFile,
533
+ saveState: 'SAVED',
534
+ saveMessage: undefined,
535
+ checkpoints: checkpoint
536
+ ? upsertCheckpoint(this.state.checkpoints, checkpoint)
537
+ : this.state.checkpoints,
538
+ runtimeSync: {
539
+ ...this.state.runtimeSync,
540
+ savedRevisionId: clone.revision.id,
541
+ state: this.state.runtimeSync.state === 'RUNNING_SAVED' ||
542
+ this.state.runtimeSync.state === 'RUNNING_STALE'
543
+ ? this.state.runtimeSync.runningRevisionId === clone.revision.id
544
+ ? 'RUNNING_SAVED'
545
+ : 'RUNNING_STALE'
546
+ : this.state.runtimeSync.state,
547
+ },
548
+ });
549
+ this.options.onProjectSynchronized?.(cloneCrowdyStudioProject(clone), synchronization);
550
+ }
551
+ async restoreCheckpoint(checkpointId, approvalGrant, expectedRevisionId = this.requireProject().revision.id) {
552
+ if (approvalGrant.trim().length < 8) {
553
+ throw new Error('Checkpoint restore requires an opaque exact approval grant');
554
+ }
555
+ if (!(await this.saveNow())) {
556
+ throw new Error('Resolve the current project save before restoring a checkpoint');
557
+ }
558
+ const current = cloneCrowdyStudioProject(this.requireProject());
559
+ if (current.revision.id !== expectedRevisionId) {
560
+ throw new CrowdyStudioRevisionConflictError(`Expected revision ${expectedRevisionId}, found ${current.revision.id}`, current);
561
+ }
562
+ let restoredProject;
563
+ let preRestoreCheckpoint;
564
+ if (!this.options.synchronizationProvider) {
565
+ throw new Error('Checkpoint restore requires a durable synchronization provider');
566
+ }
567
+ const restored = await this.options.synchronizationProvider.restoreCheckpoint({
568
+ ...this.scope(),
569
+ projectId: current.projectId,
570
+ checkpointId,
571
+ expectedRevisionId,
572
+ approvalGrant,
573
+ });
574
+ restoredProject = restored.project;
575
+ preRestoreCheckpoint = restored.preRestoreCheckpoint;
576
+ if (restoredProject.projectId !== current.projectId ||
577
+ restoredProject.revision.id === current.revision.id ||
578
+ preRestoreCheckpoint.projectRevisionId !== current.revision.id) {
579
+ throw new Error('Checkpoint restore returned invalid synchronization metadata');
580
+ }
581
+ this.synchronizeProject(restoredProject, {
582
+ source: 'AGENT',
583
+ expectedPreviousRevisionId: expectedRevisionId,
584
+ checkpoint: preRestoreCheckpoint,
585
+ });
586
+ return preRestoreCheckpoint;
587
+ }
588
+ async testDraft(agentOperation) {
589
+ return this.deployProject(true, agentOperation);
357
590
  }
358
- async deployLive() {
359
- await this.deployProject(false);
591
+ async testDraftPlan(plan, agentOperation) {
592
+ return this.deployProject(true, agentOperation, plan);
360
593
  }
361
- async deployProject(draft) {
594
+ async deployLive(agentOperation) {
595
+ return this.deployProject(false, agentOperation);
596
+ }
597
+ async deployLivePlan(plan, agentOperation) {
598
+ return this.deployProject(false, agentOperation, plan);
599
+ }
600
+ async deployProject(draft, agentOperation, plan) {
601
+ this.checkAgentOperation(agentOperation);
362
602
  this.requireProject();
363
603
  if (!(await this.saveNow())) {
364
604
  this.setRuntime('ERROR', 'Project must be saved before it can be built');
365
- return;
605
+ return {
606
+ deployment: draft ? 'DRAFT' : 'LIVE',
607
+ status: 'FAILED',
608
+ projectRevisionId: this.requireProject().revision.id,
609
+ targets: projectTargets(this.requireProject().kind),
610
+ message: 'Project must be saved before it can be built',
611
+ };
366
612
  }
613
+ this.checkAgentOperation(agentOperation);
367
614
  const project = this.requireProject();
615
+ if (plan)
616
+ this.assertDeploymentPlan(project, plan, draft);
617
+ const targets = plan ? [...plan.targets] : projectTargets(project.kind);
368
618
  const operation = ++this.operationGeneration;
369
619
  this.stopSurfacePolling();
370
620
  this.update({
@@ -373,12 +623,18 @@ export class CrowdyStudioController {
373
623
  authoritativeDiagnostics: [],
374
624
  });
375
625
  try {
376
- const targets = projectTargets(project.kind);
377
626
  if (targets.length === 1) {
378
627
  const target = targets[0];
379
628
  const compiled = await this.compileTarget(project, target, draft, operation);
380
- if (!compiled)
381
- return;
629
+ if (!compiled) {
630
+ return {
631
+ deployment: draft ? 'DRAFT' : 'LIVE',
632
+ status: 'COMPILE_FAILED',
633
+ projectRevisionId: project.revision.id,
634
+ targets,
635
+ message: this.state.runtime.message ?? 'Compilation failed',
636
+ };
637
+ }
382
638
  if (target === 'SERVER') {
383
639
  await this.enableServer(compiled.name, operation);
384
640
  }
@@ -390,11 +646,25 @@ export class CrowdyStudioController {
390
646
  // Compile the client first so a client failure never publishes a new
391
647
  // server version or alters the currently live pairing requirement.
392
648
  const client = await this.compileTarget(project, 'CLIENT', draft, operation);
393
- if (!client)
394
- return;
649
+ if (!client) {
650
+ return {
651
+ deployment: draft ? 'DRAFT' : 'LIVE',
652
+ status: 'COMPILE_FAILED',
653
+ projectRevisionId: project.revision.id,
654
+ targets,
655
+ message: this.state.runtime.message ?? 'Client compilation failed',
656
+ };
657
+ }
395
658
  const server = await this.compileTarget(project, 'SERVER', draft, operation);
396
- if (!server)
397
- return;
659
+ if (!server) {
660
+ return {
661
+ deployment: draft ? 'DRAFT' : 'LIVE',
662
+ status: 'COMPILE_FAILED',
663
+ projectRevisionId: project.revision.id,
664
+ targets,
665
+ message: this.state.runtime.message ?? 'Server compilation failed',
666
+ };
667
+ }
398
668
  this.checkOperation(operation);
399
669
  const requiredClientName = project.metadata.pairingPreference === 'REQUIRED'
400
670
  ? client.name
@@ -413,13 +683,41 @@ export class CrowdyStudioController {
413
683
  phase: 'RUNNING',
414
684
  message: draft ? 'Draft test is running' : 'Project is live',
415
685
  },
686
+ runtimeSync: {
687
+ state: 'RUNNING_SAVED',
688
+ savedRevisionId: project.revision.id,
689
+ runningRevisionId: project.revision.id,
690
+ deployment: draft ? 'DRAFT' : 'LIVE',
691
+ startedAt: new Date().toISOString(),
692
+ },
416
693
  });
417
694
  await this.refreshSurface('usage').catch(() => { });
695
+ return {
696
+ deployment: draft ? 'DRAFT' : 'LIVE',
697
+ status: 'RUNNING',
698
+ projectRevisionId: project.revision.id,
699
+ targets,
700
+ message: draft ? 'Draft test is running' : 'Project is live',
701
+ };
418
702
  }
419
703
  catch (error) {
420
- if (error instanceof OperationCancelledError)
421
- return;
704
+ if (error instanceof OperationCancelledError) {
705
+ return {
706
+ deployment: draft ? 'DRAFT' : 'LIVE',
707
+ status: 'FAILED',
708
+ projectRevisionId: project.revision.id,
709
+ targets,
710
+ message: 'Deployment was cancelled',
711
+ };
712
+ }
422
713
  this.setRuntime('ERROR', errorMessage(error));
714
+ return {
715
+ deployment: draft ? 'DRAFT' : 'LIVE',
716
+ status: 'FAILED',
717
+ projectRevisionId: project.revision.id,
718
+ targets,
719
+ message: errorMessage(error),
720
+ };
423
721
  }
424
722
  finally {
425
723
  if (operation === this.operationGeneration) {
@@ -591,10 +889,15 @@ export class CrowdyStudioController {
591
889
  phase: 'PARTIAL_FAILURE',
592
890
  message: failures.join(' · '),
593
891
  },
892
+ runtimeSync: {
893
+ ...this.state.runtimeSync,
894
+ state: 'STOPPED',
895
+ },
594
896
  });
595
897
  return result;
596
898
  }
597
- async invoke(exportName, paramsJson) {
899
+ async invoke(exportName, paramsJson, agentOperation) {
900
+ this.checkAgentOperation(agentOperation);
598
901
  const project = this.requireProject();
599
902
  if (!projectTargets(project.kind).includes('SERVER')) {
600
903
  throw new Error('Invoke requires a SERVER target');
@@ -605,6 +908,7 @@ export class CrowdyStudioController {
605
908
  exportName: exportName.trim() || 'invoke',
606
909
  paramsJson: paramsJson?.trim() || null,
607
910
  });
911
+ this.checkAgentOperation(agentOperation);
608
912
  this.update({ invokeResult: result });
609
913
  return result;
610
914
  }
@@ -671,6 +975,7 @@ export class CrowdyStudioController {
671
975
  this.broker?.stop();
672
976
  this.broker = null;
673
977
  this.listeners.clear();
978
+ this.humanEditListeners.clear();
674
979
  }
675
980
  async performSaveLoop() {
676
981
  while (this.persistedGeneration !== this.editGeneration) {
@@ -701,6 +1006,16 @@ export class CrowdyStudioController {
701
1006
  projects: upsertSummary(this.state.projects, summaryOf(saved)),
702
1007
  saveState: this.persistedGeneration === this.editGeneration ? 'SAVED' : 'SAVING',
703
1008
  saveMessage: undefined,
1009
+ runtimeSync: {
1010
+ ...this.state.runtimeSync,
1011
+ savedRevisionId: saved.revision.id,
1012
+ state: this.state.runtimeSync.state === 'RUNNING_SAVED' ||
1013
+ this.state.runtimeSync.state === 'RUNNING_STALE'
1014
+ ? this.state.runtimeSync.runningRevisionId === saved.revision.id
1015
+ ? 'RUNNING_SAVED'
1016
+ : 'RUNNING_STALE'
1017
+ : this.state.runtimeSync.state,
1018
+ },
704
1019
  });
705
1020
  }
706
1021
  catch (error) {
@@ -723,8 +1038,19 @@ export class CrowdyStudioController {
723
1038
  return true;
724
1039
  }
725
1040
  markEdited() {
1041
+ for (const listener of this.humanEditListeners)
1042
+ listener();
726
1043
  this.editGeneration++;
727
- this.update({ saveState: 'SAVING', saveMessage: undefined });
1044
+ this.update({
1045
+ saveState: 'SAVING',
1046
+ saveMessage: undefined,
1047
+ agentActivity: this.state.agentActivity === 'WORKING'
1048
+ ? 'PAUSED'
1049
+ : this.state.agentActivity,
1050
+ runtimeSync: this.state.runtimeSync.state === 'RUNNING_SAVED'
1051
+ ? { ...this.state.runtimeSync, state: 'RUNNING_STALE' }
1052
+ : this.state.runtimeSync,
1053
+ });
728
1054
  this.clearTimer('autosave');
729
1055
  this.autosaveTimer = setTimeout(() => {
730
1056
  this.autosaveTimer = null;
@@ -797,11 +1123,41 @@ export class CrowdyStudioController {
797
1123
  onHostCall: this.options.onHostCall,
798
1124
  };
799
1125
  }
1126
+ assertDeploymentPlan(project, plan, draft) {
1127
+ if (project.revision.id !== plan.expectedRevisionId) {
1128
+ throw new CrowdyStudioRevisionConflictError(`Expected revision ${plan.expectedRevisionId}, found ${project.revision.id}`, project);
1129
+ }
1130
+ const authoritativeTargets = [...projectTargets(project.kind)].sort();
1131
+ const requestedTargets = [...new Set(plan.targets)].sort();
1132
+ if (requestedTargets.length !== authoritativeTargets.length ||
1133
+ requestedTargets.some((target, index) => target !== authoritativeTargets[index])) {
1134
+ throw new Error(`Deployment targets must exactly match ${authoritativeTargets.join(', ')}`);
1135
+ }
1136
+ if (plan.pairingPreference !== undefined &&
1137
+ plan.pairingPreference !== project.metadata.pairingPreference) {
1138
+ throw new Error('Deployment pairing preference changed after approval');
1139
+ }
1140
+ if (plan.projectContentHash !== undefined &&
1141
+ plan.projectContentHash !== projectContentHash(project)) {
1142
+ throw new Error('Deployment project content changed after approval');
1143
+ }
1144
+ if (!draft &&
1145
+ (plan.pairingPreference === undefined ||
1146
+ plan.projectContentHash === undefined)) {
1147
+ throw new Error('Live deployment requires exact pairing and project content bindings');
1148
+ }
1149
+ }
800
1150
  checkOperation(generation) {
801
1151
  if (generation !== this.operationGeneration || this.destroyed) {
802
1152
  throw new OperationCancelledError();
803
1153
  }
804
1154
  }
1155
+ checkAgentOperation(generation) {
1156
+ if (generation !== undefined &&
1157
+ (generation !== this.agentOperationGeneration || this.destroyed)) {
1158
+ throw new OperationCancelledError();
1159
+ }
1160
+ }
805
1161
  sleep(ms) {
806
1162
  return (this.options.sleep ??
807
1163
  ((delay) => new Promise((resolve) => setTimeout(resolve, delay))))(ms);
@@ -910,6 +1266,83 @@ function normalizeUsage(value) {
910
1266
  gateReason: value.gateReason ?? null,
911
1267
  };
912
1268
  }
1269
+ function applyValidatedPatch(baseline, input) {
1270
+ if (input.changes.length < 1 || input.changes.length > 16) {
1271
+ throw new Error('Atomic patch must contain 1 to 16 file changes');
1272
+ }
1273
+ const next = cloneCrowdyStudioProject(baseline);
1274
+ const seen = new Set();
1275
+ for (const change of input.changes) {
1276
+ const path = normalizeCrowdyStudioPath(change.path);
1277
+ const key = crowdyStudioFileKey(change.target, path);
1278
+ if (seen.has(key))
1279
+ throw new Error(`Atomic patch repeats ${key}`);
1280
+ seen.add(key);
1281
+ if (!projectTargets(next.kind).includes(change.target)) {
1282
+ throw new Error(`${next.kind} projects do not have a ${change.target} target`);
1283
+ }
1284
+ const bytes = new TextEncoder().encode(change.content).byteLength;
1285
+ if (bytes > 65536)
1286
+ throw new Error(`${key} exceeds the 65536-byte file limit`);
1287
+ const index = next.files.findIndex((file) => file.target === change.target && file.path === path);
1288
+ if (change.operation === 'CREATE') {
1289
+ if (change.expectedContentHash !== 'ABSENT' || index >= 0) {
1290
+ throw new CrowdyStudioRevisionConflictError(`${key} was expected to be absent`, baseline);
1291
+ }
1292
+ next.files.push({ target: change.target, path, content: change.content });
1293
+ continue;
1294
+ }
1295
+ if (index < 0) {
1296
+ throw new CrowdyStudioRevisionConflictError(`${key} no longer exists`, baseline);
1297
+ }
1298
+ const currentHash = sha256Digest(next.files[index].content);
1299
+ if (change.expectedContentHash !== currentHash) {
1300
+ throw new CrowdyStudioRevisionConflictError(`${key} content hash changed`, baseline);
1301
+ }
1302
+ next.files[index] = {
1303
+ target: change.target,
1304
+ path,
1305
+ content: change.content,
1306
+ };
1307
+ }
1308
+ if (next.files.length > 128) {
1309
+ throw new Error('Project exceeds the 128-file limit');
1310
+ }
1311
+ next.files.sort(compareProjectFile);
1312
+ return next;
1313
+ }
1314
+ function projectContentHash(project) {
1315
+ return digestCanonicalJson({
1316
+ contract: 'crowdy.studio-project-content/1',
1317
+ projectId: project.projectId,
1318
+ metadata: project.metadata,
1319
+ files: project.files
1320
+ .map((file) => ({
1321
+ target: file.target,
1322
+ path: file.path,
1323
+ contentHash: sha256Digest(file.content),
1324
+ }))
1325
+ .sort((left, right) => crowdyStudioFileKey(left.target, left.path).localeCompare(crowdyStudioFileKey(right.target, right.path))),
1326
+ });
1327
+ }
1328
+ function upsertCheckpoint(checkpoints, checkpoint) {
1329
+ return [
1330
+ checkpoint,
1331
+ ...checkpoints.filter((entry) => entry.checkpointId !== checkpoint.checkpointId),
1332
+ ];
1333
+ }
1334
+ function fileRefExists(project, state, ref) {
1335
+ if (ref.source === 'PROJECT') {
1336
+ return project.files.some((file) => file.target === ref.target &&
1337
+ file.path === normalizeCrowdyStudioPath(ref.path));
1338
+ }
1339
+ const references = ref.source === 'PERSONAL_LIBRARY'
1340
+ ? state.personalLibraryFiles
1341
+ : state.commonFiles;
1342
+ return references.some((file) => ref.referenceId
1343
+ ? file.id === ref.referenceId
1344
+ : file.target === ref.target && file.path === ref.path);
1345
+ }
913
1346
  function errorMessage(error) {
914
1347
  return error instanceof Error ? error.message : String(error);
915
1348
  }
@@ -1,4 +1,6 @@
1
1
  import { type CrowdyStudioController, type CrowdyStudioState } from './controller.js';
2
+ import type { CrowdyStudioAgentController } from '../crowdy-agent/controller.js';
3
+ import { type CrowdyStudioAgentDomShellOptions } from './agent-dom-shell.js';
2
4
  /** Modular DOM shell; editor implementations mount into `editorHost`. */
3
5
  export declare class CrowdyStudioDomShell {
4
6
  private readonly controller;
@@ -32,9 +34,10 @@ export declare class CrowdyStudioDomShell {
32
34
  private readonly invokeResult;
33
35
  private readonly panelButtons;
34
36
  private readonly panels;
37
+ private readonly agentShell;
35
38
  private activePanel;
36
39
  private disposed;
37
- constructor(host: HTMLElement, controller: CrowdyStudioController);
40
+ constructor(host: HTMLElement, controller: CrowdyStudioController, agentController?: CrowdyStudioAgentController, agentOptions?: CrowdyStudioAgentDomShellOptions);
38
41
  render(state: CrowdyStudioState): void;
39
42
  dispose(): void;
40
43
  private createPanel;
@@ -1 +1 @@
1
- {"version":3,"file":"dom-shell.d.ts","sourceRoot":"","sources":["../../src/crowdy-studio/dom-shell.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,sBAAsB,EAE3B,KAAK,iBAAiB,EACvB,MAAM,iBAAiB,CAAC;AAYzB,yEAAyE;AACzE,qBAAa,oBAAoB;IAoC7B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAnC7B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoB;IAClD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoB;IAC/C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAoB;IACjD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoB;IAC/C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAoB;IACjD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAc;IAC5C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAc;IAC3C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAC5C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;IACvC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAc;IACnC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAmB;IAChD,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAmB;IACvD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmB;IACpD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmB;IACpD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAC5C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAc;IAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAc;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAmB;IAChD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAsB;IACnD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAc;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA2C;IACxE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqC;IAC5D,OAAO,CAAC,WAAW,CAAyB;IAC5C,OAAO,CAAC,QAAQ,CAAS;gBAGvB,IAAI,EAAE,WAAW,EACA,UAAU,EAAE,sBAAsB;IAoMrD,MAAM,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI;IA+BtC,OAAO,IAAI,IAAI;IASf,OAAO,CAAC,WAAW;IAgBnB,OAAO,CAAC,aAAa;IAiBrB,OAAO,CAAC,cAAc;IAoBtB,OAAO,CAAC,eAAe;IAiBvB,OAAO,CAAC,cAAc;IAyBtB,OAAO,CAAC,cAAc;IAyBtB,OAAO,CAAC,cAAc;IAkDtB,OAAO,CAAC,gBAAgB;IAuCxB,OAAO,CAAC,UAAU;IAwBlB,OAAO,CAAC,cAAc;IAgCtB,OAAO,CAAC,cAAc;IAgCtB,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,iBAAiB;YAgBX,GAAG;CAQlB"}
1
+ {"version":3,"file":"dom-shell.d.ts","sourceRoot":"","sources":["../../src/crowdy-studio/dom-shell.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,sBAAsB,EAE3B,KAAK,iBAAiB,EACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AACjF,OAAO,EAEL,KAAK,gCAAgC,EACtC,MAAM,sBAAsB,CAAC;AAY9B,yEAAyE;AACzE,qBAAa,oBAAoB;IAqC7B,OAAO,CAAC,QAAQ,CAAC,UAAU;IApC7B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoB;IAClD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoB;IAC/C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAoB;IACjD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoB;IAC/C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAoB;IACjD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAc;IAC5C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAc;IAC3C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAC5C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;IACvC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAc;IACnC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAmB;IAChD,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAmB;IACvD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmB;IACpD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmB;IACpD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAC5C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAc;IAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAc;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAmB;IAChD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAsB;IACnD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAc;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA2C;IACxE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqC;IAC5D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAmC;IAC9D,OAAO,CAAC,WAAW,CAAyB;IAC5C,OAAO,CAAC,QAAQ,CAAS;gBAGvB,IAAI,EAAE,WAAW,EACA,UAAU,EAAE,sBAAsB,EACnD,eAAe,CAAC,EAAE,2BAA2B,EAC7C,YAAY,GAAE,gCAAqC;IA2MrD,MAAM,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI;IA+BtC,OAAO,IAAI,IAAI;IAUf,OAAO,CAAC,WAAW;IAgBnB,OAAO,CAAC,aAAa;IAiBrB,OAAO,CAAC,cAAc;IAoBtB,OAAO,CAAC,eAAe;IAiBvB,OAAO,CAAC,cAAc;IAyBtB,OAAO,CAAC,cAAc;IAyBtB,OAAO,CAAC,cAAc;IAkDtB,OAAO,CAAC,gBAAgB;IAuCxB,OAAO,CAAC,UAAU;IAwBlB,OAAO,CAAC,cAAc;IAgCtB,OAAO,CAAC,cAAc;IAgCtB,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,iBAAiB;YAgBX,GAAG;CAQlB"}
@@ -1,8 +1,9 @@
1
+ import { CrowdyStudioAgentDomShell, } from './agent-dom-shell.js';
1
2
  import { projectTargets, } from './models.js';
2
3
  import { CROWDY_STUDIO_STYLES } from './styles.js';
3
4
  /** Modular DOM shell; editor implementations mount into `editorHost`. */
4
5
  export class CrowdyStudioDomShell {
5
- constructor(host, controller) {
6
+ constructor(host, controller, agentController, agentOptions = {}) {
6
7
  this.controller = controller;
7
8
  this.panelButtons = new Map();
8
9
  this.panels = new Map();
@@ -99,6 +100,9 @@ export class CrowdyStudioDomShell {
99
100
  this.invokePanel.append(invokeControls, this.invokeResult);
100
101
  this.root.append(toolbar, this.newForm, main, bottom);
101
102
  host.appendChild(this.root);
103
+ this.agentShell = agentController
104
+ ? new CrowdyStudioAgentDomShell(this.root, agentController, agentOptions)
105
+ : null;
102
106
  newButton.addEventListener('click', () => {
103
107
  this.newForm.dataset.open =
104
108
  this.newForm.dataset.open === 'true' ? 'false' : 'true';
@@ -184,6 +188,7 @@ export class CrowdyStudioDomShell {
184
188
  this.controller.setSurfaceVisible('usage', false);
185
189
  this.controller.setSurfaceVisible('logs', false);
186
190
  this.controller.setSurfaceVisible('runs', false);
191
+ this.agentShell?.dispose();
187
192
  this.root.remove();
188
193
  }
189
194
  createPanel(name, label, tabs) {