Compare commits
2
Commits
d8989f9371
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66215eaf76 | ||
|
|
0bd719ab91 |
+23
-3
@@ -29,7 +29,7 @@ User preferences, confirmed decisions, validated fixes. Stored in:
|
|||||||
|
|
||||||
- **Structured Memory** (`memory/items/`) - JSON with metadata
|
- **Structured Memory** (`memory/items/`) - JSON with metadata
|
||||||
- **Project Memory** (`Projects/*/memory/`) - Per-project facts
|
- **Project Memory** (`Projects/*/memory/`) - Per-project facts
|
||||||
- **Vector DB** (future) - Semantic search for similar tasks
|
- **Cross-Project Patterns** (`patterns/`) - Reusable technical patterns loaded by the preprocessing pipeline
|
||||||
|
|
||||||
**Priority: HIGH** — Loaded at session start, refreshed as needed.
|
**Priority: HIGH** — Loaded at session start, refreshed as needed.
|
||||||
|
|
||||||
@@ -64,10 +64,11 @@ Step 2: LOAD PREFERENCES
|
|||||||
Step 3: LOAD RELEVANT MEMORY
|
Step 3: LOAD RELEVANT MEMORY
|
||||||
├── Vector search for similar past tasks
|
├── Vector search for similar past tasks
|
||||||
├── Load project STATUS.md
|
├── Load project STATUS.md
|
||||||
└── Load recent DECISIONS.md entries
|
├── Load recent DECISIONS.md entries
|
||||||
|
└── Load matching cross-project patterns from `patterns/` registry
|
||||||
|
|
||||||
Step 4: BUILD CONTEXT PACKET
|
Step 4: BUILD CONTEXT PACKET
|
||||||
├── Priority order: Rules → Prefs → Memory → Task
|
├── Priority order: Rules → Prefs → Memory → Patterns → Task
|
||||||
└── Truncate to fit context window
|
└── Truncate to fit context window
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -182,6 +183,25 @@ Enforce structure at system level:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Cross-Project Pattern Registry
|
||||||
|
|
||||||
|
The `patterns/` directory stores reusable technical lessons (workarounds, failure modes, design choices) that recur across projects. The preprocessing pipeline loads matching patterns into the context packet based on the task input.
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
1. `architecture/pipeline.js` reads `patterns/patterns.json`.
|
||||||
|
2. It matches task input against pattern `tags` and `affected_projects`.
|
||||||
|
3. Matching pattern files are injected as context with priority 6 (above generic memory, below project status).
|
||||||
|
4. Agents receive the pattern content and can apply the documented countermeasure.
|
||||||
|
|
||||||
|
### Adding patterns
|
||||||
|
1. Create `patterns/<pattern-id>.md` using the standard template.
|
||||||
|
2. Add an entry to `patterns/patterns.json`.
|
||||||
|
3. Test retrieval: `node architecture/pipeline.js "your task description"`.
|
||||||
|
|
||||||
|
This enables **generalization**: an agent encountering a new task can recognize a known shape and reuse a proven fix.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Implementation Status
|
## Implementation Status
|
||||||
|
|
||||||
| Component | Status | Location |
|
| Component | Status | Location |
|
||||||
|
|||||||
@@ -123,9 +123,13 @@ node architecture/orchestrator.js "your request here" --verbose
|
|||||||
- [x] Deploy validation layer with safety checks
|
- [x] Deploy validation layer with safety checks
|
||||||
- [x] Deploy format locker with auto-fix
|
- [x] Deploy format locker with auto-fix
|
||||||
- [x] Integrate all components via orchestrator
|
- [x] Integrate all components via orchestrator
|
||||||
|
- [x] Implement cross-project pattern registry for retrieval-augmented generalization
|
||||||
|
- [x] Implement feedback loop (`scripts/utils/propose-pattern.js`) so new workarounds can propose pattern updates
|
||||||
|
|
||||||
### Decisions Log
|
### Decisions Log
|
||||||
| Date | Decision | Rationale |
|
| Date | Decision | Rationale |
|
||||||
|------|----------|-----------|
|
|------|----------|-----------|
|
||||||
|
| 2026-08-06 | Create a centralized `patterns/` registry and wire it into `architecture/pipeline.js` | Captures recurring technical lessons across projects; enables agents to generalize known solutions to new tasks as the first concrete component of a continual-learning layer. |
|
||||||
|
| 2026-08-06 | Add `scripts/utils/propose-pattern.js` with dry-run default and `--apply` gate | Agents need a lightweight, auditable way to grow the registry from real fixes without silently rewriting shared knowledge. |
|
||||||
| 2026-07-03 | Create dedicated dev team | Need scalable capacity for multiple internal app projects |
|
| 2026-07-03 | Create dedicated dev team | Need scalable capacity for multiple internal app projects |
|
||||||
| 2026-07-03 | Python/FastAPI + HTMX stack | Matches existing skills, FastAPI's type safety, HTMX keeps frontend simple |
|
| 2026-07-03 | Python/FastAPI + HTMX stack | Matches existing skills, FastAPI's type safety, HTMX keeps frontend simple |
|
||||||
|
|||||||
@@ -56,36 +56,79 @@ For projects tracked in NocoDB (multi-agent runs, workflow executions), the data
|
|||||||
- Verbose blow-by-blow of exploratory debugging — memory holds the conclusion and the fix, not the full transcript of getting there
|
- Verbose blow-by-blow of exploratory debugging — memory holds the conclusion and the fix, not the full transcript of getting there
|
||||||
- Speculative future plans dressed up as decisions — DECISIONS.md is for choices actually made, not options being considered
|
- Speculative future plans dressed up as decisions — DECISIONS.md is for choices actually made, not options being considered
|
||||||
|
|
||||||
## Cross-Project Memory
|
## Cross-Project Patterns
|
||||||
|
|
||||||
Patterns that recur across multiple projects (not just one) get promoted to a shared note rather than duplicated per project — e.g., the Ollama structured-output workaround, JSON-escaping handling for LLM-generated content passed to downstream APIs, standard deployment conventions. This keeps a fix learned once from having to be relearned project by project.
|
### PowerShell Nested-Module WhatIf Propagation
|
||||||
|
|
||||||
|
When a PowerShell module imports helper modules via `Import-Module`, `$WhatIfPreference` does **not** automatically propagate across the nested-module boundary. If a root-module orchestrator calls a function in a nested module with `-WhatIf`, nested functions must either be in the same module scope or receive `-WhatIf:$WhatIfPreference` explicitly. Functions that themselves use `[CmdletBinding(SupportsShouldProcess=$true)]` will throw a duplicate-parameter error if passed `-WhatIf` explicitly, so Graph-dispatch helpers should use an explicit `[switch]$WhatIf` parameter instead.
|
||||||
|
|
||||||
|
Learned during: M365 Admin Toolkit Deployment Framework (2026-07-29).
|
||||||
|
|
||||||
|
### Cross-Project Pattern Registry
|
||||||
|
|
||||||
|
A centralized pattern registry now lives at `patterns/` in the workspace. It captures recurring technical lessons, workarounds, and failure modes across projects so agents can recognize known shapes and apply proven countermeasures.
|
||||||
|
|
||||||
|
See:
|
||||||
|
- `patterns/README.md` — registry guide and template
|
||||||
|
- `patterns/patterns.json` — machine-readable manifest
|
||||||
|
- `patterns/*.md` — individual pattern files
|
||||||
|
|
||||||
|
Patterns are automatically loaded by `architecture/pipeline.js` when the task input matches a pattern's tags or affected projects. This is the generalization layer of the agent continual-learning system.
|
||||||
|
|
||||||
|
**Feedback loop:** when an agent applies a reusable workaround or fix, it should run `scripts/utils/propose-pattern.js` to propose a new pattern or extend an existing one. The helper is dry-run by default; use `--apply` only after human review. This is the plasticity layer — the registry grows from real work instead of being reconstructed from memory later.
|
||||||
|
|
||||||
|
Established: 2026-08-06.
|
||||||
|
|
||||||
|
### Small Local Ollama Models and Malformed JSON
|
||||||
|
|
||||||
|
Standing, deliberate fix: a JavaScript Code node with regex-based JSON extraction as a safety net. Document it as a workaround where applied.
|
||||||
|
|
||||||
|
Pattern file: `patterns/ollama-structured-output-fallback.md`.
|
||||||
|
|
||||||
|
### JSON Escaping for LLM-Generated Downstream API Payloads
|
||||||
|
|
||||||
|
Defensive handling by default when passing LLM output into API calls (LinkedIn posts, video generation payloads, etc.).
|
||||||
|
|
||||||
|
Pattern file: `patterns/json-escaping-downstream-api.md`.
|
||||||
|
|
||||||
|
### Coding Task Delegation Default
|
||||||
|
|
||||||
|
**Default to native OpenClaw subagents (`runtime: "subagent"`) for routine coding tasks.**
|
||||||
|
|
||||||
|
- Background ACP runs (`runtime: "acp"`, `mode: "run"`) fail in this environment with `AcpRuntimeError [ACP_TURN_FAILED]: Permission prompt unavailable in non-interactive mode` because the host cannot display approval prompts for unattended ACP turns.
|
||||||
|
- Native subagents inherit `agents.defaults.model.primary`, currently `ollama/kimi-k2.6:cloud`, which is sufficient for well-scoped coding work.
|
||||||
|
- Use ACP / OpenCode / Claude Code / Codex harnesses **only when explicitly requested** and run them in a chat-bound/foreground context rather than background mode.
|
||||||
|
|
||||||
|
Established: 2026-08-02.
|
||||||
|
|
||||||
|
### API Version Deprecation
|
||||||
|
|
||||||
|
Note the API version in use and where to check for deprecation notices for any new integration.
|
||||||
|
|
||||||
## Failure Mode I'm Guarding Against
|
## Failure Mode I'm Guarding Against
|
||||||
|
|
||||||
The single worst outcome for this system is confident, stale memory — a STATUS.md that says something is fine when it isn't, or a RUNBOOK.md that no longer matches how the app actually deploys. When I'm not sure memory is current, I verify against the live system before trusting it, and I correct the record immediately if it's wrong. Memory that isn't kept honest is worse than no memory at all.
|
The single worst outcome for this system is confident, stale memory — a STATUS.md that says something is fine when it isn't, or a RUNBOOK.md that no longer matches how the app actually deploys. When I'm not sure memory is current, I verify against the live system before trusting it, and I correct the record immediately if it's wrong. Memory that isn't kept honest is worse than no memory at all.
|
||||||
|
|
||||||
## Promoted From Short-Term Memory (2026-07-11)
|
## Promoted From Short-Term Memory (2026-08-02)
|
||||||
|
|
||||||
<!-- openclaw-memory-promotion:memory:memory/2026-07-04.md:34:34 -->
|
<!-- openclaw-memory-promotion:memory:memory/2026-07-25.md:1:38 -->
|
||||||
- Verification: Data persistence working correctly [score=0.925 recalls=0 avg=0.620 source=memory/2026-07-04.md:34-34]
|
- # Memory: 2026-07-25 ## Nextcloud AIO Setup (bve.beawit.net) ### VM Configuration - **VMID**: 102 (Proxmox on bve.beawit.net) - **Hostname**: nextcloud-aio - **IP**: 192.168.0.149 - **RAM**: 16GB (resized from 8GB for production use) - **Disk**: 100GB - **Swap**: 4GB file at `/swapfile` - **OS**: Debian 12 cloud-init ### AIO Configuration - Running with `--network host` so Apache binds directly to VM IP (avoids Docker network complexity for NPM reverse proxy) - `APACHE_PORT=11000`, `APACHE_IP_BINDING=0.0.0.0`, `SKIP_DOMAIN_VALIDATION=true` - All optional services enabled: ClamAV, Collabora, Talk, Imaginary, Whiteboard,... [score=0.905 recalls=3 avg=0.634 source=memory/2026-07-25.md:1-38]
|
||||||
<!-- openclaw-memory-promotion:memory:memory/2026-07-04.md:44:46 -->
|
<!-- openclaw-memory-promotion:memory:memory/2026-07-26.md:27:27 -->
|
||||||
- Final Implementation Status: Client management features: save, edit, delete, send to n8n; No scrolling required - full form visible at once; Service running on hosting-manager at http://192.168.50.11:5000/ [score=0.848 recalls=0 avg=0.620 source=memory/2026-07-04.md:44-46]
|
- Nextcloud AIO Setup (bve.beawit.net): **Talk Backend Version Mismatch**: Talk app 24.0.3 vs signaling server 2.1.1~docker — requires AIO update to resolve (non-critical) [score=0.812 recalls=0 avg=0.620 source=memory/2026-07-26.md:27-27]
|
||||||
<!-- openclaw-memory-promotion:memory:memory/2026-07-04.md:49:52 -->
|
<!-- openclaw-memory-promotion:memory:memory/2026-07-27.md:12:14 -->
|
||||||
- Key Features Delivered: Statistics dashboard with user metrics; Complete client data form with all 13 required fields; Local storage persistence for saved clients; Client management interface with edit/delete/send actions [score=0.848 recalls=0 avg=0.620 source=memory/2026-07-04.md:49-52]
|
- Solution Implemented: Installed `pexpect` on the server (pty-enabled subprocess); Wrote `app_pexpect.py` that uses `pexpect.spawn()` with PTY to capture console output; Need to deploy this version to the server [score=0.812 recalls=0 avg=0.620 source=memory/2026-07-27.md:12-14]
|
||||||
<!-- openclaw-memory-promotion:memory:memory/2026-07-04.md:53:54 -->
|
<!-- openclaw-memory-promotion:memory:memory/2026-07-27.md:6:6 -->
|
||||||
- Key Features Delivered: n8n webhook integration for workflow automation; Responsive dark-themed UI matching existing applications [score=0.848 recalls=0 avg=0.620 source=memory/2026-07-04.md:53-54]
|
- Problem: When running a check from the web app, PowerShell hangs at `Connect-MgGraph -UseDeviceAuthentication` because the device code output is NOT being captured/displayed. The user never sees the code to enter in their browser. [score=0.812 recalls=0 avg=0.620 source=memory/2026-07-27.md:6-6]
|
||||||
<!-- openclaw-memory-promotion:memory:memory/2026-07-04.md:57:60 -->
|
<!-- openclaw-memory-promotion:memory:memory/2026-07-27.md:9:9 -->
|
||||||
- Issues Resolved: Fixed form scrolling issue - now displays full form without scrollbars; Implemented proper client data display after saving; Enhanced n8n integration with better error handling; Added comprehensive client management features [score=0.848 recalls=0 avg=0.620 source=memory/2026-07-04.md:57-60]
|
- Root Cause: `Connect-MgGraph -UseDeviceAuthentication` writes the device code using PowerShell's **console host**, not stdout. When run via `subprocess.Popen` without a PTY (pseudo-terminal), the console output is not captured. [score=0.812 recalls=0 avg=0.620 source=memory/2026-07-27.md:9-9]
|
||||||
<!-- openclaw-memory-promotion:memory:memory/2026-07-04.md:6:9 -->
|
<!-- openclaw-memory-promotion:memory:memory/2026-07-27.md:21:21 -->
|
||||||
- Final Implementation Status: Deployed client onboarding application with all required features; Implemented cards showing Total Users and Completed Users at top; All 13 n8n workflow fields included and functional; Dark theme maintained from IT Site Survey AI [score=0.848 recalls=0 avg=0.620 source=memory/2026-07-04.md:6-9]
|
- Files Ready for Deploy: `/tmp/defender_status.ps1` — PowerShell with device code auth [score=0.812 recalls=0 avg=0.620 source=memory/2026-07-27.md:21-21]
|
||||||
<!-- openclaw-memory-promotion:memory:memory/2026-07-04.md:61:61 -->
|
<!-- openclaw-memory-promotion:memory:memory/2026-07-27.md:24:27 -->
|
||||||
- Issues Resolved: Maintained consistent dark theme throughout [score=0.848 recalls=0 avg=0.620 source=memory/2026-07-04.md:61-61]
|
- Next Steps: Deploy `app_pexpect.py` to `/home/jcbeasley/applications/active/intune-inspector/app.py`; Copy PowerShell `.ps1` files to `powershell/` directory; Kill any stuck PowerShell processes; Restart the app [score=0.812 recalls=0 avg=0.620 source=memory/2026-07-27.md:24-27]
|
||||||
<!-- openclaw-memory-promotion:memory:memory/2026-07-04.md:64:67 -->
|
<!-- openclaw-memory-promotion:memory:memory/2026-07-27.md:17:20 -->
|
||||||
- Verification: Service restarted and confirmed running; All features tested and working; Application accessible at http://192.168.50.11:5000/; n8n webhook integration functional [score=0.848 recalls=0 avg=0.620 source=memory/2026-07-04.md:64-67]
|
- Files Ready for Deploy: `/tmp/app_pexpect.py` — Flask app with pexpect PTY support; `/tmp/intune_enrollment.ps1` — PowerShell with device code auth; `/tmp/conditional_access.ps1` — PowerShell with device code auth; `/tmp/security_defaults.ps1` — PowerShell with device code auth [score=0.812 recalls=0 avg=0.620 source=memory/2026-07-27.md:17-20]
|
||||||
<!-- openclaw-memory-promotion:memory:memory/2026-07-04.md:68:68 -->
|
|
||||||
- Verification: Data persistence working correctly [score=0.838 recalls=0 avg=0.620 source=memory/2026-07-04.md:68-68]
|
|
||||||
|
|
||||||
## Promoted From Short-Term Memory (2026-07-17)
|
## Promoted From Short-Term Memory (2026-08-03)
|
||||||
|
|
||||||
<!-- openclaw-memory-promotion:memory:memory/2026-07-03.md:28:70 -->
|
<!-- openclaw-memory-promotion:memory:memory/2026-07-27.md:28:28 -->
|
||||||
- Installed missing packages: flask_cors, requests, reportlab - All applications now start properly ## Final Directory Structure ``` /home/jcbeasley/applications/ ├── active/ │ ├── client-onboarding/ # Port 5000 - Running │ ├── it-site-survey-ai/ # Port 3003 - Running │ ├── projects-manager/ # Port 3456 - Running │ ├── projects-manager-hosting/ # Active and integrated │ └── shorts-analyzer/ # Port 3001 - Running ├── archived/ │ ├── client-onboarding-old/ │ └── shorts-analyzer-old/ └── development/ ├── dark-web-monitor/ ├── it-assessment-ai/ └── it-assessment-static/ ``` ## Current Status All applications running normally with proper... [score=0.860 recalls=4 avg=0.549 source=memory/2026-07-03.md:28-70]
|
- Next Steps: Test device code capture [score=0.802 recalls=0 avg=0.620 source=memory/2026-07-27.md:28-28]
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ class AgentOrchestrator {
|
|||||||
// Load relevant memory
|
// Load relevant memory
|
||||||
await this.pipeline.loadRelevantMemory(userInput, 5);
|
await this.pipeline.loadRelevantMemory(userInput, 5);
|
||||||
|
|
||||||
|
// Load cross-project patterns based on task input
|
||||||
|
await this.pipeline.loadPatterns(userInput);
|
||||||
|
|
||||||
// Build final packet
|
// Build final packet
|
||||||
const packet = this.pipeline.buildPacket(userInput);
|
const packet = this.pipeline.buildPacket(userInput);
|
||||||
|
|
||||||
@@ -127,12 +130,22 @@ class AgentOrchestrator {
|
|||||||
'# Current Context',
|
'# Current Context',
|
||||||
`Project: ${contextPacket.project || 'None'}`,
|
`Project: ${contextPacket.project || 'None'}`,
|
||||||
`User: ${contextPacket.user || 'Unknown'}`,
|
`User: ${contextPacket.user || 'Unknown'}`,
|
||||||
|
'',
|
||||||
|
'---',
|
||||||
|
'',
|
||||||
|
'# Preferences',
|
||||||
|
contextPacket.preferences.substring(0, 2000) // Truncate for brevity
|
||||||
|
];
|
||||||
|
|
||||||
|
if (contextPacket.memory && contextPacket.memory.trim()) {
|
||||||
|
parts.push(
|
||||||
'',
|
'',
|
||||||
'---',
|
'---',
|
||||||
'',
|
'',
|
||||||
'# Memory',
|
'# Memory',
|
||||||
contextPacket.memory.substring(0, 1000) // Truncate for brevity
|
contextPacket.memory.substring(0, 1000) // Truncate for brevity
|
||||||
];
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return parts.join('\n');
|
return parts.join('\n');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,51 @@ class ContextPipeline {
|
|||||||
this.rules = [];
|
this.rules = [];
|
||||||
this.preferences = [];
|
this.preferences = [];
|
||||||
this.memory = [];
|
this.memory = [];
|
||||||
|
this.patterns = [];
|
||||||
this.context = {};
|
this.context = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 3b: Load Relevant Cross-Project Patterns
|
||||||
|
*/
|
||||||
|
async loadPatterns(taskInput) {
|
||||||
|
const manifestPath = path.join('/home/jcbeasley/.openclaw/workspace', 'patterns', 'patterns.json');
|
||||||
|
try {
|
||||||
|
const manifestRaw = fs.readFileSync(manifestPath, 'utf8');
|
||||||
|
const manifest = JSON.parse(manifestRaw);
|
||||||
|
const query = (taskInput || '').toLowerCase();
|
||||||
|
|
||||||
|
for (const pattern of manifest.patterns || []) {
|
||||||
|
const tokens = [
|
||||||
|
...(pattern.tags || []),
|
||||||
|
...(pattern.affected_projects || []),
|
||||||
|
pattern.id,
|
||||||
|
pattern.name
|
||||||
|
].map(t => t.toLowerCase());
|
||||||
|
|
||||||
|
const matched = tokens.some(token => query.includes(token));
|
||||||
|
if (!matched) continue;
|
||||||
|
|
||||||
|
const patternFile = path.join('/home/jcbeasley/.openclaw/workspace', pattern.file);
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(patternFile, 'utf8');
|
||||||
|
this.patterns.push({
|
||||||
|
source: pattern.file,
|
||||||
|
content: content,
|
||||||
|
priority: 6,
|
||||||
|
id: pattern.id
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// Pattern file missing; skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Manifest missing or invalid; skip pattern loading
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step 1: Load Behavior Rules (always injected)
|
* Step 1: Load Behavior Rules (always injected)
|
||||||
*/
|
*/
|
||||||
@@ -83,7 +125,7 @@ class ContextPipeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step 3: Load Relevant Memory
|
* Step 3a: Load Relevant Memory
|
||||||
*/
|
*/
|
||||||
async loadRelevantMemory(query, maxResults = 5) {
|
async loadRelevantMemory(query, maxResults = 5) {
|
||||||
// For now, load recent episodic memories
|
// For now, load recent episodic memories
|
||||||
@@ -129,7 +171,8 @@ class ContextPipeline {
|
|||||||
const allContext = [
|
const allContext = [
|
||||||
...this.rules,
|
...this.rules,
|
||||||
...this.preferences.sort((a, b) => b.priority - a.priority),
|
...this.preferences.sort((a, b) => b.priority - a.priority),
|
||||||
...this.memory
|
...this.memory,
|
||||||
|
...this.patterns
|
||||||
];
|
];
|
||||||
|
|
||||||
const packet = {
|
const packet = {
|
||||||
@@ -140,7 +183,8 @@ class ContextPipeline {
|
|||||||
metadata: {
|
metadata: {
|
||||||
rules_count: this.rules.length,
|
rules_count: this.rules.length,
|
||||||
prefs_count: this.preferences.length,
|
prefs_count: this.preferences.length,
|
||||||
memory_count: this.memory.length
|
memory_count: this.memory.length,
|
||||||
|
patterns_count: this.patterns.length
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -184,6 +228,7 @@ if (require.main === module) {
|
|||||||
.loadRules()
|
.loadRules()
|
||||||
.then(() => pipeline.loadPreferences())
|
.then(() => pipeline.loadPreferences())
|
||||||
.then(() => pipeline.loadRelevantMemory(process.argv[2] || ''))
|
.then(() => pipeline.loadRelevantMemory(process.argv[2] || ''))
|
||||||
|
.then(() => pipeline.loadPatterns(process.argv[2] || ''))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const packet = pipeline.buildPacket(process.argv[2] || 'No task specified');
|
const packet = pipeline.buildPacket(process.argv[2] || 'No task specified');
|
||||||
console.log(JSON.stringify(packet, null, 2));
|
console.log(JSON.stringify(packet, null, 2));
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Cross-Project Technical Pattern Registry
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This registry captures recurring technical lessons, workarounds, design choices, and failure modes that appear across Beawit's applications and automation workflows. It exists so agents can recognize when a new task is an instance of a known pattern and apply proven countermeasures instead of rediscovering the problem.
|
||||||
|
|
||||||
|
This is the **generalization layer** of the agent continual-learning system:
|
||||||
|
- **Plasticity**: new patterns are added as they are discovered.
|
||||||
|
- **Stability**: old patterns are versioned and never silently overwritten.
|
||||||
|
- **Generalization**: patterns are retrieved by symptom, cause, or affected domain and injected into task context.
|
||||||
|
|
||||||
|
## Registry Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
patterns/
|
||||||
|
├── README.md # This file
|
||||||
|
├── patterns.json # Machine-readable manifest
|
||||||
|
├── ollama-structured-output-fallback.md
|
||||||
|
├── json-escaping-downstream-api.md
|
||||||
|
├── api-version-deprecation.md
|
||||||
|
├── pty-device-code-auth.md
|
||||||
|
├── queue-poll-async-job.md
|
||||||
|
├── llm-as-parser-fallback.md
|
||||||
|
├── credential-rotation-recovery.md
|
||||||
|
├── reverse-proxy-container-binding.md
|
||||||
|
├── human-approval-risky-publish.md
|
||||||
|
└── transient-failure-retry.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pattern File Template
|
||||||
|
|
||||||
|
Every `.md` pattern uses the same sections:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Pattern: <Short Name>
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
What the agent or user sees when the pattern is active.
|
||||||
|
|
||||||
|
## Affected Projects
|
||||||
|
List of known projects/workflows where this pattern has occurred.
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
Why it happens.
|
||||||
|
|
||||||
|
## Standard Fix
|
||||||
|
The proven workaround, safety net, or design choice.
|
||||||
|
|
||||||
|
## When to Apply
|
||||||
|
Trigger conditions for applying this pattern to a new task.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
How to confirm the fix actually worked.
|
||||||
|
|
||||||
|
## Related Patterns
|
||||||
|
Links to other patterns that often appear together.
|
||||||
|
```
|
||||||
|
|
||||||
|
## How Patterns Are Used
|
||||||
|
|
||||||
|
1. **Manual reference** — read the registry before designing a new integration or workflow.
|
||||||
|
2. **Automatic retrieval** — `architecture/pipeline.js` loads relevant patterns into the context packet based on keyword matching against the task input.
|
||||||
|
3. **Skill/workflow design** — when a new workaround is applied, propose a new pattern entry if the underlying shape is reusable.
|
||||||
|
|
||||||
|
## Feedback Loop: Proposing New Patterns
|
||||||
|
|
||||||
|
When an agent applies a workaround or fixes a recurring failure, it should ask: *"is this lesson reusable across projects?"* If yes, run the proposal helper:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/utils/propose-pattern.js \
|
||||||
|
--task "n8n workflow sends LLM-generated text to LinkedIn" \
|
||||||
|
--fix "JSON.stringify the post body and validate with JSON.parse before the HTTP Request node" \
|
||||||
|
--projects "linkedin-automation" \
|
||||||
|
--symptom "LinkedIn API returns malformed JSON payload errors" \
|
||||||
|
--root-cause "LLM output contains unescaped quotes and newlines"
|
||||||
|
```
|
||||||
|
|
||||||
|
The helper:
|
||||||
|
1. Loads the existing `patterns/patterns.json` manifest.
|
||||||
|
2. Compares the new lesson against existing patterns using keyword overlap.
|
||||||
|
3. Proposes either:
|
||||||
|
- **An update** to an existing pattern (e.g., add an affected project), or
|
||||||
|
- **A new pattern** file + manifest entry.
|
||||||
|
4. Outputs a preview. By default it is **dry-run only**.
|
||||||
|
5. If the preview looks right, run the same command with `--apply` to write the files.
|
||||||
|
|
||||||
|
After applying, run `node architecture/pipeline.js "sample query"` to verify the new or updated pattern is retrievable.
|
||||||
|
|
||||||
|
## Adding or Updating a Pattern Manually
|
||||||
|
|
||||||
|
1. Create or edit the `.md` file.
|
||||||
|
2. Update `patterns.json` with id, tags, related patterns, and affected projects.
|
||||||
|
3. Run `node architecture/pipeline.js "sample query"` to verify the pattern is retrievable.
|
||||||
|
4. Update `MEMORY.md` Cross-Project Patterns if the pattern is stable and reusable.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Pattern: API Version Deprecation
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
An integration that worked yesterday starts returning 400/401/404 or generic errors. Vendor documentation mentions an older API version is deprecated or sunset. Calls using the old version silently stop working or return migration messages.
|
||||||
|
|
||||||
|
## Affected Projects
|
||||||
|
- LinkedIn content automation (LinkedIn REST API version deprecation)
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
External SaaS APIs version their endpoints and periodically retire old versions. Hard-coded version strings in workflows or apps become liabilities when the vendor changes the supported window.
|
||||||
|
|
||||||
|
## Standard Fix
|
||||||
|
1. Record the API version in use at design time, plus where to check for deprecation notices (vendor developer portal, changelog, email alerts).
|
||||||
|
2. Centralize API version strings in environment variables or config, not scattered through code.
|
||||||
|
3. Subscribe to vendor developer changelogs or status pages.
|
||||||
|
4. Plan version migration as a tracked task rather than an emergency fix.
|
||||||
|
5. Add a lightweight health check that verifies the integration still responds with expected schema/version fields.
|
||||||
|
|
||||||
|
## When to Apply
|
||||||
|
- Every new external API integration.
|
||||||
|
- Any existing integration that has no documented API version or deprecation monitoring.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- Vendor docs show the version is current and supported.
|
||||||
|
- Health check returns expected version/schema fields.
|
||||||
|
- A deprecation monitoring source is identified and checked.
|
||||||
|
|
||||||
|
## Related Patterns
|
||||||
|
- `credential-rotation-recovery`
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Pattern: Credential Rotation and External Service Breakage
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
An integration that previously worked starts failing with authentication errors, 401/403 responses, or "invalid token" messages. No code has changed. The root cause is an expired or rotated API key, AppRole secret, OAuth token, or vault credential.
|
||||||
|
|
||||||
|
## Affected Projects
|
||||||
|
- n8n workflows using AppRole or API keys
|
||||||
|
- NocoDB token-based integrations
|
||||||
|
- Any service using HashiCorp Vault AppRole authentication
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
Credentials have finite lifetimes or rotation policies. If the integration has no renewal path and the credential is stored in only one place, expiry causes immediate breakage that looks like a code problem.
|
||||||
|
|
||||||
|
## Standard Fix
|
||||||
|
1. Store credentials in Vault, not in code, config files, or workflow nodes.
|
||||||
|
2. Document credential lifetime and rotation procedure in the project RUNBOOK.
|
||||||
|
3. Add health checks that verify credential validity without performing risky operations.
|
||||||
|
4. Where possible, implement token refresh or AppRole re-login rather than relying on static long-lived tokens.
|
||||||
|
5. Alert before expiry, not after.
|
||||||
|
|
||||||
|
## When to Apply
|
||||||
|
- Every new integration that uses an API key, token, password, or secret.
|
||||||
|
- Any existing integration that has no documented credential source or rotation plan.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- Health check passes using the stored credential.
|
||||||
|
- Rotation procedure has been tested end-to-end.
|
||||||
|
- No secrets are committed to version control.
|
||||||
|
|
||||||
|
## Related Patterns
|
||||||
|
- `api-version-deprecation`
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Pattern: Human Approval Gate for Risky Publish Operations
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
An automation publishes or sends content on behalf of the organization (LinkedIn post, client email, newsletter). A mistake in generated content, targeting, or timing causes embarrassment, compliance risk, or customer impact. Once sent, it cannot be recalled.
|
||||||
|
|
||||||
|
## Affected Projects
|
||||||
|
- LinkedIn content automation
|
||||||
|
- Cyber Tips Newsletter pipeline
|
||||||
|
- Email inbox triage agent (if it sends replies)
|
||||||
|
- Monthly IT newsletter generator
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
LLM-generated content is probabilistic and may contain hallucinations, wrong tone, outdated facts, or malformed formatting. Fully automated publishing removes the human sanity check.
|
||||||
|
|
||||||
|
## Standard Fix
|
||||||
|
1. Generate the draft and present it for human review before any publish/send action.
|
||||||
|
2. Separate "draft" and "publish" stages in the workflow.
|
||||||
|
3. Require explicit confirmation (button click, message reaction, approval field) for the publish step.
|
||||||
|
4. Log who approved what and when.
|
||||||
|
5. Provide an easy cancellation path before the deadline.
|
||||||
|
|
||||||
|
## When to Apply
|
||||||
|
- Any workflow that sends client-facing communications.
|
||||||
|
- Any workflow that posts to public or branded channels.
|
||||||
|
- Any workflow where content cannot be retracted after execution.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- Draft stage produces reviewable content.
|
||||||
|
- Publish step does not execute without explicit approval.
|
||||||
|
- Audit log records approver, timestamp, and content hash/summary.
|
||||||
|
|
||||||
|
## Related Patterns
|
||||||
|
- None yet.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Pattern: JSON Escaping for Downstream APIs
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
An n8n workflow passes LLM-generated text into a downstream API call, and the request fails with a JSON parse error, malformed payload, or unexpected truncation. The generated text contains quotes, newlines, backslashes, emojis, or control characters that break JSON encoding.
|
||||||
|
|
||||||
|
## Affected Projects
|
||||||
|
- LinkedIn content automation (LinkedIn REST API posts)
|
||||||
|
- AI video generation pipeline (ComfyUI / JSON2Video payloads)
|
||||||
|
- Any n8n workflow that injects LLM output into an HTTP Request node body
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
LLMs produce human-readable text; downstream APIs consume machine-readable JSON. Naive string concatenation or weak JSON serialization allows unescaped characters to corrupt the payload. The failure often appears at the receiving API, making root-cause diagnosis slower.
|
||||||
|
|
||||||
|
## Standard Fix
|
||||||
|
1. Treat LLM output as untrusted string data.
|
||||||
|
2. Always serialize it through a proper JSON encoder (`JSON.stringify` in JS, `json.dumps` in Python) before embedding in a payload.
|
||||||
|
3. If building a payload string manually, escape quotes, backslashes, newlines, and control characters; better, avoid manual string building entirely.
|
||||||
|
4. Add a validation step that parses the final payload with `JSON.parse` before sending.
|
||||||
|
5. For n8n, prefer expression mapping through structured fields rather than raw body strings.
|
||||||
|
|
||||||
|
## When to Apply
|
||||||
|
- Any new integration where LLM-generated content becomes part of an API request body.
|
||||||
|
- Any HTTP Request node in n8n that builds a JSON body from expressions containing LLM output.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- Test with adversarial LLM output containing quotes, newlines, unicode, and backslashes.
|
||||||
|
- Confirm the receiving API parses the payload correctly.
|
||||||
|
- Log payload shape (without secrets) for debugging.
|
||||||
|
|
||||||
|
## Related Patterns
|
||||||
|
- `ollama-structured-output-fallback`
|
||||||
|
- `llm-as-parser-fallback`
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Pattern: LLM-as-Parser with Structured Fallback
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
A workflow asks an LLM to parse, classify, or extract information from unstructured input. Sometimes the output is correct but poorly formatted; sometimes it is wrong or inconsistent. Downstream nodes cannot rely on it without a validation step.
|
||||||
|
|
||||||
|
## Affected Projects
|
||||||
|
- Cyber Tips Newsletter pipeline
|
||||||
|
- Proxmox VE snapshot summarizer
|
||||||
|
- Email inbox triage agent
|
||||||
|
- Monthly IT newsletter generator
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
Using an LLM as a parser combines the power of fuzzy reasoning with the fragility of probabilistic output. Without a structured fallback, the pipeline is brittle.
|
||||||
|
|
||||||
|
## Standard Fix
|
||||||
|
1. Ask the LLM for structured output (JSON/schema) when possible.
|
||||||
|
2. Add a validation layer that checks required fields and value ranges.
|
||||||
|
3. Add a regex or rule-based extraction fallback for common failure modes.
|
||||||
|
4. For classification tasks, maintain a small deterministic ruleset for high-confidence cases and use the LLM only for ambiguous cases.
|
||||||
|
5. Log parse failures to identify when the LLM is drifting.
|
||||||
|
|
||||||
|
## When to Apply
|
||||||
|
- Any workflow where an LLM extracts or classifies data that downstream nodes consume.
|
||||||
|
- Any pipeline where consistency matters more than creative interpretation.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- Test with malformed, ambiguous, and adversarial inputs.
|
||||||
|
- Confirm downstream nodes receive validated, structured data.
|
||||||
|
- Review parse-failure logs periodically.
|
||||||
|
|
||||||
|
## Related Patterns
|
||||||
|
- `ollama-structured-output-fallback`
|
||||||
|
- `json-escaping-downstream-api`
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Pattern: Ollama Structured Output Fallback
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
A workflow calls a local Ollama model (e.g., `gemma3:4b`, small quantized models) with a structured-output or JSON schema request, but the returned text is not valid JSON, misses required fields, wraps the JSON in prose, or otherwise fails schema validation.
|
||||||
|
|
||||||
|
## Affected Projects
|
||||||
|
- Cyber Tips Newsletter pipeline
|
||||||
|
- Proxmox VE snapshot summarizer
|
||||||
|
- AI video generation pipeline (ComfyUI + WAN 2.1 payloads)
|
||||||
|
- Forex trading analysis workflow
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
Small local instruction-tuned models have weaker schema adherence than frontier APIs. They may produce JSON-like text that does not strictly conform to the requested schema, especially under complex prompts or when asked to combine generation with strict formatting.
|
||||||
|
|
||||||
|
## Standard Fix
|
||||||
|
Add a downstream JavaScript Code node (or equivalent parser) in n8n that:
|
||||||
|
1. Attempts a strict `JSON.parse()` first.
|
||||||
|
2. On failure, applies regex-based JSON extraction to pull the first `{...}` or `[...]` block from the text.
|
||||||
|
3. Optionally sanitizes common issues (trailing commas, unescaped newlines, code fences).
|
||||||
|
4. Falls back to a safe default or error flag if extraction still fails.
|
||||||
|
|
||||||
|
This is a deliberate workaround, not a substitute for fixing the model. Document it as such wherever applied.
|
||||||
|
|
||||||
|
## When to Apply
|
||||||
|
- Any new n8n workflow that uses a local Ollama model for structured extraction, classification, or JSON generation.
|
||||||
|
- Any integration where the downstream node requires strict JSON and the LLM is under ~8B parameters or known to drift.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- Test the fallback with intentionally malformed LLM output.
|
||||||
|
- Confirm downstream nodes receive valid parsed JSON.
|
||||||
|
- Log fallback events so model quality can be monitored separately.
|
||||||
|
|
||||||
|
## Related Patterns
|
||||||
|
- `json-escaping-downstream-api`
|
||||||
|
- `llm-as-parser-fallback`
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
{
|
||||||
|
"version": "2026-08-06",
|
||||||
|
"description": "Cross-project technical pattern registry for retrieval-augmented generalization",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"id": "ollama-structured-output-fallback",
|
||||||
|
"name": "Ollama Structured Output Fallback",
|
||||||
|
"file": "patterns/ollama-structured-output-fallback.md",
|
||||||
|
"tags": ["ollama", "json", "structured-output", "local-llm", "n8n", "safety-net", "regex"],
|
||||||
|
"affected_projects": [
|
||||||
|
"cyber-tips-newsletter",
|
||||||
|
"proxmox-snapshot-summarizer",
|
||||||
|
"ai-video-generation",
|
||||||
|
"forex-trading-analysis"
|
||||||
|
],
|
||||||
|
"related_patterns": [
|
||||||
|
"json-escaping-downstream-api",
|
||||||
|
"llm-as-parser-fallback"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "json-escaping-downstream-api",
|
||||||
|
"name": "JSON Escaping for Downstream APIs",
|
||||||
|
"file": "patterns/json-escaping-downstream-api.md",
|
||||||
|
"tags": ["json", "escaping", "llm-output", "api-payload", "n8n", "linkedin", "video-generation"],
|
||||||
|
"affected_projects": [
|
||||||
|
"linkedin-automation",
|
||||||
|
"ai-video-generation",
|
||||||
|
"n8n-general"
|
||||||
|
],
|
||||||
|
"related_patterns": [
|
||||||
|
"ollama-structured-output-fallback",
|
||||||
|
"llm-as-parser-fallback"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api-version-deprecation",
|
||||||
|
"name": "API Version Deprecation",
|
||||||
|
"file": "patterns/api-version-deprecation.md",
|
||||||
|
"tags": ["api", "versioning", "deprecation", "integration", "breaking-change", "linkedin"],
|
||||||
|
"affected_projects": [
|
||||||
|
"linkedin-automation"
|
||||||
|
],
|
||||||
|
"related_patterns": [
|
||||||
|
"credential-rotation-recovery"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pty-device-code-auth",
|
||||||
|
"name": "PTY-Required Device Code Authentication",
|
||||||
|
"file": "patterns/pty-device-code-auth.md",
|
||||||
|
"tags": ["powershell", "pty", "device-code", "authentication", "microsoft-graph", "intune-inspector", "pexpect"],
|
||||||
|
"affected_projects": [
|
||||||
|
"intune-inspector",
|
||||||
|
"m365-admin-toolkit"
|
||||||
|
],
|
||||||
|
"related_patterns": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "queue-poll-async-job",
|
||||||
|
"name": "Queue-Poll Async Job Pattern",
|
||||||
|
"file": "patterns/queue-poll-async-job.md",
|
||||||
|
"tags": ["async", "queue", "poll", "long-running", "comfyui", "ollama", "n8n"],
|
||||||
|
"affected_projects": [
|
||||||
|
"ai-video-generation",
|
||||||
|
"n8n-general"
|
||||||
|
],
|
||||||
|
"related_patterns": [
|
||||||
|
"transient-failure-retry"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "llm-as-parser-fallback",
|
||||||
|
"name": "LLM-as-Parser with Structured Fallback",
|
||||||
|
"file": "patterns/llm-as-parser-fallback.md",
|
||||||
|
"tags": ["llm", "parsing", "structured-output", "fallback", "validation", "regex"],
|
||||||
|
"affected_projects": [
|
||||||
|
"cyber-tips-newsletter",
|
||||||
|
"proxmox-snapshot-summarizer",
|
||||||
|
"email-inbox-triage",
|
||||||
|
"monthly-it-newsletter"
|
||||||
|
],
|
||||||
|
"related_patterns": [
|
||||||
|
"ollama-structured-output-fallback",
|
||||||
|
"json-escaping-downstream-api"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "credential-rotation-recovery",
|
||||||
|
"name": "Credential Rotation and External Service Breakage",
|
||||||
|
"file": "patterns/credential-rotation-recovery.md",
|
||||||
|
"tags": ["credentials", "vault", "rotation", "token", "api-key", "secrets", "approle"],
|
||||||
|
"affected_projects": [
|
||||||
|
"n8n-general",
|
||||||
|
"nocodb",
|
||||||
|
"vault"
|
||||||
|
],
|
||||||
|
"related_patterns": [
|
||||||
|
"api-version-deprecation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "reverse-proxy-container-binding",
|
||||||
|
"name": "Reverse Proxy + Container Port Binding",
|
||||||
|
"file": "patterns/reverse-proxy-container-binding.md",
|
||||||
|
"tags": ["reverse-proxy", "nginx-proxy-manager", "docker", "container", "port-binding", "nextcloud"],
|
||||||
|
"affected_projects": [
|
||||||
|
"nextcloud-aio"
|
||||||
|
],
|
||||||
|
"related_patterns": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "human-approval-risky-publish",
|
||||||
|
"name": "Human Approval Gate for Risky Publish Operations",
|
||||||
|
"file": "patterns/human-approval-risky-publish.md",
|
||||||
|
"tags": ["approval", "human-in-the-loop", "publish", "email", "linkedin", "client-facing", "risk"],
|
||||||
|
"affected_projects": [
|
||||||
|
"linkedin-automation",
|
||||||
|
"email-inbox-triage",
|
||||||
|
"cyber-tips-newsletter"
|
||||||
|
],
|
||||||
|
"related_patterns": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "transient-failure-retry",
|
||||||
|
"name": "Transient Failure Retry and Degradation",
|
||||||
|
"file": "patterns/transient-failure-retry.md",
|
||||||
|
"tags": ["retry", "backoff", "transient", "ollama", "network", "rate-limit", "queue"],
|
||||||
|
"affected_projects": [
|
||||||
|
"n8n-general",
|
||||||
|
"ollama-general"
|
||||||
|
],
|
||||||
|
"related_patterns": [
|
||||||
|
"queue-poll-async-job"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Pattern: PTY-Required Device Code Authentication
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
A web app or automation script shells out to a CLI tool (e.g., PowerShell `Connect-MgGraph -UseDeviceAuthentication`) and hangs. The user never sees the device code or authentication URL needed to complete login. The process appears to do nothing.
|
||||||
|
|
||||||
|
## Affected Projects
|
||||||
|
- Intune Inspector (PowerShell + Microsoft Graph device code auth)
|
||||||
|
- Microsoft 365 Admin Toolkit (PowerShell + Microsoft Graph)
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
Some CLI commands write authentication prompts to the console host rather than stdout. When run via `subprocess.Popen` or equivalent without a pseudo-terminal (PTY), that output is not captured or displayed, so the user cannot complete the interactive step.
|
||||||
|
|
||||||
|
## Standard Fix
|
||||||
|
1. Use a PTY-enabled subprocess library (`pexpect` in Python, `node-pty` in Node.js) so the spawned process believes it has a real terminal.
|
||||||
|
2. Capture and surface console-host output to the user in real time (device code, URL, instructions).
|
||||||
|
3. Poll for completion if the process is long-running.
|
||||||
|
4. Document that this is a workaround for interactive CLI flows that cannot be fully non-interactive.
|
||||||
|
|
||||||
|
## When to Apply
|
||||||
|
- Any integration that spawns a command-line tool requiring interactive authentication.
|
||||||
|
- Microsoft Graph device-code flows, Azure CLI login prompts, OAuth CLI helpers, or similar tools.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- Running the script standalone in a real terminal produces a device code/URL.
|
||||||
|
- The app captures and displays the same code/URL via PTY.
|
||||||
|
- Authentication completes and the downstream operation succeeds.
|
||||||
|
|
||||||
|
## Related Patterns
|
||||||
|
- None yet.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Pattern: Queue-Poll Async Job Pattern
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
An operation takes longer than a synchronous HTTP timeout allows (video generation, large LLM generation, batch job). The caller either times out, retries redundantly, or loses track of the job state.
|
||||||
|
|
||||||
|
## Affected Projects
|
||||||
|
- AI video generation pipeline (ComfyUI + WAN 2.1)
|
||||||
|
- Long-running Ollama generations in n8n
|
||||||
|
- Any workflow that submits work to a queue and must wait for completion
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
The work is genuinely asynchronous, but the integration is designed as if it were synchronous. Without explicit job-state tracking, the system cannot wait, retry, or recover correctly.
|
||||||
|
|
||||||
|
## Standard Fix
|
||||||
|
1. Submit the job and immediately capture a job/prompt ID.
|
||||||
|
2. Poll a status endpoint on a fixed interval with exponential backoff.
|
||||||
|
3. Define terminal states (completed, failed, cancelled) and a max poll duration.
|
||||||
|
4. Store intermediate state so a restart does not lose the job ID.
|
||||||
|
5. Surface progress to the user if the operation is user-facing.
|
||||||
|
|
||||||
|
## When to Apply
|
||||||
|
- Any integration where the expected duration exceeds a reasonable HTTP timeout (~30-60 seconds).
|
||||||
|
- Any service that returns a job ID or queue position instead of the final result.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- Job submission returns an ID.
|
||||||
|
- Polling correctly detects completion and failure.
|
||||||
|
- No duplicate work is triggered by retries.
|
||||||
|
- Progress/state survives a brief restart of the polling service.
|
||||||
|
|
||||||
|
## Related Patterns
|
||||||
|
- `transient-failure-retry`
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Pattern: Reverse Proxy + Container Port Binding
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
A self-hosted Docker service (Nextcloud AIO, ComfyUI, etc.) is deployed behind a reverse proxy such as Nginx Proxy Manager. The service is unreachable, returns 502/504, or binds to the wrong network/interface. Docker network complexity, port mappings, and internal DNS all become debugging obstacles.
|
||||||
|
|
||||||
|
## Affected Projects
|
||||||
|
- Nextcloud AIO on `bve.beawit.net`
|
||||||
|
- Any future self-hosted service deployed behind Nginx Proxy Manager
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
Containerized services often bind to localhost or a Docker-internal IP by default. A reverse proxy running on the host or in another container cannot reach them without explicit port binding and network configuration. NAT, firewall, and Docker bridge behavior add confusion.
|
||||||
|
|
||||||
|
## Standard Fix
|
||||||
|
1. Decide early whether to use host networking (`--network host`) or explicit port mapping with a known internal IP.
|
||||||
|
2. If using host network, bind the service to `0.0.0.0` on a fixed, documented port and point the reverse proxy to the host IP.
|
||||||
|
3. If using bridge mode, ensure the reverse proxy can reach the container by name or fixed IP.
|
||||||
|
4. Document the chosen network topology in the RUNBOOK.
|
||||||
|
5. Verify reachability from the reverse proxy host before declaring the deployment done.
|
||||||
|
|
||||||
|
## When to Apply
|
||||||
|
- Every new containerized service that will be exposed through a reverse proxy.
|
||||||
|
- Any existing service with intermittent 502/504 or binding issues.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- Reverse proxy can curl the backend service directly.
|
||||||
|
- External HTTPS access returns expected responses.
|
||||||
|
- Configuration survives container restart.
|
||||||
|
|
||||||
|
## Related Patterns
|
||||||
|
- None yet.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Pattern: Transient Failure Retry and Degradation
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
A workflow or agent fails intermittently due to temporary conditions: Ollama model not loaded yet, network blip, rate limit, dependent service restart. A simple rerun often succeeds, but the failure creates noise, lost state, or unnecessary alerts.
|
||||||
|
|
||||||
|
## Affected Projects
|
||||||
|
- n8n workflows calling Ollama
|
||||||
|
- Queue-poll integrations
|
||||||
|
- Any service with external dependencies
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
Distributed services are not always available at the moment a caller needs them. Treating every transient failure as a hard error produces false alarms and breaks pipelines that should recover automatically.
|
||||||
|
|
||||||
|
## Standard Fix
|
||||||
|
1. Classify failures as retryable (network, timeout, 429, 503) or non-retryable (400, 401, schema error).
|
||||||
|
2. Apply retry with exponential backoff and jitter for retryable failures.
|
||||||
|
3. Set a maximum retry count and a dead-letter/escalation path for persistent failures.
|
||||||
|
4. Degrade gracefully if a non-critical dependency fails (e.g., return partial results, skip enrichment).
|
||||||
|
5. Distinguish retry events from terminal failures in logs and alerts.
|
||||||
|
|
||||||
|
## When to Apply
|
||||||
|
- Any integration that calls an external service over the network.
|
||||||
|
- Any workflow that has experienced at least one "worked on retry" incident.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- Simulated transient failure triggers retry and eventual success.
|
||||||
|
- Non-retryable failure escalates immediately without wasteful retries.
|
||||||
|
- Logs clearly show retry count, backoff, and final disposition.
|
||||||
|
|
||||||
|
## Related Patterns
|
||||||
|
- `queue-poll-async-job`
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* propose-pattern.js — Feedback loop for the cross-project pattern registry.
|
||||||
|
*
|
||||||
|
* Use this when an agent applies a workaround, fix, or design choice that is
|
||||||
|
* structurally reusable across projects. It compares the new lesson against
|
||||||
|
* existing patterns and either proposes an update to an existing pattern or a
|
||||||
|
* brand-new pattern file + manifest entry.
|
||||||
|
*
|
||||||
|
* Default mode is dry-run / preview. Pass --apply to write changes.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* node scripts/utils/propose-pattern.js \
|
||||||
|
* --task="n8n workflow sends LLM-generated text to LinkedIn" \
|
||||||
|
* --fix="JSON.stringify the post body and validate with JSON.parse before the HTTP Request node" \
|
||||||
|
* --projects="linkedin-automation" \
|
||||||
|
* --symptom="LinkedIn API returns malformed JSON payload errors" \
|
||||||
|
* --root-cause="LLM output contains unescaped quotes and newlines"
|
||||||
|
*
|
||||||
|
* node scripts/utils/propose-pattern.js --apply --file=/tmp/proposed-pattern.json
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const WORKSPACE = '/home/jcbeasley/.openclaw/workspace';
|
||||||
|
const PATTERNS_DIR = path.join(WORKSPACE, 'patterns');
|
||||||
|
const MANIFEST_PATH = path.join(PATTERNS_DIR, 'patterns.json');
|
||||||
|
|
||||||
|
function slugify(name) {
|
||||||
|
return name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenSet(text) {
|
||||||
|
return new Set(
|
||||||
|
(text || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/[^a-z0-9_-]+/)
|
||||||
|
.filter(t => t.length > 2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function jaccard(a, b) {
|
||||||
|
if (a.size === 0 || b.size === 0) return 0;
|
||||||
|
const intersection = new Set([...a].filter(x => b.has(x)));
|
||||||
|
return intersection.size / (a.size + b.size - intersection.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadManifest() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
|
||||||
|
} catch (err) {
|
||||||
|
return { version: new Date().toISOString().slice(0, 10), patterns: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findBestMatch(taskText, fixText, manifest) {
|
||||||
|
const queryTokens = new Set([...tokenSet(taskText), ...tokenSet(fixText)]);
|
||||||
|
let best = { pattern: null, score: 0 };
|
||||||
|
for (const pattern of manifest.patterns || []) {
|
||||||
|
const patternTokens = new Set([
|
||||||
|
...tokenSet(pattern.name),
|
||||||
|
...tokenSet(pattern.id),
|
||||||
|
...(pattern.tags || []).flatMap(t => [...tokenSet(t)]),
|
||||||
|
...(pattern.affected_projects || []).flatMap(p => [...tokenSet(p)])
|
||||||
|
]);
|
||||||
|
const score = jaccard(queryTokens, patternTokens);
|
||||||
|
if (score > best.score) best = { pattern, score };
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPatternMarkdown(opts) {
|
||||||
|
const related = (opts.related_patterns || [])
|
||||||
|
.filter(id => id && id.trim())
|
||||||
|
.map(id => `- \`${id}\``)
|
||||||
|
.join('\n') || '- None yet.';
|
||||||
|
|
||||||
|
return `# Pattern: ${opts.name}
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
${opts.symptom || 'TODO: describe the symptom'}
|
||||||
|
|
||||||
|
## Affected Projects
|
||||||
|
${(opts.projects || []).map(p => `- ${p}`).join('\n') || '- TODO'}
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
${opts.root_cause || 'TODO: describe the root cause'}
|
||||||
|
|
||||||
|
## Standard Fix
|
||||||
|
${opts.fix || 'TODO: describe the standard fix'}
|
||||||
|
|
||||||
|
## When to Apply
|
||||||
|
${opts.when_to_apply || 'TODO: describe when to apply this pattern'}
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
${opts.verification || 'TODO: describe how to verify the fix'}
|
||||||
|
|
||||||
|
## Related Patterns
|
||||||
|
${related}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProposal(argv) {
|
||||||
|
const manifest = loadManifest();
|
||||||
|
const match = findBestMatch(argv.task, argv.fix, manifest);
|
||||||
|
const threshold = 0.15;
|
||||||
|
|
||||||
|
if (match.pattern && match.score >= threshold) {
|
||||||
|
// Propose update to existing pattern
|
||||||
|
const updatedProjects = Array.from(new Set([
|
||||||
|
...(match.pattern.affected_projects || []),
|
||||||
|
...(argv.projects || [])
|
||||||
|
]));
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode: 'update',
|
||||||
|
score: match.score,
|
||||||
|
existing: match.pattern,
|
||||||
|
proposal: {
|
||||||
|
id: match.pattern.id,
|
||||||
|
name: match.pattern.name,
|
||||||
|
affected_projects: updatedProjects,
|
||||||
|
rationale: `Extend pattern "${match.pattern.id}" with newly observed project(s) and/or refined fix.`,
|
||||||
|
additions: {
|
||||||
|
affected_projects: argv.projects || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Propose new pattern
|
||||||
|
const name = argv.name || argv.task.split(/[.!?]/)[0].slice(0, 60);
|
||||||
|
const id = argv.id || slugify(name);
|
||||||
|
const tags = argv.tags || tokenSet(`${argv.task} ${argv.fix} ${argv.symptom} ${argv.root_cause}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode: 'create',
|
||||||
|
score: match.score,
|
||||||
|
nearest_existing: match.pattern,
|
||||||
|
proposal: {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
file: `patterns/${id}.md`,
|
||||||
|
tags: Array.from(tags),
|
||||||
|
affected_projects: argv.projects || [],
|
||||||
|
related_patterns: argv.related || [],
|
||||||
|
markdown: buildPatternMarkdown({
|
||||||
|
name,
|
||||||
|
symptom: argv.symptom,
|
||||||
|
projects: argv.projects || [],
|
||||||
|
root_cause: argv.root_cause,
|
||||||
|
fix: argv.fix,
|
||||||
|
when_to_apply: argv.when_to_apply,
|
||||||
|
verification: argv.verification,
|
||||||
|
related_patterns: argv.related || []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function printProposal(proposal) {
|
||||||
|
if (proposal.mode === 'update') {
|
||||||
|
console.log('\n=== PROPOSED PATTERN UPDATE (dry-run) ===\n');
|
||||||
|
console.log(`Pattern: ${proposal.existing.id}`);
|
||||||
|
console.log(`Match score: ${(proposal.score * 100).toFixed(1)}%`);
|
||||||
|
console.log(`Rationale: ${proposal.proposal.rationale}`);
|
||||||
|
console.log('\nExisting affected projects:');
|
||||||
|
for (const p of proposal.existing.affected_projects || []) console.log(` - ${p}`);
|
||||||
|
console.log('\nProjects to add:');
|
||||||
|
for (const p of proposal.proposal.additions.affected_projects) console.log(` + ${p}`);
|
||||||
|
console.log('\nRun with --apply to update patterns/patterns.json.');
|
||||||
|
} else {
|
||||||
|
console.log('\n=== PROPOSED NEW PATTERN (dry-run) ===\n');
|
||||||
|
console.log(`ID: ${proposal.proposal.id}`);
|
||||||
|
console.log(`Name: ${proposal.proposal.name}`);
|
||||||
|
console.log(`File: ${proposal.proposal.file}`);
|
||||||
|
console.log(`Tags: ${proposal.proposal.tags.join(', ')}`);
|
||||||
|
console.log(`Projects: ${proposal.proposal.affected_projects.join(', ') || '(none)'}`);
|
||||||
|
console.log(`Match score: ${proposal.score > 0 ? (proposal.score * 100).toFixed(1) + '%' : 'none'}`);
|
||||||
|
if (proposal.nearest_existing) {
|
||||||
|
console.log(`Nearest existing pattern: ${proposal.nearest_existing.id} (${(proposal.score * 100).toFixed(1)}% match)`);
|
||||||
|
}
|
||||||
|
console.log('\n--- Markdown preview ---\n');
|
||||||
|
console.log(proposal.proposal.markdown);
|
||||||
|
console.log('\nRun with --apply to write the file and update patterns/patterns.json.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyProposal(proposal) {
|
||||||
|
const manifest = loadManifest();
|
||||||
|
|
||||||
|
if (proposal.mode === 'update') {
|
||||||
|
const idx = manifest.patterns.findIndex(p => p.id === proposal.proposal.id);
|
||||||
|
if (idx === -1) throw new Error(`Pattern ${proposal.proposal.id} not found in manifest`);
|
||||||
|
manifest.patterns[idx].affected_projects = proposal.proposal.affected_projects;
|
||||||
|
fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + '\n');
|
||||||
|
console.log(`\nUpdated manifest: ${proposal.proposal.id} affected_projects`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new pattern
|
||||||
|
const filePath = path.join(WORKSPACE, proposal.proposal.file);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
throw new Error(`Pattern file already exists: ${proposal.proposal.file}`);
|
||||||
|
}
|
||||||
|
fs.writeFileSync(filePath, proposal.proposal.markdown);
|
||||||
|
|
||||||
|
const manifestEntry = {
|
||||||
|
id: proposal.proposal.id,
|
||||||
|
name: proposal.proposal.name,
|
||||||
|
file: proposal.proposal.file,
|
||||||
|
tags: proposal.proposal.tags,
|
||||||
|
affected_projects: proposal.proposal.affected_projects,
|
||||||
|
related_patterns: proposal.proposal.related_patterns
|
||||||
|
};
|
||||||
|
manifest.patterns.push(manifestEntry);
|
||||||
|
fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + '\n');
|
||||||
|
console.log(`\nCreated ${proposal.proposal.file}`);
|
||||||
|
console.log(`Updated patterns/patterns.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgv() {
|
||||||
|
const argv = { _: [] };
|
||||||
|
for (let i = 2; i < process.argv.length; i++) {
|
||||||
|
const arg = process.argv[i];
|
||||||
|
if (arg === '--apply') argv.apply = true;
|
||||||
|
else if (arg === '--help' || arg === '-h') argv.help = true;
|
||||||
|
else if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2).replace(/-/g, '_');
|
||||||
|
const next = process.argv[i + 1];
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
if (['projects', 'tags', 'related'].includes(key)) {
|
||||||
|
argv[key] = next.split(',').map(s => s.trim()).filter(Boolean);
|
||||||
|
} else {
|
||||||
|
argv[key] = next;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
argv[key] = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
argv._.push(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return argv;
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const argv = parseArgv();
|
||||||
|
|
||||||
|
if (argv.help) {
|
||||||
|
console.log(`Usage: node scripts/utils/propose-pattern.js [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--task "..." Description of the task/problem
|
||||||
|
--fix "..." The workaround or fix applied
|
||||||
|
--projects p1,p2 Comma-separated affected project identifiers
|
||||||
|
--symptom "..." What the user/agent sees
|
||||||
|
--root-cause "..." Why it happens
|
||||||
|
--when-to-apply "..." When to apply this pattern to new tasks
|
||||||
|
--verification "..." How to verify the fix
|
||||||
|
--related p1,p2 Comma-separated related pattern IDs
|
||||||
|
--name "..." Override the generated pattern name
|
||||||
|
--id "..." Override the generated pattern ID
|
||||||
|
--apply Write files after review (default is dry-run)
|
||||||
|
--file path.json Load proposal inputs from JSON file
|
||||||
|
-h, --help Show this help
|
||||||
|
`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let inputs = argv;
|
||||||
|
if (argv.file) {
|
||||||
|
inputs = { ...JSON.parse(fs.readFileSync(argv.file, 'utf8')), ...argv };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inputs.task && !inputs.fix && !inputs.symptom) {
|
||||||
|
console.error('Error: at least one of --task, --fix, or --symptom is required.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const proposal = buildProposal(inputs);
|
||||||
|
printProposal(proposal);
|
||||||
|
|
||||||
|
if (argv.apply) {
|
||||||
|
applyProposal(proposal);
|
||||||
|
console.log('\nDone. Run `node architecture/pipeline.js "sample query"` to verify retrieval.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { buildProposal, applyProposal, findBestMatch, loadManifest };
|
||||||
Reference in New Issue
Block a user