chatroom-cli 1.88.5 → 1.90.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.
package/dist/index.js CHANGED
@@ -77720,13 +77720,15 @@ function renderWebappUxHandoffReference() {
77720
77720
  'Complete the optional **UX** section in your output when the planner proposes UI changes. Write exactly "Not Applicable." for non-UI tasks. Put code snippets in **Suggested edits** only.',
77721
77721
  "",
77722
77722
  "1. **Flows** — primary action ≤3 clicks? simpler path exists?",
77723
- "2. **Patterns** — matches existing components? recommend one if multiple. mobile vs desktop (md: variants vs separate mobile UI)?",
77723
+ "2. **Patterns** — matches existing components? recommend one if multiple. mobile vs desktop (responsive variants vs separate mobile UI)?",
77724
77724
  "3. **Layout** — compact title+menu row, description, trailing end-aligned CTA? unnecessary wrappers?",
77725
- "4. **Shortcuts** — consistent with catalog below? gaps or conflicts?",
77725
+ "4. **Shortcuts** — consistent with project conventions? gaps or conflicts?",
77726
77726
  "5. **States** — loading spinners/skeletons for async data? error messages on failure? empty states?",
77727
- "6. **Error boundaries** — risky subtrees wrapped so a throw does not crash the whole app? failure isolated from the dashboard?",
77728
- "7. **Alignment** — traced parent layout before leaf styles? position/height match siblings? snapshot test to map hierarchy?",
77729
- '8. **Feedback** — immediate pending state on async actions (e.g. ⌘Enter save → button "Saving...")?',
77727
+ "6. **Error boundaries** — risky subtrees wrapped so a throw does not crash the whole app? failure isolated from the shell?",
77728
+ "7. **Alignment** — traced parent layout before leaf styles? position/height match siblings?",
77729
+ '8. **Feedback** — immediate pending state on async actions (e.g. save → button "Saving...")?',
77730
+ "9. **Destructive actions** — confirmation dialog before delete/remove/archive/reset/clear or other irreversible/high-impact single actions?",
77731
+ "10. **Bulk actions** — confirmation before batch/multi-item operations (with count or impact summary)?",
77730
77732
  "",
77731
77733
  "### Flow complexity",
77732
77734
  "- Primary action ≤3 clicks from entry point",
@@ -77735,66 +77737,66 @@ function renderWebappUxHandoffReference() {
77735
77737
  "- Prefer inline actions over navigate-away-and-back",
77736
77738
  "",
77737
77739
  "### Presentation & responsive patterns",
77738
- "- Reuse existing components: `CommandPalette`, industrial dialogs, `ChatroomLoader`, timeline row chrome",
77739
- "- Match badge/button patterns from timeline (`BADGE_BASE`, `navButtonClass` in All-tab)",
77740
+ "- Reuse existing design-system components and established UI patterns before introducing new abstractions",
77741
+ "- Match badge/button styling from similar surfaces in the app",
77740
77742
  "- When multiple valid patterns exist, recommend one and explain tradeoff",
77741
- "- **md: breakpoint** (768px) splits mobile vs desktop",
77742
- "- **Hide/show:** `hidden md:flex` / `flex md:hidden` for alternate chrome",
77743
- "- **Mobile overlay:** fixed sidebar overlay with backdrop (`md:hidden`)",
77744
- "- **Separate mobile UI:** dedicated mobile modal/picker when desktop uses side panel",
77745
- "- **Shared responsive density:** same component, `md:` size variants (`h-7 md:h-9`)",
77746
- "- **Command dialogs:** industrial theme via `commandDialogStyles.ts`, top-anchored, max-w-[90vw]",
77743
+ "- Use the project's standard breakpoint(s) for mobile vs desktop",
77744
+ "- **Hide/show:** responsive utility classes or equivalent for alternate chrome per viewport",
77745
+ "- **Mobile overlay:** full-screen or sheet overlay with backdrop when desktop uses persistent panels",
77746
+ "- **Separate mobile UI:** dedicated mobile modal/picker when desktop uses side panel or split view",
77747
+ "- **Shared responsive density:** same component with size/density variants per breakpoint",
77748
+ "- **Command/search dialogs:** match existing modal/dialog styling; sensible max-width on small screens",
77747
77749
  "",
77748
77750
  "### Layout simplification",
77749
77751
  "- Review card/section layouts for unnecessary rows, nested wrappers, or misaligned actions",
77750
- "- Prefer compact rows: title and overflow menu (⋮ `MoreVertical` popover) on one line via flex/grid",
77751
- '- Description on the next line; primary CTA (e.g. "View Details") on a trailing row aligned end',
77752
+ "- Prefer compact rows: title and overflow menu on one line via flex/grid",
77753
+ "- Description on the next line; primary CTA on a trailing row aligned end",
77752
77754
  "- Canonical simplified card pattern:",
77753
77755
  " ```",
77754
- " <title> <overflow-menu ⋮>",
77756
+ " <title> <overflow-menu>",
77755
77757
  " <description>",
77756
77758
  " <primary-cta aligned end>",
77757
77759
  " ```",
77758
- "- Reuse `CardHeader` + `CardAction` grid (`grid-cols-[1fr_auto]`) or equivalent flex `justify-between`",
77759
- "- Flag multi-row chrome that could collapse (menu on its own row, CTA left-aligned when end-aligned matches existing cards)",
77760
+ "- Use header + action grid or equivalent flex `justify-between`",
77761
+ "- Flag multi-row chrome that could collapse (menu on its own row, CTA misaligned vs similar cards)",
77760
77762
  "",
77761
77763
  "### Error & loading states",
77762
- "- Initial fetch: `ChatroomLoader` centered (see `ChatroomTimelineFeed`, `ConversationSlicePanel`)",
77763
- "- Pagination: `isLoadingOlder` / `isLoadingMore` inline loader at scroll edge",
77764
- "- Save mutations: inline success/error text beside button (`AgentSettingsModal` `saveResult` pattern)",
77764
+ "- Initial fetch: centered loader or skeleton for the content area",
77765
+ "- Pagination/infinite scroll: inline loader at scroll edge",
77766
+ "- Save/submit mutations: inline success/error feedback beside the trigger control",
77765
77767
  "- Never leave blank panels on fetch failure — show error message or retry affordance",
77766
- "- Disable interactive controls while `isLoading` / `isPending`",
77768
+ "- Disable interactive controls while loading or pending",
77767
77769
  "",
77768
77770
  "### Error boundaries",
77769
- "- Wrap data-dependent or third-party subtrees with `ErrorBoundary` (chatroom) or rely on `SentryErrorBoundary` (root) / `AuthErrorBoundary` (app shell)",
77770
- "- A single component throw must not unmount the entire dashboard — scope boundaries to the failing panel/section",
77771
- "- Provide fallback UI with recovery action (reload button pattern in `ErrorBoundary.tsx`)",
77771
+ "- Wrap data-dependent or third-party subtrees with error boundaries so a single failure does not unmount the whole app",
77772
+ "- Scope boundaries to the failing panel/section, not the entire shell",
77773
+ "- Provide fallback UI with a recovery action (retry, reload, or navigate away)",
77772
77774
  "",
77773
77775
  "### Alignment & component hierarchy",
77774
- "- Before styling a leaf component, trace parent flex/grid — sticky headers, `grid-cols-[1fr_auto_1fr]` timeline nav, `items-center` vs `items-start`",
77775
- "- Match sibling heights and vertical rhythm (`h-7 md:h-9` density pattern)",
77776
+ "- Before styling a leaf component, trace parent flex/grid context",
77777
+ "- Match sibling heights and vertical rhythm",
77776
77778
  "- Flag absolute positioning or fixed heights that fight parent layout",
77777
- "- **Planner/builder shortcut:** when hierarchy is unclear, write a vitest inline snapshot (`render` + `toMatchInlineSnapshot`, the repo convention in backend prompt tests) to inspect the full component tree before deciding leaf styles — remove or keep the snapshot based on team preference. Webapp currently has no snapshot-test precedent, so treat this as a diagnostic tool, not a convention to mandate.",
77779
+ "- When hierarchy is unclear, inspect the rendered component tree (e.g. DOM inspector or component test snapshot) before deciding leaf styles",
77778
77780
  "",
77779
77781
  "### Fast user feedback",
77780
- "- Async actions triggered by keyboard (`⌘Enter` via `isModEnterKey`) or click must show **immediate** UI response",
77781
- "- Canonical pattern: `isSaving` / `isPending` local state → button label `Saving...`, `disabled` while in flight (`AgentSettingsModal`)",
77782
+ "- Async actions triggered by keyboard shortcut or click must show **immediate** UI response",
77783
+ '- Canonical pattern: pending local state → button label changes (e.g. "Saving..."), control disabled while in flight',
77782
77784
  "- Show inline error on failure; brief success confirmation optional",
77783
- '- Pair shortcut hints with pending state ("Press ⌘Enter to save" only when save shows pending feedback)',
77785
+ "- Pair shortcut hints with pending state only when the action shows pending feedback",
77784
77786
  "",
77785
- "### Keyboard shortcuts (reference)",
77786
- "| Shortcut | Action |",
77787
- "|----------|--------|",
77788
- "| ⌘K / Ctrl+K | Chatroom switcher |",
77789
- "| ⌘P / Ctrl+P | File selector |",
77790
- "| ⌘⇧P / Ctrl+Shift+P | Command palette (scripts, saved commands) |",
77791
- "| ⌘⇧F / Ctrl+Shift+F | Workspace search |",
77792
- "| ⌘I / Ctrl+I | Attach explorer snippet |",
77793
- "| Enter (desktop, no Shift) | Send message |",
77794
- "| Shift+Enter | New line in composer |",
77795
- "| ⌘Enter / Ctrl+Enter | Confirm/save in modals |",
77796
- "| ⌘S / Ctrl+S | Save in workspace file dialogs |",
77797
- "| Escape | Close modal/dialog |"
77787
+ "### Destructive & bulk action safeguards",
77788
+ "- **Destructive actions** (delete, remove, archive, reset, clear, disable) require an explicit confirmation step — never fire immediately from a menu item or button without a dialog",
77789
+ "- Use the project's standard confirmation dialog/modal pattern",
77790
+ "- Confirmation includes clear title + description of what will happen; primary action styled as destructive when appropriate",
77791
+ "- **Bulk actions** (multi-select delete, batch disable, clear-all) require confirmation before execution — show how many items are affected",
77792
+ '- Bulk confirm should summarize scope (e.g. "Delete 12 items?") and list material impact when non-obvious',
77793
+ "- Flag plans that wire bulk/destructive handlers directly to mutations/API calls without a confirm gate",
77794
+ "",
77795
+ "### Keyboard shortcuts",
77796
+ "- Align proposed shortcuts with the project's existing shortcut catalog and platform conventions (⌘ on macOS, Ctrl on Windows/Linux)",
77797
+ "- Avoid conflicting bindings; document new shortcuts when introducing them",
77798
+ "- Common patterns: modifier+letter for global commands, Enter to confirm in dialogs, Escape to cancel/close, Shift+Enter for multiline input where applicable",
77799
+ "- Flag plans that add shortcuts without checking for conflicts or omit keyboard access for primary actions"
77798
77800
  ].join(`
77799
77801
  `);
77800
77802
  }
@@ -77832,6 +77834,8 @@ function getEnhancerFeedbackTemplateBody() {
77832
77834
  - **Error boundaries:** <error boundary placement; failure isolated from the whole app>
77833
77835
  - **Alignment:** <hierarchy traced; position/height issues; inline snapshot consideration>
77834
77836
  - **Feedback:** <immediate pending state on async actions; ⌘Enter + button state>
77837
+ - **Destructive safeguards:** <single-item irreversible/high-impact actions gated by confirm dialog; cite missing confirms>
77838
+ - **Bulk safeguards:** <batch/multi-item operations gated by confirm with count/impact summary; cite missing confirms>
77835
77839
  </handoff-ux>
77836
77840
 
77837
77841
  <handoff-notes>
@@ -78025,6 +78029,28 @@ function getHandoffSeverityGuidanceBlock() {
78025
78029
  - [low] <minor cleanup — nice to have>`;
78026
78030
  }
78027
78031
 
78032
+ // ../../services/backend/prompts/utils/proof-of-verification.ts
78033
+ function messagesAnchorCommand(params = {}) {
78034
+ const prefix = params.cliEnvPrefix ?? "";
78035
+ const chatroomId = params.chatroomId ?? "<chatroom-id>";
78036
+ const role = params.role ?? "<role>";
78037
+ return `${prefix}chatroom messages anchor --chatroom-id="${chatroomId}" --role="${role}"`;
78038
+ }
78039
+ function getProofOfVerificationComment(params = {}) {
78040
+ const anchorCmd = messagesAnchorCommand(params);
78041
+ return `<!-- Entry-point proof-of-completion workflow — run before filling this section:
78042
+ 1. \`${anchorCmd}\` — locate the user's last message (and prior user messages for context)
78043
+ 2. \`messages download --since-message-id=<id>\` — download grep-friendly history since anchor; read handoffs and goals
78044
+ 3. If the user's last message was terse (e.g. "do it", "raise a PR"), review prior user messages from anchor output and widen --limit before validating
78045
+ 4. Validate commits/PRs against ALL requirements — not just the last slice. Incomplete → rework; do NOT hand off to user.
78046
+ Then: ${anchorCmd} → messages download --since-message-id=<id> from anchor output -->`;
78047
+ }
78048
+ function getEntryPointProofOfCompletionVerificationBlock(params = {}) {
78049
+ return `${getProofOfVerificationComment(params)}
78050
+ ${PROOF_OF_VERIFICATION_CHECKBOX}`;
78051
+ }
78052
+ var PROOF_OF_VERIFICATION_CHECKBOX = "- [ ] I confirm I verified the user's full request: anchored on the last user message, downloaded history since that anchor, reviewed handoffs/goals (including prior user messages when the latest was a terse follow-up), and validated every requirement below before this handoff";
78053
+
78028
78054
  // ../../services/backend/prompts/utils/unresolved-decisions.ts
78029
78055
  function getUnresolvedDecisionsSectionBlock() {
78030
78056
  return `## Unresolved Decisions
@@ -78036,7 +78062,7 @@ ${getHandoffSeverityGuidanceBlock()}
78036
78062
  var init_unresolved_decisions = () => {};
78037
78063
 
78038
78064
  // ../../services/backend/prompts/utils/handoff-report-template-body.ts
78039
- function getHandoffReportTemplateBody(roleGuidanceContext) {
78065
+ function getHandoffReportTemplateBody(roleGuidanceContext, options = {}) {
78040
78066
  return `<handoff-overview>
78041
78067
  <!-- For informational tasks (summaries, feedback, Q&amp;A with no code changes): put the complete primary answer in Summary and What changed — the user only sees this handoff. -->
78042
78068
  ## Summary
@@ -78061,10 +78087,16 @@ ${getHandoffNotApplicableSectionComment("List planning steps for trivial single-
78061
78087
  ${getHandoffQualityPrinciplesSectionBlock()}
78062
78088
 
78063
78089
  ## Proof of Completion
78090
+ ${options.includeProofOfVerification ? `${getEntryPointProofOfCompletionVerificationBlock(roleGuidanceContext)}
78064
78091
  ${getContextReadDisclosureBlock(roleGuidanceContext)}
78092
+ - Context goal: <state the context goal and confirm it was achieved>
78093
+ - Requirements (one bullet per user requirement from the user's message — met/not met + evidence):
78094
+ - <requirement> — <PR URL, commit hash, or file evidence>
78095
+ - Files changed (code tasks — list every file modified):
78065
78096
  ${getFileReferenceProofOfCompletionExample()}
78066
- <evidence the goal was met — list every file you (or the builder) modified>
78067
-
78097
+ - <additional files as needed>` : `${getContextReadDisclosureBlock(roleGuidanceContext)}
78098
+ ${getFileReferenceProofOfCompletionExample()}
78099
+ <evidence the goal was met — list every file you (or the builder) modified>`}
78068
78100
  ## Backlog Tasks Implemented
78069
78101
  ${getHandoffNotApplicableSectionComment("List backlog items addressed if none were in scope")}
78070
78102
  - \`backlog-item-id\` — <backlog item title/summary and how this work addresses it>
@@ -78133,7 +78165,7 @@ function getPlannerToUserReportTemplate(roleGuidanceContext) {
78133
78165
  ${getHandoffReportTemplateIntro("Report Template (Planner → User)")}
78134
78166
 
78135
78167
  \`\`\`markdown
78136
- ${getHandoffReportTemplateBody(roleGuidanceContext)}
78168
+ ${getHandoffReportTemplateBody(roleGuidanceContext, { includeProofOfVerification: true })}
78137
78169
  \`\`\``;
78138
78170
  }
78139
78171
  var init_planner_to_user = __esm(() => {
@@ -78176,7 +78208,7 @@ function getSoloToUserReportTemplate(roleGuidanceContext) {
78176
78208
  ${getHandoffReportTemplateIntro("Report Template (Solo → User)")}
78177
78209
 
78178
78210
  \`\`\`markdown
78179
- ${getHandoffReportTemplateBody(roleGuidanceContext)}
78211
+ ${getHandoffReportTemplateBody(roleGuidanceContext, { includeProofOfVerification: true })}
78180
78212
  \`\`\``;
78181
78213
  }
78182
78214
  var init_solo_to_user = __esm(() => {
@@ -78491,12 +78523,16 @@ var init_glossary = __esm(() => {
78491
78523
  var init_builder = () => {};
78492
78524
  // ../../services/backend/prompts/cli/sections/handoff-rules.ts
78493
78525
  var init_handoff_rules = () => {};
78526
+ // ../../services/backend/prompts/cli/sections/proof-of-verification.ts
78527
+ var init_proof_of_verification = () => {};
78528
+
78494
78529
  // ../../services/backend/prompts/cli/sections/operating-model.ts
78495
78530
  var init_operating_model = () => {};
78496
78531
 
78497
78532
  // ../../services/backend/prompts/cli/sections/index.ts
78498
78533
  var init_sections = __esm(() => {
78499
78534
  init_handoff_rules();
78535
+ init_proof_of_verification();
78500
78536
  init_operating_model();
78501
78537
  });
78502
78538
 
@@ -78506,8 +78542,14 @@ var init_planner = __esm(() => {
78506
78542
  init_sections();
78507
78543
  });
78508
78544
 
78545
+ // ../../services/backend/prompts/cli/roles/planner-guidance-context.ts
78546
+ var init_planner_guidance_context = __esm(() => {
78547
+ init_env();
78548
+ });
78549
+
78509
78550
  // ../../services/backend/prompts/teams/solo/prompts/solo.ts
78510
78551
  var init_solo = __esm(() => {
78552
+ init_planner_guidance_context();
78511
78553
  init_sections();
78512
78554
  });
78513
78555
 
@@ -78523,11 +78565,6 @@ var init_builder2 = __esm(() => {
78523
78565
  init_builder();
78524
78566
  });
78525
78567
 
78526
- // ../../services/backend/prompts/cli/roles/planner-guidance-context.ts
78527
- var init_planner_guidance_context = __esm(() => {
78528
- init_env();
78529
- });
78530
-
78531
78568
  // ../../services/backend/prompts/teams/duo/prompts/planner.ts
78532
78569
  var init_planner2 = __esm(() => {
78533
78570
  init_planner_guidance_context();
@@ -80535,40 +80572,56 @@ async function downloadMessages(chatroomId, options, deps) {
80535
80572
  };
80536
80573
  await exports_Effect.runPromise(downloadMessagesEffect(chatroomId, options).pipe(exports_Effect.catchAll(handler), exports_Effect.provide(exports_Layer.mergeAll(layer, MessagesFsServiceLive))));
80537
80574
  }
80538
- var PAGE_SIZE = 50, ABSOLUTE_MAX = 5000, DEFAULT_LIMIT = 10, downloadMessagesEffect = (chatroomId, options) => exports_Effect.gen(function* () {
80539
- const backend2 = yield* BackendService;
80540
- const fs11 = yield* MessagesFsService;
80541
- const sessionId = yield* requireSessionIdEffect((a) => ({
80542
- _tag: "NotAuthenticated",
80543
- convexUrl: a.convexUrl,
80544
- otherUrls: a.otherUrls
80545
- }));
80546
- yield* validateChatroomIdEffect(chatroomId, (id3) => ({
80547
- _tag: "InvalidChatroomId",
80548
- id: id3
80549
- }));
80550
- const format4 = options.format ?? "linear";
80551
- const maxDownload = parseLimit(options.limit);
80552
- const outputDir = options.outputDir ?? resolveDownloadOutputDir(format4);
80553
- const absoluteOutputDir = nodePath2.resolve(outputDir);
80575
+ var PAGE_SIZE = 50, SINCE_PAGE_SIZE = 500, ABSOLUTE_MAX = 5000, DEFAULT_LIMIT = 10, fetchForwardSince = (args2, backend2) => exports_Effect.gen(function* () {
80576
+ const messages = [];
80577
+ let cursor = args2.sinceMessageId;
80578
+ let reachedEnd = false;
80579
+ let truncated = false;
80580
+ while (messages.length < args2.maxDownload && !reachedEnd) {
80581
+ const remaining = args2.maxDownload - messages.length;
80582
+ const pageSize = Math.min(SINCE_PAGE_SIZE, remaining);
80583
+ const batch = yield* backend2.query(api.messages.listSinceMessage, {
80584
+ sessionId: args2.sessionId,
80585
+ chatroomId: args2.chatroomId,
80586
+ sinceMessageId: cursor,
80587
+ limit: pageSize
80588
+ });
80589
+ if (batch.length === 0) {
80590
+ reachedEnd = true;
80591
+ break;
80592
+ }
80593
+ const toAppend = cursor === args2.sinceMessageId ? batch : batch.slice(1);
80594
+ if (toAppend.length === 0) {
80595
+ reachedEnd = true;
80596
+ break;
80597
+ }
80598
+ messages.push(...toAppend);
80599
+ if (messages.length >= args2.maxDownload) {
80600
+ truncated = true;
80601
+ break;
80602
+ }
80603
+ cursor = toAppend[toAppend.length - 1]._id;
80604
+ }
80605
+ return { messages, truncated, hasMore: !reachedEnd };
80606
+ }), fetchLatestBackward = (args2, backend2) => exports_Effect.gen(function* () {
80554
80607
  const messages = [];
80555
80608
  let truncated = false;
80556
80609
  let hasMore = true;
80557
- const firstPageSize = Math.min(PAGE_SIZE, maxDownload);
80558
- const latest = yield* backend2.query(api.messageList.getLatestMessages, { sessionId, chatroomId, limit: firstPageSize });
80610
+ const firstPageSize = Math.min(PAGE_SIZE, args2.maxDownload);
80611
+ const latest = yield* backend2.query(api.messageList.getLatestMessages, { sessionId: args2.sessionId, chatroomId: args2.chatroomId, limit: firstPageSize });
80559
80612
  messages.push(...latest.messages);
80560
80613
  hasMore = latest.hasMore;
80561
- while (hasMore && messages.length < maxDownload) {
80614
+ while (hasMore && messages.length < args2.maxDownload) {
80562
80615
  const oldest = messages[0];
80563
80616
  if (!oldest) {
80564
80617
  hasMore = false;
80565
80618
  break;
80566
80619
  }
80567
- const remaining = maxDownload - messages.length;
80620
+ const remaining = args2.maxDownload - messages.length;
80568
80621
  const pageSize = Math.min(PAGE_SIZE, remaining);
80569
80622
  const batch = yield* backend2.query(api.messageList.listMessagesBefore, {
80570
- sessionId,
80571
- chatroomId,
80623
+ sessionId: args2.sessionId,
80624
+ chatroomId: args2.chatroomId,
80572
80625
  before: oldest._creationTime,
80573
80626
  limit: pageSize
80574
80627
  });
@@ -80576,16 +80629,39 @@ var PAGE_SIZE = 50, ABSOLUTE_MAX = 5000, DEFAULT_LIMIT = 10, downloadMessagesEff
80576
80629
  hasMore = false;
80577
80630
  break;
80578
80631
  }
80579
- const space = maxDownload - messages.length;
80632
+ const space = args2.maxDownload - messages.length;
80580
80633
  const toPrepend = batch.slice(Math.max(0, batch.length - space));
80581
80634
  for (let i2 = toPrepend.length - 1;i2 >= 0; i2--)
80582
80635
  messages.unshift(toPrepend[i2]);
80583
- hasMore = batch.length >= pageSize && messages.length < maxDownload;
80584
- if (messages.length >= maxDownload && (batch.length >= pageSize || latest.hasMore))
80636
+ hasMore = batch.length >= pageSize && messages.length < args2.maxDownload;
80637
+ if (messages.length >= args2.maxDownload && (batch.length >= pageSize || latest.hasMore))
80585
80638
  truncated = true;
80586
80639
  if (toPrepend.length < batch.length)
80587
80640
  truncated = true;
80588
80641
  }
80642
+ return { messages, truncated, hasMore };
80643
+ }), downloadMessagesEffect = (chatroomId, options) => exports_Effect.gen(function* () {
80644
+ const backend2 = yield* BackendService;
80645
+ const fs11 = yield* MessagesFsService;
80646
+ const sessionId = yield* requireSessionIdEffect((a) => ({
80647
+ _tag: "NotAuthenticated",
80648
+ convexUrl: a.convexUrl,
80649
+ otherUrls: a.otherUrls
80650
+ }));
80651
+ yield* validateChatroomIdEffect(chatroomId, (id3) => ({
80652
+ _tag: "InvalidChatroomId",
80653
+ id: id3
80654
+ }));
80655
+ const format4 = options.format ?? "linear";
80656
+ const maxDownload = parseLimit(options.limit);
80657
+ const outputDir = options.outputDir ?? resolveDownloadOutputDir(format4);
80658
+ const absoluteOutputDir = nodePath2.resolve(outputDir);
80659
+ const { messages, truncated, hasMore } = yield* options.sinceMessageId ? fetchForwardSince({
80660
+ sessionId,
80661
+ chatroomId,
80662
+ sinceMessageId: options.sinceMessageId,
80663
+ maxDownload
80664
+ }, backend2) : fetchLatestBackward({ sessionId, chatroomId, maxDownload }, backend2);
80589
80665
  const complete3 = !truncated && !hasMore;
80590
80666
  yield* fs11.rm(outputDir, { recursive: true, force: true }).pipe(exports_Effect.catchAll(() => exports_Effect.void));
80591
80667
  yield* fs11.mkdir(outputDir, { recursive: true }).pipe(exports_Effect.mapError((cause3) => ({ _tag: "OutputDirError", cause: cause3 })));
@@ -80613,6 +80689,8 @@ var PAGE_SIZE = 50, ABSOLUTE_MAX = 5000, DEFAULT_LIMIT = 10, downloadMessagesEff
80613
80689
  count: messages.length,
80614
80690
  complete: complete3,
80615
80691
  truncated,
80692
+ sinceMessageId: options.sinceMessageId ?? null,
80693
+ anchorMessageId: options.sinceMessageId ?? null,
80616
80694
  oldestDownloadedAt: messages[0] ? new Date(messages[0]._creationTime).toISOString() : null,
80617
80695
  newestDownloadedAt: messages[messages.length - 1] ? new Date(messages[messages.length - 1]._creationTime).toISOString() : null,
80618
80696
  messages: manifestEntries
@@ -80637,7 +80715,13 @@ var PAGE_SIZE = 50, ABSOLUTE_MAX = 5000, DEFAULT_LIMIT = 10, downloadMessagesEff
80637
80715
  const nextLimit = Math.min(messages.length * 2, ABSOLUTE_MAX);
80638
80716
  console.log(`
80639
80717
  \uD83D\uDCA1 Truncated — fetch more history by increasing --limit:`);
80640
- console.log(` chatroom messages download --chatroom-id=${chatroomId} --role=${options.role} --format=linear --limit=${nextLimit}`);
80718
+ console.log(` chatroom messages download --chatroom-id=${chatroomId} --role=${options.role} --format=linear --limit=${nextLimit}${options.sinceMessageId ? ` --since-message-id=${options.sinceMessageId}` : ""}`);
80719
+ }
80720
+ if (!options.sinceMessageId && !options.limit) {
80721
+ console.log(`
80722
+ \uD83D\uDCA1 Anchor on the user's last message for proof of verification:`);
80723
+ console.log(` chatroom messages anchor --chatroom-id=${chatroomId} --role=${options.role}`);
80724
+ console.log(` Then download history since that anchor: messages download --since-message-id=<id> from anchor output`);
80641
80725
  }
80642
80726
  });
80643
80727
  });
@@ -80648,6 +80732,108 @@ var init_download = __esm(() => {
80648
80732
  init_services();
80649
80733
  });
80650
80734
 
80735
+ // src/commands/messages/anchor.ts
80736
+ var exports_anchor = {};
80737
+ __export(exports_anchor, {
80738
+ anchorMessagesEffect: () => anchorMessagesEffect,
80739
+ anchorMessages: () => anchorMessages
80740
+ });
80741
+ function previewContent(content, maxChars = PREVIEW_MAX_CHARS) {
80742
+ const collapsed = content.replace(/\s+/g, " ").trim();
80743
+ return collapsed.length > maxChars ? `${collapsed.slice(0, maxChars)}…` : collapsed;
80744
+ }
80745
+ function formatAnchorTime(creationTime) {
80746
+ return new Date(creationTime).toISOString();
80747
+ }
80748
+ async function anchorMessages(chatroomId, options, deps) {
80749
+ const { getSessionId: getSessionId2, getOtherSessionUrls: getOtherSessionUrls2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
80750
+ const { getConvexClient: getConvexClient2, getConvexUrl: getConvexUrl2 } = await Promise.resolve().then(() => (init_client2(), exports_client));
80751
+ const client4 = await getConvexClient2();
80752
+ const actualDeps = deps ?? {
80753
+ backend: {
80754
+ mutation: (ep, args2) => client4.mutation(ep, args2),
80755
+ query: (ep, args2) => client4.query(ep, args2)
80756
+ },
80757
+ session: { getSessionId: getSessionId2, getConvexUrl: getConvexUrl2, getOtherSessionUrls: getOtherSessionUrls2 }
80758
+ };
80759
+ const layer = commandServicesLayerFromDeps(actualDeps);
80760
+ const handler = (err) => {
80761
+ return exports_Effect.sync(() => {
80762
+ if (err._tag === "NotAuthenticated") {
80763
+ console.error(`❌ Not authenticated`);
80764
+ process.exit(1);
80765
+ } else if (err._tag === "InvalidChatroomId") {
80766
+ console.error(`❌ Invalid chatroom ID`);
80767
+ process.exit(1);
80768
+ } else if (err._tag === "QueryFailed") {
80769
+ console.error(`
80770
+ ❌ Error fetching messages: ${err.cause.message}`);
80771
+ process.exit(1);
80772
+ } else {
80773
+ console.error(`
80774
+ ❌ Anchor failed: ${err.message}`);
80775
+ process.exit(1);
80776
+ }
80777
+ });
80778
+ };
80779
+ await exports_Effect.runPromise(anchorMessagesEffect(chatroomId, options).pipe(exports_Effect.catchAll(handler), exports_Effect.provide(layer)));
80780
+ }
80781
+ var PREVIEW_MAX_CHARS = 200, DEFAULT_PRIOR_LIMIT = 3, ANCHOR_DOWNLOAD_DIR = ".chatroom/downloads/messages/linear/anchor", anchorMessagesEffect = (chatroomId, options) => exports_Effect.gen(function* () {
80782
+ const backend2 = yield* BackendService;
80783
+ const sessionId = yield* requireSessionIdEffect((a) => ({
80784
+ _tag: "NotAuthenticated",
80785
+ convexUrl: a.convexUrl,
80786
+ otherUrls: a.otherUrls
80787
+ }));
80788
+ yield* validateChatroomIdEffect(chatroomId, (id3) => ({
80789
+ _tag: "InvalidChatroomId",
80790
+ id: id3
80791
+ }));
80792
+ const priorLimit = options.priorLimit ?? DEFAULT_PRIOR_LIMIT;
80793
+ const result = yield* backend2.query(api.messages.getLastUserMessage, {
80794
+ sessionId,
80795
+ chatroomId,
80796
+ priorLimit
80797
+ }).pipe(exports_Effect.mapError((cause3) => ({ _tag: "QueryFailed", cause: cause3 })));
80798
+ yield* exports_Effect.sync(() => {
80799
+ const { last: last2, prior } = result;
80800
+ if (!last2) {
80801
+ console.log(`
80802
+ \uD83D\uDCED No user messages found in chatroom ${chatroomId}`);
80803
+ return;
80804
+ }
80805
+ console.log(`
80806
+ \uD83C\uDFAF Last user message anchor:`);
80807
+ console.log(` ID: ${last2._id}`);
80808
+ console.log(` Time: ${formatAnchorTime(last2._creationTime)}`);
80809
+ console.log(` Content: ${previewContent(last2.content)}`);
80810
+ if (prior.length > 0) {
80811
+ console.log(`
80812
+ Prior user messages (context for terse follow-ups):`);
80813
+ for (const msg of prior) {
80814
+ console.log(` - ${msg._id} (${formatAnchorTime(msg._creationTime)}) — ${previewContent(msg.content)}`);
80815
+ }
80816
+ }
80817
+ console.log(`
80818
+ ✅ Proof of verification workflow:`);
80819
+ console.log(` chatroom messages download --chatroom-id=${chatroomId} --role=${options.role} --since-message-id=${last2._id} --limit=100 --output-dir=${ANCHOR_DOWNLOAD_DIR}`);
80820
+ console.log(` rg "handoff|Goal|Requirements" "${ANCHOR_DOWNLOAD_DIR}/"`);
80821
+ console.log(`
80822
+ \uD83D\uDCA1 If the download reports truncated=true, increase --limit (e.g. 200, 500).`);
80823
+ console.log(`\uD83D\uDCA1 For context before the anchor, download without --since-message-id:`);
80824
+ console.log(` chatroom messages download --chatroom-id=${chatroomId} --role=${options.role} --format=linear --limit=200`);
80825
+ if (prior.length > 0) {
80826
+ console.log(`
80827
+ \uD83D\uDCA1 Terse follow-up (e.g. "do it", "raise a PR")? Review the prior user messages above to recover the full request before validating commits/PRs.`);
80828
+ }
80829
+ });
80830
+ });
80831
+ var init_anchor = __esm(() => {
80832
+ init_esm();
80833
+ init_api3();
80834
+ init_services();
80835
+ });
80836
+
80651
80837
  // src/commands/messages/index.ts
80652
80838
  var exports_messages = {};
80653
80839
  __export(exports_messages, {
@@ -80655,7 +80841,9 @@ __export(exports_messages, {
80655
80841
  listSinceMessage: () => listSinceMessage,
80656
80842
  listBySenderRoleEffect: () => listBySenderRoleEffect,
80657
80843
  listBySenderRole: () => listBySenderRole,
80658
- downloadMessages: () => downloadMessages
80844
+ downloadMessages: () => downloadMessages,
80845
+ anchorMessagesEffect: () => anchorMessagesEffect,
80846
+ anchorMessages: () => anchorMessages
80659
80847
  });
80660
80848
  async function createDefaultDeps14() {
80661
80849
  const client4 = await getConvexClient();
@@ -80818,6 +81006,7 @@ var init_messages = __esm(() => {
80818
81006
  init_client2();
80819
81007
  init_services();
80820
81008
  init_download();
81009
+ init_anchor();
80821
81010
  });
80822
81011
 
80823
81012
  // src/commands/context/format-new-context-error.ts
@@ -107107,7 +107296,7 @@ var init_assert_registered_working_dir = __esm(() => {
107107
107296
 
107108
107297
  // src/infrastructure/services/workspace/workspace-path-security.ts
107109
107298
  import { realpath } from "node:fs/promises";
107110
- import { basename as basename3, dirname as dirname8, isAbsolute, relative as relative2, resolve as resolve5, sep } from "node:path";
107299
+ import { dirname as dirname8, isAbsolute, relative as relative2, resolve as resolve5, sep } from "node:path";
107111
107300
  import { gunzipSync } from "node:zlib";
107112
107301
  function validateRelativePathSegments(filePath) {
107113
107302
  if (filePath.includes("\x00"))
@@ -107128,6 +107317,39 @@ function isPathInsideRoot(workspaceRoot, targetPath) {
107128
107317
  return false;
107129
107318
  return true;
107130
107319
  }
107320
+ function assertContained(workspaceRoot, targetPath) {
107321
+ if (!isPathInsideRoot(workspaceRoot, targetPath)) {
107322
+ return { ok: false, error: "Path escapes workspace" };
107323
+ }
107324
+ return { ok: true };
107325
+ }
107326
+ async function resolveNonExistentTarget(workspaceRoot, candidate) {
107327
+ let current = dirname8(candidate);
107328
+ for (;; ) {
107329
+ try {
107330
+ const ancestorReal = await realpath(current);
107331
+ const ancestorCheck = assertContained(workspaceRoot, ancestorReal);
107332
+ if (!ancestorCheck.ok)
107333
+ return ancestorCheck;
107334
+ const suffix = relative2(current, candidate);
107335
+ const resolved = resolve5(ancestorReal, suffix);
107336
+ const resolvedCheck = assertContained(workspaceRoot, resolved);
107337
+ if (!resolvedCheck.ok)
107338
+ return resolvedCheck;
107339
+ return { ok: true, absolutePath: resolved };
107340
+ } catch (err) {
107341
+ const code2 = err?.code;
107342
+ if (code2 !== "ENOENT") {
107343
+ return { ok: false, error: "Invalid file path" };
107344
+ }
107345
+ const parent = dirname8(current);
107346
+ if (parent === current) {
107347
+ return { ok: false, error: "Path escapes workspace" };
107348
+ }
107349
+ current = parent;
107350
+ }
107351
+ }
107352
+ }
107131
107353
  async function resolvePathWithinWorkspace(workingDir, filePath) {
107132
107354
  const basic = validateRelativePathSegments(filePath);
107133
107355
  if (!basic.ok)
@@ -107144,32 +107366,16 @@ async function resolvePathWithinWorkspace(workingDir, filePath) {
107144
107366
  }
107145
107367
  try {
107146
107368
  const resolved = await realpath(candidate);
107147
- if (!isPathInsideRoot(workspaceRoot, resolved)) {
107148
- return { ok: false, error: "Path escapes workspace" };
107149
- }
107369
+ const containment = assertContained(workspaceRoot, resolved);
107370
+ if (!containment.ok)
107371
+ return containment;
107150
107372
  return { ok: true, absolutePath: resolved };
107151
107373
  } catch (err) {
107152
107374
  const code2 = err?.code;
107153
107375
  if (code2 !== "ENOENT") {
107154
107376
  return { ok: false, error: "Invalid file path" };
107155
107377
  }
107156
- const parent = dirname8(candidate);
107157
- if (parent === workspaceRoot || isPathInsideRoot(workspaceRoot, parent)) {
107158
- try {
107159
- const parentReal = await realpath(parent);
107160
- if (!isPathInsideRoot(workspaceRoot, parentReal)) {
107161
- return { ok: false, error: "Path escapes workspace" };
107162
- }
107163
- const resolved = resolve5(parentReal, basename3(candidate));
107164
- if (!isPathInsideRoot(workspaceRoot, resolved)) {
107165
- return { ok: false, error: "Path escapes workspace" };
107166
- }
107167
- return { ok: true, absolutePath: resolved };
107168
- } catch {
107169
- return { ok: true, absolutePath: candidate };
107170
- }
107171
- }
107172
- return { ok: false, error: "Path escapes workspace" };
107378
+ return resolveNonExistentTarget(workspaceRoot, candidate);
107173
107379
  }
107174
107380
  }
107175
107381
  function gunzipBase64Payload(base643, maxBytes) {
@@ -108819,10 +109025,10 @@ var init_readdirp = __esm(() => {
108819
109025
  }
108820
109026
  async _formatEntry(dirent, path6) {
108821
109027
  let entry;
108822
- const basename4 = this._isDirent ? dirent.name : dirent;
109028
+ const basename3 = this._isDirent ? dirent.name : dirent;
108823
109029
  try {
108824
- const fullPath = presolve(pjoin(path6, basename4));
108825
- entry = { path: prelative(this._root, fullPath), fullPath, basename: basename4 };
109030
+ const fullPath = presolve(pjoin(path6, basename3));
109031
+ entry = { path: prelative(this._root, fullPath), fullPath, basename: basename3 };
108826
109032
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
108827
109033
  } catch (err) {
108828
109034
  this._onError(err);
@@ -108909,9 +109115,9 @@ class NodeFsHandler {
108909
109115
  _watchWithNodeFs(path6, listener) {
108910
109116
  const opts = this.fsw.options;
108911
109117
  const directory = sp.dirname(path6);
108912
- const basename5 = sp.basename(path6);
109118
+ const basename4 = sp.basename(path6);
108913
109119
  const parent = this.fsw._getWatchedDir(directory);
108914
- parent.add(basename5);
109120
+ parent.add(basename4);
108915
109121
  const absolutePath = sp.resolve(path6);
108916
109122
  const options = {
108917
109123
  persistent: opts.persistent
@@ -108921,7 +109127,7 @@ class NodeFsHandler {
108921
109127
  let closer;
108922
109128
  if (opts.usePolling) {
108923
109129
  const enableBin = opts.interval !== opts.binaryInterval;
108924
- options.interval = enableBin && isBinaryPath(basename5) ? opts.binaryInterval : opts.interval;
109130
+ options.interval = enableBin && isBinaryPath(basename4) ? opts.binaryInterval : opts.interval;
108925
109131
  closer = setFsWatchFileListener(path6, absolutePath, options, {
108926
109132
  listener,
108927
109133
  rawEmitter: this.fsw._emitRaw
@@ -108940,10 +109146,10 @@ class NodeFsHandler {
108940
109146
  return;
108941
109147
  }
108942
109148
  const dirname10 = sp.dirname(file2);
108943
- const basename5 = sp.basename(file2);
109149
+ const basename4 = sp.basename(file2);
108944
109150
  const parent = this.fsw._getWatchedDir(dirname10);
108945
109151
  let prevStats = stats;
108946
- if (parent.has(basename5))
109152
+ if (parent.has(basename4))
108947
109153
  return;
108948
109154
  const listener = async (path6, newStats) => {
108949
109155
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file2, 5))
@@ -108968,9 +109174,9 @@ class NodeFsHandler {
108968
109174
  prevStats = newStats2;
108969
109175
  }
108970
109176
  } catch (error51) {
108971
- this.fsw._remove(dirname10, basename5);
109177
+ this.fsw._remove(dirname10, basename4);
108972
109178
  }
108973
- } else if (parent.has(basename5)) {
109179
+ } else if (parent.has(basename4)) {
108974
109180
  const at = newStats.atimeMs;
108975
109181
  const mt = newStats.mtimeMs;
108976
109182
  if (!at || at <= mt || mt !== prevStats.mtimeMs) {
@@ -110907,11 +111113,114 @@ var init_file_tree_subscription = __esm(() => {
110907
111113
  init_daemon_services();
110908
111114
  });
110909
111115
 
111116
+ // ../../services/backend/src/domain/constants/chat-attachment-upload-path.ts
111117
+ function sanitizeChatAttachmentFileName(originalFileName) {
111118
+ const base = originalFileName.replace(/\\/g, "/").split("/").pop() ?? "";
111119
+ const cleaned = base.replace(/[\0-\x1f\x7f"<>\|:*?]/g, "").replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
111120
+ const valid = cleaned.length > 0 && cleaned !== "." && cleaned !== "..";
111121
+ return valid ? cleaned.slice(0, 120) : "attachment";
111122
+ }
111123
+ function getAttachmentRemainder(normalized) {
111124
+ if (!normalized.startsWith(`${CHAT_ATTACHMENT_UPLOAD_DIR}/`))
111125
+ return null;
111126
+ const remainder = normalized.slice(CHAT_ATTACHMENT_UPLOAD_DIR.length + 1);
111127
+ if (!remainder || remainder.includes("/"))
111128
+ return null;
111129
+ return remainder;
111130
+ }
111131
+ function parseAttachmentFileName(remainder) {
111132
+ const underscore = remainder.indexOf("_");
111133
+ if (underscore <= 0)
111134
+ return false;
111135
+ const uuid3 = remainder.slice(0, underscore);
111136
+ const basename5 = remainder.slice(underscore + 1);
111137
+ if (!UUID_V4_RE.test(uuid3) || !basename5)
111138
+ return false;
111139
+ return sanitizeChatAttachmentFileName(basename5) === basename5;
111140
+ }
111141
+ function getInvalidChatAttachmentUploadPathReason(relativePath) {
111142
+ if (relativePath.includes("\\"))
111143
+ return "Invalid attachment path";
111144
+ const normalized = relativePath.replace(/\\/g, "/");
111145
+ const remainder = getAttachmentRemainder(normalized);
111146
+ if (!remainder || !parseAttachmentFileName(remainder)) {
111147
+ return "Invalid attachment path";
111148
+ }
111149
+ if (new TextEncoder().encode(remainder).length > CHAT_ATTACHMENT_FILENAME_MAX_BYTES) {
111150
+ return "Invalid attachment path";
111151
+ }
111152
+ return null;
111153
+ }
111154
+ var CHAT_ATTACHMENT_UPLOAD_DIR = ".chatroom/downloads/attachments/files", UUID_V4_RE, CHAT_ATTACHMENT_FILENAME_MAX_BYTES = 255;
111155
+ var init_chat_attachment_upload_path = __esm(() => {
111156
+ UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
111157
+ });
111158
+
111159
+ // ../../services/backend/src/domain/constants/workspace-upload-path-policy.ts
111160
+ function normalizeSegments(relativePath) {
111161
+ const segments = relativePath.replace(/\\/g, "/").split("/").filter(Boolean);
111162
+ const lowerSegments = segments.map((segment) => segment.toLowerCase());
111163
+ return { lowerSegments, basename: lowerSegments.at(-1) ?? "" };
111164
+ }
111165
+ function hasBlockedSegment(lowerSegments) {
111166
+ return lowerSegments.some((segment) => BLOCKED_SEGMENT_NAMES.has(segment));
111167
+ }
111168
+ function hasBlockedBasename(basename5) {
111169
+ return /^\.env(?:\.|$)/.test(basename5) || /\.(?:pem|key)$/.test(basename5) || basename5 === "id_rsa" || basename5 === "credentials.json";
111170
+ }
111171
+ function isBlockedUploadTargetPath(relativePath) {
111172
+ if (!relativePath)
111173
+ return false;
111174
+ const { lowerSegments, basename: basename5 } = normalizeSegments(relativePath);
111175
+ return hasBlockedSegment(lowerSegments) || hasBlockedBasename(basename5);
111176
+ }
111177
+ function getBlockedUploadTargetReason(relativePath) {
111178
+ if (!isBlockedUploadTargetPath(relativePath))
111179
+ return null;
111180
+ return "Cannot upload to this location (.git and sensitive paths are blocked)";
111181
+ }
111182
+ var BLOCKED_SEGMENT_NAMES;
111183
+ var init_workspace_upload_path_policy = __esm(() => {
111184
+ BLOCKED_SEGMENT_NAMES = new Set([".git", "secrets", ".aws"]);
111185
+ });
111186
+
111187
+ // ../../services/backend/src/domain/constants/workspace-upload.ts
111188
+ var MAX_WORKSPACE_UPLOAD_BYTES;
111189
+ var init_workspace_upload = __esm(() => {
111190
+ MAX_WORKSPACE_UPLOAD_BYTES = 70 * 1024 * 1024;
111191
+ });
111192
+
110910
111193
  // src/daemon/entry/files/file-write-errors.ts
110911
111194
  function unsupportedFileWriteOperationMessage(operation) {
110912
111195
  return `Unsupported file write operation "${operation}". ` + "Please upgrade chatroom-cli to the latest version and restart the machine daemon " + "(e.g. npm install -g chatroom-cli@latest).";
110913
111196
  }
110914
111197
 
111198
+ // src/daemon/entry/files/file-write-request-priority.ts
111199
+ function compareFileWriteRequestPriority(a, b) {
111200
+ const rank = (r) => {
111201
+ if (FAST_OPERATIONS.has(r.operation))
111202
+ return 0;
111203
+ if (r.storageId)
111204
+ return 2;
111205
+ return 1;
111206
+ };
111207
+ const diff8 = rank(a) - rank(b);
111208
+ if (diff8 !== 0)
111209
+ return diff8;
111210
+ return a._id.localeCompare(b._id);
111211
+ }
111212
+ function sortPendingFileWriteRequests(requests) {
111213
+ return [...requests].sort(compareFileWriteRequestPriority);
111214
+ }
111215
+ var FAST_OPERATIONS;
111216
+ var init_file_write_request_priority = __esm(() => {
111217
+ FAST_OPERATIONS = new Set([
111218
+ "delete",
111219
+ "mkdir",
111220
+ "rename"
111221
+ ]);
111222
+ });
111223
+
110915
111224
  // src/daemon/entry/files/file-write-fulfillment.ts
110916
111225
  import { access as access4, mkdir as mkdir10, rename as rename4, rm as rm4, writeFile as writeFile9 } from "node:fs/promises";
110917
111226
  import { dirname as dirname11 } from "node:path";
@@ -110928,20 +111237,43 @@ function isTerminalFileWriteError(errorMessage) {
110928
111237
  "Target path is required for rename",
110929
111238
  "Rename target must differ from source path",
110930
111239
  "Directory already exists",
110931
- "Workspace not registered for this machine"
111240
+ "Workspace not registered for this machine",
111241
+ "Cannot upload to this location (.git and sensitive paths are blocked)",
111242
+ "Invalid attachment path"
110932
111243
  ]);
110933
111244
  if (errorMessage.startsWith("Unsupported file write operation"))
110934
111245
  return true;
110935
111246
  return terminalMessages.has(errorMessage);
110936
111247
  }
110937
- async function completeWriteRequest(session2, requestId, result) {
111248
+ function getChatAttachmentValidationError(request2) {
111249
+ if (request2.uploadKind === "chatAttachment") {
111250
+ return getInvalidChatAttachmentUploadPathReason(request2.filePath);
111251
+ }
111252
+ if (request2.storageId && request2.filePath.replace(/\\/g, "/").startsWith(CHAT_ATTACHMENT_RESERVED_PREFIX)) {
111253
+ return getInvalidChatAttachmentUploadPathReason(request2.filePath);
111254
+ }
111255
+ return null;
111256
+ }
111257
+ async function completeWriteRequest(session2, requestId, revision, result) {
110938
111258
  await session2.backend.mutation(api.workspaceFiles.completeFileWriteRequest, {
110939
111259
  sessionId: session2.sessionId,
110940
111260
  requestId,
111261
+ revision,
110941
111262
  status: result.status,
110942
111263
  errorMessage: result.status === "error" ? result.errorMessage : undefined
110943
111264
  });
110944
111265
  }
111266
+ async function claimWriteRequest(session2, request2) {
111267
+ const result = await session2.backend.mutation(api.workspaceFiles.claimFileWriteRequest, {
111268
+ sessionId: session2.sessionId,
111269
+ requestId: request2._id,
111270
+ expectedRevision: request2.revision
111271
+ });
111272
+ if (result.status !== "claimed") {
111273
+ return { status: "stale" };
111274
+ }
111275
+ return result;
111276
+ }
110945
111277
  async function fileExistsAt(absolutePath) {
110946
111278
  return access4(absolutePath).then(() => true).catch(() => false);
110947
111279
  }
@@ -110949,7 +111281,31 @@ function decodeWritePayload(request2) {
110949
111281
  if (!request2.data) {
110950
111282
  return { ok: false, errorMessage: "Missing file data" };
110951
111283
  }
110952
- return gunzipBase64Payload(request2.data.content, MAX_CONTENT_BYTES2);
111284
+ return gunzipBase64Payload(request2.data.content, MAX_INLINE_CONTENT_BYTES);
111285
+ }
111286
+ async function fetchStoragePayload(session2, request2) {
111287
+ const storageUrl = await session2.backend.query(api.workspaceFiles.getWriteRequestStorageUrl, {
111288
+ sessionId: session2.sessionId,
111289
+ requestId: request2._id
111290
+ });
111291
+ if (!storageUrl) {
111292
+ return { ok: false, errorMessage: "Missing upload data" };
111293
+ }
111294
+ const response = await fetch(storageUrl);
111295
+ if (!response.ok) {
111296
+ return { ok: false, errorMessage: "Failed to fetch upload" };
111297
+ }
111298
+ const content = Buffer.from(await response.arrayBuffer());
111299
+ if (content.length > MAX_UPLOAD_CONTENT_BYTES) {
111300
+ return { ok: false, errorMessage: "File content too large" };
111301
+ }
111302
+ return { ok: true, content };
111303
+ }
111304
+ async function resolveWritePayload(session2, request2) {
111305
+ if (request2.data?.content) {
111306
+ return decodeWritePayload(request2);
111307
+ }
111308
+ return fetchStoragePayload(session2, request2);
110953
111309
  }
110954
111310
  async function validateWriteOperation(operation, absolutePath) {
110955
111311
  const exists3 = await fileExistsAt(absolutePath);
@@ -110967,23 +111323,38 @@ async function validateWriteOperation(operation, absolutePath) {
110967
111323
  async function writePayloadToDisk(absolutePath, operation, content) {
110968
111324
  if (operation === "create") {
110969
111325
  await mkdir10(dirname11(absolutePath), { recursive: true });
111326
+ await writeFile9(absolutePath, content, { flag: "wx" });
111327
+ return;
110970
111328
  }
110971
111329
  await writeFile9(absolutePath, content);
110972
111330
  }
110973
111331
  async function fulfillOneFileWriteRequest(session2, request2) {
111332
+ const claim = await claimWriteRequest(session2, request2);
111333
+ if (claim.status !== "claimed") {
111334
+ return;
111335
+ }
111336
+ const claimed = claim.request;
110974
111337
  const startTime = Date.now();
110975
- const { workingDir, filePath, operation } = request2;
111338
+ const { workingDir, filePath, operation } = claimed;
110976
111339
  const registered2 = await assertRegisteredWorkingDir(session2, workingDir);
110977
111340
  if (!registered2.ok) {
110978
- await completeWriteRequest(session2, request2._id, {
111341
+ await completeWriteRequest(session2, claimed._id, claimed.revision, {
110979
111342
  status: "error",
110980
111343
  errorMessage: registered2.error
110981
111344
  });
110982
111345
  return;
110983
111346
  }
111347
+ const attachmentReason = getChatAttachmentValidationError(claimed);
111348
+ if (attachmentReason) {
111349
+ await completeWriteRequest(session2, claimed._id, claimed.revision, {
111350
+ status: "error",
111351
+ errorMessage: attachmentReason
111352
+ });
111353
+ return;
111354
+ }
110984
111355
  const resolved = await resolvePathWithinWorkspace(workingDir, filePath);
110985
111356
  if (!resolved.ok) {
110986
- await completeWriteRequest(session2, request2._id, {
111357
+ await completeWriteRequest(session2, claimed._id, claimed.revision, {
110987
111358
  status: "error",
110988
111359
  errorMessage: resolved.error
110989
111360
  });
@@ -110991,23 +111362,23 @@ async function fulfillOneFileWriteRequest(session2, request2) {
110991
111362
  }
110992
111363
  try {
110993
111364
  if (operation === "rename") {
110994
- if (!request2.targetFilePath) {
110995
- await completeWriteRequest(session2, request2._id, {
111365
+ if (!claimed.targetFilePath) {
111366
+ await completeWriteRequest(session2, claimed._id, claimed.revision, {
110996
111367
  status: "error",
110997
111368
  errorMessage: "Target path is required for rename"
110998
111369
  });
110999
111370
  return;
111000
111371
  }
111001
- if (request2.targetFilePath === filePath) {
111002
- await completeWriteRequest(session2, request2._id, {
111372
+ if (claimed.targetFilePath === filePath) {
111373
+ await completeWriteRequest(session2, claimed._id, claimed.revision, {
111003
111374
  status: "error",
111004
111375
  errorMessage: "Rename target must differ from source path"
111005
111376
  });
111006
111377
  return;
111007
111378
  }
111008
- const targetResolved = await resolvePathWithinWorkspace(workingDir, request2.targetFilePath);
111379
+ const targetResolved = await resolvePathWithinWorkspace(workingDir, claimed.targetFilePath);
111009
111380
  if (!targetResolved.ok) {
111010
- await completeWriteRequest(session2, request2._id, {
111381
+ await completeWriteRequest(session2, claimed._id, claimed.revision, {
111011
111382
  status: "error",
111012
111383
  errorMessage: targetResolved.error
111013
111384
  });
@@ -111015,7 +111386,7 @@ async function fulfillOneFileWriteRequest(session2, request2) {
111015
111386
  }
111016
111387
  const sourceExists = await fileExistsAt(resolved.absolutePath);
111017
111388
  if (!sourceExists) {
111018
- await completeWriteRequest(session2, request2._id, {
111389
+ await completeWriteRequest(session2, claimed._id, claimed.revision, {
111019
111390
  status: "error",
111020
111391
  errorMessage: "File does not exist"
111021
111392
  });
@@ -111023,7 +111394,7 @@ async function fulfillOneFileWriteRequest(session2, request2) {
111023
111394
  }
111024
111395
  const targetExists = await fileExistsAt(targetResolved.absolutePath);
111025
111396
  if (targetExists) {
111026
- await completeWriteRequest(session2, request2._id, {
111397
+ await completeWriteRequest(session2, claimed._id, claimed.revision, {
111027
111398
  status: "error",
111028
111399
  errorMessage: "Target path already exists"
111029
111400
  });
@@ -111031,29 +111402,29 @@ async function fulfillOneFileWriteRequest(session2, request2) {
111031
111402
  }
111032
111403
  await mkdir10(dirname11(targetResolved.absolutePath), { recursive: true });
111033
111404
  await rename4(resolved.absolutePath, targetResolved.absolutePath);
111034
- await completeWriteRequest(session2, request2._id, { status: "done" });
111405
+ await completeWriteRequest(session2, claimed._id, claimed.revision, { status: "done" });
111035
111406
  const elapsed4 = Date.now() - startTime;
111036
- console.log(`[${formatTimestamp()}] ✏️ File rename fulfilled: ${filePath} → ${request2.targetFilePath} (${elapsed4}ms)`);
111407
+ console.log(`[${formatTimestamp()}] ✏️ File rename fulfilled: ${filePath} → ${claimed.targetFilePath} (${elapsed4}ms)`);
111037
111408
  return;
111038
111409
  }
111039
111410
  if (operation === "mkdir") {
111040
111411
  const exists3 = await fileExistsAt(resolved.absolutePath);
111041
111412
  if (exists3) {
111042
- await completeWriteRequest(session2, request2._id, {
111413
+ await completeWriteRequest(session2, claimed._id, claimed.revision, {
111043
111414
  status: "error",
111044
111415
  errorMessage: "Directory already exists"
111045
111416
  });
111046
111417
  return;
111047
111418
  }
111048
111419
  await mkdir10(resolved.absolutePath, { recursive: true });
111049
- await completeWriteRequest(session2, request2._id, { status: "done" });
111420
+ await completeWriteRequest(session2, claimed._id, claimed.revision, { status: "done" });
111050
111421
  const elapsed4 = Date.now() - startTime;
111051
111422
  console.log(`[${formatTimestamp()}] ✏️ Directory mkdir fulfilled: ${filePath} (${elapsed4}ms)`);
111052
111423
  return;
111053
111424
  }
111054
111425
  if (operation === "delete") {
111055
111426
  if (filePath === "") {
111056
- await completeWriteRequest(session2, request2._id, {
111427
+ await completeWriteRequest(session2, claimed._id, claimed.revision, {
111057
111428
  status: "error",
111058
111429
  errorMessage: "Cannot delete workspace root"
111059
111430
  });
@@ -111061,66 +111432,83 @@ async function fulfillOneFileWriteRequest(session2, request2) {
111061
111432
  }
111062
111433
  const operationCheck2 = await validateWriteOperation(operation, resolved.absolutePath);
111063
111434
  if (!operationCheck2.ok) {
111064
- await completeWriteRequest(session2, request2._id, {
111435
+ await completeWriteRequest(session2, claimed._id, claimed.revision, {
111065
111436
  status: "error",
111066
111437
  errorMessage: operationCheck2.errorMessage
111067
111438
  });
111068
111439
  return;
111069
111440
  }
111070
111441
  await rm4(resolved.absolutePath, { recursive: true, force: false });
111071
- await completeWriteRequest(session2, request2._id, { status: "done" });
111442
+ await completeWriteRequest(session2, claimed._id, claimed.revision, { status: "done" });
111072
111443
  const elapsed4 = Date.now() - startTime;
111073
111444
  console.log(`[${formatTimestamp()}] ✏️ File delete fulfilled: ${filePath} (${elapsed4}ms)`);
111074
111445
  return;
111075
111446
  }
111076
111447
  if (operation !== "create" && operation !== "update") {
111077
- await completeWriteRequest(session2, request2._id, {
111448
+ await completeWriteRequest(session2, claimed._id, claimed.revision, {
111078
111449
  status: "error",
111079
111450
  errorMessage: unsupportedFileWriteOperationMessage(operation)
111080
111451
  });
111081
111452
  return;
111082
111453
  }
111083
- const payload = decodeWritePayload(request2);
111454
+ if (claimed.storageId) {
111455
+ const blockedReason = getBlockedUploadTargetReason(filePath);
111456
+ if (blockedReason) {
111457
+ await completeWriteRequest(session2, claimed._id, claimed.revision, {
111458
+ status: "error",
111459
+ errorMessage: blockedReason
111460
+ });
111461
+ return;
111462
+ }
111463
+ }
111464
+ const payload = await resolveWritePayload(session2, claimed);
111084
111465
  if (!payload.ok) {
111085
- await completeWriteRequest(session2, request2._id, {
111466
+ await completeWriteRequest(session2, claimed._id, claimed.revision, {
111086
111467
  status: "error",
111087
111468
  errorMessage: payload.errorMessage
111088
111469
  });
111470
+ console.warn(`[${formatTimestamp()}] ⚠️ File write failed for ${filePath}: ${payload.errorMessage}`);
111089
111471
  return;
111090
111472
  }
111091
111473
  const operationCheck = await validateWriteOperation(operation, resolved.absolutePath);
111092
111474
  if (!operationCheck.ok) {
111093
- await completeWriteRequest(session2, request2._id, {
111475
+ await completeWriteRequest(session2, claimed._id, claimed.revision, {
111094
111476
  status: "error",
111095
111477
  errorMessage: operationCheck.errorMessage
111096
111478
  });
111097
111479
  return;
111098
111480
  }
111099
111481
  await writePayloadToDisk(resolved.absolutePath, operation, payload.content);
111100
- await completeWriteRequest(session2, request2._id, { status: "done" });
111482
+ await completeWriteRequest(session2, claimed._id, claimed.revision, { status: "done" });
111101
111483
  const elapsed3 = Date.now() - startTime;
111102
111484
  console.log(`[${formatTimestamp()}] ✏️ File write fulfilled: ${filePath} (${operation}, ${(payload.content.length / 1024).toFixed(1)}KB, ${elapsed3}ms)`);
111103
111485
  } catch (err) {
111104
111486
  const message = err instanceof Error ? err.message : "Write failed";
111105
- if (isTerminalFileWriteError(message)) {
111106
- await completeWriteRequest(session2, request2._id, {
111487
+ const normalizedMessage = err.code === "EEXIST" ? "File already exists" : message;
111488
+ if (isTerminalFileWriteError(normalizedMessage)) {
111489
+ await completeWriteRequest(session2, claimed._id, claimed.revision, {
111107
111490
  status: "error",
111108
- errorMessage: message
111491
+ errorMessage: normalizedMessage
111109
111492
  });
111110
- console.warn(`[${formatTimestamp()}] ⚠️ File write failed for ${filePath}: ${message}`);
111493
+ console.warn(`[${formatTimestamp()}] ⚠️ File write failed for ${filePath}: ${normalizedMessage}`);
111111
111494
  return;
111112
111495
  }
111113
111496
  console.warn(`[${formatTimestamp()}] ⚠️ File write transient failure for ${filePath}: ${message} (will retry)`);
111114
111497
  }
111115
111498
  }
111116
- var MAX_CONTENT_BYTES2, fulfillFileWriteRequestsEffect;
111499
+ var MAX_INLINE_CONTENT_BYTES, MAX_UPLOAD_CONTENT_BYTES, CHAT_ATTACHMENT_RESERVED_PREFIX = ".chatroom/downloads/attachments/", fulfillFileWriteRequestsEffect;
111117
111500
  var init_file_write_fulfillment = __esm(() => {
111501
+ init_chat_attachment_upload_path();
111502
+ init_workspace_upload_path_policy();
111503
+ init_workspace_upload();
111118
111504
  init_esm();
111505
+ init_file_write_request_priority();
111119
111506
  init_api3();
111120
111507
  init_assert_registered_working_dir();
111121
111508
  init_workspace_path_security();
111122
111509
  init_daemon_services();
111123
- MAX_CONTENT_BYTES2 = 512 * 1024;
111510
+ MAX_INLINE_CONTENT_BYTES = 512 * 1024;
111511
+ MAX_UPLOAD_CONTENT_BYTES = MAX_WORKSPACE_UPLOAD_BYTES;
111124
111512
  fulfillFileWriteRequestsEffect = exports_Effect.gen(function* () {
111125
111513
  const session2 = yield* DaemonSessionService;
111126
111514
  const requests = yield* exports_Effect.catchAll(exports_Effect.tryPromise(() => session2.backend.query(api.workspaceFiles.getPendingFileWriteRequests, {
@@ -111129,8 +111517,9 @@ var init_file_write_fulfillment = __esm(() => {
111129
111517
  })), () => exports_Effect.succeed([]));
111130
111518
  if (requests.length === 0)
111131
111519
  return;
111132
- console.log(`[${formatTimestamp()}] ✏️ Received ${requests.length} pending file write request(s): ${requests.map((r) => r.filePath).join(", ")}`);
111133
- for (const request2 of requests) {
111520
+ const sortedRequests = sortPendingFileWriteRequests(requests);
111521
+ console.log(`[${formatTimestamp()}] ✏️ Received ${sortedRequests.length} pending file write request(s): ${sortedRequests.map((r) => r.filePath).join(", ")}`);
111522
+ for (const request2 of sortedRequests) {
111134
111523
  yield* exports_Effect.catchAll(exports_Effect.tryPromise(() => fulfillOneFileWriteRequest(session2, request2)), () => exports_Effect.void);
111135
111524
  }
111136
111525
  });
@@ -115842,7 +116231,20 @@ messagesCommand.command("list").description("List messages by sender role or sin
115842
116231
  });
115843
116232
  }
115844
116233
  });
115845
- messagesCommand.command("download").description("Download chatroom message history to local files for reading/grep").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role").option("--format <format>", "Download format (default: linear)", "linear").option("--output-dir <path>", "Output directory (default: .chatroom/downloads/messages/linear/<download-id>)").option("--limit <n>", "Max messages to download (default: 10, max: 5000)").action(async (options) => {
116234
+ messagesCommand.command("anchor").description("Locate the last user message and print proof-of-verification workflow commands").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role").option("--prior-limit <n>", "Prior user messages to show (default: 3, max: 5)", "3").action(async (options) => {
116235
+ await maybeRequireAuth();
116236
+ let parsedPriorLimit = parseInt(options.priorLimit, 10);
116237
+ if (isNaN(parsedPriorLimit) || parsedPriorLimit < 0)
116238
+ parsedPriorLimit = 3;
116239
+ if (parsedPriorLimit > 5)
116240
+ parsedPriorLimit = 5;
116241
+ const { anchorMessages: anchorMessages2 } = await Promise.resolve().then(() => (init_anchor(), exports_anchor));
116242
+ await anchorMessages2(options.chatroomId, {
116243
+ role: options.role,
116244
+ priorLimit: parsedPriorLimit
116245
+ });
116246
+ });
116247
+ messagesCommand.command("download").description("Download chatroom message history to local files for reading/grep").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role").option("--format <format>", "Download format (default: linear)", "linear").option("--output-dir <path>", "Output directory (default: .chatroom/downloads/messages/linear/<download-id>)").option("--limit <n>", "Max messages to download (default: 10, max: 5000)").option("--since-message-id <messageId>", "Download history since this message ID (inclusive, ascending)").action(async (options) => {
115846
116248
  await maybeRequireAuth();
115847
116249
  if (options.format && options.format !== "linear") {
115848
116250
  console.error("❌ Unsupported format. Only --format=linear is supported.");
@@ -115861,7 +116263,8 @@ messagesCommand.command("download").description("Download chatroom message histo
115861
116263
  role: options.role,
115862
116264
  format: "linear",
115863
116265
  outputDir: options.outputDir,
115864
- limit: parsedLimit
116266
+ limit: parsedLimit,
116267
+ sinceMessageId: options.sinceMessageId
115865
116268
  });
115866
116269
  });
115867
116270
  var contextCommand = program2.command("context").description("Manage chatroom context and state (explicit context management)");
@@ -116009,4 +116412,4 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
116009
116412
  });
116010
116413
  program2.parse();
116011
116414
 
116012
- //# debugId=C6C301A0C18AEA2064756E2164756E21
116415
+ //# debugId=B2378E8C8597995864756E2164756E21