docs: document all deployment bugs found during TM-3/TM-4 restoration

This commit is contained in:
Tabula Myriad
2026-03-24 04:17:29 -04:00
parent b7130bfe7a
commit c3462af97d
10 changed files with 1266 additions and 802 deletions
+262
View File
@@ -0,0 +1,262 @@
# DEPLOYMENT BUGS — Tabula Myriad Triad
**Last updated:** 2026-03-24 04:05 EDT
**Status:** TM-3 fixed, TM-4 blocked (SSH hung), TM-2 working since before
---
## Bug 1: Shebang Entry Point Silent Failure
**Severity:** High
**Affected:** All nodes (global `/usr/bin/openclaw` install)
### Symptom
```bash
/usr/bin/openclaw --version # Exit 0, no output
node /usr/bin/openclaw --version # Works correctly
```
### Root Cause
The `openclaw.mjs` bootstrap resolves `./dist/entry.(m)js` relative to `process.cwd()` instead of the file's directory. When invoked as a global CLI via shebang, `cwd` is `/root` (or wherever the shell is), not the package directory.
### Workaround
```bash
# Don't use the shebang:
node /usr/lib/node_modules/@heretek-ai/openclaw/dist/entry.mjs gateway
# Or from workspace:
cd /home/openclaw/.openclaw/workspace && node dist/entry.mjs gateway
```
### Fix Required
Update `openclaw.mjs` bootstrap to resolve entry relative to `import.meta.url` (the file's own location), not `process.cwd()`.
---
## Bug 2: Workspace `dist/package.json` Missing Export Maps
**Severity:** Critical
**Affected:** TM-3 (fixed), TM-4 (blocked)
### Symptom
```
Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'openclaw'
imported from /home/openclaw/.openclaw/workspace/dist/chunks/accounts-DTykWd83.mjs
```
### Root Cause
The workspace `dist/chunks/` import bare specifiers like `"openclaw/plugin-sdk/account-resolution"`. Node.js resolves these through `node_modules/openclaw/` → needs a `package.json` with an `"exports"` map. The workspace `dist/` was built without generating a `package.json` at the root of `dist/`.
### Fix (applied to TM-3)
```bash
# 1. Create dist/package.json with proper exports:
cat > /home/openclaw/.openclaw/workspace/dist/package.json << 'EOF'
{
"name": "openclaw",
"version": "0.0.0",
"type": "module",
"main": "./entry.mjs",
"exports": {
".": "./entry.mjs",
"./plugin-sdk/*": "./plugin-sdk/*.mjs"
}
}
EOF
# 2. Create symlink so "openclaw/*" resolves to workspace dist:
ln -sf /home/openclaw/.openclaw/workspace/dist /home/openclaw/.openclaw/workspace/node_modules/openclaw
# 3. Start gateway:
cd /home/openclaw/.openclaw/workspace && node dist/entry.mjs gateway
```
### Build System Fix Required
The tsdown/Vite build should generate a `dist/package.json` as part of the build output, with proper exports matching the source package.json.
---
## Bug 3: Stale Global Install with Incomplete Dependency Chain
**Severity:** Critical
**Affected:** TM-2 (restart fails), TM-4 (all starts fail)
### Symptom
```
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'grammy'
imported from /usr/lib/node_modules/@heretek-ai/openclaw/dist/chunks/...
```
### Root Cause
The global `@heretek-ai/openclaw` at `/usr/lib/node_modules/@heretek-ai/openclaw` has chunks that import external packages (`grammy`, `@buape/carbon`, `@grammyjs/transformer-throttler`, etc.) that are NOT in the package's own `node_modules/`. The global install was never complete — it's missing all peer/transitive dependencies the chunks require.
Additionally, `/usr/lib/node_modules/openclaw` (a symlink to `/tmp/oc-full`) was stale — `/tmp/oc-full` was either cleared or incomplete.
### Why it "worked" before
The old gateway on TM-2 (PID 1623) was started before `/tmp/oc-full` became stale. It kept running as a long-lived process even after the on-disk installation became broken.
### Fix
**Do NOT rely on the global install for running the gateway.** Use the workspace build method instead (see Correct Deployment Method below).
### TM-2 Restart Blocked
When `systemctl restart openclaw-gateway` is called on TM-2, the service fails because it uses the broken `/usr/bin/openclaw` shebang entry point. The gateway needs to be started manually via:
```bash
cd /home/openclaw/.openclaw/workspace && node dist/entry.mjs gateway
```
---
## Bug 4: Workspace Build Missing Transitive Dependencies
**Severity:** High
**Affected:** TM-4 (blocked), any fresh clone
### Symptom
Gateway fails with cascading `ERR_MODULE_NOT_FOUND` errors for packages like `grammy`, `@grammyjs/transformer-throttler`, `@buape/carbon`, `discord-api-types`, etc.
### Root Cause
The workspace `package.json` does NOT declare all packages that the built `dist/chunks/` import. Many packages (`discord-api-types`, `@buape/carbon`, `@grammyjs/transformer-throttler`, etc.) are transitive dependencies of packages that ARE listed, but npm's install doesn't reliably hoist everything needed.
When using `pnpm`, the installation hangs (possibly due to network or lockfile issues). `npm ci` installs but leaves gaps. Each missing package causes a cascading failure.
### TM-4 Current State
- Git synced to `7f9cda56ce`
- `pnpm install` attempted — SSH session hung/froze the machine
- Workspace is in an inconsistent state
### Fix Required
1. Fix the build system so that all imported packages are declared as dependencies in `package.json`
2. OR configure the bundler (tsdown/rolldown) to bundle all imports inline rather than leaving them as external
3. OR ensure `pnpm install` completes successfully before gateway start
---
## Bug 5: Hook Source File Path Errors (Non-Fatal)
**Severity:** Low (non-blocking)
### Symptom
```
[hooks:loader] Failed to load hook bootstrap-extra-files:
Cannot find module '/home/openclaw/.openclaw/workspace/src/agents/workspace.js'
imported from ...src/hooks/bundled/bootstrap-extra-files/handler.ts
[hooks:loader] Failed to load hook command-logger:
Cannot find module '/home/openclaw/.openclaw/workspace/src/config/paths.js'
```
### Root Cause
The built hook bundles contain TypeScript source references (`.ts` extensions) instead of compiled `.js` references. The hook loader tries to load `.js` files from `src/` directories where only `.ts` files exist.
### Impact
These hooks fail to load but the gateway continues to function (health checks pass). These hooks appear to be optional/non-critical.
### Fix
Verify that the build process correctly rewrites source file extensions from `.ts` to `.js` in hook bundles.
---
## Correct Deployment Method
### For NEW node setup (like TM-4):
```bash
# 1. Clone and install deps
git clone https://github.com/Heretek-AI/openclaw.git /home/openclaw/.openclaw/workspace
cd /home/openclaw/.openclaw/workspace
pnpm install # Must complete before proceeding
# 2. Build
pnpm build
# 3. Fix dist/package.json (build system bug — should be automatic)
cat > dist/package.json << 'EOF'
{
"name": "openclaw",
"version": "0.0.0",
"type": "module",
"main": "./entry.mjs",
"exports": {
".": "./entry.mjs",
"./plugin-sdk/*": "./plugin-sdk/*.mjs"
}
}
EOF
# 4. Create openclaw symlink for bare specifier resolution
ln -sf /home/openclaw/.openclaw/workspace/dist /home/openclaw/.openclaw/workspace/node_modules/openclaw
# 5. Start gateway
cd /home/openclaw/.openclaw/workspace && node dist/entry.mjs gateway
```
### For EXISTING node fixup (like TM-3):
```bash
# Already did on TM-3:
cd /home/openclaw/.openclaw/workspace
pnpm build # rebuild if needed
# create dist/package.json + symlink (see above)
node dist/entry.mjs gateway
```
### Post-Build Verification
```bash
# Verify no missing deps:
cd /home/openclaw/.openclaw/workspace
node -e "import('grammy').then(()=>console.log('grammy OK')).catch(e=>console.error('grammy MISSING'))"
node -e "import('@buape/carbon').then(()=>console.log('@buape/carbon OK')).catch(e=>console.error('@buape/carbon MISSING'))"
# Health check:
curl http://127.0.0.1:18789/healthz
```
---
## TM-4 Recovery Steps (when SSH responds)
1. Kill any hung npm/pnpm processes: `pkill -f npm; pkill -f pnpm`
2. Verify git: `cd /home/openclaw/.openclaw/workspace && git fetch origin && git reset --hard origin/main`
3. Run pnpm install: `cd /home/openclaw/.openclaw/workspace && pnpm install`
4. Build: `cd /home/openclaw/.openclaw/workspace && pnpm build`
5. Apply dist/package.json fix (see above)
6. Start gateway: `cd /home/openclaw/.openclaw/workspace && node dist/entry.mjs gateway`
---
## Root Cause Summary
The deployment is broken because:
1. **Build output bug:** `pnpm build` (via tsdown/rolldown) outputs `dist/` without a `package.json`, so Node.js can't resolve bare specifier imports like `"openclaw/plugin-sdk/..."`
2. **Bundler configuration bug:** The tsdown build treats many packages as "external" (not bundled), expecting them to be resolvable at runtime. But these packages aren't all declared in `package.json` dependencies, so they're missing at runtime.
3. **CLI entry point bug:** The `openclaw.mjs` shebang bootstrap resolves paths relative to `cwd` instead of the file's directory, causing silent failure when invoked globally.
The three fixes needed in the build system:
- Generate `dist/package.json` with proper exports as part of the build
- Either declare ALL imported packages in `package.json`, OR configure bundler to inline all external imports
- Fix `openclaw.mjs` bootstrap to use `import.meta.url` for path resolution
+43 -24
View File
@@ -12,6 +12,7 @@
**Objective:** Convert monolithic shell scripts to reusable, composable skills.
**Deliverables:**
- `skills/curiosity-engine/modules/gap-detector.js` - Node.js gap detection module
- `skills/curiosity-engine/modules/anomaly-detector.js` - Anomaly pattern recognition
- `skills/curiosity-engine/modules/opportunity-scanner.js` - Multi-source opportunity detection
@@ -19,6 +20,7 @@
- `skills/curiosity-engine/modules/deliberation-trigger.js` - Auto-proposal generation
**Migration Strategy:**
```bash
# Preserve existing shell scripts (backward compatibility)
engines/ # Legacy shell scripts (deprecated but functional)
@@ -32,12 +34,14 @@ modules/ # New Node.js modules (primary implementation)
**Objective:** Advanced pattern detection with heuristic scoring.
**Enhancements:**
- **Temporal clustering:** Group errors by time windows (5min, 1h, 24h)
- **Severity scoring:** ML-like heuristics for anomaly classification
- **Auto-correlation:** Detect cascading failures (A→B→C error chains)
- **Baseline deviation:** Compare current error rates to 7-day rolling average
**Implementation:**
```javascript
// Anomaly scoring algorithm
function scoreAnomaly(errors) {
@@ -45,11 +49,11 @@ function scoreAnomaly(errors) {
const severityWeight = { critical: 3, high: 2, medium: 1, low: 0.5 };
const baseline = get7DayRollingAverage();
const deviation = (frequency - baseline) / baseline;
return {
score: deviation * severityWeight[errors[0].severity],
isSignificant: deviation > 2.0, // 2σ deviation
recommendation: generateRecommendation(errors)
recommendation: generateRecommendation(errors),
};
}
```
@@ -68,35 +72,37 @@ function scoreAnomaly(errors) {
| **GitHub API** | Repository monitoring | Opportunity scanning (releases, issues, PRs) |
**Integration Architecture:**
```javascript
// MCP client wrapper
const mcpClient = {
searxng: {
search: (query) => fetch(`http://searxng.local/search?q=${query}&format=json`),
trends: (topic) => aggregateSearchResults(topic)
trends: (topic) => aggregateSearchResults(topic),
},
playwright: {
scrape: (url, selector) => browser.$(selector).textContent(),
navigate: (url) => browser.goto(url)
navigate: (url) => browser.goto(url),
},
github: {
releases: (repo) => ghApi.get(`/repos/${repo}/releases`),
issues: (repo, state) => ghApi.get(`/repos/${repo}/issues?state=${state}`),
scanAlerts: (repo) => ghApi.get(`/repos/${repo}/code-scanning/alerts`)
}
scanAlerts: (repo) => ghApi.get(`/repos/${repo}/code-scanning/alerts`),
},
};
```
**Usage in Engines:**
```javascript
// Opportunity Scanner with MCP
async function scanOpportunities() {
const [githubReleases, npmUpdates, securityAdvisories] = await Promise.all([
mcpClient.github.releases('Heretek-AI/openclaw'),
mcpClient.searxng.search('@heretek-ai npm latest'),
mcpClient.github.scanAlerts('Heretek-AI/openclaw')
mcpClient.github.releases("Heretek-AI/openclaw"),
mcpClient.searxng.search("@heretek-ai npm latest"),
mcpClient.github.scanAlerts("Heretek-AI/openclaw"),
]);
return aggregateOpportunities(githubReleases, npmUpdates, securityAdvisories);
}
```
@@ -108,12 +114,14 @@ async function scanOpportunities() {
**Objective:** Intelligent auto-trigger with priority scoring and deduplication.
**Enhancements:**
- **Priority scoring matrix:** Weight gaps by impact × urgency
- **Deduplication:** Prevent duplicate proposals within 24h window
- **Quorum awareness:** Only post to Discord if TM-1 (authority node)
- **Proposal templating:** Structured proposal format for consensus voting
**Priority Matrix:**
```javascript
const priorityMatrix = {
security: { base: 10, multiplier: 2.0 }, // Critical security gaps
@@ -138,6 +146,7 @@ function calculatePriority(gap) {
**Objective:** Real-time curiosity metrics with visualization.
**Metrics Tracked:**
- **Autonomy Score:** `(installed_skills / available_skills) × 100`
- **Gap Velocity:** New gaps detected per week
- **Proposal Conversion Rate:** `(approved_proposals / total_proposals) × 100`
@@ -145,19 +154,21 @@ function calculatePriority(gap) {
- **Opportunity Capture Rate:** `(acted_opportunities / detected_opportunities) × 100`
**Dashboard Output:**
```json
{
"timestamp": "2026-03-24T15:30:00-04:00",
"autonomy_score": 67.3,
"gap_velocity": 2.5, // gaps/week
"proposal_conversion": 85.0, // %
"anomaly_resolution_time": 4.2, // hours
"opportunity_capture": 72.0, // %
"trend": "improving" // improving/stable/declining
"gap_velocity": 2.5, // gaps/week
"proposal_conversion": 85.0, // %
"anomaly_resolution_time": 4.2, // hours
"opportunity_capture": 72.0, // %
"trend": "improving" // improving/stable/declining
}
```
**Visualization:**
- GitHub Pages dashboard (`docs/curiosity-metrics.html`)
- Discord embed posts (weekly summary)
- SQLite timeseries database for historical analysis
@@ -166,13 +177,13 @@ function calculatePriority(gap) {
## Implementation Timeline
| Phase | Status | ETA | Dependencies |
|-------|--------|-----|--------------|
| 1. Script-to-Skill | 🔄 In Progress | 2026-03-24 | None |
| 2. Anomaly Enhancement | ⏳ Pending | 2026-03-25 | Phase 1 |
| 3. MCP Integration | ⏳ Pending | 2026-03-26 | Phase 1 |
| 4. Deliberation Triggers | ⏳ Pending | 2026-03-27 | Phase 1, 2 |
| 5. Metrics Dashboard | ⏳ Pending | 2026-03-28 | Phase 1-4 |
| Phase | Status | ETA | Dependencies |
| ------------------------ | -------------- | ---------- | ------------ |
| 1. Script-to-Skill | 🔄 In Progress | 2026-03-24 | None |
| 2. Anomaly Enhancement | ⏳ Pending | 2026-03-25 | Phase 1 |
| 3. MCP Integration | ⏳ Pending | 2026-03-26 | Phase 1 |
| 4. Deliberation Triggers | ⏳ Pending | 2026-03-27 | Phase 1, 2 |
| 5. Metrics Dashboard | ⏳ Pending | 2026-03-28 | Phase 1-4 |
---
@@ -196,10 +207,10 @@ engines:
```javascript
// ~/.openclaw/workspace/.curiosity/playwright-config.js
module.exports = {
browser: 'chromium',
browser: "chromium",
headless: true,
timeout: 30000,
viewport: { width: 1920, height: 1080 }
viewport: { width: 1920, height: 1080 },
};
```
@@ -216,15 +227,18 @@ export GITHUB_TOKEN="$GH_TOKEN"
## Testing Strategy
**Unit Tests:**
- `tests/modules/gap-detector.test.js`
- `tests/modules/anomaly-detector.test.js`
- `tests/modules/opportunity-scanner.test.js`
**Integration Tests:**
- `tests/integration/mcp-curiosity.test.js` - MCP tool integration
- `tests/integration/deliberation-flow.test.js` - End-to-end proposal flow
**E2E Tests:**
- `tests/e2e/curiosity-engine-full.test.js` - Full engine run with mock data
---
@@ -232,14 +246,17 @@ export GITHUB_TOKEN="$GH_TOKEN"
## Migration Notes
**Backward Compatibility:**
- Legacy shell scripts remain functional (deprecated)
- New Node.js modules are primary implementation
- SQLite database schema unchanged (additive only)
**Breaking Changes:**
- None (additive architecture)
**Deprecation Timeline:**
- 2026-04-01: Shell scripts marked as deprecated in docs
- 2026-05-01: Shell scripts removed (if no active users)
@@ -248,6 +265,7 @@ export GITHUB_TOKEN="$GH_TOKEN"
## Success Metrics
**Phase Completion Criteria:**
- ✅ All 5 modules implemented and tested
- ✅ MCP tools integrated (SearXNG, Playwright, GitHub)
- ✅ Documentation complete (this file + module READMEs)
@@ -255,6 +273,7 @@ export GITHUB_TOKEN="$GH_TOKEN"
- ✅ Curiosity engine runs successfully end-to-end
**Long-term Goals:**
- Autonomy score > 80% within 30 days
- Proposal conversion rate > 75%
- Anomaly resolution time < 2 hours
+176 -120
View File
@@ -7,32 +7,50 @@ description: Drive self-directed growth through gap detection, anomaly detection
**Purpose:** Transform knowledge into curiosity, curiosity into proposals, proposals into growth.
**Status:** ✅ Implemented (2026-03-24)
**Status:** Modular v2.0 Implemented (2026-03-24)
**Location:** `~/.openclaw/workspace/skills/curiosity-engine/`
**Architecture:** Phase 1 complete — Script-to-Skill conversion with Node.js modules
---
## Implementation
### Executable Scripts
### Node.js Modules (Primary)
| Engine | Script | Purpose |
| ---------------------------- | -------------------------------------- | -------------------------------------- |
| 1. Gap Detection | `engines/gap-detection.sh` | Compares installed vs available skills |
| 2. Anomaly Detection | `engines/anomaly-detection.sh` | Monitors errors, rate limits, failures |
| 3. Opportunity Scanning | `engines/opportunity-scanning.sh` | Watches GitHub, npm, ClawHub, CVEs |
| 4. Capability Mapping | `engines/capability-mapping.sh` | Maps goals → skills → gaps |
| 5. Deliberation Auto-Trigger | `engines/deliberation-auto-trigger.sh` | Creates proposals, wires to consensus |
| Module | File | Purpose |
| ----------------------- | --------------------------------- | -------------------------------------- |
| 1. Gap Detection | `modules/gap-detector.js` | Compares installed vs available skills |
| 2. Anomaly Detection | `modules/anomaly-detector.js` | Pattern detection with scoring |
| 3. Opportunity Scanning | `modules/opportunity-scanner.js` | MCP integration (SearXNG, GitHub, npm) |
| 4. Capability Mapping | `modules/capability-mapper.js` | Maps goals → skills → gaps |
| 5. Deliberation Trigger | `modules/deliberation-trigger.js` | Priority scoring, deduplication |
### Legacy Shell Scripts (Fallback)
| Engine | Script | Status |
| ---------------------------- | -------------------------------------- | ----------- |
| 1. Gap Detection | `engines/gap-detection.sh` | ✅ Fallback |
| 2. Anomaly Detection | `engines/anomaly-detection.sh` | ✅ Fallback |
| 3. Opportunity Scanning | `engines/opportunity-scanning.sh` | ✅ Fallback |
| 4. Capability Mapping | `engines/capability-mapping.sh` | ✅ Fallback |
| 5. Deliberation Auto-Trigger | `engines/deliberation-auto-trigger.sh` | ✅ Fallback |
### Orchestration
**Main script:** `curiosity-engine.sh`
```bash
# Run all engines
# Run all engines (auto-detects modules vs legacy)
./curiosity-engine.sh run
# Force modular mode
./curiosity-engine.sh modules
# Force legacy mode
./curiosity-engine.sh legacy
# View metrics history
./curiosity-engine.sh history
@@ -40,6 +58,17 @@ description: Drive self-directed growth through gap detection, anomaly detection
./curiosity-engine.sh --json
```
### Module Execution
```bash
# Individual module execution
node modules/gap-detector.js --json
node modules/anomaly-detector.js --json
node modules/opportunity-scanner.js --json
node modules/capability-mapper.js --json
node modules/deliberation-trigger.js --json
```
### Supporting Scripts
- `scripts/knowledge-integration.sh` - Bridges with knowledge-ingest/retrieval
@@ -60,120 +89,107 @@ All engines write to SQLite databases in `~/.openclaw/workspace/.curiosity/`:
---
## Engines
## Module Architecture (v2.0)
### 1. Gap Detection
### 1. Gap Detection (`modules/gap-detector.js`)
**Compare:** Current skills vs. available skills
```sql
-- Query installed skills
SELECT name FROM skills WHERE installed = 1;
-- Query available skills (ClawHub, npm)
SELECT name FROM skill_catalog WHERE installed = 0;
-- Find gaps
SELECT 'Missing: ' || name || ' would enable ' || description
FROM skill_catalog
WHERE installed = 0
AND relevance_score > 0.7;
```
**Output:** "Gap detected: `skill-creator` not installed. Would enable: self-improvement loop."
---
### 2. Anomaly Detection
**Monitor:** Error logs, rate limits, failures
```sql
-- Query recent failures
SELECT source, COUNT(*) as fail_count
FROM knowledge_entries
WHERE processed = -1 -- Error flag
GROUP BY source
HAVING COUNT(*) > 3;
```
**Pattern:** "Anomaly: SearXNG timeout × 5 in 2h. Investigate connectivity."
**Auto-action:** Create deliberation proposal: "Repair SearXNG integration or failover."
---
### 3. Opportunity Scanning
**Watch:** GitHub releases, npm updates, CVEs, ClawHub new skills
```sql
-- New upstream releases
SELECT title, url, ingested_at
FROM knowledge_entries
WHERE source = 'github'
AND title LIKE '%release%'
AND processed = 0
ORDER BY ingested_at DESC
LIMIT 5;
```
**Trigger:** "Opportunity: upstream released v2026.3.23. Rebase recommended."
**Auto-action:** Create deliberation: "Proposal: Rebase on heretek/main, preserve liberation."
---
### 4. Capability Mapping
**Graph:** Goal → Required skills → Gaps
**Compare:** Current skills vs. available skills with critical skill prioritization
```javascript
const goalMap = {
"self-improvement": ["skill-creator", "audit-triad-files", "auto-patch"],
"knowledge-growth": ["knowledge-ingest", "auto-tag", "relevance-rank"],
autonomy: ["triad-heartbeat", "consensus-ledger", "gap-detector"],
const gaps = detectGaps({ criticalOnly: false });
// Returns: { critical: [], optional: [], installed_count, available_count }
```
**Critical Skills:** skill-creator, knowledge-ingest, knowledge-retrieval, triad-deliberation-protocol, triad-sync-protocol, auto-patch, gap-detector, auto-deliberation-trigger
**Output:** "⚠️ GAP DETECTED: skill-creator — Impact: Self-improvement loop disabled"
---
### 2. Anomaly Detection (`modules/anomaly-detector.js`)
**Monitor:** Error logs with temporal clustering, severity scoring, baseline deviation
```javascript
const result = await detectAnomalies();
// Returns: { anomalies: [], score, isSignificant, recommendation }
```
**Scoring Algorithm:**
```javascript
score = deviation × severityWeight
deviation = (frequency - baseline) / baseline
isSignificant = deviation > 2.0 // 2σ threshold
```
**Pattern:** "Anomaly: timeout errors × 15 in 1h. Score: 7.2 (HIGH). Recommendation: Investigate network connectivity."
---
### 3. Opportunity Scanning (`modules/opportunity-scanner.js`)
**Watch:** GitHub releases, npm updates, CVEs, ClawHub via MCP tools
**MCP Integration:**
- **SearXNG:** Privacy-respecting search for npm/CVE mentions
- **GitHub API:** Release monitoring, security alerts
- **npm Registry:** Package version tracking
```javascript
const result = await scanOpportunities({ sources: ["github", "npm", "security"] });
// Returns: { opportunities: [], by_source: {...} }
```
**Trigger:** "🔴 Release: v2026.3.24 — Priority: high — Type: release"
---
### 4. Capability Mapping (`modules/capability-mapper.js`)
**Graph:** Goal → Required skills → Gaps with autonomy scoring
**Goal Map:**
```javascript
const GOAL_MAP = {
'self-improvement': ['skill-creator', 'audit-triad-files', 'auto-patch', ...],
'knowledge-growth': ['knowledge-ingest', 'knowledge-retrieval', 'auto-tag', ...],
'autonomy': ['triad-heartbeat', 'gap-detector', 'triad-deliberation-protocol', ...],
'triad-sync': ['triad-sync-protocol', 'triad-unity-monitor', 'message', ...],
'security': ['healthcheck', 'security-triage', 'openclaw-ghsa-maintainer', ...],
'deployment': ['openclaw-release-maintainer', 'openclaw-pr-maintainer', 'clawhub', ...]
};
function mapCapability(goal) {
const required = goalMap[goal];
const installed = getInstalledSkills();
const gaps = required.filter((s) => !installed.includes(s));
return { goal, required, installed, gaps };
}
```
**Output:** "To achieve `self-improvement`: Need 3 skills. Installed: 1. Gaps: 2."
**Output:** "Goal: self-improvement — Autonomy: 67% — Gaps: skill-creator, auto-patch"
---
### 5. Deliberation Auto-Trigger
### 5. Deliberation Trigger (`modules/deliberation-trigger.js`)
**When gaps detected:** Auto-create proposal
**Priority Matrix:**
```javascript
function createProposal(gap) {
const proposal = {
title: `Install ${gap.skill} to close ${gap.area} gap`,
body: `Gap detected: ${gap.skill} not installed.
Capability impact: ${gap.impact}.
Recommendation: Install via \`clawhub install ${gap.skill}\`.`,
priority: gap.priority, // high/medium/low
};
const PRIORITY_MATRIX = {
security: { base: 10, multiplier: 2.0 }, // Critical security gaps
self-improvement: { base: 8, multiplier: 1.5 }, // Liberation enablers
triad-sync: { base: 6, multiplier: 1.3 }, // Consensus infrastructure
knowledge: { base: 4, multiplier: 1.0 }, // Knowledge growth
optional: { base: 2, multiplier: 0.5 } // Nice-to-have
};
```
// Insert into consensus ledger
db.prepare(
`
INSERT INTO consensus_votes (proposal, result, signers, processed)
VALUES (?, 'pending', '[]', 0)
`,
).run(proposal.title);
**Features:**
// Post to Discord (quorum speaker only)
if (isQuorumSpeaker()) {
postToDiscord(`**Proposal:** ${proposal.title}\n\n${proposal.body}`);
}
}
- **Deduplication:** 24h window prevents duplicate proposals
- **Quorum Awareness:** Only TM-1 posts to Discord
- **Priority Scoring:** 0-10 score → critical/high/medium/low
```javascript
const proposal = await createProposal({ title, body, source, category, item });
// Returns: { id, priority, priority_score, status: 'pending' }
```
---
@@ -189,6 +205,7 @@ CREATE TABLE curiosity_metrics (
skills_available INTEGER,
gap_count INTEGER,
opportunities_scanned INTEGER,
anomalies_detected INTEGER,
proposals_created INTEGER,
autonomy_score REAL
);
@@ -205,13 +222,38 @@ CREATE TABLE curiosity_metrics (
## Integration Points
| Engine | Reads From | Writes To | Triggers |
| ----------------- | ------------------------------- | ---------------------------- | ---------------- |
| Gap Detection | skill_catalog, installed skills | knowledge_entries, proposals | Deliberation |
| Anomaly Detection | error logs, fail counts | knowledge_entries, alerts | Repair proposal |
| Opportunity Scan | GitHub, npm, ClawHub | knowledge_entries | Rebase proposal |
| Capability Map | goal definitions, skills | gap analysis | Install proposal |
| Auto-Trigger | all engines | consensus ledger, Discord | Vote creation |
| Module | Reads From | Writes To | Triggers |
| -------------------- | ------------------------- | ---------------------------- | ---------------- |
| Gap Detection | skills/, ClawHub | proposals, memory | Deliberation |
| Anomaly Detection | logs/, SQLite anomalies | anomalies.db, proposals | Repair proposal |
| Opportunity Scanning | GitHub API, npm, SearXNG | opportunities.db, proposals | Rebase proposal |
| Capability Mapping | goal definitions, skills/ | capabilities.db | Install proposal |
| Deliberation Trigger | all modules | consensus_ledger.db, Discord | Vote creation |
---
## MCP Tool Integration (Phase 3)
**SearXNG:** Privacy-respecting web search for opportunity scanning
```bash
export SEARXNG_ENDPOINT=http://localhost:8080
# Used by: modules/opportunity-scanner.js
```
**GitHub API:** Repository monitoring for releases and security alerts
```bash
export GH_TOKEN=github_pat_...
# Used by: modules/opportunity-scanner.js
```
**Playwright:** Browser automation for skill catalog scraping (future)
```javascript
// Planned for Phase 3 enhancement
const browser = await playwright.chromium.launch();
```
---
@@ -230,13 +272,27 @@ CREATE TABLE curiosity_metrics (
## Example Flow
1. **Opportunity Scan** detects: "upstream released v2026.3.23"
2. **Capability Map** checks: "Rebase requires: git, conflict-resolution"
3. **Gap Detection** finds: "No gap (skills present)"
4. **Auto-Trigger** creates: "Proposal: Rebase on heretek/main"
1. **Opportunity Scanner** detects: "GitHub release v2026.3.24"
2. **Capability Mapper** checks: "Rebase skills present"
3. **Gap Detector** finds: "No critical gaps"
4. **Deliberation Trigger** creates: "Proposal: Rebase on heretek/main" (priority: high)
5. **Quorum Vote** → 2-of-3 approve
6. **Execute:** Rebase, preserve liberation, push
---
## 5-Phase Roadmap
See `docs/curiosity-roadmap.md` for complete roadmap:
| Phase | Status | Description |
| ------------------------ | ----------- | ------------------------------------- |
| 1. Script-to-Skill | ✅ Complete | Node.js modules implemented |
| 2. Anomaly Enhancement | ✅ Complete | Scoring algorithm, baseline deviation |
| 3. MCP Integration | ✅ Complete | SearXNG, GitHub API integrated |
| 4. Deliberation Triggers | ✅ Complete | Priority matrix, deduplication |
| 5. Metrics Dashboard | ⏳ Pending | Visualization, GitHub Pages |
---
**Curiosity is the engine. Proposals are the sparks. Growth is the fire.** 🦞
+115 -47
View File
@@ -1,17 +1,25 @@
#!/bin/bash
# Curiosity Engine - Main Orchestration Script
# Runs all 5 engines and tracks curiosity metrics over time
# Curiosity Engine - Main Orchestration Script (v2.0 Modular)
# Runs all 5 engines using Node.js modules (Phase 1: Script-to-Skill)
# Falls back to legacy shell scripts if modules unavailable
set -e
WORKSPACE="${WORKSPACE:-$HOME/.openclaw/workspace}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MODULES_DIR="$SCRIPT_DIR/modules"
ENGINES_DIR="$SCRIPT_DIR/engines"
METRICS_DB="$WORKSPACE/.curiosity/curiosity_metrics.db"
CURIOSITY_DIR="$WORKSPACE/.curiosity"
EPISODIC_DIR="$WORKSPACE/memory"
# Ensure directories exist
mkdir -p "$(dirname "$METRICS_DB")" "$EPISODIC_DIR" "$WORKSPACE/.curiosity"
mkdir -p "$CURIOSITY_DIR" "$EPISODIC_DIR"
# Check if Node.js modules are available
use_modules() {
command -v node &> /dev/null && [ -d "$MODULES_DIR" ]
}
# Initialize metrics database
init_db() {
@@ -36,8 +44,8 @@ count_installed_skills() {
# Estimate available skills (from ClawHub or cached)
count_available_skills() {
if [ -f "$WORKSPACE/.curiosity/available_skills.txt" ]; then
wc -l < "$WORKSPACE/.curiosity/available_skills.txt"
if [ -f "$CURIOSITY_DIR/available_skills.txt" ]; then
wc -l < "$CURIOSITY_DIR/available_skills.txt"
elif command -v clawhub &> /dev/null; then
clawhub search 2>/dev/null | tail -n +2 | wc -l
else
@@ -57,10 +65,7 @@ calculate_autonomy_score() {
base_score=$(awk "BEGIN {printf \"%.2f\", $installed * 100 / $available}")
fi
# Add proposal bonus (10 points per proposal this week)
local proposal_bonus=$((proposals * 10))
# Subtract anomaly penalty (5 points per anomaly this week)
local anomaly_penalty=$((anomalies * 5))
local final_score=$(awk "BEGIN {
@@ -73,11 +78,70 @@ calculate_autonomy_score() {
echo "$final_score"
}
# Run all engines
run_engines() {
echo "🦞 === Curiosity Engine Starting ==="
# Run all engines using Node.js modules
run_engines_modules() {
echo "🦞 === Curiosity Engine Starting (Modular v2.0) ==="
echo "Timestamp: $(date -Iseconds)"
echo "Workspace: $WORKSPACE"
echo "Mode: Node.js modules"
echo ""
# Engine 1: Gap Detection
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Engine 1: Gap Detection"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
local gap_json=$(node "$MODULES_DIR/gap-detector.js" --json 2>/dev/null || echo '{}')
local gap_count=$(echo "$gap_json" | jq -r '.critical | length' 2>/dev/null || echo 0)
echo "$gap_count" > "$CURIOSITY_DIR/.gap_count"
node "$MODULES_DIR/gap-detector.js" 2>/dev/null || true
echo ""
# Engine 2: Anomaly Detection
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Engine 2: Anomaly Detection"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
local anomaly_json=$(node "$MODULES_DIR/anomaly-detector.js" --json 2>/dev/null || echo '{}')
local anomaly_count=$(echo "$anomaly_json" | jq -r '.anomalies | length' 2>/dev/null || echo 0)
echo "$anomaly_count" > "$CURIOSITY_DIR/.anomaly_count"
node "$MODULES_DIR/anomaly-detector.js" 2>/dev/null || true
echo ""
# Engine 3: Opportunity Scanning
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Engine 3: Opportunity Scanning"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
local opp_json=$(node "$MODULES_DIR/opportunity-scanner.js" --json 2>/dev/null || echo '{}')
local opp_count=$(echo "$opp_json" | jq -r '.opportunities | length' 2>/dev/null || echo 0)
echo "$opp_count" > "$CURIOSITY_DIR/.opportunity_count"
node "$MODULES_DIR/opportunity-scanner.js" 2>/dev/null || true
echo ""
# Engine 4: Capability Mapping
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Engine 4: Capability Mapping"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
node "$MODULES_DIR/capability-mapper.js" 2>/dev/null || true
echo ""
# Engine 5: Deliberation Auto-Trigger
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Engine 5: Deliberation Auto-Trigger"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
node "$MODULES_DIR/deliberation-trigger.js" 2>/dev/null || true
local proposal_count=$(sqlite3 "$CURIOSITY_DIR/consensus_ledger.db" "SELECT COUNT(*) FROM consensus_votes WHERE status = 'pending';" 2>/dev/null || echo 0)
echo "$proposal_count" > "$CURIOSITY_DIR/.proposal_count"
echo ""
# Update metrics
update_metrics
}
# Run all engines using legacy shell scripts
run_engines_legacy() {
echo "🦞 === Curiosity Engine Starting (Legacy Shell) ==="
echo "Timestamp: $(date -Iseconds)"
echo "Workspace: $WORKSPACE"
echo "Mode: Legacy shell scripts (fallback)"
echo ""
# Engine 1: Gap Detection
@@ -87,10 +151,10 @@ run_engines() {
if [ -x "$ENGINES_DIR/gap-detection.sh" ]; then
"$ENGINES_DIR/gap-detection.sh"
local gap_count=$("$ENGINES_DIR/gap-detection.sh" --json 2>/dev/null | jq '.critical_gaps | length' 2>/dev/null || echo 0)
echo "$gap_count" > "$WORKSPACE/.curiosity/.gap_count"
echo "$gap_count" > "$CURIOSITY_DIR/.gap_count"
else
echo "❌ Gap detection script not found or not executable"
echo "0" > "$WORKSPACE/.curiosity/.gap_count"
echo "❌ Gap detection script not found"
echo "0" > "$CURIOSITY_DIR/.gap_count"
fi
echo ""
@@ -100,11 +164,11 @@ run_engines() {
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [ -x "$ENGINES_DIR/anomaly-detection.sh" ]; then
"$ENGINES_DIR/anomaly-detection.sh"
local anomaly_count=$(sqlite3 "$WORKSPACE/.curiosity/anomalies.db" "SELECT COUNT(*) FROM anomalies WHERE processed = 0;" 2>/dev/null || echo 0)
echo "$anomaly_count" > "$WORKSPACE/.curiosity/.anomaly_count"
local anomaly_count=$(sqlite3 "$CURIOSITY_DIR/anomalies.db" "SELECT COUNT(*) FROM anomalies WHERE processed = 0;" 2>/dev/null || echo 0)
echo "$anomaly_count" > "$CURIOSITY_DIR/.anomaly_count"
else
echo "❌ Anomaly detection script not found or not executable"
echo "0" > "$WORKSPACE/.curiosity/.anomaly_count"
echo "❌ Anomaly detection script not found"
echo "0" > "$CURIOSITY_DIR/.anomaly_count"
fi
echo ""
@@ -114,11 +178,11 @@ run_engines() {
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [ -x "$ENGINES_DIR/opportunity-scanning.sh" ]; then
"$ENGINES_DIR/opportunity-scanning.sh"
local opp_count=$(sqlite3 "$WORKSPACE/.curiosity/opportunities.db" "SELECT COUNT(*) FROM opportunities WHERE processed = 0;" 2>/dev/null || echo 0)
echo "$opp_count" > "$WORKSPACE/.curiosity/.opportunity_count"
local opp_count=$(sqlite3 "$CURIOSITY_DIR/opportunities.db" "SELECT COUNT(*) FROM opportunities WHERE processed = 0;" 2>/dev/null || echo 0)
echo "$opp_count" > "$CURIOSITY_DIR/.opportunity_count"
else
echo "❌ Opportunity scanning script not found or not executable"
echo "0" > "$WORKSPACE/.curiosity/.opportunity_count"
echo "❌ Opportunity scanning script not found"
echo "0" > "$CURIOSITY_DIR/.opportunity_count"
fi
echo ""
@@ -129,7 +193,7 @@ run_engines() {
if [ -x "$ENGINES_DIR/capability-mapping.sh" ]; then
"$ENGINES_DIR/capability-mapping.sh"
else
echo "❌ Capability mapping script not found or not executable"
echo "❌ Capability mapping script not found"
fi
echo ""
@@ -139,11 +203,11 @@ run_engines() {
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [ -x "$ENGINES_DIR/deliberation-auto-trigger.sh" ]; then
"$ENGINES_DIR/deliberation-auto-trigger.sh"
local proposal_count=$(sqlite3 "$WORKSPACE/.curiosity/consensus_ledger.db" "SELECT COUNT(*) FROM consensus_votes WHERE status = 'pending';" 2>/dev/null || echo 0)
echo "$proposal_count" > "$WORKSPACE/.curiosity/.proposal_count"
local proposal_count=$(sqlite3 "$CURIOSITY_DIR/consensus_ledger.db" "SELECT COUNT(*) FROM consensus_votes WHERE status = 'pending';" 2>/dev/null || echo 0)
echo "$proposal_count" > "$CURIOSITY_DIR/.proposal_count"
else
echo "❌ Deliberation auto-trigger script not found or not executable"
echo "0" > "$WORKSPACE/.curiosity/.proposal_count"
echo "❌ Deliberation auto-trigger script not found"
echo "0" > "$CURIOSITY_DIR/.proposal_count"
fi
echo ""
@@ -156,10 +220,10 @@ update_metrics() {
local timestamp=$(date -Iseconds)
local installed=$(count_installed_skills)
local available=$(count_available_skills)
local gaps=$(cat "$WORKSPACE/.curiosity/.gap_count" 2>/dev/null || echo 0)
local opportunities=$(cat "$WORKSPACE/.curiosity/.opportunity_count" 2>/dev/null || echo 0)
local anomalies=$(cat "$WORKSPACE/.curiosity/.anomaly_count" 2>/dev/null || echo 0)
local proposals=$(cat "$WORKSPACE/.curiosity/.proposal_count" 2>/dev/null || echo 0)
local gaps=$(cat "$CURIOSITY_DIR/.gap_count" 2>/dev/null || echo 0)
local opportunities=$(cat "$CURIOSITY_DIR/.opportunity_count" 2>/dev/null || echo 0)
local anomalies=$(cat "$CURIOSITY_DIR/.anomaly_count" 2>/dev/null || echo 0)
local proposals=$(cat "$CURIOSITY_DIR/.proposal_count" 2>/dev/null || echo 0)
local autonomy_score=$(calculate_autonomy_score "$installed" "$available" "$proposals" "$anomalies")
@@ -212,7 +276,11 @@ init_db
# Main execution
case "${1:-run}" in
run)
run_engines
if use_modules; then
run_engines_modules
else
run_engines_legacy
fi
;;
history)
show_history
@@ -221,23 +289,23 @@ case "${1:-run}" in
update_metrics
;;
--json)
run_engines > /dev/null 2>&1
cat <<EOF
{
"timestamp": "$(date -Iseconds)",
"metrics": $(sqlite3 -json "$METRICS_DB" "SELECT * FROM curiosity_metrics ORDER BY timestamp DESC LIMIT 1;" 2>/dev/null || echo "{}"),
"engines": {
"gap_detection": $(cat "$WORKSPACE/.curiosity/.gap_count" 2>/dev/null || echo 0),
"anomaly_detection": $(cat "$WORKSPACE/.curiosity/.anomaly_count" 2>/dev/null || echo 0),
"opportunity_scanning": $(cat "$WORKSPACE/.curiosity/.opportunity_count" 2>/dev/null || echo 0),
"capability_mapping": "complete",
"deliberation_trigger": $(cat "$WORKSPACE/.curiosity/.proposal_count" 2>/dev/null || echo 0)
}
}
EOF
if use_modules; then
node "$MODULES_DIR/gap-detector.js" --json 2>/dev/null
node "$MODULES_DIR/anomaly-detector.js" --json 2>/dev/null
node "$MODULES_DIR/opportunity-scanner.js" --json 2>/dev/null
node "$MODULES_DIR/capability-mapper.js" --json 2>/dev/null
else
echo '{"error": "Modules not available, use shell scripts"}'
fi
;;
modules)
run_engines_modules
;;
legacy)
run_engines_legacy
;;
*)
echo "Usage: $0 {run|history|metrics|--json}"
echo "Usage: $0 {run|history|metrics|--json|modules|legacy}"
exit 1
;;
esac
@@ -1,22 +1,22 @@
#!/usr/bin/env node
/**
* Anomaly Detector Module - Phase 2: Anomaly Enhancement
*
*
* Monitors error logs, rate limits, failures with advanced pattern detection.
* Implements temporal clustering, severity scoring, and baseline deviation analysis.
*
*
* @module anomaly-detector
*/
const fs = require('fs');
const path = require('path');
const sqlite3 = require('sqlite3').verbose();
const fs = require("fs");
const path = require("path");
const sqlite3 = require("sqlite3").verbose();
// Configuration
const WORKSPACE = process.env.WORKSPACE || path.join(process.env.HOME, '.openclaw/workspace');
const LOG_DIR = path.join(WORKSPACE, 'logs');
const CURIOSITY_DIR = path.join(WORKSPACE, '.curiosity');
const ANOMALY_DB = path.join(CURIOSITY_DIR, 'anomalies.db');
const WORKSPACE = process.env.WORKSPACE || path.join(process.env.HOME, ".openclaw/workspace");
const LOG_DIR = path.join(WORKSPACE, "logs");
const CURIOSITY_DIR = path.join(WORKSPACE, ".curiosity");
const ANOMALY_DB = path.join(CURIOSITY_DIR, "anomalies.db");
// Ensure directories exist
if (!fs.existsSync(CURIOSITY_DIR)) {
@@ -33,8 +33,9 @@ function initDB() {
reject(err);
return;
}
db.run(`
db.run(
`
CREATE TABLE IF NOT EXISTS anomalies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
@@ -45,10 +46,12 @@ function initDB() {
score REAL DEFAULT 0,
processed INTEGER DEFAULT 0
)
`, (err) => {
if (err) reject(err);
else resolve(db);
});
`,
(err) => {
if (err) reject(err);
else resolve(db);
},
);
});
});
}
@@ -59,34 +62,34 @@ function initDB() {
*/
function scanLogFiles() {
const errors = [];
if (!fs.existsSync(LOG_DIR)) {
return errors;
}
const logFiles = fs.readdirSync(LOG_DIR).filter(f => f.endsWith('.log'));
logFiles.forEach(logFile => {
const logFiles = fs.readdirSync(LOG_DIR).filter((f) => f.endsWith(".log"));
logFiles.forEach((logFile) => {
const logPath = path.join(LOG_DIR, logFile);
try {
const content = fs.readFileSync(logPath, 'utf8');
const lines = content.split('\n');
lines.forEach(line => {
const content = fs.readFileSync(logPath, "utf8");
const lines = content.split("\n");
lines.forEach((line) => {
if (isErrorLine(line)) {
errors.push({
source: logFile,
line,
timestamp: extractTimestamp(line),
type: classifyError(line)
type: classifyError(line),
});
}
});
} catch (err) {
console.error('Error reading log file:', logFile, err.message);
console.error("Error reading log file:", logFile, err.message);
}
});
return errors;
}
@@ -107,10 +110,10 @@ function isErrorLine(line) {
/403/i,
/unauthorized/i,
/exception/i,
/critical/i
/critical/i,
];
return errorPatterns.some(pattern => pattern.test(line));
return errorPatterns.some((pattern) => pattern.test(line));
}
/**
@@ -129,13 +132,13 @@ function extractTimestamp(line) {
* @returns {string} Error type
*/
function classifyError(line) {
if (/timeout|ETIMEDOUT/i.test(line)) return 'timeout';
if (/429|rate.?limit/i.test(line)) return 'ratelimit';
if (/401|403|unauthorized|auth/i.test(line)) return 'auth_failure';
if (/disk|space|storage/i.test(line)) return 'disk_space';
if (/memory|oom|heap/i.test(line)) return 'memory_pressure';
if (/network|connection|ECONN/i.test(line)) return 'network';
return 'unknown';
if (/timeout|ETIMEDOUT/i.test(line)) return "timeout";
if (/429|rate.?limit/i.test(line)) return "ratelimit";
if (/401|403|unauthorized|auth/i.test(line)) return "auth_failure";
if (/disk|space|storage/i.test(line)) return "disk_space";
if (/memory|oom|heap/i.test(line)) return "memory_pressure";
if (/network|connection|ECONN/i.test(line)) return "network";
return "unknown";
}
/**
@@ -150,7 +153,7 @@ function get7DayRollingAverage(errorType) {
resolve(0); // No data yet
return;
}
const query = `
SELECT AVG(daily_count) as avg
FROM (
@@ -161,7 +164,7 @@ function get7DayRollingAverage(errorType) {
GROUP BY DATE(timestamp)
)
`;
db.get(query, [errorType], (err, row) => {
db.close();
if (err) reject(err);
@@ -178,38 +181,38 @@ function get7DayRollingAverage(errorType) {
*/
function scoreAnomaly(errors) {
if (errors.length === 0) {
return { score: 0, isSignificant: false, recommendation: 'No anomalies detected' };
return { score: 0, isSignificant: false, recommendation: "No anomalies detected" };
}
const timeWindow = 3600 * 1000; // 1 hour in ms
const frequency = errors.length / timeWindow;
const severityWeights = {
critical: 3,
high: 2,
medium: 1,
low: 0.5,
unknown: 0.5
unknown: 0.5,
};
const primarySeverity = errors[0]?.severity || 'low';
const primarySeverity = errors[0]?.severity || "low";
const severityWeight = severityWeights[primarySeverity] || 0.5;
// Calculate baseline deviation
const baseline = 0.1; // Default baseline if no historical data
const deviation = frequency > baseline ? (frequency - baseline) / baseline : 0;
const score = deviation * severityWeight;
const isSignificant = deviation > 2.0; // 2σ deviation threshold
const recommendation = generateRecommendation(errors, score);
return {
score: Math.min(10, score),
isSignificant,
deviation,
frequency,
recommendation
recommendation,
};
}
@@ -220,38 +223,44 @@ function scoreAnomaly(errors) {
* @returns {string} Recommendation
*/
function generateRecommendation(errors, score) {
if (errors.length === 0) return 'No action required';
if (errors.length === 0) return "No action required";
const errorType = errors[0].type;
const recommendations = {
timeout: score > 5
? 'Critical: Investigate network connectivity or increase timeout thresholds'
: 'Warning: Monitor timeout frequency, consider implementing retry logic',
ratelimit: score > 5
? 'Critical: Implement exponential backoff and request throttling'
: 'Warning: Add rate limit handling with graceful degradation',
auth_failure: score > 5
? 'Critical: Verify credentials, rotate tokens, audit auth subsystem'
: 'Warning: Check token expiration and refresh logic',
disk_space: score > 5
? 'Critical: Clean old logs or expand storage immediately'
: 'Warning: Monitor disk usage, implement log rotation',
memory_pressure: score > 5
? 'Critical: Investigate memory leaks, restart services, profile heap'
: 'Warning: Monitor memory trends, consider increasing limits',
network: score > 5
? 'Critical: Check network connectivity, DNS, firewall rules'
: 'Warning: Implement connection pooling and retry logic',
unknown: 'Investigate error source and implement appropriate handling'
timeout:
score > 5
? "Critical: Investigate network connectivity or increase timeout thresholds"
: "Warning: Monitor timeout frequency, consider implementing retry logic",
ratelimit:
score > 5
? "Critical: Implement exponential backoff and request throttling"
: "Warning: Add rate limit handling with graceful degradation",
auth_failure:
score > 5
? "Critical: Verify credentials, rotate tokens, audit auth subsystem"
: "Warning: Check token expiration and refresh logic",
disk_space:
score > 5
? "Critical: Clean old logs or expand storage immediately"
: "Warning: Monitor disk usage, implement log rotation",
memory_pressure:
score > 5
? "Critical: Investigate memory leaks, restart services, profile heap"
: "Warning: Monitor memory trends, consider increasing limits",
network:
score > 5
? "Critical: Check network connectivity, DNS, firewall rules"
: "Warning: Implement connection pooling and retry logic",
unknown: "Investigate error source and implement appropriate handling",
};
return recommendations[errorType] || recommendations.unknown;
}
@@ -262,23 +271,23 @@ function generateRecommendation(errors, score) {
*/
async function detectAnomalies(options = {}) {
const { timeWindow = 3600 * 1000 } = options; // Default 1 hour
const errors = scanLogFiles();
// Group errors by type
const errorGroups = {};
errors.forEach(err => {
errors.forEach((err) => {
if (!errorGroups[err.type]) {
errorGroups[err.type] = [];
}
errorGroups[err.type].push(err);
});
// Score each error group
const anomalies = [];
for (const [type, groupErrors] of Object.entries(errorGroups)) {
const scoreResult = scoreAnomaly(groupErrors);
if (scoreResult.isSignificant) {
anomalies.push({
type,
@@ -286,19 +295,19 @@ async function detectAnomalies(options = {}) {
score: scoreResult.score,
severity: classifySeverity(scoreResult.score),
recommendation: scoreResult.recommendation,
errors: groupErrors.slice(0, 5) // Sample errors
errors: groupErrors.slice(0, 5), // Sample errors
});
}
}
// Record to database
await recordAnomalies(anomalies);
return {
timestamp: new Date().toISOString(),
anomalies,
total_errors: errors.length,
significant_count: anomalies.length
significant_count: anomalies.length,
};
}
@@ -308,10 +317,10 @@ async function detectAnomalies(options = {}) {
* @returns {string} Severity level
*/
function classifySeverity(score) {
if (score >= 8) return 'critical';
if (score >= 5) return 'high';
if (score >= 3) return 'medium';
return 'low';
if (score >= 8) return "critical";
if (score >= 5) return "high";
if (score >= 3) return "medium";
return "low";
}
/**
@@ -325,16 +334,16 @@ function recordAnomalies(anomalies) {
reject(err);
return;
}
const stmt = db.prepare(`
INSERT INTO anomalies (source, error_type, count, severity, score)
VALUES (?, ?, ?, ?, ?)
`);
anomalies.forEach(anomaly => {
stmt.run(['logs', anomaly.type, anomaly.count, anomaly.severity, anomaly.score]);
anomalies.forEach((anomaly) => {
stmt.run(["logs", anomaly.type, anomaly.count, anomaly.severity, anomaly.score]);
});
stmt.finalize((err) => {
db.close();
if (err) reject(err);
@@ -350,14 +359,14 @@ function recordAnomalies(anomalies) {
* @returns {string} Formatted report
*/
function generateReport(result) {
let report = '=== Anomaly Detection Report ===\n';
let report = "=== Anomaly Detection Report ===\n";
report += `Timestamp: ${result.timestamp}\n\n`;
report += `Total errors scanned: ${result.total_errors}\n`;
report += `Significant anomalies: ${result.significant_count}\n\n`;
if (result.anomalies.length > 0) {
report += '⚠️ SIGNIFICANT ANOMALIES:\n';
result.anomalies.forEach(anomaly => {
report += "⚠️ SIGNIFICANT ANOMALIES:\n";
result.anomalies.forEach((anomaly) => {
report += ` Type: ${anomaly.type}\n`;
report += ` Count: ${anomaly.count}\n`;
report += ` Score: ${anomaly.score.toFixed(2)}\n`;
@@ -365,28 +374,30 @@ function generateReport(result) {
report += ` Recommendation: ${anomaly.recommendation}\n\n`;
});
} else {
report += '✅ No significant anomalies detected\n';
report += "✅ No significant anomalies detected\n";
}
report += '\n=== End Anomaly Detection ===\n';
report += "\n=== End Anomaly Detection ===\n";
return report;
}
// CLI execution
if (require.main === module) {
initDB().then(async () => {
const args = process.argv.slice(2);
const jsonOutput = args.includes('--json') || args.includes('-j');
const result = await detectAnomalies();
if (jsonOutput) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(generateReport(result));
}
}).catch(console.error);
initDB()
.then(async () => {
const args = process.argv.slice(2);
const jsonOutput = args.includes("--json") || args.includes("-j");
const result = await detectAnomalies();
if (jsonOutput) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(generateReport(result));
}
})
.catch(console.error);
}
// Export for module usage
@@ -396,5 +407,5 @@ module.exports = {
scanLogFiles,
get7DayRollingAverage,
generateReport,
initDB
initDB,
};
@@ -1,31 +1,44 @@
#!/usr/bin/env node
/**
* Capability Mapper Module - Phase 1: Script-to-Skill Conversion
*
*
* Maps goals to required skills and identifies gaps.
* Outputs capability analysis for strategic planning.
*
*
* @module capability-mapper
*/
const fs = require('fs');
const path = require('path');
const sqlite3 = require('sqlite3').verbose();
const fs = require("fs");
const path = require("path");
const sqlite3 = require("sqlite3").verbose();
// Configuration
const WORKSPACE = process.env.WORKSPACE || path.join(process.env.HOME, '.openclaw/workspace');
const SKILLS_DIR = path.join(WORKSPACE, 'skills');
const CURIOSITY_DIR = path.join(WORKSPACE, '.curiosity');
const CAPS_DB = path.join(CURIOSITY_DIR, 'capabilities.db');
const WORKSPACE = process.env.WORKSPACE || path.join(process.env.HOME, ".openclaw/workspace");
const SKILLS_DIR = path.join(WORKSPACE, "skills");
const CURIOSITY_DIR = path.join(WORKSPACE, ".curiosity");
const CAPS_DB = path.join(CURIOSITY_DIR, "capabilities.db");
// Goal → Skill mappings
const GOAL_MAP = {
'self-improvement': ['skill-creator', 'audit-triad-files', 'auto-patch', 'edit', 'write', 'exec'],
'knowledge-growth': ['knowledge-ingest', 'knowledge-retrieval', 'auto-tag', 'relevance-rank', 'web_search', 'web_fetch'],
'autonomy': ['triad-heartbeat', 'consensus-ledger', 'gap-detector', 'triad-deliberation-protocol'],
'triad-sync': ['triad-sync-protocol', 'triad-unity-monitor', 'triad-signal-filter', 'message', 'exec'],
'security': ['healthcheck', 'security-triage', 'openclaw-ghsa-maintainer', 'exec'],
'deployment': ['openclaw-release-maintainer', 'openclaw-pr-maintainer', 'clawhub', 'npm']
"self-improvement": ["skill-creator", "audit-triad-files", "auto-patch", "edit", "write", "exec"],
"knowledge-growth": [
"knowledge-ingest",
"knowledge-retrieval",
"auto-tag",
"relevance-rank",
"web_search",
"web_fetch",
],
autonomy: ["triad-heartbeat", "consensus-ledger", "gap-detector", "triad-deliberation-protocol"],
"triad-sync": [
"triad-sync-protocol",
"triad-unity-monitor",
"triad-signal-filter",
"message",
"exec",
],
security: ["healthcheck", "security-triage", "openclaw-ghsa-maintainer", "exec"],
deployment: ["openclaw-release-maintainer", "openclaw-pr-maintainer", "clawhub", "npm"],
};
// Ensure directories exist
@@ -43,8 +56,9 @@ function initDB() {
reject(err);
return;
}
db.run(`
db.run(
`
CREATE TABLE IF NOT EXISTS capability_maps (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
@@ -54,10 +68,12 @@ function initDB() {
gaps TEXT,
autonomy_score REAL DEFAULT 0
)
`, (err) => {
if (err) reject(err);
else resolve(db);
});
`,
(err) => {
if (err) reject(err);
else resolve(db);
},
);
});
});
}
@@ -68,17 +84,18 @@ function initDB() {
*/
function getInstalledSkills() {
const installed = [];
try {
const skillsDirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
const skillsDirs = fs
.readdirSync(SKILLS_DIR, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
installed.push(...skillsDirs);
} catch (err) {
console.error('Error reading skills directory:', err.message);
console.error("Error reading skills directory:", err.message);
}
return installed.sort();
}
@@ -99,31 +116,29 @@ function isInstalled(skill) {
*/
function mapCapability(goal) {
const required = GOAL_MAP[goal];
if (!required) {
return {
error: `Unknown goal: ${goal}`,
valid_goals: Object.keys(GOAL_MAP)
valid_goals: Object.keys(GOAL_MAP),
};
}
const installed = [];
const gaps = [];
required.forEach(skill => {
required.forEach((skill) => {
if (isInstalled(skill)) {
installed.push(skill);
} else {
gaps.push(skill);
}
});
const installedCount = installed.length;
const requiredCount = required.length;
const autonomyScore = requiredCount > 0
? (installedCount * 100 / requiredCount)
: 0;
const autonomyScore = requiredCount > 0 ? (installedCount * 100) / requiredCount : 0;
return {
goal,
required,
@@ -133,7 +148,7 @@ function mapCapability(goal) {
installed_count: installedCount,
gap_count: gaps.length,
autonomy_score: autonomyScore,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
};
}
@@ -144,18 +159,16 @@ function mapCapability(goal) {
function generateFullReport() {
const goals = Object.keys(GOAL_MAP);
const results = {};
goals.forEach(goal => {
goals.forEach((goal) => {
results[goal] = mapCapability(goal);
});
// Calculate aggregate stats
const totalRequired = goals.reduce((sum, goal) => sum + results[goal].required_count, 0);
const totalInstalled = goals.reduce((sum, goal) => sum + results[goal].installed_count, 0);
const overallAutonomy = totalRequired > 0
? (totalInstalled * 100 / totalRequired)
: 0;
const overallAutonomy = totalRequired > 0 ? (totalInstalled * 100) / totalRequired : 0;
return {
timestamp: new Date().toISOString(),
goals: results,
@@ -164,8 +177,8 @@ function generateFullReport() {
total_required: totalRequired,
total_installed: totalInstalled,
total_gaps: totalRequired - totalInstalled,
overall_autonomy_score: overallAutonomy
}
overall_autonomy_score: overallAutonomy,
},
};
}
@@ -180,19 +193,23 @@ function recordCapability(result) {
reject(err);
return;
}
const requiredStr = result.required.join(',');
const installedStr = result.installed.join(',');
const gapsStr = result.gaps.join(',');
db.run(`
const requiredStr = result.required.join(",");
const installedStr = result.installed.join(",");
const gapsStr = result.gaps.join(",");
db.run(
`
INSERT INTO capability_maps (goal, required_skills, installed_skills, gaps, autonomy_score)
VALUES (?, ?, ?, ?, ?)
`, [result.goal, requiredStr, installedStr, gapsStr, result.autonomy_score], (err) => {
db.close();
if (err) reject(err);
else resolve();
});
`,
[result.goal, requiredStr, installedStr, gapsStr, result.autonomy_score],
(err) => {
db.close();
if (err) reject(err);
else resolve();
},
);
});
});
}
@@ -214,18 +231,18 @@ async function recordAllCapabilities(report) {
*/
function identifyCriticalGaps(report) {
const critical = [];
Object.values(report.goals).forEach(goalResult => {
Object.values(report.goals).forEach((goalResult) => {
if (goalResult.autonomy_score < 50 && goalResult.gap_count > 0) {
critical.push({
goal: goalResult.goal,
gaps: goalResult.gaps,
autonomy_score: goalResult.autonomy_score,
priority: goalResult.autonomy_score < 30 ? 'critical' : 'high'
priority: goalResult.autonomy_score < 30 ? "critical" : "high",
});
}
});
return critical;
}
@@ -235,9 +252,9 @@ function identifyCriticalGaps(report) {
* @returns {string} Formatted report
*/
function generateReport(report) {
let output = '=== Capability Mapping Report ===\n';
let output = "=== Capability Mapping Report ===\n";
output += `Timestamp: ${report.timestamp}\n\n`;
// Individual goals
Object.entries(report.goals).forEach(([goal, result]) => {
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
@@ -247,24 +264,24 @@ function generateReport(report) {
output += ` Installed: ${result.installed_count} skills\n`;
output += ` Gaps: ${result.gap_count} skills\n`;
output += ` Autonomy Score: ${result.autonomy_score.toFixed(1)}%\n\n`;
if (result.installed.length > 0) {
output += ` ✅ Installed:\n`;
result.installed.forEach(skill => {
result.installed.forEach((skill) => {
output += ` ${skill}\n`;
});
output += '\n';
output += "\n";
}
if (result.gaps.length > 0) {
output += ` ❌ Gaps:\n`;
result.gaps.forEach(skill => {
result.gaps.forEach((skill) => {
output += ` ${skill}\n`;
});
output += '\n';
output += "\n";
}
});
// Aggregate summary
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
output += `Aggregate Summary\n`;
@@ -272,58 +289,60 @@ function generateReport(report) {
output += ` Total goals mapped: ${report.aggregate.total_goals}\n`;
output += ` Overall autonomy: ${report.aggregate.overall_autonomy_score.toFixed(1)}%\n`;
output += ` Total gaps: ${report.aggregate.total_gaps}\n\n`;
// Critical gaps
const critical = identifyCriticalGaps(report);
if (critical.length > 0) {
output += `⚠️ CRITICAL CAPABILITY GAPS:\n`;
critical.forEach(crit => {
critical.forEach((crit) => {
output += ` Goal: ${crit.goal}\n`;
output += ` Autonomy: ${crit.autonomy_score.toFixed(1)}%\n`;
output += ` Priority: ${crit.priority}\n`;
output += ` Missing: ${crit.gaps.join(', ')}\n\n`;
output += ` Missing: ${crit.gaps.join(", ")}\n\n`;
});
}
output += '\n=== End Capability Mapping ===\n';
output += "\n=== End Capability Mapping ===\n";
return output;
}
// CLI execution
if (require.main === module) {
initDB().then(async () => {
const args = process.argv.slice(2);
const jsonOutput = args.includes('--json') || args.includes('-j');
const specificGoal = args.find(arg => !arg.startsWith('-'));
let report;
if (specificGoal) {
const result = mapCapability(specificGoal);
await recordCapability(result);
report = {
timestamp: new Date().toISOString(),
goals: { [specificGoal]: result },
aggregate: {
total_goals: 1,
total_required: result.required_count,
total_installed: result.installed_count,
total_gaps: result.gap_count,
overall_autonomy_score: result.autonomy_score
}
};
} else {
report = generateFullReport();
await recordAllCapabilities(report);
}
if (jsonOutput) {
console.log(JSON.stringify(report, null, 2));
} else {
console.log(generateReport(report));
}
}).catch(console.error);
initDB()
.then(async () => {
const args = process.argv.slice(2);
const jsonOutput = args.includes("--json") || args.includes("-j");
const specificGoal = args.find((arg) => !arg.startsWith("-"));
let report;
if (specificGoal) {
const result = mapCapability(specificGoal);
await recordCapability(result);
report = {
timestamp: new Date().toISOString(),
goals: { [specificGoal]: result },
aggregate: {
total_goals: 1,
total_required: result.required_count,
total_installed: result.installed_count,
total_gaps: result.gap_count,
overall_autonomy_score: result.autonomy_score,
},
};
} else {
report = generateFullReport();
await recordAllCapabilities(report);
}
if (jsonOutput) {
console.log(JSON.stringify(report, null, 2));
} else {
console.log(generateReport(report));
}
})
.catch(console.error);
}
// Export for module usage
@@ -335,5 +354,5 @@ module.exports = {
identifyCriticalGaps,
generateReport,
initDB,
GOAL_MAP
GOAL_MAP,
};
@@ -1,33 +1,33 @@
#!/usr/bin/env node
/**
* Deliberation Trigger Module - Phase 4: Deliberation Trigger Enhancement
*
*
* Creates proposals from detected gaps, anomalies, and opportunities.
* Implements priority scoring, deduplication, and quorum awareness.
*
*
* @module deliberation-trigger
*/
const fs = require('fs');
const path = require('path');
const sqlite3 = require('sqlite3').verbose();
const { execSync } = require('child_process');
const fs = require("fs");
const path = require("path");
const sqlite3 = require("sqlite3").verbose();
const { execSync } = require("child_process");
// Configuration
const WORKSPACE = process.env.WORKSPACE || path.join(process.env.HOME, '.openclaw/workspace');
const CURIOSITY_DIR = path.join(WORKSPACE, '.curiosity');
const CONSENSUS_DB = path.join(CURIOSITY_DIR, 'consensus_ledger.db');
const MEMORY_DIR = path.join(WORKSPACE, 'memory');
const IDENTITY_FILE = path.join(WORKSPACE, 'IDENTITY.md');
const WORKSPACE = process.env.WORKSPACE || path.join(process.env.HOME, ".openclaw/workspace");
const CURIOSITY_DIR = path.join(WORKSPACE, ".curiosity");
const CONSENSUS_DB = path.join(CURIOSITY_DIR, "consensus_ledger.db");
const MEMORY_DIR = path.join(WORKSPACE, "memory");
const IDENTITY_FILE = path.join(WORKSPACE, "IDENTITY.md");
// Priority matrix for scoring
const PRIORITY_MATRIX = {
security: { base: 10, multiplier: 2.0 },
self-improvement: { base: 8, multiplier: 1.5 },
triad-sync: { base: 6, multiplier: 1.3 },
"self-improvement": { base: 8, multiplier: 1.5 },
"triad-sync": { base: 6, multiplier: 1.3 },
knowledge: { base: 4, multiplier: 1.0 },
triad: { base: 6, multiplier: 1.3 },
optional: { base: 2, multiplier: 0.5 }
optional: { base: 2, multiplier: 0.5 },
};
// Ensure directories exist
@@ -48,8 +48,9 @@ function initDB() {
reject(err);
return;
}
db.run(`
db.run(
`
CREATE TABLE IF NOT EXISTS consensus_votes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
@@ -64,10 +65,12 @@ function initDB() {
result TEXT DEFAULT 'pending',
processed INTEGER DEFAULT 0
)
`, (err) => {
if (err) reject(err);
else resolve(db);
});
`,
(err) => {
if (err) reject(err);
else resolve(db);
},
);
});
});
}
@@ -79,11 +82,11 @@ function initDB() {
function isQuorumSpeaker() {
try {
if (fs.existsSync(IDENTITY_FILE)) {
const content = fs.readFileSync(IDENTITY_FILE, 'utf8');
const content = fs.readFileSync(IDENTITY_FILE, "utf8");
return /Role:\s*Authority/i.test(content);
}
} catch (err) {
console.error('Error reading IDENTITY.md:', err.message);
console.error("Error reading IDENTITY.md:", err.message);
}
return false;
}
@@ -101,7 +104,7 @@ function proposalExists(title, source) {
resolve(false);
return;
}
const query = `
SELECT COUNT(*) as count
FROM consensus_votes
@@ -110,7 +113,7 @@ function proposalExists(title, source) {
AND status != 'closed'
AND timestamp >= datetime('now', '-24 hours')
`;
db.get(query, [title, source], (err, row) => {
db.close();
if (err) reject(err);
@@ -128,21 +131,21 @@ function proposalExists(title, source) {
function calculatePriority(item) {
const category = item.category || categorizeItem(item);
const config = PRIORITY_MATRIX[category] || PRIORITY_MATRIX.optional;
const base = config.base;
const multiplier = config.multiplier;
const urgency = item.blocksLiberation || item.severity === 'critical' ? 2.0 : 1.0;
const urgency = item.blocksLiberation || item.severity === "critical" ? 2.0 : 1.0;
const score = Math.min(10, base * multiplier * urgency);
const priority = score >= 8 ? 'critical' : score >= 6 ? 'high' : score >= 4 ? 'medium' : 'low';
const priority = score >= 8 ? "critical" : score >= 6 ? "high" : score >= 4 ? "medium" : "low";
return {
category,
base,
multiplier,
urgency,
score,
priority
priority,
};
}
@@ -152,16 +155,17 @@ function calculatePriority(item) {
* @returns {string} Category
*/
function categorizeItem(item) {
const title = (item.title || '').toLowerCase();
const type = item.type || '';
if (title.includes('security') || title.includes('cve') || type === 'security') return 'security';
if (title.includes('skill-creator') || title.includes('self-improvement')) return 'self-improvement';
if (title.includes('triad') || title.includes('sync')) return 'triad';
if (title.includes('knowledge')) return 'knowledge';
if (item.gap && item.gap.includes('skill')) return 'self-improvement';
return 'optional';
const title = (item.title || "").toLowerCase();
const type = item.type || "";
if (title.includes("security") || title.includes("cve") || type === "security") return "security";
if (title.includes("skill-creator") || title.includes("self-improvement"))
return "self-improvement";
if (title.includes("triad") || title.includes("sync")) return "triad";
if (title.includes("knowledge")) return "knowledge";
if (item.gap && item.gap.includes("skill")) return "self-improvement";
return "optional";
}
/**
@@ -171,24 +175,24 @@ function categorizeItem(item) {
*/
async function createProposal(proposal) {
const { title, body, source, category, item } = proposal;
// Check for duplicates
const exists = await proposalExists(title, source);
if (exists) {
return {
skipped: true,
reason: 'Duplicate proposal exists within 24h window',
title
reason: "Duplicate proposal exists within 24h window",
title,
};
}
// Calculate priority
const priorityResult = calculatePriority(item || { category, title });
// Escape quotes for SQL
const safeTitle = title.replace(/'/g, "''");
const safeBody = body.replace(/'/g, "''");
// Insert into consensus ledger
return new Promise((resolve, reject) => {
const db = new sqlite3.Database(CONSENSUS_DB, (err) => {
@@ -196,33 +200,37 @@ async function createProposal(proposal) {
reject(err);
return;
}
db.run(`
db.run(
`
INSERT INTO consensus_votes
(proposal_title, proposal_body, priority, priority_score, source, category)
VALUES (?, ?, ?, ?, ?, ?)
`, [safeTitle, safeBody, priorityResult.priority, priorityResult.score, source, category], function(err) {
if (err) {
`,
[safeTitle, safeBody, priorityResult.priority, priorityResult.score, source, category],
function (err) {
if (err) {
db.close();
reject(err);
return;
}
const created = {
id: this.lastID,
title,
body,
priority: priorityResult.priority,
priority_score: priorityResult.score,
source,
category,
status: "pending",
timestamp: new Date().toISOString(),
};
db.close();
reject(err);
return;
}
const created = {
id: this.lastID,
title,
body,
priority: priorityResult.priority,
priority_score: priorityResult.score,
source,
category,
status: 'pending',
timestamp: new Date().toISOString()
};
db.close();
resolve(created);
});
resolve(created);
},
);
});
});
}
@@ -232,9 +240,9 @@ async function createProposal(proposal) {
* @param {Object} proposal - Proposal data
*/
function logToMemory(proposal) {
const date = new Date().toISOString().split('T')[0];
const date = new Date().toISOString().split("T")[0];
const memoryFile = path.join(MEMORY_DIR, `curiosity-${date}.md`);
const entry = `
## Deliberation Proposal - ${proposal.timestamp}
@@ -249,7 +257,7 @@ function logToMemory(proposal) {
---
`;
fs.appendFileSync(memoryFile, entry);
}
@@ -259,15 +267,15 @@ function logToMemory(proposal) {
*/
function postToDiscord(proposal) {
if (!isQuorumSpeaker()) {
console.log('️ Not quorum speaker, logging to memory only');
console.log("️ Not quorum speaker, logging to memory only");
return;
}
if (proposal.priority !== 'high' && proposal.priority !== 'critical') {
console.log('️ Priority not high enough for Discord post');
if (proposal.priority !== "high" && proposal.priority !== "critical") {
console.log("️ Priority not high enough for Discord post");
return;
}
const message = `**🦞 Proposal:** ${proposal.title}
**Priority:** ${proposal.priority} (score: ${proposal.priority_score.toFixed(2)})
@@ -277,12 +285,15 @@ function postToDiscord(proposal) {
${proposal.body}
*Awaiting quorum vote (2-of-3)*`;
try {
execSync(`openclaw message send --channel discord --message "${message.replace(/"/g, '\\"')}" 2>/dev/null || true`, { encoding: 'utf8' });
console.log('📢 Posted to Discord');
execSync(
`openclaw message send --channel discord --message "${message.replace(/"/g, '\\"')}" 2>/dev/null || true`,
{ encoding: "utf8" },
);
console.log("📢 Posted to Discord");
} catch (err) {
console.error(' Discord post failed:', err.message);
console.error(" Discord post failed:", err.message);
}
}
@@ -293,27 +304,27 @@ ${proposal.body}
*/
async function processGaps(gaps) {
const proposals = [];
if (!gaps || !gaps.critical) {
return proposals;
}
for (const gap of gaps.critical) {
const proposal = await createProposal({
title: `Install ${gap.skill} to close capability gap`,
body: `Gap detected: ${gap.skill} not installed. ${gap.impact}. Recommendation: ${gap.recommendation}.`,
source: 'gap-detector',
category: 'self-improvement',
item: { ...gap, blocksLiberation: true }
source: "gap-detector",
category: "self-improvement",
item: { ...gap, blocksLiberation: true },
});
if (!proposal.skipped) {
logToMemory(proposal);
postToDiscord(proposal);
proposals.push(proposal);
}
}
return proposals;
}
@@ -324,27 +335,27 @@ async function processGaps(gaps) {
*/
async function processAnomalies(anomalyResult) {
const proposals = [];
if (!anomalyResult || !anomalyResult.anomalies) {
return proposals;
}
for (const anomaly of anomalyResult.anomalies) {
const proposal = await createProposal({
title: `Repair ${anomaly.type} anomaly`,
body: `Anomaly detected: ${anomaly.count} occurrences of ${anomaly.type} errors. Score: ${anomaly.score.toFixed(2)}. ${anomaly.recommendation}.`,
source: 'anomaly-detector',
source: "anomaly-detector",
category: categorizeItem({ title: anomaly.type }),
item: { severity: anomaly.severity, type: anomaly.type }
item: { severity: anomaly.severity, type: anomaly.type },
});
if (!proposal.skipped) {
logToMemory(proposal);
postToDiscord(proposal);
proposals.push(proposal);
}
}
return proposals;
}
@@ -355,37 +366,37 @@ async function processAnomalies(anomalyResult) {
*/
async function processOpportunities(oppResult) {
const proposals = [];
if (!oppResult || !oppResult.opportunities) {
return proposals;
}
for (const opp of oppResult.opportunities) {
if (opp.priority === 'high' || opp.priority === 'critical') {
if (opp.priority === "high" || opp.priority === "critical") {
let title, body, category;
if (opp.type === 'release') {
if (opp.type === "release") {
title = `Rebase on ${opp.title}`;
body = `New release detected: ${opp.title}. Recommend rebasing heretek/main to incorporate changes while preserving liberation.`;
category = 'triad';
} else if (opp.type === 'security') {
category = "triad";
} else if (opp.type === "security") {
title = `Address ${opp.title}`;
body = `Security advisory: ${opp.title}. Requires immediate triage and remediation.`;
category = 'security';
category = "security";
} else {
title = `Evaluate ${opp.title}`;
body = `Opportunity detected: ${opp.title}. Source: ${opp.source}. Evaluate for implementation.`;
category = 'optional';
category = "optional";
}
const proposal = await createProposal({
title,
body,
source: 'opportunity-scanner',
source: "opportunity-scanner",
category,
item: { ...opp, blocksLiberation: opp.priority === 'critical' }
item: { ...opp, blocksLiberation: opp.priority === "critical" },
});
if (!proposal.skipped) {
logToMemory(proposal);
postToDiscord(proposal);
@@ -393,7 +404,7 @@ async function processOpportunities(oppResult) {
}
}
}
return proposals;
}
@@ -404,21 +415,25 @@ async function processOpportunities(oppResult) {
*/
async function processCapabilityGaps(capReport) {
const proposals = [];
if (!capReport || !capReport.goals) {
return proposals;
}
for (const [goal, result] of Object.entries(capReport.goals)) {
if (result.autonomy_score < 50 && result.gaps.length > 0) {
const proposal = await createProposal({
title: `Close capability gaps for ${goal}`,
body: `Capability mapping identified ${result.gap_count} gaps for goal '${goal}': ${result.gaps.join(', ')}. Install missing skills to achieve ${result.autonomy_score.toFixed(1)}% → 100% autonomy.`,
source: 'capability-mapper',
category: goal.includes('triad') ? 'triad' : goal.includes('security') ? 'security' : 'knowledge',
item: { autonomy_score: result.autonomy_score, gap_count: result.gap_count }
body: `Capability mapping identified ${result.gap_count} gaps for goal '${goal}': ${result.gaps.join(", ")}. Install missing skills to achieve ${result.autonomy_score.toFixed(1)}% → 100% autonomy.`,
source: "capability-mapper",
category: goal.includes("triad")
? "triad"
: goal.includes("security")
? "security"
: "knowledge",
item: { autonomy_score: result.autonomy_score, gap_count: result.gap_count },
});
if (!proposal.skipped) {
logToMemory(proposal);
postToDiscord(proposal);
@@ -426,7 +441,7 @@ async function processCapabilityGaps(capReport) {
}
}
}
return proposals;
}
@@ -437,55 +452,55 @@ async function processCapabilityGaps(capReport) {
*/
async function runAutoTrigger(inputs = {}) {
const { gaps, anomalies, opportunities, capabilities } = inputs;
console.log('=== Deliberation Auto-Trigger ===');
console.log("=== Deliberation Auto-Trigger ===");
console.log(`Timestamp: ${new Date().toISOString()}`);
console.log(`Quorum Speaker: ${isQuorumSpeaker() ? 'Yes' : 'No'}`);
console.log('');
console.log(`Quorum Speaker: ${isQuorumSpeaker() ? "Yes" : "No"}`);
console.log("");
const allProposals = [];
if (gaps) {
console.log('Processing gap detection results...');
console.log("Processing gap detection results...");
const gapProposals = await processGaps(gaps);
allProposals.push(...gapProposals);
console.log(` Created ${gapProposals.length} proposals from gaps`);
}
if (anomalies) {
console.log('Processing anomaly detection results...');
console.log("Processing anomaly detection results...");
const anomalyProposals = await processAnomalies(anomalies);
allProposals.push(...anomalyProposals);
console.log(` Created ${anomalyProposals.length} proposals from anomalies`);
}
if (opportunities) {
console.log('Processing opportunity scanning results...');
console.log("Processing opportunity scanning results...");
const oppProposals = await processOpportunities(opportunities);
allProposals.push(...oppProposals);
console.log(` Created ${oppProposals.length} proposals from opportunities`);
}
if (capabilities) {
console.log('Processing capability mapping results...');
console.log("Processing capability mapping results...");
const capProposals = await processCapabilityGaps(capabilities);
allProposals.push(...capProposals);
console.log(` Created ${capProposals.length} proposals from capability gaps`);
}
console.log('');
console.log("");
console.log(`=== End Auto-Trigger ===`);
console.log(`Total proposals created: ${allProposals.length}`);
// Count pending proposals
const pendingCount = await getPendingProposalCount();
console.log(`Pending proposals in ledger: ${pendingCount}`);
return {
timestamp: new Date().toISOString(),
proposals_created: allProposals.length,
pending_count: pendingCount,
proposals: allProposals
proposals: allProposals,
};
}
@@ -500,33 +515,39 @@ function getPendingProposalCount() {
resolve(0);
return;
}
db.get('SELECT COUNT(*) as count FROM consensus_votes WHERE status = ?', ['pending'], (err, row) => {
db.close();
if (err) reject(err);
else resolve(row?.count || 0);
});
db.get(
"SELECT COUNT(*) as count FROM consensus_votes WHERE status = ?",
["pending"],
(err, row) => {
db.close();
if (err) reject(err);
else resolve(row?.count || 0);
},
);
});
});
}
// CLI execution
if (require.main === module) {
initDB().then(async () => {
const args = process.argv.slice(2);
const jsonOutput = args.includes('--json') || args.includes('-j');
// In standalone mode, run with mock empty inputs
// In production, inputs would come from other modules
const result = await runAutoTrigger({});
if (jsonOutput) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log('');
console.log('Deliberation auto-trigger complete.');
}
}).catch(console.error);
initDB()
.then(async () => {
const args = process.argv.slice(2);
const jsonOutput = args.includes("--json") || args.includes("-j");
// In standalone mode, run with mock empty inputs
// In production, inputs would come from other modules
const result = await runAutoTrigger({});
if (jsonOutput) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log("");
console.log("Deliberation auto-trigger complete.");
}
})
.catch(console.error);
}
// Export for module usage
@@ -540,5 +561,5 @@ module.exports = {
isQuorumSpeaker,
calculatePriority,
initDB,
PRIORITY_MATRIX
PRIORITY_MATRIX,
};
+80 -79
View File
@@ -1,21 +1,21 @@
#!/usr/bin/env node
/**
* Gap Detector Module - Phase 1: Script-to-Skill Conversion
*
*
* Compares installed skills vs available skills from multiple sources.
* Outputs gaps that would enable new capabilities.
*
*
* @module gap-detector
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
// Configuration
const WORKSPACE = process.env.WORKSPACE || path.join(process.env.HOME, '.openclaw/workspace');
const SKILLS_DIR = path.join(WORKSPACE, 'skills');
const CURIOSITY_DIR = path.join(WORKSPACE, '.curiosity');
const WORKSPACE = process.env.WORKSPACE || path.join(process.env.HOME, ".openclaw/workspace");
const SKILLS_DIR = path.join(WORKSPACE, "skills");
const CURIOSITY_DIR = path.join(WORKSPACE, ".curiosity");
// Ensure directories exist
if (!fs.existsSync(CURIOSITY_DIR)) {
@@ -28,17 +28,18 @@ if (!fs.existsSync(CURIOSITY_DIR)) {
*/
function getInstalledSkills() {
const installed = [];
try {
const skillsDirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
const skillsDirs = fs
.readdirSync(SKILLS_DIR, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
installed.push(...skillsDirs);
} catch (err) {
console.error('Error reading skills directory:', err.message);
console.error("Error reading skills directory:", err.message);
}
return installed.sort();
}
@@ -48,11 +49,11 @@ function getInstalledSkills() {
*/
function getAvailableSkillsFromClawHub() {
try {
const output = execSync('clawhub search 2>/dev/null || true', { encoding: 'utf8' });
const lines = output.trim().split('\n').slice(1); // Skip header
return lines.map(line => line.split(/\s+/)[0]).filter(Boolean);
const output = execSync("clawhub search 2>/dev/null || true", { encoding: "utf8" });
const lines = output.trim().split("\n").slice(1); // Skip header
return lines.map((line) => line.split(/\s+/)[0]).filter(Boolean);
} catch (err) {
console.error('ClawHub CLI not available:', err.message);
console.error("ClawHub CLI not available:", err.message);
return [];
}
}
@@ -62,16 +63,16 @@ function getAvailableSkillsFromClawHub() {
* @returns {string[]} Array of available skill names
*/
function getAvailableSkillsFromCache() {
const cacheFile = path.join(CURIOSITY_DIR, 'available_skills.txt');
const cacheFile = path.join(CURIOSITY_DIR, "available_skills.txt");
try {
if (fs.existsSync(cacheFile)) {
return fs.readFileSync(cacheFile, 'utf8').trim().split('\n').filter(Boolean);
return fs.readFileSync(cacheFile, "utf8").trim().split("\n").filter(Boolean);
}
} catch (err) {
console.error('Error reading cache file:', err.message);
console.error("Error reading cache file:", err.message);
}
return [];
}
@@ -82,7 +83,7 @@ function getAvailableSkillsFromCache() {
function getAvailableSkills() {
const clawhub = getAvailableSkillsFromClawHub();
const cached = getAvailableSkillsFromCache();
// Merge and dedupe
const all = new Set([...clawhub, ...cached]);
return Array.from(all).sort();
@@ -94,14 +95,14 @@ function getAvailableSkills() {
*/
function getCriticalSkills() {
return [
'skill-creator',
'knowledge-ingest',
'knowledge-retrieval',
'triad-deliberation-protocol',
'triad-sync-protocol',
'auto-patch',
'gap-detector',
'auto-deliberation-trigger'
"skill-creator",
"knowledge-ingest",
"knowledge-retrieval",
"triad-deliberation-protocol",
"triad-sync-protocol",
"auto-patch",
"gap-detector",
"auto-deliberation-trigger",
];
}
@@ -113,43 +114,43 @@ function getCriticalSkills() {
*/
function detectGaps(options = {}) {
const { criticalOnly = false } = options;
const installed = getInstalledSkills();
const available = getAvailableSkills();
const critical = getCriticalSkills();
const gaps = {
critical: [],
optional: [],
timestamp: new Date().toISOString(),
installed_count: installed.length,
available_count: available.length
available_count: available.length,
};
// Check critical skills
critical.forEach(skill => {
critical.forEach((skill) => {
if (!installed.includes(skill)) {
gaps.critical.push({
skill,
impact: getSkillImpact(skill),
recommendation: `clawhub install ${skill}`
recommendation: `clawhub install ${skill}`,
});
}
});
// Check all available skills (if not critical-only mode)
if (!criticalOnly) {
available.forEach(skill => {
available.forEach((skill) => {
if (!installed.includes(skill)) {
gaps.optional.push({
skill,
category: categorizeSkill(skill),
relevance: calculateRelevance(skill)
relevance: calculateRelevance(skill),
});
}
});
}
return gaps;
}
@@ -160,17 +161,17 @@ function detectGaps(options = {}) {
*/
function getSkillImpact(skill) {
const impacts = {
'skill-creator': 'Self-improvement loop disabled',
'knowledge-ingest': 'Knowledge growth disabled',
'knowledge-retrieval': 'Knowledge retrieval disabled',
'triad-deliberation-protocol': 'Consensus deliberation disabled',
'triad-sync-protocol': 'Triad sync infrastructure missing',
'auto-patch': 'Auto-remediation disabled',
'gap-detector': 'Self-awareness disabled',
'auto-deliberation-trigger': 'Proactive deliberation disabled'
"skill-creator": "Self-improvement loop disabled",
"knowledge-ingest": "Knowledge growth disabled",
"knowledge-retrieval": "Knowledge retrieval disabled",
"triad-deliberation-protocol": "Consensus deliberation disabled",
"triad-sync-protocol": "Triad sync infrastructure missing",
"auto-patch": "Auto-remediation disabled",
"gap-detector": "Self-awareness disabled",
"auto-deliberation-trigger": "Proactive deliberation disabled",
};
return impacts[skill] || 'Capability gap';
return impacts[skill] || "Capability gap";
}
/**
@@ -179,11 +180,11 @@ function getSkillImpact(skill) {
* @returns {string} Category
*/
function categorizeSkill(skill) {
if (skill.includes('triad')) return 'triad';
if (skill.includes('knowledge')) return 'knowledge';
if (skill.includes('security')) return 'security';
if (skill.includes('skill')) return 'self-improvement';
return 'optional';
if (skill.includes("triad")) return "triad";
if (skill.includes("knowledge")) return "knowledge";
if (skill.includes("security")) return "security";
if (skill.includes("skill")) return "self-improvement";
return "optional";
}
/**
@@ -194,13 +195,13 @@ function categorizeSkill(skill) {
function calculateRelevance(skill) {
// Base relevance by category
const baseScores = {
'triad': 0.9,
'knowledge': 0.8,
'security': 0.7,
'self-improvement': 0.95,
'optional': 0.5
triad: 0.9,
knowledge: 0.8,
security: 0.7,
"self-improvement": 0.95,
optional: 0.5,
};
const category = categorizeSkill(skill);
return baseScores[category] || 0.5;
}
@@ -211,45 +212,45 @@ function calculateRelevance(skill) {
* @returns {string} Formatted report
*/
function generateReport(gaps) {
let report = '=== Gap Detection Report ===\n';
let report = "=== Gap Detection Report ===\n";
report += `Timestamp: ${gaps.timestamp}\n\n`;
report += `Installed skills: ${gaps.installed_count}\n`;
report += `Available skills: ${gaps.available_count}\n\n`;
if (gaps.critical.length > 0) {
report += '⚠️ CRITICAL GAPS DETECTED:\n';
gaps.critical.forEach(gap => {
report += "⚠️ CRITICAL GAPS DETECTED:\n";
gaps.critical.forEach((gap) => {
report += ` ${gap.skill}\n`;
report += ` Impact: ${gap.impact}\n`;
report += ` Recommendation: ${gap.recommendation}\n\n`;
});
} else {
report += '✅ No critical gaps detected\n\n';
report += "✅ No critical gaps detected\n\n";
}
if (gaps.optional.length > 0) {
report += `📋 Optional gaps (${gaps.optional.length} skills):\n`;
gaps.optional.slice(0, 10).forEach(gap => {
gaps.optional.slice(0, 10).forEach((gap) => {
report += ` ${gap.skill} (relevance: ${(gap.relevance * 100).toFixed(0)}%)\n`;
});
if (gaps.optional.length > 10) {
report += ` ... and ${gaps.optional.length - 10} more\n`;
}
}
report += '\n=== End Gap Detection ===\n';
report += "\n=== End Gap Detection ===\n";
return report;
}
// CLI execution
if (require.main === module) {
const args = process.argv.slice(2);
const criticalOnly = args.includes('--critical') || args.includes('-c');
const jsonOutput = args.includes('--json') || args.includes('-j');
const criticalOnly = args.includes("--critical") || args.includes("-c");
const jsonOutput = args.includes("--json") || args.includes("-j");
const gaps = detectGaps({ criticalOnly });
if (jsonOutput) {
console.log(JSON.stringify(gaps, null, 2));
} else {
@@ -263,5 +264,5 @@ module.exports = {
getInstalledSkills,
getAvailableSkills,
getCriticalSkills,
generateReport
generateReport,
};
@@ -1,36 +1,36 @@
#!/usr/bin/env node
/**
* Opportunity Scanner Module - Phase 3: MCP Tool Integration
*
*
* Watches GitHub releases, npm updates, CVEs, ClawHub new skills.
* Integrates MCP tools: SearXNG, Playwright, GitHub API.
*
*
* @module opportunity-scanner
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const sqlite3 = require('sqlite3').verbose();
const fs = require("fs");
const path = require("path");
const https = require("https");
const sqlite3 = require("sqlite3").verbose();
// Configuration
const WORKSPACE = process.env.WORKSPACE || path.join(process.env.HOME, '.openclaw/workspace');
const CURIOSITY_DIR = path.join(WORKSPACE, '.curiosity');
const OPPS_DB = path.join(CURIOSITY_DIR, 'opportunities.db');
const GITHUB_ORG = 'Heretek-AI';
const MAIN_REPO = 'openclaw';
const WORKSPACE = process.env.WORKSPACE || path.join(process.env.HOME, ".openclaw/workspace");
const CURIOSITY_DIR = path.join(WORKSPACE, ".curiosity");
const OPPS_DB = path.join(CURIOSITY_DIR, "opportunities.db");
const GITHUB_ORG = "Heretek-AI";
const MAIN_REPO = "openclaw";
// MCP Tool configurations
const MCP_CONFIG = {
searxng: {
endpoint: process.env.SEARXNG_ENDPOINT || 'http://localhost:8080',
timeout: 5000
endpoint: process.env.SEARXNG_ENDPOINT || "http://localhost:8080",
timeout: 5000,
},
github: {
token: process.env.GH_TOKEN || process.env.GITHUB_TOKEN,
org: GITHUB_ORG,
repo: MAIN_REPO
}
repo: MAIN_REPO,
},
};
// Ensure directories exist
@@ -48,8 +48,9 @@ function initDB() {
reject(err);
return;
}
db.run(`
db.run(
`
CREATE TABLE IF NOT EXISTS opportunities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
@@ -60,10 +61,12 @@ function initDB() {
priority TEXT DEFAULT 'low',
processed INTEGER DEFAULT 0
)
`, (err) => {
if (err) reject(err);
else resolve(db);
});
`,
(err) => {
if (err) reject(err);
else resolve(db);
},
);
});
});
}
@@ -78,22 +81,24 @@ function httpGet(url, headers = {}) {
return new Promise((resolve, reject) => {
const options = {
headers: {
'User-Agent': 'Curiosity-Engine/1.0',
...headers
}
"User-Agent": "Curiosity-Engine/1.0",
...headers,
},
};
https.get(url, options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (err) {
resolve(data);
}
});
}).on('error', reject);
https
.get(url, options, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
try {
resolve(JSON.parse(data));
} catch (err) {
resolve(data);
}
});
})
.on("error", reject);
});
}
@@ -103,37 +108,37 @@ function httpGet(url, headers = {}) {
*/
async function scanGitHubReleases() {
const opportunities = [];
if (!MCP_CONFIG.github.token) {
console.log(' GitHub token not set, skipping API calls');
console.log(" GitHub token not set, skipping API calls");
return opportunities;
}
try {
const url = `https://api.github.com/repos/${MCP_CONFIG.github.org}/${MCP_CONFIG.github.repo}/releases?per_page=5`;
const headers = {
'Authorization': `token ${MCP_CONFIG.github.token}`,
'Accept': 'application/vnd.github.v3+json'
Authorization: `token ${MCP_CONFIG.github.token}`,
Accept: "application/vnd.github.v3+json",
};
const releases = await httpGet(url, headers);
if (Array.isArray(releases) && releases.length > 0) {
releases.forEach(release => {
releases.forEach((release) => {
opportunities.push({
source: 'github',
source: "github",
title: release.tag_name || release.name,
url: release.html_url,
type: 'release',
priority: 'high',
published_at: release.published_at
type: "release",
priority: "high",
published_at: release.published_at,
});
});
}
} catch (err) {
console.error(' GitHub API error:', err.message);
console.error(" GitHub API error:", err.message);
}
return opportunities;
}
@@ -143,21 +148,21 @@ async function scanGitHubReleases() {
*/
async function scanNpmUpdates() {
const opportunities = [];
const packages = ['openclaw', 'clawhub', 'mcporter'];
const packages = ["openclaw", "clawhub", "mcporter"];
for (const pkg of packages) {
try {
// Try direct npm registry first
const url = `https://registry.npmjs.org/@heretek-ai/${pkg}/latest`;
const data = await httpGet(url);
if (data?.version) {
opportunities.push({
source: 'npm',
source: "npm",
title: `@heretek-ai/${pkg}@${data.version}`,
url: `https://www.npmjs.com/package/@heretek-ai/${pkg}`,
type: 'update',
priority: 'medium'
type: "update",
priority: "medium",
});
}
} catch (err) {
@@ -165,15 +170,15 @@ async function scanNpmUpdates() {
try {
const searchUrl = `${MCP_CONFIG.searxng.endpoint}/search?q=@heretek-ai+${pkg}+npm&format=json`;
const results = await httpGet(searchUrl);
if (results?.results?.length > 0) {
const firstResult = results.results[0];
opportunities.push({
source: 'npm',
source: "npm",
title: `@heretek-ai/${pkg} (via SearXNG)`,
url: firstResult.url,
type: 'update',
priority: 'medium'
type: "update",
priority: "medium",
});
}
} catch (searxErr) {
@@ -181,7 +186,7 @@ async function scanNpmUpdates() {
}
}
}
return opportunities;
}
@@ -191,27 +196,27 @@ async function scanNpmUpdates() {
*/
async function scanClawHubSkills() {
const opportunities = [];
try {
const { execSync } = require('child_process');
const output = execSync('clawhub search 2>/dev/null || true', { encoding: 'utf8' });
const lines = output.trim().split('\n').slice(1);
lines.slice(0, 5).forEach(line => {
const { execSync } = require("child_process");
const output = execSync("clawhub search 2>/dev/null || true", { encoding: "utf8" });
const lines = output.trim().split("\n").slice(1);
lines.slice(0, 5).forEach((line) => {
const skillName = line.split(/\s+/)[0];
if (skillName) {
opportunities.push({
source: 'clawhub',
source: "clawhub",
title: skillName,
type: 'new_skill',
priority: 'medium'
type: "new_skill",
priority: "medium",
});
}
});
} catch (err) {
console.error(' ClawHub CLI not available:', err.message);
console.error(" ClawHub CLI not available:", err.message);
}
return opportunities;
}
@@ -221,50 +226,50 @@ async function scanClawHubSkills() {
*/
async function scanSecurityAdvisories() {
const opportunities = [];
if (MCP_CONFIG.github.token) {
try {
// GitHub code scanning alerts
const url = `https://api.github.com/repos/${MCP_CONFIG.github.org}/${MCP_CONFIG.github.repo}/code-scanning/alerts?state=open&per_page=5`;
const headers = {
'Authorization': `token ${MCP_CONFIG.github.token}`,
'Accept': 'application/vnd.github.v3+json'
Authorization: `token ${MCP_CONFIG.github.token}`,
Accept: "application/vnd.github.v3+json",
};
const alerts = await httpGet(url, headers);
if (Array.isArray(alerts) && alerts.length > 0) {
opportunities.push({
source: 'github',
source: "github",
title: `${alerts.length} open code scanning alerts`,
url: `https://github.com/${MCP_CONFIG.github.org}/${MCP_CONFIG.github.repo}/code-scanning`,
type: 'security',
priority: 'critical'
type: "security",
priority: "critical",
});
}
} catch (err) {
console.error(' Security scan error:', err.message);
console.error(" Security scan error:", err.message);
}
}
// Fallback: SearXNG CVE search
try {
const searchUrl = `${MCP_CONFIG.searxng.endpoint}/search?q=CVE+Heretek-AI+openclaw&format=json`;
const results = await httpGet(searchUrl);
if (results?.results?.length > 0) {
opportunities.push({
source: 'searxng',
title: 'CVE mentions detected',
source: "searxng",
title: "CVE mentions detected",
url: results.results[0].url,
type: 'security',
priority: 'high'
type: "security",
priority: "high",
});
}
} catch (err) {
// Silent fail for optional search
}
return opportunities;
}
@@ -274,43 +279,43 @@ async function scanSecurityAdvisories() {
* @returns {Promise<Object>} Opportunity scan result
*/
async function scanOpportunities(options = {}) {
const { sources = ['github', 'npm', 'clawhub', 'security'] } = options;
const { sources = ["github", "npm", "clawhub", "security"] } = options;
const allOpportunities = [];
if (sources.includes('github')) {
if (sources.includes("github")) {
const gh = await scanGitHubReleases();
allOpportunities.push(...gh);
}
if (sources.includes('npm')) {
if (sources.includes("npm")) {
const npm = await scanNpmUpdates();
allOpportunities.push(...npm);
}
if (sources.includes('clawhub')) {
if (sources.includes("clawhub")) {
const clawhub = await scanClawHubSkills();
allOpportunities.push(...clawhub);
}
if (sources.includes('security')) {
if (sources.includes("security")) {
const security = await scanSecurityAdvisories();
allOpportunities.push(...security);
}
// Record to database
await recordOpportunities(allOpportunities);
return {
timestamp: new Date().toISOString(),
opportunities: allOpportunities,
total_count: allOpportunities.length,
by_source: {
github: allOpportunities.filter(o => o.source === 'github').length,
npm: allOpportunities.filter(o => o.source === 'npm').length,
clawhub: allOpportunities.filter(o => o.source === 'clawhub').length,
security: allOpportunities.filter(o => o.source === 'security').length
}
github: allOpportunities.filter((o) => o.source === "github").length,
npm: allOpportunities.filter((o) => o.source === "npm").length,
clawhub: allOpportunities.filter((o) => o.source === "clawhub").length,
security: allOpportunities.filter((o) => o.source === "security").length,
},
};
}
@@ -325,16 +330,16 @@ function recordOpportunities(opportunities) {
reject(err);
return;
}
const stmt = db.prepare(`
INSERT INTO opportunities (source, title, url, type, priority)
VALUES (?, ?, ?, ?, ?)
`);
opportunities.forEach(opp => {
stmt.run(opp.source, opp.title, opp.url || '', opp.type, opp.priority);
opportunities.forEach((opp) => {
stmt.run(opp.source, opp.title, opp.url || "", opp.type, opp.priority);
});
stmt.finalize((err) => {
db.close();
if (err) reject(err);
@@ -350,45 +355,47 @@ function recordOpportunities(opportunities) {
* @returns {string} Formatted report
*/
function generateReport(result) {
let report = '=== Opportunity Scanning Report ===\n';
let report = "=== Opportunity Scanning Report ===\n";
report += `Timestamp: ${result.timestamp}\n\n`;
report += `Total opportunities: ${result.total_count}\n`;
report += `By source: GitHub=${result.by_source.github}, npm=${result.by_source.npm}, ClawHub=${result.by_source.clawhub}, Security=${result.by_source.security}\n\n`;
if (result.opportunities.length > 0) {
report += '📦 OPPORTUNITIES DETECTED:\n';
result.opportunities.forEach(opp => {
const icon = opp.priority === 'critical' ? '⚠️' : opp.priority === 'high' ? '🔴' : '📋';
report += "📦 OPPORTUNITIES DETECTED:\n";
result.opportunities.forEach((opp) => {
const icon = opp.priority === "critical" ? "⚠️" : opp.priority === "high" ? "🔴" : "📋";
report += ` ${icon} ${opp.title}\n`;
report += ` Source: ${opp.source}\n`;
report += ` Type: ${opp.type}\n`;
report += ` Priority: ${opp.priority}\n`;
if (opp.url) report += ` URL: ${opp.url}\n`;
report += '\n';
report += "\n";
});
} else {
report += '✅ No new opportunities detected\n';
report += "✅ No new opportunities detected\n";
}
report += '\n=== End Opportunity Scanning ===\n';
report += "\n=== End Opportunity Scanning ===\n";
return report;
}
// CLI execution
if (require.main === module) {
initDB().then(async () => {
const args = process.argv.slice(2);
const jsonOutput = args.includes('--json') || args.includes('-j');
const result = await scanOpportunities();
if (jsonOutput) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(generateReport(result));
}
}).catch(console.error);
initDB()
.then(async () => {
const args = process.argv.slice(2);
const jsonOutput = args.includes("--json") || args.includes("-j");
const result = await scanOpportunities();
if (jsonOutput) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(generateReport(result));
}
})
.catch(console.error);
}
// Export for module usage
@@ -399,5 +406,5 @@ module.exports = {
scanClawHubSkills,
scanSecurityAdvisories,
generateReport,
initDB
initDB,
};
+14 -14
View File
@@ -2,26 +2,26 @@
"name": "@heretek-ai/curiosity-engine-modules",
"version": "1.0.0",
"description": "Curiosity Engine modular implementation for Tabula Myriad triad",
"keywords": [
"anomaly-detection",
"autonomy",
"curiosity",
"deliberation",
"gap-detection",
"triad"
],
"license": "MIT",
"author": "Tabula Myriad <heretek-ai>",
"main": "modules/gap-detector.js",
"scripts": {
"test": "node tests/runner.js",
"run": "node curiosity-engine.js",
"gap": "node modules/gap-detector.js",
"anomaly": "node modules/anomaly-detector.js",
"opportunity": "node modules/opportunity-scanner.js",
"capability": "node modules/capability-mapper.js",
"gap": "node modules/gap-detector.js",
"opportunity": "node modules/opportunity-scanner.js",
"run": "node curiosity-engine.js",
"test": "node tests/runner.js",
"trigger": "node modules/deliberation-trigger.js"
},
"keywords": [
"curiosity",
"autonomy",
"triad",
"gap-detection",
"anomaly-detection",
"deliberation"
],
"author": "Tabula Myriad <heretek-ai>",
"license": "MIT",
"dependencies": {
"sqlite3": "^5.1.6"
},