feed-the-machine 1.6.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +170 -170
- package/bin/brain.py +1340 -0
- package/bin/convert_claude_skills_to_codex.py +490 -0
- package/bin/generate-manifest.mjs +463 -463
- package/bin/harden_codex_skills.py +141 -0
- package/bin/install.mjs +491 -491
- package/bin/migrate-eng-buddy-data.py +875 -0
- package/bin/playbook_engine/__init__.py +1 -0
- package/bin/playbook_engine/conftest.py +8 -0
- package/bin/playbook_engine/extractor.py +33 -0
- package/bin/playbook_engine/manager.py +102 -0
- package/bin/playbook_engine/models.py +84 -0
- package/bin/playbook_engine/registry.py +35 -0
- package/bin/playbook_engine/test_extractor.py +72 -0
- package/bin/playbook_engine/test_integration.py +129 -0
- package/bin/playbook_engine/test_manager.py +85 -0
- package/bin/playbook_engine/test_models.py +166 -0
- package/bin/playbook_engine/test_registry.py +67 -0
- package/bin/playbook_engine/test_tracer.py +86 -0
- package/bin/playbook_engine/tracer.py +93 -0
- package/bin/tasks_db.py +456 -0
- package/docs/HOOKS.md +243 -243
- package/docs/INBOX.md +233 -233
- package/ftm/SKILL.md +125 -122
- package/ftm-audit/SKILL.md +623 -623
- package/ftm-audit/references/protocols/PROJECT-PATTERNS.md +91 -91
- package/ftm-audit/references/protocols/RUNTIME-WIRING.md +66 -66
- package/ftm-audit/references/protocols/WIRING-CONTRACTS.md +135 -135
- package/ftm-audit/references/strategies/AUTO-FIX-STRATEGIES.md +69 -69
- package/ftm-audit/references/templates/REPORT-FORMAT.md +96 -96
- package/ftm-audit/scripts/run-knip.sh +23 -23
- package/ftm-audit.yml +2 -2
- package/ftm-brainstorm/SKILL.md +1003 -498
- package/ftm-brainstorm/evals/evals.json +180 -100
- package/ftm-brainstorm/evals/promptfoo.yaml +109 -109
- package/ftm-brainstorm/references/agent-prompts.md +552 -224
- package/ftm-brainstorm/references/plan-template.md +209 -121
- package/ftm-brainstorm.yml +2 -2
- package/ftm-browse/SKILL.md +454 -454
- package/ftm-browse/daemon/browser-manager.ts +206 -206
- package/ftm-browse/daemon/bun.lock +30 -30
- package/ftm-browse/daemon/cli.ts +347 -347
- package/ftm-browse/daemon/commands.ts +410 -410
- package/ftm-browse/daemon/main.ts +357 -357
- package/ftm-browse/daemon/package.json +17 -17
- package/ftm-browse/daemon/server.ts +189 -189
- package/ftm-browse/daemon/snapshot.ts +519 -519
- package/ftm-browse/daemon/tsconfig.json +22 -22
- package/ftm-browse.yml +4 -4
- package/ftm-capture/SKILL.md +370 -370
- package/ftm-capture.yml +4 -4
- package/ftm-codex-gate/SKILL.md +361 -361
- package/ftm-codex-gate.yml +2 -2
- package/ftm-config/SKILL.md +422 -345
- package/ftm-config.default.yml +125 -82
- package/ftm-config.yml +44 -2
- package/ftm-council/SKILL.md +416 -416
- package/ftm-council/references/prompts/CLAUDE-INVESTIGATION.md +60 -60
- package/ftm-council/references/prompts/CODEX-INVESTIGATION.md +58 -58
- package/ftm-council/references/prompts/GEMINI-INVESTIGATION.md +58 -58
- package/ftm-council/references/prompts/REBUTTAL-TEMPLATE.md +57 -57
- package/ftm-council/references/protocols/PREREQUISITES.md +47 -47
- package/ftm-council/references/protocols/STEP-0-FRAMING.md +46 -46
- package/ftm-council.yml +2 -2
- package/ftm-dashboard/SKILL.md +163 -163
- package/ftm-dashboard.yml +4 -4
- package/ftm-debug/SKILL.md +1037 -1037
- package/ftm-debug/references/phases/PHASE-0-INTAKE.md +58 -58
- package/ftm-debug/references/phases/PHASE-1-TRIAGE.md +46 -46
- package/ftm-debug/references/phases/PHASE-2-WAR-ROOM-AGENTS.md +279 -279
- package/ftm-debug/references/phases/PHASE-3-TO-6-EXECUTION.md +436 -436
- package/ftm-debug/references/protocols/BLACKBOARD.md +86 -86
- package/ftm-debug/references/protocols/EDGE-CASES.md +103 -103
- package/ftm-debug.yml +2 -2
- package/ftm-diagram/SKILL.md +277 -277
- package/ftm-diagram.yml +2 -2
- package/ftm-executor/SKILL.md +777 -777
- package/ftm-executor/references/STYLE-TEMPLATE.md +73 -73
- package/ftm-executor/references/phases/PHASE-0-VERIFICATION.md +62 -62
- package/ftm-executor/references/phases/PHASE-2-AGENT-ASSEMBLY.md +34 -34
- package/ftm-executor/references/phases/PHASE-3-WORKTREES.md +38 -38
- package/ftm-executor/references/phases/PHASE-4-5-AUDIT.md +72 -72
- package/ftm-executor/references/phases/PHASE-4-DISPATCH.md +66 -66
- package/ftm-executor/references/phases/PHASE-5-5-CODEX-GATE.md +73 -73
- package/ftm-executor/references/protocols/DOCUMENTATION-BOOTSTRAP.md +36 -36
- package/ftm-executor/references/protocols/MODEL-PROFILE.md +59 -59
- package/ftm-executor/references/protocols/PROGRESS-TRACKING.md +66 -66
- package/ftm-executor/runtime/ftm-runtime.mjs +252 -252
- package/ftm-executor/runtime/package.json +8 -8
- package/ftm-executor.yml +2 -2
- package/ftm-git/SKILL.md +441 -441
- package/ftm-git/evals/evals.json +26 -26
- package/ftm-git/evals/promptfoo.yaml +75 -75
- package/ftm-git/hooks/post-commit-experience.sh +92 -92
- package/ftm-git/references/patterns/SECRET-PATTERNS.md +104 -104
- package/ftm-git/references/protocols/REMEDIATION.md +139 -139
- package/ftm-git/scripts/pre-commit-secrets.sh +110 -110
- package/ftm-git.yml +2 -2
- package/ftm-inbox/backend/__pycache__/main.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/adapters/_retry.py +64 -64
- package/ftm-inbox/backend/adapters/base.py +230 -230
- package/ftm-inbox/backend/adapters/freshservice.py +104 -104
- package/ftm-inbox/backend/adapters/gmail.py +125 -125
- package/ftm-inbox/backend/adapters/jira.py +136 -136
- package/ftm-inbox/backend/adapters/registry.py +192 -192
- package/ftm-inbox/backend/adapters/slack.py +110 -110
- package/ftm-inbox/backend/db/connection.py +54 -54
- package/ftm-inbox/backend/db/schema.py +78 -78
- package/ftm-inbox/backend/executor/__init__.py +7 -7
- package/ftm-inbox/backend/executor/engine.py +149 -149
- package/ftm-inbox/backend/executor/step_runner.py +98 -98
- package/ftm-inbox/backend/main.py +103 -103
- package/ftm-inbox/backend/models/__init__.py +1 -1
- package/ftm-inbox/backend/models/unified_task.py +36 -36
- package/ftm-inbox/backend/planner/__init__.py +6 -6
- package/ftm-inbox/backend/planner/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/planner/__pycache__/generator.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/planner/__pycache__/schema.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/planner/generator.py +127 -127
- package/ftm-inbox/backend/planner/schema.py +34 -34
- package/ftm-inbox/backend/requirements.txt +5 -5
- package/ftm-inbox/backend/routes/__pycache__/plan.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/routes/execute.py +186 -186
- package/ftm-inbox/backend/routes/health.py +52 -52
- package/ftm-inbox/backend/routes/inbox.py +68 -68
- package/ftm-inbox/backend/routes/plan.py +271 -271
- package/ftm-inbox/bin/launchagent.mjs +91 -91
- package/ftm-inbox/bin/setup.mjs +188 -188
- package/ftm-inbox/bin/start.sh +10 -10
- package/ftm-inbox/bin/status.sh +17 -17
- package/ftm-inbox/bin/stop.sh +8 -8
- package/ftm-inbox/config.example.yml +55 -55
- package/ftm-inbox/package-lock.json +2898 -2898
- package/ftm-inbox/package.json +26 -26
- package/ftm-inbox/postcss.config.js +6 -6
- package/ftm-inbox/src/app.css +199 -199
- package/ftm-inbox/src/app.html +18 -18
- package/ftm-inbox/src/lib/api.ts +166 -166
- package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -81
- package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -143
- package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -271
- package/ftm-inbox/src/lib/components/PlanView.svelte +206 -206
- package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -99
- package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -190
- package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -63
- package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -86
- package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -106
- package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -67
- package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -149
- package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -80
- package/ftm-inbox/src/lib/theme.ts +47 -47
- package/ftm-inbox/src/routes/+layout.svelte +76 -76
- package/ftm-inbox/src/routes/+page.svelte +401 -401
- package/ftm-inbox/svelte.config.js +12 -12
- package/ftm-inbox/tailwind.config.ts +63 -63
- package/ftm-inbox/tsconfig.json +13 -13
- package/ftm-inbox/vite.config.ts +6 -6
- package/ftm-intent/SKILL.md +241 -241
- package/ftm-intent.yml +2 -2
- package/ftm-manifest.json +3794 -3794
- package/ftm-map/SKILL.md +291 -291
- package/ftm-map/scripts/db.py +712 -712
- package/ftm-map/scripts/index.py +415 -415
- package/ftm-map/scripts/parser.py +224 -224
- package/ftm-map/scripts/queries/go-tags.scm +20 -20
- package/ftm-map/scripts/queries/javascript-tags.scm +35 -35
- package/ftm-map/scripts/queries/python-tags.scm +31 -31
- package/ftm-map/scripts/queries/ruby-tags.scm +19 -19
- package/ftm-map/scripts/queries/rust-tags.scm +37 -37
- package/ftm-map/scripts/queries/typescript-tags.scm +41 -41
- package/ftm-map/scripts/query.py +301 -301
- package/ftm-map/scripts/ranker.py +377 -377
- package/ftm-map/scripts/requirements.txt +5 -5
- package/ftm-map/scripts/setup-hooks.sh +27 -27
- package/ftm-map/scripts/setup.sh +56 -56
- package/ftm-map/scripts/test_db.py +364 -364
- package/ftm-map/scripts/test_parser.py +174 -174
- package/ftm-map/scripts/test_query.py +183 -183
- package/ftm-map/scripts/test_ranker.py +199 -199
- package/ftm-map/scripts/views.py +591 -591
- package/ftm-map.yml +2 -2
- package/ftm-mind/SKILL.md +201 -1943
- package/ftm-mind/evals/promptfoo.yaml +142 -142
- package/ftm-mind/references/blackboard-protocol.md +110 -0
- package/ftm-mind/references/blackboard-schema.md +328 -328
- package/ftm-mind/references/complexity-guide.md +110 -110
- package/ftm-mind/references/complexity-sizing.md +138 -0
- package/ftm-mind/references/decide-act-protocol.md +172 -0
- package/ftm-mind/references/direct-execution.md +51 -0
- package/ftm-mind/references/environment-discovery.md +77 -0
- package/ftm-mind/references/event-registry.md +319 -319
- package/ftm-mind/references/mcp-inventory.md +300 -296
- package/ftm-mind/references/ops-routing.md +47 -0
- package/ftm-mind/references/orient-protocol.md +234 -0
- package/ftm-mind/references/personality.md +40 -0
- package/ftm-mind/references/protocols/COMPLEXITY-SIZING.md +72 -72
- package/ftm-mind/references/protocols/MCP-HEURISTICS.md +32 -32
- package/ftm-mind/references/protocols/PLAN-APPROVAL.md +80 -80
- package/ftm-mind/references/reflexion-protocol.md +249 -249
- package/ftm-mind/references/routing/SCENARIOS.md +22 -22
- package/ftm-mind/references/routing-scenarios.md +35 -35
- package/ftm-mind.yml +2 -2
- package/ftm-ops.yml +4 -0
- package/ftm-pause/SKILL.md +395 -395
- package/ftm-pause/references/protocols/SKILL-RESTORE-PROTOCOLS.md +186 -186
- package/ftm-pause/references/protocols/VALIDATION.md +80 -80
- package/ftm-pause.yml +2 -2
- package/ftm-researcher/SKILL.md +275 -275
- package/ftm-researcher/evals/agent-diversity.yaml +17 -17
- package/ftm-researcher/evals/synthesis-quality.yaml +12 -12
- package/ftm-researcher/evals/trigger-accuracy.yaml +39 -39
- package/ftm-researcher/references/adaptive-search.md +116 -116
- package/ftm-researcher/references/agent-prompts.md +193 -193
- package/ftm-researcher/references/council-integration.md +193 -193
- package/ftm-researcher/references/output-format.md +203 -203
- package/ftm-researcher/references/synthesis-pipeline.md +165 -165
- package/ftm-researcher/scripts/score_credibility.py +234 -234
- package/ftm-researcher/scripts/validate_research.py +92 -92
- package/ftm-researcher.yml +2 -2
- package/ftm-resume/SKILL.md +518 -518
- package/ftm-resume/references/protocols/VALIDATION.md +172 -172
- package/ftm-resume.yml +2 -2
- package/ftm-retro/SKILL.md +380 -380
- package/ftm-retro/references/protocols/SCORING-RUBRICS.md +89 -89
- package/ftm-retro/references/templates/REPORT-FORMAT.md +109 -109
- package/ftm-retro.yml +2 -2
- package/ftm-routine/SKILL.md +170 -170
- package/ftm-routine.yml +4 -4
- package/ftm-state/blackboard/capabilities.json +5 -5
- package/ftm-state/blackboard/capabilities.schema.json +27 -27
- package/ftm-state/blackboard/context.json +37 -23
- package/ftm-state/blackboard/experiences/doom-statusline-fix.json +26 -0
- package/ftm-state/blackboard/experiences/hackathon-pages-site.json +26 -0
- package/ftm-state/blackboard/experiences/hindsight-sso-kickoff.json +42 -0
- package/ftm-state/blackboard/experiences/index.json +58 -9
- package/ftm-state/blackboard/experiences/learning-ragnarok-api-access.json +23 -0
- package/ftm-state/blackboard/experiences/nordlayer-members-auto-assign.json +26 -0
- package/ftm-state/blackboard/experiences/saml2aws-stale-session-fix.json +41 -0
- package/ftm-state/blackboard/patterns.json +6 -6
- package/ftm-state/schemas/context.schema.json +130 -130
- package/ftm-state/schemas/experience-index.schema.json +77 -77
- package/ftm-state/schemas/experience.schema.json +78 -78
- package/ftm-state/schemas/patterns.schema.json +44 -44
- package/ftm-upgrade/SKILL.md +194 -194
- package/ftm-upgrade/scripts/check-version.sh +76 -76
- package/ftm-upgrade/scripts/upgrade.sh +143 -143
- package/ftm-upgrade.yml +2 -2
- package/ftm-verify.yml +2 -2
- package/ftm.yml +2 -2
- package/hooks/ftm-auto-log.sh +137 -0
- package/hooks/ftm-blackboard-enforcer.sh +93 -93
- package/hooks/ftm-discovery-reminder.sh +90 -90
- package/hooks/ftm-drafts-gate.sh +61 -61
- package/hooks/ftm-event-logger.mjs +107 -107
- package/hooks/ftm-install-hooks.sh +240 -0
- package/hooks/ftm-learning-capture.sh +117 -0
- package/hooks/ftm-map-autodetect.sh +79 -79
- package/hooks/ftm-pending-sync-check.sh +22 -22
- package/hooks/ftm-plan-gate.sh +92 -92
- package/hooks/ftm-post-commit-trigger.sh +57 -57
- package/hooks/ftm-post-compaction.sh +138 -0
- package/hooks/ftm-pre-compaction.sh +147 -0
- package/hooks/ftm-session-end.sh +52 -0
- package/hooks/ftm-session-snapshot.sh +213 -0
- package/hooks/settings-template.json +81 -81
- package/install.sh +363 -363
- package/package.json +84 -84
- package/uninstall.sh +25 -25
package/bin/brain.py
ADDED
|
@@ -0,0 +1,1340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
eng-buddy Learning Engine.
|
|
3
|
+
Builds context prompts from persistent memory and parses Claude responses
|
|
4
|
+
for new patterns, stakeholder updates, and automation opportunities.
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import sqlite3
|
|
11
|
+
import sys
|
|
12
|
+
import tasks_db
|
|
13
|
+
from datetime import date, datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
# Feature flag: set BRAIN_ENABLE_POLLER=1 to re-enable poller intake code paths.
|
|
17
|
+
# When unset or "0" (default), poller code is skipped.
|
|
18
|
+
# This flag exists as a rollback escape hatch; poller intake is disabled by default.
|
|
19
|
+
BRAIN_ENABLE_POLLER = os.environ.get("BRAIN_ENABLE_POLLER", "0") == "1"
|
|
20
|
+
|
|
21
|
+
ENG_BUDDY_DIR = Path.home() / ".claude" / "eng-buddy"
|
|
22
|
+
MEMORY_DIR = ENG_BUDDY_DIR / "memory"
|
|
23
|
+
MEMORY_DIR.mkdir(exist_ok=True)
|
|
24
|
+
|
|
25
|
+
DB_PATH = ENG_BUDDY_DIR / "inbox.db"
|
|
26
|
+
DAILY_DIR = ENG_BUDDY_DIR / "daily"
|
|
27
|
+
PATTERNS_DIR = ENG_BUDDY_DIR / "patterns"
|
|
28
|
+
STAKEHOLDERS_DIR = ENG_BUDDY_DIR / "stakeholders"
|
|
29
|
+
KNOWLEDGE_DIR = ENG_BUDDY_DIR / "knowledge"
|
|
30
|
+
|
|
31
|
+
UNMAPPED_LEARNING_PATH = PATTERNS_DIR / "uncategorized-learning.md"
|
|
32
|
+
UNMAPPED_LEARNING_HEADING = "## AI Captured Uncategorized Learning"
|
|
33
|
+
|
|
34
|
+
WRITE_TOOLS = {"Write", "Edit", "MultiEdit", "NotebookEdit"}
|
|
35
|
+
TASK_TOOLS = {"Bash", "Task"}
|
|
36
|
+
ACTION_MCP_PROVIDERS = {
|
|
37
|
+
"mcp-atlassian",
|
|
38
|
+
"freshservice-mcp",
|
|
39
|
+
"gmail",
|
|
40
|
+
"google-calendar",
|
|
41
|
+
"slack",
|
|
42
|
+
"lusha",
|
|
43
|
+
"git",
|
|
44
|
+
}
|
|
45
|
+
READ_ONLY_MCP_PROVIDERS = {
|
|
46
|
+
"context7",
|
|
47
|
+
"apple-doc-mcp",
|
|
48
|
+
"glean_default",
|
|
49
|
+
"playwright",
|
|
50
|
+
"sequential-thinking",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _load(name, default=None):
|
|
55
|
+
p = MEMORY_DIR / name
|
|
56
|
+
if p.exists():
|
|
57
|
+
try:
|
|
58
|
+
return json.loads(p.read_text())
|
|
59
|
+
except json.JSONDecodeError:
|
|
60
|
+
pass
|
|
61
|
+
return default if default is not None else {}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _save(name, data):
|
|
65
|
+
(MEMORY_DIR / name).write_text(json.dumps(data, indent=2))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def load_context():
|
|
69
|
+
return _load("context.json", {})
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def load_stakeholders():
|
|
73
|
+
return _load("stakeholders.json", {})
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_patterns():
|
|
77
|
+
return _load("patterns.json", {"patterns": [], "automation_opportunities": []})
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def load_traces():
|
|
81
|
+
return _load("traces.json", {"traces": []})
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _normalize_category(name: str) -> str:
|
|
85
|
+
if not name:
|
|
86
|
+
return ""
|
|
87
|
+
normalized = re.sub(r"[^a-z0-9_-]+", "-", str(name).strip().lower())
|
|
88
|
+
normalized = re.sub(r"-+", "-", normalized).strip("-")
|
|
89
|
+
return normalized
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _default_learning_routes():
|
|
93
|
+
today_file = DAILY_DIR / f"{date.today().isoformat()}.md"
|
|
94
|
+
return {
|
|
95
|
+
"playbook": {
|
|
96
|
+
"path": KNOWLEDGE_DIR / "runbooks.md",
|
|
97
|
+
"heading": "## AI Captured Playbooks",
|
|
98
|
+
"description": "Reusable work playbooks and runbook snippets",
|
|
99
|
+
"source": "system",
|
|
100
|
+
},
|
|
101
|
+
"stakeholder": {
|
|
102
|
+
"path": STAKEHOLDERS_DIR / "communication-log.md",
|
|
103
|
+
"heading": "## AI Captured Stakeholder Notes",
|
|
104
|
+
"description": "Stakeholder communication notes",
|
|
105
|
+
"source": "system",
|
|
106
|
+
},
|
|
107
|
+
"personal": {
|
|
108
|
+
"path": today_file,
|
|
109
|
+
"heading": "## Personal Notes",
|
|
110
|
+
"description": "Personal productivity notes for today",
|
|
111
|
+
"source": "system",
|
|
112
|
+
},
|
|
113
|
+
"troubleshooting": {
|
|
114
|
+
"path": PATTERNS_DIR / "recurring-issues.md",
|
|
115
|
+
"heading": "## AI Captured Troubleshooting Patterns",
|
|
116
|
+
"description": "Recurring issues and fixes",
|
|
117
|
+
"source": "system",
|
|
118
|
+
},
|
|
119
|
+
"success-pattern": {
|
|
120
|
+
"path": PATTERNS_DIR / "success-patterns.md",
|
|
121
|
+
"heading": "## AI Captured Success Patterns",
|
|
122
|
+
"description": "Patterns behind successful outcomes",
|
|
123
|
+
"source": "system",
|
|
124
|
+
},
|
|
125
|
+
"failure-pattern": {
|
|
126
|
+
"path": PATTERNS_DIR / "failure-patterns.md",
|
|
127
|
+
"heading": "## AI Captured Failure Patterns",
|
|
128
|
+
"description": "Patterns behind failed outcomes",
|
|
129
|
+
"source": "system",
|
|
130
|
+
},
|
|
131
|
+
"recurring-question": {
|
|
132
|
+
"path": PATTERNS_DIR / "recurring-questions.md",
|
|
133
|
+
"heading": "## AI Captured Questions",
|
|
134
|
+
"description": "Frequently recurring questions",
|
|
135
|
+
"source": "system",
|
|
136
|
+
},
|
|
137
|
+
"documentation-gap": {
|
|
138
|
+
"path": PATTERNS_DIR / "documentation-gaps.md",
|
|
139
|
+
"heading": "## AI Captured Documentation Gaps",
|
|
140
|
+
"description": "Missing docs and runbook gaps",
|
|
141
|
+
"source": "system",
|
|
142
|
+
},
|
|
143
|
+
"task-execution": {
|
|
144
|
+
"path": PATTERNS_DIR / "task-execution.md",
|
|
145
|
+
"heading": "## AI Captured Task Execution Learnings",
|
|
146
|
+
"description": "Learned signals from finished task operations",
|
|
147
|
+
"source": "system",
|
|
148
|
+
},
|
|
149
|
+
"writing-update": {
|
|
150
|
+
"path": KNOWLEDGE_DIR / "writing-updates.md",
|
|
151
|
+
"heading": "## AI Captured Writing Updates",
|
|
152
|
+
"description": "Learned signals from file writes and edits",
|
|
153
|
+
"source": "system",
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _load_custom_learning_categories():
|
|
159
|
+
data = _load("learning-categories.json", {"categories": {}})
|
|
160
|
+
if not isinstance(data, dict):
|
|
161
|
+
return {"categories": {}}
|
|
162
|
+
categories = data.get("categories")
|
|
163
|
+
if not isinstance(categories, dict):
|
|
164
|
+
return {"categories": {}}
|
|
165
|
+
return {"categories": categories}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _save_custom_learning_categories(data):
|
|
169
|
+
_save("learning-categories.json", data)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def get_learning_routes():
|
|
173
|
+
routes = _default_learning_routes()
|
|
174
|
+
custom = _load_custom_learning_categories().get("categories", {})
|
|
175
|
+
|
|
176
|
+
for raw_name, meta in custom.items():
|
|
177
|
+
if not isinstance(meta, dict):
|
|
178
|
+
continue
|
|
179
|
+
bucket = _normalize_category(raw_name)
|
|
180
|
+
if not bucket:
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
path_raw = str(meta.get("path", "")).strip()
|
|
184
|
+
if path_raw:
|
|
185
|
+
path = Path(path_raw).expanduser()
|
|
186
|
+
if not path.is_absolute():
|
|
187
|
+
path = ENG_BUDDY_DIR / path
|
|
188
|
+
else:
|
|
189
|
+
path = KNOWLEDGE_DIR / f"{bucket}.md"
|
|
190
|
+
|
|
191
|
+
heading = str(meta.get("heading", "")).strip() or f"## AI Captured {bucket.replace('-', ' ').title()}"
|
|
192
|
+
description = str(meta.get("description", "")).strip() or "User-defined learning category"
|
|
193
|
+
|
|
194
|
+
routes[bucket] = {
|
|
195
|
+
"path": path,
|
|
196
|
+
"heading": heading,
|
|
197
|
+
"description": description,
|
|
198
|
+
"source": "custom",
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return routes
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def list_learning_buckets():
|
|
205
|
+
return sorted(get_learning_routes().keys())
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _ensure_learning_schema():
|
|
209
|
+
conn = sqlite3.connect(DB_PATH)
|
|
210
|
+
try:
|
|
211
|
+
conn.execute(
|
|
212
|
+
"""CREATE TABLE IF NOT EXISTS learning_categories (
|
|
213
|
+
name TEXT PRIMARY KEY,
|
|
214
|
+
description TEXT,
|
|
215
|
+
source TEXT NOT NULL DEFAULT 'system',
|
|
216
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
217
|
+
)"""
|
|
218
|
+
)
|
|
219
|
+
conn.execute(
|
|
220
|
+
"""CREATE TABLE IF NOT EXISTS learning_events (
|
|
221
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
222
|
+
session_id TEXT,
|
|
223
|
+
hook_event TEXT,
|
|
224
|
+
source TEXT,
|
|
225
|
+
scope TEXT,
|
|
226
|
+
tool_name TEXT,
|
|
227
|
+
category TEXT,
|
|
228
|
+
title TEXT,
|
|
229
|
+
note TEXT,
|
|
230
|
+
status TEXT NOT NULL DEFAULT 'captured',
|
|
231
|
+
requires_category_expansion INTEGER NOT NULL DEFAULT 0,
|
|
232
|
+
proposed_category TEXT,
|
|
233
|
+
metadata TEXT,
|
|
234
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
235
|
+
)"""
|
|
236
|
+
)
|
|
237
|
+
conn.execute(
|
|
238
|
+
"CREATE INDEX IF NOT EXISTS idx_learning_events_session ON learning_events(session_id, created_at)"
|
|
239
|
+
)
|
|
240
|
+
conn.execute(
|
|
241
|
+
"CREATE INDEX IF NOT EXISTS idx_learning_events_category ON learning_events(category, created_at)"
|
|
242
|
+
)
|
|
243
|
+
conn.execute(
|
|
244
|
+
"CREATE INDEX IF NOT EXISTS idx_learning_events_pending ON learning_events(requires_category_expansion, created_at)"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
for bucket, meta in get_learning_routes().items():
|
|
248
|
+
conn.execute(
|
|
249
|
+
"""INSERT OR IGNORE INTO learning_categories (name, description, source)
|
|
250
|
+
VALUES (?, ?, ?)""",
|
|
251
|
+
[bucket, meta.get("description", ""), meta.get("source", "system")],
|
|
252
|
+
)
|
|
253
|
+
conn.commit()
|
|
254
|
+
finally:
|
|
255
|
+
conn.close()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _ensure_ops_schema():
|
|
259
|
+
"""Create ops tracking tables: capacity, stakeholders, incidents, patterns, follow-ups, burnout."""
|
|
260
|
+
conn = sqlite3.connect(DB_PATH)
|
|
261
|
+
try:
|
|
262
|
+
conn.execute(
|
|
263
|
+
"""CREATE TABLE IF NOT EXISTS capacity_logs (
|
|
264
|
+
id INTEGER PRIMARY KEY,
|
|
265
|
+
date TEXT,
|
|
266
|
+
metric TEXT,
|
|
267
|
+
value REAL,
|
|
268
|
+
notes TEXT,
|
|
269
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
270
|
+
)"""
|
|
271
|
+
)
|
|
272
|
+
conn.execute(
|
|
273
|
+
"""CREATE TABLE IF NOT EXISTS stakeholder_contacts (
|
|
274
|
+
id INTEGER PRIMARY KEY,
|
|
275
|
+
name TEXT,
|
|
276
|
+
role TEXT,
|
|
277
|
+
preferences TEXT,
|
|
278
|
+
last_contact TEXT,
|
|
279
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
280
|
+
)"""
|
|
281
|
+
)
|
|
282
|
+
conn.execute(
|
|
283
|
+
"""CREATE TABLE IF NOT EXISTS incidents (
|
|
284
|
+
id INTEGER PRIMARY KEY,
|
|
285
|
+
title TEXT,
|
|
286
|
+
severity TEXT,
|
|
287
|
+
status TEXT DEFAULT 'open',
|
|
288
|
+
timeline TEXT,
|
|
289
|
+
root_cause TEXT,
|
|
290
|
+
resolution TEXT,
|
|
291
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
292
|
+
)"""
|
|
293
|
+
)
|
|
294
|
+
conn.execute(
|
|
295
|
+
"""CREATE TABLE IF NOT EXISTS pattern_observations (
|
|
296
|
+
id INTEGER PRIMARY KEY,
|
|
297
|
+
type TEXT,
|
|
298
|
+
title TEXT,
|
|
299
|
+
description TEXT,
|
|
300
|
+
confidence REAL,
|
|
301
|
+
evidence TEXT,
|
|
302
|
+
frequency INTEGER DEFAULT 1,
|
|
303
|
+
first_seen TEXT,
|
|
304
|
+
last_seen TEXT,
|
|
305
|
+
source_file TEXT,
|
|
306
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
307
|
+
)"""
|
|
308
|
+
)
|
|
309
|
+
conn.execute(
|
|
310
|
+
"""CREATE TABLE IF NOT EXISTS follow_ups (
|
|
311
|
+
id INTEGER PRIMARY KEY,
|
|
312
|
+
stakeholder TEXT,
|
|
313
|
+
topic TEXT,
|
|
314
|
+
due_date TEXT,
|
|
315
|
+
status TEXT DEFAULT 'pending',
|
|
316
|
+
notes TEXT,
|
|
317
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
318
|
+
)"""
|
|
319
|
+
)
|
|
320
|
+
conn.execute(
|
|
321
|
+
"""CREATE TABLE IF NOT EXISTS burnout_indicators (
|
|
322
|
+
id INTEGER PRIMARY KEY,
|
|
323
|
+
date TEXT,
|
|
324
|
+
indicator TEXT,
|
|
325
|
+
severity TEXT,
|
|
326
|
+
details TEXT,
|
|
327
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
328
|
+
)"""
|
|
329
|
+
)
|
|
330
|
+
# FTS5 virtual table for pattern search
|
|
331
|
+
conn.execute(
|
|
332
|
+
"""CREATE VIRTUAL TABLE IF NOT EXISTS pattern_observations_fts USING fts5(
|
|
333
|
+
title, description, evidence,
|
|
334
|
+
content='pattern_observations',
|
|
335
|
+
content_rowid='id'
|
|
336
|
+
)"""
|
|
337
|
+
)
|
|
338
|
+
conn.commit()
|
|
339
|
+
finally:
|
|
340
|
+
conn.close()
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _record_learning_event(
|
|
344
|
+
*,
|
|
345
|
+
session_id: str = "",
|
|
346
|
+
hook_event: str = "",
|
|
347
|
+
source: str = "",
|
|
348
|
+
scope: str = "",
|
|
349
|
+
tool_name: str = "",
|
|
350
|
+
category: str = "",
|
|
351
|
+
title: str = "",
|
|
352
|
+
note: str = "",
|
|
353
|
+
status: str = "captured",
|
|
354
|
+
requires_category_expansion: bool = False,
|
|
355
|
+
proposed_category: str = "",
|
|
356
|
+
metadata=None,
|
|
357
|
+
):
|
|
358
|
+
_ensure_learning_schema()
|
|
359
|
+
conn = sqlite3.connect(DB_PATH)
|
|
360
|
+
try:
|
|
361
|
+
conn.execute(
|
|
362
|
+
"""INSERT INTO learning_events (
|
|
363
|
+
session_id, hook_event, source, scope, tool_name,
|
|
364
|
+
category, title, note, status,
|
|
365
|
+
requires_category_expansion, proposed_category, metadata
|
|
366
|
+
)
|
|
367
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
368
|
+
""",
|
|
369
|
+
[
|
|
370
|
+
session_id or "",
|
|
371
|
+
hook_event or "",
|
|
372
|
+
source or "",
|
|
373
|
+
scope or "",
|
|
374
|
+
tool_name or "",
|
|
375
|
+
category or "",
|
|
376
|
+
title or "",
|
|
377
|
+
note or "",
|
|
378
|
+
status,
|
|
379
|
+
1 if requires_category_expansion else 0,
|
|
380
|
+
proposed_category or "",
|
|
381
|
+
json.dumps(metadata or {}),
|
|
382
|
+
],
|
|
383
|
+
)
|
|
384
|
+
conn.commit()
|
|
385
|
+
finally:
|
|
386
|
+
conn.close()
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def register_learning_category(name: str, description: str = "", path: str = "", heading: str = ""):
|
|
390
|
+
bucket = _normalize_category(name)
|
|
391
|
+
if not bucket:
|
|
392
|
+
raise ValueError("category name is required")
|
|
393
|
+
|
|
394
|
+
resolved_path = path.strip() if path else f"knowledge/{bucket}.md"
|
|
395
|
+
resolved_heading = heading.strip() if heading else f"## AI Captured {bucket.replace('-', ' ').title()}"
|
|
396
|
+
resolved_description = description.strip() if description else "User-approved custom learning category"
|
|
397
|
+
|
|
398
|
+
custom = _load_custom_learning_categories()
|
|
399
|
+
custom.setdefault("categories", {})[bucket] = {
|
|
400
|
+
"path": resolved_path,
|
|
401
|
+
"heading": resolved_heading,
|
|
402
|
+
"description": resolved_description,
|
|
403
|
+
}
|
|
404
|
+
_save_custom_learning_categories(custom)
|
|
405
|
+
|
|
406
|
+
_ensure_learning_schema()
|
|
407
|
+
conn = sqlite3.connect(DB_PATH)
|
|
408
|
+
try:
|
|
409
|
+
conn.execute(
|
|
410
|
+
"""INSERT INTO learning_categories (name, description, source)
|
|
411
|
+
VALUES (?, ?, 'custom')
|
|
412
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
413
|
+
description = excluded.description,
|
|
414
|
+
source = 'custom'""",
|
|
415
|
+
[bucket, resolved_description],
|
|
416
|
+
)
|
|
417
|
+
conn.commit()
|
|
418
|
+
finally:
|
|
419
|
+
conn.close()
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
"added": True,
|
|
423
|
+
"category": bucket,
|
|
424
|
+
"path": resolved_path,
|
|
425
|
+
"heading": resolved_heading,
|
|
426
|
+
"description": resolved_description,
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _append_markdown_note(path: Path, heading: str, line: str):
|
|
431
|
+
"""Append a single bullet line under a heading in markdown file."""
|
|
432
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
433
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
434
|
+
bullet = f"- {timestamp} | {line.strip()}"
|
|
435
|
+
|
|
436
|
+
if not path.exists():
|
|
437
|
+
title = path.stem.replace("-", " ").title()
|
|
438
|
+
path.write_text(f"# {title}\n\n{heading}\n{bullet}\n", encoding="utf-8")
|
|
439
|
+
return
|
|
440
|
+
|
|
441
|
+
content = path.read_text(encoding="utf-8")
|
|
442
|
+
if bullet in content:
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
lines = content.splitlines()
|
|
446
|
+
heading_idx = next((i for i, h in enumerate(lines) if h.strip() == heading), None)
|
|
447
|
+
|
|
448
|
+
if heading_idx is None:
|
|
449
|
+
if lines and lines[-1].strip():
|
|
450
|
+
lines.append("")
|
|
451
|
+
lines.extend([heading, bullet])
|
|
452
|
+
else:
|
|
453
|
+
insert_at = heading_idx + 1
|
|
454
|
+
if insert_at < len(lines) and lines[insert_at].strip() == "":
|
|
455
|
+
insert_at += 1
|
|
456
|
+
lines.insert(insert_at, bullet)
|
|
457
|
+
|
|
458
|
+
path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _route_learning_logs(entries):
|
|
462
|
+
"""Route structured learning notes into long-lived markdown knowledge files."""
|
|
463
|
+
if not entries:
|
|
464
|
+
return []
|
|
465
|
+
|
|
466
|
+
routes = get_learning_routes()
|
|
467
|
+
pending_expansion = []
|
|
468
|
+
|
|
469
|
+
for entry in entries:
|
|
470
|
+
if not isinstance(entry, dict):
|
|
471
|
+
continue
|
|
472
|
+
|
|
473
|
+
bucket = _normalize_category(str(entry.get("bucket", "troubleshooting")))
|
|
474
|
+
title = str(entry.get("title", "")).strip()
|
|
475
|
+
note = str(entry.get("note", "")).strip()
|
|
476
|
+
if not note:
|
|
477
|
+
continue
|
|
478
|
+
|
|
479
|
+
line = f"{title}: {note}" if title else note
|
|
480
|
+
|
|
481
|
+
if bucket in routes:
|
|
482
|
+
route = routes[bucket]
|
|
483
|
+
_append_markdown_note(route["path"], route["heading"], line)
|
|
484
|
+
_record_learning_event(
|
|
485
|
+
source="learning-log",
|
|
486
|
+
scope="ai_response",
|
|
487
|
+
category=bucket,
|
|
488
|
+
title=title,
|
|
489
|
+
note=note,
|
|
490
|
+
status="captured",
|
|
491
|
+
metadata={"entry": entry},
|
|
492
|
+
)
|
|
493
|
+
continue
|
|
494
|
+
|
|
495
|
+
proposed_bucket = bucket or "uncategorized"
|
|
496
|
+
pending_expansion.append(proposed_bucket)
|
|
497
|
+
_append_markdown_note(UNMAPPED_LEARNING_PATH, UNMAPPED_LEARNING_HEADING, line)
|
|
498
|
+
_record_learning_event(
|
|
499
|
+
source="learning-log",
|
|
500
|
+
scope="ai_response",
|
|
501
|
+
category="",
|
|
502
|
+
title=title,
|
|
503
|
+
note=note,
|
|
504
|
+
status="needs_category_expansion",
|
|
505
|
+
requires_category_expansion=True,
|
|
506
|
+
proposed_category=proposed_bucket,
|
|
507
|
+
metadata={"entry": entry},
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
return sorted(set(pending_expansion))
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def load_decisions(query, limit=5):
|
|
514
|
+
"""Search past decisions by keywords. Returns list of dicts."""
|
|
515
|
+
if not DB_PATH.exists():
|
|
516
|
+
return []
|
|
517
|
+
conn = sqlite3.connect(DB_PATH)
|
|
518
|
+
conn.row_factory = sqlite3.Row
|
|
519
|
+
try:
|
|
520
|
+
# Try FTS5 (sanitize query by quoting each term)
|
|
521
|
+
try:
|
|
522
|
+
safe_query = " ".join(f'"{w}"' for w in query.split() if w)
|
|
523
|
+
if not safe_query:
|
|
524
|
+
safe_query = '""'
|
|
525
|
+
rows = conn.execute(
|
|
526
|
+
"""SELECT d.summary, d.action, d.source, d.context_notes,
|
|
527
|
+
d.draft_response, d.decision_at
|
|
528
|
+
FROM decisions d
|
|
529
|
+
JOIN decisions_fts fts ON d.id = fts.rowid
|
|
530
|
+
WHERE decisions_fts MATCH ?
|
|
531
|
+
ORDER BY d.decision_at DESC LIMIT ?""",
|
|
532
|
+
[safe_query, limit]
|
|
533
|
+
).fetchall()
|
|
534
|
+
except sqlite3.OperationalError:
|
|
535
|
+
like = f"%{query}%"
|
|
536
|
+
rows = conn.execute(
|
|
537
|
+
"""SELECT summary, action, source, context_notes,
|
|
538
|
+
draft_response, decision_at
|
|
539
|
+
FROM decisions
|
|
540
|
+
WHERE summary LIKE ? OR context_notes LIKE ?
|
|
541
|
+
OR draft_response LIKE ? OR tags LIKE ?
|
|
542
|
+
ORDER BY decision_at DESC LIMIT ?""",
|
|
543
|
+
[like, like, like, like, limit]
|
|
544
|
+
).fetchall()
|
|
545
|
+
return [dict(r) for r in rows]
|
|
546
|
+
except Exception:
|
|
547
|
+
return []
|
|
548
|
+
finally:
|
|
549
|
+
conn.close()
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def build_context_prompt(batch_items=None):
|
|
553
|
+
"""Build the persistent context block injected into every Claude CLI call."""
|
|
554
|
+
ctx = load_context()
|
|
555
|
+
stakeholders = load_stakeholders()
|
|
556
|
+
patterns = load_patterns()
|
|
557
|
+
|
|
558
|
+
# Pick relevant stakeholders if batch has sender info
|
|
559
|
+
relevant = {}
|
|
560
|
+
if batch_items:
|
|
561
|
+
senders = set()
|
|
562
|
+
for item in batch_items:
|
|
563
|
+
s = item.get("sender_email", "") or item.get("from", "") or item.get("sender", "")
|
|
564
|
+
if s:
|
|
565
|
+
# Normalize to username
|
|
566
|
+
username = s.split("@")[0].replace(".", "_") if "@" in s else s.lower().replace(" ", "_")
|
|
567
|
+
senders.add(username)
|
|
568
|
+
for key, val in stakeholders.items():
|
|
569
|
+
normalized = key.replace(".", "_")
|
|
570
|
+
if normalized in senders or any(normalized in s for s in senders):
|
|
571
|
+
relevant[key] = val
|
|
572
|
+
|
|
573
|
+
priorities_str = "\n".join(f"- {p}" for p in ctx.get("current_priorities", [])) or "None set"
|
|
574
|
+
rules_str = "\n".join(f"- {r}" for r in ctx.get("learned_rules", [])) or "None yet"
|
|
575
|
+
|
|
576
|
+
stakeholder_str = ""
|
|
577
|
+
if relevant:
|
|
578
|
+
parts = []
|
|
579
|
+
for name, info in relevant.items():
|
|
580
|
+
parts.append(f" {name}: {info.get('role', 'unknown')} — {info.get('relationship', '')} — expects response in {info.get('avg_response_expectation', 'unknown')}")
|
|
581
|
+
stakeholder_str = "\n".join(parts)
|
|
582
|
+
else:
|
|
583
|
+
stakeholder_str = " No matching stakeholders for this batch."
|
|
584
|
+
|
|
585
|
+
playbook_str = ""
|
|
586
|
+
known = patterns.get("patterns", [])
|
|
587
|
+
if known:
|
|
588
|
+
parts = []
|
|
589
|
+
for p in known[:10]:
|
|
590
|
+
parts.append(f" - {p['id']}: trigger={p.get('trigger', '?')}, steps={len(p.get('steps', []))}, used {p.get('times_used', 0)} times")
|
|
591
|
+
playbook_str = "\n".join(parts)
|
|
592
|
+
else:
|
|
593
|
+
playbook_str = " No playbooks captured yet."
|
|
594
|
+
|
|
595
|
+
# Find similar past decisions based on batch item summaries
|
|
596
|
+
decisions_str = ""
|
|
597
|
+
if batch_items:
|
|
598
|
+
seen = set()
|
|
599
|
+
all_decisions = []
|
|
600
|
+
for item in batch_items:
|
|
601
|
+
summary = item.get("summary", "") or item.get("subject", "") or ""
|
|
602
|
+
# Extract key words for search
|
|
603
|
+
words = [w for w in summary.split() if len(w) > 3]
|
|
604
|
+
if words:
|
|
605
|
+
query = " ".join(words[:5])
|
|
606
|
+
for d in load_decisions(query, limit=3):
|
|
607
|
+
key = d.get("summary", "")
|
|
608
|
+
if key not in seen:
|
|
609
|
+
seen.add(key)
|
|
610
|
+
all_decisions.append(d)
|
|
611
|
+
if all_decisions:
|
|
612
|
+
parts = []
|
|
613
|
+
for d in all_decisions[:5]:
|
|
614
|
+
parts.append(f" - [{d.get('decision_at', '?')[:10]}] {d.get('action', '?')}: {d.get('summary', '?')}")
|
|
615
|
+
if d.get("draft_response"):
|
|
616
|
+
parts.append(f" Response sent: {d['draft_response'][:100]}...")
|
|
617
|
+
decisions_str = "\n".join(parts)
|
|
618
|
+
|
|
619
|
+
if not decisions_str:
|
|
620
|
+
decisions_str = " No similar past decisions found."
|
|
621
|
+
|
|
622
|
+
learning_buckets = "|".join(list_learning_buckets())
|
|
623
|
+
|
|
624
|
+
return f"""You are eng-buddy, an intelligent work assistant for {ctx.get('role', 'an engineer')} at {ctx.get('company', 'a company')}.
|
|
625
|
+
Manager: {ctx.get('manager', 'unknown')}
|
|
626
|
+
Team: {ctx.get('team', 'unknown')}
|
|
627
|
+
Response tone: {ctx.get('preferences', {}).get('response_tone', 'professional')}
|
|
628
|
+
|
|
629
|
+
Current priorities:
|
|
630
|
+
{priorities_str}
|
|
631
|
+
|
|
632
|
+
Learned rules (APPLY THESE):
|
|
633
|
+
{rules_str}
|
|
634
|
+
|
|
635
|
+
Relevant stakeholders:
|
|
636
|
+
{stakeholder_str}
|
|
637
|
+
|
|
638
|
+
Known playbooks:
|
|
639
|
+
{playbook_str}
|
|
640
|
+
|
|
641
|
+
Similar past decisions (use these for consistency):
|
|
642
|
+
{decisions_str}
|
|
643
|
+
|
|
644
|
+
AFTER completing your primary task, also output these sections if applicable (as JSON blocks):
|
|
645
|
+
- <!--STAKEHOLDER_UPDATES-->: [{{"name": "...", "field": "...", "value": "..."}}]
|
|
646
|
+
- <!--NEW_PATTERNS-->: [{{"trigger": "...", "steps": [...], "category": "..."}}]
|
|
647
|
+
- <!--AUTOMATION_OPPORTUNITIES-->: [{{"observation": "...", "suggestion": "..."}}]
|
|
648
|
+
- <!--LEARNED_RULES-->: ["rule text", ...]
|
|
649
|
+
- <!--WORK_TRACES-->: [{{"trigger": "...", "category": "...", "step_observed": "..."}}]
|
|
650
|
+
- <!--LEARNING_LOGS-->: [{{"bucket":"{learning_buckets}","title":"...","note":"..."}}]
|
|
651
|
+
"""
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def parse_learning(claude_response):
|
|
655
|
+
"""Parse Claude's response for learning sections and merge into memory."""
|
|
656
|
+
sections = {
|
|
657
|
+
"STAKEHOLDER_UPDATES": _parse_section(claude_response, "STAKEHOLDER_UPDATES"),
|
|
658
|
+
"NEW_PATTERNS": _parse_section(claude_response, "NEW_PATTERNS"),
|
|
659
|
+
"AUTOMATION_OPPORTUNITIES": _parse_section(claude_response, "AUTOMATION_OPPORTUNITIES"),
|
|
660
|
+
"LEARNED_RULES": _parse_section(claude_response, "LEARNED_RULES"),
|
|
661
|
+
"WORK_TRACES": _parse_section(claude_response, "WORK_TRACES"),
|
|
662
|
+
"LEARNING_LOGS": _parse_section(claude_response, "LEARNING_LOGS"),
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if sections["STAKEHOLDER_UPDATES"]:
|
|
666
|
+
sh = load_stakeholders()
|
|
667
|
+
for update in sections["STAKEHOLDER_UPDATES"]:
|
|
668
|
+
name = update.get("name", "")
|
|
669
|
+
if name:
|
|
670
|
+
if name not in sh:
|
|
671
|
+
sh[name] = {}
|
|
672
|
+
field = update.get("field", "")
|
|
673
|
+
if field:
|
|
674
|
+
sh[name][field] = update.get("value", "")
|
|
675
|
+
sh[name]["last_updated"] = datetime.now().isoformat()
|
|
676
|
+
_save("stakeholders.json", sh)
|
|
677
|
+
|
|
678
|
+
if sections["NEW_PATTERNS"]:
|
|
679
|
+
pt = load_patterns()
|
|
680
|
+
for pattern in sections["NEW_PATTERNS"]:
|
|
681
|
+
pid = pattern.get("category", "unknown") + "-" + str(len(pt["patterns"]))
|
|
682
|
+
pt["patterns"].append({
|
|
683
|
+
"id": pid,
|
|
684
|
+
"trigger": pattern.get("trigger", ""),
|
|
685
|
+
"steps": pattern.get("steps", []),
|
|
686
|
+
"category": pattern.get("category", ""),
|
|
687
|
+
"automation_level": "observe",
|
|
688
|
+
"times_used": 1,
|
|
689
|
+
"detected_at": datetime.now().isoformat(),
|
|
690
|
+
})
|
|
691
|
+
_save("patterns.json", pt)
|
|
692
|
+
|
|
693
|
+
if sections["AUTOMATION_OPPORTUNITIES"]:
|
|
694
|
+
pt = load_patterns()
|
|
695
|
+
for opp in sections["AUTOMATION_OPPORTUNITIES"]:
|
|
696
|
+
pt["automation_opportunities"].append({
|
|
697
|
+
"observation": opp.get("observation", ""),
|
|
698
|
+
"suggestion": opp.get("suggestion", ""),
|
|
699
|
+
"status": "pending_review",
|
|
700
|
+
"detected_at": datetime.now().isoformat(),
|
|
701
|
+
})
|
|
702
|
+
_save("patterns.json", pt)
|
|
703
|
+
|
|
704
|
+
if sections["LEARNED_RULES"]:
|
|
705
|
+
ctx = load_context()
|
|
706
|
+
existing = set(ctx.get("learned_rules", []))
|
|
707
|
+
for rule in sections["LEARNED_RULES"]:
|
|
708
|
+
if isinstance(rule, str) and rule not in existing:
|
|
709
|
+
ctx.setdefault("learned_rules", []).append(rule)
|
|
710
|
+
_save("context.json", ctx)
|
|
711
|
+
|
|
712
|
+
if sections["WORK_TRACES"]:
|
|
713
|
+
tr = load_traces()
|
|
714
|
+
for trace in sections["WORK_TRACES"]:
|
|
715
|
+
tr["traces"].append({
|
|
716
|
+
**trace,
|
|
717
|
+
"timestamp": datetime.now().isoformat(),
|
|
718
|
+
})
|
|
719
|
+
# Cap at 500 traces
|
|
720
|
+
tr["traces"] = tr["traces"][-500:]
|
|
721
|
+
_save("traces.json", tr)
|
|
722
|
+
|
|
723
|
+
pending_categories = []
|
|
724
|
+
if sections["LEARNING_LOGS"]:
|
|
725
|
+
pending_categories = _route_learning_logs(sections["LEARNING_LOGS"])
|
|
726
|
+
|
|
727
|
+
sections["PENDING_CATEGORY_EXPANSIONS"] = pending_categories
|
|
728
|
+
return sections
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _parse_section(text, section_name):
|
|
732
|
+
"""Extract a JSON block between <!--SECTION--> markers."""
|
|
733
|
+
pattern = rf'<!--{section_name}-->\s*(\[.*?\])'
|
|
734
|
+
match = re.search(pattern, text, re.DOTALL)
|
|
735
|
+
if match:
|
|
736
|
+
try:
|
|
737
|
+
return json.loads(match.group(1))
|
|
738
|
+
except json.JSONDecodeError:
|
|
739
|
+
pass
|
|
740
|
+
return []
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _extract_tool_name_parts(tool_name: str):
|
|
744
|
+
if not tool_name.startswith("mcp__"):
|
|
745
|
+
return "", ""
|
|
746
|
+
parts = tool_name.split("__")
|
|
747
|
+
provider = parts[1] if len(parts) > 1 else ""
|
|
748
|
+
action = parts[2] if len(parts) > 2 else ""
|
|
749
|
+
return provider, action
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _classify_post_tool_category(tool_name: str, tool_input: dict):
|
|
753
|
+
if tool_name in WRITE_TOOLS:
|
|
754
|
+
return "writing-update", ""
|
|
755
|
+
|
|
756
|
+
if tool_name in TASK_TOOLS:
|
|
757
|
+
return "task-execution", ""
|
|
758
|
+
|
|
759
|
+
if tool_name.startswith("mcp__"):
|
|
760
|
+
provider, _action = _extract_tool_name_parts(tool_name)
|
|
761
|
+
if provider in ACTION_MCP_PROVIDERS:
|
|
762
|
+
return "task-execution", ""
|
|
763
|
+
if provider in READ_ONLY_MCP_PROVIDERS:
|
|
764
|
+
return "", ""
|
|
765
|
+
|
|
766
|
+
proposed = _normalize_category(f"integration-{provider or 'unknown'}")
|
|
767
|
+
return "", proposed
|
|
768
|
+
|
|
769
|
+
return "", ""
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def _summarize_post_tool_learning(tool_name: str, tool_input: dict):
|
|
773
|
+
if not isinstance(tool_input, dict):
|
|
774
|
+
tool_input = {}
|
|
775
|
+
|
|
776
|
+
if tool_name in WRITE_TOOLS:
|
|
777
|
+
file_path = str(tool_input.get("file_path", "")).strip()
|
|
778
|
+
if not file_path and isinstance(tool_input.get("files"), list):
|
|
779
|
+
file_path = ", ".join(str(p) for p in tool_input.get("files", [])[:3])
|
|
780
|
+
if file_path:
|
|
781
|
+
return "File update", f"{tool_name} completed on {file_path}"
|
|
782
|
+
return "File update", f"{tool_name} completed"
|
|
783
|
+
|
|
784
|
+
if tool_name == "Bash":
|
|
785
|
+
command = str(tool_input.get("command", "")).strip()
|
|
786
|
+
if len(command) > 180:
|
|
787
|
+
command = command[:177] + "..."
|
|
788
|
+
if command:
|
|
789
|
+
return "Task execution", f"Bash command completed: {command}"
|
|
790
|
+
return "Task execution", "Bash command completed"
|
|
791
|
+
|
|
792
|
+
if tool_name.startswith("mcp__"):
|
|
793
|
+
provider, action = _extract_tool_name_parts(tool_name)
|
|
794
|
+
provider_label = provider or "unknown"
|
|
795
|
+
action_label = action or "operation"
|
|
796
|
+
return "External integration", f"{provider_label} {action_label} completed"
|
|
797
|
+
|
|
798
|
+
return "Task execution", f"{tool_name} completed"
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def capture_post_tool_learning(payload: dict):
|
|
802
|
+
"""Capture PostToolUse learning into DB/markdown routes."""
|
|
803
|
+
if not isinstance(payload, dict):
|
|
804
|
+
return {"recorded": False, "reason": "invalid_payload"}
|
|
805
|
+
|
|
806
|
+
tool_name = str(payload.get("tool_name", "")).strip()
|
|
807
|
+
tool_input = payload.get("tool_input")
|
|
808
|
+
if isinstance(tool_input, str):
|
|
809
|
+
try:
|
|
810
|
+
tool_input = json.loads(tool_input)
|
|
811
|
+
except json.JSONDecodeError:
|
|
812
|
+
tool_input = {"raw": tool_input}
|
|
813
|
+
if not isinstance(tool_input, dict):
|
|
814
|
+
tool_input = {}
|
|
815
|
+
|
|
816
|
+
category, proposed_category = _classify_post_tool_category(tool_name, tool_input)
|
|
817
|
+
if not category and not proposed_category:
|
|
818
|
+
return {"recorded": False, "reason": "untracked_tool"}
|
|
819
|
+
|
|
820
|
+
title, note = _summarize_post_tool_learning(tool_name, tool_input)
|
|
821
|
+
session_id = str(payload.get("session_id", ""))
|
|
822
|
+
|
|
823
|
+
if category:
|
|
824
|
+
routes = get_learning_routes()
|
|
825
|
+
route = routes.get(category)
|
|
826
|
+
if route:
|
|
827
|
+
_append_markdown_note(route["path"], route["heading"], note)
|
|
828
|
+
_record_learning_event(
|
|
829
|
+
session_id=session_id,
|
|
830
|
+
hook_event="PostToolUse",
|
|
831
|
+
source="hook",
|
|
832
|
+
scope="tool_completion",
|
|
833
|
+
tool_name=tool_name,
|
|
834
|
+
category=category,
|
|
835
|
+
title=title,
|
|
836
|
+
note=note,
|
|
837
|
+
status="captured",
|
|
838
|
+
metadata={"tool_input": tool_input},
|
|
839
|
+
)
|
|
840
|
+
return {
|
|
841
|
+
"recorded": True,
|
|
842
|
+
"category": category,
|
|
843
|
+
"needs_category_expansion": False,
|
|
844
|
+
"title": title,
|
|
845
|
+
"note": note,
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
proposed_category = category
|
|
849
|
+
|
|
850
|
+
# Category couldn't be routed: capture as pending and ask user later.
|
|
851
|
+
_append_markdown_note(UNMAPPED_LEARNING_PATH, UNMAPPED_LEARNING_HEADING, note)
|
|
852
|
+
_record_learning_event(
|
|
853
|
+
session_id=session_id,
|
|
854
|
+
hook_event="PostToolUse",
|
|
855
|
+
source="hook",
|
|
856
|
+
scope="tool_completion",
|
|
857
|
+
tool_name=tool_name,
|
|
858
|
+
category="",
|
|
859
|
+
title=title,
|
|
860
|
+
note=note,
|
|
861
|
+
status="needs_category_expansion",
|
|
862
|
+
requires_category_expansion=True,
|
|
863
|
+
proposed_category=proposed_category,
|
|
864
|
+
metadata={"tool_input": tool_input},
|
|
865
|
+
)
|
|
866
|
+
return {
|
|
867
|
+
"recorded": True,
|
|
868
|
+
"category": "",
|
|
869
|
+
"needs_category_expansion": True,
|
|
870
|
+
"proposed_category": proposed_category,
|
|
871
|
+
"title": title,
|
|
872
|
+
"note": note,
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def _cli():
|
|
877
|
+
parser = argparse.ArgumentParser(description="eng-buddy learning engine utilities")
|
|
878
|
+
parser.add_argument("--register-learning-category", dest="register_learning_category", default="")
|
|
879
|
+
parser.add_argument("--description", default="")
|
|
880
|
+
parser.add_argument("--path", default="")
|
|
881
|
+
parser.add_argument("--heading", default="")
|
|
882
|
+
parser.add_argument(
|
|
883
|
+
"--capture-post-tool",
|
|
884
|
+
action="store_true",
|
|
885
|
+
help="Read PostToolUse payload JSON from stdin and capture learning event",
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
# --- Playbook Engine Commands ---
|
|
889
|
+
parser.add_argument("--playbook-trace-event", action="store_true",
|
|
890
|
+
help="Record a trace event (reads JSON from stdin: {trace_id, event})")
|
|
891
|
+
parser.add_argument("--playbook-extract", type=str, metavar="TRACE_ID",
|
|
892
|
+
help="Extract a draft playbook from a completed trace")
|
|
893
|
+
parser.add_argument("--playbook-extract-name", type=str, default="Untitled",
|
|
894
|
+
help="Name for the extracted playbook (used with --playbook-extract)")
|
|
895
|
+
parser.add_argument("--playbook-match", type=str, metavar="TEXT",
|
|
896
|
+
help="Find playbooks matching ticket text")
|
|
897
|
+
parser.add_argument("--playbook-match-type", type=str, default="",
|
|
898
|
+
help="Ticket type for matching (used with --playbook-match)")
|
|
899
|
+
parser.add_argument("--playbook-match-source", type=str, default="freshservice",
|
|
900
|
+
help="Source system for matching (used with --playbook-match)")
|
|
901
|
+
parser.add_argument("--playbook-list", action="store_true",
|
|
902
|
+
help="List all approved playbooks")
|
|
903
|
+
parser.add_argument("--playbook-list-drafts", action="store_true",
|
|
904
|
+
help="List all draft playbooks")
|
|
905
|
+
parser.add_argument("--playbook-promote", type=str, metavar="PLAYBOOK_ID",
|
|
906
|
+
help="Promote a draft playbook to approved")
|
|
907
|
+
|
|
908
|
+
# --- Task Management Commands ---
|
|
909
|
+
parser.add_argument("--tasks", action="store_true",
|
|
910
|
+
help="List all non-completed tasks")
|
|
911
|
+
parser.add_argument("--tasks-all", action="store_true",
|
|
912
|
+
help="List ALL tasks including completed")
|
|
913
|
+
parser.add_argument("--task", type=int, metavar="N",
|
|
914
|
+
help="Show full detail for task N")
|
|
915
|
+
parser.add_argument("--task-add", action="store_true",
|
|
916
|
+
help="Create a new task (requires --title)")
|
|
917
|
+
parser.add_argument("--task-update", type=int, metavar="N",
|
|
918
|
+
help="Update task N (use with --status, --priority, --deferred-until)")
|
|
919
|
+
parser.add_argument("--task-search", type=str, metavar="KEYWORD",
|
|
920
|
+
help="Full-text search for tasks")
|
|
921
|
+
parser.add_argument("--task-json", action="store_true",
|
|
922
|
+
help="Output task results as JSON instead of table format")
|
|
923
|
+
parser.add_argument("--task-export", type=int, metavar="N",
|
|
924
|
+
help="Export task N as markdown context block")
|
|
925
|
+
parser.add_argument("--title", type=str, default="",
|
|
926
|
+
help="Title for --task-add")
|
|
927
|
+
parser.add_argument("--status", type=str, default="",
|
|
928
|
+
help="Status for --task-update")
|
|
929
|
+
parser.add_argument("--priority", type=str, default="",
|
|
930
|
+
help="Priority for --task-add or --task-update")
|
|
931
|
+
parser.add_argument("--jira-key", type=str, default="",
|
|
932
|
+
help="Jira key for --task-add")
|
|
933
|
+
parser.add_argument("--deferred-until", type=str, default="",
|
|
934
|
+
help="Deferred date for --task-update")
|
|
935
|
+
|
|
936
|
+
# --- Ops Tracking Commands ---
|
|
937
|
+
parser.add_argument("--capacity-log", action="store_true",
|
|
938
|
+
help="Add a capacity log entry (requires --metric, --value; optional --date, --notes)")
|
|
939
|
+
parser.add_argument("--stakeholder-add", action="store_true",
|
|
940
|
+
help="Add a stakeholder contact (requires --name; optional --role, --preferences)")
|
|
941
|
+
parser.add_argument("--stakeholder-list", action="store_true",
|
|
942
|
+
help="List all stakeholder contacts (JSON output)")
|
|
943
|
+
parser.add_argument("--incident-add", action="store_true",
|
|
944
|
+
help="Add an incident (requires --title, --severity; optional --timeline)")
|
|
945
|
+
parser.add_argument("--incident-list", action="store_true",
|
|
946
|
+
help="List incidents (JSON output; optional --status filter)")
|
|
947
|
+
parser.add_argument("--pattern-add", action="store_true",
|
|
948
|
+
help="Add a pattern observation (requires --title; optional --type, --description, --confidence)")
|
|
949
|
+
parser.add_argument("--pattern-list", action="store_true",
|
|
950
|
+
help="List pattern observations (JSON output)")
|
|
951
|
+
parser.add_argument("--followup-add", action="store_true",
|
|
952
|
+
help="Add a follow-up item (requires --stakeholder, --topic; optional --due-date)")
|
|
953
|
+
parser.add_argument("--followup-list", action="store_true",
|
|
954
|
+
help="List follow-up items (JSON output; optional --status filter)")
|
|
955
|
+
# Shared optional args for ops commands
|
|
956
|
+
parser.add_argument("--date", type=str, default="",
|
|
957
|
+
help="Date string for --capacity-log")
|
|
958
|
+
parser.add_argument("--metric", type=str, default="",
|
|
959
|
+
help="Metric name for --capacity-log")
|
|
960
|
+
parser.add_argument("--value", type=float, default=None,
|
|
961
|
+
help="Numeric value for --capacity-log")
|
|
962
|
+
parser.add_argument("--notes", type=str, default="",
|
|
963
|
+
help="Notes for --capacity-log or --followup-add")
|
|
964
|
+
parser.add_argument("--name", type=str, default="",
|
|
965
|
+
help="Name for --stakeholder-add")
|
|
966
|
+
parser.add_argument("--role", type=str, default="",
|
|
967
|
+
help="Role for --stakeholder-add")
|
|
968
|
+
parser.add_argument("--preferences", type=str, default="",
|
|
969
|
+
help="Preferences for --stakeholder-add")
|
|
970
|
+
parser.add_argument("--severity", type=str, default="",
|
|
971
|
+
help="Severity for --incident-add or --burnout-add")
|
|
972
|
+
parser.add_argument("--timeline", type=str, default="",
|
|
973
|
+
help="Timeline notes for --incident-add")
|
|
974
|
+
parser.add_argument("--type", type=str, default="",
|
|
975
|
+
dest="obs_type",
|
|
976
|
+
help="Observation type for --pattern-add")
|
|
977
|
+
parser.add_argument("--confidence", type=float, default=None,
|
|
978
|
+
help="Confidence score (0.0-1.0) for --pattern-add")
|
|
979
|
+
parser.add_argument("--stakeholder", type=str, default="",
|
|
980
|
+
help="Stakeholder name for --followup-add or --followup-list")
|
|
981
|
+
parser.add_argument("--topic", type=str, default="",
|
|
982
|
+
help="Topic for --followup-add")
|
|
983
|
+
parser.add_argument("--due-date", type=str, default="",
|
|
984
|
+
help="Due date for --followup-add")
|
|
985
|
+
|
|
986
|
+
args = parser.parse_args()
|
|
987
|
+
|
|
988
|
+
# --- Task Management Handlers ---
|
|
989
|
+
if args.tasks or args.tasks_all:
|
|
990
|
+
rows = tasks_db.list_tasks(status=None)
|
|
991
|
+
if not args.tasks_all:
|
|
992
|
+
rows = [r for r in rows if r.get("status") != "completed"]
|
|
993
|
+
if args.task_json:
|
|
994
|
+
print(json.dumps(rows, indent=2, default=str))
|
|
995
|
+
else:
|
|
996
|
+
print(f"{'ID':>4} {'Status':<14}{'Priority':<10}{'Jira':<16}Title")
|
|
997
|
+
print(f"{'--':>4} {'------':<14}{'--------':<10}{'----':<16}-----")
|
|
998
|
+
for r in rows:
|
|
999
|
+
print(f"{r.get('id', ''):>4} {r.get('status', ''):<14}{r.get('priority', ''):<10}{(r.get('jira_key') or ''):<16}{r.get('title', '')}")
|
|
1000
|
+
return 0
|
|
1001
|
+
|
|
1002
|
+
if args.task is not None:
|
|
1003
|
+
t = tasks_db.get_task(args.task)
|
|
1004
|
+
if not t:
|
|
1005
|
+
print(f"Error: task #{args.task} not found", file=sys.stderr)
|
|
1006
|
+
return 1
|
|
1007
|
+
if args.task_json:
|
|
1008
|
+
print(json.dumps(t, indent=2, default=str))
|
|
1009
|
+
else:
|
|
1010
|
+
for k, v in t.items():
|
|
1011
|
+
print(f"{k:>20}: {v}")
|
|
1012
|
+
return 0
|
|
1013
|
+
|
|
1014
|
+
if args.task_add:
|
|
1015
|
+
if not args.title:
|
|
1016
|
+
print("Error: --title is required for --task-add", file=sys.stderr)
|
|
1017
|
+
return 1
|
|
1018
|
+
task_id = tasks_db.add_task(
|
|
1019
|
+
title=args.title,
|
|
1020
|
+
description=args.description or None,
|
|
1021
|
+
priority=args.priority or "medium",
|
|
1022
|
+
jira_key=args.jira_key or None,
|
|
1023
|
+
)
|
|
1024
|
+
if args.task_json:
|
|
1025
|
+
print(json.dumps({"id": task_id, "title": args.title}))
|
|
1026
|
+
else:
|
|
1027
|
+
print(f"Created task #{task_id}: {args.title}")
|
|
1028
|
+
return 0
|
|
1029
|
+
|
|
1030
|
+
if args.task_update is not None:
|
|
1031
|
+
kwargs = {}
|
|
1032
|
+
if args.status:
|
|
1033
|
+
kwargs["status"] = args.status
|
|
1034
|
+
if args.priority:
|
|
1035
|
+
kwargs["priority"] = args.priority
|
|
1036
|
+
if args.deferred_until:
|
|
1037
|
+
kwargs["deferred_until"] = args.deferred_until
|
|
1038
|
+
ok = tasks_db.update_task(args.task_update, **kwargs)
|
|
1039
|
+
if ok:
|
|
1040
|
+
print(f"Updated task #{args.task_update}")
|
|
1041
|
+
else:
|
|
1042
|
+
print(f"Error: task #{args.task_update} not found or update failed", file=sys.stderr)
|
|
1043
|
+
return 1
|
|
1044
|
+
return 0
|
|
1045
|
+
|
|
1046
|
+
if args.task_search:
|
|
1047
|
+
rows = tasks_db.search_tasks(args.task_search)
|
|
1048
|
+
if args.task_json:
|
|
1049
|
+
print(json.dumps(rows, indent=2, default=str))
|
|
1050
|
+
else:
|
|
1051
|
+
print(f"{'ID':>4} {'Status':<14}{'Priority':<10}{'Jira':<16}Title")
|
|
1052
|
+
print(f"{'--':>4} {'------':<14}{'--------':<10}{'----':<16}-----")
|
|
1053
|
+
for r in rows:
|
|
1054
|
+
print(f"{r.get('id', ''):>4} {r.get('status', ''):<14}{r.get('priority', ''):<10}{(r.get('jira_key') or ''):<16}{r.get('title', '')}")
|
|
1055
|
+
return 0
|
|
1056
|
+
|
|
1057
|
+
if args.task_export is not None:
|
|
1058
|
+
t = tasks_db.get_task(args.task_export)
|
|
1059
|
+
if not t:
|
|
1060
|
+
print(f"Error: task #{args.task_export} not found", file=sys.stderr)
|
|
1061
|
+
return 1
|
|
1062
|
+
print(f"## Task #{t['id']}: {t.get('title', '')}")
|
|
1063
|
+
print(f"**Jira**: {t.get('jira_key') or 'None'}")
|
|
1064
|
+
print(f"**Status**: {t.get('status', '')}")
|
|
1065
|
+
print(f"**Priority**: {t.get('priority', '')}")
|
|
1066
|
+
print(f"**Description**: {t.get('description') or ''}")
|
|
1067
|
+
return 0
|
|
1068
|
+
|
|
1069
|
+
if args.register_learning_category:
|
|
1070
|
+
result = register_learning_category(
|
|
1071
|
+
name=args.register_learning_category,
|
|
1072
|
+
description=args.description,
|
|
1073
|
+
path=args.path,
|
|
1074
|
+
heading=args.heading,
|
|
1075
|
+
)
|
|
1076
|
+
print(json.dumps(result))
|
|
1077
|
+
return 0
|
|
1078
|
+
|
|
1079
|
+
if args.capture_post_tool:
|
|
1080
|
+
payload_text = sys.stdin.read().strip()
|
|
1081
|
+
if not payload_text:
|
|
1082
|
+
print(json.dumps({"recorded": False, "reason": "empty_payload"}))
|
|
1083
|
+
return 0
|
|
1084
|
+
try:
|
|
1085
|
+
payload = json.loads(payload_text)
|
|
1086
|
+
except json.JSONDecodeError:
|
|
1087
|
+
print(json.dumps({"recorded": False, "reason": "invalid_json"}))
|
|
1088
|
+
return 0
|
|
1089
|
+
|
|
1090
|
+
print(json.dumps(capture_post_tool_learning(payload)))
|
|
1091
|
+
return 0
|
|
1092
|
+
|
|
1093
|
+
# --- Playbook Engine Handlers ---
|
|
1094
|
+
import os
|
|
1095
|
+
PLAYBOOKS_DIR = os.path.expanduser("~/.claude/eng-buddy/playbooks")
|
|
1096
|
+
TRACES_DIR = os.path.expanduser("~/.claude/eng-buddy/traces")
|
|
1097
|
+
REGISTRY_DIR = os.path.join(PLAYBOOKS_DIR, "tool-registry")
|
|
1098
|
+
|
|
1099
|
+
# Add playbook_engine to sys.path
|
|
1100
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "playbook_engine"))
|
|
1101
|
+
|
|
1102
|
+
if args.playbook_trace_event:
|
|
1103
|
+
from playbook_engine.tracer import WorkflowTracer, TraceEvent
|
|
1104
|
+
payload = json.load(sys.stdin)
|
|
1105
|
+
tracer = WorkflowTracer(traces_dir=TRACES_DIR)
|
|
1106
|
+
trace_id = payload["trace_id"]
|
|
1107
|
+
tracer.load_trace(trace_id) or tracer.start_trace(trace_id)
|
|
1108
|
+
event_data = payload["event"]
|
|
1109
|
+
tracer.add_event(TraceEvent.from_dict(event_data))
|
|
1110
|
+
tracer.flush(trace_id)
|
|
1111
|
+
print(json.dumps({"status": "ok", "trace_id": trace_id}))
|
|
1112
|
+
return 0
|
|
1113
|
+
|
|
1114
|
+
if args.playbook_extract:
|
|
1115
|
+
from playbook_engine.tracer import WorkflowTracer
|
|
1116
|
+
from playbook_engine.registry import ToolRegistry
|
|
1117
|
+
from playbook_engine.extractor import PlaybookExtractor
|
|
1118
|
+
from playbook_engine.manager import PlaybookManager
|
|
1119
|
+
tracer = WorkflowTracer(traces_dir=TRACES_DIR)
|
|
1120
|
+
trace = tracer.load_trace(args.playbook_extract)
|
|
1121
|
+
if not trace:
|
|
1122
|
+
print(json.dumps({"error": f"Trace {args.playbook_extract} not found"}))
|
|
1123
|
+
return 1
|
|
1124
|
+
registry = ToolRegistry(REGISTRY_DIR)
|
|
1125
|
+
extractor = PlaybookExtractor(registry=registry)
|
|
1126
|
+
pb = extractor.extract_from_trace(trace, name=args.playbook_extract_name)
|
|
1127
|
+
manager = PlaybookManager(PLAYBOOKS_DIR)
|
|
1128
|
+
path = manager.save_draft(pb)
|
|
1129
|
+
print(json.dumps({"status": "ok", "playbook_id": pb.id, "path": path, "steps": len(pb.steps)}))
|
|
1130
|
+
return 0
|
|
1131
|
+
|
|
1132
|
+
if args.playbook_match:
|
|
1133
|
+
from playbook_engine.manager import PlaybookManager
|
|
1134
|
+
manager = PlaybookManager(PLAYBOOKS_DIR)
|
|
1135
|
+
matches = manager.match_ticket(
|
|
1136
|
+
ticket_type=args.playbook_match_type,
|
|
1137
|
+
text=args.playbook_match,
|
|
1138
|
+
source=args.playbook_match_source,
|
|
1139
|
+
)
|
|
1140
|
+
print(json.dumps({"matches": [{"id": m.id, "name": m.name, "confidence": m.confidence, "executions": m.executions} for m in matches]}))
|
|
1141
|
+
return 0
|
|
1142
|
+
|
|
1143
|
+
if args.playbook_list:
|
|
1144
|
+
from playbook_engine.manager import PlaybookManager
|
|
1145
|
+
manager = PlaybookManager(PLAYBOOKS_DIR)
|
|
1146
|
+
pbs = manager.list_playbooks()
|
|
1147
|
+
print(json.dumps({"playbooks": [{"id": p.id, "name": p.name, "confidence": p.confidence, "version": p.version, "executions": p.executions} for p in pbs]}))
|
|
1148
|
+
return 0
|
|
1149
|
+
|
|
1150
|
+
if args.playbook_list_drafts:
|
|
1151
|
+
from playbook_engine.manager import PlaybookManager
|
|
1152
|
+
manager = PlaybookManager(PLAYBOOKS_DIR)
|
|
1153
|
+
drafts = manager.list_drafts()
|
|
1154
|
+
print(json.dumps({"drafts": [{"id": d.id, "name": d.name, "confidence": d.confidence, "steps": len(d.steps)} for d in drafts]}))
|
|
1155
|
+
return 0
|
|
1156
|
+
|
|
1157
|
+
if args.playbook_promote:
|
|
1158
|
+
from playbook_engine.manager import PlaybookManager
|
|
1159
|
+
manager = PlaybookManager(PLAYBOOKS_DIR)
|
|
1160
|
+
pb = manager.promote_draft(args.playbook_promote)
|
|
1161
|
+
if pb:
|
|
1162
|
+
print(json.dumps({"status": "ok", "playbook_id": pb.id}))
|
|
1163
|
+
return 0
|
|
1164
|
+
print(json.dumps({"error": f"Draft {args.playbook_promote} not found"}))
|
|
1165
|
+
return 1
|
|
1166
|
+
|
|
1167
|
+
# --- Ops Tracking Handlers ---
|
|
1168
|
+
_ensure_ops_schema()
|
|
1169
|
+
|
|
1170
|
+
if args.capacity_log:
|
|
1171
|
+
if not args.metric:
|
|
1172
|
+
print("Error: --metric is required for --capacity-log", file=sys.stderr)
|
|
1173
|
+
return 1
|
|
1174
|
+
if args.value is None:
|
|
1175
|
+
print("Error: --value is required for --capacity-log", file=sys.stderr)
|
|
1176
|
+
return 1
|
|
1177
|
+
conn = sqlite3.connect(DB_PATH)
|
|
1178
|
+
try:
|
|
1179
|
+
cur = conn.execute(
|
|
1180
|
+
"INSERT INTO capacity_logs (date, metric, value, notes) VALUES (?, ?, ?, ?)",
|
|
1181
|
+
[args.date or date.today().isoformat(), args.metric, args.value, args.notes or ""],
|
|
1182
|
+
)
|
|
1183
|
+
conn.commit()
|
|
1184
|
+
print(json.dumps({"id": cur.lastrowid, "metric": args.metric, "value": args.value}))
|
|
1185
|
+
finally:
|
|
1186
|
+
conn.close()
|
|
1187
|
+
return 0
|
|
1188
|
+
|
|
1189
|
+
if args.stakeholder_add:
|
|
1190
|
+
if not args.name:
|
|
1191
|
+
print("Error: --name is required for --stakeholder-add", file=sys.stderr)
|
|
1192
|
+
return 1
|
|
1193
|
+
conn = sqlite3.connect(DB_PATH)
|
|
1194
|
+
try:
|
|
1195
|
+
cur = conn.execute(
|
|
1196
|
+
"INSERT INTO stakeholder_contacts (name, role, preferences) VALUES (?, ?, ?)",
|
|
1197
|
+
[args.name, args.role or "", args.preferences or ""],
|
|
1198
|
+
)
|
|
1199
|
+
conn.commit()
|
|
1200
|
+
print(json.dumps({"id": cur.lastrowid, "name": args.name}))
|
|
1201
|
+
finally:
|
|
1202
|
+
conn.close()
|
|
1203
|
+
return 0
|
|
1204
|
+
|
|
1205
|
+
if args.stakeholder_list:
|
|
1206
|
+
conn = sqlite3.connect(DB_PATH)
|
|
1207
|
+
conn.row_factory = sqlite3.Row
|
|
1208
|
+
try:
|
|
1209
|
+
rows = conn.execute(
|
|
1210
|
+
"SELECT * FROM stakeholder_contacts ORDER BY created_at DESC"
|
|
1211
|
+
).fetchall()
|
|
1212
|
+
print(json.dumps([dict(r) for r in rows], indent=2, default=str))
|
|
1213
|
+
finally:
|
|
1214
|
+
conn.close()
|
|
1215
|
+
return 0
|
|
1216
|
+
|
|
1217
|
+
if args.incident_add:
|
|
1218
|
+
if not args.title:
|
|
1219
|
+
print("Error: --title is required for --incident-add", file=sys.stderr)
|
|
1220
|
+
return 1
|
|
1221
|
+
if not args.severity:
|
|
1222
|
+
print("Error: --severity is required for --incident-add", file=sys.stderr)
|
|
1223
|
+
return 1
|
|
1224
|
+
conn = sqlite3.connect(DB_PATH)
|
|
1225
|
+
try:
|
|
1226
|
+
cur = conn.execute(
|
|
1227
|
+
"INSERT INTO incidents (title, severity, timeline) VALUES (?, ?, ?)",
|
|
1228
|
+
[args.title, args.severity, args.timeline or ""],
|
|
1229
|
+
)
|
|
1230
|
+
conn.commit()
|
|
1231
|
+
print(json.dumps({"id": cur.lastrowid, "title": args.title, "severity": args.severity}))
|
|
1232
|
+
finally:
|
|
1233
|
+
conn.close()
|
|
1234
|
+
return 0
|
|
1235
|
+
|
|
1236
|
+
if args.incident_list:
|
|
1237
|
+
conn = sqlite3.connect(DB_PATH)
|
|
1238
|
+
conn.row_factory = sqlite3.Row
|
|
1239
|
+
try:
|
|
1240
|
+
if args.status:
|
|
1241
|
+
rows = conn.execute(
|
|
1242
|
+
"SELECT * FROM incidents WHERE status = ? ORDER BY created_at DESC",
|
|
1243
|
+
[args.status],
|
|
1244
|
+
).fetchall()
|
|
1245
|
+
else:
|
|
1246
|
+
rows = conn.execute(
|
|
1247
|
+
"SELECT * FROM incidents ORDER BY created_at DESC"
|
|
1248
|
+
).fetchall()
|
|
1249
|
+
print(json.dumps([dict(r) for r in rows], indent=2, default=str))
|
|
1250
|
+
finally:
|
|
1251
|
+
conn.close()
|
|
1252
|
+
return 0
|
|
1253
|
+
|
|
1254
|
+
if args.pattern_add:
|
|
1255
|
+
if not args.title:
|
|
1256
|
+
print("Error: --title is required for --pattern-add", file=sys.stderr)
|
|
1257
|
+
return 1
|
|
1258
|
+
now = datetime.now().isoformat()
|
|
1259
|
+
conn = sqlite3.connect(DB_PATH)
|
|
1260
|
+
try:
|
|
1261
|
+
cur = conn.execute(
|
|
1262
|
+
"""INSERT INTO pattern_observations
|
|
1263
|
+
(type, title, description, confidence, first_seen, last_seen)
|
|
1264
|
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
1265
|
+
[
|
|
1266
|
+
args.obs_type or "",
|
|
1267
|
+
args.title,
|
|
1268
|
+
args.description or "",
|
|
1269
|
+
args.confidence if args.confidence is not None else 0.5,
|
|
1270
|
+
now,
|
|
1271
|
+
now,
|
|
1272
|
+
],
|
|
1273
|
+
)
|
|
1274
|
+
new_id = cur.lastrowid
|
|
1275
|
+
# Sync FTS5 index
|
|
1276
|
+
conn.execute(
|
|
1277
|
+
"INSERT INTO pattern_observations_fts(rowid, title, description, evidence) VALUES (?, ?, ?, ?)",
|
|
1278
|
+
[new_id, args.title, args.description or "", ""],
|
|
1279
|
+
)
|
|
1280
|
+
conn.commit()
|
|
1281
|
+
print(json.dumps({"id": new_id, "title": args.title}))
|
|
1282
|
+
finally:
|
|
1283
|
+
conn.close()
|
|
1284
|
+
return 0
|
|
1285
|
+
|
|
1286
|
+
if args.pattern_list:
|
|
1287
|
+
conn = sqlite3.connect(DB_PATH)
|
|
1288
|
+
conn.row_factory = sqlite3.Row
|
|
1289
|
+
try:
|
|
1290
|
+
rows = conn.execute(
|
|
1291
|
+
"SELECT * FROM pattern_observations ORDER BY created_at DESC"
|
|
1292
|
+
).fetchall()
|
|
1293
|
+
print(json.dumps([dict(r) for r in rows], indent=2, default=str))
|
|
1294
|
+
finally:
|
|
1295
|
+
conn.close()
|
|
1296
|
+
return 0
|
|
1297
|
+
|
|
1298
|
+
if args.followup_add:
|
|
1299
|
+
if not args.stakeholder:
|
|
1300
|
+
print("Error: --stakeholder is required for --followup-add", file=sys.stderr)
|
|
1301
|
+
return 1
|
|
1302
|
+
if not args.topic:
|
|
1303
|
+
print("Error: --topic is required for --followup-add", file=sys.stderr)
|
|
1304
|
+
return 1
|
|
1305
|
+
conn = sqlite3.connect(DB_PATH)
|
|
1306
|
+
try:
|
|
1307
|
+
cur = conn.execute(
|
|
1308
|
+
"INSERT INTO follow_ups (stakeholder, topic, due_date, notes) VALUES (?, ?, ?, ?)",
|
|
1309
|
+
[args.stakeholder, args.topic, args.due_date or "", args.notes or ""],
|
|
1310
|
+
)
|
|
1311
|
+
conn.commit()
|
|
1312
|
+
print(json.dumps({"id": cur.lastrowid, "stakeholder": args.stakeholder, "topic": args.topic}))
|
|
1313
|
+
finally:
|
|
1314
|
+
conn.close()
|
|
1315
|
+
return 0
|
|
1316
|
+
|
|
1317
|
+
if args.followup_list:
|
|
1318
|
+
conn = sqlite3.connect(DB_PATH)
|
|
1319
|
+
conn.row_factory = sqlite3.Row
|
|
1320
|
+
try:
|
|
1321
|
+
if args.status:
|
|
1322
|
+
rows = conn.execute(
|
|
1323
|
+
"SELECT * FROM follow_ups WHERE status = ? ORDER BY due_date, created_at DESC",
|
|
1324
|
+
[args.status],
|
|
1325
|
+
).fetchall()
|
|
1326
|
+
else:
|
|
1327
|
+
rows = conn.execute(
|
|
1328
|
+
"SELECT * FROM follow_ups ORDER BY due_date, created_at DESC"
|
|
1329
|
+
).fetchall()
|
|
1330
|
+
print(json.dumps([dict(r) for r in rows], indent=2, default=str))
|
|
1331
|
+
finally:
|
|
1332
|
+
conn.close()
|
|
1333
|
+
return 0
|
|
1334
|
+
|
|
1335
|
+
parser.print_help()
|
|
1336
|
+
return 1
|
|
1337
|
+
|
|
1338
|
+
|
|
1339
|
+
if __name__ == "__main__":
|
|
1340
|
+
raise SystemExit(_cli())
|