@thelogicatelier/sylva 1.0.11 → 1.0.12

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.
@@ -46,8 +46,16 @@ const CONFIDENCE_SCORES = {
46
46
  function detectStacks(signals) {
47
47
  const grouped = new Map();
48
48
  for (const signal of signals) {
49
- // Skip tooling/entrypoint signals for stack detection
50
- if (signal.kind === "tooling" || signal.kind === "entrypoint")
49
+ // Skip non-framework signals from stack detection
50
+ if (signal.kind === "tooling" ||
51
+ signal.kind === "entrypoint" ||
52
+ signal.kind === "agent" ||
53
+ signal.kind === "subagent" ||
54
+ signal.kind === "heartbeat" ||
55
+ signal.kind === "cron" ||
56
+ signal.kind === "hook" ||
57
+ signal.kind === "skill" ||
58
+ signal.kind === "plugin")
51
59
  continue;
52
60
  if (!grouped.has(signal.frameworkId)) {
53
61
  grouped.set(signal.frameworkId, []);
@@ -50,7 +50,7 @@ const webGrounding_1 = require("./webGrounding");
50
50
  * The ARCHITECTURE CONSTRAINTS block injected into LLM context.
51
51
  * This is authoritative and must not be overridden by the model.
52
52
  */
53
- function buildConstraintsBlock(stacks, resolvedVersions, hasOrchestrator) {
53
+ function buildConstraintsBlock(stacks, resolvedVersions, hasOrchestrator, signals) {
54
54
  const lines = [
55
55
  "=== ARCHITECTURE CONSTRAINTS (AUTHORITATIVE) ===",
56
56
  "1) The detected frameworks/stacks listed below are authoritative because they were derived from repository manifest/config files.",
@@ -78,9 +78,71 @@ function buildConstraintsBlock(stacks, resolvedVersions, hasOrchestrator) {
78
78
  lines.push(` Evidence: ${ev.evidence.reason} [${ev.evidence.file}]`);
79
79
  }
80
80
  }
81
+ // OpenClaw-specific constraint sections (only when detected)
82
+ if (hasOrchestrator) {
83
+ appendOpenClawConstraints(lines, signals);
84
+ }
81
85
  lines.push("=== END ARCHITECTURE CONSTRAINTS ===");
82
86
  return lines.join("\n");
83
87
  }
88
+ /**
89
+ * Append OpenClaw-specific sections to the constraints block.
90
+ * Groups signals by kind (agent, hook, skill, subagent, plugin, heartbeat).
91
+ */
92
+ function appendOpenClawConstraints(lines, signals) {
93
+ // Agent config
94
+ const agentSignals = signals.filter((s) => s.kind === "agent");
95
+ if (agentSignals.length > 0) {
96
+ lines.push("", "OPENCLAW AGENT CONFIG:");
97
+ for (const sig of agentSignals) {
98
+ lines.push(` - ${sig.frameworkName}: ${sig.evidence.excerpt}`);
99
+ }
100
+ }
101
+ // Hooks
102
+ const hookSignals = signals.filter((s) => s.kind === "hook");
103
+ if (hookSignals.length > 0) {
104
+ lines.push("", "OPENCLAW HOOKS:");
105
+ for (const sig of hookSignals) {
106
+ const name = sig.frameworkName.replace("OpenClaw Hook: ", "");
107
+ lines.push(` - /${name}: ${sig.evidence.reason}`);
108
+ }
109
+ }
110
+ // Skills
111
+ const skillSignals = signals.filter((s) => s.kind === "skill");
112
+ if (skillSignals.length > 0) {
113
+ lines.push("", "OPENCLAW SKILLS:");
114
+ for (const sig of skillSignals) {
115
+ lines.push(` - ${sig.frameworkName.replace("OpenClaw Skill: ", "")}: ${sig.evidence.reason}`);
116
+ }
117
+ }
118
+ // Subagents
119
+ const subagentSignals = signals.filter((s) => s.kind === "subagent");
120
+ if (subagentSignals.length > 0) {
121
+ lines.push("", "OPENCLAW SUBAGENTS:");
122
+ for (const sig of subagentSignals) {
123
+ lines.push(` - ${sig.frameworkName.replace("OpenClaw Subagent: ", "")}: ${sig.evidence.reason}`);
124
+ if (sig.evidence.excerpt) {
125
+ lines.push(` ${sig.evidence.excerpt}`);
126
+ }
127
+ }
128
+ }
129
+ // Plugins
130
+ const pluginSignals = signals.filter((s) => s.kind === "plugin");
131
+ if (pluginSignals.length > 0) {
132
+ lines.push("", "OPENCLAW PLUGINS:");
133
+ for (const sig of pluginSignals) {
134
+ lines.push(` - ${sig.frameworkName.replace("OpenClaw Plugin: ", "")}: ${sig.evidence.excerpt}`);
135
+ }
136
+ }
137
+ // Heartbeat
138
+ const heartbeatSignals = signals.filter((s) => s.kind === "heartbeat");
139
+ if (heartbeatSignals.length > 0) {
140
+ lines.push("", "OPENCLAW HEARTBEAT:");
141
+ for (const sig of heartbeatSignals) {
142
+ lines.push(` - ${sig.evidence.excerpt}`);
143
+ }
144
+ }
145
+ }
84
146
  /**
85
147
  * Build the full awareness context string for LLM prompts.
86
148
  */
@@ -170,6 +232,65 @@ function saveAwarenessJson(repoName, result, baseDir = "projects") {
170
232
  ` Check disk space and directory write permissions if you need this output.`);
171
233
  }
172
234
  }
235
+ /**
236
+ * Save grounding.json for web grounding transparency.
237
+ * Always saved — contains references + structured errors.
238
+ */
239
+ function saveGroundingJson(repoName, webReferences, errors, baseDir = "projects") {
240
+ const folderName = repoName.toLowerCase().replace(/\s+/g, "-");
241
+ const targetDir = path.join(baseDir, folderName);
242
+ fs.mkdirSync(targetDir, { recursive: true });
243
+ const filePath = path.join(targetDir, "grounding.json");
244
+ try {
245
+ // Build structured error entries
246
+ const structuredErrors = errors.map((err) => {
247
+ // Parse known error patterns
248
+ if (err.includes("BRAVE_API_KEY not set")) {
249
+ return {
250
+ reason: "BRAVE_API_KEY not set",
251
+ impact: "Web grounding disabled — no documentation references gathered",
252
+ resolution: "Set BRAVE_API_KEY in your .env file. Get a free key at https://brave.com/search/api/",
253
+ };
254
+ }
255
+ if (err.includes("rate limit") || err.includes("HTTP 429")) {
256
+ const queryMatch = err.match(/Query: "([^"]+)"/);
257
+ return {
258
+ query: queryMatch ? queryMatch[1] : undefined,
259
+ reason: "Brave Search API rate limit exceeded (HTTP 429)",
260
+ impact: "Results missing for this query",
261
+ resolution: "Wait a moment and retry, or check your Brave API plan limits",
262
+ };
263
+ }
264
+ if (err.includes("Web search failed")) {
265
+ const queryMatch = err.match(/for "([^"]+)"/);
266
+ const reasonMatch = err.match(/: (.+)$/);
267
+ return {
268
+ query: queryMatch ? queryMatch[1] : undefined,
269
+ reason: reasonMatch ? reasonMatch[1] : err,
270
+ impact: "Results missing for this query",
271
+ };
272
+ }
273
+ return {
274
+ reason: err,
275
+ impact: "Unknown web grounding error",
276
+ };
277
+ });
278
+ const grounding = {
279
+ generatedAt: new Date().toISOString(),
280
+ totalReferences: webReferences.reduce((sum, ref) => sum + ref.results.length, 0),
281
+ frameworksCovered: webReferences.length,
282
+ references: webReferences,
283
+ errors: structuredErrors,
284
+ };
285
+ fs.writeFileSync(filePath, JSON.stringify(grounding, null, 2), "utf-8");
286
+ console.log(`✅ Saved grounding.json to: ${filePath}`);
287
+ }
288
+ catch (error) {
289
+ console.warn(`⚠️ Could not save grounding.json to ${filePath}: ${error.message}\n` +
290
+ ` This is a debug file and does not affect AGENTS.md generation.\n` +
291
+ ` Check disk space and directory write permissions if you need this output.`);
292
+ }
293
+ }
173
294
  /**
174
295
  * Run the full awareness pipeline.
175
296
  * This is the main entry point called from the CLI.
@@ -210,9 +331,28 @@ async function runAwareness(repoPath, repoName) {
210
331
  }
211
332
  if (hasOrchestrator) {
212
333
  console.log(" 🎯 OpenClaw orchestrator detected");
334
+ // Log OpenClaw-specific discoveries
335
+ const hookCount = signals.filter((s) => s.kind === "hook").length;
336
+ const skillCount = signals.filter((s) => s.kind === "skill").length;
337
+ const subagentCount = signals.filter((s) => s.kind === "subagent").length;
338
+ const pluginCount = signals.filter((s) => s.kind === "plugin").length;
339
+ if (hookCount > 0)
340
+ console.log(` 🪝 ${hookCount} hook(s) detected`);
341
+ if (skillCount > 0)
342
+ console.log(` 🎯 ${skillCount} skill(s) detected`);
343
+ if (subagentCount > 0)
344
+ console.log(` 🤖 ${subagentCount} subagent(s) detected`);
345
+ if (pluginCount > 0)
346
+ console.log(` 🔌 ${pluginCount} plugin(s) detected`);
347
+ const heartbeat = signals.find((s) => s.kind === "heartbeat");
348
+ if (heartbeat) {
349
+ const active = heartbeat.evidence.excerpt?.includes("ACTIVE") &&
350
+ !heartbeat.evidence.excerpt?.includes("INACTIVE");
351
+ console.log(` 💓 Heartbeat: ${active ? "ACTIVE" : "INACTIVE"}`);
352
+ }
213
353
  }
214
- // Step 5: Build constraints block
215
- const constraintsBlock = buildConstraintsBlock(stacks, resolvedVersions, hasOrchestrator);
354
+ // Step 5: Build constraints block (now includes OpenClaw-specific sections)
355
+ const constraintsBlock = buildConstraintsBlock(stacks, resolvedVersions, hasOrchestrator, signals);
216
356
  // Step 6: Web grounding
217
357
  console.log(" → Gathering web references...");
218
358
  const cacheDir = path.join("projects", repoName.toLowerCase().replace(/\s+/g, "-"), "cache", "brave");
@@ -254,6 +394,8 @@ async function runAwareness(repoPath, repoName) {
254
394
  };
255
395
  // Step 8: Save awareness.json
256
396
  saveAwarenessJson(repoName, result);
397
+ // Step 9: Save grounding.json (always, even on errors)
398
+ saveGroundingJson(repoName, webReferences, errors);
257
399
  console.log("✅ Framework Awareness scan complete\n");
258
400
  return result;
259
401
  }
@@ -1,6 +1,20 @@
1
1
  /**
2
- * OpenClaw manifest parser.
3
- * Parses openclaw.json to extract orchestrator configuration.
2
+ * OpenClaw manifest parser — deep extraction.
3
+ * Parses openclaw.json / .openclaw.json to extract:
4
+ * - Orchestrator signal with version from meta
5
+ * - Agent config (models, workspace, concurrency)
6
+ * - Hooks (internal event scripts with paths + descriptions)
7
+ * - Plugins (enabled extensions)
8
+ * - Gateway config (port, auth, denied commands)
9
+ * - Channels (policies, stream modes)
10
+ * - Tools (web search, fetch)
11
+ * - Commands / messages config
12
+ *
13
+ * Also scans the OpenClaw workspace directory for:
14
+ * - Skills (reusable .md workflows)
15
+ * - Subagents (background agent directories)
16
+ * - Workspace .md files (AGENTS.md, IDENTITY.md, HEARTBEAT.md, etc.)
17
+ * - Hook scripts
4
18
  */
5
19
  import { Signal, ManifestFile } from "../types";
6
20
  export declare function parseOpenclawJson(manifest: ManifestFile): Signal[];
@@ -1,7 +1,21 @@
1
1
  "use strict";
2
2
  /**
3
- * OpenClaw manifest parser.
4
- * Parses openclaw.json to extract orchestrator configuration.
3
+ * OpenClaw manifest parser — deep extraction.
4
+ * Parses openclaw.json / .openclaw.json to extract:
5
+ * - Orchestrator signal with version from meta
6
+ * - Agent config (models, workspace, concurrency)
7
+ * - Hooks (internal event scripts with paths + descriptions)
8
+ * - Plugins (enabled extensions)
9
+ * - Gateway config (port, auth, denied commands)
10
+ * - Channels (policies, stream modes)
11
+ * - Tools (web search, fetch)
12
+ * - Commands / messages config
13
+ *
14
+ * Also scans the OpenClaw workspace directory for:
15
+ * - Skills (reusable .md workflows)
16
+ * - Subagents (background agent directories)
17
+ * - Workspace .md files (AGENTS.md, IDENTITY.md, HEARTBEAT.md, etc.)
18
+ * - Hook scripts
5
19
  */
6
20
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
21
  if (k2 === undefined) k2 = k;
@@ -40,6 +54,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
40
54
  exports.parseOpenclawJson = parseOpenclawJson;
41
55
  const fs = __importStar(require("fs"));
42
56
  const path = __importStar(require("path"));
57
+ // ---------------------
58
+ // Deep JSON extraction
59
+ // ---------------------
43
60
  function parseOpenclawJson(manifest) {
44
61
  const signals = [];
45
62
  const content = fs.readFileSync(manifest.absolutePath, "utf-8");
@@ -54,50 +71,546 @@ function parseOpenclawJson(manifest) {
54
71
  return signals;
55
72
  }
56
73
  const rootPath = path.dirname(manifest.relativePath) || ".";
57
- // Primary orchestrator signal
74
+ // --- Extract OpenClaw version from meta ---
75
+ const meta = config.meta;
76
+ const openclawVersion = meta?.lastTouchedVersion
77
+ ? {
78
+ value: String(meta.lastTouchedVersion),
79
+ certainty: "exact",
80
+ sourceFile: manifest.relativePath,
81
+ notes: "From meta.lastTouchedVersion in openclaw.json",
82
+ }
83
+ : undefined;
84
+ // --- Primary orchestrator signal ---
58
85
  signals.push({
59
86
  kind: "orchestrator",
60
87
  frameworkId: "openclaw",
61
88
  frameworkName: "OpenClaw",
89
+ ...(openclawVersion ? { version: openclawVersion } : {}),
62
90
  evidence: {
63
91
  file: manifest.relativePath,
64
92
  reason: "openclaw.json configuration file found",
65
- excerpt: content.length > 500 ? content.substring(0, 500) + "..." : content,
93
+ excerpt: openclawVersion
94
+ ? `OpenClaw version ${openclawVersion.value}`
95
+ : "OpenClaw orchestrator (version not specified in config)",
96
+ },
97
+ scope: { pathRoot: rootPath },
98
+ });
99
+ // --- Agent config ---
100
+ extractAgentConfig(config, manifest, rootPath, signals);
101
+ // --- Hooks ---
102
+ extractHooks(config, manifest, rootPath, signals);
103
+ // --- Plugins ---
104
+ extractPlugins(config, manifest, rootPath, signals);
105
+ // --- Gateway ---
106
+ extractGateway(config, manifest, rootPath, signals);
107
+ // --- Channels (expanded) ---
108
+ extractChannels(config, manifest, rootPath, signals);
109
+ // --- Tools (expanded) ---
110
+ extractTools(config, manifest, rootPath, signals);
111
+ // --- Commands + messages ---
112
+ extractCommandsAndMessages(config, manifest, rootPath, signals);
113
+ // --- Workspace scanning ---
114
+ scanWorkspace(config, manifest, rootPath, signals);
115
+ return signals;
116
+ }
117
+ // ---------------------
118
+ // Section extractors
119
+ // ---------------------
120
+ function extractAgentConfig(config, manifest, rootPath, signals) {
121
+ const agents = config.agents;
122
+ if (!agents || typeof agents !== "object")
123
+ return;
124
+ const defaults = agents.defaults;
125
+ if (!defaults || typeof defaults !== "object")
126
+ return;
127
+ // Primary model
128
+ const model = defaults.model;
129
+ const primaryModel = model?.primary ? String(model.primary) : undefined;
130
+ // Available models
131
+ const models = defaults.models;
132
+ const modelCatalog = models ? Object.keys(models) : [];
133
+ // Workspace path
134
+ const workspace = defaults.workspace ? String(defaults.workspace) : undefined;
135
+ // Concurrency
136
+ const maxConcurrent = typeof defaults.maxConcurrent === "number" ? defaults.maxConcurrent : undefined;
137
+ const subagentConfig = defaults.subagents;
138
+ const subagentMaxConcurrent = subagentConfig && typeof subagentConfig.maxConcurrent === "number"
139
+ ? subagentConfig.maxConcurrent
140
+ : undefined;
141
+ // Compaction
142
+ const compaction = defaults.compaction;
143
+ const compactionMode = compaction?.mode ? String(compaction.mode) : undefined;
144
+ const details = [];
145
+ if (primaryModel)
146
+ details.push(`primary model: ${primaryModel}`);
147
+ if (modelCatalog.length > 0)
148
+ details.push(`${modelCatalog.length} model(s) available`);
149
+ if (workspace)
150
+ details.push(`workspace: ${workspace}`);
151
+ if (maxConcurrent)
152
+ details.push(`maxConcurrent: ${maxConcurrent}`);
153
+ if (subagentMaxConcurrent)
154
+ details.push(`subagent maxConcurrent: ${subagentMaxConcurrent}`);
155
+ if (compactionMode)
156
+ details.push(`compaction: ${compactionMode}`);
157
+ signals.push({
158
+ kind: "agent",
159
+ frameworkId: "openclaw-agent-config",
160
+ frameworkName: "OpenClaw Agent Config",
161
+ evidence: {
162
+ file: manifest.relativePath,
163
+ reason: "Agent configuration in openclaw.json",
164
+ excerpt: details.join("; "),
66
165
  },
67
166
  scope: { pathRoot: rootPath },
68
167
  });
69
- // Extract tool signals
168
+ // Emit individual model entries for the LLM to know available models
169
+ if (modelCatalog.length > 0) {
170
+ signals.push({
171
+ kind: "agent",
172
+ frameworkId: "openclaw-model-catalog",
173
+ frameworkName: "OpenClaw Model Catalog",
174
+ evidence: {
175
+ file: manifest.relativePath,
176
+ reason: "Available LLM models configured in openclaw.json",
177
+ excerpt: modelCatalog.join(", "),
178
+ },
179
+ scope: { pathRoot: rootPath },
180
+ });
181
+ }
182
+ }
183
+ function extractHooks(config, manifest, rootPath, signals) {
184
+ const hooks = config.hooks;
185
+ if (!hooks || typeof hooks !== "object")
186
+ return;
187
+ const internal = hooks.internal;
188
+ if (!internal || typeof internal !== "object")
189
+ return;
190
+ if (internal.enabled === false)
191
+ return;
192
+ const entries = internal.entries;
193
+ if (!entries || typeof entries !== "object")
194
+ return;
195
+ for (const [hookName, hookConfig] of Object.entries(entries)) {
196
+ if (!hookConfig || typeof hookConfig !== "object")
197
+ continue;
198
+ const enabled = hookConfig.enabled !== false; // default true
199
+ const hookPath = hookConfig.path ? String(hookConfig.path) : undefined;
200
+ const description = hookConfig.description ? String(hookConfig.description) : undefined;
201
+ const details = [`enabled: ${enabled}`];
202
+ if (hookPath)
203
+ details.push(`path: ${hookPath}`);
204
+ if (description)
205
+ details.push(`desc: ${description}`);
206
+ signals.push({
207
+ kind: "hook",
208
+ frameworkId: `openclaw-hook-${hookName}`,
209
+ frameworkName: `OpenClaw Hook: ${hookName}`,
210
+ evidence: {
211
+ file: manifest.relativePath,
212
+ reason: description || `Hook '${hookName}' configured in openclaw.json`,
213
+ excerpt: details.join("; "),
214
+ },
215
+ scope: { pathRoot: rootPath },
216
+ });
217
+ }
218
+ }
219
+ function extractPlugins(config, manifest, rootPath, signals) {
220
+ const plugins = config.plugins;
221
+ if (!plugins || typeof plugins !== "object")
222
+ return;
223
+ const entries = plugins.entries;
224
+ if (!entries || typeof entries !== "object")
225
+ return;
226
+ for (const [pluginName, pluginConfig] of Object.entries(entries)) {
227
+ if (!pluginConfig || typeof pluginConfig !== "object")
228
+ continue;
229
+ const enabled = pluginConfig.enabled !== false;
230
+ signals.push({
231
+ kind: "plugin",
232
+ frameworkId: `openclaw-plugin-${pluginName}`,
233
+ frameworkName: `OpenClaw Plugin: ${pluginName}`,
234
+ evidence: {
235
+ file: manifest.relativePath,
236
+ reason: `Plugin '${pluginName}' ${enabled ? "enabled" : "disabled"} in openclaw.json`,
237
+ excerpt: `enabled: ${enabled}`,
238
+ },
239
+ scope: { pathRoot: rootPath },
240
+ });
241
+ }
242
+ }
243
+ function extractGateway(config, manifest, rootPath, signals) {
244
+ const gateway = config.gateway;
245
+ if (!gateway || typeof gateway !== "object")
246
+ return;
247
+ const details = [];
248
+ if (gateway.port)
249
+ details.push(`port: ${gateway.port}`);
250
+ if (gateway.mode)
251
+ details.push(`mode: ${gateway.mode}`);
252
+ if (gateway.bind)
253
+ details.push(`bind: ${gateway.bind}`);
254
+ const auth = gateway.auth;
255
+ if (auth?.mode)
256
+ details.push(`auth: ${auth.mode}`);
257
+ const tailscale = gateway.tailscale;
258
+ if (tailscale?.mode)
259
+ details.push(`tailscale: ${tailscale.mode}`);
260
+ const nodes = gateway.nodes;
261
+ const denyCommands = nodes?.denyCommands;
262
+ if (denyCommands && Array.isArray(denyCommands)) {
263
+ details.push(`denied commands: ${denyCommands.join(", ")}`);
264
+ }
265
+ signals.push({
266
+ kind: "tooling",
267
+ frameworkId: "openclaw-gateway",
268
+ frameworkName: "OpenClaw Gateway",
269
+ evidence: {
270
+ file: manifest.relativePath,
271
+ reason: "Gateway configuration in openclaw.json",
272
+ excerpt: details.join("; "),
273
+ },
274
+ scope: { pathRoot: rootPath },
275
+ });
276
+ }
277
+ function extractChannels(config, manifest, rootPath, signals) {
278
+ const channels = config.channels;
279
+ if (!channels || typeof channels !== "object")
280
+ return;
281
+ for (const [channelName, channelConfig] of Object.entries(channels)) {
282
+ if (!channelConfig || typeof channelConfig !== "object")
283
+ continue;
284
+ const ch = channelConfig;
285
+ const details = [];
286
+ if (ch.enabled === false)
287
+ details.push("disabled");
288
+ if (ch.dmPolicy)
289
+ details.push(`dmPolicy: ${ch.dmPolicy}`);
290
+ if (ch.groupPolicy)
291
+ details.push(`groupPolicy: ${ch.groupPolicy}`);
292
+ if (ch.streamMode)
293
+ details.push(`streamMode: ${ch.streamMode}`);
294
+ if (ch.selfChatMode !== undefined)
295
+ details.push(`selfChat: ${ch.selfChatMode}`);
296
+ if (ch.debounceMs !== undefined)
297
+ details.push(`debounce: ${ch.debounceMs}ms`);
298
+ if (ch.mediaMaxMb)
299
+ details.push(`mediaMax: ${ch.mediaMaxMb}MB`);
300
+ signals.push({
301
+ kind: "tooling",
302
+ frameworkId: `openclaw-channel-${channelName}`,
303
+ frameworkName: `OpenClaw Channel: ${channelName}`,
304
+ evidence: {
305
+ file: manifest.relativePath,
306
+ reason: `Channel '${channelName}' configured in openclaw.json`,
307
+ excerpt: details.length > 0 ? details.join("; ") : `channel ${channelName} configured`,
308
+ },
309
+ scope: { pathRoot: rootPath },
310
+ });
311
+ }
312
+ }
313
+ function extractTools(config, manifest, rootPath, signals) {
70
314
  const tools = config.tools;
71
- if (tools && typeof tools === "object") {
72
- for (const [toolName, toolConfig] of Object.entries(tools)) {
315
+ if (!tools || typeof tools !== "object")
316
+ return;
317
+ for (const [toolName, toolConfig] of Object.entries(tools)) {
318
+ if (!toolConfig || typeof toolConfig !== "object")
319
+ continue;
320
+ // Build a summary of the tool's sub-capabilities
321
+ const subCapabilities = [];
322
+ for (const [subName, subConfig] of Object.entries(toolConfig)) {
323
+ if (subConfig && typeof subConfig === "object") {
324
+ const sc = subConfig;
325
+ const enabled = sc.enabled !== false;
326
+ subCapabilities.push(`${subName}: ${enabled ? "enabled" : "disabled"}`);
327
+ }
328
+ }
329
+ signals.push({
330
+ kind: "tooling",
331
+ frameworkId: `openclaw-tool-${toolName}`,
332
+ frameworkName: `OpenClaw Tool: ${toolName}`,
333
+ evidence: {
334
+ file: manifest.relativePath,
335
+ reason: `Tool '${toolName}' configured in openclaw.json`,
336
+ excerpt: subCapabilities.length > 0 ? subCapabilities.join("; ") : `tool ${toolName} configured`,
337
+ },
338
+ scope: { pathRoot: rootPath },
339
+ });
340
+ }
341
+ }
342
+ function extractCommandsAndMessages(config, manifest, rootPath, signals) {
343
+ const commands = config.commands;
344
+ if (commands && typeof commands === "object") {
345
+ const details = [];
346
+ if (commands.native)
347
+ details.push(`native: ${commands.native}`);
348
+ if (commands.nativeSkills)
349
+ details.push(`nativeSkills: ${commands.nativeSkills}`);
350
+ if (details.length > 0) {
73
351
  signals.push({
74
352
  kind: "tooling",
75
- frameworkId: `openclaw-tool-${toolName}`,
76
- frameworkName: `OpenClaw Tool: ${toolName}`,
353
+ frameworkId: "openclaw-commands",
354
+ frameworkName: "OpenClaw Commands",
77
355
  evidence: {
78
356
  file: manifest.relativePath,
79
- reason: `Tool '${toolName}' configured in openclaw.json`,
80
- excerpt: JSON.stringify(toolConfig, null, 2).substring(0, 200),
357
+ reason: "Command configuration in openclaw.json",
358
+ excerpt: details.join("; "),
81
359
  },
82
360
  scope: { pathRoot: rootPath },
83
361
  });
84
362
  }
85
363
  }
86
- // Extract channel signals
87
- const channels = config.channels;
88
- if (channels && typeof channels === "object") {
89
- for (const [channelName] of Object.entries(channels)) {
364
+ const messages = config.messages;
365
+ if (messages && typeof messages === "object") {
366
+ const details = [];
367
+ if (messages.ackReactionScope)
368
+ details.push(`ackReactionScope: ${messages.ackReactionScope}`);
369
+ if (details.length > 0) {
90
370
  signals.push({
91
371
  kind: "tooling",
92
- frameworkId: `openclaw-channel-${channelName}`,
93
- frameworkName: `OpenClaw Channel: ${channelName}`,
372
+ frameworkId: "openclaw-messages",
373
+ frameworkName: "OpenClaw Messages",
94
374
  evidence: {
95
375
  file: manifest.relativePath,
96
- reason: `Channel '${channelName}' configured in openclaw.json`,
376
+ reason: "Message configuration in openclaw.json",
377
+ excerpt: details.join("; "),
97
378
  },
98
379
  scope: { pathRoot: rootPath },
99
380
  });
100
381
  }
101
382
  }
102
- return signals;
383
+ }
384
+ // ---------------------
385
+ // Workspace scanner
386
+ // ---------------------
387
+ function scanWorkspace(config, manifest, rootPath, signals) {
388
+ // Resolve workspace directory:
389
+ // 1. From agents.defaults.workspace in config
390
+ // 2. Fallback to sibling "workspace/" directory relative to the config file
391
+ const agents = config.agents;
392
+ const defaults = agents?.defaults;
393
+ const configuredWorkspace = defaults?.workspace ? String(defaults.workspace) : undefined;
394
+ const configDir = path.dirname(manifest.absolutePath);
395
+ let workspaceDir;
396
+ if (configuredWorkspace) {
397
+ // Could be absolute or relative — try both
398
+ const candidate = path.isAbsolute(configuredWorkspace)
399
+ ? configuredWorkspace
400
+ : path.resolve(configDir, configuredWorkspace);
401
+ if (existsAndIsDir(candidate)) {
402
+ workspaceDir = candidate;
403
+ }
404
+ }
405
+ if (!workspaceDir) {
406
+ // Fallback: sibling "workspace/" directory
407
+ const fallback = path.join(configDir, "workspace");
408
+ if (existsAndIsDir(fallback)) {
409
+ workspaceDir = fallback;
410
+ }
411
+ }
412
+ if (!workspaceDir)
413
+ return;
414
+ const workspaceRel = path.relative(path.dirname(manifest.absolutePath), workspaceDir);
415
+ // Scan workspace .md files (identity layer)
416
+ scanWorkspaceMdFiles(workspaceDir, workspaceRel, manifest, rootPath, signals);
417
+ // Scan skills
418
+ scanSkills(workspaceDir, workspaceRel, manifest, rootPath, signals);
419
+ // Scan subagents
420
+ scanSubagents(workspaceDir, workspaceRel, manifest, rootPath, signals);
421
+ }
422
+ function scanWorkspaceMdFiles(workspaceDir, workspaceRel, manifest, rootPath, signals) {
423
+ const WORKSPACE_FILES = {
424
+ "AGENTS.md": "Agent behavioral instructions and skill routing",
425
+ "IDENTITY.md": "Agent identity, personality, and avatar",
426
+ "HEARTBEAT.md": "Periodic awareness checklist (cron-like proactive checks)",
427
+ "MEMORY.md": "Long-term curated memory store",
428
+ "SOUL.md": "Agent personality and core values",
429
+ "USER.md": "User profile and preferences",
430
+ "TOOLS.md": "Tool configuration and capabilities",
431
+ "OPTIMIZATION.md": "Performance and cost optimization rules",
432
+ };
433
+ for (const [filename, description] of Object.entries(WORKSPACE_FILES)) {
434
+ const filePath = path.join(workspaceDir, filename);
435
+ if (!existsAndIsFile(filePath))
436
+ continue;
437
+ // For HEARTBEAT.md, check if it has actual tasks (non-empty, non-comment content)
438
+ if (filename === "HEARTBEAT.md") {
439
+ const heartbeatContent = safeReadFile(filePath);
440
+ const hasActiveTasks = heartbeatContent
441
+ ? heartbeatContent.split("\n").some((line) => line.trim() && !line.trim().startsWith("#"))
442
+ : false;
443
+ signals.push({
444
+ kind: "heartbeat",
445
+ frameworkId: "openclaw-heartbeat",
446
+ frameworkName: "OpenClaw Heartbeat",
447
+ evidence: {
448
+ file: path.join(workspaceRel, filename),
449
+ reason: hasActiveTasks
450
+ ? "HEARTBEAT.md found with active periodic tasks"
451
+ : "HEARTBEAT.md found but no active tasks configured",
452
+ excerpt: hasActiveTasks ? "Status: ACTIVE" : "Status: INACTIVE (empty/comments only)",
453
+ },
454
+ scope: { pathRoot: rootPath },
455
+ });
456
+ continue;
457
+ }
458
+ // For IDENTITY.md, extract the agent name if present
459
+ let excerpt = description;
460
+ if (filename === "IDENTITY.md") {
461
+ const identityContent = safeReadFile(filePath);
462
+ if (identityContent) {
463
+ const nameMatch = identityContent.match(/\*\*Name:\*\*\s*(.+)/);
464
+ if (nameMatch) {
465
+ excerpt = `Agent name: ${nameMatch[1].trim()} — ${description}`;
466
+ }
467
+ }
468
+ }
469
+ signals.push({
470
+ kind: "agent",
471
+ frameworkId: `openclaw-workspace-${filename.replace(".md", "").toLowerCase()}`,
472
+ frameworkName: `OpenClaw Workspace: ${filename}`,
473
+ evidence: {
474
+ file: path.join(workspaceRel, filename),
475
+ reason: `Workspace file ${filename} found`,
476
+ excerpt,
477
+ },
478
+ scope: { pathRoot: rootPath },
479
+ });
480
+ }
481
+ }
482
+ function scanSkills(workspaceDir, workspaceRel, manifest, rootPath, signals) {
483
+ const skillsDir = path.join(workspaceDir, "skills");
484
+ if (!existsAndIsDir(skillsDir))
485
+ return;
486
+ let entries;
487
+ try {
488
+ entries = fs.readdirSync(skillsDir);
489
+ }
490
+ catch {
491
+ return;
492
+ }
493
+ for (const entry of entries) {
494
+ if (!entry.endsWith(".md"))
495
+ continue;
496
+ if (entry === "SKILL_INDEX.md")
497
+ continue; // Index file, not a skill itself
498
+ const skillPath = path.join(skillsDir, entry);
499
+ if (!existsAndIsFile(skillPath))
500
+ continue;
501
+ // Read first meaningful line for description
502
+ const content = safeReadFile(skillPath);
503
+ let description = `Skill workflow: ${entry.replace(".md", "")}`;
504
+ if (content) {
505
+ const lines = content.split("\n").filter((l) => l.trim());
506
+ const firstLine = lines[0] || "";
507
+ if (firstLine.startsWith("#")) {
508
+ description = firstLine.replace(/^#+\s*/, "").trim();
509
+ }
510
+ }
511
+ const skillName = entry
512
+ .replace(".md", "")
513
+ .replace(/-/g, " ")
514
+ .replace(/\b\w/g, (c) => c.toUpperCase());
515
+ signals.push({
516
+ kind: "skill",
517
+ frameworkId: `openclaw-skill-${entry.replace(".md", "")}`,
518
+ frameworkName: `OpenClaw Skill: ${skillName}`,
519
+ evidence: {
520
+ file: path.join(workspaceRel, "skills", entry),
521
+ reason: description,
522
+ excerpt: `Skill file: ${entry}`,
523
+ },
524
+ scope: { pathRoot: rootPath },
525
+ });
526
+ }
527
+ }
528
+ function scanSubagents(workspaceDir, workspaceRel, manifest, rootPath, signals) {
529
+ const subagentsDir = path.join(workspaceDir, "subagents");
530
+ if (!existsAndIsDir(subagentsDir))
531
+ return;
532
+ let entries;
533
+ try {
534
+ entries = fs.readdirSync(subagentsDir);
535
+ }
536
+ catch {
537
+ return;
538
+ }
539
+ for (const entry of entries) {
540
+ const entryPath = path.join(subagentsDir, entry);
541
+ if (!existsAndIsDir(entryPath))
542
+ continue;
543
+ // List files in the subagent directory
544
+ let subFiles;
545
+ try {
546
+ subFiles = fs.readdirSync(entryPath).filter((f) => {
547
+ const fp = path.join(entryPath, f);
548
+ try {
549
+ return fs.statSync(fp).isFile() && !f.startsWith(".");
550
+ }
551
+ catch {
552
+ return false;
553
+ }
554
+ });
555
+ }
556
+ catch {
557
+ continue;
558
+ }
559
+ // Filter out __pycache__ etc
560
+ const meaningfulFiles = subFiles.filter((f) => !f.endsWith(".pyc") && f !== "__pycache__");
561
+ // Try to read protocol.md for description
562
+ let description = `Subagent: ${entry}`;
563
+ const protocolPath = path.join(entryPath, "protocol.md");
564
+ if (existsAndIsFile(protocolPath)) {
565
+ const content = safeReadFile(protocolPath);
566
+ if (content) {
567
+ const lines = content.split("\n").filter((l) => l.trim());
568
+ const firstLine = lines[0] || "";
569
+ if (firstLine.startsWith("#")) {
570
+ description = firstLine.replace(/^#+\s*/, "").trim();
571
+ }
572
+ }
573
+ }
574
+ const subagentName = entry.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
575
+ signals.push({
576
+ kind: "subagent",
577
+ frameworkId: `openclaw-subagent-${entry}`,
578
+ frameworkName: `OpenClaw Subagent: ${subagentName}`,
579
+ evidence: {
580
+ file: path.join(workspaceRel, "subagents", entry),
581
+ reason: description,
582
+ excerpt: meaningfulFiles.length > 0
583
+ ? `Files: ${meaningfulFiles.join(", ")}`
584
+ : "Empty subagent directory",
585
+ },
586
+ scope: { pathRoot: rootPath },
587
+ });
588
+ }
589
+ }
590
+ // ---------------------
591
+ // Helpers
592
+ // ---------------------
593
+ function existsAndIsDir(p) {
594
+ try {
595
+ return fs.statSync(p).isDirectory();
596
+ }
597
+ catch {
598
+ return false;
599
+ }
600
+ }
601
+ function existsAndIsFile(p) {
602
+ try {
603
+ return fs.statSync(p).isFile();
604
+ }
605
+ catch {
606
+ return false;
607
+ }
608
+ }
609
+ function safeReadFile(p) {
610
+ try {
611
+ return fs.readFileSync(p, "utf-8");
612
+ }
613
+ catch {
614
+ return null;
615
+ }
103
616
  }
@@ -13,7 +13,7 @@ export interface VersionInfo {
13
13
  notes?: string;
14
14
  }
15
15
  /** Signal kinds emitted by manifest parsers */
16
- export type SignalKind = "framework" | "version" | "orchestrator" | "entrypoint" | "tooling";
16
+ export type SignalKind = "framework" | "version" | "orchestrator" | "entrypoint" | "tooling" | "agent" | "subagent" | "heartbeat" | "cron" | "hook" | "skill" | "plugin";
17
17
  /** Evidence for a detected signal */
18
18
  export interface SignalEvidence {
19
19
  file: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thelogicatelier/sylva",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "Auto-generate AGENTS.md for your repository using Ax-LLM. Analyze the structural backbone, data flow, and day-to-day coding conventions natively.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {