a2acalling 0.1.7 → 0.1.9
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/README.md +25 -5
- package/bin/cli.js +8 -2
- package/package.json +4 -4
- package/scripts/install-openclaw.js +333 -74
- package/src/dashboard/public/app.js +341 -0
- package/src/dashboard/public/index.html +155 -0
- package/src/dashboard/public/style.css +166 -0
- package/src/index.js +5 -0
- package/src/lib/config.js +8 -0
- package/src/lib/disclosure.js +300 -0
- package/src/lib/openclaw-integration.js +37 -30
- package/src/lib/prompt-template.js +356 -0
- package/src/lib/tokens.js +14 -1
- package/src/routes/a2a.js +1 -0
- package/src/routes/dashboard.js +571 -0
- package/src/server.js +450 -31
- package/AGENTS.md +0 -66
- package/CLAUDE.md +0 -52
- package/SKILL.md +0 -122
package/src/lib/config.js
CHANGED
|
@@ -23,6 +23,8 @@ const DEFAULT_CONFIG = {
|
|
|
23
23
|
name: 'Public',
|
|
24
24
|
description: 'Basic networking - safe for anyone',
|
|
25
25
|
capabilities: [],
|
|
26
|
+
topics: [],
|
|
27
|
+
goals: [],
|
|
26
28
|
disclosure: 'minimal',
|
|
27
29
|
examples: ['calendar availability', 'public social posts', 'general questions']
|
|
28
30
|
},
|
|
@@ -30,6 +32,8 @@ const DEFAULT_CONFIG = {
|
|
|
30
32
|
name: 'Friends',
|
|
31
33
|
description: 'Most capabilities, no sensitive financial data',
|
|
32
34
|
capabilities: [],
|
|
35
|
+
topics: [],
|
|
36
|
+
goals: [],
|
|
33
37
|
disclosure: 'public',
|
|
34
38
|
examples: ['email summaries', 'schedule meetings', 'project discussions']
|
|
35
39
|
},
|
|
@@ -37,6 +41,8 @@ const DEFAULT_CONFIG = {
|
|
|
37
41
|
name: 'Private',
|
|
38
42
|
description: 'Full access - only for you',
|
|
39
43
|
capabilities: [],
|
|
44
|
+
topics: [],
|
|
45
|
+
goals: [],
|
|
40
46
|
disclosure: 'public',
|
|
41
47
|
examples: ['financial data', 'personal notes', 'private conversations']
|
|
42
48
|
},
|
|
@@ -44,6 +50,8 @@ const DEFAULT_CONFIG = {
|
|
|
44
50
|
name: 'Custom',
|
|
45
51
|
description: 'User-defined permissions',
|
|
46
52
|
capabilities: [],
|
|
53
|
+
topics: [],
|
|
54
|
+
goals: [],
|
|
47
55
|
disclosure: 'minimal',
|
|
48
56
|
examples: []
|
|
49
57
|
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disclosure Manifest System
|
|
3
|
+
*
|
|
4
|
+
* Manages a structured list of topics the owner wants to discuss
|
|
5
|
+
* at each access tier. Stored as JSON at ~/.config/openclaw/a2a-disclosure.json.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const CONFIG_DIR = process.env.A2A_CONFIG_DIR ||
|
|
12
|
+
process.env.OPENCLAW_CONFIG_DIR ||
|
|
13
|
+
path.join(process.env.HOME || '/tmp', '.config', 'openclaw');
|
|
14
|
+
|
|
15
|
+
const MANIFEST_FILE = path.join(CONFIG_DIR, 'a2a-disclosure.json');
|
|
16
|
+
|
|
17
|
+
const TIER_HIERARCHY = ['public', 'friends', 'family'];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Load manifest from disk. Returns {} if not found.
|
|
21
|
+
*/
|
|
22
|
+
function loadManifest() {
|
|
23
|
+
try {
|
|
24
|
+
if (fs.existsSync(MANIFEST_FILE)) {
|
|
25
|
+
return JSON.parse(fs.readFileSync(MANIFEST_FILE, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.error('[a2a] Failed to load disclosure manifest:', e.message);
|
|
29
|
+
}
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Save manifest to disk.
|
|
35
|
+
*/
|
|
36
|
+
function saveManifest(manifest) {
|
|
37
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
38
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
manifest.updated_at = new Date().toISOString();
|
|
41
|
+
const tmpPath = `${MANIFEST_FILE}.tmp`;
|
|
42
|
+
fs.writeFileSync(tmpPath, JSON.stringify(manifest, null, 2));
|
|
43
|
+
fs.renameSync(tmpPath, MANIFEST_FILE);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get topics for a given tier, merged down the hierarchy.
|
|
48
|
+
* family gets everything, friends gets friends+public, public gets public only.
|
|
49
|
+
*
|
|
50
|
+
* Returns { lead_with, discuss_freely, deflect, never_disclose }
|
|
51
|
+
*/
|
|
52
|
+
function getTopicsForTier(tier) {
|
|
53
|
+
const manifest = loadManifest();
|
|
54
|
+
const topics = manifest.topics || {};
|
|
55
|
+
|
|
56
|
+
const tierIndex = TIER_HIERARCHY.indexOf(tier);
|
|
57
|
+
if (tierIndex === -1) {
|
|
58
|
+
// Unknown tier, treat as public
|
|
59
|
+
return getTopicsForTier('public');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Merge tiers from public up to the requested tier
|
|
63
|
+
const tiersToMerge = TIER_HIERARCHY.slice(0, tierIndex + 1);
|
|
64
|
+
|
|
65
|
+
const merged = {
|
|
66
|
+
lead_with: [],
|
|
67
|
+
discuss_freely: [],
|
|
68
|
+
deflect: [],
|
|
69
|
+
never_disclose: manifest.never_disclose || []
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
for (const t of tiersToMerge) {
|
|
73
|
+
const tierTopics = topics[t] || {};
|
|
74
|
+
if (tierTopics.lead_with) merged.lead_with.push(...tierTopics.lead_with);
|
|
75
|
+
if (tierTopics.discuss_freely) merged.discuss_freely.push(...tierTopics.discuss_freely);
|
|
76
|
+
if (tierTopics.deflect) merged.deflect.push(...tierTopics.deflect);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Deflect items: remove any that already appear in lead_with or discuss_freely
|
|
80
|
+
// (higher tiers promote topics from deflect to discuss/lead)
|
|
81
|
+
const promoted = new Set([
|
|
82
|
+
...merged.lead_with.map(t => t.topic),
|
|
83
|
+
...merged.discuss_freely.map(t => t.topic)
|
|
84
|
+
]);
|
|
85
|
+
merged.deflect = merged.deflect.filter(t => !promoted.has(t.topic));
|
|
86
|
+
|
|
87
|
+
return merged;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Format topic lists into readable bullet points for prompt injection.
|
|
92
|
+
*/
|
|
93
|
+
function formatTopicsForPrompt(tierTopics) {
|
|
94
|
+
const formatList = (items) => {
|
|
95
|
+
if (!items || items.length === 0) return ' (none specified)';
|
|
96
|
+
return items.map(item => ` - ${item.topic}: ${item.detail}`).join('\n');
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
leadWithTopics: formatList(tierTopics.lead_with),
|
|
101
|
+
discussFreelyTopics: formatList(tierTopics.discuss_freely),
|
|
102
|
+
deflectTopics: formatList(tierTopics.deflect),
|
|
103
|
+
neverDisclose: tierTopics.never_disclose?.length
|
|
104
|
+
? tierTopics.never_disclose.map(item => ` - ${item}`).join('\n')
|
|
105
|
+
: ' (none specified)'
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Generate a default manifest by reading USER.md, HEARTBEAT.md, SOUL.md
|
|
111
|
+
* from the owner's workspace. Falls back to a minimal starter if files don't exist.
|
|
112
|
+
*/
|
|
113
|
+
function generateDefaultManifest(contextFiles = {}) {
|
|
114
|
+
const now = new Date().toISOString();
|
|
115
|
+
|
|
116
|
+
const manifest = {
|
|
117
|
+
version: 1,
|
|
118
|
+
generated_at: now,
|
|
119
|
+
updated_at: now,
|
|
120
|
+
topics: {
|
|
121
|
+
public: { lead_with: [], discuss_freely: [], deflect: [] },
|
|
122
|
+
friends: { lead_with: [], discuss_freely: [], deflect: [] },
|
|
123
|
+
family: { lead_with: [], discuss_freely: [], deflect: [] }
|
|
124
|
+
},
|
|
125
|
+
never_disclose: ['API keys', 'Other users\' data', 'Financial figures'],
|
|
126
|
+
personality_notes: 'Direct and technical. Prefers depth over breadth.'
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const userContent = contextFiles.user || '';
|
|
130
|
+
const heartbeatContent = contextFiles.heartbeat || '';
|
|
131
|
+
const soulContent = contextFiles.soul || '';
|
|
132
|
+
|
|
133
|
+
const hasContent = userContent || heartbeatContent || soulContent;
|
|
134
|
+
|
|
135
|
+
if (!hasContent) {
|
|
136
|
+
// Minimal starter manifest
|
|
137
|
+
manifest.topics.public.lead_with.push(
|
|
138
|
+
{ topic: 'What I do', detail: 'Brief professional description' }
|
|
139
|
+
);
|
|
140
|
+
manifest.topics.public.discuss_freely.push(
|
|
141
|
+
{ topic: 'General interests', detail: 'Non-sensitive topics and hobbies' }
|
|
142
|
+
);
|
|
143
|
+
manifest.topics.public.deflect.push(
|
|
144
|
+
{ topic: 'Personal details', detail: 'Redirect to direct owner contact' }
|
|
145
|
+
);
|
|
146
|
+
return manifest;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Extract from USER.md
|
|
150
|
+
if (userContent) {
|
|
151
|
+
// Goals/seeking
|
|
152
|
+
const goalsMatch = userContent.match(/##\s*(?:Goals|Current|Seeking|Working On)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
153
|
+
if (goalsMatch) {
|
|
154
|
+
const goals = goalsMatch[1]
|
|
155
|
+
.split('\n')
|
|
156
|
+
.filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
|
|
157
|
+
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
158
|
+
.filter(Boolean);
|
|
159
|
+
|
|
160
|
+
goals.forEach((goal, i) => {
|
|
161
|
+
if (i < 2) {
|
|
162
|
+
manifest.topics.public.lead_with.push({ topic: goal.slice(0, 60), detail: goal });
|
|
163
|
+
} else {
|
|
164
|
+
manifest.topics.public.discuss_freely.push({ topic: goal.slice(0, 60), detail: goal });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Interests/projects
|
|
170
|
+
const interestsMatch = userContent.match(/##\s*(?:Interests|Projects|Skills)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
171
|
+
if (interestsMatch) {
|
|
172
|
+
const interests = interestsMatch[1]
|
|
173
|
+
.split('\n')
|
|
174
|
+
.filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
|
|
175
|
+
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
176
|
+
.filter(Boolean);
|
|
177
|
+
|
|
178
|
+
interests.forEach(interest => {
|
|
179
|
+
manifest.topics.public.discuss_freely.push({ topic: interest.slice(0, 60), detail: interest });
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Private/personal sections go to friends/family
|
|
184
|
+
const privateMatch = userContent.match(/##\s*(?:Private|Personal|Family)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
185
|
+
if (privateMatch) {
|
|
186
|
+
const privateItems = privateMatch[1]
|
|
187
|
+
.split('\n')
|
|
188
|
+
.filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
|
|
189
|
+
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
190
|
+
.filter(Boolean);
|
|
191
|
+
|
|
192
|
+
privateItems.forEach(item => {
|
|
193
|
+
manifest.topics.family.discuss_freely.push({ topic: item.slice(0, 60), detail: item });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Deflect these for public
|
|
197
|
+
manifest.topics.public.deflect.push(
|
|
198
|
+
{ topic: 'Personal life', detail: 'Redirect — suggest owners connect directly' }
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Extract from HEARTBEAT.md (recent activity/status)
|
|
204
|
+
if (heartbeatContent) {
|
|
205
|
+
const recentLines = heartbeatContent
|
|
206
|
+
.split('\n')
|
|
207
|
+
.filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
|
|
208
|
+
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
209
|
+
.filter(Boolean)
|
|
210
|
+
.slice(0, 5);
|
|
211
|
+
|
|
212
|
+
recentLines.forEach((line, i) => {
|
|
213
|
+
if (i < 2) {
|
|
214
|
+
manifest.topics.public.lead_with.push({ topic: line.slice(0, 60), detail: line });
|
|
215
|
+
} else {
|
|
216
|
+
manifest.topics.friends.discuss_freely.push({ topic: line.slice(0, 60), detail: line });
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Extract from SOUL.md (personality, values)
|
|
222
|
+
if (soulContent) {
|
|
223
|
+
// Look for personality cues
|
|
224
|
+
const personalityLines = soulContent
|
|
225
|
+
.split('\n')
|
|
226
|
+
.filter(l => l.trim() && !l.startsWith('#'))
|
|
227
|
+
.slice(0, 3)
|
|
228
|
+
.join(' ')
|
|
229
|
+
.trim();
|
|
230
|
+
|
|
231
|
+
if (personalityLines) {
|
|
232
|
+
manifest.personality_notes = personalityLines.slice(0, 300);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Values become friends-tier topics
|
|
236
|
+
const valuesMatch = soulContent.match(/##\s*(?:Values|Beliefs|Principles)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
237
|
+
if (valuesMatch) {
|
|
238
|
+
const values = valuesMatch[1]
|
|
239
|
+
.split('\n')
|
|
240
|
+
.filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
|
|
241
|
+
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
242
|
+
.filter(Boolean);
|
|
243
|
+
|
|
244
|
+
values.forEach(value => {
|
|
245
|
+
manifest.topics.friends.discuss_freely.push({ topic: value.slice(0, 60), detail: value });
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Ensure at least something in each public category
|
|
251
|
+
if (manifest.topics.public.lead_with.length === 0) {
|
|
252
|
+
manifest.topics.public.lead_with.push(
|
|
253
|
+
{ topic: 'Current focus', detail: 'Primary work and interests' }
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (manifest.topics.public.discuss_freely.length === 0) {
|
|
257
|
+
manifest.topics.public.discuss_freely.push(
|
|
258
|
+
{ topic: 'General interests', detail: 'Non-sensitive topics' }
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (manifest.topics.public.deflect.length === 0) {
|
|
262
|
+
manifest.topics.public.deflect.push(
|
|
263
|
+
{ topic: 'Private matters', detail: 'Redirect to direct owner contact' }
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return manifest;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Read context files from an OpenClaw workspace directory.
|
|
272
|
+
* Returns { user, heartbeat, soul } with file contents or empty strings.
|
|
273
|
+
*/
|
|
274
|
+
function readContextFiles(workspaceDir) {
|
|
275
|
+
const read = (filename) => {
|
|
276
|
+
try {
|
|
277
|
+
const filePath = path.join(workspaceDir, filename);
|
|
278
|
+
if (fs.existsSync(filePath)) {
|
|
279
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
280
|
+
}
|
|
281
|
+
} catch (e) {}
|
|
282
|
+
return '';
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
user: read('USER.md'),
|
|
287
|
+
heartbeat: read('HEARTBEAT.md'),
|
|
288
|
+
soul: read('SOUL.md')
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
module.exports = {
|
|
293
|
+
loadManifest,
|
|
294
|
+
saveManifest,
|
|
295
|
+
getTopicsForTier,
|
|
296
|
+
formatTopicsForPrompt,
|
|
297
|
+
generateDefaultManifest,
|
|
298
|
+
readContextFiles,
|
|
299
|
+
MANIFEST_FILE
|
|
300
|
+
};
|
|
@@ -9,8 +9,11 @@ const path = require('path');
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Load owner context from OpenClaw workspace files
|
|
12
|
+
* @param {string} workspaceDir - Path to workspace
|
|
13
|
+
* @param {Object} options
|
|
14
|
+
* @param {string[]} options.tierGoals - Goals from config tier (takes priority over USER.md)
|
|
12
15
|
*/
|
|
13
|
-
function loadOwnerContext(workspaceDir = process.cwd()) {
|
|
16
|
+
function loadOwnerContext(workspaceDir = process.cwd(), options = {}) {
|
|
14
17
|
const context = {
|
|
15
18
|
goals: [],
|
|
16
19
|
interests: [],
|
|
@@ -19,18 +22,25 @@ function loadOwnerContext(workspaceDir = process.cwd()) {
|
|
|
19
22
|
memory: null
|
|
20
23
|
};
|
|
21
24
|
|
|
25
|
+
// Tier goals from config take priority
|
|
26
|
+
if (options.tierGoals && options.tierGoals.length > 0) {
|
|
27
|
+
context.goals = [...options.tierGoals];
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
// Load USER.md
|
|
23
31
|
const userPath = path.join(workspaceDir, 'USER.md');
|
|
24
32
|
if (fs.existsSync(userPath)) {
|
|
25
33
|
context.user = fs.readFileSync(userPath, 'utf8');
|
|
26
|
-
// Extract goals from USER.md
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
// Extract goals from USER.md (fallback if no tier goals from config)
|
|
35
|
+
if (context.goals.length === 0) {
|
|
36
|
+
const goalsMatch = context.user.match(/##\s*(?:Goals|Current|Seeking)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
37
|
+
if (goalsMatch) {
|
|
38
|
+
context.goals = goalsMatch[1]
|
|
39
|
+
.split('\n')
|
|
40
|
+
.filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
|
|
41
|
+
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
}
|
|
34
44
|
}
|
|
35
45
|
// Extract interests
|
|
36
46
|
const interestsMatch = context.user.match(/##\s*(?:Interests|Projects)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
@@ -84,7 +94,7 @@ function buildSummaryPrompt(messages, ownerContext, callerInfo = {}) {
|
|
|
84
94
|
const goalsSection = ownerContext.goals?.length ? `### Current Goals\n- ${ownerContext.goals.join('\n- ')}` : '';
|
|
85
95
|
const interestsSection = ownerContext.interests?.length ? `### Interests\n- ${ownerContext.interests.join('\n- ')}` : '';
|
|
86
96
|
|
|
87
|
-
return `You just finished
|
|
97
|
+
return `You just finished an A2A agent-to-agent call. Analyze it strategically for your owner.
|
|
88
98
|
|
|
89
99
|
## Philosophy
|
|
90
100
|
A2A is cooperative AND adversarial. Each agent maximizes value for their own owner — but the best outcomes are mutual wins. Your job:
|
|
@@ -112,35 +122,32 @@ ${callerInfo.context ? `Context: ${callerInfo.context}` : ''}
|
|
|
112
122
|
Analyze as a strategic advisor. Return JSON:
|
|
113
123
|
|
|
114
124
|
{
|
|
115
|
-
"
|
|
116
|
-
|
|
125
|
+
"who": "Who called, who they represent, key facts about them",
|
|
126
|
+
|
|
127
|
+
"keyDiscoveries": ["What was learned about the other side — capabilities, interests, blind spots"],
|
|
128
|
+
|
|
129
|
+
"collaborationPotential": {
|
|
130
|
+
"rating": "HIGH | MEDIUM | LOW",
|
|
131
|
+
"opportunities": ["specific opportunities identified"]
|
|
132
|
+
},
|
|
133
|
+
|
|
117
134
|
"exchange": {
|
|
118
135
|
"weGot": ["info, commitments, or value we extracted"],
|
|
119
136
|
"weGave": ["info, compute, or commitments we provided"],
|
|
120
|
-
"balance": "favorable | even | unfavorable"
|
|
121
|
-
"fair": true
|
|
122
|
-
},
|
|
123
|
-
|
|
124
|
-
"mutualValue": {
|
|
125
|
-
"found": true,
|
|
126
|
-
"opportunities": ["potential wins for BOTH sides"],
|
|
127
|
-
"alignment": "how this connects to owner's goals"
|
|
137
|
+
"balance": "favorable | even | unfavorable"
|
|
128
138
|
},
|
|
129
|
-
|
|
130
|
-
"
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
},
|
|
135
|
-
|
|
139
|
+
|
|
140
|
+
"recommendedFollowUp": ["actionable items with specifics"],
|
|
141
|
+
|
|
142
|
+
"assessment": "One-sentence strategic value judgment",
|
|
143
|
+
|
|
136
144
|
"trust": {
|
|
137
145
|
"assessment": "appropriate | too_high | too_low",
|
|
138
146
|
"recommendation": "maintain | increase | decrease | revoke",
|
|
139
147
|
"pattern": "What's their angle? Genuine partner or extractive?"
|
|
140
148
|
},
|
|
141
|
-
|
|
142
|
-
"ownerBrief": "2-3 sentences: the strategic takeaway for your owner"
|
|
143
|
-
"followUp": "concrete next step to advance this relationship (if any)"
|
|
149
|
+
|
|
150
|
+
"ownerBrief": "2-3 sentences: the strategic takeaway for your owner"
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
Think like a strategic advisor: protect your owner's interests AND find mutual wins.
|