feed-the-machine 1.0.0 → 1.2.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.
Files changed (136) hide show
  1. package/bin/generate-manifest.mjs +253 -0
  2. package/bin/install.mjs +134 -4
  3. package/docs/HOOKS.md +243 -0
  4. package/docs/INBOX.md +233 -0
  5. package/ftm/SKILL.md +34 -0
  6. package/ftm-audit/SKILL.md +69 -0
  7. package/ftm-brainstorm/SKILL.md +51 -0
  8. package/ftm-browse/SKILL.md +39 -0
  9. package/ftm-capture/SKILL.md +370 -0
  10. package/ftm-capture.yml +4 -0
  11. package/ftm-codex-gate/SKILL.md +59 -0
  12. package/ftm-config/SKILL.md +35 -0
  13. package/ftm-council/SKILL.md +56 -0
  14. package/ftm-dashboard/SKILL.md +163 -0
  15. package/ftm-debug/SKILL.md +84 -0
  16. package/ftm-diagram/SKILL.md +44 -0
  17. package/ftm-executor/SKILL.md +97 -0
  18. package/ftm-git/SKILL.md +60 -0
  19. package/ftm-inbox/backend/__init__.py +0 -0
  20. package/ftm-inbox/backend/__pycache__/main.cpython-314.pyc +0 -0
  21. package/ftm-inbox/backend/adapters/__init__.py +0 -0
  22. package/ftm-inbox/backend/adapters/_retry.py +64 -0
  23. package/ftm-inbox/backend/adapters/base.py +230 -0
  24. package/ftm-inbox/backend/adapters/freshservice.py +104 -0
  25. package/ftm-inbox/backend/adapters/gmail.py +125 -0
  26. package/ftm-inbox/backend/adapters/jira.py +136 -0
  27. package/ftm-inbox/backend/adapters/registry.py +192 -0
  28. package/ftm-inbox/backend/adapters/slack.py +110 -0
  29. package/ftm-inbox/backend/db/__init__.py +0 -0
  30. package/ftm-inbox/backend/db/connection.py +54 -0
  31. package/ftm-inbox/backend/db/schema.py +78 -0
  32. package/ftm-inbox/backend/executor/__init__.py +7 -0
  33. package/ftm-inbox/backend/executor/engine.py +149 -0
  34. package/ftm-inbox/backend/executor/step_runner.py +98 -0
  35. package/ftm-inbox/backend/main.py +103 -0
  36. package/ftm-inbox/backend/models/__init__.py +1 -0
  37. package/ftm-inbox/backend/models/unified_task.py +36 -0
  38. package/ftm-inbox/backend/planner/__init__.py +6 -0
  39. package/ftm-inbox/backend/planner/__pycache__/__init__.cpython-314.pyc +0 -0
  40. package/ftm-inbox/backend/planner/__pycache__/generator.cpython-314.pyc +0 -0
  41. package/ftm-inbox/backend/planner/__pycache__/schema.cpython-314.pyc +0 -0
  42. package/ftm-inbox/backend/planner/generator.py +127 -0
  43. package/ftm-inbox/backend/planner/schema.py +34 -0
  44. package/ftm-inbox/backend/requirements.txt +5 -0
  45. package/ftm-inbox/backend/routes/__init__.py +0 -0
  46. package/ftm-inbox/backend/routes/__pycache__/plan.cpython-314.pyc +0 -0
  47. package/ftm-inbox/backend/routes/execute.py +186 -0
  48. package/ftm-inbox/backend/routes/health.py +52 -0
  49. package/ftm-inbox/backend/routes/inbox.py +68 -0
  50. package/ftm-inbox/backend/routes/plan.py +271 -0
  51. package/ftm-inbox/bin/launchagent.mjs +91 -0
  52. package/ftm-inbox/bin/setup.mjs +188 -0
  53. package/ftm-inbox/bin/start.sh +10 -0
  54. package/ftm-inbox/bin/status.sh +17 -0
  55. package/ftm-inbox/bin/stop.sh +8 -0
  56. package/ftm-inbox/config.example.yml +55 -0
  57. package/ftm-inbox/package-lock.json +2898 -0
  58. package/ftm-inbox/package.json +26 -0
  59. package/ftm-inbox/postcss.config.js +6 -0
  60. package/ftm-inbox/src/app.css +199 -0
  61. package/ftm-inbox/src/app.html +18 -0
  62. package/ftm-inbox/src/lib/api.ts +166 -0
  63. package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -0
  64. package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -0
  65. package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -0
  66. package/ftm-inbox/src/lib/components/PlanView.svelte +206 -0
  67. package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -0
  68. package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -0
  69. package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -0
  70. package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -0
  71. package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -0
  72. package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -0
  73. package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -0
  74. package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -0
  75. package/ftm-inbox/src/lib/theme.ts +47 -0
  76. package/ftm-inbox/src/routes/+layout.svelte +76 -0
  77. package/ftm-inbox/src/routes/+page.svelte +401 -0
  78. package/ftm-inbox/static/favicon.png +0 -0
  79. package/ftm-inbox/svelte.config.js +12 -0
  80. package/ftm-inbox/tailwind.config.ts +63 -0
  81. package/ftm-inbox/tsconfig.json +13 -0
  82. package/ftm-inbox/vite.config.ts +6 -0
  83. package/ftm-intent/SKILL.md +44 -0
  84. package/ftm-manifest.json +3794 -0
  85. package/ftm-map/SKILL.md +259 -0
  86. package/ftm-map/scripts/db.py +391 -0
  87. package/ftm-map/scripts/index.py +341 -0
  88. package/ftm-map/scripts/parser.py +455 -0
  89. package/ftm-map/scripts/queries/.gitkeep +0 -0
  90. package/ftm-map/scripts/queries/javascript-tags.scm +23 -0
  91. package/ftm-map/scripts/queries/python-tags.scm +17 -0
  92. package/ftm-map/scripts/queries/typescript-tags.scm +29 -0
  93. package/ftm-map/scripts/query.py +149 -0
  94. package/ftm-map/scripts/requirements.txt +2 -0
  95. package/ftm-map/scripts/setup-hooks.sh +27 -0
  96. package/ftm-map/scripts/setup.sh +45 -0
  97. package/ftm-map/scripts/test_db.py +124 -0
  98. package/ftm-map/scripts/test_parser.py +106 -0
  99. package/ftm-map/scripts/test_query.py +66 -0
  100. package/ftm-map/scripts/tests/fixtures/__init__.py +0 -0
  101. package/ftm-map/scripts/tests/fixtures/sample_project/api.ts +16 -0
  102. package/ftm-map/scripts/tests/fixtures/sample_project/auth.py +15 -0
  103. package/ftm-map/scripts/tests/fixtures/sample_project/utils.js +16 -0
  104. package/ftm-map/scripts/views.py +545 -0
  105. package/ftm-mind/SKILL.md +173 -66
  106. package/ftm-pause/SKILL.md +43 -0
  107. package/ftm-researcher/SKILL.md +275 -0
  108. package/ftm-researcher/evals/agent-diversity.yaml +17 -0
  109. package/ftm-researcher/evals/synthesis-quality.yaml +12 -0
  110. package/ftm-researcher/evals/trigger-accuracy.yaml +39 -0
  111. package/ftm-researcher/references/adaptive-search.md +116 -0
  112. package/ftm-researcher/references/agent-prompts.md +193 -0
  113. package/ftm-researcher/references/council-integration.md +193 -0
  114. package/ftm-researcher/references/output-format.md +203 -0
  115. package/ftm-researcher/references/synthesis-pipeline.md +165 -0
  116. package/ftm-researcher/scripts/score_credibility.py +234 -0
  117. package/ftm-researcher/scripts/validate_research.py +92 -0
  118. package/ftm-resume/SKILL.md +47 -0
  119. package/ftm-retro/SKILL.md +54 -0
  120. package/ftm-routine/SKILL.md +170 -0
  121. package/ftm-state/blackboard/capabilities.json +5 -0
  122. package/ftm-state/blackboard/capabilities.schema.json +27 -0
  123. package/ftm-upgrade/SKILL.md +41 -0
  124. package/ftm-upgrade/scripts/check-version.sh +1 -1
  125. package/ftm-upgrade/scripts/upgrade.sh +1 -1
  126. package/hooks/ftm-blackboard-enforcer.sh +94 -0
  127. package/hooks/ftm-discovery-reminder.sh +90 -0
  128. package/hooks/ftm-drafts-gate.sh +61 -0
  129. package/hooks/ftm-event-logger.mjs +107 -0
  130. package/hooks/ftm-map-autodetect.sh +79 -0
  131. package/hooks/ftm-pending-sync-check.sh +22 -0
  132. package/hooks/ftm-plan-gate.sh +96 -0
  133. package/hooks/ftm-post-commit-trigger.sh +57 -0
  134. package/hooks/settings-template.json +81 -0
  135. package/install.sh +140 -11
  136. package/package.json +12 -2
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env python3
2
+ """ftm-map indexer: builds the code knowledge graph from source files."""
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ import time
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+
13
+ # Add scripts dir to path for sibling imports
14
+ sys.path.insert(0, os.path.dirname(__file__))
15
+
16
+ from db import (
17
+ get_connection,
18
+ add_symbol,
19
+ remove_symbols_by_file,
20
+ add_edge,
21
+ get_symbol_by_name,
22
+ get_stats,
23
+ )
24
+ from parser import parse_file, extract_relationships, EXTENSION_MAP
25
+
26
+ META_REGISTRY = os.path.expanduser("~/.claude/ftm-state/maps/index.json")
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # File discovery
31
+ # ---------------------------------------------------------------------------
32
+
33
+
34
+ def discover_files(project_root: str) -> list[str]:
35
+ """Get tracked source files using git ls-files.
36
+
37
+ Filters to files whose extensions are in EXTENSION_MAP so only
38
+ tree-sitter-parseable files are returned. Returns absolute paths.
39
+ """
40
+ result = subprocess.run(
41
+ ["git", "ls-files"],
42
+ capture_output=True,
43
+ text=True,
44
+ cwd=project_root,
45
+ )
46
+ if result.returncode != 0:
47
+ print(
48
+ f"Error: git ls-files failed: {result.stderr.strip()}",
49
+ file=sys.stderr,
50
+ )
51
+ return []
52
+
53
+ supported_exts = set(EXTENSION_MAP.keys())
54
+ files = []
55
+ for line in result.stdout.strip().split("\n"):
56
+ if not line:
57
+ continue
58
+ ext = Path(line).suffix.lower()
59
+ if ext in supported_exts:
60
+ files.append(os.path.join(project_root, line))
61
+ return files
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Core indexing logic
66
+ # ---------------------------------------------------------------------------
67
+
68
+
69
+ def index_files(conn, files: list[str], project_root: str) -> dict:
70
+ """Parse and insert symbols + edges for a list of absolute file paths.
71
+
72
+ Two-phase approach:
73
+ Phase 1 — parse every file and insert all symbols so that the
74
+ symbol table is fully populated before edge resolution.
75
+ Phase 2 — extract relationships and resolve source/target names to
76
+ existing symbol IDs. Unknown targets are silently skipped.
77
+
78
+ Returns a dict with 'symbols' and 'edges' counts.
79
+ """
80
+ total_symbols = 0
81
+ total_edges = 0
82
+
83
+ # Phase 1: insert all symbols first so cross-file edges can be resolved.
84
+ for fpath in files:
85
+ if not os.path.exists(fpath):
86
+ print(f"Warning: file not found, skipping: {fpath}", file=sys.stderr)
87
+ continue
88
+
89
+ rel_path = os.path.relpath(fpath, project_root)
90
+ symbols = parse_file(fpath)
91
+
92
+ for sym in symbols:
93
+ add_symbol(
94
+ conn,
95
+ name=sym.name,
96
+ kind=sym.kind,
97
+ file_path=rel_path,
98
+ start_line=sym.start_line,
99
+ end_line=sym.end_line,
100
+ signature=sym.signature,
101
+ doc_comment=sym.doc_comment,
102
+ content_hash=sym.content_hash,
103
+ )
104
+ total_symbols += 1
105
+
106
+ # Phase 2: resolve and insert edges.
107
+ for fpath in files:
108
+ if not os.path.exists(fpath):
109
+ continue
110
+
111
+ rels = extract_relationships(fpath)
112
+ for rel in rels:
113
+ source_rows = get_symbol_by_name(conn, rel.source_name)
114
+ target_rows = get_symbol_by_name(conn, rel.target_name)
115
+
116
+ # Skip if either end of the relationship is unresolvable.
117
+ if not source_rows or not target_rows:
118
+ continue
119
+
120
+ add_edge(conn, source_rows[0]["id"], target_rows[0]["id"], rel.kind)
121
+ total_edges += 1
122
+
123
+ return {"symbols": total_symbols, "edges": total_edges}
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # Bootstrap mode
128
+ # ---------------------------------------------------------------------------
129
+
130
+
131
+ def bootstrap(project_root: str) -> None:
132
+ """Full scan: (re)build the entire code graph for *project_root*."""
133
+ abs_root = os.path.abspath(project_root)
134
+ start = time.time()
135
+
136
+ files = discover_files(abs_root)
137
+ if not files:
138
+ print(
139
+ json.dumps({"error": "No parseable source files found in git repository"}),
140
+ file=sys.stderr,
141
+ )
142
+ sys.exit(1)
143
+
144
+ conn = get_connection(abs_root)
145
+ try:
146
+ # Full rebuild — clear existing content first.
147
+ # FTS5 rows must be removed before symbol rows because the content=
148
+ # table does not cascade deletes.
149
+ symbol_ids = [
150
+ row[0] for row in conn.execute("SELECT id FROM symbols").fetchall()
151
+ ]
152
+ for sid in symbol_ids:
153
+ conn.execute("DELETE FROM symbols_fts WHERE rowid=?", (sid,))
154
+ conn.execute("DELETE FROM symbols")
155
+ conn.execute("DELETE FROM edges")
156
+
157
+ stats = index_files(conn, files, abs_root)
158
+ conn.commit()
159
+
160
+ duration = time.time() - start
161
+ result = {
162
+ "mode": "bootstrap",
163
+ "files_parsed": len(files),
164
+ "symbols": stats["symbols"],
165
+ "edges": stats["edges"],
166
+ "duration_s": round(duration, 2),
167
+ }
168
+ print(json.dumps(result))
169
+ update_meta_registry(abs_root, stats["symbols"])
170
+ except Exception as exc: # noqa: BLE001
171
+ print(f"Error during bootstrap: {exc}", file=sys.stderr)
172
+ conn.rollback()
173
+ conn.close()
174
+ sys.exit(1)
175
+ finally:
176
+ conn.close()
177
+
178
+
179
+ # ---------------------------------------------------------------------------
180
+ # Incremental mode
181
+ # ---------------------------------------------------------------------------
182
+
183
+
184
+ def incremental(project_root: str, files_str: str) -> None:
185
+ """Incremental update: re-index only the specified files.
186
+
187
+ *files_str* is a comma-separated list of file paths (relative or absolute).
188
+ Old symbol/edge data for each file is removed before re-parsing so stale
189
+ entries do not accumulate.
190
+ """
191
+ abs_root = os.path.abspath(project_root)
192
+ start = time.time()
193
+
194
+ raw_files = [f.strip() for f in files_str.split(",") if f.strip()]
195
+ abs_files = [
196
+ f if os.path.isabs(f) else os.path.join(abs_root, f) for f in raw_files
197
+ ]
198
+
199
+ conn = get_connection(abs_root)
200
+ try:
201
+ # Remove stale data for all targeted files before re-parsing.
202
+ for fpath in abs_files:
203
+ rel_path = os.path.relpath(fpath, abs_root)
204
+ remove_symbols_by_file(conn, rel_path)
205
+
206
+ existing_files = [f for f in abs_files if os.path.exists(f)]
207
+ if not existing_files:
208
+ print(
209
+ json.dumps({"error": "None of the specified files exist"}),
210
+ file=sys.stderr,
211
+ )
212
+ conn.close()
213
+ sys.exit(1)
214
+
215
+ stats = index_files(conn, existing_files, abs_root)
216
+ conn.commit()
217
+
218
+ db_stats = get_stats(conn)
219
+ duration = time.time() - start
220
+ result = {
221
+ "mode": "incremental",
222
+ "files_parsed": len(existing_files),
223
+ "symbols": stats["symbols"],
224
+ "edges": stats["edges"],
225
+ "duration_s": round(duration, 2),
226
+ }
227
+ print(json.dumps(result))
228
+ update_meta_registry(abs_root, db_stats["symbols"])
229
+ except Exception as exc: # noqa: BLE001
230
+ print(f"Error during incremental update: {exc}", file=sys.stderr)
231
+ conn.rollback()
232
+ conn.close()
233
+ sys.exit(1)
234
+ finally:
235
+ conn.close()
236
+
237
+
238
+ # ---------------------------------------------------------------------------
239
+ # Meta-registry management
240
+ # ---------------------------------------------------------------------------
241
+
242
+
243
+ def update_meta_registry(project_root: str, symbol_count: int) -> None:
244
+ """Upsert project entry in the global meta-registry at META_REGISTRY."""
245
+ registry_dir = os.path.dirname(META_REGISTRY)
246
+ os.makedirs(registry_dir, exist_ok=True)
247
+
248
+ registry: dict = {"projects": []}
249
+ if os.path.exists(META_REGISTRY):
250
+ try:
251
+ with open(META_REGISTRY) as fh:
252
+ registry = json.load(fh)
253
+ except (json.JSONDecodeError, IOError):
254
+ # Corrupt or unreadable registry — start fresh.
255
+ registry = {"projects": []}
256
+
257
+ abs_root = os.path.abspath(project_root)
258
+ db_path = os.path.join(abs_root, ".ftm-map", "map.db")
259
+ now = datetime.now(timezone.utc).isoformat()
260
+
261
+ found = False
262
+ for proj in registry["projects"]:
263
+ if proj.get("path") == abs_root:
264
+ proj["last_indexed"] = now
265
+ proj["symbol_count"] = symbol_count
266
+ found = True
267
+ break
268
+
269
+ if not found:
270
+ registry["projects"].append(
271
+ {
272
+ "path": abs_root,
273
+ "db_path": db_path,
274
+ "last_indexed": now,
275
+ "symbol_count": symbol_count,
276
+ }
277
+ )
278
+
279
+ with open(META_REGISTRY, "w") as fh:
280
+ json.dump(registry, fh, indent=2)
281
+
282
+
283
+ # ---------------------------------------------------------------------------
284
+ # CLI entry point
285
+ # ---------------------------------------------------------------------------
286
+
287
+
288
+ def main() -> None:
289
+ parser = argparse.ArgumentParser(
290
+ description="ftm-map indexer — builds the code knowledge graph from source files.",
291
+ formatter_class=argparse.RawDescriptionHelpFormatter,
292
+ epilog=(
293
+ "Examples:\n"
294
+ " python3 index.py --bootstrap /path/to/project\n"
295
+ " python3 index.py --incremental --files src/foo.ts,src/bar.py\n"
296
+ " python3 index.py --incremental --files src/foo.ts --project-root /path/to/project\n"
297
+ ),
298
+ )
299
+
300
+ mode = parser.add_mutually_exclusive_group(required=True)
301
+ mode.add_argument(
302
+ "--bootstrap",
303
+ metavar="PROJECT_ROOT",
304
+ help="Full scan: index all tracked source files in PROJECT_ROOT.",
305
+ )
306
+ mode.add_argument(
307
+ "--incremental",
308
+ action="store_true",
309
+ help="Incremental update: re-index only the files given by --files.",
310
+ )
311
+
312
+ parser.add_argument(
313
+ "--files",
314
+ metavar="FILE_LIST",
315
+ help="Comma-separated list of files to re-index (required for --incremental).",
316
+ )
317
+ parser.add_argument(
318
+ "--project-root",
319
+ metavar="PATH",
320
+ default=None,
321
+ help=(
322
+ "Project root used to locate the database for incremental mode. "
323
+ "Defaults to the current working directory."
324
+ ),
325
+ )
326
+
327
+ args = parser.parse_args()
328
+
329
+ if args.bootstrap:
330
+ bootstrap(args.bootstrap)
331
+ else:
332
+ # Incremental mode
333
+ if not args.files:
334
+ print("Error: --incremental requires --files", file=sys.stderr)
335
+ sys.exit(1)
336
+ project_root = args.project_root or os.getcwd()
337
+ incremental(project_root, args.files)
338
+
339
+
340
+ if __name__ == "__main__":
341
+ main()