feed-the-machine 1.6.0 → 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/tasks_db.py
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
"""Shared task management module backed by SQLite.
|
|
2
|
+
|
|
3
|
+
All consumers should ``from tasks_db import …`` to interact with the
|
|
4
|
+
eng-buddy task database. The schema is created lazily on the first
|
|
5
|
+
call to :func:`get_conn`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sqlite3
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
DB_PATH: Path = Path.home() / ".claude" / "eng-buddy" / "tasks.db"
|
|
16
|
+
|
|
17
|
+
_schema_ensured: bool = False
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Priority helpers
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
_PRIORITY_ORDER_EXPR = """
|
|
24
|
+
CASE priority
|
|
25
|
+
WHEN 'high' THEN 1
|
|
26
|
+
WHEN 'medium' THEN 2
|
|
27
|
+
WHEN 'low' THEN 3
|
|
28
|
+
ELSE 4
|
|
29
|
+
END
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
_JIRA_PRIORITY_MAP: Dict[str, str] = {
|
|
33
|
+
"highest": "high",
|
|
34
|
+
"high": "high",
|
|
35
|
+
"medium": "medium",
|
|
36
|
+
"low": "low",
|
|
37
|
+
"lowest": "low",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Connection & schema
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_conn() -> sqlite3.Connection:
|
|
46
|
+
"""Return a connection with Row factory, WAL mode, and foreign keys ON.
|
|
47
|
+
|
|
48
|
+
On the first call the schema is created automatically via
|
|
49
|
+
:func:`ensure_schema`.
|
|
50
|
+
"""
|
|
51
|
+
global _schema_ensured
|
|
52
|
+
|
|
53
|
+
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
|
|
55
|
+
conn = sqlite3.connect(str(DB_PATH))
|
|
56
|
+
conn.row_factory = sqlite3.Row
|
|
57
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
58
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
59
|
+
|
|
60
|
+
if not _schema_ensured:
|
|
61
|
+
ensure_schema(conn)
|
|
62
|
+
_schema_ensured = True
|
|
63
|
+
|
|
64
|
+
return conn
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def ensure_schema(conn: Optional[sqlite3.Connection] = None) -> None:
|
|
68
|
+
"""Idempotently create all tables, indexes, triggers and FTS index."""
|
|
69
|
+
own_conn = conn is None
|
|
70
|
+
if own_conn:
|
|
71
|
+
conn = get_conn()
|
|
72
|
+
|
|
73
|
+
cur = conn.cursor()
|
|
74
|
+
|
|
75
|
+
# -- core tables ---------------------------------------------------------
|
|
76
|
+
cur.executescript(
|
|
77
|
+
"""
|
|
78
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
79
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
80
|
+
jira_key TEXT UNIQUE,
|
|
81
|
+
freshservice_url TEXT,
|
|
82
|
+
title TEXT NOT NULL,
|
|
83
|
+
description TEXT,
|
|
84
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
85
|
+
priority TEXT NOT NULL DEFAULT 'medium',
|
|
86
|
+
jira_status TEXT,
|
|
87
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
88
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
89
|
+
completed_at TEXT,
|
|
90
|
+
deferred_until TEXT,
|
|
91
|
+
metadata TEXT DEFAULT '{}'
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status
|
|
95
|
+
ON tasks(status);
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_jira_key
|
|
97
|
+
ON tasks(jira_key) WHERE jira_key IS NOT NULL;
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_priority
|
|
99
|
+
ON tasks(priority, status);
|
|
100
|
+
|
|
101
|
+
CREATE TABLE IF NOT EXISTS task_events (
|
|
102
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
103
|
+
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
104
|
+
event_type TEXT NOT NULL,
|
|
105
|
+
detail TEXT,
|
|
106
|
+
actor TEXT NOT NULL DEFAULT 'system',
|
|
107
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_task_events_task
|
|
111
|
+
ON task_events(task_id, created_at);
|
|
112
|
+
"""
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# -- FTS5 virtual table --------------------------------------------------
|
|
116
|
+
# executescript cannot handle virtual-table DDL reliably; use execute.
|
|
117
|
+
cur.execute(
|
|
118
|
+
"""
|
|
119
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS tasks_fts USING fts5(
|
|
120
|
+
title, description, jira_key, metadata,
|
|
121
|
+
content='tasks', content_rowid='id'
|
|
122
|
+
)
|
|
123
|
+
"""
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# -- FTS sync triggers ---------------------------------------------------
|
|
127
|
+
cur.executescript(
|
|
128
|
+
"""
|
|
129
|
+
CREATE TRIGGER IF NOT EXISTS tasks_ai AFTER INSERT ON tasks BEGIN
|
|
130
|
+
INSERT INTO tasks_fts(rowid, title, description, jira_key, metadata)
|
|
131
|
+
VALUES (new.id, new.title, new.description, new.jira_key, new.metadata);
|
|
132
|
+
END;
|
|
133
|
+
|
|
134
|
+
CREATE TRIGGER IF NOT EXISTS tasks_ad AFTER DELETE ON tasks BEGIN
|
|
135
|
+
INSERT INTO tasks_fts(tasks_fts, rowid, title, description, jira_key, metadata)
|
|
136
|
+
VALUES ('delete', old.id, old.title, old.description, old.jira_key, old.metadata);
|
|
137
|
+
END;
|
|
138
|
+
|
|
139
|
+
CREATE TRIGGER IF NOT EXISTS tasks_au AFTER UPDATE ON tasks BEGIN
|
|
140
|
+
INSERT INTO tasks_fts(tasks_fts, rowid, title, description, jira_key, metadata)
|
|
141
|
+
VALUES ('delete', old.id, old.title, old.description, old.jira_key, old.metadata);
|
|
142
|
+
INSERT INTO tasks_fts(rowid, title, description, jira_key, metadata)
|
|
143
|
+
VALUES (new.id, new.title, new.description, new.jira_key, new.metadata);
|
|
144
|
+
END;
|
|
145
|
+
"""
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
conn.commit()
|
|
149
|
+
|
|
150
|
+
if own_conn:
|
|
151
|
+
conn.close()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# Row helpers
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
def task_to_dict(row: sqlite3.Row) -> Dict[str, Any]:
|
|
159
|
+
"""Convert a ``sqlite3.Row`` to a plain dict, parsing metadata JSON."""
|
|
160
|
+
d = dict(row)
|
|
161
|
+
raw = d.get("metadata")
|
|
162
|
+
if isinstance(raw, str):
|
|
163
|
+
try:
|
|
164
|
+
d["metadata"] = json.loads(raw)
|
|
165
|
+
except (json.JSONDecodeError, TypeError):
|
|
166
|
+
d["metadata"] = {}
|
|
167
|
+
return d
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
# CRUD
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
def list_tasks(
|
|
175
|
+
status: Optional[str] = None,
|
|
176
|
+
limit: int = 50,
|
|
177
|
+
) -> List[Dict[str, Any]]:
|
|
178
|
+
"""List tasks, optionally filtered by *status*.
|
|
179
|
+
|
|
180
|
+
Ordering: priority DESC (high first), then created_at ASC.
|
|
181
|
+
"""
|
|
182
|
+
conn = get_conn()
|
|
183
|
+
try:
|
|
184
|
+
sql = f"SELECT * FROM tasks"
|
|
185
|
+
params: list[Any] = []
|
|
186
|
+
if status is not None:
|
|
187
|
+
sql += " WHERE status = ?"
|
|
188
|
+
params.append(status)
|
|
189
|
+
sql += f" ORDER BY {_PRIORITY_ORDER_EXPR}, created_at ASC LIMIT ?"
|
|
190
|
+
params.append(limit)
|
|
191
|
+
rows = conn.execute(sql, params).fetchall()
|
|
192
|
+
return [task_to_dict(r) for r in rows]
|
|
193
|
+
finally:
|
|
194
|
+
conn.close()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def get_task(task_id: int) -> Optional[Dict[str, Any]]:
|
|
198
|
+
"""Get a single task by its local ID."""
|
|
199
|
+
conn = get_conn()
|
|
200
|
+
try:
|
|
201
|
+
row = conn.execute("SELECT * FROM tasks WHERE id = ?", (task_id,)).fetchone()
|
|
202
|
+
return task_to_dict(row) if row else None
|
|
203
|
+
finally:
|
|
204
|
+
conn.close()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def get_task_by_jira_key(jira_key: str) -> Optional[Dict[str, Any]]:
|
|
208
|
+
"""Lookup a task by its Jira key."""
|
|
209
|
+
conn = get_conn()
|
|
210
|
+
try:
|
|
211
|
+
row = conn.execute(
|
|
212
|
+
"SELECT * FROM tasks WHERE jira_key = ?", (jira_key,)
|
|
213
|
+
).fetchone()
|
|
214
|
+
return task_to_dict(row) if row else None
|
|
215
|
+
finally:
|
|
216
|
+
conn.close()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def add_task(
|
|
220
|
+
title: str,
|
|
221
|
+
description: Optional[str] = None,
|
|
222
|
+
priority: str = "medium",
|
|
223
|
+
jira_key: Optional[str] = None,
|
|
224
|
+
freshservice_url: Optional[str] = None,
|
|
225
|
+
metadata: Optional[dict] = None,
|
|
226
|
+
) -> int:
|
|
227
|
+
"""Insert a new task and record a 'created' event. Returns the new ID."""
|
|
228
|
+
meta_str = json.dumps(metadata or {})
|
|
229
|
+
conn = get_conn()
|
|
230
|
+
try:
|
|
231
|
+
cur = conn.execute(
|
|
232
|
+
"""
|
|
233
|
+
INSERT INTO tasks (title, description, priority, jira_key,
|
|
234
|
+
freshservice_url, metadata)
|
|
235
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
236
|
+
""",
|
|
237
|
+
(title, description, priority, jira_key, freshservice_url, meta_str),
|
|
238
|
+
)
|
|
239
|
+
task_id = cur.lastrowid
|
|
240
|
+
conn.execute(
|
|
241
|
+
"""
|
|
242
|
+
INSERT INTO task_events (task_id, event_type, detail, actor)
|
|
243
|
+
VALUES (?, 'created', ?, 'system')
|
|
244
|
+
""",
|
|
245
|
+
(task_id, f"Task created: {title}"),
|
|
246
|
+
)
|
|
247
|
+
conn.commit()
|
|
248
|
+
return task_id
|
|
249
|
+
finally:
|
|
250
|
+
conn.close()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
_VALID_TASK_FIELDS = {
|
|
254
|
+
"title",
|
|
255
|
+
"description",
|
|
256
|
+
"status",
|
|
257
|
+
"priority",
|
|
258
|
+
"jira_key",
|
|
259
|
+
"jira_status",
|
|
260
|
+
"freshservice_url",
|
|
261
|
+
"completed_at",
|
|
262
|
+
"deferred_until",
|
|
263
|
+
"metadata",
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def update_task(task_id: int, **fields: Any) -> bool:
|
|
268
|
+
"""Update arbitrary valid fields on a task.
|
|
269
|
+
|
|
270
|
+
Automatically sets ``updated_at``, records an 'updated' event, and
|
|
271
|
+
sets ``completed_at`` when *status* changes to ``'completed'``.
|
|
272
|
+
|
|
273
|
+
Returns ``True`` if a row was updated, ``False`` otherwise.
|
|
274
|
+
"""
|
|
275
|
+
invalid = set(fields) - _VALID_TASK_FIELDS
|
|
276
|
+
if invalid:
|
|
277
|
+
raise ValueError(f"Invalid task fields: {invalid}")
|
|
278
|
+
if not fields:
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
# Serialise metadata if present
|
|
282
|
+
if "metadata" in fields and not isinstance(fields["metadata"], str):
|
|
283
|
+
fields["metadata"] = json.dumps(fields["metadata"])
|
|
284
|
+
|
|
285
|
+
conn = get_conn()
|
|
286
|
+
try:
|
|
287
|
+
# Handle completed_at automatically
|
|
288
|
+
if fields.get("status") == "completed" and "completed_at" not in fields:
|
|
289
|
+
fields["completed_at"] = "datetime('now')"
|
|
290
|
+
|
|
291
|
+
set_clauses: list[str] = []
|
|
292
|
+
params: list[Any] = []
|
|
293
|
+
for key, val in fields.items():
|
|
294
|
+
if key == "completed_at" and val == "datetime('now')":
|
|
295
|
+
set_clauses.append("completed_at = datetime('now')")
|
|
296
|
+
else:
|
|
297
|
+
set_clauses.append(f"{key} = ?")
|
|
298
|
+
params.append(val)
|
|
299
|
+
|
|
300
|
+
set_clauses.append("updated_at = datetime('now')")
|
|
301
|
+
params.append(task_id)
|
|
302
|
+
|
|
303
|
+
sql = f"UPDATE tasks SET {', '.join(set_clauses)} WHERE id = ?"
|
|
304
|
+
cur = conn.execute(sql, params)
|
|
305
|
+
|
|
306
|
+
if cur.rowcount == 0:
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
changed = ", ".join(f"{k}={v!r}" for k, v in fields.items())
|
|
310
|
+
conn.execute(
|
|
311
|
+
"""
|
|
312
|
+
INSERT INTO task_events (task_id, event_type, detail, actor)
|
|
313
|
+
VALUES (?, 'updated', ?, 'system')
|
|
314
|
+
""",
|
|
315
|
+
(task_id, f"Fields changed: {changed}"),
|
|
316
|
+
)
|
|
317
|
+
conn.commit()
|
|
318
|
+
return True
|
|
319
|
+
finally:
|
|
320
|
+
conn.close()
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def upsert_jira_task(
|
|
324
|
+
jira_key: str,
|
|
325
|
+
title: str,
|
|
326
|
+
jira_status: str,
|
|
327
|
+
priority: str,
|
|
328
|
+
metadata: Optional[dict] = None,
|
|
329
|
+
) -> int:
|
|
330
|
+
"""Insert or update a task keyed by *jira_key*.
|
|
331
|
+
|
|
332
|
+
On conflict the Jira status and priority are updated but user-edited
|
|
333
|
+
title/description are preserved. If the Jira status maps to ``Done``
|
|
334
|
+
the local status is set to ``'completed'``.
|
|
335
|
+
|
|
336
|
+
Returns the task ID.
|
|
337
|
+
"""
|
|
338
|
+
mapped_priority = _JIRA_PRIORITY_MAP.get(priority.lower(), "medium")
|
|
339
|
+
meta_str = json.dumps(metadata or {})
|
|
340
|
+
is_done = jira_status.lower() == "done"
|
|
341
|
+
|
|
342
|
+
conn = get_conn()
|
|
343
|
+
try:
|
|
344
|
+
existing = conn.execute(
|
|
345
|
+
"SELECT * FROM tasks WHERE jira_key = ?", (jira_key,)
|
|
346
|
+
).fetchone()
|
|
347
|
+
|
|
348
|
+
if existing is None:
|
|
349
|
+
# Insert new
|
|
350
|
+
status = "completed" if is_done else "pending"
|
|
351
|
+
cur = conn.execute(
|
|
352
|
+
"""
|
|
353
|
+
INSERT INTO tasks (title, jira_key, jira_status, priority,
|
|
354
|
+
status, metadata)
|
|
355
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
356
|
+
""",
|
|
357
|
+
(title, jira_key, jira_status, mapped_priority, status, meta_str),
|
|
358
|
+
)
|
|
359
|
+
task_id = cur.lastrowid
|
|
360
|
+
conn.execute(
|
|
361
|
+
"""
|
|
362
|
+
INSERT INTO task_events (task_id, event_type, detail, actor)
|
|
363
|
+
VALUES (?, 'created', ?, 'jira-sync')
|
|
364
|
+
""",
|
|
365
|
+
(task_id, f"Synced from Jira: {jira_key}"),
|
|
366
|
+
)
|
|
367
|
+
if is_done:
|
|
368
|
+
conn.execute(
|
|
369
|
+
"UPDATE tasks SET completed_at = datetime('now') WHERE id = ?",
|
|
370
|
+
(task_id,),
|
|
371
|
+
)
|
|
372
|
+
conn.commit()
|
|
373
|
+
return task_id
|
|
374
|
+
else:
|
|
375
|
+
# Update existing — preserve user-edited title/description
|
|
376
|
+
task_id = existing["id"]
|
|
377
|
+
update_parts = [
|
|
378
|
+
"jira_status = ?",
|
|
379
|
+
"priority = ?",
|
|
380
|
+
"updated_at = datetime('now')",
|
|
381
|
+
]
|
|
382
|
+
params: list[Any] = [jira_status, mapped_priority]
|
|
383
|
+
|
|
384
|
+
if is_done and existing["status"] != "completed":
|
|
385
|
+
update_parts.append("status = 'completed'")
|
|
386
|
+
update_parts.append("completed_at = datetime('now')")
|
|
387
|
+
|
|
388
|
+
params.append(task_id)
|
|
389
|
+
conn.execute(
|
|
390
|
+
f"UPDATE tasks SET {', '.join(update_parts)} WHERE id = ?",
|
|
391
|
+
params,
|
|
392
|
+
)
|
|
393
|
+
conn.execute(
|
|
394
|
+
"""
|
|
395
|
+
INSERT INTO task_events (task_id, event_type, detail, actor)
|
|
396
|
+
VALUES (?, 'updated', ?, 'jira-sync')
|
|
397
|
+
""",
|
|
398
|
+
(
|
|
399
|
+
task_id,
|
|
400
|
+
f"Jira sync: status={jira_status}, priority={mapped_priority}",
|
|
401
|
+
),
|
|
402
|
+
)
|
|
403
|
+
conn.commit()
|
|
404
|
+
return task_id
|
|
405
|
+
finally:
|
|
406
|
+
conn.close()
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# ---------------------------------------------------------------------------
|
|
410
|
+
# Search
|
|
411
|
+
# ---------------------------------------------------------------------------
|
|
412
|
+
|
|
413
|
+
def search_tasks(query: str, limit: int = 20) -> List[Dict[str, Any]]:
|
|
414
|
+
"""Full-text search across tasks using FTS5."""
|
|
415
|
+
conn = get_conn()
|
|
416
|
+
try:
|
|
417
|
+
rows = conn.execute(
|
|
418
|
+
"""
|
|
419
|
+
SELECT t.*
|
|
420
|
+
FROM tasks_fts fts
|
|
421
|
+
JOIN tasks t ON t.id = fts.rowid
|
|
422
|
+
WHERE tasks_fts MATCH ?
|
|
423
|
+
ORDER BY rank
|
|
424
|
+
LIMIT ?
|
|
425
|
+
""",
|
|
426
|
+
(query, limit),
|
|
427
|
+
).fetchall()
|
|
428
|
+
return [task_to_dict(r) for r in rows]
|
|
429
|
+
finally:
|
|
430
|
+
conn.close()
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# ---------------------------------------------------------------------------
|
|
434
|
+
# Events
|
|
435
|
+
# ---------------------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
def record_event(
|
|
438
|
+
task_id: int,
|
|
439
|
+
event_type: str,
|
|
440
|
+
detail: Optional[str] = None,
|
|
441
|
+
actor: str = "system",
|
|
442
|
+
) -> int:
|
|
443
|
+
"""Insert a row into ``task_events`` and return its ID."""
|
|
444
|
+
conn = get_conn()
|
|
445
|
+
try:
|
|
446
|
+
cur = conn.execute(
|
|
447
|
+
"""
|
|
448
|
+
INSERT INTO task_events (task_id, event_type, detail, actor)
|
|
449
|
+
VALUES (?, ?, ?, ?)
|
|
450
|
+
""",
|
|
451
|
+
(task_id, event_type, detail, actor),
|
|
452
|
+
)
|
|
453
|
+
conn.commit()
|
|
454
|
+
return cur.lastrowid
|
|
455
|
+
finally:
|
|
456
|
+
conn.close()
|