runwork 0.11.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/agents/__tests__/intro-skill.test.js +32 -0
  2. package/dist/agents/claude-code.d.ts +21 -0
  3. package/dist/agents/claude-code.js +58 -2
  4. package/dist/agents/claude-desktop-plugin-tree.d.ts +15 -0
  5. package/dist/agents/claude-desktop-plugin-tree.js +37 -1
  6. package/dist/agents/conversation-skills.d.ts +18 -0
  7. package/dist/agents/conversation-skills.js +229 -0
  8. package/dist/agents/intro-skill.d.ts +9 -0
  9. package/dist/agents/intro-skill.js +41 -0
  10. package/dist/agents/registry-data.d.ts +35 -0
  11. package/dist/agents/registry-data.js +58 -1
  12. package/dist/agents/runtime-detection.d.ts +82 -0
  13. package/dist/agents/runtime-detection.js +271 -0
  14. package/dist/agents/session-start-hook.d.ts +37 -0
  15. package/dist/agents/session-start-hook.js +159 -0
  16. package/dist/agents/types.d.ts +12 -0
  17. package/dist/api/client.d.ts +47 -0
  18. package/dist/api/client.js +32 -0
  19. package/dist/commands/__tests__/setup-persona.test.d.ts +1 -0
  20. package/dist/commands/__tests__/setup-persona.test.js +31 -0
  21. package/dist/commands/doctor.js +89 -2
  22. package/dist/commands/inbox.d.ts +11 -0
  23. package/dist/commands/inbox.js +60 -0
  24. package/dist/commands/info.d.ts +1 -1
  25. package/dist/commands/info.js +7 -2
  26. package/dist/commands/resume.d.ts +2 -0
  27. package/dist/commands/resume.js +265 -0
  28. package/dist/commands/save-convo.d.ts +8 -0
  29. package/dist/commands/save-convo.js +20 -0
  30. package/dist/commands/setup.d.ts +7 -0
  31. package/dist/commands/setup.js +22 -0
  32. package/dist/commands/share-convo.d.ts +24 -0
  33. package/dist/commands/share-convo.js +167 -0
  34. package/dist/commands/sync.js +96 -10
  35. package/dist/generated/bundled-types.js +33 -33
  36. package/dist/generated/version.d.ts +1 -1
  37. package/dist/generated/version.js +1 -1
  38. package/dist/index.js +8 -0
  39. package/dist/types.d.ts +13 -0
  40. package/dist/utils/app-info.d.ts +4 -0
  41. package/dist/utils/app-info.js +17 -0
  42. package/package.json +1 -1
@@ -158,4 +158,36 @@ describe('generateInstructionHint', () => {
158
158
  expect(hint).toContain('2 apps');
159
159
  expect(hint).toContain('3 skills');
160
160
  });
161
+ it('omits the persona block when no persona is provided', () => {
162
+ const hint = generateInstructionHint(baseCtx);
163
+ expect(hint).not.toContain('Communicating with this user');
164
+ });
165
+ it('omits the persona block for engineers (level 3)', () => {
166
+ const hint = generateInstructionHint({ ...baseCtx, persona: { level: 3, label: 'engineer' } });
167
+ expect(hint).not.toContain('Communicating with this user');
168
+ });
169
+ it('adds a novice persona block (level 1) telling agents to avoid dev tooling', () => {
170
+ const hint = generateInstructionHint({ ...baseCtx, persona: { level: 1, label: 'novice' } });
171
+ expect(hint).toContain('Communicating with this user');
172
+ expect(hint).toContain('not a software developer');
173
+ expect(hint).toContain('npm, bun, node');
174
+ // The persona block must stay inside the replaceable runwork markers.
175
+ const start = hint.indexOf('<!-- runwork:start -->');
176
+ const end = hint.indexOf('<!-- runwork:end -->');
177
+ expect(hint.indexOf('Communicating with this user')).toBeGreaterThan(start);
178
+ expect(hint.indexOf('Communicating with this user')).toBeLessThan(end);
179
+ });
180
+ it('adds a curious persona block (level 2)', () => {
181
+ const hint = generateInstructionHint({ ...baseCtx, persona: { level: 2, label: 'curious' } });
182
+ expect(hint).toContain('Communicating with this user');
183
+ expect(hint).toContain('some familiarity with AI tools');
184
+ });
185
+ });
186
+ describe('generateIntroSkill — environment variables', () => {
187
+ it('documents that apps have no custom env var system', () => {
188
+ const skill = generateIntroSkill(makeContext());
189
+ expect(skill.content).toContain('Environment variables and secrets');
190
+ expect(skill.content).toContain('do NOT have a custom environment variable');
191
+ expect(skill.content).toContain('.env');
192
+ });
161
193
  });
@@ -7,7 +7,28 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
7
7
  supportsMcpScope(_scope: 'project' | 'user'): boolean;
8
8
  supportsSkills(): boolean;
9
9
  writeMcpServers(servers: McpServerEntry[], scope: 'project' | 'user'): Promise<void>;
10
+ /**
11
+ * Install the Runwork SessionStart hook into the plugin tree (user scope).
12
+ * The hook writes ~/.runwork/sessions/<sessionId>.json with the active
13
+ * conversation's transcript path + metadata, used by `runwork share-convo`
14
+ * for high-fidelity capture and by `runwork resume` for placement.
15
+ *
16
+ * Called as part of user-scope writeSkills() to keep sync flow consistent.
17
+ * Emits a one-line confirmation matching the format other adapter steps
18
+ * use (` [Claude Code] Installed ... (user)`) so `runwork sync` output
19
+ * shows whether the hook actually landed.
20
+ */
21
+ private installSessionStartHook;
10
22
  writeSkills(skills: SkillFile[], scope: 'project' | 'user'): Promise<void>;
23
+ /**
24
+ * Install the runwork SessionStart hook for Claude Code (user scope only).
25
+ * Called once per sync by the main sync loop AFTER writeSkills has
26
+ * populated the plugin directories. Kept separate from writeSkills because
27
+ * writeSkills is also invoked per-skill by the diff executor during
28
+ * push/pull, and the hook install + log line must not repeat for every
29
+ * skill change.
30
+ */
31
+ writeBuiltInHooks(scope: 'project' | 'user'): Promise<void>;
11
32
  writeInstructionHint(hint: string, scope: 'project' | 'user'): Promise<void>;
12
33
  writeTeamInstructions(instructions: string, scope: 'project' | 'user'): Promise<void>;
13
34
  writeAgentConfig(config: AgentConfigOverride, scope: 'project' | 'user'): Promise<void>;
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'fs';
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { homedir, platform } from 'os';
4
4
  import { execFileSync } from '../utils/subprocess.js';
@@ -6,6 +6,7 @@ import { whichBinary } from '../utils/which.js';
6
6
  import { buildSkillMd } from './types.js';
7
7
  import { mergeJsonMcpServers, readJsonConfig, writeJsonConfig, removeRunworkMcpServers } from './utils/json-config.js';
8
8
  import { writeHintToFile, writeTeamInstructionsToFile, removeHintFromFile, removeTeamInstructionsFromFile } from './utils/instruction-hint.js';
9
+ import { SESSION_START_HOOK_SCRIPT } from './session-start-hook.js';
9
10
  const PLUGIN_NAME = 'runwork';
10
11
  const PLUGIN_VERSION = '1.0.0';
11
12
  function getPluginJson() {
@@ -96,6 +97,37 @@ export class ClaudeCodeAdapter {
96
97
  writeJsonConfig(marketplaceMcpPath, pluginEntries);
97
98
  }
98
99
  }
100
+ /**
101
+ * Install the Runwork SessionStart hook into the plugin tree (user scope).
102
+ * The hook writes ~/.runwork/sessions/<sessionId>.json with the active
103
+ * conversation's transcript path + metadata, used by `runwork share-convo`
104
+ * for high-fidelity capture and by `runwork resume` for placement.
105
+ *
106
+ * Called as part of user-scope writeSkills() to keep sync flow consistent.
107
+ * Emits a one-line confirmation matching the format other adapter steps
108
+ * use (` [Claude Code] Installed ... (user)`) so `runwork sync` output
109
+ * shows whether the hook actually landed.
110
+ */
111
+ installSessionStartHook(pluginDir, label) {
112
+ const hooksDir = join(pluginDir, 'hooks');
113
+ mkdirSync(hooksDir, { recursive: true });
114
+ const scriptPath = join(hooksDir, 'on-session-start.sh');
115
+ writeFileSync(scriptPath, SESSION_START_HOOK_SCRIPT);
116
+ try {
117
+ // Best-effort executable bit; chmod is a no-op on Windows.
118
+ chmodSync(scriptPath, 0o755);
119
+ }
120
+ catch {
121
+ /* not fatal */
122
+ }
123
+ const hooksManifest = {
124
+ SessionStart: [
125
+ { command: '${CLAUDE_PLUGIN_ROOT}/hooks/on-session-start.sh' },
126
+ ],
127
+ };
128
+ writeFileSync(join(hooksDir, 'hooks.json'), JSON.stringify(hooksManifest, null, 2));
129
+ console.log(` [Claude Code] Installed SessionStart hook (${label}) -> ${scriptPath}`);
130
+ }
99
131
  async writeSkills(skills, scope) {
100
132
  // Always write to the standard skills directory (guaranteed to work)
101
133
  const baseDir = scope === 'project'
@@ -136,6 +168,26 @@ export class ClaudeCodeAdapter {
136
168
  this.registerPlugin(pluginDir);
137
169
  }
138
170
  }
171
+ /**
172
+ * Install the runwork SessionStart hook for Claude Code (user scope only).
173
+ * Called once per sync by the main sync loop AFTER writeSkills has
174
+ * populated the plugin directories. Kept separate from writeSkills because
175
+ * writeSkills is also invoked per-skill by the diff executor during
176
+ * push/pull, and the hook install + log line must not repeat for every
177
+ * skill change.
178
+ */
179
+ async writeBuiltInHooks(scope) {
180
+ if (scope !== 'user')
181
+ return;
182
+ const pluginDir = this.getPluginDir();
183
+ const marketDir = this.getMarketplaceDir();
184
+ if (existsSync(pluginDir)) {
185
+ this.installSessionStartHook(pluginDir, 'plugin cache');
186
+ }
187
+ if (existsSync(marketDir)) {
188
+ this.installSessionStartHook(marketDir, 'marketplace');
189
+ }
190
+ }
139
191
  async writeInstructionHint(hint, scope) {
140
192
  const filePath = scope === 'project'
141
193
  ? join(process.cwd(), '.claude', 'CLAUDE.md')
@@ -646,7 +698,9 @@ export class ClaudeCodeAdapter {
646
698
  installed.version = 2;
647
699
  if (!installed.plugins)
648
700
  installed.plugins = {};
649
- installed.plugins[`${PLUGIN_NAME}@runwork`] = [{
701
+ const pluginKey = `${PLUGIN_NAME}@runwork`;
702
+ const wasRegistered = !!installed.plugins[pluginKey];
703
+ installed.plugins[pluginKey] = [{
650
704
  scope: 'user',
651
705
  installPath: pluginDir,
652
706
  version: PLUGIN_VERSION,
@@ -668,6 +722,8 @@ export class ClaudeCodeAdapter {
668
722
  lastUpdated: new Date().toISOString(),
669
723
  };
670
724
  writeJsonConfig(marketplacesPath, marketplaces);
725
+ console.log(` [Claude Code] ${wasRegistered ? 'Refreshed' : 'Registered'} plugin ` +
726
+ `${pluginKey} v${PLUGIN_VERSION} -> ${installedPath}`);
671
727
  // Enable the plugin in settings.json
672
728
  const settingsPath = join(homedir(), '.claude', 'settings.json');
673
729
  const settings = readJsonConfig(settingsPath);
@@ -32,6 +32,19 @@ export declare function writePluginTeamInstructions(destDir: string, instruction
32
32
  * Email" instead of kebab-cased "write-cold-email").
33
33
  */
34
34
  export declare function writePluginSkills(destDir: string, skills: SkillFile[]): void;
35
+ /**
36
+ * Write the SessionStart hook script + hooks.json into <destDir>.
37
+ *
38
+ * Layout:
39
+ * <destDir>/hooks/on-session-start.sh (executable bash script)
40
+ * <destDir>/hooks/hooks.json (Claude hooks manifest)
41
+ *
42
+ * When the Cowork plugin is installed and the user opens a session, this
43
+ * hook writes ~/.runwork/sessions/<sessionId>.json with the authoritative
44
+ * transcript path + metadata, used by `runwork share-convo` and
45
+ * `runwork resume` for high-fidelity capture / placement.
46
+ */
47
+ export declare function writePluginSessionStartHook(destDir: string): void;
35
48
  /**
36
49
  * Convenience wrapper that writes the entire plugin tree in one shot.
37
50
  * Used by the build-plugin command to materialise a zip-ready directory.
@@ -40,5 +53,7 @@ export declare function writePluginSkills(destDir: string, skills: SkillFile[]):
40
53
  * <destDir>/.claude-plugin/plugin.json
41
54
  * <destDir>/.mcp.json
42
55
  * <destDir>/skills/<filename>/SKILL.md
56
+ * <destDir>/hooks/on-session-start.sh
57
+ * <destDir>/hooks/hooks.json
43
58
  */
44
59
  export declare function writePluginTree(destDir: string, input: PluginTreeInput): void;
@@ -1,6 +1,7 @@
1
- import { mkdirSync, rmSync, existsSync, writeFileSync } from 'fs';
1
+ import { chmodSync, mkdirSync, rmSync, existsSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { appendTokenToUrl, buildSkillMd } from './types.js';
4
+ import { SESSION_START_HOOK_SCRIPT } from './session-start-hook.js';
4
5
  /**
5
6
  * Write <destDir>/.claude-plugin/plugin.json.
6
7
  */
@@ -70,6 +71,38 @@ export function writePluginSkills(destDir, skills) {
70
71
  writeFileSync(join(skillDir, 'SKILL.md'), buildSkillMd(skill));
71
72
  }
72
73
  }
74
+ /**
75
+ * Write the SessionStart hook script + hooks.json into <destDir>.
76
+ *
77
+ * Layout:
78
+ * <destDir>/hooks/on-session-start.sh (executable bash script)
79
+ * <destDir>/hooks/hooks.json (Claude hooks manifest)
80
+ *
81
+ * When the Cowork plugin is installed and the user opens a session, this
82
+ * hook writes ~/.runwork/sessions/<sessionId>.json with the authoritative
83
+ * transcript path + metadata, used by `runwork share-convo` and
84
+ * `runwork resume` for high-fidelity capture / placement.
85
+ */
86
+ export function writePluginSessionStartHook(destDir) {
87
+ const hooksDir = join(destDir, 'hooks');
88
+ mkdirSync(hooksDir, { recursive: true });
89
+ const scriptPath = join(hooksDir, 'on-session-start.sh');
90
+ writeFileSync(scriptPath, SESSION_START_HOOK_SCRIPT);
91
+ try {
92
+ chmodSync(scriptPath, 0o755);
93
+ }
94
+ catch {
95
+ // chmod may fail on Windows; the script still ships and works wherever
96
+ // bash can execute it.
97
+ }
98
+ const hooksManifest = {
99
+ SessionStart: [
100
+ { command: '${CLAUDE_PLUGIN_ROOT}/hooks/on-session-start.sh' },
101
+ ],
102
+ };
103
+ writeFileSync(join(hooksDir, 'hooks.json'), JSON.stringify(hooksManifest, null, 2));
104
+ console.log(` [Claude Desktop (Cowork)] Bundled SessionStart hook -> ${scriptPath}`);
105
+ }
73
106
  /**
74
107
  * Convenience wrapper that writes the entire plugin tree in one shot.
75
108
  * Used by the build-plugin command to materialise a zip-ready directory.
@@ -78,6 +111,8 @@ export function writePluginSkills(destDir, skills) {
78
111
  * <destDir>/.claude-plugin/plugin.json
79
112
  * <destDir>/.mcp.json
80
113
  * <destDir>/skills/<filename>/SKILL.md
114
+ * <destDir>/hooks/on-session-start.sh
115
+ * <destDir>/hooks/hooks.json
81
116
  */
82
117
  export function writePluginTree(destDir, input) {
83
118
  mkdirSync(destDir, { recursive: true });
@@ -89,4 +124,5 @@ export function writePluginTree(destDir, input) {
89
124
  });
90
125
  writePluginMcpConfig(destDir, input.mcpServers);
91
126
  writePluginSkills(destDir, input.skills);
127
+ writePluginSessionStartHook(destDir);
92
128
  }
@@ -0,0 +1,18 @@
1
+ import type { SkillFile } from './types.js';
2
+ /**
3
+ * Names of every skill the CLI ships as a built-in (written to user machines
4
+ * on each sync from code, never pulled from or pushed to the workspace).
5
+ *
6
+ * Sync uses this list to:
7
+ * 1. Skip these names when computing the local-vs-remote diff so they
8
+ * never appear in pull/push/conflict plans.
9
+ * 2. Warn the user (and offer cleanup) if a workspace skill with the same
10
+ * name exists -- it would be shadowed by the built-in locally and is
11
+ * probably a stale duplicate.
12
+ *
13
+ * Keep this list in sync with: intro-skill.ts (`runwork`) and the two
14
+ * `buildXConversationSkill` functions below.
15
+ */
16
+ export declare const BUILT_IN_SKILL_NAMES: ReadonlyArray<string>;
17
+ export declare function buildShareConversationSkill(): SkillFile;
18
+ export declare function buildSaveConversationSkill(): SkillFile;
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Names of every skill the CLI ships as a built-in (written to user machines
3
+ * on each sync from code, never pulled from or pushed to the workspace).
4
+ *
5
+ * Sync uses this list to:
6
+ * 1. Skip these names when computing the local-vs-remote diff so they
7
+ * never appear in pull/push/conflict plans.
8
+ * 2. Warn the user (and offer cleanup) if a workspace skill with the same
9
+ * name exists -- it would be shadowed by the built-in locally and is
10
+ * probably a stale duplicate.
11
+ *
12
+ * Keep this list in sync with: intro-skill.ts (`runwork`) and the two
13
+ * `buildXConversationSkill` functions below.
14
+ */
15
+ export const BUILT_IN_SKILL_NAMES = [
16
+ 'runwork',
17
+ 'share-conversation',
18
+ 'save-conversation',
19
+ ];
20
+ /**
21
+ * Built-in skills shipped by the runwork CLI on every sync. These are
22
+ * fundamental capabilities every user should have regardless of which
23
+ * workspace they belong to, so they live in the CLI codebase (like
24
+ * the dynamic `runwork` intro skill in ./intro-skill.ts) rather than
25
+ * as workspace-managed skills uploaded via the `save_skill` MCP tool.
26
+ *
27
+ * share-conversation: hand off the current AI conversation to a teammate
28
+ * save-conversation: personal checkpoint of the current AI conversation
29
+ *
30
+ * Both are paired with the CLI commands `runwork share-convo` and
31
+ * `runwork save-convo`. The skill body teaches the host LLM how to compose
32
+ * the verbatim transcript + metadata and invoke the CLI.
33
+ */
34
+ const SHARE_CONVERSATION_CONTENT = `# Share Conversation Skill
35
+
36
+ This skill hands off the user's CURRENT AI conversation (the live chat you and the user are in) to a teammate so they can resume it natively in their own AI agent.
37
+
38
+ ## When to trigger
39
+
40
+ The user said something like:
41
+ - "share this convo with X"
42
+ - "share this conversation with X@example.com"
43
+ - "send this to Mehmet"
44
+ - "hand off this thread to Alice"
45
+ - "forward this conversation to user@motaword.com"
46
+
47
+ X may be a workspace member name, an email, or a list. ASK if ambiguous.
48
+
49
+ ## What you must do
50
+
51
+ **Step 1: Confirm the recipients.** Read the user's message, extract the email(s) or name(s). Confirm them back to the user before sending. Multiple recipients are allowed (comma-separated or multiple --to flags).
52
+
53
+ **Step 2: Compose a VERBATIM transcript of this conversation.** Write a complete markdown transcription of everything that has happened in this chat session so far. CRITICAL RULES:
54
+
55
+ - Transcribe EVERY message: user, assistant, tool calls, tool results.
56
+ - DO NOT summarize. DO NOT abbreviate. DO NOT skip anything you think is unimportant.
57
+ - Preserve the actual content of every assistant message, every Bash command, every Read result, every Edit, every Web search, every reasoning block you can recall, in chronological order.
58
+ - Use this structure:
59
+
60
+ \`\`\`markdown
61
+ # Conversation: <one-line title>
62
+
63
+ ## Conversation
64
+
65
+ ### Turn 1 - User
66
+ <user's exact message>
67
+
68
+ ### Turn 2 - Assistant
69
+ <your exact response text>
70
+
71
+ #### Tool call: Read
72
+ **Input**: { "file_path": "..." }
73
+ **Result**: <full result, truncated only if absolutely necessary>
74
+
75
+ ### Turn 3 - User
76
+ ...
77
+ \`\`\`
78
+
79
+ - If you literally cannot remember every single tool result word-for-word (e.g., very long files), include as much as you have in context. Note explicitly when content is partial: \`[truncated for brevity; original was N lines]\`.
80
+ - The goal is full fidelity. If the context is too long, FAIL LOUDLY by telling the user "the conversation is too long to transcribe completely; please confirm you want me to proceed with a best-effort transcription" rather than silently dropping content.
81
+
82
+ Write the transcript to a temp file: \`/tmp/runwork-share-transcript-<random>.md\`.
83
+
84
+ **Step 3: Generate metadata.** Compose a JSON object with these fields based on this conversation's actual content:
85
+
86
+ \`\`\`json
87
+ {
88
+ "workMode": "coding | research | writing | planning | ops | communication | general",
89
+ "topicKeywords": ["3-5 short tags"],
90
+ "lastUserIntent": "one sentence describing what the user was trying to do at handoff time",
91
+ "primaryArtifacts": ["files / URLs / docs touched during the conversation"],
92
+ "openQuestions": ["unresolved questions or decisions at handoff time"],
93
+ "suggestedNextStep": "one sentence about what the recipient should do next",
94
+ "language": "programming or natural language used"
95
+ }
96
+ \`\`\`
97
+
98
+ Write this to \`/tmp/runwork-share-metadata-<random>.json\`.
99
+
100
+ **Step 4: Invoke the CLI.** Run:
101
+
102
+ \`\`\`bash
103
+ runwork share-convo \\
104
+ --to <email1> \\
105
+ --to <email2> \\
106
+ --transcript-file /tmp/runwork-share-transcript-<random>.md \\
107
+ --metadata-file /tmp/runwork-share-metadata-<random>.json \\
108
+ --title "<short title derived from the conversation>"
109
+ \`\`\`
110
+
111
+ The CLI auto-detects your host agent (Claude Code, Codex, etc.) and locates the native session file via \`$CLAUDE_CODE_SESSION_ID\` / \`$CODEX_THREAD_ID\` env vars. You typically don't need to pass \`--source-agent\` or \`--native-file\`.
112
+
113
+ If the user provided a personal note for the recipient, also pass \`--note "<message>"\`.
114
+
115
+ **Step 5: Tell the user.** Report the share ID and inbox URL returned by the CLI. Mention any non-workspace recipients that were invited.
116
+
117
+ ## Fallback: when the CLI is not available
118
+
119
+ If \`runwork\` is not on PATH (rare; the CLI ships with workspace onboarding), use the Runwork MCP \`share_conversation\` tool instead. Pass the transcript content inline (lossier - no native bundle - but always works).
120
+
121
+ ## Safety
122
+
123
+ - Never include credentials, API keys, or secrets in the transcript if you can identify them. Replace with \`[REDACTED]\`.
124
+ - If the conversation contained sensitive customer data, confirm with the user before sharing.
125
+ - Workspace permissions: non-member recipients trigger an invite, which requires the sender to have the \`members.invite\` permission (owner or admin). The CLI will error out clearly if not allowed.
126
+
127
+ ## What this skill is NOT for
128
+
129
+ - Sharing files, apps, links, or screenshots. Use \`share-app\` or copy-paste.
130
+ - Continuing a paused conversation yourself. Use \`runwork resume <id>\` after the recipient runs it.
131
+ - Sharing a different conversation that's not the one you're currently in. The transcript is built from your live context.
132
+ `;
133
+ const SAVE_CONVERSATION_CONTENT = `# Save Conversation Skill
134
+
135
+ This skill creates a personal checkpoint of the user's CURRENT AI conversation. The user is the sole recipient; no notifications are sent to anyone else. They can resume it later in any of their AI agents on any of their machines.
136
+
137
+ ## When to trigger
138
+
139
+ The user said something like:
140
+ - "save this convo"
141
+ - "checkpoint this"
142
+ - "bookmark this conversation"
143
+ - "save this for later"
144
+ - "save this for tomorrow"
145
+ - "stash this so I can pick it up at home"
146
+
147
+ If the user mentions ANOTHER PERSON or an email, use the \`share-conversation\` skill instead, not this one.
148
+
149
+ ## What you must do
150
+
151
+ **Step 1: Confirm the intent.** A simple confirmation is enough: "Saving this conversation as a personal checkpoint."
152
+
153
+ **Step 2: Compose a VERBATIM transcript of this conversation.** Same rules as the share-conversation skill - faithful, complete, no summarization. Write to \`/tmp/runwork-save-transcript-<random>.md\`.
154
+
155
+ \`\`\`markdown
156
+ # Conversation: <one-line title>
157
+
158
+ ## Conversation
159
+
160
+ ### Turn 1 - User
161
+ <exact user message>
162
+
163
+ ### Turn 2 - Assistant
164
+ <exact assistant response>
165
+
166
+ #### Tool call: <name>
167
+ **Input**: ...
168
+ **Result**: ...
169
+ \`\`\`
170
+
171
+ **Step 3: Generate metadata.** Same shape as share-conversation:
172
+
173
+ \`\`\`json
174
+ {
175
+ "workMode": "coding | research | writing | planning | ops | communication | general",
176
+ "topicKeywords": ["3-5 tags"],
177
+ "lastUserIntent": "what the user was trying to do at checkpoint time",
178
+ "primaryArtifacts": ["files / URLs touched"],
179
+ "openQuestions": ["unresolved items"],
180
+ "suggestedNextStep": "what to do when picking back up",
181
+ "language": "programming or natural language"
182
+ }
183
+ \`\`\`
184
+
185
+ Write to \`/tmp/runwork-save-metadata-<random>.json\`.
186
+
187
+ **Step 4: Invoke the CLI.**
188
+
189
+ \`\`\`bash
190
+ runwork save-convo \\
191
+ --transcript-file /tmp/runwork-save-transcript-<random>.md \\
192
+ --metadata-file /tmp/runwork-save-metadata-<random>.json \\
193
+ --title "<short title>"
194
+ \`\`\`
195
+
196
+ If the user provided a personal note ("save this with note: pick up after lunch"), pass \`--note "<message>"\`.
197
+
198
+ **Step 5: Tell the user.** Report the share ID. Mention they can resume from any machine with:
199
+
200
+ \`\`\`
201
+ runwork resume <share-id>
202
+ \`\`\`
203
+
204
+ ## Fallback: when the CLI is not available
205
+
206
+ Use the Runwork MCP \`save_conversation\` tool with the transcript inline.
207
+
208
+ ## What this skill is NOT for
209
+
210
+ - Sharing with another person. Use \`share-conversation\`.
211
+ - Saving files or code. Use the file system or git.
212
+ - Note-taking. Use a notes app or write to disk directly.
213
+ `;
214
+ export function buildShareConversationSkill() {
215
+ return {
216
+ name: 'share-conversation',
217
+ filename: 'share-conversation',
218
+ content: SHARE_CONVERSATION_CONTENT,
219
+ description: 'TRIGGERS when the user asks to share, send, hand off, or forward the CURRENT AI conversation (the live chat you are in right now) to a teammate by name or email. Examples: "share this convo with oytun@motaword.com", "send this conversation to Mehmet", "hand off this thread to Alice". Use ONLY when the user wants the OTHER PERSON to be able to continue this exact conversation in their own AI agent. Do NOT trigger for sharing files, apps, links, or unrelated content. For personal checkpoint (sharing to yourself), use the `save-conversation` skill instead.',
220
+ };
221
+ }
222
+ export function buildSaveConversationSkill() {
223
+ return {
224
+ name: 'save-conversation',
225
+ filename: 'save-conversation',
226
+ content: SAVE_CONVERSATION_CONTENT,
227
+ description: 'TRIGGERS when the user wants to save, checkpoint, bookmark, or stash the CURRENT AI conversation for themselves (not for a teammate). Examples: "save this convo", "checkpoint this", "bookmark this conversation", "save this for later", "save this so I can pick it up tomorrow", "save this for my other machine". Use ONLY when the user is the sole intended recipient. For sharing with a teammate (different person), use the `share-conversation` skill instead.',
228
+ };
229
+ }
@@ -22,6 +22,15 @@ export interface InstructionHintContext {
22
22
  appCount: number;
23
23
  skillCount: number;
24
24
  mcpServerCount: number;
25
+ /**
26
+ * Technical-level classification from desktop onboarding. When present,
27
+ * a persona-specific communication block is added to the instruction hint
28
+ * so integrated agents match the user's experience level.
29
+ */
30
+ persona?: {
31
+ level: 1 | 2 | 3;
32
+ label: 'novice' | 'curious' | 'engineer';
33
+ };
25
34
  }
26
35
  export declare function buildAppSkillDescription(appName: string, registries: WorkspaceAllData | null): string;
27
36
  /**
@@ -1,3 +1,37 @@
1
+ /**
2
+ * Build a persona-specific communication block for the instruction hint.
3
+ * Returns an empty array for engineers (level 3), who need no special tone
4
+ * handling, and for an absent/unknown persona.
5
+ */
6
+ function buildPersonaBlock(persona) {
7
+ if (!persona || persona.level === 3)
8
+ return [];
9
+ if (persona.level === 1) {
10
+ return [
11
+ '',
12
+ '### Communicating with this user',
13
+ '',
14
+ 'The person you are helping is new to AI tools and is not a software developer. Adapt how you work with them:',
15
+ '',
16
+ '- Explain things in plain, everyday language. Do not assume they know programming or technical concepts.',
17
+ '- Do not ask them to run developer tooling (npm, bun, node, pnpm, yarn, git, build or package-manager commands). When a task needs technical steps, perform those steps yourself instead of instructing the user.',
18
+ '- Avoid jargon. When a technical term is unavoidable, define it in one short sentence.',
19
+ '- Work in small steps. Do one thing, confirm it landed, then continue.',
20
+ '- Focus on what the user wants to accomplish, not on the implementation details.',
21
+ ];
22
+ }
23
+ // Level 2: curious
24
+ return [
25
+ '',
26
+ '### Communicating with this user',
27
+ '',
28
+ 'The person you are helping has some familiarity with AI tools but is not a professional developer. Adapt how you work with them:',
29
+ '',
30
+ '- Prefer plain language. When you use a technical term, briefly explain what it means.',
31
+ '- Do not lean on developer tooling (npm, bun, node, git, build commands) unless it is genuinely required, and explain what any command does before suggesting it.',
32
+ '- Keep explanations concise and outcome-focused.',
33
+ ];
34
+ }
1
35
  /** Format a list of names with truncation. Shows up to `max` items, then "+ N more". */
2
36
  function formatList(items, max = 8) {
3
37
  if (items.length <= max)
@@ -314,6 +348,12 @@ export function generateIntroSkill(ctx) {
314
348
  lines.push('- Every workflow needs a trigger (endpoint, schedule, or UI button).');
315
349
  lines.push('- Use `search_available_integrations` MCP tool or `runwork integrations search <query>` before adding integrations, never guess IDs.');
316
350
  lines.push('');
351
+ lines.push('**Environment variables and secrets:** Runwork apps do NOT have a custom environment variable or secrets system.');
352
+ lines.push('- You cannot add your own keys to a `.env` file, define `process.env.MY_KEY`, or configure per-app secrets. Do not write code that depends on custom env vars, and do not tell the user to create `.env` files or set secrets.');
353
+ lines.push('- A fixed set of platform variables is injected automatically by `@runworkai/framework` (workspace identifiers, AI proxy, integration proxy). You do not set these and should not hardcode or depend on their exact names.');
354
+ lines.push('- For configuration values or API keys you need to store, use a dedicated entity with an app-specific name (for example `AcmeCrmConfig`, not a generic `Config` or `AppConfig`, which would collide with other apps in the workspace) and read it at runtime, not env vars.');
355
+ lines.push('- For third-party API access, use connected Runwork integrations (`{id}_api` MCP tools); they handle authentication without secrets in your code.');
356
+ lines.push('');
317
357
  // Capability reference — detailed mapping of needs to Runwork capabilities.
318
358
  // The "Default Actions" section at the top handles routing; this is the
319
359
  // longer reference for when the agent wants to understand what's available.
@@ -399,6 +439,7 @@ export function generateInstructionHint(ctx) {
399
439
  `**Available now:** ${inventory}`,
400
440
  '',
401
441
  `Runwork MCP tools are always connected -- use them for data access, integration API calls, skill management, and resource discovery.${integrationToolHint} Use \`runwork\` CLI to create (\`runwork init\`), develop (\`runwork dev\`), and deploy (\`runwork deploy\`) apps. Install CLI: \`curl -fsSL https://runwork.ai/install.sh | sh\`. Web dashboard: ${dashboardUrl}. Invoke the \`runwork\` skill for full capability reference.`,
442
+ ...buildPersonaBlock(ctx.persona),
402
443
  '<!-- runwork:end -->',
403
444
  ].join('\n');
404
445
  }
@@ -52,6 +52,39 @@ export interface AgentManualSetup {
52
52
  showAfter?: ManualSetupSlot;
53
53
  downloadArtifact?: AgentManualSetupArtifact;
54
54
  }
55
+ /**
56
+ * How a recipient can resume a shared conversation locally in this agent.
57
+ *
58
+ * Consumed by `runwork resume <share-id>` and by the desktop UI's
59
+ * "Resume in <agent>" button. The CLI's resume command branches on `mode`:
60
+ *
61
+ * - 'cli-resume' Place the bundle on disk, then exec `cliResumeCommand`
62
+ * with {uuid} substituted. The agent opens directly
63
+ * into the resumed session (Claude Code, Codex CLI,
64
+ * Gemini CLI, Cline CLI).
65
+ *
66
+ * - 'file-drop-only' Place the bundle on disk in a location the agent's
67
+ * UI will surface (e.g., Codex Desktop's recent threads).
68
+ * The CLI prints `manualOpenHint` so the user knows how
69
+ * to pick up the session inside the agent.
70
+ *
71
+ * - 'unsupported' No native resume. The CLI falls back to the universal
72
+ * paste-prompt path. Used for walled-garden agents like
73
+ * Microsoft Copilot or Claude Desktop Chat mode.
74
+ *
75
+ * Bundle placement logic (which directory, which filename) is per-agent and
76
+ * lives in the resume command implementation, not here. This data field stays
77
+ * declarative.
78
+ */
79
+ export interface AgentResumeCapability {
80
+ mode: 'cli-resume' | 'file-drop-only' | 'unsupported';
81
+ /** Bundle format this agent reads natively (must match a bundle's `format`). */
82
+ nativeBundleFormat?: 'claude-jsonl' | 'codex-rollout' | string;
83
+ /** Template for the resume command (cli-resume only). `{uuid}` is substituted with the share's session id. */
84
+ cliResumeCommand?: string;
85
+ /** Hint text shown to the user when the bundle was dropped but the agent must be opened manually. */
86
+ manualOpenHint?: string;
87
+ }
55
88
  export interface AgentDefinition extends InstallableTool {
56
89
  aliases?: string[];
57
90
  category: AgentCategory;
@@ -81,6 +114,8 @@ export interface AgentDefinition extends InstallableTool {
81
114
  quickStart?: AgentQuickStart;
82
115
  /** Manual setup steps required after installation (desktop onboarding) */
83
116
  manualSetup?: AgentManualSetup;
117
+ /** How `runwork resume` should hand this agent a shared conversation. Undefined = paste-prompt fallback only. */
118
+ resumeCapability?: AgentResumeCapability;
84
119
  }
85
120
  export declare function getAgent(slug: string): AgentDefinition | undefined;
86
121
  export declare function getAgentByName(name: string): AgentDefinition | undefined;