runwork 0.24.1 → 0.25.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 +958 -107
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2169,7 +2169,7 @@ var init_agent_guidance = __esm(() => {
2169
2169
  "worker/components.ts": "Reusable UI components other workspace apps can embed.",
2170
2170
  "shared/types.ts": "TypeScript interfaces shared between frontend and backend.",
2171
2171
  "src/pages/": "Frontend React pages. Each file becomes a route.",
2172
- "blueprint.json": "App feature registry. Update after adding entities, workflows, agents, etc.",
2172
+ "blueprint.json": "App feature registry. Update after adding entities, workflows, agents, etc., including each one's valueProfile (what manual work it replaced).",
2173
2173
  "CLAUDE.md": "Complete framework documentation. Read this before editing anything."
2174
2174
  };
2175
2175
  COMMON_TIPS = [
@@ -2180,6 +2180,7 @@ var init_agent_guidance = __esm(() => {
2180
2180
  "Every workflow MUST have a trigger (API endpoint, scheduled job, or UI button). Workflows without triggers are dead code.",
2181
2181
  "Every conversational agent MUST have a frontend page to access it.",
2182
2182
  "After adding entities, workflows, or agents, update blueprint.json with the new metadata.",
2183
+ "When you add a feature that takes over manual work, ASK your human what they used to do by hand, how often THEY did it, and how long one round took, then record it as a valueProfile on that blueprint element. Set confirmedWithHuman only if they actually agreed to the numbers. If they cannot answer, omit valueProfile rather than guessing. See the valueProfile section in CLAUDE.md.",
2183
2184
  "Never guess integration IDs. Run: runwork integrations search <query>"
2184
2185
  ];
2185
2186
  });
@@ -7839,7 +7840,7 @@ function createKeyboardListener() {
7839
7840
  }
7840
7841
 
7841
7842
  // src/generated/version.ts
7842
- var VERSION = "0.24.1";
7843
+ var VERSION = "0.25.0";
7843
7844
 
7844
7845
  // src/commands/dev.ts
7845
7846
  var exports_dev = {};
@@ -9747,6 +9748,187 @@ var init_registry = __esm(() => {
9747
9748
  });
9748
9749
 
9749
9750
  // src/agents/utils/session-digest.ts
9751
+ function emptyGapBuckets() {
9752
+ return { lt10s: 0, s10to30: 0, s30to2m: 0, m2to5: 0, m5to15: 0, gte15m: 0 };
9753
+ }
9754
+ function gapBucketKey(seconds) {
9755
+ if (seconds < 10)
9756
+ return "lt10s";
9757
+ if (seconds < 30)
9758
+ return "s10to30";
9759
+ if (seconds < 120)
9760
+ return "s30to2m";
9761
+ if (seconds < 300)
9762
+ return "m2to5";
9763
+ if (seconds < 900)
9764
+ return "m5to15";
9765
+ return "gte15m";
9766
+ }
9767
+ function buildGapHistogram(epochMs) {
9768
+ const ts = epochMs.filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
9769
+ if (ts.length < 2)
9770
+ return null;
9771
+ const gapCounts = emptyGapBuckets();
9772
+ const gapSeconds = emptyGapBuckets();
9773
+ let longest = 0;
9774
+ for (let i = 1;i < ts.length; i++) {
9775
+ const gap = (ts[i] - ts[i - 1]) / 1000;
9776
+ const key = gapBucketKey(gap);
9777
+ gapCounts[key]++;
9778
+ gapSeconds[key] += gap;
9779
+ if (gap > longest)
9780
+ longest = gap;
9781
+ }
9782
+ for (const key of Object.keys(gapSeconds)) {
9783
+ gapSeconds[key] = Math.round(gapSeconds[key]);
9784
+ }
9785
+ return {
9786
+ gapCounts,
9787
+ gapSeconds,
9788
+ spanMinutes: Math.round((ts[ts.length - 1] - ts[0]) / 6000) / 10,
9789
+ longestGapSeconds: Math.round(longest)
9790
+ };
9791
+ }
9792
+ function emptySessionSignals() {
9793
+ return {
9794
+ provenance: null,
9795
+ gapCounts: null,
9796
+ gapSeconds: null,
9797
+ spanMinutes: null,
9798
+ longestGapSeconds: null,
9799
+ permissionModes: null,
9800
+ modeEscalations: null,
9801
+ approvalPolicy: null,
9802
+ sandboxPolicy: null,
9803
+ subagentCount: null,
9804
+ planModeUsed: null,
9805
+ slashCommandCount: null,
9806
+ modelInitiatedSkillCount: null,
9807
+ promptLengthBuckets: null,
9808
+ toolFailureStreakMax: null,
9809
+ repeatedCallMax: null,
9810
+ models: null,
9811
+ tokens: null,
9812
+ tokensByModel: null
9813
+ };
9814
+ }
9815
+ function applyGapHistogram(signals, epochMs) {
9816
+ const h = buildGapHistogram(epochMs);
9817
+ if (!h)
9818
+ return;
9819
+ signals.gapCounts = h.gapCounts;
9820
+ signals.gapSeconds = h.gapSeconds;
9821
+ signals.spanMinutes = h.spanMinutes;
9822
+ signals.longestGapSeconds = h.longestGapSeconds;
9823
+ }
9824
+ function promptLengthBucket(len) {
9825
+ if (len < 120)
9826
+ return "short";
9827
+ if (len <= 600)
9828
+ return "medium";
9829
+ return "long";
9830
+ }
9831
+ function classifyClaudeProvenance(obj) {
9832
+ const entrypoint = typeof obj.entrypoint === "string" ? obj.entrypoint : null;
9833
+ const promptSource = typeof obj.promptSource === "string" ? obj.promptSource : null;
9834
+ const originKind = obj.origin?.kind ?? null;
9835
+ if (entrypoint === "sdk-cli" || entrypoint === "sdk-ts")
9836
+ return "headless";
9837
+ if (promptSource === "sdk")
9838
+ return "headless";
9839
+ if (originKind !== null && originKind !== "human")
9840
+ return "agent-spawned";
9841
+ if (entrypoint === "cli" || entrypoint === "claude-desktop" || entrypoint === "local-agent")
9842
+ return "user-driven";
9843
+ if (originKind === "human" || promptSource === "typed")
9844
+ return "user-driven";
9845
+ return null;
9846
+ }
9847
+ function sumTokenTotals(byModel) {
9848
+ let tokensIn = 0;
9849
+ let tokensOut = 0;
9850
+ let cacheReadTokens = null;
9851
+ let cacheCreationTokens = null;
9852
+ for (const t of byModel.values()) {
9853
+ tokensIn += t.tokensIn;
9854
+ tokensOut += t.tokensOut;
9855
+ if (t.cacheReadTokens !== null)
9856
+ cacheReadTokens = (cacheReadTokens ?? 0) + t.cacheReadTokens;
9857
+ if (t.cacheCreationTokens !== null)
9858
+ cacheCreationTokens = (cacheCreationTokens ?? 0) + t.cacheCreationTokens;
9859
+ }
9860
+ return { tokensIn, tokensOut, cacheReadTokens, cacheCreationTokens };
9861
+ }
9862
+ function applyTokenMap(signals, byModel) {
9863
+ if (byModel.size === 0)
9864
+ return;
9865
+ signals.models = [...byModel.keys()];
9866
+ signals.tokens = sumTokenTotals(byModel);
9867
+ signals.tokensByModel = Object.fromEntries(byModel);
9868
+ }
9869
+ function stableStringify(value) {
9870
+ if (Array.isArray(value))
9871
+ return "[" + value.map(stableStringify).join(",") + "]";
9872
+ if (value && typeof value === "object") {
9873
+ return "{" + Object.keys(value).sort().map((k) => JSON.stringify(k) + ":" + stableStringify(value[k])).join(",") + "}";
9874
+ }
9875
+ return JSON.stringify(value) ?? "undefined";
9876
+ }
9877
+
9878
+ class FrictionCounters {
9879
+ failStreak = 0;
9880
+ failStreakMax = 0;
9881
+ callCounts = new Map;
9882
+ repeatedCallMax = 0;
9883
+ result(failed) {
9884
+ if (failed) {
9885
+ this.failStreak++;
9886
+ if (this.failStreak > this.failStreakMax)
9887
+ this.failStreakMax = this.failStreak;
9888
+ } else {
9889
+ this.failStreak = 0;
9890
+ }
9891
+ }
9892
+ call(name, args) {
9893
+ const key = name + "\x00" + stableStringify(args ?? null);
9894
+ const n = (this.callCounts.get(key) ?? 0) + 1;
9895
+ this.callCounts.set(key, n);
9896
+ if (n > this.repeatedCallMax)
9897
+ this.repeatedCallMax = n;
9898
+ }
9899
+ }
9900
+ function recordAssetAttempt(acc, rawName, epochMs, turnsBefore, toolCallsBefore) {
9901
+ const existing = acc.get(rawName);
9902
+ if (existing) {
9903
+ existing.attempts++;
9904
+ return;
9905
+ }
9906
+ acc.set(rawName, { firstEpochMs: epochMs, attempts: 1, turnsBefore, toolCallsBefore });
9907
+ }
9908
+ function buildAssetMarkers(acc, epochMs) {
9909
+ if (acc.size === 0)
9910
+ return;
9911
+ const sorted = epochMs.filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
9912
+ const start = sorted[0];
9913
+ const markers = [];
9914
+ for (const [rawName, m] of acc) {
9915
+ const first = m.firstEpochMs;
9916
+ const histogram = first !== null ? buildGapHistogram(sorted.filter((ms) => ms <= first)) : null;
9917
+ markers.push({
9918
+ kind: "skill",
9919
+ rawName,
9920
+ attempts: m.attempts,
9921
+ turnsBefore: m.turnsBefore,
9922
+ toolCallsBefore: m.toolCallsBefore,
9923
+ wallSecondsToAsset: first !== null && sorted.length > 0 ? Math.round((first - start) / 1000) : null,
9924
+ gapCountsToAsset: histogram?.gapCounts ?? null,
9925
+ gapSecondsToAsset: histogram?.gapSeconds ?? null
9926
+ });
9927
+ if (markers.length >= 8)
9928
+ break;
9929
+ }
9930
+ return markers;
9931
+ }
9750
9932
  function extractClaudeJsonlSession(raw, agentSlug, project) {
9751
9933
  const digest = {
9752
9934
  agentSlug,
@@ -9760,6 +9942,21 @@ function extractClaudeJsonlSession(raw, agentSlug, project) {
9760
9942
  errorCount: 0,
9761
9943
  assistantTurns: 0
9762
9944
  };
9945
+ const signals = emptySessionSignals();
9946
+ const epochMs = [];
9947
+ const modeStream = [];
9948
+ let subagentCount = 0;
9949
+ let planToolSeen = false;
9950
+ let slashCommandCount = 0;
9951
+ let modelInitiatedSkillCount = 0;
9952
+ const promptBuckets = { short: 0, medium: 0, long: 0 };
9953
+ const tokensByModel = new Map;
9954
+ const seenUsagePairs = new Set;
9955
+ let provenanceClassified = false;
9956
+ let sidechainUserSeen = false;
9957
+ let toolCallsSeen = 0;
9958
+ const assetAcc = new Map;
9959
+ const friction = new FrictionCounters;
9763
9960
  for (const line of raw.split(`
9764
9961
  `)) {
9765
9962
  if (!line.trim())
@@ -9775,12 +9972,29 @@ function extractClaudeJsonlSession(raw, agentSlug, project) {
9775
9972
  if (!digest.start)
9776
9973
  digest.start = ts;
9777
9974
  digest.end = ts;
9975
+ const ms = Date.parse(ts);
9976
+ if (Number.isFinite(ms))
9977
+ epochMs.push(ms);
9778
9978
  }
9779
9979
  const type = obj.type;
9980
+ if (type === "permission-mode" && typeof obj.permissionMode === "string") {
9981
+ modeStream.push(obj.permissionMode);
9982
+ continue;
9983
+ }
9780
9984
  const message = obj.message;
9781
9985
  if (!message || typeof message !== "object")
9782
9986
  continue;
9783
9987
  if (type === "user" && message.role === "user") {
9988
+ if (obj.isSidechain === true)
9989
+ sidechainUserSeen = true;
9990
+ else if (!provenanceClassified) {
9991
+ provenanceClassified = true;
9992
+ signals.provenance = classifyClaudeProvenance(obj);
9993
+ }
9994
+ if (typeof obj.permissionMode === "string")
9995
+ modeStream.push(obj.permissionMode);
9996
+ const originKind = obj.origin?.kind;
9997
+ const humanLine = obj.isSidechain !== true && (originKind === undefined || originKind === "human");
9784
9998
  const texts = [];
9785
9999
  if (typeof message.content === "string") {
9786
10000
  texts.push(message.content);
@@ -9790,15 +10004,22 @@ function extractClaudeJsonlSession(raw, agentSlug, project) {
9790
10004
  const it = item;
9791
10005
  if (it.type === "text" && typeof it.text === "string")
9792
10006
  texts.push(it.text);
9793
- if (it.type === "tool_result" && it.is_error === true)
9794
- digest.errorCount++;
10007
+ if (it.type === "tool_result") {
10008
+ if (it.is_error === true)
10009
+ digest.errorCount++;
10010
+ friction.result(it.is_error === true);
10011
+ }
9795
10012
  }
9796
10013
  }
9797
10014
  }
9798
10015
  for (const text2 of texts) {
9799
10016
  const trimmed = text2.trim();
10017
+ if (trimmed.startsWith("<command-name>"))
10018
+ slashCommandCount++;
9800
10019
  if (!trimmed || SKIP_PREFIXES.some((p) => trimmed.startsWith(p)))
9801
10020
  continue;
10021
+ if (humanLine)
10022
+ promptBuckets[promptLengthBucket(trimmed.length)]++;
9802
10023
  if (digest.userMessages.length >= MAX_MSGS_PER_SESSION) {
9803
10024
  digest.droppedUserMessages++;
9804
10025
  continue;
@@ -9806,15 +10027,47 @@ function extractClaudeJsonlSession(raw, agentSlug, project) {
9806
10027
  digest.userMessages.push(trimmed.length > MAX_MSG_CHARS ? trimmed.slice(0, MAX_MSG_CHARS) + " [...]" : trimmed);
9807
10028
  }
9808
10029
  }
9809
- if (type === "assistant" && message.role === "assistant" && Array.isArray(message.content)) {
10030
+ if (type === "assistant" && message.role === "assistant") {
10031
+ const am = message;
10032
+ const model = typeof am.model === "string" ? am.model : null;
10033
+ const usage = am.usage;
10034
+ if (usage && typeof usage === "object" && model && model !== "<synthetic>") {
10035
+ const requestId = typeof obj.requestId === "string" ? obj.requestId : null;
10036
+ const pairKey = typeof am.id === "string" && requestId ? `${am.id}:${requestId}` : null;
10037
+ if (!pairKey || !seenUsagePairs.has(pairKey)) {
10038
+ if (pairKey)
10039
+ seenUsagePairs.add(pairKey);
10040
+ const t = tokensByModel.get(model) ?? { tokensIn: 0, tokensOut: 0, cacheReadTokens: 0, cacheCreationTokens: 0 };
10041
+ t.tokensIn += finiteNum(usage.input_tokens);
10042
+ t.tokensOut += finiteNum(usage.output_tokens);
10043
+ t.cacheReadTokens = (t.cacheReadTokens ?? 0) + finiteNum(usage.cache_read_input_tokens);
10044
+ t.cacheCreationTokens = (t.cacheCreationTokens ?? 0) + finiteNum(usage.cache_creation_input_tokens);
10045
+ tokensByModel.set(model, t);
10046
+ }
10047
+ }
10048
+ if (!Array.isArray(message.content))
10049
+ continue;
9810
10050
  digest.assistantTurns++;
9811
10051
  for (const item of message.content) {
9812
10052
  if (item && typeof item === "object") {
9813
10053
  const it = item;
9814
10054
  if (it.type === "tool_use" && typeof it.name === "string") {
9815
10055
  digest.toolCounts[it.name] = (digest.toolCounts[it.name] ?? 0) + 1;
9816
- if (it.name === "Skill" && it.input?.skill)
9817
- digest.skillInvocations.push(it.input.skill);
10056
+ friction.call(it.name, it.input);
10057
+ if (it.name === "Agent" || it.name === "Task")
10058
+ subagentCount++;
10059
+ if (it.name === "EnterPlanMode" || it.name === "ExitPlanMode")
10060
+ planToolSeen = true;
10061
+ if (it.name === "Skill") {
10062
+ modelInitiatedSkillCount++;
10063
+ if (it.input?.skill)
10064
+ digest.skillInvocations.push(it.input.skill);
10065
+ }
10066
+ if (it.name.endsWith("__save_skill") && typeof it.input?.name === "string" && it.input.name.trim()) {
10067
+ const ms = ts ? Date.parse(ts) : NaN;
10068
+ recordAssetAttempt(assetAcc, it.input.name.trim(), Number.isFinite(ms) ? ms : null, Math.max(0, digest.assistantTurns - 1), toolCallsSeen);
10069
+ }
10070
+ toolCallsSeen++;
9818
10071
  }
9819
10072
  }
9820
10073
  }
@@ -9822,11 +10075,40 @@ function extractClaudeJsonlSession(raw, agentSlug, project) {
9822
10075
  }
9823
10076
  if (digest.userMessages.length === 0)
9824
10077
  return null;
10078
+ if (!provenanceClassified && sidechainUserSeen)
10079
+ signals.provenance = "agent-spawned";
10080
+ applyGapHistogram(signals, epochMs);
10081
+ const collapsed = modeStream.filter((m, i) => i === 0 || m !== modeStream[i - 1]);
10082
+ signals.permissionModes = collapsed.length > 0 ? [...new Set(collapsed)] : null;
10083
+ signals.modeEscalations = collapsed.length > 0 ? collapsed.length - 1 : null;
10084
+ signals.subagentCount = subagentCount;
10085
+ signals.planModeUsed = planToolSeen || collapsed.includes("plan");
10086
+ signals.slashCommandCount = slashCommandCount;
10087
+ signals.modelInitiatedSkillCount = modelInitiatedSkillCount;
10088
+ signals.promptLengthBuckets = promptBuckets;
10089
+ signals.toolFailureStreakMax = friction.failStreakMax;
10090
+ signals.repeatedCallMax = friction.repeatedCallMax;
10091
+ applyTokenMap(signals, tokensByModel);
10092
+ digest.signals = signals;
10093
+ digest.assetMarkers = buildAssetMarkers(assetAcc, epochMs);
9825
10094
  return digest;
9826
10095
  }
9827
10096
  function decodeProjectDir(encoded) {
9828
10097
  return encoded.replace(/^-/, "/").replace(/-/g, "/");
9829
10098
  }
10099
+ function classifyCodexProvenance(meta) {
10100
+ const source = meta.source;
10101
+ const originator = typeof meta.originator === "string" ? meta.originator : null;
10102
+ if (source && typeof source === "object" && "subagent" in source)
10103
+ return "agent-spawned";
10104
+ if (originator === "Claude Code")
10105
+ return "agent-spawned";
10106
+ if (source === "exec")
10107
+ return "headless";
10108
+ if (source === "cli" || source === "vscode")
10109
+ return "user-driven";
10110
+ return null;
10111
+ }
9830
10112
  function geminiMessageText(content) {
9831
10113
  if (typeof content === "string")
9832
10114
  return content;
@@ -9862,6 +10144,9 @@ function extractGeminiSession(raw, agentSlug, project) {
9862
10144
  digest.start = session.startTime;
9863
10145
  if (typeof session.lastUpdated === "string")
9864
10146
  digest.end = session.lastUpdated;
10147
+ const signals = emptySessionSignals();
10148
+ const epochMs = [];
10149
+ const tokensByModel = new Map;
9865
10150
  for (const raw2 of session.messages) {
9866
10151
  const m = raw2;
9867
10152
  if (typeof m.timestamp === "string") {
@@ -9869,9 +10154,21 @@ function extractGeminiSession(raw, agentSlug, project) {
9869
10154
  digest.start = m.timestamp;
9870
10155
  if (!digest.end || m.timestamp > digest.end)
9871
10156
  digest.end = m.timestamp;
10157
+ const ms = Date.parse(m.timestamp);
10158
+ if (Number.isFinite(ms))
10159
+ epochMs.push(ms);
9872
10160
  }
9873
10161
  if (m.type === "gemini") {
9874
10162
  digest.assistantTurns++;
10163
+ const tok = m.tokens;
10164
+ if (tok && typeof tok === "object") {
10165
+ const model = typeof m.model === "string" ? m.model : "unknown";
10166
+ const t = tokensByModel.get(model) ?? { tokensIn: 0, tokensOut: 0, cacheReadTokens: 0, cacheCreationTokens: null };
10167
+ t.tokensIn += finiteNum(tok.input);
10168
+ t.tokensOut += finiteNum(tok.output) + finiteNum(tok.thoughts);
10169
+ t.cacheReadTokens = (t.cacheReadTokens ?? 0) + finiteNum(tok.cached);
10170
+ tokensByModel.set(model, t);
10171
+ }
9875
10172
  continue;
9876
10173
  }
9877
10174
  if (m.type === "error") {
@@ -9889,7 +10186,12 @@ function extractGeminiSession(raw, agentSlug, project) {
9889
10186
  }
9890
10187
  digest.userMessages.push(text2.length > MAX_MSG_CHARS ? `${text2.slice(0, MAX_MSG_CHARS)}...` : text2);
9891
10188
  }
9892
- return digest.userMessages.length > 0 ? digest : null;
10189
+ if (digest.userMessages.length === 0)
10190
+ return null;
10191
+ applyGapHistogram(signals, epochMs);
10192
+ applyTokenMap(signals, tokensByModel);
10193
+ digest.signals = signals;
10194
+ return digest;
9893
10195
  }
9894
10196
  function extractCodexRolloutSession(raw, agentSlug) {
9895
10197
  const digest = {
@@ -9904,6 +10206,14 @@ function extractCodexRolloutSession(raw, agentSlug) {
9904
10206
  errorCount: 0,
9905
10207
  assistantTurns: 0
9906
10208
  };
10209
+ const signals = emptySessionSignals();
10210
+ const epochMs = [];
10211
+ const models = [];
10212
+ let lastTokenTotals = null;
10213
+ let toolCallsSeen = 0;
10214
+ const assetAcc = new Map;
10215
+ const friction = new FrictionCounters;
10216
+ let failureMarkerSeen = false;
9907
10217
  for (const line of raw.split(`
9908
10218
  `)) {
9909
10219
  if (!line.trim())
@@ -9919,12 +10229,49 @@ function extractCodexRolloutSession(raw, agentSlug) {
9919
10229
  if (!digest.start)
9920
10230
  digest.start = ts;
9921
10231
  digest.end = ts;
10232
+ const ms = Date.parse(ts);
10233
+ if (Number.isFinite(ms))
10234
+ epochMs.push(ms);
9922
10235
  }
9923
10236
  const p = o.payload;
9924
10237
  if (!p || typeof p !== "object")
9925
10238
  continue;
9926
- if (o.type === "session_meta" && typeof p.cwd === "string") {
9927
- digest.project = p.cwd;
10239
+ if (o.type === "session_meta") {
10240
+ if (typeof p.cwd === "string")
10241
+ digest.project = p.cwd;
10242
+ signals.provenance = classifyCodexProvenance(p);
10243
+ continue;
10244
+ }
10245
+ if (o.type === "turn_context") {
10246
+ const ap = p.approval_policy;
10247
+ if (typeof ap === "string")
10248
+ signals.approvalPolicy = ap;
10249
+ else if (ap && typeof ap === "object")
10250
+ signals.approvalPolicy = Object.keys(ap)[0] ?? signals.approvalPolicy;
10251
+ const sp = p.sandbox_policy;
10252
+ if (sp && typeof sp === "object" && typeof sp.type === "string")
10253
+ signals.sandboxPolicy = sp.type;
10254
+ if (typeof p.model === "string" && !models.includes(p.model))
10255
+ models.push(p.model);
10256
+ continue;
10257
+ }
10258
+ if (o.type === "event_msg") {
10259
+ if (p.type === "token_count") {
10260
+ const info = p.info;
10261
+ if (info && typeof info === "object" && info.total_token_usage && typeof info.total_token_usage === "object") {
10262
+ lastTokenTotals = info.total_token_usage;
10263
+ }
10264
+ } else if (p.type === "exec_command_end") {
10265
+ failureMarkerSeen = true;
10266
+ friction.result(typeof p.exit_code === "number" && p.exit_code !== 0);
10267
+ } else if (p.type === "mcp_tool_call_end") {
10268
+ const result = p.result;
10269
+ failureMarkerSeen = true;
10270
+ friction.result(Boolean(result && typeof result === "object" && "Err" in result));
10271
+ } else if (p.type === "patch_apply_end") {
10272
+ failureMarkerSeen = true;
10273
+ friction.result(p.success === false);
10274
+ }
9928
10275
  continue;
9929
10276
  }
9930
10277
  if (o.type !== "response_item")
@@ -9950,6 +10297,17 @@ function extractCodexRolloutSession(raw, agentSlug) {
9950
10297
  }
9951
10298
  } else if (pt === "function_call" && typeof p.name === "string") {
9952
10299
  digest.toolCounts[p.name] = (digest.toolCounts[p.name] ?? 0) + 1;
10300
+ friction.call(p.name, typeof p.arguments === "string" ? p.arguments : null);
10301
+ if (p.name === "save_skill" && typeof p.arguments === "string") {
10302
+ try {
10303
+ const args = JSON.parse(p.arguments);
10304
+ if (typeof args.name === "string" && args.name.trim()) {
10305
+ const ms = ts ? Date.parse(ts) : NaN;
10306
+ recordAssetAttempt(assetAcc, args.name.trim(), Number.isFinite(ms) ? ms : null, digest.assistantTurns, toolCallsSeen);
10307
+ }
10308
+ } catch {}
10309
+ }
10310
+ toolCallsSeen++;
9953
10311
  } else if (pt === "tool_search_call") {
9954
10312
  digest.toolCounts["tool_search"] = (digest.toolCounts["tool_search"] ?? 0) + 1;
9955
10313
  }
@@ -9958,6 +10316,21 @@ function extractCodexRolloutSession(raw, agentSlug) {
9958
10316
  return null;
9959
10317
  if (!digest.project)
9960
10318
  digest.project = "codex";
10319
+ applyGapHistogram(signals, epochMs);
10320
+ if (models.length > 0)
10321
+ signals.models = models;
10322
+ if (lastTokenTotals) {
10323
+ signals.tokens = {
10324
+ tokensIn: finiteNum(lastTokenTotals.input_tokens),
10325
+ tokensOut: finiteNum(lastTokenTotals.output_tokens),
10326
+ cacheReadTokens: finiteNum(lastTokenTotals.cached_input_tokens),
10327
+ cacheCreationTokens: null
10328
+ };
10329
+ }
10330
+ signals.toolFailureStreakMax = failureMarkerSeen ? friction.failStreakMax : null;
10331
+ signals.repeatedCallMax = friction.repeatedCallMax;
10332
+ digest.signals = signals;
10333
+ digest.assetMarkers = buildAssetMarkers(assetAcc, epochMs);
9961
10334
  return digest;
9962
10335
  }
9963
10336
  function extractCursorSessions(composerRows, bubbleRows, sinceISO) {
@@ -10006,6 +10379,7 @@ function extractCursorSessions(composerRows, bubbleRows, sinceISO) {
10006
10379
  errorCount: 0,
10007
10380
  assistantTurns: 0
10008
10381
  };
10382
+ const friction = new FrictionCounters;
10009
10383
  for (const b of bubbles) {
10010
10384
  if (b.ts) {
10011
10385
  if (!digest.start)
@@ -10016,6 +10390,7 @@ function extractCursorSessions(composerRows, bubbleRows, sinceISO) {
10016
10390
  digest.toolCounts[b.tool] = (digest.toolCounts[b.tool] ?? 0) + 1;
10017
10391
  if (b.status === "error")
10018
10392
  digest.errorCount++;
10393
+ friction.result(b.status === "error");
10019
10394
  } else if (b.type === 2) {
10020
10395
  digest.assistantTurns++;
10021
10396
  }
@@ -10042,6 +10417,9 @@ function extractCursorSessions(composerRows, bubbleRows, sinceISO) {
10042
10417
  continue;
10043
10418
  if (sinceISO && digest.end && digest.end < sinceISO)
10044
10419
  continue;
10420
+ const signals = emptySessionSignals();
10421
+ signals.toolFailureStreakMax = friction.failStreakMax;
10422
+ digest.signals = signals;
10045
10423
  digests.push(digest);
10046
10424
  }
10047
10425
  return digests;
@@ -10085,7 +10463,7 @@ function formatCombinedDigest(sessions, opts = { days: 7 }) {
10085
10463
  return lines.join(`
10086
10464
  `);
10087
10465
  }
10088
- var MAX_MSG_CHARS = 700, MAX_MSGS_PER_SESSION = 40, SKIP_PREFIXES, CODEX_SKIP_PREFIXES;
10466
+ var MAX_MSG_CHARS = 700, MAX_MSGS_PER_SESSION = 40, finiteNum = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0, SKIP_PREFIXES, CODEX_SKIP_PREFIXES;
10089
10467
  var init_session_digest = __esm(() => {
10090
10468
  SKIP_PREFIXES = [
10091
10469
  "<system-reminder",
@@ -10424,7 +10802,7 @@ import { createHash as createHash2 } from "crypto";
10424
10802
  function contentHash(content) {
10425
10803
  return "sha256:" + createHash2("sha256").update(content).digest("hex");
10426
10804
  }
10427
- function stableStringify(value) {
10805
+ function stableStringify2(value) {
10428
10806
  return JSON.stringify(sortValue(value));
10429
10807
  }
10430
10808
  function sortValue(value) {
@@ -10441,7 +10819,7 @@ function sortValue(value) {
10441
10819
  return value;
10442
10820
  }
10443
10821
  function configHash(value) {
10444
- return contentHash(stableStringify(value));
10822
+ return contentHash(stableStringify2(value));
10445
10823
  }
10446
10824
  var init_hash = () => {};
10447
10825
 
@@ -15855,10 +16233,104 @@ var init_conversation_registry = __esm(async () => {
15855
16233
 
15856
16234
  // src/reflect/session-summary.ts
15857
16235
  import { createHash as createHash4 } from "crypto";
16236
+ import { execFileSync as execFileSync3 } from "child_process";
15858
16237
  import { readFileSync as readFileSync35 } from "fs";
16238
+ import { isAbsolute as isAbsolute4 } from "path";
16239
+ function validateAssetMarkers(markers, knownSkills) {
16240
+ if (!markers || markers.length === 0)
16241
+ return null;
16242
+ let known = null;
16243
+ if (knownSkills) {
16244
+ known = new Map;
16245
+ for (const skill of knownSkills) {
16246
+ const name = canonicalSkillName(skill.name);
16247
+ if (name)
16248
+ known.set(name, skill.id);
16249
+ }
16250
+ }
16251
+ return markers.map((m) => {
16252
+ const canonical = canonicalSkillName(m.rawName);
16253
+ const validated = Boolean(known && canonical && known.has(canonical));
16254
+ return {
16255
+ assetKind: m.kind,
16256
+ assetKey: validated ? canonical : null,
16257
+ assetKeyId: validated ? known.get(canonical) ?? null : null,
16258
+ saveAttempts: m.attempts,
16259
+ turnsToAsset: m.turnsBefore,
16260
+ toolCallsToAsset: m.toolCallsBefore,
16261
+ wallSecondsToAsset: m.wallSecondsToAsset,
16262
+ gapCountsToAsset: m.gapCountsToAsset,
16263
+ gapSecondsToAsset: m.gapSecondsToAsset
16264
+ };
16265
+ });
16266
+ }
16267
+ function clampVendorString(value) {
16268
+ if (value === null)
16269
+ return null;
16270
+ return value.length > VENDOR_STRING_MAX_CHARS ? value.slice(0, VENDOR_STRING_MAX_CHARS) : value;
16271
+ }
16272
+ function clampVendorList(values) {
16273
+ if (values === null)
16274
+ return null;
16275
+ return values.slice(0, VENDOR_LIST_MAX_ITEMS).map((v) => clampVendorString(v));
16276
+ }
16277
+ function clampTokensByModel(map) {
16278
+ if (map === null)
16279
+ return null;
16280
+ const entries = Object.entries(map).slice(0, VENDOR_LIST_MAX_ITEMS).map(([model, totals]) => [clampVendorString(model), totals]);
16281
+ return Object.fromEntries(entries);
16282
+ }
15859
16283
  function hashSessionKey(key) {
15860
16284
  return createHash4("sha256").update(key).digest("hex").slice(0, 32);
15861
16285
  }
16286
+ function normalizeGitRemote(url) {
16287
+ let out = url.trim().toLowerCase();
16288
+ if (!out)
16289
+ return null;
16290
+ const hadScheme = /^[a-z+]+:\/\//.test(out);
16291
+ out = out.replace(/^[a-z+]+:\/\//, "");
16292
+ out = out.replace(/^[^@/]+@/, "");
16293
+ if (hadScheme)
16294
+ out = out.replace(/^([^/:]+):(\d+)(?=\/)/, "$1");
16295
+ out = out.replace(":", "/");
16296
+ out = out.replace(/\/+$/, "");
16297
+ out = out.replace(/\.git$/, "");
16298
+ out = out.replace(/\/+$/, "");
16299
+ return out || null;
16300
+ }
16301
+ function normalizeProjectName(project) {
16302
+ if (!project)
16303
+ return null;
16304
+ const trimmed = project.trim().replace(/[/\\]+$/, "");
16305
+ if (!trimmed)
16306
+ return null;
16307
+ const idx = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
16308
+ const base = (idx >= 0 ? trimmed.slice(idx + 1) : trimmed).toLowerCase();
16309
+ if (!base)
16310
+ return null;
16311
+ if (idx < 0 && PROJECT_PLACEHOLDERS.has(base))
16312
+ return null;
16313
+ return base;
16314
+ }
16315
+ function resolveProjectHash(project) {
16316
+ const normalized = normalizeProjectName(project);
16317
+ return normalized ? hashSessionKey(normalized) : null;
16318
+ }
16319
+ function resolveRepoHash(projectDir) {
16320
+ if (!projectDir || !isAbsolute4(projectDir))
16321
+ return null;
16322
+ let url;
16323
+ try {
16324
+ url = execFileSync3("git", ["-C", projectDir, "config", "--get", "remote.origin.url"], {
16325
+ stdio: ["ignore", "pipe", "ignore"],
16326
+ timeout: 3000
16327
+ }).toString().trim();
16328
+ } catch {
16329
+ return null;
16330
+ }
16331
+ const normalized = url ? normalizeGitRemote(url) : null;
16332
+ return normalized ? hashSessionKey(normalized) : null;
16333
+ }
15862
16334
  function digestForEntry(entry) {
15863
16335
  if (!entry.transcriptPath)
15864
16336
  return null;
@@ -15892,9 +16364,10 @@ function deriveMcpUsage(toolCounts) {
15892
16364
  }
15893
16365
  return { mcpCallCount, mcpServers };
15894
16366
  }
15895
- function buildSessionSummary(key, entry, digest) {
16367
+ function buildSessionSummary(key, entry, digest, opts = {}) {
15896
16368
  const toolCallCount = digest ? Object.values(digest.toolCounts).reduce((sum, n) => sum + n, 0) : null;
15897
16369
  const userMessageCount = digest ? digest.userMessages.length + digest.droppedUserMessages : null;
16370
+ const sig = digest?.signals;
15898
16371
  return {
15899
16372
  agentSlug: entry.agentSlug,
15900
16373
  sessionIdHash: hashSessionKey(key),
@@ -15907,7 +16380,32 @@ function buildSessionSummary(key, entry, digest) {
15907
16380
  toolCounts: digest ? digest.toolCounts : null,
15908
16381
  ...deriveMcpUsage(digest ? digest.toolCounts : null),
15909
16382
  errorCount: digest?.errorCount ?? null,
15910
- skillsUsed: [...new Set((digest?.skillInvocations ?? []).map(canonicalSkillName).filter(Boolean))]
16383
+ skillsUsed: clampVendorList([...new Set((digest?.skillInvocations ?? []).map(canonicalSkillName).filter(Boolean))]) ?? [],
16384
+ provenance: sig?.provenance ?? null,
16385
+ gapCounts: sig?.gapCounts ?? null,
16386
+ gapSeconds: sig?.gapSeconds ?? null,
16387
+ spanMinutes: sig?.spanMinutes ?? null,
16388
+ longestGapSeconds: sig?.longestGapSeconds ?? null,
16389
+ permissionModes: clampVendorList(sig?.permissionModes ?? null),
16390
+ modeEscalations: sig?.modeEscalations ?? null,
16391
+ approvalPolicy: clampVendorString(sig?.approvalPolicy ?? null),
16392
+ sandboxPolicy: clampVendorString(sig?.sandboxPolicy ?? null),
16393
+ subagentCount: sig?.subagentCount ?? null,
16394
+ planModeUsed: sig?.planModeUsed ?? null,
16395
+ slashCommandCount: sig?.slashCommandCount ?? null,
16396
+ modelInitiatedSkillCount: sig?.modelInitiatedSkillCount ?? null,
16397
+ promptLengthBuckets: sig?.promptLengthBuckets ?? null,
16398
+ toolFailureStreakMax: sig?.toolFailureStreakMax ?? null,
16399
+ repeatedCallMax: sig?.repeatedCallMax ?? null,
16400
+ models: clampVendorList(sig?.models ?? null),
16401
+ tokensIn: sig?.tokens?.tokensIn ?? null,
16402
+ tokensOut: sig?.tokens?.tokensOut ?? null,
16403
+ cacheReadTokens: sig?.tokens?.cacheReadTokens ?? null,
16404
+ cacheCreationTokens: sig?.tokens?.cacheCreationTokens ?? null,
16405
+ tokensByModel: clampTokensByModel(sig?.tokensByModel ?? null),
16406
+ assetsCreated: validateAssetMarkers(digest?.assetMarkers, opts.knownSkills ?? null),
16407
+ repoHash: opts.repoHash ?? null,
16408
+ projectHash: resolveProjectHash(entry.project)
15911
16409
  };
15912
16410
  }
15913
16411
  function buildSessionSummaryEvent(summary, nowISO) {
@@ -15928,14 +16426,41 @@ function buildSessionSummaryEvent(summary, nowISO) {
15928
16426
  mcpCallCount: summary.mcpCallCount,
15929
16427
  mcpServers: summary.mcpServers,
15930
16428
  errorCount: summary.errorCount,
15931
- skillsUsed: summary.skillsUsed
16429
+ skillsUsed: summary.skillsUsed,
16430
+ provenance: summary.provenance,
16431
+ gapCounts: summary.gapCounts,
16432
+ gapSeconds: summary.gapSeconds,
16433
+ spanMinutes: summary.spanMinutes,
16434
+ longestGapSeconds: summary.longestGapSeconds,
16435
+ permissionModes: summary.permissionModes,
16436
+ modeEscalations: summary.modeEscalations,
16437
+ approvalPolicy: summary.approvalPolicy,
16438
+ sandboxPolicy: summary.sandboxPolicy,
16439
+ subagentCount: summary.subagentCount,
16440
+ planModeUsed: summary.planModeUsed,
16441
+ slashCommandCount: summary.slashCommandCount,
16442
+ modelInitiatedSkillCount: summary.modelInitiatedSkillCount,
16443
+ promptLengthBuckets: summary.promptLengthBuckets,
16444
+ toolFailureStreakMax: summary.toolFailureStreakMax,
16445
+ repeatedCallMax: summary.repeatedCallMax,
16446
+ models: summary.models,
16447
+ tokensIn: summary.tokensIn,
16448
+ tokensOut: summary.tokensOut,
16449
+ cacheReadTokens: summary.cacheReadTokens,
16450
+ cacheCreationTokens: summary.cacheCreationTokens,
16451
+ tokensByModel: summary.tokensByModel,
16452
+ assetsCreated: summary.assetsCreated,
16453
+ repoHash: summary.repoHash,
16454
+ projectHash: summary.projectHash
15932
16455
  },
15933
16456
  timestamp: nowISO
15934
16457
  }
15935
16458
  };
15936
16459
  }
16460
+ var VENDOR_STRING_MAX_CHARS = 64, VENDOR_LIST_MAX_ITEMS = 12, PROJECT_PLACEHOLDERS;
15937
16461
  var init_session_summary = __esm(() => {
15938
16462
  init_session_digest();
16463
+ PROJECT_PLACEHOLDERS = new Set(["codex", "cowork", "cursor"]);
15939
16464
  });
15940
16465
 
15941
16466
  // src/reflect/telemetry-outbox.ts
@@ -15977,6 +16502,222 @@ var init_telemetry_outbox = __esm(() => {
15977
16502
  init_atomic_json();
15978
16503
  });
15979
16504
 
16505
+ // src/reflect/active-time.ts
16506
+ function isValidActiveTimeCap(capSeconds) {
16507
+ return VALID_ACTIVE_TIME_CAPS_SECONDS.includes(capSeconds);
16508
+ }
16509
+ function activeSecondsFromBuckets(gapCounts, gapSeconds, capSeconds) {
16510
+ if (!gapCounts || !gapSeconds)
16511
+ return null;
16512
+ if (!isValidActiveTimeCap(capSeconds))
16513
+ return null;
16514
+ let total = 0;
16515
+ for (const { key, lower } of BUCKET_LOWER_EDGES) {
16516
+ if (lower >= capSeconds) {
16517
+ total += capSeconds * (gapCounts[key] ?? 0);
16518
+ } else {
16519
+ total += gapSeconds[key] ?? 0;
16520
+ }
16521
+ }
16522
+ return total;
16523
+ }
16524
+ var BUCKET_LOWER_EDGES, VALID_ACTIVE_TIME_CAPS_SECONDS;
16525
+ var init_active_time = __esm(() => {
16526
+ BUCKET_LOWER_EDGES = [
16527
+ { key: "lt10s", lower: 0 },
16528
+ { key: "s10to30", lower: 10 },
16529
+ { key: "s30to2m", lower: 30 },
16530
+ { key: "m2to5", lower: 120 },
16531
+ { key: "m5to15", lower: 300 },
16532
+ { key: "gte15m", lower: 900 }
16533
+ ];
16534
+ VALID_ACTIVE_TIME_CAPS_SECONDS = [10, 30, 120, 300, 900];
16535
+ });
16536
+
16537
+ // src/reflect/pattern-store.ts
16538
+ import { createHash as createHash5 } from "crypto";
16539
+ import { join as join42 } from "path";
16540
+ import { homedir as homedir23 } from "os";
16541
+ function addBuckets(a, b) {
16542
+ if (!a)
16543
+ return b ? { ...b } : null;
16544
+ if (!b)
16545
+ return { ...a };
16546
+ return {
16547
+ lt10s: a.lt10s + b.lt10s,
16548
+ s10to30: a.s10to30 + b.s10to30,
16549
+ s30to2m: a.s30to2m + b.s30to2m,
16550
+ m2to5: a.m2to5 + b.m2to5,
16551
+ m5to15: a.m5to15 + b.m5to15,
16552
+ gte15m: a.gte15m + b.gte15m
16553
+ };
16554
+ }
16555
+ function canonicalizeEntities(entities) {
16556
+ const cleaned = entities.map((e) => e.trim().toLowerCase().replace(/\s+/g, " ")).filter((e) => e.length > 0);
16557
+ return [...new Set(cleaned)].sort().slice(0, MAX_KEY_ENTITIES);
16558
+ }
16559
+ function patternKeyHash(taskType, entities) {
16560
+ const task = taskType.trim().toLowerCase();
16561
+ const canonical = canonicalizeEntities(entities);
16562
+ if (!task || canonical.length === 0)
16563
+ return null;
16564
+ return createHash5("sha256").update(`${task}|${canonical.join(",")}`).digest("hex").slice(0, 32);
16565
+ }
16566
+ function median(sorted) {
16567
+ if (sorted.length === 0)
16568
+ return 0;
16569
+ const mid = Math.floor(sorted.length / 2);
16570
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
16571
+ }
16572
+ function medianOf(values) {
16573
+ return median([...values].sort((a, b) => a - b));
16574
+ }
16575
+ function modal(values) {
16576
+ const counts = new Map;
16577
+ for (const v of values)
16578
+ counts.set(v, (counts.get(v) ?? 0) + 1);
16579
+ let best = null;
16580
+ let bestCount = 0;
16581
+ for (const [value, count] of counts) {
16582
+ if (count > bestCount) {
16583
+ best = value;
16584
+ bestCount = count;
16585
+ }
16586
+ }
16587
+ return best;
16588
+ }
16589
+ function emptyPatternStore() {
16590
+ return { version: PATTERN_STORE_VERSION, patterns: {} };
16591
+ }
16592
+ function recordObservation(state, observation) {
16593
+ const key = patternKeyHash(observation.taskType, observation.keyEntities);
16594
+ if (!key)
16595
+ return { state, key: null };
16596
+ const at = Date.parse(observation.at);
16597
+ if (!Number.isFinite(at))
16598
+ return { state, key: null };
16599
+ const activeSeconds = observation.gapCounts && observation.gapSeconds ? activeSecondsFromBuckets(observation.gapCounts, observation.gapSeconds, ACTIVE_MINUTES_CAP_SECONDS) : null;
16600
+ const existing = state.patterns[key];
16601
+ const occurrences = existing ? [...existing.occurrences] : [];
16602
+ const mergeIndex = occurrences.findIndex((o) => Math.abs(Date.parse(o.at) - at) <= SAME_OCCURRENCE_WINDOW_HOURS * 3600 * 1000);
16603
+ if (mergeIndex >= 0) {
16604
+ const prior = occurrences[mergeIndex];
16605
+ const priorAt = Date.parse(prior.at);
16606
+ occurrences[mergeIndex] = {
16607
+ at: priorAt <= at ? prior.at : observation.at,
16608
+ activeSeconds: prior.activeSeconds === null && activeSeconds === null ? null : (prior.activeSeconds ?? 0) + (activeSeconds ?? 0),
16609
+ gapCounts: addBuckets(prior.gapCounts, observation.gapCounts),
16610
+ gapSeconds: addBuckets(prior.gapSeconds, observation.gapSeconds),
16611
+ confidence: Math.min(prior.confidence, observation.confidence),
16612
+ sessionIdHashes: observation.sessionIdHash && !prior.sessionIdHashes.includes(observation.sessionIdHash) ? [...prior.sessionIdHashes, observation.sessionIdHash] : prior.sessionIdHashes,
16613
+ suggestedCapability: prior.suggestedCapability ?? observation.suggestedCapability ?? null,
16614
+ projectHash: prior.projectHash ?? observation.projectHash ?? null
16615
+ };
16616
+ } else {
16617
+ occurrences.push({
16618
+ at: observation.at,
16619
+ activeSeconds,
16620
+ gapCounts: observation.gapCounts ?? null,
16621
+ gapSeconds: observation.gapSeconds ?? null,
16622
+ confidence: observation.confidence,
16623
+ sessionIdHashes: observation.sessionIdHash ? [observation.sessionIdHash] : [],
16624
+ suggestedCapability: observation.suggestedCapability ?? null,
16625
+ projectHash: observation.projectHash ?? null
16626
+ });
16627
+ }
16628
+ occurrences.sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
16629
+ const newest = Date.parse(occurrences[occurrences.length - 1].at);
16630
+ const kept = occurrences.filter((o) => newest - Date.parse(o.at) <= OCCURRENCE_RETENTION_DAYS * 24 * 3600 * 1000);
16631
+ return {
16632
+ state: {
16633
+ ...state,
16634
+ patterns: {
16635
+ ...state.patterns,
16636
+ [key]: {
16637
+ taskType: observation.taskType.trim().toLowerCase(),
16638
+ keyEntities: canonicalizeEntities(observation.keyEntities),
16639
+ occurrences: kept
16640
+ }
16641
+ }
16642
+ },
16643
+ key
16644
+ };
16645
+ }
16646
+ function evaluateMaturity(key, record) {
16647
+ const sorted = [...record.occurrences].sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
16648
+ if (sorted.length < MATURITY_MIN_OCCURRENCES)
16649
+ return null;
16650
+ const newest = Date.parse(sorted[sorted.length - 1].at);
16651
+ const inWindow = sorted.filter((o) => newest - Date.parse(o.at) <= MATURITY_WINDOW_DAYS * 24 * 3600 * 1000);
16652
+ if (inWindow.length < MATURITY_MIN_OCCURRENCES)
16653
+ return null;
16654
+ const gapsDays = [];
16655
+ for (let i = 1;i < inWindow.length; i++) {
16656
+ gapsDays.push((Date.parse(inWindow[i].at) - Date.parse(inWindow[i - 1].at)) / (24 * 3600 * 1000));
16657
+ }
16658
+ const medianGap = medianOf(gapsDays);
16659
+ if (medianGap < MATURITY_MIN_PERIODICITY_DAYS)
16660
+ return null;
16661
+ const mad = medianOf(gapsDays.map((g) => Math.abs(g - medianGap)));
16662
+ if (mad > MATURITY_MAX_MAD_RATIO * medianGap)
16663
+ return null;
16664
+ const activeSecondsValues = inWindow.map((o) => o.activeSeconds).filter((s) => typeof s === "number");
16665
+ let activeGapCounts = null;
16666
+ let activeGapSeconds = null;
16667
+ for (const occurrence of inWindow) {
16668
+ activeGapCounts = addBuckets(activeGapCounts, occurrence.gapCounts);
16669
+ activeGapSeconds = addBuckets(activeGapSeconds, occurrence.gapSeconds);
16670
+ }
16671
+ const projectHashes = inWindow.map((o) => o.projectHash).filter((p) => typeof p === "string" && p.length > 0);
16672
+ const capabilities = inWindow.map((o) => o.suggestedCapability).filter((c) => typeof c === "string" && c.length > 0);
16673
+ return {
16674
+ patternKeyHash: key,
16675
+ taskType: record.taskType,
16676
+ occurrences: inWindow.length,
16677
+ periodicityDays: Math.round(medianGap * 10) / 10,
16678
+ medianActiveMinutes: activeSecondsValues.length > 0 ? Math.round(medianOf(activeSecondsValues) / 60 * 10) / 10 : null,
16679
+ activeMinutesCapSeconds: ACTIVE_MINUTES_CAP_SECONDS,
16680
+ activeGapCounts,
16681
+ activeGapSeconds,
16682
+ suggestedCapability: modal(capabilities),
16683
+ firstSeenAt: inWindow[0].at,
16684
+ lastSeenAt: inWindow[inWindow.length - 1].at,
16685
+ confidence: Math.min(...inWindow.map((o) => o.confidence)),
16686
+ projectHash: modal(projectHashes),
16687
+ distinctProjectHashes: new Set(projectHashes).size
16688
+ };
16689
+ }
16690
+ function buildPatternEvent(payload, nowISO) {
16691
+ return {
16692
+ dedupeKey: `pattern:${payload.patternKeyHash}`,
16693
+ event: {
16694
+ eventType: "local_agent.repeated_pattern",
16695
+ metadata: { ...payload },
16696
+ timestamp: nowISO
16697
+ }
16698
+ };
16699
+ }
16700
+ function storePath3() {
16701
+ return join42(homedir23(), ".runwork", "pattern-store.json");
16702
+ }
16703
+ function loadPatternStore() {
16704
+ const parsed = readJsonOrNull(storePath3());
16705
+ if (!parsed || typeof parsed !== "object" || !parsed.patterns || typeof parsed.patterns !== "object") {
16706
+ return emptyPatternStore();
16707
+ }
16708
+ if (parsed.version !== PATTERN_STORE_VERSION)
16709
+ return emptyPatternStore();
16710
+ return parsed;
16711
+ }
16712
+ function savePatternStore(state) {
16713
+ writeJsonAtomic(storePath3(), state);
16714
+ }
16715
+ var SAME_OCCURRENCE_WINDOW_HOURS = 4, MATURITY_MIN_OCCURRENCES = 3, MATURITY_WINDOW_DAYS = 45, MATURITY_MAX_MAD_RATIO = 0.5, MATURITY_MIN_PERIODICITY_DAYS = 1, OCCURRENCE_RETENTION_DAYS = 180, MAX_KEY_ENTITIES = 4, ACTIVE_MINUTES_CAP_SECONDS = 300, PATTERN_STORE_VERSION = 1;
16716
+ var init_pattern_store = __esm(() => {
16717
+ init_atomic_json();
16718
+ init_active_time();
16719
+ });
16720
+
15980
16721
  // src/reflect/triage.ts
15981
16722
  var exports_triage = {};
15982
16723
  __export(exports_triage, {
@@ -15987,7 +16728,8 @@ __export(exports_triage, {
15987
16728
  packTriageBatches: () => packTriageBatches,
15988
16729
  buildTriagePrompt: () => buildTriagePrompt,
15989
16730
  TRIAGE_ITEM_CHAR_CAP: () => TRIAGE_ITEM_CHAR_CAP,
15990
- TRIAGE_BATCH_CHAR_BUDGET: () => TRIAGE_BATCH_CHAR_BUDGET
16731
+ TRIAGE_BATCH_CHAR_BUDGET: () => TRIAGE_BATCH_CHAR_BUDGET,
16732
+ TASK_TYPES: () => TASK_TYPES
15991
16733
  });
15992
16734
  function renderTriageItem(id, candidate) {
15993
16735
  const d = candidate.digest;
@@ -16036,12 +16778,17 @@ You are the cheap triage tier of Runwork's reflection engine. At the end of this
16036
16778
 
16037
16779
  Mark a conversation NOT worthy when it is trivial or one-shot, is routine work with no repeated pattern, or would only re-derive something in the already-suggested list. Most conversations are not worthy; be strict. Do not analyze content deeply — skim and judge.
16038
16780
 
16781
+ For EACH conversation also label the work itself, which is used to notice the same chore recurring across conversations:
16782
+
16783
+ - "taskType": exactly one of ${TASK_TYPES.join(" | ")}.
16784
+ - "keyEntities": 1 to 4 short CANONICAL noun phrases naming the OBJECT of the work ("invoice reformatting", "weekly sales report", "hubspot contact cleanup"). Canonical means: singular, lowercase, generic. NO dates, file names, person names, company names, counts, or version numbers. Two conversations doing the same chore in different words MUST produce the same phrases, so prefer the plainest wording of the underlying task over the user's phrasing.
16785
+
16039
16786
  Reply with ONLY a single fenced \`json\` block:
16040
16787
 
16041
16788
  \`\`\`json
16042
16789
  {
16043
16790
  "verdicts": [
16044
- { "id": "<the conversation id shown in its header>", "worthy": true|false, "confidence": <0..1> }
16791
+ { "id": "<the conversation id shown in its header>", "worthy": true|false, "confidence": <0..1>, "taskType": "<one of the values above>", "keyEntities": ["<canonical phrase>"] }
16045
16792
  ]
16046
16793
  }
16047
16794
  \`\`\`
@@ -16071,9 +16818,13 @@ function parseTriageVerdicts(text2) {
16071
16818
  const v = raw;
16072
16819
  if (typeof v.id !== "string" || typeof v.worthy !== "boolean")
16073
16820
  continue;
16821
+ const taskType = typeof v.taskType === "string" && TASK_TYPES.includes(v.taskType.trim().toLowerCase()) ? v.taskType.trim().toLowerCase() : undefined;
16822
+ const keyEntities = Array.isArray(v.keyEntities) ? v.keyEntities.filter((e) => typeof e === "string" && e.trim().length > 0).map((e) => e.trim()) : [];
16074
16823
  out.set(v.id, {
16075
16824
  worthy: v.worthy,
16076
- confidence: typeof v.confidence === "number" ? Math.max(0, Math.min(1, v.confidence)) : 0.5
16825
+ confidence: typeof v.confidence === "number" ? Math.max(0, Math.min(1, v.confidence)) : 0.5,
16826
+ ...taskType ? { taskType } : {},
16827
+ ...keyEntities.length > 0 ? { keyEntities } : {}
16077
16828
  });
16078
16829
  }
16079
16830
  return out.size > 0 ? out : null;
@@ -16144,7 +16895,7 @@ async function runTriagePass(candidates, opts) {
16144
16895
  }
16145
16896
  return { verdicts, summary };
16146
16897
  }
16147
- var TRIAGE_ITEM_CHAR_CAP = 12000, TRIAGE_BATCH_CHAR_BUDGET = 150000, TRIAGE_TIMEOUT_MS;
16898
+ var TRIAGE_ITEM_CHAR_CAP = 12000, TRIAGE_BATCH_CHAR_BUDGET = 150000, TRIAGE_TIMEOUT_MS, TASK_TYPES;
16148
16899
  var init_triage = __esm(async () => {
16149
16900
  init_which();
16150
16901
  init_session_listing();
@@ -16153,6 +16904,7 @@ var init_triage = __esm(async () => {
16153
16904
  init_conversation_analysis()
16154
16905
  ]);
16155
16906
  TRIAGE_TIMEOUT_MS = 2 * 60 * 1000;
16907
+ TASK_TYPES = ["build", "automate", "analyze", "write", "research", "admin", "debug"];
16156
16908
  });
16157
16909
 
16158
16910
  // src/reflect/conversation-analysis.ts
@@ -16317,6 +17069,48 @@ async function analyzeConversations(opts = {}) {
16317
17069
  releaseRunLock();
16318
17070
  }
16319
17071
  }
17072
+ function recordPatternObservations(verdicts, state, progress) {
17073
+ try {
17074
+ let store = loadPatternStore();
17075
+ const touchedKeys = new Set;
17076
+ for (const [key, verdict] of verdicts) {
17077
+ const entry = state.entries[key];
17078
+ if (!entry || !verdict.taskType || !verdict.keyEntities?.length)
17079
+ continue;
17080
+ const stats = entry.stats;
17081
+ const result = recordObservation(store, {
17082
+ taskType: verdict.taskType,
17083
+ keyEntities: verdict.keyEntities,
17084
+ at: entry.lastActivityAt,
17085
+ confidence: verdict.confidence,
17086
+ sessionIdHash: stats?.sessionIdHash,
17087
+ gapCounts: stats?.gapCounts ?? null,
17088
+ gapSeconds: stats?.gapSeconds ?? null,
17089
+ projectHash: stats?.projectHash ?? null
17090
+ });
17091
+ store = result.state;
17092
+ if (result.key)
17093
+ touchedKeys.add(result.key);
17094
+ }
17095
+ if (touchedKeys.size === 0)
17096
+ return;
17097
+ savePatternStore(store);
17098
+ const nowISO = new Date().toISOString();
17099
+ const events = [];
17100
+ for (const key of touchedKeys) {
17101
+ const record = store.patterns[key];
17102
+ if (!record)
17103
+ continue;
17104
+ const payload = evaluateMaturity(key, record);
17105
+ if (payload)
17106
+ events.push(buildPatternEvent(payload, nowISO));
17107
+ }
17108
+ if (events.length > 0) {
17109
+ appendToTelemetryOutbox(events);
17110
+ progress(`${events.length} recurring chore pattern(s) reached the recurrence threshold.`);
17111
+ }
17112
+ } catch {}
17113
+ }
16320
17114
  async function analyzeConversationsLocked(opts, outcome, gates) {
16321
17115
  const { cadence, explicitSession, manual } = gates;
16322
17116
  if (!cadence.enabled && !manual && !explicitSession) {
@@ -16453,9 +17247,11 @@ async function analyzeConversationsLocked(opts, outcome, gates) {
16453
17247
  for (const [key, verdict] of pass.verdicts) {
16454
17248
  const entry = state.entries[key];
16455
17249
  if (entry) {
16456
- state = { entries: { ...state.entries, [key]: { ...entry, triage: { ...verdict, at: entry.lastActivityAt } } } };
17250
+ const { keyEntities: _entities, ...cacheable } = verdict;
17251
+ state = { entries: { ...state.entries, [key]: { ...entry, triage: { ...cacheable, at: entry.lastActivityAt } } } };
16457
17252
  }
16458
17253
  }
17254
+ recordPatternObservations(pass.verdicts, state, progress);
16459
17255
  }
16460
17256
  }
16461
17257
  const ranked = [];
@@ -16675,6 +17471,7 @@ var init_conversation_analysis = __esm(async () => {
16675
17471
  init_session_listing();
16676
17472
  init_insight_store();
16677
17473
  init_telemetry_outbox();
17474
+ init_pattern_store();
16678
17475
  init_run_log();
16679
17476
  init_insight_store();
16680
17477
  await __promiseAll([
@@ -19171,14 +19968,18 @@ init_session_summary();
19171
19968
  init_telemetry_outbox();
19172
19969
  init_insight_store();
19173
19970
  init_atomic_json();
19971
+ init_store();
19972
+ init_client();
19973
+ init_resolve();
19974
+ init_workspace_state();
19174
19975
  init_colors();
19175
19976
  await __promiseAll([
19176
19977
  init_conversation_registry(),
19177
19978
  init_conversation_analysis()
19178
19979
  ]);
19179
19980
  import { Command as Command15 } from "commander";
19180
- import { join as join42 } from "path";
19181
- import { homedir as homedir23 } from "os";
19981
+ import { join as join43 } from "path";
19982
+ import { homedir as homedir24 } from "os";
19182
19983
  var DEFAULT_LOOKBACK_DAYS = 30;
19183
19984
  function sinceFromDays(days) {
19184
19985
  if (days <= 0)
@@ -19191,6 +19992,46 @@ function parseDays(raw) {
19191
19992
  return DEFAULT_LOOKBACK_DAYS;
19192
19993
  return Math.floor(n);
19193
19994
  }
19995
+ function bareRegistrySkillId(skill) {
19996
+ if (skill.type !== "external" || typeof skill.id !== "string")
19997
+ return null;
19998
+ return skill.id.startsWith("ext-") ? skill.id.slice("ext-".length) || null : null;
19999
+ }
20000
+ async function fetchKnownSkills() {
20001
+ const creds = getCredentials();
20002
+ if (!creds)
20003
+ return null;
20004
+ let workspaceId;
20005
+ const client = new ApiClient(creds);
20006
+ try {
20007
+ workspaceId = (await resolveWorkspace2(client, {})).workspaceId;
20008
+ } catch {
20009
+ workspaceId = creds.defaultWorkspaceId;
20010
+ }
20011
+ if (!workspaceId)
20012
+ return null;
20013
+ try {
20014
+ const skills = await client.listWorkspaceSkills(workspaceId);
20015
+ const known = [];
20016
+ const seen = new Set;
20017
+ for (const sk of skills) {
20018
+ if (sk.type === "app")
20019
+ continue;
20020
+ const name = canonicalSkillName(sk.name);
20021
+ if (!name || seen.has(name))
20022
+ continue;
20023
+ seen.add(name);
20024
+ known.push({ name, id: bareRegistrySkillId(sk) });
20025
+ }
20026
+ updateWorkspaceRecord(workspaceId, { knownSkills: known });
20027
+ return known;
20028
+ } catch {
20029
+ const record = loadWorkspaceRecord(workspaceId);
20030
+ if (record.knownSkills)
20031
+ return record.knownSkills;
20032
+ return record.skillNames ? record.skillNames.map((name) => ({ name, id: null })) : null;
20033
+ }
20034
+ }
19194
20035
  function projectLabel(project) {
19195
20036
  const trimmed = project.replace(/[/\\]+$/, "");
19196
20037
  const idx = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
@@ -19247,11 +20088,21 @@ conversationsCommand.command("scan").description("Detect finished conversations
19247
20088
  const nowISO = new Date().toISOString();
19248
20089
  const summaryEvents = [];
19249
20090
  const newlyPending = new Set(scan.newlyPendingKeys);
20091
+ const knownSkills = newlyPending.size > 0 ? await fetchKnownSkills() : null;
20092
+ const repoHashByProject = new Map;
20093
+ const repoHashFor = (project) => {
20094
+ if (!repoHashByProject.has(project))
20095
+ repoHashByProject.set(project, resolveRepoHash(project));
20096
+ return repoHashByProject.get(project) ?? null;
20097
+ };
19250
20098
  let statsWritten = 0;
19251
20099
  for (const [key, entry] of Object.entries(scan.state.entries)) {
19252
20100
  if (!newlyPending.has(key) && entry.stats)
19253
20101
  continue;
19254
- const summary = buildSessionSummary(key, entry, digestForEntry(entry));
20102
+ const summary = buildSessionSummary(key, entry, digestForEntry(entry), {
20103
+ knownSkills,
20104
+ repoHash: repoHashFor(entry.project)
20105
+ });
19255
20106
  scan.state.entries[key] = { ...entry, stats: summary };
19256
20107
  statsWritten++;
19257
20108
  if (newlyPending.has(key))
@@ -19290,7 +20141,7 @@ conversationsCommand.command("scan").description("Detect finished conversations
19290
20141
  idleMinutes,
19291
20142
  conversations: [...listed, ...rescued].sort((a, b) => b.lastActivityAt.localeCompare(a.lastActivityAt))
19292
20143
  };
19293
- writeJsonAtomic(join42(homedir23(), ".runwork", "conversations.json"), snapshot);
20144
+ writeJsonAtomic(join43(homedir24(), ".runwork", "conversations.json"), snapshot);
19294
20145
  if (json) {
19295
20146
  jsonOut({ queued: scan.queued, reopened: scan.reopened, pruned: scan.pruned, pending, finished: finished.length });
19296
20147
  return;
@@ -19320,8 +20171,8 @@ init_client();
19320
20171
  init_resolve();
19321
20172
  import { Command as Command16 } from "commander";
19322
20173
  import { readFileSync as readFileSync37, existsSync as existsSync48 } from "fs";
19323
- import { join as join43 } from "path";
19324
- import { homedir as homedir24 } from "os";
20174
+ import { join as join44 } from "path";
20175
+ import { homedir as homedir25 } from "os";
19325
20176
 
19326
20177
  // ../../shared/agent-instructions/runwork-instructions.ts
19327
20178
  function formatList(items, max = 8) {
@@ -19801,8 +20652,8 @@ async function buildInstructionContext(client, workspace) {
19801
20652
  // src/commands/instructions.ts
19802
20653
  function readSetupExtras(workspaceId) {
19803
20654
  for (const path2 of [
19804
- join43(process.cwd(), ".runwork", "setup.json"),
19805
- join43(homedir24(), ".runwork", "setup.json")
20655
+ join44(process.cwd(), ".runwork", "setup.json"),
20656
+ join44(homedir25(), ".runwork", "setup.json")
19806
20657
  ]) {
19807
20658
  if (!existsSync48(path2))
19808
20659
  continue;
@@ -21541,8 +22392,8 @@ init_resolve();
21541
22392
  init_prompt();
21542
22393
  await init_detect();
21543
22394
  import { Command as Command27 } from "commander";
21544
- import { join as join47 } from "path";
21545
- import { homedir as homedir26 } from "os";
22395
+ import { join as join48 } from "path";
22396
+ import { homedir as homedir27 } from "os";
21546
22397
 
21547
22398
  // src/commands/sync.ts
21548
22399
  init_store();
@@ -21554,8 +22405,8 @@ await __promiseAll([
21554
22405
  ]);
21555
22406
  import { Command as Command26 } from "commander";
21556
22407
  import { readFileSync as readFileSync40, existsSync as existsSync51 } from "fs";
21557
- import { join as join46 } from "path";
21558
- import { homedir as homedir25 } from "os";
22408
+ import { join as join47 } from "path";
22409
+ import { homedir as homedir26 } from "os";
21559
22410
 
21560
22411
  // src/commands/mcp-entries.ts
21561
22412
  init_types();
@@ -22454,13 +23305,13 @@ function readLocalSkills(state) {
22454
23305
  if (!baseDir)
22455
23306
  continue;
22456
23307
  for (const skillName of state.skills) {
22457
- const skillMdPath = join46(baseDir, skillName, "SKILL.md");
23308
+ const skillMdPath = join47(baseDir, skillName, "SKILL.md");
22458
23309
  if (existsSync51(skillMdPath)) {
22459
23310
  results.push({ name: skillName, content: readFileSync40(skillMdPath, "utf-8") });
22460
23311
  continue;
22461
23312
  }
22462
23313
  const filename = skillName.toLowerCase().replace(/[^a-z0-9]+/g, "-");
22463
- const flatPath = join46(baseDir, `${filename}.md`);
23314
+ const flatPath = join47(baseDir, `${filename}.md`);
22464
23315
  if (existsSync51(flatPath)) {
22465
23316
  results.push({ name: skillName, content: readFileSync40(flatPath, "utf-8") });
22466
23317
  }
@@ -22942,7 +23793,7 @@ This account is not a member of "${state.workspaceName || state.workspaceId}".`)
22942
23793
  }
22943
23794
  for (const adapter2 of adapters) {
22944
23795
  if (adapter2 instanceof CodexAdapter) {
22945
- const runworkDir = join46(homedir25(), ".runwork");
23796
+ const runworkDir = join47(homedir26(), ".runwork");
22946
23797
  const result = adapter2.registerDesktopWorkspace(runworkDir, "Runwork");
22947
23798
  if (result === "written") {
22948
23799
  vlog(` [${adapter2.name}] Registered workspace in Codex desktop app`);
@@ -23084,8 +23935,8 @@ var syncCommand = new Command26("sync").description("Sync skills bidirectionally
23084
23935
  verbose: !!opts.verbose,
23085
23936
  redetect: !!opts.redetect
23086
23937
  };
23087
- const projectStatePath = join46(process.cwd(), ".runwork", "setup.json");
23088
- const userStatePath = join46(homedir25(), ".runwork", "setup.json");
23938
+ const projectStatePath = join47(process.cwd(), ".runwork", "setup.json");
23939
+ const userStatePath = join47(homedir26(), ".runwork", "setup.json");
23089
23940
  const projectState = loadSetupState(projectStatePath);
23090
23941
  const userState = loadSetupState(userStatePath);
23091
23942
  if (!projectState && !userState) {
@@ -23154,7 +24005,7 @@ function toSkillFilename(name) {
23154
24005
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
23155
24006
  }
23156
24007
  function loadSetupStateForScope(scope) {
23157
- const path4 = scope === "project" ? join47(process.cwd(), ".runwork", "setup.json") : join47(homedir26(), ".runwork", "setup.json");
24008
+ const path4 = scope === "project" ? join48(process.cwd(), ".runwork", "setup.json") : join48(homedir27(), ".runwork", "setup.json");
23158
24009
  return readJsonOrNull(path4);
23159
24010
  }
23160
24011
  async function parkAndTeardownWorkspace(previous, scopes) {
@@ -23323,8 +24174,8 @@ Re-run without --dry-run to sync workspace data.`);
23323
24174
  }
23324
24175
  persistDefaultWorkspace(workspaceId, workspaceName);
23325
24176
  for (const s of scopes) {
23326
- const dir = s === "project" ? ".runwork" : join47(homedir26(), ".runwork");
23327
- writeJsonAtomic(join47(dir, "setup.json"), state);
24177
+ const dir = s === "project" ? ".runwork" : join48(homedir27(), ".runwork");
24178
+ writeJsonAtomic(join48(dir, "setup.json"), state);
23328
24179
  }
23329
24180
  if (restored)
23330
24181
  clearParkedState(workspaceId);
@@ -23332,7 +24183,7 @@ Re-run without --dry-run to sync workspace data.`);
23332
24183
  Syncing workspace data...
23333
24184
  `);
23334
24185
  for (const s of scopes) {
23335
- const statePath2 = s === "project" ? join47(process.cwd(), ".runwork", "setup.json") : join47(homedir26(), ".runwork", "setup.json");
24186
+ const statePath2 = s === "project" ? join48(process.cwd(), ".runwork", "setup.json") : join48(homedir27(), ".runwork", "setup.json");
23336
24187
  await syncFromState(state, statePath2, credentials, {
23337
24188
  dryRun: false,
23338
24189
  pullOnly: true,
@@ -23353,11 +24204,11 @@ import { Command as Command28 } from "commander";
23353
24204
 
23354
24205
  // src/utils/setup-state.ts
23355
24206
  import { existsSync as existsSync52, readFileSync as readFileSync41 } from "fs";
23356
- import { join as join48 } from "path";
23357
- import { homedir as homedir27 } from "os";
24207
+ import { join as join49 } from "path";
24208
+ import { homedir as homedir28 } from "os";
23358
24209
  function loadSetupState2() {
23359
- const projectPath = join48(process.cwd(), ".runwork", "setup.json");
23360
- const userPath = join48(homedir27(), ".runwork", "setup.json");
24210
+ const projectPath = join49(process.cwd(), ".runwork", "setup.json");
24211
+ const userPath = join49(homedir28(), ".runwork", "setup.json");
23361
24212
  for (const p of [projectPath, userPath]) {
23362
24213
  if (existsSync52(p)) {
23363
24214
  try {
@@ -23421,8 +24272,8 @@ init_types();
23421
24272
  await init_detect();
23422
24273
  import { Command as Command29 } from "commander";
23423
24274
  import { existsSync as existsSync53, readFileSync as readFileSync42 } from "fs";
23424
- import { resolve as resolve3, join as join49 } from "path";
23425
- import { homedir as homedir28 } from "os";
24275
+ import { resolve as resolve3, join as join50 } from "path";
24276
+ import { homedir as homedir29 } from "os";
23426
24277
  function loadSetupState3(filePath) {
23427
24278
  if (!existsSync53(filePath))
23428
24279
  return null;
@@ -23443,8 +24294,8 @@ var buildPluginCommand = new Command29("build-plugin").description("Build an ins
23443
24294
  process.exit(1);
23444
24295
  }
23445
24296
  const credentials = requireAuth();
23446
- const projectStatePath = join49(process.cwd(), ".runwork", "setup.json");
23447
- const userStatePath = join49(homedir28(), ".runwork", "setup.json");
24297
+ const projectStatePath = join50(process.cwd(), ".runwork", "setup.json");
24298
+ const userStatePath = join50(homedir29(), ".runwork", "setup.json");
23448
24299
  const state = loadSetupState3(projectStatePath) ?? loadSetupState3(userStatePath);
23449
24300
  if (!state) {
23450
24301
  console.error("No setup state found. Run `runwork setup` first.");
@@ -23537,8 +24388,8 @@ init_prompt();
23537
24388
  await init_detect();
23538
24389
  import { Command as Command30 } from "commander";
23539
24390
  import { existsSync as existsSync54, readFileSync as readFileSync43, rmSync as rmSync13, unlinkSync as unlinkSync8 } from "fs";
23540
- import { join as join50 } from "path";
23541
- import { homedir as homedir29 } from "os";
24391
+ import { join as join51 } from "path";
24392
+ import { homedir as homedir30 } from "os";
23542
24393
  function loadSetupState4(filePath) {
23543
24394
  if (!existsSync54(filePath))
23544
24395
  return null;
@@ -23549,8 +24400,8 @@ function loadSetupState4(filePath) {
23549
24400
  }
23550
24401
  }
23551
24402
  var uninstallCommand = new Command30("uninstall").description("Remove all Runwork configuration from local agents (MCP servers, skills, instructions)").option("-y, --yes", "Skip confirmation prompt").option("--keep-auth", "Keep authentication credentials (only remove agent configs)").action(async (opts) => {
23552
- const projectStatePath = join50(process.cwd(), ".runwork", "setup.json");
23553
- const userStatePath = join50(homedir29(), ".runwork", "setup.json");
24403
+ const projectStatePath = join51(process.cwd(), ".runwork", "setup.json");
24404
+ const userStatePath = join51(homedir30(), ".runwork", "setup.json");
23554
24405
  const projectState = loadSetupState4(projectStatePath);
23555
24406
  const userState = loadSetupState4(userStatePath);
23556
24407
  if (!projectState && !userState) {
@@ -23630,9 +24481,9 @@ This will remove all Runwork configuration from your local agents:
23630
24481
  }
23631
24482
  }
23632
24483
  }
23633
- const stateDir = label === "project" ? join50(process.cwd(), ".runwork") : join50(homedir29(), ".runwork");
24484
+ const stateDir = label === "project" ? join51(process.cwd(), ".runwork") : join51(homedir30(), ".runwork");
23634
24485
  if (opts.keepAuth && label === "user") {
23635
- const setupFile = join50(stateDir, "setup.json");
24486
+ const setupFile = join51(stateDir, "setup.json");
23636
24487
  if (existsSync54(setupFile)) {
23637
24488
  try {
23638
24489
  unlinkSync8(setupFile);
@@ -23883,8 +24734,8 @@ init_credentials();
23883
24734
  await init_detect();
23884
24735
  import { parse as parse2 } from "smol-toml";
23885
24736
  import { existsSync as existsSync55, readFileSync as readFileSync45 } from "fs";
23886
- import { join as join51, sep as sep4 } from "path";
23887
- import { homedir as homedir30, platform as osPlatform2, arch as osArch } from "os";
24737
+ import { join as join52, sep as sep4 } from "path";
24738
+ import { homedir as homedir31, platform as osPlatform2, arch as osArch } from "os";
23888
24739
  var BASE_URL2 = process.env.RUNWORK_DOWNLOAD_BASE_URL || "https://runwork.ai";
23889
24740
  var LATEST_JSON_URL2 = `${BASE_URL2}/cli/latest.json`;
23890
24741
  function detectPlatform() {
@@ -23912,7 +24763,7 @@ function buildContext() {
23912
24763
  const credentials = getCredentials();
23913
24764
  const client = credentials ? new ApiClient(credentials) : null;
23914
24765
  let config = null;
23915
- const configPath = join51(process.cwd(), ".runwork.json");
24766
+ const configPath = join52(process.cwd(), ".runwork.json");
23916
24767
  if (existsSync55(configPath)) {
23917
24768
  try {
23918
24769
  config = JSON.parse(readFileSync45(configPath, "utf-8"));
@@ -24016,9 +24867,9 @@ async function checkCliArtifactReachable() {
24016
24867
  }
24017
24868
  async function checkCliInstallLocation() {
24018
24869
  const isWindows2 = osPlatform2() === "win32";
24019
- const home = homedir30();
24020
- const canonicalDir = join51(home, ".runwork", "bin");
24021
- const canonicalBinary = isWindows2 ? join51(canonicalDir, "runwork.exe") : join51(canonicalDir, "runwork");
24870
+ const home = homedir31();
24871
+ const canonicalDir = join52(home, ".runwork", "bin");
24872
+ const canonicalBinary = isWindows2 ? join52(canonicalDir, "runwork.exe") : join52(canonicalDir, "runwork");
24022
24873
  const candidates = [process.execPath, process.argv[1] || ""].filter(Boolean);
24023
24874
  const runsFromCanonical = candidates.some((p) => normalizePath(p) === normalizePath(canonicalBinary));
24024
24875
  if (runsFromCanonical) {
@@ -24150,7 +25001,7 @@ async function checkGitCredentialHelper(ctx) {
24150
25001
  };
24151
25002
  }
24152
25003
  async function checkProjectConfig(ctx) {
24153
- const configPath = join51(ctx.cwd, ".runwork.json");
25004
+ const configPath = join52(ctx.cwd, ".runwork.json");
24154
25005
  if (!existsSync55(configPath)) {
24155
25006
  if (!ctx.credentials) {
24156
25007
  return { name: "project-config", status: "skip", message: "no project (not logged in)" };
@@ -24213,7 +25064,7 @@ async function checkGitRemote(ctx) {
24213
25064
  if (!ctx.config) {
24214
25065
  return { name: "git-remote", status: "skip", message: "skipped (no project)" };
24215
25066
  }
24216
- if (!existsSync55(join51(ctx.cwd, ".git"))) {
25067
+ if (!existsSync55(join52(ctx.cwd, ".git"))) {
24217
25068
  return {
24218
25069
  name: "git-remote",
24219
25070
  status: "fail",
@@ -24267,8 +25118,8 @@ async function checkDeployFreshness(ctx) {
24267
25118
  return { name: "deploy-freshness", status: "skip", message: "local HEAD unknown" };
24268
25119
  }
24269
25120
  function loadSetupState5() {
24270
- const projectPath = join51(process.cwd(), ".runwork", "setup.json");
24271
- const userPath = join51(homedir30(), ".runwork", "setup.json");
25121
+ const projectPath = join52(process.cwd(), ".runwork", "setup.json");
25122
+ const userPath = join52(homedir31(), ".runwork", "setup.json");
24272
25123
  for (const p of [projectPath, userPath]) {
24273
25124
  if (existsSync55(p)) {
24274
25125
  try {
@@ -24287,7 +25138,7 @@ async function checkCodexNetwork() {
24287
25138
  if (!state || !state.configuredAgents.includes("codex")) {
24288
25139
  return { name, status: "skip", message: "Codex not configured for Runwork" };
24289
25140
  }
24290
- const configPath = join51(homedir30(), ".codex", "config.toml");
25141
+ const configPath = join52(homedir31(), ".codex", "config.toml");
24291
25142
  if (!existsSync55(configPath)) {
24292
25143
  return { name, status: "skip", message: "no Codex config found" };
24293
25144
  }
@@ -24346,7 +25197,7 @@ async function checkCodexDesktopProject() {
24346
25197
  if (!usesCodex) {
24347
25198
  return { name, status: "skip", message: "Codex not configured for Runwork" };
24348
25199
  }
24349
- const statePath2 = join51(homedir30(), ".codex", ".codex-global-state.json");
25200
+ const statePath2 = join52(homedir31(), ".codex", ".codex-global-state.json");
24350
25201
  if (!existsSync55(statePath2)) {
24351
25202
  return { name, status: "skip", message: "Codex desktop app not detected" };
24352
25203
  }
@@ -24358,7 +25209,7 @@ async function checkCodexDesktopProject() {
24358
25209
  } catch {
24359
25210
  return { name, status: "warn", message: "could not read Codex desktop state" };
24360
25211
  }
24361
- const runworkDir = join51(homedir30(), ".runwork");
25212
+ const runworkDir = join52(homedir31(), ".runwork");
24362
25213
  if (savedRoots.includes(runworkDir)) {
24363
25214
  return { name, status: "pass", message: "Runwork project added to Codex desktop sidebar" };
24364
25215
  }
@@ -24436,7 +25287,7 @@ async function checkAgentSetup() {
24436
25287
  if (!skillsDir)
24437
25288
  continue;
24438
25289
  const missingSkills = state.skills.filter((name) => {
24439
- const skillPath = join51(skillsDir, name, "SKILL.md");
25290
+ const skillPath = join52(skillsDir, name, "SKILL.md");
24440
25291
  return !existsSync55(skillPath);
24441
25292
  });
24442
25293
  if (missingSkills.length > 0) {
@@ -24462,25 +25313,25 @@ async function checkAgentSetup() {
24462
25313
  };
24463
25314
  }
24464
25315
  function getMcpConfigPath2(slug, scope) {
24465
- const home = homedir30();
25316
+ const home = homedir31();
24466
25317
  switch (slug) {
24467
25318
  case "claude-code":
24468
- return scope === "project" ? join51(process.cwd(), ".mcp.json") : join51(home, ".claude", "settings.json");
25319
+ return scope === "project" ? join52(process.cwd(), ".mcp.json") : join52(home, ".claude", "settings.json");
24469
25320
  case "cursor":
24470
- return scope === "project" ? join51(process.cwd(), ".cursor", "mcp.json") : join51(home, ".cursor", "mcp.json");
25321
+ return scope === "project" ? join52(process.cwd(), ".cursor", "mcp.json") : join52(home, ".cursor", "mcp.json");
24471
25322
  case "windsurf":
24472
- return scope === "project" ? join51(process.cwd(), ".windsurf", "mcp.json") : join51(home, ".windsurf", "mcp.json");
25323
+ return scope === "project" ? join52(process.cwd(), ".windsurf", "mcp.json") : join52(home, ".windsurf", "mcp.json");
24473
25324
  case "codex":
24474
25325
  case "codex-app":
24475
- return scope === "user" ? join51(home, ".codex", "config.toml") : null;
25326
+ return scope === "user" ? join52(home, ".codex", "config.toml") : null;
24476
25327
  case "gemini":
24477
- return scope === "user" ? join51(home, ".gemini", "settings.json") : null;
25328
+ return scope === "user" ? join52(home, ".gemini", "settings.json") : null;
24478
25329
  default:
24479
25330
  return null;
24480
25331
  }
24481
25332
  }
24482
25333
  async function checkWorkspacePointers() {
24483
- const userStatePath = join51(homedir30(), ".runwork", "setup.json");
25334
+ const userStatePath = join52(homedir31(), ".runwork", "setup.json");
24484
25335
  const state = existsSync55(userStatePath) ? (() => {
24485
25336
  try {
24486
25337
  return JSON.parse(readFileSync45(userStatePath, "utf-8"));
@@ -24513,15 +25364,15 @@ async function checkWorkspacePointers() {
24513
25364
  };
24514
25365
  }
24515
25366
  function getSkillsDir(slug, scope) {
24516
- const home = homedir30();
25367
+ const home = homedir31();
24517
25368
  switch (slug) {
24518
25369
  case "claude-code":
24519
- return scope === "project" ? join51(process.cwd(), ".claude", "skills") : join51(home, ".claude", "skills");
25370
+ return scope === "project" ? join52(process.cwd(), ".claude", "skills") : join52(home, ".claude", "skills");
24520
25371
  case "codex":
24521
25372
  case "codex-app":
24522
- return scope === "project" ? join51(process.cwd(), ".agents", "skills") : join51(home, ".agents", "skills");
25373
+ return scope === "project" ? join52(process.cwd(), ".agents", "skills") : join52(home, ".agents", "skills");
24523
25374
  case "gemini":
24524
- return scope === "project" ? join51(process.cwd(), ".gemini", "skills") : join51(home, ".gemini", "skills");
25375
+ return scope === "project" ? join52(process.cwd(), ".gemini", "skills") : join52(home, ".gemini", "skills");
24525
25376
  default:
24526
25377
  return null;
24527
25378
  }
@@ -24575,7 +25426,7 @@ async function runAllChecks(options) {
24575
25426
  init_credentials();
24576
25427
  init_remote();
24577
25428
  import { existsSync as existsSync56 } from "fs";
24578
- import { join as join52 } from "path";
25429
+ import { join as join53 } from "path";
24579
25430
  async function applyDoctorFixes(ctx, failingNames) {
24580
25431
  const failing = new Set(failingNames);
24581
25432
  const outcomes = [];
@@ -24602,7 +25453,7 @@ async function applyDoctorFixes(ctx, failingNames) {
24602
25453
  applied: false,
24603
25454
  message: "no project config -- run inside an app directory"
24604
25455
  });
24605
- } else if (!existsSync56(join52(ctx.cwd, ".git"))) {
25456
+ } else if (!existsSync56(join53(ctx.cwd, ".git"))) {
24606
25457
  outcomes.push({
24607
25458
  name: "git-remote",
24608
25459
  applied: false,
@@ -24622,9 +25473,9 @@ async function applyDoctorFixes(ctx, failingNames) {
24622
25473
 
24623
25474
  // src/agents/runtime-detection.ts
24624
25475
  import { existsSync as existsSync57, readFileSync as readFileSync46, statSync as statSync10, readdirSync as readdirSync16 } from "fs";
24625
- import { homedir as homedir31 } from "os";
24626
- import { join as join53 } from "path";
24627
- var RUNWORK_SESSIONS_DIR = join53(homedir31(), ".runwork", "sessions");
25476
+ import { homedir as homedir32 } from "os";
25477
+ import { join as join54 } from "path";
25478
+ var RUNWORK_SESSIONS_DIR = join54(homedir32(), ".runwork", "sessions");
24628
25479
  function detectCurrentAgent() {
24629
25480
  const claudeCodeSessionId = process.env.CLAUDE_CODE_SESSION_ID;
24630
25481
  if (claudeCodeSessionId) {
@@ -24687,7 +25538,7 @@ function detectCurrentAgent() {
24687
25538
  return null;
24688
25539
  }
24689
25540
  function readHookSessionInfo(sessionId) {
24690
- const path4 = join53(RUNWORK_SESSIONS_DIR, `${sessionId}.json`);
25541
+ const path4 = join54(RUNWORK_SESSIONS_DIR, `${sessionId}.json`);
24691
25542
  if (!existsSync57(path4))
24692
25543
  return null;
24693
25544
  try {
@@ -24699,7 +25550,7 @@ function readHookSessionInfo(sessionId) {
24699
25550
  }
24700
25551
  }
24701
25552
  function findClaudeCodeSessionFile(sessionId) {
24702
- const root = join53(homedir31(), ".claude", "projects");
25553
+ const root = join54(homedir32(), ".claude", "projects");
24703
25554
  if (!existsSync57(root))
24704
25555
  return null;
24705
25556
  let projectDirs;
@@ -24709,14 +25560,14 @@ function findClaudeCodeSessionFile(sessionId) {
24709
25560
  return null;
24710
25561
  }
24711
25562
  for (const dir of projectDirs) {
24712
- const candidate = join53(root, dir, `${sessionId}.jsonl`);
25563
+ const candidate = join54(root, dir, `${sessionId}.jsonl`);
24713
25564
  if (existsSync57(candidate))
24714
25565
  return candidate;
24715
25566
  }
24716
25567
  return null;
24717
25568
  }
24718
25569
  function findCodexRolloutFile(threadId) {
24719
- const root = join53(homedir31(), ".codex", "sessions");
25570
+ const root = join54(homedir32(), ".codex", "sessions");
24720
25571
  if (!existsSync57(root))
24721
25572
  return null;
24722
25573
  const stack = [root];
@@ -24729,7 +25580,7 @@ function findCodexRolloutFile(threadId) {
24729
25580
  continue;
24730
25581
  }
24731
25582
  for (const entry of entries) {
24732
- const full = join53(dir, entry);
25583
+ const full = join54(dir, entry);
24733
25584
  let s;
24734
25585
  try {
24735
25586
  s = statSync10(full);
@@ -24746,7 +25597,7 @@ function findCodexRolloutFile(threadId) {
24746
25597
  return null;
24747
25598
  }
24748
25599
  function findNewestClaudeCodeSession() {
24749
- const root = join53(homedir31(), ".claude", "projects");
25600
+ const root = join54(homedir32(), ".claude", "projects");
24750
25601
  if (!existsSync57(root))
24751
25602
  return null;
24752
25603
  let projectDirs;
@@ -24757,7 +25608,7 @@ function findNewestClaudeCodeSession() {
24757
25608
  }
24758
25609
  let best = null;
24759
25610
  for (const dir of projectDirs) {
24760
- const projectPath = join53(root, dir);
25611
+ const projectPath = join54(root, dir);
24761
25612
  let files;
24762
25613
  try {
24763
25614
  files = readdirSync16(projectPath);
@@ -24767,7 +25618,7 @@ function findNewestClaudeCodeSession() {
24767
25618
  for (const file of files) {
24768
25619
  if (!file.endsWith(".jsonl"))
24769
25620
  continue;
24770
- const full = join53(projectPath, file);
25621
+ const full = join54(projectPath, file);
24771
25622
  try {
24772
25623
  const s = statSync10(full);
24773
25624
  if (!best || s.mtimeMs > best.mtime) {
@@ -24785,7 +25636,7 @@ function findNewestClaudeCodeSession() {
24785
25636
  return best ? { sessionId: best.sessionId, path: best.path } : null;
24786
25637
  }
24787
25638
  function findNewestCodexRollout() {
24788
- const root = join53(homedir31(), ".codex", "sessions");
25639
+ const root = join54(homedir32(), ".codex", "sessions");
24789
25640
  if (!existsSync57(root))
24790
25641
  return null;
24791
25642
  const stack = [root];
@@ -24799,7 +25650,7 @@ function findNewestCodexRollout() {
24799
25650
  continue;
24800
25651
  }
24801
25652
  for (const entry of entries) {
24802
- const full = join53(dir, entry);
25653
+ const full = join54(dir, entry);
24803
25654
  let s;
24804
25655
  try {
24805
25656
  s = statSync10(full);
@@ -25032,9 +25883,9 @@ init_client();
25032
25883
  init_resolve();
25033
25884
  import { Command as Command35 } from "commander";
25034
25885
  import { readFileSync as readFileSync47, writeFileSync as writeFileSync31, existsSync as existsSync58, mkdtempSync as mkdtempSync4 } from "fs";
25035
- import { join as join54 } from "path";
25886
+ import { join as join55 } from "path";
25036
25887
  import { tmpdir as tmpdir4 } from "os";
25037
- import { createHash as createHash5 } from "crypto";
25888
+ import { createHash as createHash6 } from "crypto";
25038
25889
 
25039
25890
  // src/agents/utils/transcript-render.ts
25040
25891
  init_session_digest();
@@ -25159,8 +26010,8 @@ function resolveLocalSessionShare(opts, conversation) {
25159
26010
  console.error("Error: this conversation has no shareable content.");
25160
26011
  process.exit(1);
25161
26012
  }
25162
- const tempDir = mkdtempSync4(join54(tmpdir4(), "runwork-share-"));
25163
- const transcriptFile = join54(tempDir, "transcript.md");
26013
+ const tempDir = mkdtempSync4(join55(tmpdir4(), "runwork-share-"));
26014
+ const transcriptFile = join55(tempDir, "transcript.md");
25164
26015
  writeFileSync31(transcriptFile, markdown);
25165
26016
  opts.transcriptFile = transcriptFile;
25166
26017
  opts.nativeFile = opts.nativeFile ?? conversation.transcriptPath;
@@ -25178,7 +26029,7 @@ function nativeBundleFormatForAgent(slug) {
25178
26029
  return null;
25179
26030
  }
25180
26031
  function sha256Hex(content) {
25181
- return createHash5("sha256").update(content, "utf8").digest("hex");
26032
+ return createHash6("sha256").update(content, "utf8").digest("hex");
25182
26033
  }
25183
26034
  function utf8ByteLength(content) {
25184
26035
  return Buffer.byteLength(content, "utf8");
@@ -25370,8 +26221,8 @@ init_resolve();
25370
26221
  init_registry_data();
25371
26222
  import { Command as Command38 } from "commander";
25372
26223
  import { writeFileSync as writeFileSync32, mkdirSync as mkdirSync28, realpathSync } from "fs";
25373
- import { homedir as homedir32 } from "os";
25374
- import { join as join55 } from "path";
26224
+ import { homedir as homedir33 } from "os";
26225
+ import { join as join56 } from "path";
25375
26226
  import { spawn as spawn5 } from "child_process";
25376
26227
  init_registry();
25377
26228
  init_which();
@@ -25410,9 +26261,9 @@ function extractCodexUuid(rolloutContent) {
25410
26261
  }
25411
26262
  function placeClaudeJsonl(uuid, content, recipientCwd) {
25412
26263
  const encoded = encodeClaudeCodeCwd(recipientCwd);
25413
- const projectDir = join55(homedir32(), ".claude", "projects", encoded);
26264
+ const projectDir = join56(homedir33(), ".claude", "projects", encoded);
25414
26265
  mkdirSync28(projectDir, { recursive: true });
25415
- const placedAt = join55(projectDir, `${uuid}.jsonl`);
26266
+ const placedAt = join56(projectDir, `${uuid}.jsonl`);
25416
26267
  writeFileSync32(placedAt, content);
25417
26268
  return { placedAt, runFromCwd: recipientCwd };
25418
26269
  }
@@ -25421,10 +26272,10 @@ function placeCodexRollout(uuid, content) {
25421
26272
  const yyyy = String(now.getUTCFullYear());
25422
26273
  const mm = String(now.getUTCMonth() + 1).padStart(2, "0");
25423
26274
  const dd = String(now.getUTCDate()).padStart(2, "0");
25424
- const dir = join55(homedir32(), ".codex", "sessions", yyyy, mm, dd);
26275
+ const dir = join56(homedir33(), ".codex", "sessions", yyyy, mm, dd);
25425
26276
  mkdirSync28(dir, { recursive: true });
25426
26277
  const ts = now.toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "-");
25427
- const placedAt = join55(dir, `rollout-${ts}-${uuid}.jsonl`);
26278
+ const placedAt = join56(dir, `rollout-${ts}-${uuid}.jsonl`);
25428
26279
  writeFileSync32(placedAt, content);
25429
26280
  return { placedAt };
25430
26281
  }