runwork 0.28.0 → 0.29.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 (2) hide show
  1. package/dist/index.js +594 -186
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1793,6 +1793,20 @@ function isRelPathIgnored(relPath, sets) {
1793
1793
  }
1794
1794
  return isPathIgnored(path, sets);
1795
1795
  }
1796
+ function isIgnoredDirectory(relDir, sets) {
1797
+ const path = toPosixRelPath(relDir);
1798
+ if (!path || path === ".")
1799
+ return false;
1800
+ for (const ignored of sets.paths) {
1801
+ if (path === ignored || path.startsWith(`${ignored}/`))
1802
+ return true;
1803
+ }
1804
+ for (const segment of path.split("/")) {
1805
+ if (sets.dirs.has(segment))
1806
+ return true;
1807
+ }
1808
+ return false;
1809
+ }
1796
1810
  function toPosixRelPath(relPath) {
1797
1811
  return relPath.includes("\\") ? relPath.replace(/\\/g, "/") : relPath;
1798
1812
  }
@@ -2711,6 +2725,7 @@ var init_registry_data = __esm(() => {
2711
2725
  app: { macos: "Claude", windows: "Claude" },
2712
2726
  deepLink: "claude://claude.ai/new?q={prompt}",
2713
2727
  deepLinkInFolder: "claude://cowork/new?q={prompt}&folder={folder}",
2728
+ deepLinkInFolderSplit: { gapMs: 500 },
2714
2729
  openInFolder: {
2715
2730
  rank: 4,
2716
2731
  instruction: "Open Claude Desktop and add {dir} as a project folder",
@@ -4079,6 +4094,18 @@ var init_types = __esm(() => {
4079
4094
  toSlug = toSkillSlug;
4080
4095
  });
4081
4096
 
4097
+ // ../../shared/spaces/launch-preamble.ts
4098
+ function isRunworkLaunchPreamble(text2) {
4099
+ const trimmed = text2.trim();
4100
+ if (trimmed.startsWith(LAUNCH_IN_FOLDER)) {
4101
+ return trimmed.includes(LAUNCH_FOLDER_IS_SPACE) || trimmed.includes(LAUNCH_CONTINUE_CONVERSATION);
4102
+ }
4103
+ if (trimmed.startsWith(LAUNCH_NO_FOLDER))
4104
+ return trimmed.includes(LAUNCH_SPACE_IN_RUNWORK);
4105
+ return false;
4106
+ }
4107
+ var LAUNCH_IN_FOLDER = "Work in ", LAUNCH_FOLDER_IS_SPACE = ". That folder is ", LAUNCH_CONTINUE_CONVERSATION = ". Continue the conversation we had there", LAUNCH_NO_FOLDER = "Work on ", LAUNCH_SPACE_IN_RUNWORK = "space in Runwork";
4108
+
4082
4109
  // src/agents/utils/session-digest.ts
4083
4110
  import { posix, win32 } from "path";
4084
4111
  function pathFlavourOf(path) {
@@ -4253,6 +4280,12 @@ function buildAssetMarkers(acc, epochMs) {
4253
4280
  }
4254
4281
  return markers;
4255
4282
  }
4283
+ function isInjectedUserText(text2) {
4284
+ const trimmed = text2.trim();
4285
+ if (!trimmed)
4286
+ return true;
4287
+ return SKIP_PREFIXES.some((p) => trimmed.startsWith(p)) || isRunworkLaunchPreamble(trimmed);
4288
+ }
4256
4289
  function formatCombinedDigest(sessions, opts = { days: 7 }) {
4257
4290
  const sorted = [...sessions].sort((a, b) => (a.start ?? "").localeCompare(b.start ?? ""));
4258
4291
  const lines = [];
@@ -4767,6 +4800,7 @@ function parseMcpPrefixedToolName(name) {
4767
4800
 
4768
4801
  // src/agents/utils/session-listing.ts
4769
4802
  import { openSync, readSync, closeSync, fstatSync } from "fs";
4803
+ import { StringDecoder } from "string_decoder";
4770
4804
  function isReflectionOwnSession(titleOrFirstMessage) {
4771
4805
  if (!titleOrFirstMessage)
4772
4806
  return false;
@@ -4839,7 +4873,44 @@ function titleFromText(text2) {
4839
4873
  return null;
4840
4874
  return oneLine.length > MAX_TITLE_CHARS ? oneLine.slice(0, MAX_TITLE_CHARS - 3) + "..." : oneLine;
4841
4875
  }
4842
- var HEAD_BYTES, CONVERSATION_PROMPT_MARKER = "# Conversation reflection:", WEEKLY_PROMPT_MARKER = "# Weekly reflection:", TRIAGE_PROMPT_MARKER = "# Reflection triage:", REFLECTION_SESSION_MARKERS, MAX_TITLE_CHARS = 80, TAIL_BYTES;
4876
+ function readTranscriptRecords(path, onRecord, maxBytes = DEEP_SCAN_BYTES) {
4877
+ let fd = null;
4878
+ try {
4879
+ fd = openSync(path, "r");
4880
+ const buf = Buffer.alloc(Math.min(HEAD_BYTES, maxBytes));
4881
+ const decoder = new StringDecoder("utf-8");
4882
+ let position = 0;
4883
+ let carry = "";
4884
+ while (position < maxBytes) {
4885
+ const bytesRead = readSync(fd, buf, 0, Math.min(buf.length, maxBytes - position), position);
4886
+ if (bytesRead <= 0)
4887
+ break;
4888
+ position += bytesRead;
4889
+ const lines = (carry + decoder.write(buf.subarray(0, bytesRead))).split(`
4890
+ `);
4891
+ carry = lines.pop() ?? "";
4892
+ for (const raw of lines) {
4893
+ if (!raw.trim())
4894
+ continue;
4895
+ let record;
4896
+ try {
4897
+ record = JSON.parse(raw);
4898
+ } catch {
4899
+ continue;
4900
+ }
4901
+ if (onRecord(record))
4902
+ return;
4903
+ }
4904
+ }
4905
+ } catch {} finally {
4906
+ if (fd !== null) {
4907
+ try {
4908
+ closeSync(fd);
4909
+ } catch {}
4910
+ }
4911
+ }
4912
+ }
4913
+ var HEAD_BYTES, CONVERSATION_PROMPT_MARKER = "# Conversation reflection:", WEEKLY_PROMPT_MARKER = "# Weekly reflection:", TRIAGE_PROMPT_MARKER = "# Reflection triage:", REFLECTION_SESSION_MARKERS, MAX_TITLE_CHARS = 80, TAIL_BYTES, DEEP_SCAN_BYTES;
4843
4914
  var init_session_listing = __esm(() => {
4844
4915
  HEAD_BYTES = 64 * 1024;
4845
4916
  REFLECTION_SESSION_MARKERS = [
@@ -4848,12 +4919,60 @@ var init_session_listing = __esm(() => {
4848
4919
  TRIAGE_PROMPT_MARKER
4849
4920
  ];
4850
4921
  TAIL_BYTES = 16 * 1024;
4922
+ DEEP_SCAN_BYTES = 1536 * 1024;
4851
4923
  });
4852
4924
 
4853
4925
  // src/agents/claude/jsonl-listing.ts
4854
- function scanClaudeJsonlHead(head) {
4855
- const result = { firstTimestamp: null, title: null, cwd: null };
4856
- let firstUserText = null;
4926
+ function claudeTitleCandidate(record) {
4927
+ if (record.type === "custom-title" && typeof record.customTitle === "string" && record.customTitle.trim()) {
4928
+ return { source: "custom-title", text: record.customTitle };
4929
+ }
4930
+ if (record.type === "ai-title" && typeof record.aiTitle === "string" && record.aiTitle.trim()) {
4931
+ return { source: "ai-title", text: record.aiTitle };
4932
+ }
4933
+ if (record.type !== "user")
4934
+ return null;
4935
+ const message = record.message;
4936
+ if (!message || message.role !== "user")
4937
+ return null;
4938
+ const texts = [];
4939
+ if (typeof message.content === "string")
4940
+ texts.push(message.content);
4941
+ else if (Array.isArray(message.content)) {
4942
+ for (const item of message.content) {
4943
+ const it = item;
4944
+ if (it && it.type === "text" && typeof it.text === "string")
4945
+ texts.push(it.text);
4946
+ }
4947
+ }
4948
+ for (const text2 of texts) {
4949
+ const trimmed = text2.trim();
4950
+ if (isInjectedUserText(trimmed))
4951
+ continue;
4952
+ return { source: "user-text", text: trimmed };
4953
+ }
4954
+ return null;
4955
+ }
4956
+ function absorbClaudeRecord(state, o, rankTitles) {
4957
+ const { result } = state;
4958
+ if (!result.firstTimestamp && typeof o.timestamp === "string")
4959
+ result.firstTimestamp = o.timestamp;
4960
+ if (!result.cwd && typeof o.cwd === "string")
4961
+ result.cwd = o.cwd;
4962
+ if (!result.surface && typeof o.entrypoint === "string")
4963
+ result.surface = o.entrypoint;
4964
+ const candidate = claudeTitleCandidate(o);
4965
+ if (candidate && (rankTitles ? TITLE_RANK[candidate.source] < state.bestRank : result.title === null)) {
4966
+ state.bestRank = TITLE_RANK[candidate.source];
4967
+ result.title = titleFromText(candidate.text);
4968
+ }
4969
+ const complete = Boolean(result.firstTimestamp && result.cwd && result.surface);
4970
+ return rankTitles ? state.bestRank === 0 && complete : result.title !== null && complete;
4971
+ }
4972
+ function emptyState() {
4973
+ return { result: { firstTimestamp: null, title: null, cwd: null }, bestRank: Number.POSITIVE_INFINITY };
4974
+ }
4975
+ function scanHeadInto(state, head) {
4857
4976
  for (const line of head.split(`
4858
4977
  `)) {
4859
4978
  if (!line.trim())
@@ -4864,47 +4983,30 @@ function scanClaudeJsonlHead(head) {
4864
4983
  } catch {
4865
4984
  continue;
4866
4985
  }
4867
- if (!result.firstTimestamp && typeof o.timestamp === "string")
4868
- result.firstTimestamp = o.timestamp;
4869
- if (!result.cwd && typeof o.cwd === "string")
4870
- result.cwd = o.cwd;
4871
- if (!result.surface && typeof o.entrypoint === "string")
4872
- result.surface = o.entrypoint;
4873
- if (!result.title && o.type === "ai-title" && typeof o.aiTitle === "string") {
4874
- result.title = titleFromText(o.aiTitle);
4875
- }
4876
- if (firstUserText === null && o.type === "user") {
4877
- const message = o.message;
4878
- if (message && message.role === "user") {
4879
- const texts = [];
4880
- if (typeof message.content === "string")
4881
- texts.push(message.content);
4882
- else if (Array.isArray(message.content)) {
4883
- for (const item of message.content) {
4884
- const it = item;
4885
- if (it && it.type === "text" && typeof it.text === "string")
4886
- texts.push(it.text);
4887
- }
4888
- }
4889
- for (const text2 of texts) {
4890
- const trimmed = text2.trim();
4891
- if (!trimmed || SKIP_PREFIXES.some((p) => trimmed.startsWith(p)))
4892
- continue;
4893
- firstUserText = trimmed;
4894
- break;
4895
- }
4896
- }
4897
- }
4898
- if (result.title && result.firstTimestamp && result.cwd && result.surface)
4899
- break;
4986
+ if (absorbClaudeRecord(state, o, true))
4987
+ return true;
4900
4988
  }
4901
- if (!result.title && firstUserText)
4902
- result.title = titleFromText(firstUserText);
4989
+ return false;
4990
+ }
4991
+ function scanClaudeTranscript(path, head, maxBytes) {
4992
+ const state = emptyState();
4993
+ const { result } = state;
4994
+ if (scanHeadInto(state, head))
4995
+ return result;
4996
+ if (result.title && result.firstTimestamp && result.cwd && result.surface)
4997
+ return result;
4998
+ readTranscriptRecords(path, (o) => absorbClaudeRecord(state, o, false), maxBytes);
4903
4999
  return result;
4904
5000
  }
5001
+ var TITLE_RANK;
4905
5002
  var init_jsonl_listing = __esm(() => {
4906
5003
  init_session_listing();
4907
5004
  init_session_digest();
5005
+ TITLE_RANK = {
5006
+ "custom-title": 0,
5007
+ "ai-title": 1,
5008
+ "user-text": 2
5009
+ };
4908
5010
  });
4909
5011
 
4910
5012
  // src/agents/utils/skill-name.ts
@@ -5080,23 +5182,39 @@ function spaceKindsSentence() {
5080
5182
  function sentenceName(plural) {
5081
5183
  return /^[A-Z]{2,}\b/.test(plural) ? plural : plural.toLowerCase();
5082
5184
  }
5083
- var SPACE_KINDS, BY_ID;
5185
+ var SPACE_TABS, APPS_AND_AUTOMATIONS, SPACE_KINDS, TAB_BY_NAME, BY_ID;
5084
5186
  var init_kinds = __esm(() => {
5187
+ SPACE_TABS = [
5188
+ { id: "appsAuto", label: "Apps and automations" },
5189
+ { id: "skills", label: "Skills" },
5190
+ { id: "files", label: "Files" },
5191
+ { id: "integ", label: "Integrations" },
5192
+ { id: "conversations", label: "Chats" }
5193
+ ];
5194
+ APPS_AND_AUTOMATIONS = SPACE_TABS[0];
5085
5195
  SPACE_KINDS = [
5086
- { id: "app", singular: "app", plural: "Apps", tab: "apps" },
5196
+ { id: "app", singular: "app", plural: "Apps", tab: "appsAuto" },
5087
5197
  { id: "skill", singular: "skill", plural: "Skills", tab: "skills" },
5088
- { id: "agent", singular: "agent", plural: "Agents", tab: "agents" },
5198
+ { id: "agent", singular: "agent", plural: "Agents", tab: "appsAuto" },
5089
5199
  { id: "file", singular: "file", plural: "Files", tab: "files" },
5090
5200
  { id: "conversation", singular: "chat", plural: "Chats", tab: "conversations" },
5091
- { id: "data", singular: "entity", plural: "Data", tab: "data" },
5092
- { id: "fileStorage", singular: "file storage", plural: "File storage", tab: "data" },
5093
- { id: "component", singular: "component", plural: "Components", tab: "data" },
5094
- { id: "schedule", singular: "scheduled job", plural: "Scheduled work", tab: "auto" },
5095
- { id: "workflow", singular: "workflow", plural: "Workflows", tab: "auto" },
5096
- { id: "endpoint", singular: "public endpoint", plural: "Endpoints", tab: "auto" },
5201
+ { id: "data", singular: "entity", plural: "Data", tab: "appsAuto" },
5202
+ { id: "fileStorage", singular: "file storage", plural: "File storage", tab: "appsAuto" },
5203
+ { id: "component", singular: "component", plural: "Components", tab: "appsAuto" },
5204
+ { id: "schedule", singular: "scheduled job", plural: "Scheduled work", tab: "appsAuto" },
5205
+ { id: "workflow", singular: "workflow", plural: "Workflows", tab: "appsAuto" },
5206
+ { id: "endpoint", singular: "public endpoint", plural: "Endpoints", tab: "appsAuto" },
5097
5207
  { id: "integration", singular: "integration", plural: "Integrations", tab: "integ" },
5098
5208
  { id: "mcpServer", singular: "MCP server", plural: "MCP servers", tab: "integ" }
5099
5209
  ];
5210
+ TAB_BY_NAME = new Map([
5211
+ ...SPACE_TABS.map((tab) => [tab.id, tab.id]),
5212
+ ["chats", "conversations"],
5213
+ ["apps", "appsAuto"],
5214
+ ["auto", "appsAuto"],
5215
+ ["agents", "appsAuto"],
5216
+ ["data", "appsAuto"]
5217
+ ]);
5100
5218
  BY_ID = new Map(SPACE_KINDS.map((k) => [k.id, k]));
5101
5219
  });
5102
5220
 
@@ -5175,7 +5293,7 @@ function annotateSpacesMcp(tree) {
5175
5293
  visit(root);
5176
5294
  return tree;
5177
5295
  }
5178
- var SPACE_DEFINITION = 'A space is where one piece of work lives: its files, how your AI should handle it, the apps and automations around it, and the conversations. Start a chat from a space and the AI already knows the job. Share a file and a teammate picks it up where you left it. Think "Q3 board deck", "Customer onboarding", "Website redesign".', SPACE_API_PREFIX = "/api/workspaces/:id/spaces", SPACE_JOBS, SPACE_LAUNCH_SENTENCE = "Start a chat in a space with the `runwork://launch?space=<space id>` link on the person's computer, or ask the `space_launch_link` tool for that URL.", machineOnlySpaceJobs = () => SPACE_JOBS.filter((j) => j.mcp === null), SPACES_ROUTING_SENTENCE = "Keep a document the team should have in a space (`space_list`, then `space_write_file`; the CLI twins are `runwork spaces list` and `runwork spaces put`, and the route is `POST /api/workspaces/:id/spaces/:spaceId/files`), not in a local file or a storage bucket.", SPACES_GROUP_DESCRIPTION, HELP_FOOTER_DOORS, SPACES_SUBCOMMAND_MCP, TEAM_BLOCK_RUNWORK_LINE = "Runwork is connected here: the `runwork` block above says what it can do, and any folder mapped to a space carries its own `runwork-space` block in its AGENTS.md.";
5296
+ var SPACE_DEFINITION = 'A space is where one piece of work lives: its files, how your AI should handle it, the apps and automations around it, and the conversations. Start a chat from a space and the AI already knows the job. Share a file and a teammate picks it up where you left it. Think "Q3 board deck", "Customer onboarding", "Website redesign".', SPACE_API_PREFIX = "/api/workspaces/:id/spaces", SPACE_JOBS, SPACE_LAUNCH_SENTENCE = "Start a chat in a space with the `runwork://launch?space=<space id>` link on the person's computer, or ask the `space_launch_link` tool for that URL.", machineOnlySpaceJobs = () => SPACE_JOBS.filter((j) => j.mcp === null), SPACES_GROUP_DESCRIPTION, HELP_FOOTER_DOORS, SPACES_SUBCOMMAND_MCP, TEAM_BLOCK_RUNWORK_LINE = "Runwork is connected here: the `runwork` block above says what it can do, and any folder mapped to a space carries its own `runwork-space` block in its AGENTS.md.";
5179
5297
  var init_spaces = __esm(() => {
5180
5298
  init_kinds();
5181
5299
  SPACE_JOBS = [
@@ -6521,7 +6639,7 @@ ${instructions}`;
6521
6639
  const head = readFileHead(filePath);
6522
6640
  if (head === null)
6523
6641
  continue;
6524
- const scanned = scanClaudeJsonlHead(head);
6642
+ const scanned = scanClaudeTranscript(filePath, head);
6525
6643
  if (isReflectionOwnSession(scanned.title))
6526
6644
  continue;
6527
6645
  const sessionId = file.slice(0, -".jsonl".length);
@@ -10174,7 +10292,7 @@ function extractCodexRolloutSession(raw, agentSlug) {
10174
10292
  digest.assetMarkers = buildAssetMarkers(assetAcc, epochMs);
10175
10293
  return digest;
10176
10294
  }
10177
- var CODEX_SKIP_PREFIXES, CODEX_UNKNOWN_PROJECT = "codex";
10295
+ var CODEX_ASSESSOR_PROMPT_PREFIX = "The following is the Codex agent history", CODEX_SKIP_PREFIXES, CODEX_UNKNOWN_PROJECT = "codex";
10178
10296
  var init_rollout_digest = __esm(() => {
10179
10297
  init_session_digest();
10180
10298
  CODEX_SKIP_PREFIXES = [
@@ -10185,7 +10303,9 @@ var init_rollout_digest = __esm(() => {
10185
10303
  "<environment_context",
10186
10304
  "<system",
10187
10305
  "AGENTS.md instructions",
10188
- "<recommended_plugins"
10306
+ "<recommended_plugins",
10307
+ CODEX_ASSESSOR_PROMPT_PREFIX,
10308
+ ">>> TRANSCRIPT START"
10189
10309
  ];
10190
10310
  });
10191
10311
 
@@ -10379,6 +10499,50 @@ var init_analyst = __esm(() => {
10379
10499
  function codexUuidFromRolloutName(basenameNoExt) {
10380
10500
  return UUID_TAIL.exec(basenameNoExt)?.[1] ?? null;
10381
10501
  }
10502
+ function codexTitleCandidate(record) {
10503
+ const p = record.payload;
10504
+ if (!p || typeof p !== "object")
10505
+ return null;
10506
+ if (record.type !== "response_item" || p.type !== "message" || p.role !== "user" || !Array.isArray(p.content))
10507
+ return null;
10508
+ const isAssessorTurn = p.content.some((item) => {
10509
+ const it = item;
10510
+ return typeof it.text === "string" && it.text.trim().startsWith(CODEX_ASSESSOR_PROMPT_PREFIX);
10511
+ });
10512
+ if (isAssessorTurn)
10513
+ return null;
10514
+ for (const item of p.content) {
10515
+ const it = item;
10516
+ if (it.type !== "input_text" && it.type !== "text")
10517
+ continue;
10518
+ if (typeof it.text !== "string")
10519
+ continue;
10520
+ const trimmed = it.text.trim();
10521
+ if (CODEX_SKIP_PREFIXES.some((pre) => trimmed.startsWith(pre)) || isInjectedUserText(trimmed))
10522
+ continue;
10523
+ return { source: "user-text", text: trimmed };
10524
+ }
10525
+ return null;
10526
+ }
10527
+ function absorbCodexRecord(result, o) {
10528
+ if (!result.firstTimestamp && typeof o.timestamp === "string")
10529
+ result.firstTimestamp = o.timestamp;
10530
+ const p = o.payload;
10531
+ if (p && typeof p === "object" && o.type === "session_meta") {
10532
+ if (typeof p.cwd === "string" && !result.cwd)
10533
+ result.cwd = p.cwd;
10534
+ if (typeof p.id === "string" && !result.sessionId)
10535
+ result.sessionId = p.id;
10536
+ if (typeof p.originator === "string" && !result.surface)
10537
+ result.surface = p.originator;
10538
+ }
10539
+ if (!result.title) {
10540
+ const candidate = codexTitleCandidate(o);
10541
+ if (candidate)
10542
+ result.title = titleFromText(candidate.text);
10543
+ }
10544
+ return Boolean(result.title && result.firstTimestamp && result.cwd);
10545
+ }
10382
10546
  function scanCodexRolloutHead(head) {
10383
10547
  const result = { firstTimestamp: null, title: null, cwd: null };
10384
10548
  for (const line of head.split(`
@@ -10391,36 +10555,18 @@ function scanCodexRolloutHead(head) {
10391
10555
  } catch {
10392
10556
  continue;
10393
10557
  }
10394
- if (!result.firstTimestamp && typeof o.timestamp === "string")
10395
- result.firstTimestamp = o.timestamp;
10396
- const p = o.payload;
10397
- if (!p || typeof p !== "object")
10398
- continue;
10399
- if (o.type === "session_meta") {
10400
- if (typeof p.cwd === "string" && !result.cwd)
10401
- result.cwd = p.cwd;
10402
- if (typeof p.id === "string" && !result.sessionId)
10403
- result.sessionId = p.id;
10404
- if (typeof p.originator === "string" && !result.surface)
10405
- result.surface = p.originator;
10406
- }
10407
- if (!result.title && o.type === "response_item" && p.type === "message" && p.role === "user" && Array.isArray(p.content)) {
10408
- for (const item of p.content) {
10409
- const it = item;
10410
- if ((it.type === "input_text" || it.type === "text") && typeof it.text === "string") {
10411
- const trimmed = it.text.trim();
10412
- if (!trimmed || CODEX_SKIP_PREFIXES.some((pre) => trimmed.startsWith(pre)) || SKIP_PREFIXES.some((pre) => trimmed.startsWith(pre)))
10413
- continue;
10414
- result.title = titleFromText(trimmed);
10415
- break;
10416
- }
10417
- }
10418
- }
10419
- if (result.title && result.firstTimestamp && result.cwd)
10558
+ if (absorbCodexRecord(result, o))
10420
10559
  break;
10421
10560
  }
10422
10561
  return result;
10423
10562
  }
10563
+ function scanCodexRollout(path, head, maxBytes) {
10564
+ const result = scanCodexRolloutHead(head);
10565
+ if (result.title && result.firstTimestamp && result.cwd)
10566
+ return result;
10567
+ readTranscriptRecords(path, (o) => absorbCodexRecord(result, o), maxBytes);
10568
+ return result;
10569
+ }
10424
10570
  var CODEX_HEAD_BYTES, UUID_TAIL;
10425
10571
  var init_rollout_listing = __esm(() => {
10426
10572
  init_session_listing();
@@ -11145,7 +11291,7 @@ var init_adapter3 = __esm(async () => {
11145
11291
  const head = readFileHead(file, CODEX_HEAD_BYTES);
11146
11292
  if (head === null)
11147
11293
  continue;
11148
- const scanned = scanCodexRolloutHead(head);
11294
+ const scanned = scanCodexRollout(file, head);
11149
11295
  if (isReflectionOwnSession(scanned.title))
11150
11296
  continue;
11151
11297
  sessions.push({
@@ -12434,6 +12580,18 @@ function findAgentFiles(folder, env) {
12434
12580
  }
12435
12581
  return out;
12436
12582
  }
12583
+ function liveFolderActivity(conversations) {
12584
+ const live = [];
12585
+ for (const c of conversations) {
12586
+ if (c.status !== "live")
12587
+ continue;
12588
+ const folders = c.folders ?? [];
12589
+ if (folders.length === 0)
12590
+ continue;
12591
+ live.push({ agentSlug: c.agentSlug, folders, lastActivityAt: c.lastActivityAt });
12592
+ }
12593
+ return live;
12594
+ }
12437
12595
  function buildAssetCensus(input) {
12438
12596
  const env = input.env ?? defaultCensusEnvironment();
12439
12597
  const now = input.now ?? new Date;
@@ -12455,6 +12613,18 @@ function buildAssetCensus(input) {
12455
12613
  byFolder.set(f, cur);
12456
12614
  }
12457
12615
  }
12616
+ for (const live of input.liveActivity ?? []) {
12617
+ for (const folder of live.folders) {
12618
+ const cur = byFolder.get(resolve3(folder));
12619
+ if (!cur)
12620
+ continue;
12621
+ const agent = cur.agents.get(live.agentSlug);
12622
+ if (agent && live.lastActivityAt > agent.last)
12623
+ agent.last = live.lastActivityAt;
12624
+ if (live.lastActivityAt > cur.last)
12625
+ cur.last = live.lastActivityAt;
12626
+ }
12627
+ }
12458
12628
  const outerFolders = [...byFolder.keys()].filter((f) => spaceExclusionReason(f, env) === null).map((f) => resolve3(f));
12459
12629
  const isNested = (folder) => {
12460
12630
  const f = resolve3(folder);
@@ -18827,7 +18997,7 @@ function createKeyboardListener() {
18827
18997
  }
18828
18998
 
18829
18999
  // src/generated/version.ts
18830
- var VERSION = "0.28.0";
19000
+ var VERSION = "0.29.0";
18831
19001
 
18832
19002
  // src/commands/dev.ts
18833
19003
  var exports_dev = {};
@@ -21362,6 +21532,17 @@ function localAppDataRoot(input) {
21362
21532
  const join53 = joiner(input);
21363
21533
  return input.localAppData ?? join53(home(input), "AppData", "Local");
21364
21534
  }
21535
+ function desktopStateDirs(input) {
21536
+ const join53 = joiner(input);
21537
+ const named = (root) => join53(root, DESKTOP_APP_IDENTIFIER);
21538
+ if (input.platform === "win32") {
21539
+ return [named(appDataRoot(input)), named(localAppDataRoot(input))];
21540
+ }
21541
+ if (input.platform === "darwin")
21542
+ return [named(appDataRoot(input))];
21543
+ return [named(join53(home(input), ".local", "share")), named(appDataRoot(input))];
21544
+ }
21545
+ var DESKTOP_APP_IDENTIFIER = "ai.runwork.desktop";
21365
21546
 
21366
21547
  // src/agents/claude/declarations.ts
21367
21548
  function claudeEntrypoint(head) {
@@ -21393,7 +21574,7 @@ function attributeClaudeEntrypoint(marker) {
21393
21574
  return null;
21394
21575
  }
21395
21576
  }
21396
- var CLAUDE_HEAD, CLAUDE_PROJECTS_STORE, CLAUDE_COWORK_STORE, CLAUDE_CODE_CAPTURE_TARGETS, CLAUDE_DESKTOP_CAPTURE_TARGETS, CLAUDE_ACCOUNT_PROFILE_FIELDS;
21577
+ var CLAUDE_HEAD, CLAUDE_PROJECTS_STORE, CLAUDE_COWORK_STORE, CLAUDE_CODE_CAPTURE_TARGETS, CLAUDE_DESKTOP_CAPTURE_TARGETS, CLAUDE_ACCOUNT_PROFILE_FIELD = "conversation_preferences";
21397
21578
  var init_declarations = __esm(() => {
21398
21579
  CLAUDE_HEAD = 64 * 1024;
21399
21580
  CLAUDE_PROJECTS_STORE = {
@@ -21440,10 +21621,6 @@ var init_declarations = __esm(() => {
21440
21621
  note: "the MCP config we write for Claude Desktop, next to its session store"
21441
21622
  }
21442
21623
  ];
21443
- CLAUDE_ACCOUNT_PROFILE_FIELDS = {
21444
- full: "cowork_global_instructions",
21445
- compact: "conversation_preferences"
21446
- };
21447
21624
  });
21448
21625
 
21449
21626
  // src/agents/codex/declarations.ts
@@ -26362,6 +26539,44 @@ function isExplicitlyIncluded(relPath, rules) {
26362
26539
  }
26363
26540
  return false;
26364
26541
  }
26542
+ function admitsNothingUnder(relDir, rules) {
26543
+ const dir = includeRulePath(relDir);
26544
+ if (!dir)
26545
+ return false;
26546
+ const lineage = pathAndAncestors(dir);
26547
+ for (const step of lineage) {
26548
+ if (rules.denies.has(step))
26549
+ return true;
26550
+ }
26551
+ for (const step of lineage) {
26552
+ if (rules.directories.has(step))
26553
+ return false;
26554
+ }
26555
+ const under2 = `${dir}/`;
26556
+ for (const path2 of rules.paths) {
26557
+ if (path2 === dir || path2.startsWith(under2))
26558
+ return false;
26559
+ }
26560
+ for (const directory of rules.directories) {
26561
+ if (directory.startsWith(under2))
26562
+ return false;
26563
+ }
26564
+ return true;
26565
+ }
26566
+ function sharesNothingUnder(relDir, rules) {
26567
+ const dir = includeRulePath(relDir);
26568
+ if (!dir)
26569
+ return false;
26570
+ for (const step of pathAndAncestors(dir)) {
26571
+ if (rules.denies.has(step))
26572
+ return true;
26573
+ }
26574
+ if (rules.names.size > 0)
26575
+ return false;
26576
+ if (rules.directories.size === 0 && rules.paths.size === 0)
26577
+ return false;
26578
+ return admitsNothingUnder(dir, rules);
26579
+ }
26365
26580
  function includeRulePath(relPath) {
26366
26581
  return relPath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
26367
26582
  }
@@ -26432,6 +26647,13 @@ function hiddenFromSpace(relPath, ignoreSets, rules, floor = buildSpaceFloorSets
26432
26647
  return true;
26433
26648
  return !isExplicitlyIncluded(relPath, rules);
26434
26649
  }
26650
+ function spaceSkipsDirectory(relDir, ignoreSets, rules, floor = buildSpaceFloorSets()) {
26651
+ if (sharesNothingUnder(relDir, rules))
26652
+ return true;
26653
+ if (!isIgnoredDirectory(relDir, ignoreSets))
26654
+ return false;
26655
+ return isIgnoredDirectory(relDir, floor) || admitsNothingUnder(relDir, rules);
26656
+ }
26435
26657
  function agentManagedInstructionPaths() {
26436
26658
  const paths = new Set;
26437
26659
  for (const adapter2 of getAllAdapters()) {
@@ -26462,6 +26684,10 @@ function readIncludeRules(folder) {
26462
26684
 
26463
26685
  // src/spaces/space-repo.ts
26464
26686
  init_instruction_hint();
26687
+ // ../../shared/spaces/refusal.ts
26688
+ var SPACE_RESET_COMMAND = "runwork spaces reset";
26689
+ var SPACE_EXCLUDE_COMMAND = "runwork spaces exclude";
26690
+
26465
26691
  // ../../shared/spaces/waiting-changes.ts
26466
26692
  var WAITING_SUMMARY_ENTRY_CAP = 200;
26467
26693
 
@@ -26633,6 +26859,7 @@ function normaliseMtime(ms) {
26633
26859
 
26634
26860
  // src/spaces/space-repo.ts
26635
26861
  init_atomic_json();
26862
+ init_trash();
26636
26863
  var SPACE_BRANCH = "main";
26637
26864
  var REMOTE_MAIN = `refs/remotes/${RUNWORK_REMOTE}/${SPACE_BRANCH}`;
26638
26865
  var LOCAL_MAIN = `refs/heads/${SPACE_BRANCH}`;
@@ -26667,7 +26894,13 @@ function spaceRemainingBytes(storage, fallbackLimit = SPACE_SIZE_LIMIT_BYTES) {
26667
26894
  function spaceGitdir(folder) {
26668
26895
  const normalized = resolve7(folder);
26669
26896
  const hash = createHash7("sha256").update(normalized).digest("hex").slice(0, 12);
26670
- return join59(homedir36(), ".runwork", "space-repos", `${basename9(normalized)}-${hash}`);
26897
+ return join59(spaceReposRoot(), `${basename9(normalized)}-${hash}`);
26898
+ }
26899
+ function spaceReposRoot() {
26900
+ return join59(homedir36(), ".runwork", "space-repos");
26901
+ }
26902
+ function isSpaceHistoryPath(gitdir) {
26903
+ return dirname17(gitdir) === spaceReposRoot();
26671
26904
  }
26672
26905
  function repoAt(folder) {
26673
26906
  return { fs: fs6, dir: folder, gitdir: spaceGitdir(folder) };
@@ -26678,8 +26911,80 @@ function normalizeUrl(url) {
26678
26911
  async function repoMappingRefusal(folder, spaceRemoteUrl) {
26679
26912
  return null;
26680
26913
  }
26914
+ function spaceIdFromRemoteUrl(url) {
26915
+ const match = /\/spaces\/(spc_[A-Za-z0-9_-]+)(?:[/?#]|$)/.exec(url);
26916
+ return match ? match[1] : null;
26917
+ }
26918
+ async function foreignSpaceHistory(folder, spaceRemoteUrl) {
26919
+ const gitdir = spaceGitdir(folder);
26920
+ if (!existsSync59(gitdir))
26921
+ return null;
26922
+ const now = spaceIdFromRemoteUrl(spaceRemoteUrl);
26923
+ if (!now)
26924
+ return null;
26925
+ let was = null;
26926
+ try {
26927
+ const url = (await git.listRemotes({ fs: fs6, gitdir })).find((r) => r.remote === RUNWORK_REMOTE)?.url;
26928
+ was = url ? spaceIdFromRemoteUrl(url) : null;
26929
+ } catch {
26930
+ return null;
26931
+ }
26932
+ return was && was !== now ? { was, now } : null;
26933
+ }
26934
+ function directoryWeight(dir) {
26935
+ let bytes = 0;
26936
+ let files = 0;
26937
+ const pending = [dir];
26938
+ while (pending.length > 0) {
26939
+ const current = pending.pop();
26940
+ let entries;
26941
+ try {
26942
+ entries = readdirSync24(current, { withFileTypes: true });
26943
+ } catch {
26944
+ continue;
26945
+ }
26946
+ for (const entry of entries) {
26947
+ const full = join59(current, entry.name);
26948
+ if (entry.isDirectory()) {
26949
+ pending.push(full);
26950
+ continue;
26951
+ }
26952
+ try {
26953
+ const stats = lstatSync2(full);
26954
+ if (stats.isFile()) {
26955
+ bytes += stats.size;
26956
+ files++;
26957
+ }
26958
+ } catch {}
26959
+ }
26960
+ }
26961
+ return { bytes, files };
26962
+ }
26963
+ function discardSpaceHistory(folder, reason) {
26964
+ const gitdir = spaceGitdir(folder);
26965
+ if (!isSpaceHistoryPath(gitdir))
26966
+ return { state: "outside", gitdir, root: spaceReposRoot() };
26967
+ if (!existsSync59(gitdir))
26968
+ return { state: "none", gitdir };
26969
+ const weight = directoryWeight(gitdir);
26970
+ const trash = moveToTrash(gitdir, reason);
26971
+ if (trash === null)
26972
+ return { state: "stuck", gitdir, trashRoot: trashRoot() };
26973
+ forgetWaitingCache(folder);
26974
+ return { state: "discarded", gitdir, trash, bytes: weight.bytes, files: weight.files };
26975
+ }
26681
26976
  async function ensureSpaceRepo(folder, spaceRemoteUrl) {
26682
26977
  const gitdir = spaceGitdir(folder);
26978
+ let setAside = null;
26979
+ const foreign = await foreignSpaceHistory(folder, spaceRemoteUrl);
26980
+ if (foreign) {
26981
+ const discarded = discardSpaceHistory(folder, `space history for ${foreign.was}: ${folder} is now mapped to ${foreign.now}`);
26982
+ if (discarded.state === "stuck") {
26983
+ throw new Error(`This folder's earlier history belonged to another space and could not be moved to ${discarded.trashRoot}, so nothing was synced. It is at ${discarded.gitdir}.`);
26984
+ }
26985
+ if (discarded.state === "discarded")
26986
+ setAside = { spaceId: foreign.was, trash: discarded.trash, bytes: discarded.bytes };
26987
+ }
26683
26988
  let created = false;
26684
26989
  if (!existsSync59(gitdir)) {
26685
26990
  migrateLegacyInFolderRepo(folder, gitdir);
@@ -26694,7 +26999,7 @@ async function ensureSpaceRepo(folder, spaceRemoteUrl) {
26694
26999
  if (!current || normalizeUrl(current.url) !== normalizeUrl(spaceRemoteUrl)) {
26695
27000
  await git.addRemote({ ...repoAt(folder), remote: RUNWORK_REMOTE, url: spaceRemoteUrl, force: true });
26696
27001
  }
26697
- return { created };
27002
+ return { created, setAside };
26698
27003
  }
26699
27004
  function migrateLegacyInFolderRepo(folder, gitdir) {
26700
27005
  const inFolder = join59(folder, ".git");
@@ -26931,9 +27236,29 @@ function unaddressableNamesIn(folder) {
26931
27236
  walk(folder, "");
26932
27237
  return found.sort();
26933
27238
  }
26934
- async function readStatusMatrix(folder) {
27239
+ function spaceVisibleFs(folder, skip) {
27240
+ const root = resolve7(folder).replace(/\\/g, "/").replace(/\/+$/, "");
27241
+ const relativeToFolder = (target) => {
27242
+ const path2 = String(target).replace(/\\/g, "/").replace(/\/+$/, "");
27243
+ if (path2 === root)
27244
+ return "";
27245
+ return path2.startsWith(`${root}/`) ? path2.slice(root.length + 1) : null;
27246
+ };
27247
+ return {
27248
+ promises: {
27249
+ ...fs6.promises,
27250
+ readdir: async (target) => {
27251
+ const rel = relativeToFolder(String(target));
27252
+ if (rel !== null && rel !== "" && skip(rel))
27253
+ return [];
27254
+ return fs6.promises.readdir(target);
27255
+ }
27256
+ }
27257
+ };
27258
+ }
27259
+ async function readStatusMatrix(folder, skip) {
26935
27260
  try {
26936
- return await git.statusMatrix({ ...repoAt(folder) });
27261
+ return await git.statusMatrix({ ...repoAt(folder), fs: spaceVisibleFs(folder, skip) });
26937
27262
  } catch (err) {
26938
27263
  const offenders = unaddressableNamesIn(folder);
26939
27264
  if (offenders.length === 0)
@@ -26970,14 +27295,14 @@ async function planFolderChanges(folder, opts = {}) {
26970
27295
  const ignoreSets = buildSpaceIgnoreSets(folder);
26971
27296
  const managedInstructionPaths = new Set(agentManagedInstructionPaths());
26972
27297
  const includeRules = readIncludeRules(folder);
26973
- const matrix = await readStatusMatrix(folder);
27298
+ const floor = buildSpaceFloorSets();
27299
+ const matrix = await readStatusMatrix(folder, (relDir) => spaceSkipsDirectory(relDir, ignoreSets, includeRules, floor));
26974
27300
  const changes = [];
26975
27301
  const skipped = [];
26976
27302
  let addedBytes = 0;
26977
27303
  const remove = (filepath) => {
26978
27304
  changes.push({ path: filepath, kind: "remove", bytes: 0 });
26979
27305
  };
26980
- const floor = buildSpaceFloorSets();
26981
27306
  for (const [filepath, head, workdir] of matrix) {
26982
27307
  if (hiddenFromSpace(filepath, ignoreSets, includeRules, floor))
26983
27308
  continue;
@@ -27101,7 +27426,7 @@ function splitPlannedChanges(changes, rules) {
27101
27426
  split.heldNewestMtimeMs = Math.max(split.heldNewestMtimeMs ?? 0, change.mtimeMs);
27102
27427
  continue;
27103
27428
  }
27104
- const age = nowMs - (change.mtimeMs ?? 0);
27429
+ const age = nowMs - Math.floor(change.mtimeMs ?? 0);
27105
27430
  const unfinished = change.kind !== "remove" && rules.settle !== false && age >= 0 && age < SPACE_SETTLE_MS;
27106
27431
  if (unfinished && !namedForSharing(change.path, shareNow)) {
27107
27432
  split.settling.push(change.path);
@@ -27272,7 +27597,7 @@ function weightSentence(heaviest) {
27272
27597
  function ceilingMessage(refusal) {
27273
27598
  const sizes = `${formatBytes(refusal.bytesToPush)} and the space has ${formatBytes(refusal.remainingBytes)} left`;
27274
27599
  if (refusal.pendingCommits === 0) {
27275
- const nothingWritten = `Sharing this folder would send ${sizes}, so nothing was committed here and nothing went up.${weightSentence(refusal.heaviest)} Share less of it with \`runwork spaces exclude <path>\`, or remove the large files, then sync again.`;
27600
+ const nothingWritten = `Sharing this folder would send ${sizes}, so nothing was committed here and nothing went up.${weightSentence(refusal.heaviest)} Share less of it with \`${SPACE_EXCLUDE_COMMAND} <path>\`, or remove the large files, then sync again.`;
27276
27601
  if (!refusal.spaceHasWorkToGive)
27277
27602
  return nothingWritten;
27278
27603
  return `${nothingWritten} The space's own newer work is still waiting for this folder and arrives on the first sync that is not refused.`;
@@ -27280,7 +27605,7 @@ function ceilingMessage(refusal) {
27280
27605
  const commits = refusal.pendingCommits === 1 ? "Its commit is" : `Its ${refusal.pendingCommits} commits are`;
27281
27606
  const alreadyWritten = `This push carries ${sizes}. ${commits} already in this folder's space history, so removing files now adds another commit and takes nothing away. What has to start over is that history: it lives at ${refusal.historyPath}, outside your folder, and neither your files nor your own git repository are part of it.`;
27282
27607
  const bothWays = refusal.spaceHasWorkToGive ? " Until the push lands, this folder cannot take the space's newer work either, because the space combines the two sides when a push arrives." : "";
27283
- const exit = ` Start it over with \`runwork spaces reset ${refusal.folder}\`: this machine's copy of that history is discarded and the next sync rebuilds it from the space. Those commits never reached the space, so they go with it; your own files stay where they are.`;
27608
+ const exit = ` Start it over with \`${SPACE_RESET_COMMAND} ${refusal.folder}\`: this machine's copy of that history is discarded and the next sync rebuilds it from the space. Those commits never reached the space, so they go with it; your own files stay where they are.`;
27284
27609
  return `${alreadyWritten}${bothWays}${exit}`;
27285
27610
  }
27286
27611
  async function resolveOid(folder, ref) {
@@ -27388,7 +27713,7 @@ async function localVersionsToKeep(folder, commit) {
27388
27713
  }
27389
27714
  return keep;
27390
27715
  }
27391
- async function ceilingCheck(input, limits, plan) {
27716
+ async function ceilingCheck(input, limits, plan, now) {
27392
27717
  const local = await resolveOid(input.folder, LOCAL_MAIN);
27393
27718
  const remote = await resolveOid(input.folder, REMOTE_MAIN);
27394
27719
  const common = local && remote ? await mergeBase(input.folder, local, remote) : null;
@@ -27399,7 +27724,9 @@ async function ceilingCheck(input, limits, plan) {
27399
27724
  if (isUnreadableStorage(input.spaceStorage))
27400
27725
  return { decision, refusal: null, unmeasured: input.spaceStorage.unreadable };
27401
27726
  const pending = local && (decision === "push" || decision === "push-then-fast-forward") ? await unpushedCommits(input.folder, local, common) : [];
27402
- const bytesToPush = pending.reduce((sum, c) => sum + c.bytes, 0) + plan.addedBytes;
27727
+ const staging = splitPlannedChanges(plan.changes, { settle: input.settle, shareNow: input.shareNow, auto: input.auto, now }).staging;
27728
+ const addingBytes = staging.reduce((sum, change) => change.kind === "remove" ? sum : sum + change.bytes, 0);
27729
+ const bytesToPush = pending.reduce((sum, c) => sum + c.bytes, 0) + addingBytes;
27403
27730
  const remainingBytes = spaceRemainingBytes(input.spaceStorage, limits.spaceBytes);
27404
27731
  if (!pushWouldExceed(remainingBytes, bytesToPush))
27405
27732
  return nothingToSay;
@@ -27412,7 +27739,7 @@ async function ceilingCheck(input, limits, plan) {
27412
27739
  bytesToPush,
27413
27740
  pendingCommits: pending.length,
27414
27741
  historyPath: spaceGitdir(input.folder),
27415
- heaviest: pending.length === 0 ? heaviestShares(plan.changes) : [],
27742
+ heaviest: pending.length === 0 ? heaviestShares(staging) : [],
27416
27743
  spaceHasWorkToGive: decision === "fast-forward" || decision === "checkout-remote" || decision === "push-then-fast-forward"
27417
27744
  }
27418
27745
  };
@@ -27420,11 +27747,13 @@ async function ceilingCheck(input, limits, plan) {
27420
27747
  async function syncSpaceFolder(input) {
27421
27748
  const limits = input.limits ?? DEFAULT_SPACE_REPO_LIMITS;
27422
27749
  const platform9 = input.platform ?? hostPathPlatform();
27750
+ const now = input.now ?? new Date;
27423
27751
  const onAuth = spaceRepoAuth(input.apiKey);
27424
27752
  const base = {
27425
27753
  folder: input.folder,
27426
27754
  decision: "nothing",
27427
27755
  initialized: false,
27756
+ historySetAside: null,
27428
27757
  commit: { committed: false, commits: [], added: [], removed: [], skipped: [], settling: [], held: [], heldNewestMtimeMs: null, heldSummary: noWaiting(), addedBytes: 0 },
27429
27758
  pushed: false,
27430
27759
  batches: 0,
@@ -27439,7 +27768,9 @@ async function syncSpaceFolder(input) {
27439
27768
  };
27440
27769
  let measured = null;
27441
27770
  try {
27442
- base.initialized = (await ensureSpaceRepo(input.folder, input.spaceRemoteUrl)).created;
27771
+ const ensured = await ensureSpaceRepo(input.folder, input.spaceRemoteUrl);
27772
+ base.initialized = ensured.created;
27773
+ base.historySetAside = ensured.setAside;
27443
27774
  base.skippedForPlatform = [...platformSkippedPaths(input.folder, platform9)];
27444
27775
  let fetchFailure = null;
27445
27776
  try {
@@ -27452,7 +27783,7 @@ async function syncSpaceFolder(input) {
27452
27783
  const plan = firstFromSpace ? await dropAlreadyInSpace(input.folder, walked, REMOTE_MAIN) : walked;
27453
27784
  measured = plan;
27454
27785
  const holding = input.auto !== true && !askedToShare(input);
27455
- const ceiling = fetchFailure || holding ? { decision: "nothing", refusal: null, unmeasured: null } : await ceilingCheck(input, limits, plan);
27786
+ const ceiling = fetchFailure || holding ? { decision: "nothing", refusal: null, unmeasured: null } : await ceilingCheck(input, limits, plan, now);
27456
27787
  const refused = ceiling.unmeasured !== null ? unmeasuredMessage(ceiling.unmeasured) : ceiling.refusal ? ceilingMessage(ceiling.refusal) : null;
27457
27788
  if (refused) {
27458
27789
  base.commit = { committed: false, commits: [], added: [], removed: [], skipped: plan.skipped, settling: [], held: [], heldNewestMtimeMs: null, heldSummary: noWaiting(), addedBytes: plan.addedBytes };
@@ -27470,7 +27801,7 @@ async function syncSpaceFolder(input) {
27470
27801
  settle: input.settle,
27471
27802
  shareNow: input.shareNow,
27472
27803
  auto: input.auto,
27473
- now: input.now
27804
+ now
27474
27805
  });
27475
27806
  if (fetchFailure)
27476
27807
  throw fetchFailure;
@@ -27540,7 +27871,7 @@ async function syncSpaceFolder(input) {
27540
27871
  base.error = describeError(err);
27541
27872
  return base;
27542
27873
  } finally {
27543
- rememberWaitingChanges(input.folder, measured, base, { auto: input.auto, now: input.now });
27874
+ rememberWaitingChanges(input.folder, measured, base, { auto: input.auto, now });
27544
27875
  }
27545
27876
  }
27546
27877
  function rememberWaitingChanges(folder, plan, result, rules) {
@@ -27829,11 +28160,13 @@ function scopeOfMappedFolder(folder, opts = {}) {
27829
28160
  const ignoreSets = buildSpaceIgnoreSets(folder);
27830
28161
  const rules = readIncludeRules(folder);
27831
28162
  const shares = at ? () => true : (rel) => rel === SPACE_INCLUDE_FILE || isIncluded(rel, rules);
28163
+ const hidden = hiddenPredicate(ignoreSets, rules);
28164
+ const outOfSight = at ? hidden : (rel) => hidden(rel) || sharesNothingUnder(rel, rules);
27832
28165
  const groups = new Map;
27833
28166
  let files = 0;
27834
28167
  let bytes = 0;
27835
28168
  const groupPrefix = at ? `${at}/` : "";
27836
- countUnder(at ? join60(folder, at) : folder, folder, hiddenPredicate(ignoreSets, rules), shares, {
28169
+ countUnder(at ? join60(folder, at) : folder, folder, outOfSight, shares, {
27837
28170
  maxDepth: Number.POSITIVE_INFINITY,
27838
28171
  budget: { left: Number.POSITIVE_INFINITY },
27839
28172
  followLinks: false,
@@ -27852,6 +28185,32 @@ function scopeOfMappedFolder(folder, opts = {}) {
27852
28185
  const largest = [...groups.values()].sort((a, b) => b.files - a.files || a.name.localeCompare(b.name)).slice(0, limit);
27853
28186
  return { folder, files, bytes, largest };
27854
28187
  }
28188
+ function holdsMoreFilesThan(folder, max) {
28189
+ const hidden = hiddenPredicate(buildSpaceIgnoreSets(folder), readIncludeRules(folder));
28190
+ let found = 0;
28191
+ const walk = (dir) => {
28192
+ for (const name of safeReadDir(dir)) {
28193
+ const full = join60(dir, name);
28194
+ const rel = relative8(folder, full).split(sep9).join("/");
28195
+ if (hidden(rel))
28196
+ continue;
28197
+ if (weigh(full, false).directory) {
28198
+ if (walk(full))
28199
+ return true;
28200
+ continue;
28201
+ }
28202
+ found += 1;
28203
+ if (found > max)
28204
+ return true;
28205
+ }
28206
+ return false;
28207
+ };
28208
+ return walk(folder);
28209
+ }
28210
+ function topLevelSpaceEntries(folder) {
28211
+ const hidden = hiddenPredicate(buildSpaceIgnoreSets(folder), readIncludeRules(folder));
28212
+ return safeReadDir(folder).filter((name) => !hidden(name)).sort();
28213
+ }
27855
28214
  function safeReadDir(dir) {
27856
28215
  try {
27857
28216
  return readdirSync25(dir);
@@ -27881,6 +28240,10 @@ function measure(path2) {
27881
28240
  // ../../shared/spaces/map-scope.ts
27882
28241
  var MAP_SCOPE_MAX_FILES = 1e4;
27883
28242
  var MAP_SCOPE_MAX_BYTES = 500 * 1024 * 1024;
28243
+ var MAP_SEED_MAX_FILES = 5000;
28244
+ function mapSeededNothingSentence() {
28245
+ return `This folder holds more than ${MAP_SEED_MAX_FILES.toLocaleString("en-US")} files, so nothing is shared until you pick what to share.`;
28246
+ }
27884
28247
  function isOverMapScope(scope) {
27885
28248
  return scope.files > MAP_SCOPE_MAX_FILES || scope.bytes > MAP_SCOPE_MAX_BYTES;
27886
28249
  }
@@ -27926,6 +28289,10 @@ function mapScopeWarning(scope) {
27926
28289
  example: mapScopeIncludeExample(scope)
27927
28290
  };
27928
28291
  }
28292
+ function historySetAsideSentence(setAside) {
28293
+ const where = setAside.trash ? ` at ${setAside.trash}` : "";
28294
+ return `This folder's earlier history belonged to another space and was set aside${where} (${formatBytes(setAside.bytes)}). Your files were not touched.`;
28295
+ }
27929
28296
 
27930
28297
  // src/commands/spaces.ts
27931
28298
  init_remote();
@@ -28033,8 +28400,12 @@ function lockPath2() {
28033
28400
  return join61(runworkDir(), "sync.lock");
28034
28401
  }
28035
28402
  var STALE_LOCK_MS = 5 * 60 * 1000;
28036
- var DEFAULT_WAIT_MS = 30000;
28403
+ var HEARTBEAT_MS = 30000;
28404
+ var POLL_START_MS = 250;
28405
+ var POLL_MAX_MS = 5000;
28406
+ var WAIT_NOTICE_MS = 15000;
28037
28407
  var exitHandlerRegistered = false;
28408
+ var heartbeat = null;
28038
28409
  function ensureExitHandler() {
28039
28410
  if (exitHandlerRegistered)
28040
28411
  return;
@@ -28056,12 +28427,21 @@ function readLock() {
28056
28427
  return null;
28057
28428
  }
28058
28429
  }
28430
+ function heardFrom(existing) {
28431
+ return existing.refreshedAt ?? existing.startedAt;
28432
+ }
28433
+ function holderAlive(existing) {
28434
+ if (Date.now() - heardFrom(existing) > STALE_LOCK_MS)
28435
+ return false;
28436
+ return isProcessAlive2(existing.pid);
28437
+ }
28059
28438
  function writeLockExclusive() {
28060
28439
  try {
28061
28440
  if (!existsSync61(runworkDir())) {
28062
28441
  mkdirSync34(runworkDir(), { recursive: true });
28063
28442
  }
28064
- writeFileSync32(lockPath2(), JSON.stringify({ pid: process.pid, startedAt: Date.now() }), {
28443
+ const now = Date.now();
28444
+ writeFileSync32(lockPath2(), JSON.stringify({ pid: process.pid, startedAt: now, refreshedAt: now }), {
28065
28445
  flag: "wx"
28066
28446
  });
28067
28447
  return true;
@@ -28069,20 +28449,40 @@ function writeLockExclusive() {
28069
28449
  return false;
28070
28450
  }
28071
28451
  }
28452
+ function startHeartbeat() {
28453
+ if (heartbeat)
28454
+ return;
28455
+ heartbeat = setInterval(() => {
28456
+ const existing = readLock();
28457
+ if (existing?.pid !== process.pid)
28458
+ return;
28459
+ try {
28460
+ writeFileSync32(lockPath2(), JSON.stringify({ ...existing, refreshedAt: Date.now() }));
28461
+ } catch {}
28462
+ }, HEARTBEAT_MS);
28463
+ heartbeat.unref();
28464
+ }
28465
+ function stopHeartbeat() {
28466
+ if (!heartbeat)
28467
+ return;
28468
+ clearInterval(heartbeat);
28469
+ heartbeat = null;
28470
+ }
28072
28471
  function tryAcquireOnce() {
28073
28472
  if (writeLockExclusive()) {
28074
28473
  ensureExitHandler();
28474
+ startHeartbeat();
28075
28475
  return true;
28076
28476
  }
28077
28477
  const existing = readLock();
28078
- const stale = !existing || Date.now() - existing.startedAt > STALE_LOCK_MS || !isProcessAlive2(existing.pid);
28079
- if (!stale)
28478
+ if (existing && holderAlive(existing))
28080
28479
  return false;
28081
28480
  try {
28082
28481
  unlinkSync9(lockPath2());
28083
28482
  } catch {}
28084
28483
  if (writeLockExclusive()) {
28085
28484
  ensureExitHandler();
28485
+ startHeartbeat();
28086
28486
  return true;
28087
28487
  }
28088
28488
  return false;
@@ -28090,20 +28490,34 @@ function tryAcquireOnce() {
28090
28490
  function sleep(ms) {
28091
28491
  return new Promise((resolve8) => setTimeout(resolve8, ms));
28092
28492
  }
28093
- async function acquireSyncLock(waitMs = DEFAULT_WAIT_MS) {
28094
- const deadline = Date.now() + waitMs;
28095
- let delay = 250;
28493
+ function noticeWaiting(pid, waitedMs) {
28494
+ if (waitedMs < WAIT_NOTICE_MS) {
28495
+ console.error(`Another Runwork sync is running on this machine (pid ${pid}). Waiting for it to finish.`);
28496
+ return;
28497
+ }
28498
+ console.error(`Still waiting for that sync, ${Math.round(waitedMs / 1000)}s so far.`);
28499
+ }
28500
+ async function acquireSyncLock() {
28501
+ const startedWaiting = Date.now();
28502
+ let delay = POLL_START_MS;
28503
+ let notices = 0;
28096
28504
  for (;; ) {
28097
28505
  if (tryAcquireOnce())
28098
28506
  return true;
28099
- const remaining = deadline - Date.now();
28100
- if (remaining <= 0)
28507
+ const existing = readLock();
28508
+ if (!existing || !holderAlive(existing))
28101
28509
  return false;
28102
- await sleep(Math.min(delay, remaining));
28103
- delay = Math.min(delay * 2, 5000);
28510
+ const waitedMs = Date.now() - startedWaiting;
28511
+ if (waitedMs >= notices * WAIT_NOTICE_MS) {
28512
+ noticeWaiting(existing.pid, waitedMs);
28513
+ notices += 1;
28514
+ }
28515
+ await sleep(delay);
28516
+ delay = Math.min(delay * 2, POLL_MAX_MS);
28104
28517
  }
28105
28518
  }
28106
28519
  function releaseSyncLock() {
28520
+ stopHeartbeat();
28107
28521
  const existing = readLock();
28108
28522
  if (existing?.pid === process.pid) {
28109
28523
  try {
@@ -28353,6 +28767,7 @@ function failedFolder(folder, err) {
28353
28767
  folder,
28354
28768
  decision: "nothing",
28355
28769
  initialized: false,
28770
+ historySetAside: null,
28356
28771
  commit: { committed: false, commits: [], added: [], removed: [], skipped: [], settling: [], held: [], heldNewestMtimeMs: null, heldSummary: noWaiting(), addedBytes: 0 },
28357
28772
  pushed: false,
28358
28773
  batches: 0,
@@ -28434,7 +28849,12 @@ spacesCommand.command("suggest").description("Propose spaces and things to keep
28434
28849
  saveCensusStore(store);
28435
28850
  const records = Object.fromEntries([...candidates.keys()].map((key) => [key, store.sessions[key]]).filter(([, r]) => Boolean(r)));
28436
28851
  const knownSkills = opts.offline ? null : await fetchKnownSkills();
28437
- const census = buildAssetCensus({ records, knownSkills, mappedFolders: mappedFolderIndex(loadSpaceMappings().mappings) });
28852
+ const census = buildAssetCensus({
28853
+ records,
28854
+ knownSkills,
28855
+ mappedFolders: mappedFolderIndex(loadSpaceMappings().mappings),
28856
+ liveActivity: liveFolderActivity(listing.conversations)
28857
+ });
28438
28858
  if (json) {
28439
28859
  jsonOut({ captured, days, census });
28440
28860
  return;
@@ -28536,6 +28956,13 @@ spacesCommand.command("map <folder>").description("Keep a folder in a space: eve
28536
28956
  writeFileSync33(localList, fromSpace);
28537
28957
  }
28538
28958
  }
28959
+ const narrowed = !existsSync64(localList) && holdsMoreFilesThan(target, MAP_SEED_MAX_FILES) ? mapSeededNothingSentence() : null;
28960
+ if (narrowed) {
28961
+ let seeded = readIncludeRules(target);
28962
+ for (const entry of topLevelSpaceEntries(target))
28963
+ seeded = excludePath(seeded, entry);
28964
+ writeIncludeRules(target, seeded);
28965
+ }
28539
28966
  if (existsSync64(localList)) {
28540
28967
  excludeFromTheirGit(target, SPACE_INCLUDE_FILE);
28541
28968
  }
@@ -28574,7 +29001,7 @@ spacesCommand.command("map <folder>").description("Keep a folder in a space: eve
28574
29001
  writeSpaceMarker(mapping);
28575
29002
  excludeFromTheirGit(target, ".runwork/");
28576
29003
  if (!json) {
28577
- console.log(dim(mapScopeWarning(scope).count));
29004
+ console.log(dim(narrowed ?? mapScopeWarning(scope).count));
28578
29005
  if (overScope && !auto) {
28579
29006
  console.log(dim(`${waitingScopeSentence(scope)}, and none of it leaves this computer until you share it.`));
28580
29007
  }
@@ -28591,7 +29018,7 @@ spacesCommand.command("map <folder>").description("Keep a folder in a space: eve
28591
29018
  }
28592
29019
  const problem = firstSyncProblem(firstSync, firstSyncError);
28593
29020
  if (json) {
28594
- jsonOut({ mapped: mapping, path: spaceMappingsPath(), refused: false, scope, createdFolder, adoptedInstructions: adopted, firstSync, firstSyncError, firstSyncOk: problem === null, firstSyncProblem: problem });
29021
+ jsonOut({ mapped: mapping, path: spaceMappingsPath(), refused: false, scope, createdFolder, adoptedInstructions: adopted, narrowed, firstSync, firstSyncError, firstSyncOk: problem === null, firstSyncProblem: problem });
28595
29022
  if (problem)
28596
29023
  process.exitCode = MAP_FIRST_SYNC_FAILED;
28597
29024
  return;
@@ -28600,6 +29027,9 @@ spacesCommand.command("map <folder>").description("Keep a folder in a space: eve
28600
29027
  console.log(dim(`Created ${mapping.folder}.`));
28601
29028
  console.log(green(`Mapped ${mapping.folder} to ${spaceDisplayName(space)}.`));
28602
29029
  const outcome = firstSync?.results[0];
29030
+ if (outcome?.historySetAside) {
29031
+ console.log(dim(` ${historySetAsideSentence(outcome.historySetAside)}`));
29032
+ }
28603
29033
  if (outcome && !problem) {
28604
29034
  console.log(dim(` ${describeFolderSync(outcome)}`));
28605
29035
  const kept = outcome.keptLocalCopies;
@@ -28693,9 +29123,6 @@ function writeIncludeRules(folder, rules) {
28693
29123
  writeFileSync33(file, existing === null ? formatIncludeFile(rules) : updateIncludeFile(existing, rules));
28694
29124
  excludeFromTheirGit(folder, SPACE_INCLUDE_FILE);
28695
29125
  }
28696
- function spaceReposRoot() {
28697
- return join64(homedir39(), ".runwork", "space-repos");
28698
- }
28699
29126
  var LOCAL_MAIN_REF = `refs/heads/${SPACE_BRANCH}`;
28700
29127
  var REMOTE_MAIN_REF = `refs/remotes/${RUNWORK_REMOTE}/${SPACE_BRANCH}`;
28701
29128
  async function readSpaceRef(gitdir, ref) {
@@ -28780,35 +29207,6 @@ function describeOrphan(repo) {
28780
29207
  const state = repo.state === "unpushed" ? red(`holds ${repo.unpushedCommits || "some"} commit(s) the space does not have`) : repo.state === "in-sync" ? green("the space has all of it") : repo.state === "behind" ? green("the space has moved past it") : dim("never committed anything");
28781
29208
  return ` ${repo.name} ${dim(formatBytes(repo.bytes))} ${state}${repo.spaceId ? dim(` ${repo.spaceId}`) : ""}`;
28782
29209
  }
28783
- function directoryWeight(dir) {
28784
- let bytes = 0;
28785
- let files = 0;
28786
- const pending = [dir];
28787
- while (pending.length > 0) {
28788
- const current = pending.pop();
28789
- let entries;
28790
- try {
28791
- entries = readdirSync26(current, { withFileTypes: true });
28792
- } catch {
28793
- continue;
28794
- }
28795
- for (const entry of entries) {
28796
- const full = join64(current, entry.name);
28797
- if (entry.isDirectory()) {
28798
- pending.push(full);
28799
- continue;
28800
- }
28801
- try {
28802
- const stats = lstatSync4(full);
28803
- if (stats.isFile()) {
28804
- bytes += stats.size;
28805
- files++;
28806
- }
28807
- } catch {}
28808
- }
28809
- }
28810
- return { bytes, files };
28811
- }
28812
29210
  async function readStdinBytes() {
28813
29211
  if (process.stdin.isTTY)
28814
29212
  return null;
@@ -29356,7 +29754,7 @@ async function underSyncLock(json, run) {
29356
29754
  const acquired = await acquireSyncLock();
29357
29755
  if (!acquired) {
29358
29756
  if (!json)
29359
- console.log(" Another sync appears to be in progress on this machine; proceeding anyway.");
29757
+ console.log(" This machine's sync lock could not be taken, and nothing is holding it; proceeding anyway.");
29360
29758
  await run();
29361
29759
  return;
29362
29760
  }
@@ -29567,12 +29965,24 @@ spacesCommand.command("reset <folder>").description("Throw away this machine's c
29567
29965
  process.exit(1);
29568
29966
  }
29569
29967
  const gitdir = spaceGitdir(found.folder);
29570
- const root = join64(homedir39(), ".runwork", "space-repos");
29571
- if (dirname18(gitdir) !== root) {
29968
+ const root = spaceReposRoot();
29969
+ if (!isSpaceHistoryPath(gitdir)) {
29572
29970
  console.error(`Error: refusing to remove ${gitdir}. A space history is a folder directly under ${root}, and this is not one.`);
29573
29971
  process.exit(1);
29574
29972
  }
29575
- if (!existsSync64(gitdir)) {
29973
+ if (opts.yes !== true && !json && existsSync64(gitdir)) {
29974
+ const confirmed = await promptConfirm(`Throw away ${formatBytes(directoryWeight(gitdir).bytes)} of ${found.mapping.spaceName} history for ${found.folder}? Your files stay where they are.`);
29975
+ if (!confirmed) {
29976
+ console.log("Cancelled.");
29977
+ return;
29978
+ }
29979
+ }
29980
+ const discarded = discardSpaceHistory(found.folder, `runwork spaces reset ${found.folder}`);
29981
+ if (discarded.state === "outside") {
29982
+ console.error(`Error: refusing to remove ${discarded.gitdir}. A space history is a folder directly under ${discarded.root}, and this is not one.`);
29983
+ process.exit(1);
29984
+ }
29985
+ if (discarded.state === "none") {
29576
29986
  if (json) {
29577
29987
  jsonOut({ folder: found.folder, spaceId: found.mapping.spaceId, spaceName: found.mapping.spaceName, gitdir, removed: false, bytes: 0, files: 0, trash: null });
29578
29988
  return;
@@ -29580,25 +29990,15 @@ spacesCommand.command("reset <folder>").description("Throw away this machine's c
29580
29990
  console.log(dim(`${found.folder} has no space history on this machine. The next sync builds one: runwork spaces sync ${found.folder}`));
29581
29991
  return;
29582
29992
  }
29583
- const weight = directoryWeight(gitdir);
29584
- if (opts.yes !== true && !json) {
29585
- const confirmed = await promptConfirm(`Throw away ${formatBytes(weight.bytes)} of ${found.mapping.spaceName} history for ${found.folder}? Your files stay where they are.`);
29586
- if (!confirmed) {
29587
- console.log("Cancelled.");
29588
- return;
29589
- }
29590
- }
29591
- const trashed = moveToTrash(gitdir, `runwork spaces reset ${found.folder}`);
29592
- if (trashed === null) {
29593
- console.error(`Error: ${gitdir} could not be moved to ${join64(homedir39(), ".runwork", "trash")}, so nothing was removed.`);
29993
+ if (discarded.state === "stuck") {
29994
+ console.error(`Error: ${gitdir} could not be moved to ${discarded.trashRoot}, so nothing was removed.`);
29594
29995
  process.exit(1);
29595
29996
  }
29596
- forgetWaitingCache(found.folder);
29597
29997
  if (json) {
29598
- jsonOut({ folder: found.folder, spaceId: found.mapping.spaceId, spaceName: found.mapping.spaceName, gitdir, removed: true, bytes: weight.bytes, files: weight.files, trash: trashed });
29998
+ jsonOut({ folder: found.folder, spaceId: found.mapping.spaceId, spaceName: found.mapping.spaceName, gitdir, removed: true, bytes: discarded.bytes, files: discarded.files, trash: discarded.trash });
29599
29999
  return;
29600
30000
  }
29601
- console.log(green(`${found.folder} starts over with ${found.mapping.spaceName}.`) + dim(` ${formatBytes(weight.bytes)} of history moved to ${trashed}`));
30001
+ console.log(green(`${found.folder} starts over with ${found.mapping.spaceName}.`) + dim(` ${formatBytes(discarded.bytes)} of history moved to ${discarded.trash}`));
29602
30002
  console.log(dim(`Nothing in your folder was touched. Rebuild from the space with: runwork spaces sync ${found.folder}`));
29603
30003
  console.log(dim(`If the pushes were being refused for size, narrow what this folder shares first: runwork spaces files ${found.folder}`));
29604
30004
  });
@@ -29863,9 +30263,8 @@ function routingTable(surface) {
29863
30263
  ];
29864
30264
  }
29865
30265
  var PRODUCT_DEV_EXCEPTION = "**Exception -- product development:** Editing your current codebase, running tests, git operations, and building features in the product you're working on are NOT Runwork territory. Runwork is for infrastructure and team tooling, not the product itself.";
29866
- var ROUTING_PROSE = `Save or update a skill with \`save_skill\`, not local SKILL.md files. Store structured data with \`entity_{Name}\` tools, not SQLite or JSON files. Schedule recurring jobs as Runwork schedules (\`schedule_{name}\`), not OS cron. Run multi-step processes as Runwork workflows (\`workflow_{name}\`), not ad-hoc scripts. Call third-party services (Slack, HubSpot, Gmail, Google) through \`{id}_api\` tools, not raw \`fetch\`. ${SPACES_ROUTING_SENTENCE} Store other files in Runwork file storage, not the local filesystem. Expose public APIs as Runwork endpoints (\`endpoint_*\`), not standalone servers.`;
29867
30266
  function buildRunworkInstructions(ctx) {
29868
- const { workspaceName, workspaceId, workspaceSlug, inventory, surface = "machine", form = "full" } = ctx;
30267
+ const { workspaceName, workspaceId, workspaceSlug, inventory, surface = "machine" } = ctx;
29869
30268
  const dashboardUrl = workspaceSlug ? `https://${workspaceSlug}.runwork.ai` : "https://runwork.ai";
29870
30269
  const workspaceIdLine = `Workspace ID: \`${workspaceId}\`${workspaceSlug ? ` | Dashboard: ${dashboardUrl}` : ""}`;
29871
30270
  const connection = surface === "mcp" ? `You are connected to the **${workspaceName}** Runwork workspace` : `This machine is connected to the **${workspaceName}** Runwork workspace`;
@@ -29878,11 +30277,7 @@ function buildRunworkInstructions(ctx) {
29878
30277
  workspaceIdLine,
29879
30278
  ""
29880
30279
  ];
29881
- if (form === "compact") {
29882
- lines.push(`**Default to Runwork -- team sync beats local files.** ${ROUTING_PROSE} These sync across every teammate's AI agents; local alternatives only work for you.`, "", PRODUCT_DEV_EXCEPTION, "", `**Available now:** ${buildInventoryLine(inventory)}`, "", `Runwork MCP tools are always connected -- use them for data access, integration API calls, skill management, and resource discovery.${integrationToolHint} Web dashboard: ${dashboardUrl}. Invoke the \`runwork\` skill for the full capability reference, or run \`runwork doctor\` to check setup.`, ...buildPersonaBlock(ctx.persona));
29883
- } else {
29884
- lines.push("**Default to Runwork -- team sync beats local files.** When the user asks for any of the following, use the Runwork tool shown. These operations sync across every teammate's AI agents; local alternatives only work for the current user.", "", ...routingTable(surface), "", ...spacesInstructionBlock(surface), "", PRODUCT_DEV_EXCEPTION, "", `**Available now:** ${buildInventoryLine(inventory)}`, "", `Runwork MCP tools are always connected -- use them for data access, integration API calls, skill management, and resource discovery.${integrationToolHint} Use \`runwork\` CLI to create (\`runwork init\`), develop (\`runwork dev\`), and deploy (\`runwork deploy\`) apps. Install CLI: \`curl -fsSL https://runwork.ai/install.sh | sh\`. Web dashboard: ${dashboardUrl}. Invoke the \`runwork\` skill for full capability reference.`, ...buildPersonaBlock(ctx.persona));
29885
- }
30280
+ lines.push("**Default to Runwork -- team sync beats local files.** When the user asks for any of the following, use the Runwork tool shown. These operations sync across every teammate's AI agents; local alternatives only work for the current user.", "", ...routingTable(surface), "", ...spacesInstructionBlock(surface), "", PRODUCT_DEV_EXCEPTION, "", `**Available now:** ${buildInventoryLine(inventory)}`, "", `Runwork MCP tools are always connected -- use them for data access, integration API calls, skill management, and resource discovery.${integrationToolHint} Use \`runwork\` CLI to create (\`runwork init\`), develop (\`runwork dev\`), and deploy (\`runwork deploy\`) apps. Install CLI: \`curl -fsSL https://runwork.ai/install.sh | sh\`. Web dashboard: ${dashboardUrl}. Invoke the \`runwork\` skill for full capability reference, or run \`runwork doctor\` to check setup.`, ...buildPersonaBlock(ctx.persona));
29886
30281
  return lines.join(`
29887
30282
  `);
29888
30283
  }
@@ -30229,15 +30624,14 @@ function generateIntroSkill(ctx) {
30229
30624
  description: "TRIGGER when user asks to: save/create/share/update a skill, store or persist data, schedule a recurring job, run a workflow or pipeline, call Slack/HubSpot/Gmail/any third-party integration, upload or persist a file, share team knowledge or expertise, connect AI tools via MCP, expose a public API, or build a team app or internal tool. Runwork is the team's shared infrastructure platform -- these operations sync across every teammate's AI agents, while local files only work for the current user. Always invoke BEFORE reaching for local files, native cron, SQLite, raw fetch calls, or one-off scripts. Features: skills, MCP servers, entities, file storage, schedules, workflows, integrations, agents, public endpoints, components. Interact via MCP tools (always available), CLI (`runwork`), web dashboard, desktop app, or API."
30230
30625
  };
30231
30626
  }
30232
- function generateInstructionHint(ctx, form = "full") {
30627
+ function generateInstructionHint(ctx) {
30233
30628
  const body = buildRunworkInstructions({
30234
30629
  workspaceName: ctx.workspaceName,
30235
30630
  workspaceId: ctx.workspaceId,
30236
30631
  workspaceSlug: ctx.workspaceSlug,
30237
30632
  inventory: toRunworkInventory(ctx),
30238
30633
  persona: ctx.persona,
30239
- surface: "machine",
30240
- form
30634
+ surface: "machine"
30241
30635
  });
30242
30636
  return [
30243
30637
  "<!-- runwork:start -->",
@@ -30288,7 +30682,7 @@ function readSetupExtras(workspaceId) {
30288
30682
  }
30289
30683
  return {};
30290
30684
  }
30291
- var instructionsCommand = new Command17("instructions").description("Print the Runwork instruction block sync writes into each agent's config").option("--workspace <name-or-id>", "Workspace name or ID").option("--form <full|compact>", "Which rendering to print (default: full)", "full").action(async (opts, command) => {
30685
+ var instructionsCommand = new Command17("instructions").description("Print the Runwork instruction block sync writes into each agent's config").option("--workspace <name-or-id>", "Workspace name or ID").action(async (opts, command) => {
30292
30686
  const useJson = shouldOutputJson(command.optsWithGlobals().json);
30293
30687
  const credentials = requireAuth();
30294
30688
  const client = new ApiClient(credentials);
@@ -30300,18 +30694,16 @@ var instructionsCommand = new Command17("instructions").description("Print the R
30300
30694
  workspaceSlug: extras.workspaceSlug,
30301
30695
  persona: extras.persona
30302
30696
  });
30303
- const full = generateInstructionHint(ctx, "full");
30304
- const compact = generateInstructionHint(ctx, "compact");
30697
+ const block = generateInstructionHint(ctx);
30305
30698
  if (useJson) {
30306
30699
  jsonOut({
30307
30700
  workspaceId,
30308
30701
  workspaceName: ctx.workspaceName,
30309
- [CLAUDE_ACCOUNT_PROFILE_FIELDS.full]: full,
30310
- [CLAUDE_ACCOUNT_PROFILE_FIELDS.compact]: compact
30702
+ [CLAUDE_ACCOUNT_PROFILE_FIELD]: block
30311
30703
  });
30312
30704
  return;
30313
30705
  }
30314
- process.stdout.write((opts.form === "compact" ? compact : full) + `
30706
+ process.stdout.write(block + `
30315
30707
  `);
30316
30708
  });
30317
30709
 
@@ -32978,7 +33370,7 @@ function ensureWorkspacePointer(state, statePath2, credentials) {
32978
33370
  async function syncFromState(state, statePath2, credentials, opts) {
32979
33371
  const acquired = await acquireSyncLock();
32980
33372
  if (!acquired) {
32981
- console.log(" Another sync appears to be in progress on this machine; proceeding anyway.");
33373
+ console.log(" This machine's sync lock could not be taken, and nothing is holding it; proceeding anyway.");
32982
33374
  await runSyncFromState(state, statePath2, credentials, opts);
32983
33375
  return;
32984
33376
  }
@@ -35798,6 +36190,7 @@ function buildCapturePlan(input) {
35798
36190
  const home2 = input.homeDir.replace(/[/\\]+$/, "");
35799
36191
  const rw = (...parts) => join75(home2, ".runwork", ...parts);
35800
36192
  const steps = [];
36193
+ const storePathsInput = { platform: input.platform, homeDir: home2, sep: sep12, appData: input.appData, localAppData: input.localAppData };
35801
36194
  steps.push({ id: "env", kind: "env", names: ENV_NAMES, note: "process environment (proxy credentials stripped)" });
35802
36195
  for (const [id, file] of [
35803
36196
  ["runwork.setup", "setup.json"],
@@ -35818,13 +36211,28 @@ function buildCapturePlan(input) {
35818
36211
  steps.push({ id, kind: "stat", path: rw(file), note: "size and mtime only: contains conversation text" });
35819
36212
  }
35820
36213
  steps.push({ id: "runwork.dir", kind: "census", path: rw(), depth: 1, maxEntries: CENSUS_MAX_ENTRIES }, { id: "runwork.bin", kind: "census", path: rw("bin"), depth: 1, maxEntries: 50, note: "which CLI binaries are installed here" }, { id: "runwork.sessions", kind: "census", path: rw("sessions"), depth: 1, maxEntries: CENSUS_MAX_ENTRIES, note: "written by the agent hooks we install" }, { id: "runwork.waiting", kind: "census", path: rw("waiting"), depth: 1, maxEntries: CENSUS_MAX_ENTRIES, note: "size and mtime only: each holds a mapped folder's own file names" }, { id: "runwork.log", kind: "file", path: rw("desktop-launch.log"), maxBytes: LOG_TAIL_BYTES, tail: true, note: "tail: this log has no rotation" });
35821
- const storeInput = { platform: input.platform, homeDir: home2, sep: sep12, appData: input.appData, localAppData: input.localAppData };
36214
+ desktopStateDirs(storePathsInput).forEach((dir, i) => {
36215
+ steps.push({
36216
+ id: i === 0 ? "desktop.app-state" : `desktop.app-state.alt${i - 1}`,
36217
+ kind: "file",
36218
+ path: join75(dir, "app-state.json"),
36219
+ maxBytes: CONFIG_MAX_BYTES,
36220
+ note: "desktop state: onboarding, updater, the last vendor account-write refusal"
36221
+ });
36222
+ steps.push({
36223
+ id: i === 0 ? "desktop.window-state" : `desktop.window-state.alt${i - 1}`,
36224
+ kind: "file",
36225
+ path: join75(dir, ".window-state.json"),
36226
+ maxBytes: CONFIG_MAX_BYTES,
36227
+ note: "window geometry: size, position, maximized, fullscreen"
36228
+ });
36229
+ });
35822
36230
  for (const target of CAPTURE_TARGETS) {
35823
36231
  const note = target.note ? { note: target.note } : {};
35824
- steps.push(target.kind === "file" ? { id: target.id, kind: "file", path: target.path(storeInput), maxBytes: target.maxBytes ?? CONFIG_MAX_BYTES, ...note } : { id: target.id, kind: "census", path: target.path(storeInput), depth: 1, maxEntries: CENSUS_MAX_ENTRIES, ...note });
36232
+ steps.push(target.kind === "file" ? { id: target.id, kind: "file", path: target.path(storePathsInput), maxBytes: target.maxBytes ?? CONFIG_MAX_BYTES, ...note } : { id: target.id, kind: "census", path: target.path(storePathsInput), depth: 1, maxEntries: CENSUS_MAX_ENTRIES, ...note });
35825
36233
  }
35826
36234
  steps.push({ id: "agents.skills", kind: "census", path: join75(home2, ".agents", "skills"), depth: 1, maxEntries: CENSUS_MAX_ENTRIES });
35827
- for (const store of resolveTranscriptStores(storeInput)) {
36235
+ for (const store of resolveTranscriptStores(storePathsInput)) {
35828
36236
  const id = `stores.${store.def.id.replace(/\//g, ".")}`;
35829
36237
  steps.push(store.def.kind === "file" ? { id, kind: "stat", path: store.path, ...store.def.note ? { note: store.def.note } : {} } : { id, kind: "census", path: store.path, depth: 2, maxEntries: CENSUS_MAX_ENTRIES, ...store.def.note ? { note: store.def.note } : {} });
35830
36238
  for (const parent of parentsWorthProbing(store.path, sep12)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runwork",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "CLI for Runwork: develop, preview, and deploy Runwork apps from your local machine.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Runwork, Inc. <info@runwork.ai> (https://www.runwork.ai)",