@open-mercato/cezar 0.9.2 → 0.9.3

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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -2
  3. package/dist/contract/health.d.ts +2 -0
  4. package/dist/contract/index.js +23 -10
  5. package/dist/git-diff-base.d.ts +51 -26
  6. package/dist/git-diff-base.js +163 -37
  7. package/dist/git-diff-base.js.map +1 -1
  8. package/dist/git-worktree.d.ts +15 -6
  9. package/dist/git-worktree.js +10 -3
  10. package/dist/git-worktree.js.map +1 -1
  11. package/dist/server/capabilities.d.ts +15 -1
  12. package/dist/server/capabilities.js +16 -1
  13. package/dist/server/capabilities.js.map +1 -1
  14. package/dist/server/git-changes.d.ts +7 -5
  15. package/dist/server/git-changes.js +15 -10
  16. package/dist/server/git-changes.js.map +1 -1
  17. package/dist/server/server.d.ts +59 -58
  18. package/dist/server/server.js +54 -3
  19. package/dist/server/server.js.map +1 -1
  20. package/dist/workflows/run.d.ts +27 -7
  21. package/dist/workflows/run.js +106 -43
  22. package/dist/workflows/run.js.map +1 -1
  23. package/dist/workspace/config.d.ts +8 -0
  24. package/dist/workspace/config.js +29 -2
  25. package/dist/workspace/config.js.map +1 -1
  26. package/dist/workspace/semaphore.d.ts +7 -1
  27. package/dist/workspace/semaphore.js +10 -4
  28. package/dist/workspace/semaphore.js.map +1 -1
  29. package/dist/workspace/ui-state.d.ts +3 -2
  30. package/dist/workspace/ui-state.js +3 -2
  31. package/dist/workspace/ui-state.js.map +1 -1
  32. package/package.json +3 -3
  33. package/web/dist/assets/{alert-dialog-ghffK6g9.js → alert-dialog-V9cigyAO.js} +1 -1
  34. package/web/dist/assets/arrow-down-DflHdNlN.js +1 -0
  35. package/web/dist/assets/arrow-left-IIL4uR-C.js +1 -0
  36. package/web/dist/assets/centered-state-BkAJVLJE.js +43 -0
  37. package/web/dist/assets/chevron-right-CxAoD1eD.js +1 -0
  38. package/web/dist/assets/circle-check-CdwcIbkx.js +1 -0
  39. package/web/dist/assets/{circle-x-C3EFZ3xf.js → circle-x-Cnc43xJ2.js} +1 -1
  40. package/web/dist/assets/{collapsible-B0JVzd0B.js → collapsible-BlF-7CeC.js} +1 -1
  41. package/web/dist/assets/{commit-list-wm6DPHNs.js → commit-list-D2mKElUx.js} +1 -1
  42. package/web/dist/assets/{compare-variants-DbdP4Y5T.js → compare-variants-CFBY16Rc.js} +1 -1
  43. package/web/dist/assets/{diff-Bizc96qe.js → diff-DE6u5dTe.js} +2 -2
  44. package/web/dist/assets/{diff-stat-Dyfg3ltF.js → diff-stat-B1j6L7C6.js} +1 -1
  45. package/web/dist/assets/{diff-view-CG8ek6tC.js → diff-view-Gb2kk3Le.js} +1 -1
  46. package/web/dist/assets/{dropdown-menu-C1Dlf35P.js → dropdown-menu-Ns-rY6sm.js} +1 -1
  47. package/web/dist/assets/editable-title-BGuiDxFT.js +1 -0
  48. package/web/dist/assets/{ellipsis-vertical-DpGTRqC2.js → ellipsis-vertical-BbxtXb6s.js} +1 -1
  49. package/web/dist/assets/{file-DlUGpUlb.js → file-j5O4NryU.js} +1 -1
  50. package/web/dist/assets/{folder-BAY6Vddy.js → folder-w9Lwj9x9.js} +1 -1
  51. package/web/dist/assets/{git-pull-request-agOG0iGM.js → git-pull-request-DlSop3Ch.js} +1 -1
  52. package/web/dist/assets/{git-toolbar-DGiNMmtY.js → git-toolbar-BdJ8XSxI.js} +1 -1
  53. package/web/dist/assets/github-CCLn2OAU.js +1 -0
  54. package/web/dist/assets/{image-preview-BWGk5fPX.js → image-preview-CcFdkZXW.js} +1 -1
  55. package/web/dist/assets/index-BmHHLbwc.js +6 -0
  56. package/web/dist/assets/index-u8OAnq0r.css +2 -0
  57. package/web/dist/assets/{markdown-BGBOGCA_.js → markdown-q9lm7kQ2.js} +1 -1
  58. package/web/dist/assets/{new-task-form-BpAw_Dks.js → new-task-form-BuYxAWN7.js} +1 -1
  59. package/web/dist/assets/{pill-Dvs-P0xy.js → pill-t97MvPW_.js} +1 -1
  60. package/web/dist/assets/{project-router-Cx8NPoqn.js → project-router-DudOmkHf.js} +1 -1
  61. package/web/dist/assets/{prompt-templates-CSBYBR1I.js → prompt-templates-C5iO3Lda.js} +1 -1
  62. package/web/dist/assets/{refresh-cw-BoGdAykg.js → refresh-cw-_C2EGJK5.js} +1 -1
  63. package/web/dist/assets/{repo-git-PmX-BNMo.js → repo-git-DqQzj33d.js} +1 -1
  64. package/web/dist/assets/{run-diff-EcOkf3ti.js → run-diff-BLoKEnTx.js} +1 -1
  65. package/web/dist/assets/run-header-Dho4eurS.js +1 -0
  66. package/web/dist/assets/{search-x-X3EpVhgs.js → search-x-BgxaJgro.js} +1 -1
  67. package/web/dist/assets/{skill-empty-hint-CZLALx6l.js → skill-empty-hint-D5MOmycv.js} +1 -1
  68. package/web/dist/assets/skills-B5g064CC.js +1 -0
  69. package/web/dist/assets/{sparkles-Dt4q7pbG.js → sparkles-ANSka5bH.js} +1 -1
  70. package/web/dist/assets/{square-terminal-hwBL8XsC.js → square-terminal-qojq20cH.js} +1 -1
  71. package/web/dist/assets/{tab-link-DIlR5T07.js → tab-link-CIcwFJtC.js} +1 -1
  72. package/web/dist/assets/task-changes-yRfsEsnh.js +1 -0
  73. package/web/dist/assets/{task-commits-BTTA5k5T.js → task-commits-yXmDrCoE.js} +1 -1
  74. package/web/dist/assets/{task-files-Da8_4C9g.js → task-files-BDRmCCPN.js} +2 -2
  75. package/web/dist/assets/task-thread-C9AnIz4Z.js +9 -0
  76. package/web/dist/assets/{textarea-CUdokhkv.js → textarea-DVO7pg8Y.js} +1 -1
  77. package/web/dist/assets/{thread-loading-B8q8ukXJ.js → thread-loading-DJDl701Z.js} +1 -1
  78. package/web/dist/assets/{trash-2-DhVIdWv8.js → trash-2-DKcnmg6e.js} +1 -1
  79. package/web/dist/assets/{triangle-alert-XYT0DGCI.js → triangle-alert-CPUEVSZ3.js} +1 -1
  80. package/web/dist/assets/{upload-BKjN59-1.js → upload-D5Jrq7QK.js} +1 -1
  81. package/web/dist/assets/{use-desktop-Bf5LS_P4.js → use-desktop-CXae6uyk.js} +1 -1
  82. package/web/dist/assets/{use-submit-shortcut-CzVyoAMd.js → use-submit-shortcut-RKJLKdsW.js} +1 -1
  83. package/web/dist/assets/{utils-DsLuRXwV.js → utils-Wtkd5E6j.js} +1 -1
  84. package/web/dist/assets/{workflows-o2QU_pvU.js → workflows-CGyTdyds.js} +3 -3
  85. package/web/dist/assets/{zoomable-image-7WyUiaVD.js → zoomable-image-hcqwUIlQ.js} +1 -1
  86. package/web/dist/index.html +22 -22
  87. package/web/dist/assets/arrow-down-Cz8qb71e.js +0 -1
  88. package/web/dist/assets/arrow-left-wcYKQXI_.js +0 -1
  89. package/web/dist/assets/centered-state-ZtnFHA-n.js +0 -43
  90. package/web/dist/assets/chevron-right-G4caF2QV.js +0 -1
  91. package/web/dist/assets/circle-check--KvBxXWp.js +0 -1
  92. package/web/dist/assets/editable-title-5rC_QRBZ.js +0 -1
  93. package/web/dist/assets/github-Vp7lxYGQ.js +0 -1
  94. package/web/dist/assets/index-DSxyJpuK.css +0 -2
  95. package/web/dist/assets/index-DiawI6RX.js +0 -6
  96. package/web/dist/assets/run-header-BGiv6_qX.js +0 -1
  97. package/web/dist/assets/skills-CC91ouGH.js +0 -1
  98. package/web/dist/assets/task-changes-XkHDxzQA.js +0 -1
  99. package/web/dist/assets/task-thread-DoNXUsHr.js +0 -9
@@ -20,6 +20,14 @@ export declare const AUTOSAVE_INTERVAL_MS = 90000;
20
20
  * noise interleaving PR history. The flushes (`autosaveCommit` at turn end and before
21
21
  * a draft PR) are NOT gated: the branch must still end holding the finished state. */
22
22
  export declare function periodicAutosaveEnabled(env?: NodeJS.ProcessEnv): boolean;
23
+ /**
24
+ * Explicitly opt out of the repository-root lease for runs that execute in the
25
+ * current checkout. This covers explicit worktree opt-out, non-Git degradation,
26
+ * and continuations whose worktree cannot be restored (spec 006 hardening, #438).
27
+ * This is intentionally unsafe: concurrent agents may overwrite each other's
28
+ * files or Git state. Isolated worktree runs are unaffected.
29
+ */
30
+ export declare function repositoryRootLockDisabled(env?: NodeJS.ProcessEnv): boolean;
23
31
  export interface StartRunInput {
24
32
  task: string;
25
33
  model?: string;
@@ -142,8 +150,9 @@ export declare class RunManager {
142
150
  private pumpAgain;
143
151
  /**
144
152
  * Runs normally isolate in worktrees and may execute in parallel. When that
145
- * isolation is unavailable (or explicitly disabled), serialize access to
146
- * `repoRoot` so two agents can never edit/revert the same files (#438).
153
+ * isolation is unavailable (or explicitly disabled), access to `repoRoot` is
154
+ * serialized by default so two agents cannot edit/revert the same files
155
+ * (#438). `CEZ_DISABLE_REPO_LOCK=1` deliberately bypasses this safety lease.
147
156
  */
148
157
  private repoRootTail;
149
158
  /** `.ai/cezar` — where the per-task handoff files and todos.json live. */
@@ -524,12 +533,23 @@ export declare function makeRunTitle(task: string, workflow: WorkflowDef): strin
524
533
  */
525
534
  export declare function skillSystemPrompt(skill: Pick<Skill, 'name' | 'description' | 'body'> & Partial<Pick<Skill, 'path' | 'source'>>): string;
526
535
  /**
527
- * Expand a registry-backed slash skill in a live chat message before it reaches
528
- * a backend. Claude otherwise intercepts an unknown leading slash command, and
536
+ * Expand a registry-backed slash skill in one prompt string before it reaches a
537
+ * backend. Claude otherwise intercepts an unknown leading slash command, and
529
538
  * Codex/OpenCode have no native slash-skill lookup at all (#676).
530
539
  *
531
- * Only the first text block is eligible, only at character zero, and unknown
532
- * commands pass through byte-for-byte. The caller persists the original user
533
- * content before applying this delivery-only rewrite.
540
+ * Only a match at character zero counts, and unknown commands pass through
541
+ * byte-for-byte a backend's OWN slash commands must keep working. The caller
542
+ * persists the original user text before applying this delivery-only rewrite.
543
+ *
544
+ * Both delivery seams route through here: live-session messages via
545
+ * `expandRegistrySlashSkill`, and a continuation's opening prompt, which becomes
546
+ * the session's `userPrompt` and never passes through `deliverMessage` at all
547
+ * (#811).
548
+ */
549
+ export declare function expandRegistrySlashSkillText(text: string, skills: readonly Skill[]): string;
550
+ /**
551
+ * `expandRegistrySlashSkillText` over a live chat message: only the first text
552
+ * block is eligible, and an unchanged block returns the caller's array
553
+ * identity untouched.
534
554
  */
535
555
  export declare function expandRegistrySlashSkill(content: ContentBlock[], skills: readonly Skill[]): ContentBlock[];
@@ -98,6 +98,17 @@ export const AUTOSAVE_INTERVAL_MS = 90_000;
98
98
  export function periodicAutosaveEnabled(env = process.env) {
99
99
  return env.CEZ_AUTOSAVE === '1';
100
100
  }
101
+ /**
102
+ * Explicitly opt out of the repository-root lease for runs that execute in the
103
+ * current checkout. This covers explicit worktree opt-out, non-Git degradation,
104
+ * and continuations whose worktree cannot be restored (spec 006 hardening, #438).
105
+ * This is intentionally unsafe: concurrent agents may overwrite each other's
106
+ * files or Git state. Isolated worktree runs are unaffected.
107
+ */
108
+ export function repositoryRootLockDisabled(env = process.env) {
109
+ return env.CEZ_DISABLE_REPO_LOCK === '1';
110
+ }
111
+ const REPOSITORY_ROOT_LOCK_DISABLED_NOTE = 'repository-root lock disabled by CEZ_DISABLE_REPO_LOCK=1 (shared checkout is unsafe)';
101
112
  /** Safety cap on autonomous auto-continues per run — stops a stuck agent from nudging forever. */
102
113
  const MAX_AUTO_CONTINUES = 40;
103
114
  const AUTONOMOUS_NUDGE = 'Continue working autonomously until the task is fully complete. Do not ask me for confirmation or clarification — make reasonable assumptions and proceed. When everything is done, end the session with your done signal.';
@@ -234,8 +245,9 @@ export class RunManager {
234
245
  pumpAgain = false;
235
246
  /**
236
247
  * Runs normally isolate in worktrees and may execute in parallel. When that
237
- * isolation is unavailable (or explicitly disabled), serialize access to
238
- * `repoRoot` so two agents can never edit/revert the same files (#438).
248
+ * isolation is unavailable (or explicitly disabled), access to `repoRoot` is
249
+ * serialized by default so two agents cannot edit/revert the same files
250
+ * (#438). `CEZ_DISABLE_REPO_LOCK=1` deliberately bypasses this safety lease.
239
251
  */
240
252
  repoRootTail = Promise.resolve();
241
253
  /** `.ai/cezar` — where the per-task handoff files and todos.json live. */
@@ -1300,19 +1312,27 @@ export class RunManager {
1300
1312
  this.active.set(runId, state);
1301
1313
  this.starting.delete(runId);
1302
1314
  if (state.cwd === this.repoRoot) {
1303
- this.store.appendEvent(runId, {
1304
- type: 'note',
1305
- message: 'waiting for exclusive access to the repository working tree',
1306
- });
1307
- if (!(await this.acquireRepoRoot(runId, state))) {
1308
- this.store.updateRun(runId, {
1309
- status: 'cancelled',
1310
- finishedAt: new Date().toISOString(),
1311
- currentStepId: undefined,
1315
+ if (repositoryRootLockDisabled()) {
1316
+ this.store.appendEvent(runId, {
1317
+ type: 'note',
1318
+ message: REPOSITORY_ROOT_LOCK_DISABLED_NOTE,
1312
1319
  });
1313
- this.store.appendEvent(runId, { type: 'lifecycle', message: 'run cancelled' });
1314
- this.dropActive(runId);
1315
- return;
1320
+ }
1321
+ else {
1322
+ this.store.appendEvent(runId, {
1323
+ type: 'note',
1324
+ message: 'waiting for exclusive access to the repository working tree',
1325
+ });
1326
+ if (!(await this.acquireRepoRoot(runId, state))) {
1327
+ this.store.updateRun(runId, {
1328
+ status: 'cancelled',
1329
+ finishedAt: new Date().toISOString(),
1330
+ currentStepId: undefined,
1331
+ });
1332
+ this.store.appendEvent(runId, { type: 'lifecycle', message: 'run cancelled' });
1333
+ this.dropActive(runId);
1334
+ return;
1335
+ }
1316
1336
  }
1317
1337
  }
1318
1338
  this.armAutosave(state);
@@ -1333,6 +1353,17 @@ export class RunManager {
1333
1353
  backend,
1334
1354
  });
1335
1355
  this.store.appendEvent(runId, { type: 'step-start', stepId, name: 'Continue', kind: 'agent', iteration: 1 });
1356
+ // Registry snapshot for `/skill` expansion. `execute` loads this for the workflow's own
1357
+ // sessions; a continuation builds its OWN ActiveRun, and without this the resumed session
1358
+ // expanded against an empty registry and leaked `/om-...` verbatim to the backend, which
1359
+ // answered "Unknown skill" (#811). Best-effort — discovery must never break Continue.
1360
+ //
1361
+ // Loaded AFTER the run is marked `running`, not before: `continueRun` returns the moment it
1362
+ // schedules this, so every await ahead of that write is a window in which the run still
1363
+ // reads with its previous TERMINAL status. A directory scan there is long enough to be
1364
+ // observed. Nothing between here and `startSession` reads `state.skills`, and no message can
1365
+ // be delivered into a session that does not exist yet, so the invariant is unaffected.
1366
+ state.skills = await discoverSkills(this.repoRoot).catch(() => []);
1336
1367
  // Attachments pasted into the follow-up composer, on the same terms as a live-session
1337
1368
  // message (#357): persisted to the run's own image store so the thread renders the bubble's
1338
1369
  // images rather than a bare count, and handed to the agent BOTH as base64 blocks (so it can
@@ -1493,12 +1524,19 @@ export class RunManager {
1493
1524
  const runner = createRunner(continueBackend);
1494
1525
  state.currentStepId = stepId;
1495
1526
  this.beginUsageInvocation(runId, state, stepId);
1527
+ // A continuation's opening message becomes the session's `userPrompt` and never passes
1528
+ // through `deliverMessage`, so it needs the SAME delivery-only `/skill` rewrite the
1529
+ // live path applies (#811). Delivery-only: the `user-message` event above already
1530
+ // persisted the user's original text, and the transcript must keep showing that.
1531
+ const openingPrompt = expandRegistrySlashSkillText(prompt, state.skills ?? []);
1496
1532
  const session = runner.startSession({
1497
1533
  // The Continue step is a fresh agent session on the same run — the
1498
1534
  // run's extra system prompt (already resolved at execute time and
1499
1535
  // echoed on the record) rides along with the handoff contract.
1500
1536
  systemPrompt: composeSystemPrompt(record?.systemPrompt, generateFollowups ? HANDOFF_INSTRUCTIONS : HANDOFF_ONLY_INSTRUCTIONS),
1501
- userPrompt: attachments.length ? `${prompt}\n\n${pastedAttachmentsText(attachments)}` : prompt,
1537
+ userPrompt: attachments.length
1538
+ ? `${openingPrompt}\n\n${pastedAttachmentsText(attachments)}`
1539
+ : openingPrompt,
1502
1540
  ...(openingImages.length ? { images: openingImages } : {}),
1503
1541
  cwd: state.cwd,
1504
1542
  allowedTools: DEFAULT_ALLOWED_TOOLS,
@@ -1601,7 +1639,8 @@ export class RunManager {
1601
1639
  const repo = await getRepoInfo(this.repoRoot);
1602
1640
  if (repo && input.worktree === false) {
1603
1641
  // Composer opt-out: run in the repo working tree, no branch/worktree. The
1604
- // repository-root lease serializes these runs so workflows cannot overlap.
1642
+ // repository-root lease serializes these runs by default; the explicit
1643
+ // CEZ_DISABLE_REPO_LOCK=1 escape hatch allows unsafe overlap.
1605
1644
  // Pin the starting commit: the session's Changes and Commits views use it
1606
1645
  // as their stable lower bound while reading the current working copy.
1607
1646
  const startingCommit = await getHeadCommit(repo.root);
@@ -1673,14 +1712,22 @@ export class RunManager {
1673
1712
  emit({ type: 'note', message: 'not a git repository — running in place, one task at a time' });
1674
1713
  }
1675
1714
  if (state.cwd === this.repoRoot) {
1676
- emit({
1677
- type: 'note',
1678
- message: 'waiting for exclusive access to the repository working tree',
1679
- });
1680
- // A cancel during the wait leaves the lease ungranted; the step loop
1681
- // below breaks on `cancelled` before touching the tree and settles the
1682
- // run through the usual path.
1683
- await this.acquireRepoRoot(runId, state);
1715
+ if (repositoryRootLockDisabled()) {
1716
+ emit({
1717
+ type: 'note',
1718
+ message: REPOSITORY_ROOT_LOCK_DISABLED_NOTE,
1719
+ });
1720
+ }
1721
+ else {
1722
+ emit({
1723
+ type: 'note',
1724
+ message: 'waiting for exclusive access to the repository working tree',
1725
+ });
1726
+ // A cancel during the wait leaves the lease ungranted; the step loop
1727
+ // below breaks on `cancelled` before touching the tree and settles the
1728
+ // run through the usual path.
1729
+ await this.acquireRepoRoot(runId, state);
1730
+ }
1684
1731
  }
1685
1732
  // Handoff journal (spec 007) — seeded after the worktree exists so the
1686
1733
  // header can name the branch. Idempotent: an existing file stays as-is.
@@ -1688,6 +1735,8 @@ export class RunManager {
1688
1735
  if (seeded)
1689
1736
  seedHandoffFile(this.dataDir, seeded);
1690
1737
  const skills = await discoverSkills(this.repoRoot);
1738
+ // Every ActiveRun construction site must carry the registry — `runContinuation` builds
1739
+ // its own, and the one that skipped this leaked raw `/skill` text to the backend (#811).
1691
1740
  state.skills = skills;
1692
1741
  const retriesUsed = new Map();
1693
1742
  let checkFailure = null;
@@ -2210,11 +2259,13 @@ export class RunManager {
2210
2259
  // deliberately NEVER a title source; see maybeRefreshTitle below. The
2211
2260
  // one exception is an explicit CEZ:TITLE declaration (applied above).
2212
2261
  if (run.worktreePath && existsSync(run.worktreePath)) {
2213
- // `taskBranch` is what keeps this number *this task's* (#751): a review/QA run
2214
- // repoints the worktree onto the branch under review, and without the branch to
2215
- // compare HEAD against, the stat would claim that whole branch's diff.
2262
+ // `taskBranch` + `runStartedAt` are what keep this number *this task's* (#751): a
2263
+ // review/QA run repoints the worktree onto the branch under review, and without the
2264
+ // branch to compare HEAD against and the moment it was checked out, the stat would
2265
+ // claim that whole branch's diff.
2216
2266
  const stat = await worktreeShortstat(run.worktreePath, run.baseBranch ?? 'HEAD', {
2217
2267
  taskBranch: run.branch,
2268
+ runStartedAt: run.startedAt,
2218
2269
  });
2219
2270
  if (stat)
2220
2271
  this.store.updateRun(runId, { diffStat: stat });
@@ -2586,13 +2637,33 @@ export function skillSystemPrompt(skill) {
2586
2637
  return lines.join('\n');
2587
2638
  }
2588
2639
  /**
2589
- * Expand a registry-backed slash skill in a live chat message before it reaches
2590
- * a backend. Claude otherwise intercepts an unknown leading slash command, and
2640
+ * Expand a registry-backed slash skill in one prompt string before it reaches a
2641
+ * backend. Claude otherwise intercepts an unknown leading slash command, and
2591
2642
  * Codex/OpenCode have no native slash-skill lookup at all (#676).
2592
2643
  *
2593
- * Only the first text block is eligible, only at character zero, and unknown
2594
- * commands pass through byte-for-byte. The caller persists the original user
2595
- * content before applying this delivery-only rewrite.
2644
+ * Only a match at character zero counts, and unknown commands pass through
2645
+ * byte-for-byte a backend's OWN slash commands must keep working. The caller
2646
+ * persists the original user text before applying this delivery-only rewrite.
2647
+ *
2648
+ * Both delivery seams route through here: live-session messages via
2649
+ * `expandRegistrySlashSkill`, and a continuation's opening prompt, which becomes
2650
+ * the session's `userPrompt` and never passes through `deliverMessage` at all
2651
+ * (#811).
2652
+ */
2653
+ export function expandRegistrySlashSkillText(text, skills) {
2654
+ const match = /^\/([A-Za-z0-9][A-Za-z0-9._-]*)(?=\s|$)/.exec(text);
2655
+ if (!match)
2656
+ return text;
2657
+ const skill = skills.find((candidate) => candidate.name === match[1]);
2658
+ if (!skill)
2659
+ return text;
2660
+ const request = text.slice(match[0].length).trim();
2661
+ return request ? `${skillSystemPrompt(skill)}\n\nUser request:\n${request}` : skillSystemPrompt(skill);
2662
+ }
2663
+ /**
2664
+ * `expandRegistrySlashSkillText` over a live chat message: only the first text
2665
+ * block is eligible, and an unchanged block returns the caller's array
2666
+ * identity untouched.
2596
2667
  */
2597
2668
  export function expandRegistrySlashSkill(content, skills) {
2598
2669
  const textIndex = content.findIndex((block) => block.type === 'text');
@@ -2601,19 +2672,11 @@ export function expandRegistrySlashSkill(content, skills) {
2601
2672
  const block = content[textIndex];
2602
2673
  if (!block || block.type !== 'text')
2603
2674
  return content;
2604
- const match = /^\/([A-Za-z0-9][A-Za-z0-9._-]*)(?=\s|$)/.exec(block.text);
2605
- if (!match)
2606
- return content;
2607
- const skill = skills.find((candidate) => candidate.name === match[1]);
2608
- if (!skill)
2675
+ const text = expandRegistrySlashSkillText(block.text, skills);
2676
+ if (text === block.text)
2609
2677
  return content;
2610
- const request = block.text.slice(match[0].length).trim();
2611
- const replacement = {
2612
- type: 'text',
2613
- text: request ? `${skillSystemPrompt(skill)}\n\nUser request:\n${request}` : skillSystemPrompt(skill),
2614
- };
2615
2678
  const expanded = [...content];
2616
- expanded[textIndex] = replacement;
2679
+ expanded[textIndex] = { type: 'text', text };
2617
2680
  return expanded;
2618
2681
  }
2619
2682
  //# sourceMappingURL=run.js.map