runwork 0.24.0 → 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.
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
  });
@@ -5100,8 +5101,8 @@ export declare function componentRoutes(app: Hono<{
5100
5101
  Bindings: Env;
5101
5102
  }>): void;
5102
5103
  `,
5103
- "workspace.d.ts": `export { WorkspaceContext, getWorkspaceContext, listWorkspaceEntity, getWorkspaceEntity, createWorkspaceEntity, updateWorkspaceEntity, deleteWorkspaceEntity, initializeWorkspace, } from './core-workspace';
5104
- export type { WorkspaceUser, ListUsersOptions, ListUsersResponse, NotifyUserParams, NotifyUserResult, ListEntityRequest, ListEntityResponse, GetEntityRequest, CreateEntityRequest, UpdateEntityRequest, DeleteEntityRequest, RegisterEntityRequest, RegisterAppRequest, RegisterComponentRequest, RegisterScheduleRequest, RegisterWorkflowRequest, RegisterEndpointRequest, RegisterIntegrationRequest, RegisterAgentRequest as WorkspaceRegisterAgentRequest, } from './core-workspace';
5104
+ "workspace.d.ts": `export { WorkspaceContext, getWorkspaceContext, listWorkspaceEntity, getWorkspaceEntity, createWorkspaceEntity, updateWorkspaceEntity, deleteWorkspaceEntity, callAppEndpoint, initializeWorkspace, } from './core-workspace';
5105
+ export type { WorkspaceUser, ListUsersOptions, ListUsersResponse, NotifyUserParams, NotifyUserResult, ListEntityRequest, ListEntityResponse, GetEntityRequest, CreateEntityRequest, UpdateEntityRequest, DeleteEntityRequest, RegisterEntityRequest, RegisterAppRequest, RegisterComponentRequest, RegisterScheduleRequest, RegisterWorkflowRequest, RegisterEndpointRequest, RegisterIntegrationRequest, CallAppEndpointOptions, CallAppEndpointResult, RegisterAgentRequest as WorkspaceRegisterAgentRequest, } from './core-workspace';
5105
5106
  `,
5106
5107
  "core-scheduler.d.ts": `/**
5107
5108
  * Core Scheduled Jobs Framework
@@ -5443,11 +5444,16 @@ type ZodSchema = z.ZodType<any, any, any>;
5443
5444
  export type EndpointAuthType = 'apiKey' | 'public';
5444
5445
  /**
5445
5446
  * Caller principal type resolved by the platform and passed via the
5446
- * \`X-Public-Endpoint-Auth-Type\` header. \`user\` and \`workspace_key\` are
5447
+ * \`X-Public-Endpoint-Auth-Type\` header. \`user\`, \`workspace_key\` and \`app\` are
5447
5448
  * authenticated principals the platform has already authorized; \`api_key\`
5448
5449
  * additionally carries a validated key id; \`none\` is unauthenticated.
5450
+ *
5451
+ * \`app\` means a sibling app in the same workspace called this endpoint through
5452
+ * the platform (see \`callAppEndpoint\`). The platform records which app made the
5453
+ * call, but does not forward that id here: it is self-asserted by the caller and
5454
+ * must not be used for authorization until app identity is verifiable.
5449
5455
  */
5450
- export type EndpointPrincipalType = 'none' | 'api_key' | 'workspace_key' | 'user';
5456
+ export type EndpointPrincipalType = 'none' | 'api_key' | 'workspace_key' | 'user' | 'app';
5451
5457
  /**
5452
5458
  * HTTP methods supported by endpoints
5453
5459
  */
@@ -5490,7 +5496,7 @@ export interface EndpointContext<TQuery = Record<string, unknown>, TBody = unkno
5490
5496
  export interface EndpointAuthInfo {
5491
5497
  /** Authentication requirement declared by the endpoint */
5492
5498
  type: EndpointAuthType;
5493
- /** Resolved caller principal type (api_key, workspace_key, user, none) */
5499
+ /** Resolved caller principal type (api_key, workspace_key, user, app, none) */
5494
5500
  principalType?: EndpointPrincipalType;
5495
5501
  /** API key ID (if authenticated with an API key) */
5496
5502
  apiKeyId?: string;
@@ -6319,6 +6325,34 @@ export interface RegisterAgentRequest extends Record<string, unknown> {
6319
6325
  * WorkspaceContext - Helper for cross-app data queries
6320
6326
  * Provides methods to query entities from other apps in the same workspace
6321
6327
  */
6328
+ /**
6329
+ * Options for calling a public endpoint on another app in the workspace.
6330
+ */
6331
+ export interface CallAppEndpointOptions {
6332
+ /**
6333
+ * Target app, normally its slug as shown in the dashboard (e.g. 'labcrm').
6334
+ * An app id or worker name also resolves. If the slug is wrong, the error
6335
+ * response lists the slugs available in this workspace.
6336
+ */
6337
+ app: string;
6338
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
6339
+ /** Endpoint path exactly as registered, path params included: '/v1/orders/:id'. */
6340
+ path: string;
6341
+ /** Values for the \`:param\` segments in \`path\`. */
6342
+ pathParams?: Record<string, string>;
6343
+ queryParams?: Record<string, string>;
6344
+ /** JSON body. Ignored for GET and HEAD. */
6345
+ body?: unknown;
6346
+ }
6347
+ /**
6348
+ * Result of a cross-app endpoint call. \`ok\` and \`status\` are the target
6349
+ * endpoint's own response status, not the transport's.
6350
+ */
6351
+ export interface CallAppEndpointResult<T = unknown> {
6352
+ ok: boolean;
6353
+ status: number;
6354
+ data: T;
6355
+ }
6322
6356
  export declare class WorkspaceContext {
6323
6357
  private env;
6324
6358
  private workspaceId;
@@ -6355,6 +6389,45 @@ export declare class WorkspaceContext {
6355
6389
  * Delete an entity in another app in the workspace
6356
6390
  */
6357
6391
  deleteEntity(entityName: string, id: string): Promise<void>;
6392
+ /**
6393
+ * Transport for cross-app endpoint calls.
6394
+ *
6395
+ * Deliberately NOT \`workspaceFetch\`, for two reasons:
6396
+ *
6397
+ * 1. \`workspaceFetch\` retries over HTTP whenever the Durable Object returns a
6398
+ * non-2xx. That is right for entity CRUD, where a non-2xx means the DO
6399
+ * itself failed. Here a non-2xx is the TARGET ENDPOINT'S OWN ANSWER, so
6400
+ * retrying would re-send the request — duplicating POSTs — and in production
6401
+ * the retry goes to the platform's own domain, which a Workers for Platforms
6402
+ * worker cannot fetch: it hangs until timeout, the very failure this function
6403
+ * exists to avoid. A status is a result, never a transport error.
6404
+ *
6405
+ * 2. Production therefore uses the DO binding ONLY, mirroring \`workspaceApiFetch\`
6406
+ * in core-utils.ts, which is the proven path for every other platform call
6407
+ * made from a deployed app. Preview and sandbox keep the HTTP path, where the
6408
+ * app is reached over a tunnel rather than the dispatch namespace.
6409
+ */
6410
+ private endpointCallFetch;
6411
+ /**
6412
+ * Call a public endpoint on another app in this workspace.
6413
+ *
6414
+ * Production apps run in a Workers for Platforms dispatch namespace and cannot
6415
+ * HTTP-fetch the platform's app domain, so a direct \`fetch()\` to a sibling app's
6416
+ * public URL hangs until it times out. This routes through the workspace instead,
6417
+ * which reaches the target app via the dispatcher.
6418
+ *
6419
+ * The call arrives at the target as an \`app\` principal, which satisfies endpoints
6420
+ * declared \`auth: 'apiKey'\`. No API key is needed or sent.
6421
+ *
6422
+ * @example
6423
+ * const invite = await callAppEndpoint(env, {
6424
+ * app: 'labcrm',
6425
+ * method: 'GET',
6426
+ * path: '/v1/portal/invite/:token',
6427
+ * pathParams: { token },
6428
+ * });
6429
+ */
6430
+ callAppEndpoint<T = unknown>(options: CallAppEndpointOptions): Promise<CallAppEndpointResult<T>>;
6358
6431
  /**
6359
6432
  * Register an entity with the workspace so other apps can query it
6360
6433
  */
@@ -6587,6 +6660,22 @@ export declare function listWorkspaceEntity<T = unknown>(env: Env, entityName: s
6587
6660
  * @example
6588
6661
  * const contact = await getWorkspaceEntity<Contact>(env, 'Contact', 'contact-123');
6589
6662
  */
6663
+ /**
6664
+ * Call a public endpoint on ANOTHER app in this workspace.
6665
+ *
6666
+ * Use this instead of \`fetch()\` against a sibling app's public URL: production apps
6667
+ * cannot HTTP-fetch the platform's app domain and such a fetch hangs until timeout.
6668
+ *
6669
+ * @example
6670
+ * const result = await callAppEndpoint(env, {
6671
+ * app: 'labcrm',
6672
+ * method: 'POST',
6673
+ * path: '/v1/portal/orders',
6674
+ * body: { items },
6675
+ * });
6676
+ * if (!result.ok) throw new Error(\`CRM rejected the order: \${result.status}\`);
6677
+ */
6678
+ export declare function callAppEndpoint<T = unknown>(env: Env, options: CallAppEndpointOptions): Promise<CallAppEndpointResult<T>>;
6590
6679
  export declare function getWorkspaceEntity<T = unknown>(env: Env, entityName: string, id: string): Promise<T | null>;
6591
6680
  /**
6592
6681
  * Create an entity in ANOTHER app in the workspace (cross-app ONLY)
@@ -6914,6 +7003,13 @@ export declare class BaseAgent extends AIChatAgent<Env> {
6914
7003
  * Build tools for accessing entities using AI SDK tool() helper
6915
7004
  * Uses entity classes directly with EntityContext
6916
7005
  */
7006
+ /**
7007
+ * One tool per endpoint the agent is allowed to call on another app.
7008
+ *
7009
+ * Routed through \`callAppEndpoint\`, so it works in preview and production
7010
+ * alike: a production app cannot HTTP-fetch a sibling's public URL.
7011
+ */
7012
+ private buildAppEndpointTools;
6917
7013
  private buildEntityTools;
6918
7014
  /**
6919
7015
  * Build tools for running complex tasks via a sandboxed AI sub-agent
@@ -7353,6 +7449,16 @@ export interface AgentDefinition {
7353
7449
  integrations?: string[];
7354
7450
  /** Entity names the agent can read/write */
7355
7451
  entities?: string[];
7452
+ /**
7453
+ * Endpoints on OTHER apps in this workspace that the agent may call, as
7454
+ * \`'<app>:<METHOD>:<path>'\` — e.g. \`'labcrm:GET:/v1/portal/catalog'\`.
7455
+ * \`<app>\` is the target app's slug; \`<path>\` is the path exactly as registered,
7456
+ * \`:param\` placeholders included.
7457
+ *
7458
+ * Declared explicitly, like \`entities\` and \`integrations\`: an agent can only
7459
+ * call what the app author listed, never an arbitrary endpoint.
7460
+ */
7461
+ appEndpoints?: string[];
7356
7462
  /** Memory strategy */
7357
7463
  memoryStrategy?: MemoryStrategy;
7358
7464
  /** Custom tools for this agent */
@@ -7362,6 +7468,27 @@ export interface AgentDefinition {
7362
7468
  /** Cost/time limits for task agent sandbox execution. Optional overrides. */
7363
7469
  executionLimits?: ExecutionLimits;
7364
7470
  }
7471
+ /** A parsed \`'<app>:<METHOD>:<path>'\` agent endpoint reference. */
7472
+ export interface AppEndpointRef {
7473
+ app: string;
7474
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
7475
+ path: string;
7476
+ }
7477
+ /**
7478
+ * Parse \`'labcrm:GET:/v1/portal/invite/:token'\`.
7479
+ *
7480
+ * Split on the first two colons only: the path itself contains colons for its
7481
+ * \`:param\` segments, so a naive split would mangle every parameterized endpoint.
7482
+ * Returns null for anything malformed rather than throwing, so one bad entry in an
7483
+ * agent definition costs that one tool instead of the whole agent.
7484
+ */
7485
+ export declare function parseAppEndpointRef(ref: string): AppEndpointRef | null;
7486
+ /**
7487
+ * Stable, model-friendly tool name for an endpoint reference, e.g.
7488
+ * \`call_labcrm_get_v1_portal_invite\`. Param segments are dropped so the name
7489
+ * stays readable; the full path is in the tool description.
7490
+ */
7491
+ export declare function appEndpointToolName(ref: AppEndpointRef): string;
7365
7492
  /**
7366
7493
  * Custom tool definition for agents
7367
7494
  */
@@ -7713,7 +7840,7 @@ function createKeyboardListener() {
7713
7840
  }
7714
7841
 
7715
7842
  // src/generated/version.ts
7716
- var VERSION = "0.24.0";
7843
+ var VERSION = "0.25.0";
7717
7844
 
7718
7845
  // src/commands/dev.ts
7719
7846
  var exports_dev = {};
@@ -8762,10 +8889,19 @@ function saveDefaultWorkspace(workspaceId, workspaceName) {
8762
8889
  function hasProjectConfig() {
8763
8890
  return existsSync21(".runwork.json");
8764
8891
  }
8892
+ function matchAppRef(apps, ref) {
8893
+ return apps.find((a) => a.id === ref || a.name === ref || a.slug === ref);
8894
+ }
8895
+ function filterRowsByApp(rows, appRef, apps) {
8896
+ const match = matchAppRef(apps, appRef);
8897
+ if (!match)
8898
+ return null;
8899
+ return rows.filter((row) => row.appId === match.id);
8900
+ }
8765
8901
  async function resolveApp2(client, workspaceId, options = {}) {
8766
8902
  if (options.app) {
8767
8903
  const apps = await client.listApps(workspaceId);
8768
- const match = apps.find((a) => a.id === options.app || a.name === options.app || a.slug === options.app);
8904
+ const match = matchAppRef(apps, options.app);
8769
8905
  if (!match) {
8770
8906
  console.error(`App "${options.app}" not found in workspace.`);
8771
8907
  process.exit(1);
@@ -9612,6 +9748,187 @@ var init_registry = __esm(() => {
9612
9748
  });
9613
9749
 
9614
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
+ }
9615
9932
  function extractClaudeJsonlSession(raw, agentSlug, project) {
9616
9933
  const digest = {
9617
9934
  agentSlug,
@@ -9625,6 +9942,21 @@ function extractClaudeJsonlSession(raw, agentSlug, project) {
9625
9942
  errorCount: 0,
9626
9943
  assistantTurns: 0
9627
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;
9628
9960
  for (const line of raw.split(`
9629
9961
  `)) {
9630
9962
  if (!line.trim())
@@ -9640,12 +9972,29 @@ function extractClaudeJsonlSession(raw, agentSlug, project) {
9640
9972
  if (!digest.start)
9641
9973
  digest.start = ts;
9642
9974
  digest.end = ts;
9975
+ const ms = Date.parse(ts);
9976
+ if (Number.isFinite(ms))
9977
+ epochMs.push(ms);
9643
9978
  }
9644
9979
  const type = obj.type;
9980
+ if (type === "permission-mode" && typeof obj.permissionMode === "string") {
9981
+ modeStream.push(obj.permissionMode);
9982
+ continue;
9983
+ }
9645
9984
  const message = obj.message;
9646
9985
  if (!message || typeof message !== "object")
9647
9986
  continue;
9648
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");
9649
9998
  const texts = [];
9650
9999
  if (typeof message.content === "string") {
9651
10000
  texts.push(message.content);
@@ -9655,15 +10004,22 @@ function extractClaudeJsonlSession(raw, agentSlug, project) {
9655
10004
  const it = item;
9656
10005
  if (it.type === "text" && typeof it.text === "string")
9657
10006
  texts.push(it.text);
9658
- if (it.type === "tool_result" && it.is_error === true)
9659
- 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
+ }
9660
10012
  }
9661
10013
  }
9662
10014
  }
9663
10015
  for (const text2 of texts) {
9664
10016
  const trimmed = text2.trim();
10017
+ if (trimmed.startsWith("<command-name>"))
10018
+ slashCommandCount++;
9665
10019
  if (!trimmed || SKIP_PREFIXES.some((p) => trimmed.startsWith(p)))
9666
10020
  continue;
10021
+ if (humanLine)
10022
+ promptBuckets[promptLengthBucket(trimmed.length)]++;
9667
10023
  if (digest.userMessages.length >= MAX_MSGS_PER_SESSION) {
9668
10024
  digest.droppedUserMessages++;
9669
10025
  continue;
@@ -9671,15 +10027,47 @@ function extractClaudeJsonlSession(raw, agentSlug, project) {
9671
10027
  digest.userMessages.push(trimmed.length > MAX_MSG_CHARS ? trimmed.slice(0, MAX_MSG_CHARS) + " [...]" : trimmed);
9672
10028
  }
9673
10029
  }
9674
- 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;
9675
10050
  digest.assistantTurns++;
9676
10051
  for (const item of message.content) {
9677
10052
  if (item && typeof item === "object") {
9678
10053
  const it = item;
9679
10054
  if (it.type === "tool_use" && typeof it.name === "string") {
9680
10055
  digest.toolCounts[it.name] = (digest.toolCounts[it.name] ?? 0) + 1;
9681
- if (it.name === "Skill" && it.input?.skill)
9682
- 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++;
9683
10071
  }
9684
10072
  }
9685
10073
  }
@@ -9687,11 +10075,40 @@ function extractClaudeJsonlSession(raw, agentSlug, project) {
9687
10075
  }
9688
10076
  if (digest.userMessages.length === 0)
9689
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);
9690
10094
  return digest;
9691
10095
  }
9692
10096
  function decodeProjectDir(encoded) {
9693
10097
  return encoded.replace(/^-/, "/").replace(/-/g, "/");
9694
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
+ }
9695
10112
  function geminiMessageText(content) {
9696
10113
  if (typeof content === "string")
9697
10114
  return content;
@@ -9727,6 +10144,9 @@ function extractGeminiSession(raw, agentSlug, project) {
9727
10144
  digest.start = session.startTime;
9728
10145
  if (typeof session.lastUpdated === "string")
9729
10146
  digest.end = session.lastUpdated;
10147
+ const signals = emptySessionSignals();
10148
+ const epochMs = [];
10149
+ const tokensByModel = new Map;
9730
10150
  for (const raw2 of session.messages) {
9731
10151
  const m = raw2;
9732
10152
  if (typeof m.timestamp === "string") {
@@ -9734,9 +10154,21 @@ function extractGeminiSession(raw, agentSlug, project) {
9734
10154
  digest.start = m.timestamp;
9735
10155
  if (!digest.end || m.timestamp > digest.end)
9736
10156
  digest.end = m.timestamp;
10157
+ const ms = Date.parse(m.timestamp);
10158
+ if (Number.isFinite(ms))
10159
+ epochMs.push(ms);
9737
10160
  }
9738
10161
  if (m.type === "gemini") {
9739
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
+ }
9740
10172
  continue;
9741
10173
  }
9742
10174
  if (m.type === "error") {
@@ -9754,7 +10186,12 @@ function extractGeminiSession(raw, agentSlug, project) {
9754
10186
  }
9755
10187
  digest.userMessages.push(text2.length > MAX_MSG_CHARS ? `${text2.slice(0, MAX_MSG_CHARS)}...` : text2);
9756
10188
  }
9757
- 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;
9758
10195
  }
9759
10196
  function extractCodexRolloutSession(raw, agentSlug) {
9760
10197
  const digest = {
@@ -9769,6 +10206,14 @@ function extractCodexRolloutSession(raw, agentSlug) {
9769
10206
  errorCount: 0,
9770
10207
  assistantTurns: 0
9771
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;
9772
10217
  for (const line of raw.split(`
9773
10218
  `)) {
9774
10219
  if (!line.trim())
@@ -9784,12 +10229,49 @@ function extractCodexRolloutSession(raw, agentSlug) {
9784
10229
  if (!digest.start)
9785
10230
  digest.start = ts;
9786
10231
  digest.end = ts;
10232
+ const ms = Date.parse(ts);
10233
+ if (Number.isFinite(ms))
10234
+ epochMs.push(ms);
9787
10235
  }
9788
10236
  const p = o.payload;
9789
10237
  if (!p || typeof p !== "object")
9790
10238
  continue;
9791
- if (o.type === "session_meta" && typeof p.cwd === "string") {
9792
- 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
+ }
9793
10275
  continue;
9794
10276
  }
9795
10277
  if (o.type !== "response_item")
@@ -9815,6 +10297,17 @@ function extractCodexRolloutSession(raw, agentSlug) {
9815
10297
  }
9816
10298
  } else if (pt === "function_call" && typeof p.name === "string") {
9817
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++;
9818
10311
  } else if (pt === "tool_search_call") {
9819
10312
  digest.toolCounts["tool_search"] = (digest.toolCounts["tool_search"] ?? 0) + 1;
9820
10313
  }
@@ -9823,6 +10316,21 @@ function extractCodexRolloutSession(raw, agentSlug) {
9823
10316
  return null;
9824
10317
  if (!digest.project)
9825
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);
9826
10334
  return digest;
9827
10335
  }
9828
10336
  function extractCursorSessions(composerRows, bubbleRows, sinceISO) {
@@ -9871,6 +10379,7 @@ function extractCursorSessions(composerRows, bubbleRows, sinceISO) {
9871
10379
  errorCount: 0,
9872
10380
  assistantTurns: 0
9873
10381
  };
10382
+ const friction = new FrictionCounters;
9874
10383
  for (const b of bubbles) {
9875
10384
  if (b.ts) {
9876
10385
  if (!digest.start)
@@ -9881,6 +10390,7 @@ function extractCursorSessions(composerRows, bubbleRows, sinceISO) {
9881
10390
  digest.toolCounts[b.tool] = (digest.toolCounts[b.tool] ?? 0) + 1;
9882
10391
  if (b.status === "error")
9883
10392
  digest.errorCount++;
10393
+ friction.result(b.status === "error");
9884
10394
  } else if (b.type === 2) {
9885
10395
  digest.assistantTurns++;
9886
10396
  }
@@ -9907,6 +10417,9 @@ function extractCursorSessions(composerRows, bubbleRows, sinceISO) {
9907
10417
  continue;
9908
10418
  if (sinceISO && digest.end && digest.end < sinceISO)
9909
10419
  continue;
10420
+ const signals = emptySessionSignals();
10421
+ signals.toolFailureStreakMax = friction.failStreakMax;
10422
+ digest.signals = signals;
9910
10423
  digests.push(digest);
9911
10424
  }
9912
10425
  return digests;
@@ -9950,7 +10463,7 @@ function formatCombinedDigest(sessions, opts = { days: 7 }) {
9950
10463
  return lines.join(`
9951
10464
  `);
9952
10465
  }
9953
- 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;
9954
10467
  var init_session_digest = __esm(() => {
9955
10468
  SKIP_PREFIXES = [
9956
10469
  "<system-reminder",
@@ -10289,7 +10802,7 @@ import { createHash as createHash2 } from "crypto";
10289
10802
  function contentHash(content) {
10290
10803
  return "sha256:" + createHash2("sha256").update(content).digest("hex");
10291
10804
  }
10292
- function stableStringify(value) {
10805
+ function stableStringify2(value) {
10293
10806
  return JSON.stringify(sortValue(value));
10294
10807
  }
10295
10808
  function sortValue(value) {
@@ -10306,7 +10819,7 @@ function sortValue(value) {
10306
10819
  return value;
10307
10820
  }
10308
10821
  function configHash(value) {
10309
- return contentHash(stableStringify(value));
10822
+ return contentHash(stableStringify2(value));
10310
10823
  }
10311
10824
  var init_hash = () => {};
10312
10825
 
@@ -15720,10 +16233,104 @@ var init_conversation_registry = __esm(async () => {
15720
16233
 
15721
16234
  // src/reflect/session-summary.ts
15722
16235
  import { createHash as createHash4 } from "crypto";
16236
+ import { execFileSync as execFileSync3 } from "child_process";
15723
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
+ }
15724
16283
  function hashSessionKey(key) {
15725
16284
  return createHash4("sha256").update(key).digest("hex").slice(0, 32);
15726
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
+ }
15727
16334
  function digestForEntry(entry) {
15728
16335
  if (!entry.transcriptPath)
15729
16336
  return null;
@@ -15757,9 +16364,10 @@ function deriveMcpUsage(toolCounts) {
15757
16364
  }
15758
16365
  return { mcpCallCount, mcpServers };
15759
16366
  }
15760
- function buildSessionSummary(key, entry, digest) {
16367
+ function buildSessionSummary(key, entry, digest, opts = {}) {
15761
16368
  const toolCallCount = digest ? Object.values(digest.toolCounts).reduce((sum, n) => sum + n, 0) : null;
15762
16369
  const userMessageCount = digest ? digest.userMessages.length + digest.droppedUserMessages : null;
16370
+ const sig = digest?.signals;
15763
16371
  return {
15764
16372
  agentSlug: entry.agentSlug,
15765
16373
  sessionIdHash: hashSessionKey(key),
@@ -15772,7 +16380,32 @@ function buildSessionSummary(key, entry, digest) {
15772
16380
  toolCounts: digest ? digest.toolCounts : null,
15773
16381
  ...deriveMcpUsage(digest ? digest.toolCounts : null),
15774
16382
  errorCount: digest?.errorCount ?? null,
15775
- 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)
15776
16409
  };
15777
16410
  }
15778
16411
  function buildSessionSummaryEvent(summary, nowISO) {
@@ -15793,14 +16426,41 @@ function buildSessionSummaryEvent(summary, nowISO) {
15793
16426
  mcpCallCount: summary.mcpCallCount,
15794
16427
  mcpServers: summary.mcpServers,
15795
16428
  errorCount: summary.errorCount,
15796
- 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
15797
16455
  },
15798
16456
  timestamp: nowISO
15799
16457
  }
15800
16458
  };
15801
16459
  }
16460
+ var VENDOR_STRING_MAX_CHARS = 64, VENDOR_LIST_MAX_ITEMS = 12, PROJECT_PLACEHOLDERS;
15802
16461
  var init_session_summary = __esm(() => {
15803
16462
  init_session_digest();
16463
+ PROJECT_PLACEHOLDERS = new Set(["codex", "cowork", "cursor"]);
15804
16464
  });
15805
16465
 
15806
16466
  // src/reflect/telemetry-outbox.ts
@@ -15842,6 +16502,222 @@ var init_telemetry_outbox = __esm(() => {
15842
16502
  init_atomic_json();
15843
16503
  });
15844
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
+
15845
16721
  // src/reflect/triage.ts
15846
16722
  var exports_triage = {};
15847
16723
  __export(exports_triage, {
@@ -15852,7 +16728,8 @@ __export(exports_triage, {
15852
16728
  packTriageBatches: () => packTriageBatches,
15853
16729
  buildTriagePrompt: () => buildTriagePrompt,
15854
16730
  TRIAGE_ITEM_CHAR_CAP: () => TRIAGE_ITEM_CHAR_CAP,
15855
- TRIAGE_BATCH_CHAR_BUDGET: () => TRIAGE_BATCH_CHAR_BUDGET
16731
+ TRIAGE_BATCH_CHAR_BUDGET: () => TRIAGE_BATCH_CHAR_BUDGET,
16732
+ TASK_TYPES: () => TASK_TYPES
15856
16733
  });
15857
16734
  function renderTriageItem(id, candidate) {
15858
16735
  const d = candidate.digest;
@@ -15901,12 +16778,17 @@ You are the cheap triage tier of Runwork's reflection engine. At the end of this
15901
16778
 
15902
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.
15903
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
+
15904
16786
  Reply with ONLY a single fenced \`json\` block:
15905
16787
 
15906
16788
  \`\`\`json
15907
16789
  {
15908
16790
  "verdicts": [
15909
- { "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>"] }
15910
16792
  ]
15911
16793
  }
15912
16794
  \`\`\`
@@ -15936,9 +16818,13 @@ function parseTriageVerdicts(text2) {
15936
16818
  const v = raw;
15937
16819
  if (typeof v.id !== "string" || typeof v.worthy !== "boolean")
15938
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()) : [];
15939
16823
  out.set(v.id, {
15940
16824
  worthy: v.worthy,
15941
- 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 } : {}
15942
16828
  });
15943
16829
  }
15944
16830
  return out.size > 0 ? out : null;
@@ -16009,7 +16895,7 @@ async function runTriagePass(candidates, opts) {
16009
16895
  }
16010
16896
  return { verdicts, summary };
16011
16897
  }
16012
- 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;
16013
16899
  var init_triage = __esm(async () => {
16014
16900
  init_which();
16015
16901
  init_session_listing();
@@ -16018,6 +16904,7 @@ var init_triage = __esm(async () => {
16018
16904
  init_conversation_analysis()
16019
16905
  ]);
16020
16906
  TRIAGE_TIMEOUT_MS = 2 * 60 * 1000;
16907
+ TASK_TYPES = ["build", "automate", "analyze", "write", "research", "admin", "debug"];
16021
16908
  });
16022
16909
 
16023
16910
  // src/reflect/conversation-analysis.ts
@@ -16182,6 +17069,48 @@ async function analyzeConversations(opts = {}) {
16182
17069
  releaseRunLock();
16183
17070
  }
16184
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
+ }
16185
17114
  async function analyzeConversationsLocked(opts, outcome, gates) {
16186
17115
  const { cadence, explicitSession, manual } = gates;
16187
17116
  if (!cadence.enabled && !manual && !explicitSession) {
@@ -16318,9 +17247,11 @@ async function analyzeConversationsLocked(opts, outcome, gates) {
16318
17247
  for (const [key, verdict] of pass.verdicts) {
16319
17248
  const entry = state.entries[key];
16320
17249
  if (entry) {
16321
- 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 } } } };
16322
17252
  }
16323
17253
  }
17254
+ recordPatternObservations(pass.verdicts, state, progress);
16324
17255
  }
16325
17256
  }
16326
17257
  const ranked = [];
@@ -16540,6 +17471,7 @@ var init_conversation_analysis = __esm(async () => {
16540
17471
  init_session_listing();
16541
17472
  init_insight_store();
16542
17473
  init_telemetry_outbox();
17474
+ init_pattern_store();
16543
17475
  init_run_log();
16544
17476
  init_insight_store();
16545
17477
  await __promiseAll([
@@ -19036,14 +19968,18 @@ init_session_summary();
19036
19968
  init_telemetry_outbox();
19037
19969
  init_insight_store();
19038
19970
  init_atomic_json();
19971
+ init_store();
19972
+ init_client();
19973
+ init_resolve();
19974
+ init_workspace_state();
19039
19975
  init_colors();
19040
19976
  await __promiseAll([
19041
19977
  init_conversation_registry(),
19042
19978
  init_conversation_analysis()
19043
19979
  ]);
19044
19980
  import { Command as Command15 } from "commander";
19045
- import { join as join42 } from "path";
19046
- import { homedir as homedir23 } from "os";
19981
+ import { join as join43 } from "path";
19982
+ import { homedir as homedir24 } from "os";
19047
19983
  var DEFAULT_LOOKBACK_DAYS = 30;
19048
19984
  function sinceFromDays(days) {
19049
19985
  if (days <= 0)
@@ -19056,6 +19992,46 @@ function parseDays(raw) {
19056
19992
  return DEFAULT_LOOKBACK_DAYS;
19057
19993
  return Math.floor(n);
19058
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
+ }
19059
20035
  function projectLabel(project) {
19060
20036
  const trimmed = project.replace(/[/\\]+$/, "");
19061
20037
  const idx = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
@@ -19112,11 +20088,21 @@ conversationsCommand.command("scan").description("Detect finished conversations
19112
20088
  const nowISO = new Date().toISOString();
19113
20089
  const summaryEvents = [];
19114
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
+ };
19115
20098
  let statsWritten = 0;
19116
20099
  for (const [key, entry] of Object.entries(scan.state.entries)) {
19117
20100
  if (!newlyPending.has(key) && entry.stats)
19118
20101
  continue;
19119
- const summary = buildSessionSummary(key, entry, digestForEntry(entry));
20102
+ const summary = buildSessionSummary(key, entry, digestForEntry(entry), {
20103
+ knownSkills,
20104
+ repoHash: repoHashFor(entry.project)
20105
+ });
19120
20106
  scan.state.entries[key] = { ...entry, stats: summary };
19121
20107
  statsWritten++;
19122
20108
  if (newlyPending.has(key))
@@ -19155,7 +20141,7 @@ conversationsCommand.command("scan").description("Detect finished conversations
19155
20141
  idleMinutes,
19156
20142
  conversations: [...listed, ...rescued].sort((a, b) => b.lastActivityAt.localeCompare(a.lastActivityAt))
19157
20143
  };
19158
- writeJsonAtomic(join42(homedir23(), ".runwork", "conversations.json"), snapshot);
20144
+ writeJsonAtomic(join43(homedir24(), ".runwork", "conversations.json"), snapshot);
19159
20145
  if (json) {
19160
20146
  jsonOut({ queued: scan.queued, reopened: scan.reopened, pruned: scan.pruned, pending, finished: finished.length });
19161
20147
  return;
@@ -19185,8 +20171,8 @@ init_client();
19185
20171
  init_resolve();
19186
20172
  import { Command as Command16 } from "commander";
19187
20173
  import { readFileSync as readFileSync37, existsSync as existsSync48 } from "fs";
19188
- import { join as join43 } from "path";
19189
- import { homedir as homedir24 } from "os";
20174
+ import { join as join44 } from "path";
20175
+ import { homedir as homedir25 } from "os";
19190
20176
 
19191
20177
  // ../../shared/agent-instructions/runwork-instructions.ts
19192
20178
  function formatList(items, max = 8) {
@@ -19327,17 +20313,22 @@ function toRunworkInventory(ctx) {
19327
20313
  mcpServerCount: ctx.mcpServerCount
19328
20314
  };
19329
20315
  }
19330
- function buildAppSkillDescription(appName, registries) {
20316
+ function buildAppSkillDescription(skill, registries) {
20317
+ const appName = skill.name;
20318
+ const fallback = `${appName} - Runwork workspace application`;
19331
20319
  if (!registries)
19332
- return `${appName} - Runwork workspace application`;
20320
+ return fallback;
20321
+ if (!skill.appId)
20322
+ return fallback;
20323
+ const appId = skill.appId;
19333
20324
  const parts = [];
19334
- const appEntities = registries.entities.filter((e) => e.appName === appName);
19335
- const appSchedules = registries.schedules.filter((s) => s.appName === appName);
19336
- const appWorkflows = registries.workflows.filter((w) => w.appName === appName);
19337
- const appAgents = registries.agents.filter((a) => a.appName === appName);
19338
- const appEndpoints = registries.endpoints.filter((e) => e.appName === appName);
19339
- const appFileStorages = registries.fileStorages.filter((f) => f.appName === appName);
19340
- const appComponents = registries.components.filter((c) => c.appName === appName);
20325
+ const appEntities = registries.entities.filter((e) => e.appId === appId);
20326
+ const appSchedules = registries.schedules.filter((s) => s.appId === appId);
20327
+ const appWorkflows = registries.workflows.filter((w) => w.appId === appId);
20328
+ const appAgents = registries.agents.filter((a) => a.appId === appId);
20329
+ const appEndpoints = registries.endpoints.filter((e) => e.appId === appId);
20330
+ const appFileStorages = registries.fileStorages.filter((f) => f.appId === appId);
20331
+ const appComponents = registries.components.filter((c) => c.appId === appId);
19341
20332
  if (appEntities.length > 0)
19342
20333
  parts.push(`entities (${appEntities.map((e) => e.entityName).join(", ")})`);
19343
20334
  if (appFileStorages.length > 0)
@@ -19353,7 +20344,7 @@ function buildAppSkillDescription(appName, registries) {
19353
20344
  if (appComponents.length > 0)
19354
20345
  parts.push(`components (${appComponents.map((c) => c.componentName).join(", ")})`);
19355
20346
  if (parts.length === 0)
19356
- return `${appName} - Runwork workspace application`;
20347
+ return fallback;
19357
20348
  return `${appName} - Runwork workspace app with ${parts.join(", ")}. Use this skill when you need data or capabilities from this app.`;
19358
20349
  }
19359
20350
  function generateIntroSkill(ctx) {
@@ -19661,8 +20652,8 @@ async function buildInstructionContext(client, workspace) {
19661
20652
  // src/commands/instructions.ts
19662
20653
  function readSetupExtras(workspaceId) {
19663
20654
  for (const path2 of [
19664
- join43(process.cwd(), ".runwork", "setup.json"),
19665
- join43(homedir24(), ".runwork", "setup.json")
20655
+ join44(process.cwd(), ".runwork", "setup.json"),
20656
+ join44(homedir25(), ".runwork", "setup.json")
19666
20657
  ]) {
19667
20658
  if (!existsSync48(path2))
19668
20659
  continue;
@@ -19877,7 +20868,12 @@ var listCommand3 = new Command17("list").description("List entities registered i
19877
20868
  try {
19878
20869
  let entities = await client.listEntities(workspaceId);
19879
20870
  if (opts.app) {
19880
- entities = entities.filter((e) => e.appId === opts.app || e.appName === opts.app);
20871
+ const filtered = filterRowsByApp(entities, opts.app, await client.listApps(workspaceId));
20872
+ if (filtered === null) {
20873
+ console.error(`App "${opts.app}" not found in this workspace.`);
20874
+ process.exit(1);
20875
+ }
20876
+ entities = filtered;
19881
20877
  }
19882
20878
  if (opts.preview) {
19883
20879
  entities = entities.filter((e) => e.deploymentMode === "preview");
@@ -20226,7 +21222,12 @@ var listCommand4 = new Command18("list").description("List workflows in a worksp
20226
21222
  try {
20227
21223
  let workflows = await client.listWorkflows(workspaceId);
20228
21224
  if (opts.app) {
20229
- workflows = workflows.filter((w) => w.appId === opts.app || w.appName === opts.app);
21225
+ const filtered = filterRowsByApp(workflows, opts.app, await client.listApps(workspaceId));
21226
+ if (filtered === null) {
21227
+ console.error(`App "${opts.app}" not found in this workspace.`);
21228
+ process.exit(1);
21229
+ }
21230
+ workflows = filtered;
20230
21231
  }
20231
21232
  if (useJson) {
20232
21233
  jsonOut({ workflows });
@@ -20585,7 +21586,10 @@ function truncate3(text2, max) {
20585
21586
  return first;
20586
21587
  return first.slice(0, max - 3) + "...";
20587
21588
  }
20588
- var listCommand6 = new Command20("list").description("List workspace public endpoints").option("--workspace <name-or-id>", "Workspace name or ID").option("--app <name>", "Filter by app name or ID").action(async (opts, command) => {
21589
+ function filterEndpointsByApp(endpoints, appRef, apps) {
21590
+ return filterRowsByApp(endpoints, appRef, apps);
21591
+ }
21592
+ var listCommand6 = new Command20("list").description("List workspace public endpoints").option("--workspace <name-or-id>", "Workspace name or ID").option("--app <slug>", "Filter by app slug, name, or ID").action(async (opts, command) => {
20589
21593
  const useJson = shouldOutputJson(command.optsWithGlobals().json);
20590
21594
  const credentials = requireAuth();
20591
21595
  const client = new ApiClient(credentials);
@@ -20593,7 +21597,12 @@ var listCommand6 = new Command20("list").description("List workspace public endp
20593
21597
  try {
20594
21598
  let endpoints = await client.listEndpoints(workspaceId);
20595
21599
  if (opts.app) {
20596
- endpoints = endpoints.filter((e) => e.appId === opts.app || e.appName === opts.app);
21600
+ const filtered = filterEndpointsByApp(endpoints, opts.app, await client.listApps(workspaceId));
21601
+ if (filtered === null) {
21602
+ console.error(`App "${opts.app}" not found in this workspace.`);
21603
+ process.exit(1);
21604
+ }
21605
+ endpoints = filtered;
20597
21606
  }
20598
21607
  if (useJson) {
20599
21608
  jsonOut({ endpoints });
@@ -20623,7 +21632,11 @@ Workspace: ${workspaceName || workspaceId}
20623
21632
  process.exit(1);
20624
21633
  }
20625
21634
  });
20626
- var callCommand2 = new Command20("call").description("Call a workspace endpoint directly").argument("<method>", "HTTP method (GET, POST, PUT, DELETE, etc.)").argument("<path>", "Endpoint path (e.g. /my-endpoint)").option("--workspace <name-or-id>", "Workspace name or ID").option("--app <name>", "App name or ID (narrows endpoint lookup)").option("--body <json>", "Request body as JSON string").option("--header <header>", 'Request header (format: "Key: Value", repeatable)', (val, prev) => [...prev, val], []).option("--query <string>", 'Query string to append to the URL (e.g. "foo=bar&baz=1")').option("--api-key <key>", "API key for Authorization: Bearer header").action(async (method, path4, opts, command) => {
21635
+ function methodsForPath(endpoints, path4) {
21636
+ const methods = endpoints.filter((e) => e.endpointPath === path4).map((e) => e.method.toUpperCase());
21637
+ return [...new Set(methods)].sort();
21638
+ }
21639
+ var callCommand2 = new Command20("call").description("Call a workspace endpoint directly").argument("<method>", "HTTP method (GET, POST, PUT, DELETE, etc.)").argument("<path>", "Endpoint path (e.g. /my-endpoint)").option("--workspace <name-or-id>", "Workspace name or ID").option("--app <slug>", "App slug, name, or ID (narrows endpoint lookup)").option("--body <json>", "Request body as JSON string").option("--header <header>", 'Request header (format: "Key: Value", repeatable)', (val, prev) => [...prev, val], []).option("--query <string>", 'Query string to append to the URL (e.g. "foo=bar&baz=1")').option("--api-key <key>", "API key for Authorization: Bearer header").action(async (method, path4, opts, command) => {
20627
21640
  const useJson = shouldOutputJson(command.optsWithGlobals().json);
20628
21641
  const credentials = requireAuth();
20629
21642
  const client = new ApiClient(credentials);
@@ -20631,13 +21644,23 @@ var callCommand2 = new Command20("call").description("Call a workspace endpoint
20631
21644
  try {
20632
21645
  let endpoints = await client.listEndpoints(workspaceId);
20633
21646
  if (opts.app) {
20634
- endpoints = endpoints.filter((e) => e.appId === opts.app || e.appName === opts.app);
21647
+ const filtered = filterEndpointsByApp(endpoints, opts.app, await client.listApps(workspaceId));
21648
+ if (filtered === null) {
21649
+ console.error(`App "${opts.app}" not found in this workspace.`);
21650
+ process.exit(1);
21651
+ }
21652
+ endpoints = filtered;
20635
21653
  }
20636
21654
  const targetMethod = method.toUpperCase();
20637
21655
  const endpoint = endpoints.find((e) => e.method.toUpperCase() === targetMethod && e.endpointPath === path4);
20638
21656
  if (!endpoint) {
20639
21657
  console.error(`Endpoint not found: ${targetMethod} ${path4}`);
20640
- if (endpoints.length > 0) {
21658
+ const otherMethods = methodsForPath(endpoints, path4);
21659
+ if (otherMethods.length > 0) {
21660
+ console.error(`
21661
+ ${path4} is registered, but not for ${targetMethod}. Try: ${otherMethods.join(", ")}`);
21662
+ console.error(` runwork endpoints call ${otherMethods[0]} ${path4}`);
21663
+ } else if (endpoints.length > 0) {
20641
21664
  console.error(`
20642
21665
  Available endpoints:`);
20643
21666
  for (const e of endpoints) {
@@ -20907,7 +21930,12 @@ var listCommand8 = new Command22("list").description("List components registered
20907
21930
  try {
20908
21931
  let components = await client.listComponents(workspaceId);
20909
21932
  if (opts.app) {
20910
- components = components.filter((c) => c.appId === opts.app || c.appName === opts.app);
21933
+ const filtered = filterRowsByApp(components, opts.app, await client.listApps(workspaceId));
21934
+ if (filtered === null) {
21935
+ console.error(`App "${opts.app}" not found in this workspace.`);
21936
+ process.exit(1);
21937
+ }
21938
+ components = filtered;
20911
21939
  }
20912
21940
  if (useJson) {
20913
21941
  jsonOut({ components });
@@ -21040,7 +22068,12 @@ var listCommand10 = new Command24("list").description("List agents in workspace"
21040
22068
  try {
21041
22069
  let agents = await client.listAgents(workspaceId);
21042
22070
  if (opts.app) {
21043
- agents = agents.filter((a) => a.appId === opts.app || a.appName === opts.app);
22071
+ const filtered = filterRowsByApp(agents, opts.app, await client.listApps(workspaceId));
22072
+ if (filtered === null) {
22073
+ console.error(`App "${opts.app}" not found in this workspace.`);
22074
+ process.exit(1);
22075
+ }
22076
+ agents = filtered;
21044
22077
  }
21045
22078
  if (useJson) {
21046
22079
  jsonOut({ agents });
@@ -21359,8 +22392,8 @@ init_resolve();
21359
22392
  init_prompt();
21360
22393
  await init_detect();
21361
22394
  import { Command as Command27 } from "commander";
21362
- import { join as join47 } from "path";
21363
- import { homedir as homedir26 } from "os";
22395
+ import { join as join48 } from "path";
22396
+ import { homedir as homedir27 } from "os";
21364
22397
 
21365
22398
  // src/commands/sync.ts
21366
22399
  init_store();
@@ -21372,8 +22405,8 @@ await __promiseAll([
21372
22405
  ]);
21373
22406
  import { Command as Command26 } from "commander";
21374
22407
  import { readFileSync as readFileSync40, existsSync as existsSync51 } from "fs";
21375
- import { join as join46 } from "path";
21376
- import { homedir as homedir25 } from "os";
22408
+ import { join as join47 } from "path";
22409
+ import { homedir as homedir26 } from "os";
21377
22410
 
21378
22411
  // src/commands/mcp-entries.ts
21379
22412
  init_types();
@@ -22272,13 +23305,13 @@ function readLocalSkills(state) {
22272
23305
  if (!baseDir)
22273
23306
  continue;
22274
23307
  for (const skillName of state.skills) {
22275
- const skillMdPath = join46(baseDir, skillName, "SKILL.md");
23308
+ const skillMdPath = join47(baseDir, skillName, "SKILL.md");
22276
23309
  if (existsSync51(skillMdPath)) {
22277
23310
  results.push({ name: skillName, content: readFileSync40(skillMdPath, "utf-8") });
22278
23311
  continue;
22279
23312
  }
22280
23313
  const filename = skillName.toLowerCase().replace(/[^a-z0-9]+/g, "-");
22281
- const flatPath = join46(baseDir, `${filename}.md`);
23314
+ const flatPath = join47(baseDir, `${filename}.md`);
22282
23315
  if (existsSync51(flatPath)) {
22283
23316
  results.push({ name: skillName, content: readFileSync40(flatPath, "utf-8") });
22284
23317
  }
@@ -22574,7 +23607,7 @@ This account is not a member of "${state.workspaceName || state.workspaceId}".`)
22574
23607
  name: s.name,
22575
23608
  filename: s.name.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
22576
23609
  content: s.content,
22577
- description: s.source === "app" ? buildAppSkillDescription(s.name, registries) || `${s.name} - Runwork workspace application` : `${s.name} - Runwork workspace skill`
23610
+ description: s.source === "app" ? buildAppSkillDescription(s, registries) || `${s.name} - Runwork workspace application` : `${s.name} - Runwork workspace skill`
22578
23611
  }));
22579
23612
  const allSkillFiles = [...builtInSkills, ...workspaceSkillFiles];
22580
23613
  const written = await adapter2.writeSkills(allSkillFiles, scope);
@@ -22760,7 +23793,7 @@ This account is not a member of "${state.workspaceName || state.workspaceId}".`)
22760
23793
  }
22761
23794
  for (const adapter2 of adapters) {
22762
23795
  if (adapter2 instanceof CodexAdapter) {
22763
- const runworkDir = join46(homedir25(), ".runwork");
23796
+ const runworkDir = join47(homedir26(), ".runwork");
22764
23797
  const result = adapter2.registerDesktopWorkspace(runworkDir, "Runwork");
22765
23798
  if (result === "written") {
22766
23799
  vlog(` [${adapter2.name}] Registered workspace in Codex desktop app`);
@@ -22902,8 +23935,8 @@ var syncCommand = new Command26("sync").description("Sync skills bidirectionally
22902
23935
  verbose: !!opts.verbose,
22903
23936
  redetect: !!opts.redetect
22904
23937
  };
22905
- const projectStatePath = join46(process.cwd(), ".runwork", "setup.json");
22906
- const userStatePath = join46(homedir25(), ".runwork", "setup.json");
23938
+ const projectStatePath = join47(process.cwd(), ".runwork", "setup.json");
23939
+ const userStatePath = join47(homedir26(), ".runwork", "setup.json");
22907
23940
  const projectState = loadSetupState(projectStatePath);
22908
23941
  const userState = loadSetupState(userStatePath);
22909
23942
  if (!projectState && !userState) {
@@ -22972,7 +24005,7 @@ function toSkillFilename(name) {
22972
24005
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
22973
24006
  }
22974
24007
  function loadSetupStateForScope(scope) {
22975
- 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");
22976
24009
  return readJsonOrNull(path4);
22977
24010
  }
22978
24011
  async function parkAndTeardownWorkspace(previous, scopes) {
@@ -23141,8 +24174,8 @@ Re-run without --dry-run to sync workspace data.`);
23141
24174
  }
23142
24175
  persistDefaultWorkspace(workspaceId, workspaceName);
23143
24176
  for (const s of scopes) {
23144
- const dir = s === "project" ? ".runwork" : join47(homedir26(), ".runwork");
23145
- writeJsonAtomic(join47(dir, "setup.json"), state);
24177
+ const dir = s === "project" ? ".runwork" : join48(homedir27(), ".runwork");
24178
+ writeJsonAtomic(join48(dir, "setup.json"), state);
23146
24179
  }
23147
24180
  if (restored)
23148
24181
  clearParkedState(workspaceId);
@@ -23150,7 +24183,7 @@ Re-run without --dry-run to sync workspace data.`);
23150
24183
  Syncing workspace data...
23151
24184
  `);
23152
24185
  for (const s of scopes) {
23153
- 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");
23154
24187
  await syncFromState(state, statePath2, credentials, {
23155
24188
  dryRun: false,
23156
24189
  pullOnly: true,
@@ -23171,11 +24204,11 @@ import { Command as Command28 } from "commander";
23171
24204
 
23172
24205
  // src/utils/setup-state.ts
23173
24206
  import { existsSync as existsSync52, readFileSync as readFileSync41 } from "fs";
23174
- import { join as join48 } from "path";
23175
- import { homedir as homedir27 } from "os";
24207
+ import { join as join49 } from "path";
24208
+ import { homedir as homedir28 } from "os";
23176
24209
  function loadSetupState2() {
23177
- const projectPath = join48(process.cwd(), ".runwork", "setup.json");
23178
- const userPath = join48(homedir27(), ".runwork", "setup.json");
24210
+ const projectPath = join49(process.cwd(), ".runwork", "setup.json");
24211
+ const userPath = join49(homedir28(), ".runwork", "setup.json");
23179
24212
  for (const p of [projectPath, userPath]) {
23180
24213
  if (existsSync52(p)) {
23181
24214
  try {
@@ -23239,8 +24272,8 @@ init_types();
23239
24272
  await init_detect();
23240
24273
  import { Command as Command29 } from "commander";
23241
24274
  import { existsSync as existsSync53, readFileSync as readFileSync42 } from "fs";
23242
- import { resolve as resolve3, join as join49 } from "path";
23243
- import { homedir as homedir28 } from "os";
24275
+ import { resolve as resolve3, join as join50 } from "path";
24276
+ import { homedir as homedir29 } from "os";
23244
24277
  function loadSetupState3(filePath) {
23245
24278
  if (!existsSync53(filePath))
23246
24279
  return null;
@@ -23261,8 +24294,8 @@ var buildPluginCommand = new Command29("build-plugin").description("Build an ins
23261
24294
  process.exit(1);
23262
24295
  }
23263
24296
  const credentials = requireAuth();
23264
- const projectStatePath = join49(process.cwd(), ".runwork", "setup.json");
23265
- const userStatePath = join49(homedir28(), ".runwork", "setup.json");
24297
+ const projectStatePath = join50(process.cwd(), ".runwork", "setup.json");
24298
+ const userStatePath = join50(homedir29(), ".runwork", "setup.json");
23266
24299
  const state = loadSetupState3(projectStatePath) ?? loadSetupState3(userStatePath);
23267
24300
  if (!state) {
23268
24301
  console.error("No setup state found. Run `runwork setup` first.");
@@ -23337,7 +24370,7 @@ var buildPluginCommand = new Command29("build-plugin").description("Build an ins
23337
24370
  name: s.name,
23338
24371
  filename: s.name.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
23339
24372
  content: s.content,
23340
- description: s.source === "app" ? buildAppSkillDescription(s.name, registries) || `${s.name} - Runwork workspace application` : `${s.name} - Runwork workspace skill`
24373
+ description: s.source === "app" ? buildAppSkillDescription(s, registries) || `${s.name} - Runwork workspace application` : `${s.name} - Runwork workspace skill`
23341
24374
  }))
23342
24375
  ];
23343
24376
  const teamInstructions = onboardingConfig?.config?.teamInstructions;
@@ -23355,8 +24388,8 @@ init_prompt();
23355
24388
  await init_detect();
23356
24389
  import { Command as Command30 } from "commander";
23357
24390
  import { existsSync as existsSync54, readFileSync as readFileSync43, rmSync as rmSync13, unlinkSync as unlinkSync8 } from "fs";
23358
- import { join as join50 } from "path";
23359
- import { homedir as homedir29 } from "os";
24391
+ import { join as join51 } from "path";
24392
+ import { homedir as homedir30 } from "os";
23360
24393
  function loadSetupState4(filePath) {
23361
24394
  if (!existsSync54(filePath))
23362
24395
  return null;
@@ -23367,8 +24400,8 @@ function loadSetupState4(filePath) {
23367
24400
  }
23368
24401
  }
23369
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) => {
23370
- const projectStatePath = join50(process.cwd(), ".runwork", "setup.json");
23371
- const userStatePath = join50(homedir29(), ".runwork", "setup.json");
24403
+ const projectStatePath = join51(process.cwd(), ".runwork", "setup.json");
24404
+ const userStatePath = join51(homedir30(), ".runwork", "setup.json");
23372
24405
  const projectState = loadSetupState4(projectStatePath);
23373
24406
  const userState = loadSetupState4(userStatePath);
23374
24407
  if (!projectState && !userState) {
@@ -23448,9 +24481,9 @@ This will remove all Runwork configuration from your local agents:
23448
24481
  }
23449
24482
  }
23450
24483
  }
23451
- const stateDir = label === "project" ? join50(process.cwd(), ".runwork") : join50(homedir29(), ".runwork");
24484
+ const stateDir = label === "project" ? join51(process.cwd(), ".runwork") : join51(homedir30(), ".runwork");
23452
24485
  if (opts.keepAuth && label === "user") {
23453
- const setupFile = join50(stateDir, "setup.json");
24486
+ const setupFile = join51(stateDir, "setup.json");
23454
24487
  if (existsSync54(setupFile)) {
23455
24488
  try {
23456
24489
  unlinkSync8(setupFile);
@@ -23701,8 +24734,8 @@ init_credentials();
23701
24734
  await init_detect();
23702
24735
  import { parse as parse2 } from "smol-toml";
23703
24736
  import { existsSync as existsSync55, readFileSync as readFileSync45 } from "fs";
23704
- import { join as join51, sep as sep4 } from "path";
23705
- 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";
23706
24739
  var BASE_URL2 = process.env.RUNWORK_DOWNLOAD_BASE_URL || "https://runwork.ai";
23707
24740
  var LATEST_JSON_URL2 = `${BASE_URL2}/cli/latest.json`;
23708
24741
  function detectPlatform() {
@@ -23730,7 +24763,7 @@ function buildContext() {
23730
24763
  const credentials = getCredentials();
23731
24764
  const client = credentials ? new ApiClient(credentials) : null;
23732
24765
  let config = null;
23733
- const configPath = join51(process.cwd(), ".runwork.json");
24766
+ const configPath = join52(process.cwd(), ".runwork.json");
23734
24767
  if (existsSync55(configPath)) {
23735
24768
  try {
23736
24769
  config = JSON.parse(readFileSync45(configPath, "utf-8"));
@@ -23834,9 +24867,9 @@ async function checkCliArtifactReachable() {
23834
24867
  }
23835
24868
  async function checkCliInstallLocation() {
23836
24869
  const isWindows2 = osPlatform2() === "win32";
23837
- const home = homedir30();
23838
- const canonicalDir = join51(home, ".runwork", "bin");
23839
- 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");
23840
24873
  const candidates = [process.execPath, process.argv[1] || ""].filter(Boolean);
23841
24874
  const runsFromCanonical = candidates.some((p) => normalizePath(p) === normalizePath(canonicalBinary));
23842
24875
  if (runsFromCanonical) {
@@ -23968,7 +25001,7 @@ async function checkGitCredentialHelper(ctx) {
23968
25001
  };
23969
25002
  }
23970
25003
  async function checkProjectConfig(ctx) {
23971
- const configPath = join51(ctx.cwd, ".runwork.json");
25004
+ const configPath = join52(ctx.cwd, ".runwork.json");
23972
25005
  if (!existsSync55(configPath)) {
23973
25006
  if (!ctx.credentials) {
23974
25007
  return { name: "project-config", status: "skip", message: "no project (not logged in)" };
@@ -24031,7 +25064,7 @@ async function checkGitRemote(ctx) {
24031
25064
  if (!ctx.config) {
24032
25065
  return { name: "git-remote", status: "skip", message: "skipped (no project)" };
24033
25066
  }
24034
- if (!existsSync55(join51(ctx.cwd, ".git"))) {
25067
+ if (!existsSync55(join52(ctx.cwd, ".git"))) {
24035
25068
  return {
24036
25069
  name: "git-remote",
24037
25070
  status: "fail",
@@ -24085,8 +25118,8 @@ async function checkDeployFreshness(ctx) {
24085
25118
  return { name: "deploy-freshness", status: "skip", message: "local HEAD unknown" };
24086
25119
  }
24087
25120
  function loadSetupState5() {
24088
- const projectPath = join51(process.cwd(), ".runwork", "setup.json");
24089
- const userPath = join51(homedir30(), ".runwork", "setup.json");
25121
+ const projectPath = join52(process.cwd(), ".runwork", "setup.json");
25122
+ const userPath = join52(homedir31(), ".runwork", "setup.json");
24090
25123
  for (const p of [projectPath, userPath]) {
24091
25124
  if (existsSync55(p)) {
24092
25125
  try {
@@ -24105,7 +25138,7 @@ async function checkCodexNetwork() {
24105
25138
  if (!state || !state.configuredAgents.includes("codex")) {
24106
25139
  return { name, status: "skip", message: "Codex not configured for Runwork" };
24107
25140
  }
24108
- const configPath = join51(homedir30(), ".codex", "config.toml");
25141
+ const configPath = join52(homedir31(), ".codex", "config.toml");
24109
25142
  if (!existsSync55(configPath)) {
24110
25143
  return { name, status: "skip", message: "no Codex config found" };
24111
25144
  }
@@ -24164,7 +25197,7 @@ async function checkCodexDesktopProject() {
24164
25197
  if (!usesCodex) {
24165
25198
  return { name, status: "skip", message: "Codex not configured for Runwork" };
24166
25199
  }
24167
- const statePath2 = join51(homedir30(), ".codex", ".codex-global-state.json");
25200
+ const statePath2 = join52(homedir31(), ".codex", ".codex-global-state.json");
24168
25201
  if (!existsSync55(statePath2)) {
24169
25202
  return { name, status: "skip", message: "Codex desktop app not detected" };
24170
25203
  }
@@ -24176,7 +25209,7 @@ async function checkCodexDesktopProject() {
24176
25209
  } catch {
24177
25210
  return { name, status: "warn", message: "could not read Codex desktop state" };
24178
25211
  }
24179
- const runworkDir = join51(homedir30(), ".runwork");
25212
+ const runworkDir = join52(homedir31(), ".runwork");
24180
25213
  if (savedRoots.includes(runworkDir)) {
24181
25214
  return { name, status: "pass", message: "Runwork project added to Codex desktop sidebar" };
24182
25215
  }
@@ -24254,7 +25287,7 @@ async function checkAgentSetup() {
24254
25287
  if (!skillsDir)
24255
25288
  continue;
24256
25289
  const missingSkills = state.skills.filter((name) => {
24257
- const skillPath = join51(skillsDir, name, "SKILL.md");
25290
+ const skillPath = join52(skillsDir, name, "SKILL.md");
24258
25291
  return !existsSync55(skillPath);
24259
25292
  });
24260
25293
  if (missingSkills.length > 0) {
@@ -24280,25 +25313,25 @@ async function checkAgentSetup() {
24280
25313
  };
24281
25314
  }
24282
25315
  function getMcpConfigPath2(slug, scope) {
24283
- const home = homedir30();
25316
+ const home = homedir31();
24284
25317
  switch (slug) {
24285
25318
  case "claude-code":
24286
- 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");
24287
25320
  case "cursor":
24288
- 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");
24289
25322
  case "windsurf":
24290
- 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");
24291
25324
  case "codex":
24292
25325
  case "codex-app":
24293
- return scope === "user" ? join51(home, ".codex", "config.toml") : null;
25326
+ return scope === "user" ? join52(home, ".codex", "config.toml") : null;
24294
25327
  case "gemini":
24295
- return scope === "user" ? join51(home, ".gemini", "settings.json") : null;
25328
+ return scope === "user" ? join52(home, ".gemini", "settings.json") : null;
24296
25329
  default:
24297
25330
  return null;
24298
25331
  }
24299
25332
  }
24300
25333
  async function checkWorkspacePointers() {
24301
- const userStatePath = join51(homedir30(), ".runwork", "setup.json");
25334
+ const userStatePath = join52(homedir31(), ".runwork", "setup.json");
24302
25335
  const state = existsSync55(userStatePath) ? (() => {
24303
25336
  try {
24304
25337
  return JSON.parse(readFileSync45(userStatePath, "utf-8"));
@@ -24331,15 +25364,15 @@ async function checkWorkspacePointers() {
24331
25364
  };
24332
25365
  }
24333
25366
  function getSkillsDir(slug, scope) {
24334
- const home = homedir30();
25367
+ const home = homedir31();
24335
25368
  switch (slug) {
24336
25369
  case "claude-code":
24337
- 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");
24338
25371
  case "codex":
24339
25372
  case "codex-app":
24340
- 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");
24341
25374
  case "gemini":
24342
- 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");
24343
25376
  default:
24344
25377
  return null;
24345
25378
  }
@@ -24393,7 +25426,7 @@ async function runAllChecks(options) {
24393
25426
  init_credentials();
24394
25427
  init_remote();
24395
25428
  import { existsSync as existsSync56 } from "fs";
24396
- import { join as join52 } from "path";
25429
+ import { join as join53 } from "path";
24397
25430
  async function applyDoctorFixes(ctx, failingNames) {
24398
25431
  const failing = new Set(failingNames);
24399
25432
  const outcomes = [];
@@ -24420,7 +25453,7 @@ async function applyDoctorFixes(ctx, failingNames) {
24420
25453
  applied: false,
24421
25454
  message: "no project config -- run inside an app directory"
24422
25455
  });
24423
- } else if (!existsSync56(join52(ctx.cwd, ".git"))) {
25456
+ } else if (!existsSync56(join53(ctx.cwd, ".git"))) {
24424
25457
  outcomes.push({
24425
25458
  name: "git-remote",
24426
25459
  applied: false,
@@ -24440,9 +25473,9 @@ async function applyDoctorFixes(ctx, failingNames) {
24440
25473
 
24441
25474
  // src/agents/runtime-detection.ts
24442
25475
  import { existsSync as existsSync57, readFileSync as readFileSync46, statSync as statSync10, readdirSync as readdirSync16 } from "fs";
24443
- import { homedir as homedir31 } from "os";
24444
- import { join as join53 } from "path";
24445
- 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");
24446
25479
  function detectCurrentAgent() {
24447
25480
  const claudeCodeSessionId = process.env.CLAUDE_CODE_SESSION_ID;
24448
25481
  if (claudeCodeSessionId) {
@@ -24505,7 +25538,7 @@ function detectCurrentAgent() {
24505
25538
  return null;
24506
25539
  }
24507
25540
  function readHookSessionInfo(sessionId) {
24508
- const path4 = join53(RUNWORK_SESSIONS_DIR, `${sessionId}.json`);
25541
+ const path4 = join54(RUNWORK_SESSIONS_DIR, `${sessionId}.json`);
24509
25542
  if (!existsSync57(path4))
24510
25543
  return null;
24511
25544
  try {
@@ -24517,7 +25550,7 @@ function readHookSessionInfo(sessionId) {
24517
25550
  }
24518
25551
  }
24519
25552
  function findClaudeCodeSessionFile(sessionId) {
24520
- const root = join53(homedir31(), ".claude", "projects");
25553
+ const root = join54(homedir32(), ".claude", "projects");
24521
25554
  if (!existsSync57(root))
24522
25555
  return null;
24523
25556
  let projectDirs;
@@ -24527,14 +25560,14 @@ function findClaudeCodeSessionFile(sessionId) {
24527
25560
  return null;
24528
25561
  }
24529
25562
  for (const dir of projectDirs) {
24530
- const candidate = join53(root, dir, `${sessionId}.jsonl`);
25563
+ const candidate = join54(root, dir, `${sessionId}.jsonl`);
24531
25564
  if (existsSync57(candidate))
24532
25565
  return candidate;
24533
25566
  }
24534
25567
  return null;
24535
25568
  }
24536
25569
  function findCodexRolloutFile(threadId) {
24537
- const root = join53(homedir31(), ".codex", "sessions");
25570
+ const root = join54(homedir32(), ".codex", "sessions");
24538
25571
  if (!existsSync57(root))
24539
25572
  return null;
24540
25573
  const stack = [root];
@@ -24547,7 +25580,7 @@ function findCodexRolloutFile(threadId) {
24547
25580
  continue;
24548
25581
  }
24549
25582
  for (const entry of entries) {
24550
- const full = join53(dir, entry);
25583
+ const full = join54(dir, entry);
24551
25584
  let s;
24552
25585
  try {
24553
25586
  s = statSync10(full);
@@ -24564,7 +25597,7 @@ function findCodexRolloutFile(threadId) {
24564
25597
  return null;
24565
25598
  }
24566
25599
  function findNewestClaudeCodeSession() {
24567
- const root = join53(homedir31(), ".claude", "projects");
25600
+ const root = join54(homedir32(), ".claude", "projects");
24568
25601
  if (!existsSync57(root))
24569
25602
  return null;
24570
25603
  let projectDirs;
@@ -24575,7 +25608,7 @@ function findNewestClaudeCodeSession() {
24575
25608
  }
24576
25609
  let best = null;
24577
25610
  for (const dir of projectDirs) {
24578
- const projectPath = join53(root, dir);
25611
+ const projectPath = join54(root, dir);
24579
25612
  let files;
24580
25613
  try {
24581
25614
  files = readdirSync16(projectPath);
@@ -24585,7 +25618,7 @@ function findNewestClaudeCodeSession() {
24585
25618
  for (const file of files) {
24586
25619
  if (!file.endsWith(".jsonl"))
24587
25620
  continue;
24588
- const full = join53(projectPath, file);
25621
+ const full = join54(projectPath, file);
24589
25622
  try {
24590
25623
  const s = statSync10(full);
24591
25624
  if (!best || s.mtimeMs > best.mtime) {
@@ -24603,7 +25636,7 @@ function findNewestClaudeCodeSession() {
24603
25636
  return best ? { sessionId: best.sessionId, path: best.path } : null;
24604
25637
  }
24605
25638
  function findNewestCodexRollout() {
24606
- const root = join53(homedir31(), ".codex", "sessions");
25639
+ const root = join54(homedir32(), ".codex", "sessions");
24607
25640
  if (!existsSync57(root))
24608
25641
  return null;
24609
25642
  const stack = [root];
@@ -24617,7 +25650,7 @@ function findNewestCodexRollout() {
24617
25650
  continue;
24618
25651
  }
24619
25652
  for (const entry of entries) {
24620
- const full = join53(dir, entry);
25653
+ const full = join54(dir, entry);
24621
25654
  let s;
24622
25655
  try {
24623
25656
  s = statSync10(full);
@@ -24850,9 +25883,9 @@ init_client();
24850
25883
  init_resolve();
24851
25884
  import { Command as Command35 } from "commander";
24852
25885
  import { readFileSync as readFileSync47, writeFileSync as writeFileSync31, existsSync as existsSync58, mkdtempSync as mkdtempSync4 } from "fs";
24853
- import { join as join54 } from "path";
25886
+ import { join as join55 } from "path";
24854
25887
  import { tmpdir as tmpdir4 } from "os";
24855
- import { createHash as createHash5 } from "crypto";
25888
+ import { createHash as createHash6 } from "crypto";
24856
25889
 
24857
25890
  // src/agents/utils/transcript-render.ts
24858
25891
  init_session_digest();
@@ -24977,8 +26010,8 @@ function resolveLocalSessionShare(opts, conversation) {
24977
26010
  console.error("Error: this conversation has no shareable content.");
24978
26011
  process.exit(1);
24979
26012
  }
24980
- const tempDir = mkdtempSync4(join54(tmpdir4(), "runwork-share-"));
24981
- const transcriptFile = join54(tempDir, "transcript.md");
26013
+ const tempDir = mkdtempSync4(join55(tmpdir4(), "runwork-share-"));
26014
+ const transcriptFile = join55(tempDir, "transcript.md");
24982
26015
  writeFileSync31(transcriptFile, markdown);
24983
26016
  opts.transcriptFile = transcriptFile;
24984
26017
  opts.nativeFile = opts.nativeFile ?? conversation.transcriptPath;
@@ -24996,7 +26029,7 @@ function nativeBundleFormatForAgent(slug) {
24996
26029
  return null;
24997
26030
  }
24998
26031
  function sha256Hex(content) {
24999
- return createHash5("sha256").update(content, "utf8").digest("hex");
26032
+ return createHash6("sha256").update(content, "utf8").digest("hex");
25000
26033
  }
25001
26034
  function utf8ByteLength(content) {
25002
26035
  return Buffer.byteLength(content, "utf8");
@@ -25188,8 +26221,8 @@ init_resolve();
25188
26221
  init_registry_data();
25189
26222
  import { Command as Command38 } from "commander";
25190
26223
  import { writeFileSync as writeFileSync32, mkdirSync as mkdirSync28, realpathSync } from "fs";
25191
- import { homedir as homedir32 } from "os";
25192
- import { join as join55 } from "path";
26224
+ import { homedir as homedir33 } from "os";
26225
+ import { join as join56 } from "path";
25193
26226
  import { spawn as spawn5 } from "child_process";
25194
26227
  init_registry();
25195
26228
  init_which();
@@ -25228,9 +26261,9 @@ function extractCodexUuid(rolloutContent) {
25228
26261
  }
25229
26262
  function placeClaudeJsonl(uuid, content, recipientCwd) {
25230
26263
  const encoded = encodeClaudeCodeCwd(recipientCwd);
25231
- const projectDir = join55(homedir32(), ".claude", "projects", encoded);
26264
+ const projectDir = join56(homedir33(), ".claude", "projects", encoded);
25232
26265
  mkdirSync28(projectDir, { recursive: true });
25233
- const placedAt = join55(projectDir, `${uuid}.jsonl`);
26266
+ const placedAt = join56(projectDir, `${uuid}.jsonl`);
25234
26267
  writeFileSync32(placedAt, content);
25235
26268
  return { placedAt, runFromCwd: recipientCwd };
25236
26269
  }
@@ -25239,10 +26272,10 @@ function placeCodexRollout(uuid, content) {
25239
26272
  const yyyy = String(now.getUTCFullYear());
25240
26273
  const mm = String(now.getUTCMonth() + 1).padStart(2, "0");
25241
26274
  const dd = String(now.getUTCDate()).padStart(2, "0");
25242
- const dir = join55(homedir32(), ".codex", "sessions", yyyy, mm, dd);
26275
+ const dir = join56(homedir33(), ".codex", "sessions", yyyy, mm, dd);
25243
26276
  mkdirSync28(dir, { recursive: true });
25244
26277
  const ts = now.toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "-");
25245
- const placedAt = join55(dir, `rollout-${ts}-${uuid}.jsonl`);
26278
+ const placedAt = join56(dir, `rollout-${ts}-${uuid}.jsonl`);
25246
26279
  writeFileSync32(placedAt, content);
25247
26280
  return { placedAt };
25248
26281
  }