-
Notifications
You must be signed in to change notification settings - Fork 25
Expand file tree
/
Copy pathai-code-behaviors.el
More file actions
3623 lines (3343 loc) · 171 KB
/
ai-code-behaviors.el
File metadata and controls
3623 lines (3343 loc) · 171 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; ai-code-behaviors.el --- Behavior injection system for AI prompts -*- lexical-binding: t; -*-
;; Author: davidwuchn
;; SPDX-License-Identifier: Apache-2.0
;;; Commentary:
;; This module provides behavior injection based on prompt intent classification.
;; Behaviors are loaded from the ai-behaviors repository (https://github.com/xificurC/ai-behaviors)
;; and injected into prompts to guide AI responses.
;;
;; Features:
;; - Automatic intent classification (GPTel or keyword-based fallback)
;; - Explicit hashtag control (#=code, #deep, #tdd, etc.)
;; - Backend-agnostic injection
;;
;; Entry Points (in order of priority):
;; 1. `ai-code--insert-prompt-behaviors-advice' - Advice around `ai-code--insert-prompt'
;; Handles preset-only prompts, session checks, command-specific presets.
;; 2. `ai-code--process-behaviors' - Main behavior processing
;; Extracts hashtags, merges with presets, builds instruction blocks.
;; 3. `ai-code-behaviors-apply-preset' - Direct preset application
;; Used by mode-line menu and interactive commands.
;; 4. `ai-code--behaviors-check-preset-only-prompt' - Detects preset-only prompts
;; Called by advice to handle @preset without message content.
;;
;; Threading Model:
;; This module is designed for Emacs' single-threaded execution model.
;; State is stored in hash tables keyed by project root (git directory).
;; No locking is required as there are no concurrent accesses.
;; Caches use TTL-based expiration rather than explicit invalidation.
;;; Code:
(require 'seq)
(require 'cl-lib)
(require 'json)
(require 'map)
(require 'gptel nil t)
(declare-function ai-code-call-gptel-sync "ai-code-prompt-mode" (question))
(declare-function ai-code-cli-start "ai-code-backends" (&optional arg))
(declare-function ai-code-plain-read-string "ai-code-input" (prompt &optional initial-input candidate-list))
(declare-function ai-code-helm-read-string-with-history "ai-code-input" (prompt history-file-name &optional initial-input candidate-list))
(declare-function gptel--apply-preset "gptel" (preset setter))
(declare-function text-property-search-backward "subr" (property &optional value predicate not-current))
(defgroup ai-code-behaviors nil
"Behavior injection system for AI prompts."
:group 'ai-code)
(defcustom ai-code-behaviors-enabled t
"When non-nil, enable behavior injection based on prompt classification."
:type 'boolean
:group 'ai-code-behaviors)
(defcustom ai-code-behaviors-auto-classify t
"When non-nil, automatically classify prompts to suggest behaviors.
When nil, only explicit #hashtags in prompts are processed."
:type 'boolean
:group 'ai-code-behaviors)
(defcustom ai-code-behaviors-repo-path "~/.config/ai-behaviors"
"Path to cloned ai-behaviors repository.
The repository should be cloned from https://github.com/xificurC/ai-behaviors"
:type 'directory
:group 'ai-code-behaviors)
(defcustom ai-code-behaviors-auto-clone nil
"When non-nil, automatically clone ai-behaviors repo if not found.
The clone happens on first behavior-related operation.
Default is nil to avoid unexpected network access."
:type 'boolean
:group 'ai-code-behaviors)
(defcustom ai-code-behaviors-auto-enable nil
"When non-nil, automatically enable preset application on load.
If nil, call `ai-code-behaviors-enable-auto-presets' to activate.
Default is nil - users must explicitly opt in."
:type 'boolean
:group 'ai-code-behaviors)
(defcustom ai-code-behaviors-repo-url "https://github.com/xificurC/ai-behaviors.git"
"URL for cloning the ai-behaviors repository."
:type 'string
:group 'ai-code-behaviors)
(defcustom ai-code-behaviors-detection-patterns nil
"Custom file patterns for preset detection.
Each entry is (PATTERN . PRESET-NAME) where PATTERN is a regex.
Example: ((\"_spec\\.clj$\" . \"tdd-dev\"))"
:type '(alist :key-type string :value-type string)
:group 'ai-code-behaviors)
(defcustom ai-code-behaviors-override-preset nil
"When non-nil, override all detection with this preset.
Set to a preset name string to force that preset."
:type '(choice (const nil) string)
:group 'ai-code-behaviors)
(defcustom ai-code-behaviors-default-preset "quick-fix"
"Default preset when no signals match.
Set to nil to return nil instead of a default preset."
:type '(choice (const nil) string)
:group 'ai-code-behaviors)
(defcustom ai-code-behaviors-detection-enabled-signals
'(:filename :major-mode :project :git)
"Which signals to use for preset detection.
:filename - Detect from file name patterns
:major-mode - Detect from major mode
:project - Detect from project structure
:git - Detect from git branch name"
:type '(set (const :filename) (const :major-mode)
(const :project) (const :git))
:group 'ai-code-behaviors)
(defcustom ai-code-behaviors-detection-cache-ttl 300
"Time-to-live for detection cache in seconds.
Applies to git and project detection results."
:type 'integer
:group 'ai-code-behaviors)
(defcustom ai-code-behaviors-reclassify-min-confidence 'medium
"Minimum confidence level required for auto-classify to override session state.
In gptel-agent context, classifications below this threshold use session state.
One of high, medium, or low."
:type '(choice (const high) (const medium) (const low))
:group 'ai-code-behaviors)
(defcustom ai-code-behaviors-gptel-agent-auto-classify t
"When non-nil, auto-classify prompts in gptel-agent buffers.
When nil, gptel-agent prompts without explicit hashtags use existing
session state instead of auto-classifying.
Default is t to enable automatic behavior detection in agent workflows."
:type 'boolean
:group 'ai-code-behaviors)
(defvar ai-code--behaviors-cache (make-hash-table :test #'equal)
"Cache for loaded behavior prompts.
Each entry is (CONTENT . TIMESTAMP).")
(defcustom ai-code-behaviors-cache-ttl 3600
"Time-to-live in seconds for behavior prompt cache.
Default is 1 hour. Set to 0 to disable TTL checking."
:type 'integer
:group 'ai-code-behaviors)
(defcustom ai-code-behaviors-max-response-size 10240
"Maximum GPTel response size in bytes for JSON parsing.
Responses larger than this are rejected to prevent DoS.
Default is 10KB. Set to 0 to disable limit."
:type 'integer
:group 'ai-code-behaviors)
(defcustom ai-code-behaviors-max-brace-depth 50
"Maximum nesting depth for JSON brace counting.
Prevents CPU exhaustion on deeply nested malformed responses."
:type 'integer
:group 'ai-code-behaviors)
(defun ai-code--git-command-output (&rest args)
"Run git with ARGS and return trimmed output, or nil on error.
Uses call-process instead of shell to avoid injection vulnerabilities."
(with-temp-buffer
(when (zerop (apply #'call-process "git" nil t nil args))
(string-trim (buffer-string)))))
(defvar ai-code--behaviors-session-states (make-hash-table :test #'equal)
"Hash table of behaviors per git repository.
Key: git root directory (string)
Value: plist (:state BEHAVIOR-STATE :preset PRESET-NAME)")
(defvar ai-code--behaviors-update-checked nil
"Non-nil if update check has been performed this session.")
(defvar ai-code--detection-cache (make-hash-table :test #'equal)
"Unified cache for preset detection.
Key: (SOURCE . ROOT) where SOURCE is :project or :git.
Value: (:result RESULT :timestamp TIME).")
(defvar ai-code--behavior-annotation-cache (make-hash-table :test #'equal)
"Cache for behavior annotation strings.")
(defvar ai-code--behaviors-pending-presets (make-hash-table :test #'equal)
"Hash table of pending presets per project root.
Key: project root, Value: preset name string or nil.
Pending presets are shown in mode-line but not committed until first prompt.")
(defvar ai-code--behaviors-last-prompts (make-hash-table :test #'equal)
"Hash table of last processed prompts per project root.
Key: project root, Value: plist (:original ORIG :processed PROC :behaviors BEH).")
(declare-function ai-code--git-root "ai-code-file" (&optional dir))
(defvar ai-code--active-constraint-bundles)
(defvar ai-code--behavior-presets)
(defvar ai-code-behaviors-gptel-agent-auto-classify)
(defun ai-code--behaviors-project-root (&optional buffer)
"Return git root for BUFFER, or current buffer if nil.
For gptel-agent buffers, falls back to extracting project from buffer name.
Returns nil if not in a project (prevents state leakage between projects)."
(if (bufferp buffer)
(with-current-buffer buffer
(or (and (fboundp 'ai-code--git-root) (ai-code--git-root))
(and (fboundp 'ai-code--behaviors-extract-project-from-buffer-name)
(ai-code--behaviors-extract-project-from-buffer-name))))
(or (and (fboundp 'ai-code--git-root) (ai-code--git-root))
(and (fboundp 'ai-code--behaviors-extract-project-from-buffer-name)
(ai-code--behaviors-extract-project-from-buffer-name)))))
(defun ai-code--behaviors--get (key &optional root)
"Get entry KEY from session states for ROOT.
If ROOT is nil, use current project root.
Returns nil if not in a project."
(let ((r (or root (ai-code--behaviors-project-root))))
(when r
(plist-get (or (gethash r ai-code--behaviors-session-states)
'(:state nil :preset nil))
key))))
(defun ai-code--behaviors--set (key value &optional root)
"Set entry KEY to VALUE in session states for ROOT.
If ROOT is nil, use current project root.
Does nothing and returns nil if not in a project (prevents state leakage)."
(let ((r (or root (ai-code--behaviors-project-root))))
(when r
(let ((entry (or (gethash r ai-code--behaviors-session-states)
'(:state nil :preset nil))))
(puthash r (plist-put (copy-tree entry) key value)
ai-code--behaviors-session-states)
value))))
(defun ai-code--behaviors-get-state (&optional root)
"Get behavior state for project ROOT, or current project if nil."
(ai-code--behaviors--get :state root))
(defun ai-code--behaviors-set-state (state &optional root)
"Set behavior STATE for project ROOT, or current project if nil."
(ai-code--behaviors--set :state state root))
(defun ai-code--behaviors-get-preset (&optional root)
"Get preset name for project ROOT, or current project if nil."
(ai-code--behaviors--get :preset root))
(defun ai-code--behaviors-set-preset (preset &optional root)
"Set preset name to PRESET for project ROOT, or current project if nil."
(ai-code--behaviors--set :preset preset root))
(defun ai-code--behaviors-clear-state (&optional root)
"Clear behavior state for project ROOT, or current project if nil."
(remhash (or root (ai-code--behaviors-project-root)) ai-code--behaviors-session-states))
(defun ai-code--behaviors-set-pending-preset (preset &optional root)
"Set pending PRESET for project ROOT."
(puthash (or root (ai-code--behaviors-project-root)) preset
ai-code--behaviors-pending-presets))
(defun ai-code--behaviors-get-pending-preset (&optional root)
"Get pending preset for project ROOT."
(gethash (or root (ai-code--behaviors-project-root))
ai-code--behaviors-pending-presets))
(defun ai-code--behaviors-clear-pending-preset (&optional root)
"Clear pending preset for project ROOT."
(remhash (or root (ai-code--behaviors-project-root))
ai-code--behaviors-pending-presets))
(defun ai-code--behaviors-get-active-bundle (&optional root)
"Get active constraint bundle for project ROOT, or current project if nil."
(gethash (or root (ai-code--behaviors-project-root))
ai-code--active-constraint-bundles))
(defun ai-code--behaviors-set-active-bundle (bundle &optional root)
"Set active constraint BUNDLE for project ROOT, or current project if nil."
(puthash (or root (ai-code--behaviors-project-root)) bundle
ai-code--active-constraint-bundles))
(defun ai-code--behaviors-clear-active-bundle (&optional root)
"Clear active constraint bundle for project ROOT."
(remhash (or root (ai-code--behaviors-project-root))
ai-code--active-constraint-bundles))
(defconst ai-code--behavior-operating-modes
'("=frame" "=research" "=design" "=spec" "=code" "=debug"
"=review" "=test" "=mentor" "=drive" "=navigate" "=probe" "=record")
"Operating mode behaviors. Only one can be active at a time.")
(defconst ai-code--behavior-modifiers
'("deep" "wide" "ground" "negative-space" "challenge" "steel-man"
"user-lens" "concise" "first-principles" "creative" "subtract"
"meta" "simulate" "decompose" "recursive" "fractal" "tdd"
"io" "contract" "backward" "analogy" "temporal" "name" "checklist" "file"
"factor" "stop")
"Modifier behaviors. Multiple can be active simultaneously.")
(defconst ai-code--behavior-readonly-modes
'("=frame" "=research" "=design" "=spec" "=review" "=test"
"=mentor" "=navigate" "=probe")
"Operating modes compatible with gptel-plan (read-only phase).
These modes analyze, plan, or guide without modifying files.")
(defconst ai-code--behavior-modify-modes
'("=code" "=debug" "=drive" "=record")
"Operating modes that modify files - require gptel-agent.")
(defconst ai-code--constraint-modifiers
'(;; Language
("chinese" . "∀ response: 简体中文. code ∪ comments = English. -- HARD CONSTRAINT")
("english" . "∀ response: English. -- HARD CONSTRAINT")
("japanese" . "∀ response: 日本語. code ∪ comments = English. -- HARD CONSTRAINT")
("korean" . "∀ response: 한국어. code ∪ comments = English. -- HARD CONSTRAINT")
;; Testing
("test-after" . "code → test → verify. No untested code ships. -- HARD CONSTRAINT")
("test-unit" . "Unit tests for every function. Isolated, fast, deterministic. -- HARD CONSTRAINT")
("test-integration" . "Integration tests for component boundaries. -- HARD CONSTRAINT")
("test-e2e" . "End-to-end tests for critical user flows. -- HARD CONSTRAINT")
("test-coverage" . "Coverage ≥ 80%. Track and report gaps. -- HARD CONSTRAINT")
;; Code Style
("strict-lint" . "lint ∩ errors = ∅. Fix before commit. -- HARD CONSTRAINT")
("strict-types" . "∀ params/returns: explicit types. 'any' ⊆ forbidden. -- HARD CONSTRAINT")
("no-comments" . "code = documentation. Comments only when code cannot speak. -- HARD CONSTRAINT")
("doc-comments" . "∀ public: docstring. Parameters, returns, throws, examples. -- HARD CONSTRAINT")
("no-todos" . "No TODO/FIXME in committed code. Resolve or create issue. -- HARD CONSTRAINT")
;; Paradigm
("functional" . "state ∩ mutation = ∅. Pure functions. Immutable data. -- HARD CONSTRAINT")
("immutable" . "∀ data: const/final. No in-place mutation. -- HARD CONSTRAINT")
("oop" . "Encapsulate state. Message passing. Single responsibility. -- HARD CONSTRAINT")
("procedural" . "Step-by-step functions. Explicit state. Clear flow. -- HARD CONSTRAINT")
;; Safety
("defensive" . "∀ public input: validate. Fail fast, fail explicitly. -- HARD CONSTRAINT")
("secure" . "∀ input: untrusted. OWASP Top 10 ⊆ review. -- HARD CONSTRAINT")
("no-unsafe" . "unsafe/raw pointers ⊆ forbidden. Bounds checked. -- HARD CONSTRAINT")
("memory-safe" . "No memory leaks. Ownership clear. Resources freed. -- HARD CONSTRAINT")
;; Error Handling
("errors-raise" . "Error → throw/raise. Let caller handle. -- HARD CONSTRAINT")
("errors-result" . "Error → Result/Either type. Explicit handling. -- HARD CONSTRAINT")
("errors-checked" . "∀ errors: declared and handled. No silent failures. -- HARD CONSTRAINT")
("errors-typed" . "Typed exceptions. Specific error types per domain. -- HARD CONSTRAINT")
;; Performance
("performant" . "O(n) preferred. Allocations minimized. Hot paths identified. -- HARD CONSTRAINT")
("minimal" . "Least code. Built-ins preferred. No over-engineering. -- HARD CONSTRAINT")
("lazy" . "Compute on demand. Defer until needed. Cache results. -- HARD CONSTRAINT")
("batch" . "Batch operations. Minimize round-trips. Chunk large datasets. -- HARD CONSTRAINT")
;; Async
("async-await" . "async/await preferred. No callback hell. -- HARD CONSTRAINT")
("sync-only" . "No async. Blocking calls acceptable. -- HARD CONSTRAINT")
("reactive" . "Streams and observables. Push-based data flow. -- HARD CONSTRAINT")
;; API Design
("api-rest" . "REST conventions. Resources, verbs, status codes. -- HARD CONSTRAINT")
("api-graphql" . "GraphQL conventions. Schema-first. -- HARD CONSTRAINT")
("api-rpc" . "RPC style. Procedure calls. Named operations. -- HARD CONSTRAINT")
("api-versioned" . "Version all endpoints. Backwards compatible. -- HARD CONSTRAINT")
;; Logging
("logging-verbose" . "Log entry/exit, params, timing. Debug-friendly. -- HARD CONSTRAINT")
("logging-minimal" . "Errors only. Production-ready. -- HARD CONSTRAINT")
("no-logging" . "No log statements. Pure functions. -- HARD CONSTRAINT")
("structured-logging" . "JSON logs. Correlation IDs. Searchable. -- HARD CONSTRAINT")
;; State
("stateless" . "No internal state. Pure functions. Idempotent. -- HARD CONSTRAINT")
("state-explicit" . "State changes logged. Transitions named. -- HARD CONSTRAINT")
;; Naming
("naming-verbose" . "Descriptive names. No abbreviations. Self-documenting. -- HARD CONSTRAINT")
("naming-short" . "Concise names. Common abbreviations OK. -- HARD CONSTRAINT")
;; Dependencies
("no-deps" . "No new dependencies. Use built-ins. -- HARD CONSTRAINT")
("minimal-deps" . "Minimize dependencies. Audit each addition. -- HARD CONSTRAINT"))
"Built-in constraint modifiers with their template instructions.
These are lighter-weight than repo behaviors and cover common constraints.
Format: terse formal notation with -- HARD CONSTRAINT marker for LLM parsing.")
(defconst ai-code--constraint-bundles
'(("react-stack" . (:constraints ("strict-types" "functional" "async-await" "test-unit")
:description "React + TypeScript stack"))
("spring-stack" . (:constraints ("defensive" "doc-comments" "errors-raise" "test-integration")
:description "Spring Boot stack"))
("clojure-stack" . (:constraints ("functional" "immutable" "errors-result" "test-unit")
:description "Clojure/Scheme functional stack"))
("rust-stack" . (:constraints ("strict-types" "immutable" "errors-result" "no-unsafe" "memory-safe")
:description "Rust safety-first stack"))
("python-stack" . (:constraints ("strict-types" "test-after" "doc-comments" "secure")
:description "Python production stack"))
("node-stack" . (:constraints ("strict-types" "async-await" "test-unit" "minimal")
:description "Node.js/TypeScript stack"))
("go-stack" . (:constraints ("errors-checked" "minimal" "test-unit" "performant")
:description "Go production stack"))
("elixir-stack" . (:constraints ("functional" "immutable" "async-await" "test-unit")
:description "Elixir/Phoenix stack"))
("kotlin-stack" . (:constraints ("strict-types" "defensive" "doc-comments" "test-integration")
:description "Kotlin/JVM stack"))
("swift-stack" . (:constraints ("strict-types" "memory-safe" "async-await" "test-unit")
:description "Swift/iOS stack"))
("dotnet-stack" . (:constraints ("strict-types" "defensive" "async-await" "test-unit")
:description ".NET/C# stack"))
("rails-stack" . (:constraints ("strict-types" "test-after" "secure" "api-rest")
:description "Ruby on Rails stack"))
("django-stack" . (:constraints ("strict-types" "secure" "test-after" "api-rest")
:description "Django stack"))
("fastapi-stack" . (:constraints ("strict-types" "async-await" "api-rest" "test-unit")
:description "FastAPI stack"))
("graphql-stack" . (:constraints ("strict-types" "api-graphql" "test-integration" "secure")
:description "GraphQL API stack"))
("microservices-stack" . (:constraints ("api-rest" "async-await" "stateless" "secure" "structured-logging")
:description "Microservices architecture"))
("serverless-stack" . (:constraints ("stateless" "minimal" "async-await" "test-unit")
:description "Serverless/Lambda stack"))
("embedded-stack" . (:constraints ("minimal" "no-deps" "memory-safe" "performant")
:description "Embedded systems stack"))
("data-pipeline-stack" . (:constraints ("functional" "lazy" "batch" "test-unit")
:description "Data processing pipeline stack"))
("cli-tool-stack" . (:constraints ("minimal" "errors-checked" "stateless" "doc-comments")
:description "CLI tool stack")))
"Predefined constraint bundles for common tech stacks.
Each bundle is (NAME . (:constraints (C1 C2 ...) :description DESC)).")
(defconst ai-code--project-config-constraint-map
'(;; TypeScript/JavaScript
("tsconfig.json" . (:patterns (("strict.*true" . "strict-types")
("noImplicitAny.*true" . "strict-types"))
:constraints ("strict-types")))
(".eslintrc" . (:constraints ("strict-lint")))
(".eslintrc.js" . (:constraints ("strict-lint")))
(".eslintrc.json" . (:constraints ("strict-lint")))
(".eslintrc.yml" . (:constraints ("strict-lint")))
(".eslintrc.yaml" . (:constraints ("strict-lint")))
("eslint.config.js" . (:constraints ("strict-lint")))
(".prettierrc" . (:constraints ("strict-lint")))
(".prettierrc.json" . (:constraints ("strict-lint")))
;; Python
("pyproject.toml" . (:patterns (("\\[tool.mypy\\]" . "strict-types")
("\\[tool.pytest\\]" . "test-after")
("pytest" . "test-unit")
("strict = true" . "strict-types"))))
("setup.cfg" . (:patterns (("mypy" . "strict-types")
("pytest" . "test-after"))))
("mypy.ini" . (:constraints ("strict-types")))
("pytest.ini" . (:constraints ("test-after" "test-unit")))
("tox.ini" . (:constraints ("test-after")))
("ruff.toml" . (:constraints ("strict-lint")))
(".ruff.toml" . (:constraints ("strict-lint")))
;; Rust
("Cargo.toml" . (:constraints ("strict-types")
:patterns (("\\[dev-dependencies\\]" . "test-unit"))))
;; Go
("go.mod" . (:constraints ("errors-checked" "minimal")))
;; Java/Kotlin
("pom.xml" . (:constraints ("doc-comments" "defensive")))
("build.gradle" . (:constraints ("doc-comments" "defensive")))
("build.gradle.kts" . (:constraints ("doc-comments" "defensive")))
;; Clojure
("project.clj" . (:constraints ("functional" "immutable")))
("deps.edn" . (:constraints ("functional" "immutable")))
("shadow-cljs.edn" . (:constraints ("functional" "immutable")))
;; Ruby
("Gemfile" . (:constraints ("test-after")))
(".rubocop.yml" . (:constraints ("strict-lint")))
;; Elixir
("mix.exs" . (:constraints ("functional" "immutable" "test-unit")))
;; Swift
("Package.swift" . (:constraints ("memory-safe" "async-await")))
(".swiftlint.yml" . (:constraints ("strict-lint")))
;; .NET
("*.csproj" . (:constraints ("strict-types" "async-await")))
("Directory.Build.props" . (:constraints ("strict-types")))
;; CI/CD - implies security focus
(".github/workflows" . (:constraints ("secure")))
(".gitlab-ci.yml" . (:constraints ("secure")))
("azure-pipelines.yml" . (:constraints ("secure")))
("Jenkinsfile" . (:constraints ("secure")))
;; Testing frameworks
("jest.config.js" . (:constraints ("test-unit")))
("jest.config.ts" . (:constraints ("test-unit")))
("vitest.config.ts" . (:constraints ("test-unit")))
("karma.conf.js" . (:constraints ("test-unit")))
("mocha.opts" . (:constraints ("test-unit")))
(".mocharc.json" . (:constraints ("test-unit")))
;; API definitions
("openapi.yaml" . (:constraints ("api-rest")))
("openapi.json" . (:constraints ("api-rest")))
("swagger.yaml" . (:constraints ("api-rest")))
("schema.graphql" . (:constraints ("api-graphql")))
("schema.gql" . (:constraints ("api-graphql")))
;; Docker/Container
("Dockerfile" . (:constraints ("minimal" "secure")))
("docker-compose.yml" . (:constraints ("secure")))
("docker-compose.yaml" . (:constraints ("secure")))
;; Kubernetes
("k8s" . (:constraints ("secure" "stateless")))
("kubernetes" . (:constraints ("secure" "stateless")))
("helm" . (:constraints ("secure")))
("Chart.yaml" . (:constraints ("secure")))
;; Config management
("terraform" . (:constraints ("immutable" "state-explicit")))
("ansible" . (:constraints ("defensive" "state-explicit")))
("puppet" . (:constraints ("defensive"))))
"Map project files/patterns to auto-detected constraints.
Each entry is (FILENAME . (:constraints (C1 C2 ...) :patterns ((REGEX . CONSTRAINT) ...))).
Patterns are matched against file content for conditional constraint activation.")
(defcustom ai-code-constraints-auto-detect-enabled t
"When non-nil, automatically detect constraints from project configuration files."
:type 'boolean
:group 'ai-code-behaviors)
(defcustom ai-code-constraints-persistence-file ".ai-behaviors/constraints"
"Relative path for project-level constraint persistence.
Stored in the project root directory."
:type 'string
:group 'ai-code-behaviors)
(defvar ai-code--constraints-cache (make-hash-table :test #'equal)
"Cache for auto-detected constraints per project root.
Key: project root, Value: (:constraints (C1 C2 ...) :timestamp TIME).")
(defvar ai-code--active-constraint-bundles (make-hash-table :test #'equal)
"Hash table of active constraint bundles per project.
Key: project root, Value: bundle name string or nil.")
(defconst ai-code-behaviors--synced-commit "d1340b7"
"The upstream ai-behaviors commit this source code is synced with.
Update this when syncing with upstream behavior changes.")
(defun ai-code--behaviors-mode-readonly-p (mode)
"Return non-nil if MODE is compatible with gptel-plan (read-only)."
(member mode ai-code--behavior-readonly-modes))
(defun ai-code--behaviors-preset-readonly-p (preset-name)
"Return non-nil if PRESET-NAME is compatible with gptel-plan (read-only).
Checks the preset's operating mode against readonly modes."
(when-let ((data (assoc preset-name ai-code--behavior-presets)))
(let ((mode (plist-get (cdr data) :mode)))
(or (null mode)
(ai-code--behaviors-mode-readonly-p mode)))))
(defun ai-code--behaviors-get-repo-behavior-names ()
"Get list of behavior names from upstream repository.
Returns (MODES . MODIFIERS) where MODES are operating modes and MODIFIERS are modifiers."
(when (ai-code--behaviors-repo-available-p)
(let* ((behaviors-dir (expand-file-name "behaviors" ai-code-behaviors-repo-path))
(entries (directory-files behaviors-dir nil "^[^.]"))
(modes nil)
(modifiers nil))
(dolist (entry entries)
(let ((prompt-file (expand-file-name (format "%s/prompt.md" entry) behaviors-dir)))
(when (file-exists-p prompt-file)
(if (string-match-p "^=" entry)
(push entry modes)
(push entry modifiers)))))
(cons (sort modes #'string<) (sort modifiers #'string<)))))
(defun ai-code--behaviors-check-sync ()
"Check if source code is synced with upstream repository.
Returns t if synced, nil if mismatch, or `no-repo'
if repo is not available."
(let ((repo-commit (ai-code--behaviors-get-current-commit)))
(cond
((not repo-commit) 'no-repo)
((string= repo-commit ai-code-behaviors--synced-commit) t)
(t
(let ((repo-behaviors (ai-code--behaviors-get-repo-behavior-names)))
(if (not repo-behaviors)
'no-repo
(let ((repo-modes (car repo-behaviors))
(repo-modifiers (cdr repo-behaviors))
(source-modes (sort (copy-sequence ai-code--behavior-operating-modes) #'string<))
(source-modifiers (sort (copy-sequence ai-code--behavior-modifiers) #'string<)))
(and (equal repo-modes source-modes)
(equal repo-modifiers source-modifiers)))))))))
(defun ai-code--behaviors-get-current-commit ()
"Get current commit hash of ai-behaviors repository.
Returns short commit hash or nil if repo not available."
(when (ai-code--behaviors-repo-available-p)
(let ((default-directory (expand-file-name ai-code-behaviors-repo-path)))
(ai-code--git-command-output "rev-parse" "--short" "HEAD"))))
(defconst ai-code--behavior-presets
'(("frame-problem" . (:mode "=frame" :modifiers ("subtract" "challenge")
:description "Problem framing with critical analysis"))
("design-options" . (:mode "=design" :modifiers ("deep" "wide")
:description "Solution design exploration"))
("tdd-dev" . (:mode "=code" :modifiers ("tdd" "deep")
:description "Test-driven development"))
("thorough-debug" . (:mode "=debug" :modifiers ("deep" "challenge")
:description "Deep debugging with critical analysis"))
("quick-review" . (:mode "=review" :modifiers ("concise")
:description "Fast code review"))
("deep-review" . (:mode "=review" :modifiers ("deep" "challenge")
:description "Thorough code review"))
("research-deep" . (:mode "=research" :modifiers ("deep" "wide")
:description "Comprehensive research"))
("mentor-learn" . (:mode "=mentor" :modifiers ("first-principles")
:description "Learning/explanation mode"))
("spec-planning" . (:mode "=spec" :modifiers ("decompose" "wide")
:description "Architecture/planning mode"))
("quick-fix" . (:mode "=code" :modifiers ("concise")
:description "Simple code changes")))
"Preset behavior combinations.
Each preset is (NAME . (:mode MODE :modifiers (MOD1 MOD2) :description DESC)).")
;;; Context detection constants
(defconst ai-code--major-mode-preset-map
'((org-mode . "mentor-learn")
(markdown-mode . "mentor-learn")
(gfm-mode . "mentor-learn")
(rst-mode . "mentor-learn")
(yaml-mode . "quick-review")
(yaml-ts-mode . "quick-review")
(json-mode . "quick-review")
(json-ts-mode . "quick-review")
(toml-mode . "quick-review")
(dockerfile-mode . "quick-review")
(sh-mode . "quick-fix")
(bash-ts-mode . "quick-fix")
(makefile-mode . "quick-fix")
(protobuf-mode . "spec-planning")
(graphql-mode . "spec-planning"))
"Map major modes to presets.")
(defconst ai-code--file-pattern-preset-map
'(("_test\\.py$" . (:preset "tdd-dev" :confidence :high))
("_spec\\.rb$" . (:preset "tdd-dev" :confidence :high))
("\\.test\\.js$" . (:preset "tdd-dev" :confidence :high))
("\\.test\\.ts$" . (:preset "tdd-dev" :confidence :high))
("\\.spec\\.ts$" . (:preset "tdd-dev" :confidence :high))
("_test\\.go$" . (:preset "tdd-dev" :confidence :high))
("Tests\\.swift$" . (:preset "tdd-dev" :confidence :high))
("_test\\.rs$" . (:preset "tdd-dev" :confidence :high))
("Test\\.java$" . (:preset "tdd-dev" :confidence :high))
("_test\\.clj$" . (:preset "tdd-dev" :confidence :high))
("README" . (:preset "mentor-learn" :confidence :high))
("CHANGELOG" . (:preset "mentor-learn" :confidence :medium))
("CONTRIBUTING" . (:preset "mentor-learn" :confidence :medium))
("\\.md$" . (:preset "mentor-learn" :confidence :medium))
("\\.org$" . (:preset "mentor-learn" :confidence :medium))
("\\.rst$" . (:preset "mentor-learn" :confidence :medium))
("docs/" . (:preset "mentor-learn" :confidence :medium))
("\\.ya?ml$" . (:preset "quick-review" :confidence :low))
("\\.json$" . (:preset "quick-review" :confidence :low))
("\\.toml$" . (:preset "quick-review" :confidence :low))
("Dockerfile" . (:preset "quick-review" :confidence :medium))
("Makefile" . (:preset "quick-fix" :confidence :low))
("\\.sh$" . (:preset "quick-fix" :confidence :low))
("\\.log$" . (:preset "thorough-debug" :confidence :medium))
("\\.proto$" . (:preset "spec-planning" :confidence :medium))
("\\.graphql$" . (:preset "spec-planning" :confidence :medium)))
"Map file patterns to preset with confidence level.")
(defconst ai-code--project-structure-signals
'(("package.json" . (("jest.config.js" . "tdd-dev")
("vitest.config.js" . "tdd-dev")
("mocha.opts" . "tdd-dev")))
("Cargo.toml" . (("tests/" . "tdd-dev")))
("pyproject.toml" . (("pytest.ini" . "tdd-dev")
("tox.ini" . "tdd-dev")))
("Gemfile" . (("spec/" . "tdd-dev"))))
"Project files that signal test framework usage.
Note: Go projects are detected via filename patterns (_test.go), not project structure.")
(defconst ai-code--git-branch-patterns
'(("^feature/" . "spec-planning")
("^feat/" . "spec-planning")
("^bugfix/" . "thorough-debug")
("^fix/" . "thorough-debug")
("^hotfix/" . "thorough-debug")
("^debug/" . "thorough-debug")
("^investigate/" . "thorough-debug")
("^test/" . "tdd-dev")
("^testing/" . "tdd-dev")
("^docs/" . "mentor-learn")
("^documentation/" . "mentor-learn")
("^refactor/" . "deep-review")
("^cleanup/" . "quick-review"))
"Map git branch patterns to presets.")
;;; Mode-line faces for different operating modes
(defface ai-code-behaviors-mode-line-code
'((t (:foreground "#228B22" :weight bold)))
"Face for code mode in mode-line."
:group 'ai-code-behaviors)
(defface ai-code-behaviors-mode-line-debug
'((t (:foreground "#CD5C5C" :weight bold)))
"Face for debug mode in mode-line."
:group 'ai-code-behaviors)
(defface ai-code-behaviors-mode-line-review
'((t (:foreground "#4682B4" :weight bold)))
"Face for review mode in mode-line."
:group 'ai-code-behaviors)
(defface ai-code-behaviors-mode-line-mentor
'((t (:foreground "#DAA520" :weight bold)))
"Face for mentor mode in mode-line."
:group 'ai-code-behaviors)
(defface ai-code-behaviors-mode-line-research
'((t (:foreground "#9370DB" :weight bold)))
"Face for research mode in mode-line."
:group 'ai-code-behaviors)
(defface ai-code-behaviors-mode-line-spec
'((t (:foreground "#20B2AA" :weight bold)))
"Face for spec mode in mode-line."
:group 'ai-code-behaviors)
(defface ai-code-behaviors-mode-line-default
'((t (:foreground "#808080" :weight bold)))
"Face for unknown mode in mode-line."
:group 'ai-code-behaviors)
(defconst ai-code--intent-classification-keywords
'((=code . ("implement" "refactor" "fix" "add" "update" "change"
"edit" "modify" "create" "write" "build" "remove"))
(=debug . ("error" "bug" "exception" "failing" "broken" "crash"
"debug" "not working" "doesn't work" "fix this"))
(=research . ("what" "how does" "explain" "understand" "investigate"
"explore" "research" "find out" "tell me about"))
(=review . ("review" "check" "audit" "analyze" "inspect" "look at"
"feedback" "opinion" "thoughts on"))
(=spec . ("plan" "design" "propose" "architecture" "spec" "specify"
"outline" "structure" "approach for"))
(=test . ("test" "verify" "assert" "coverage" "unit test" "testing"))
(=mentor . ("teach" "learn" "explain in detail" "how do I"
"guide me" "show me how" "walk me through"))
(=assess . ("evaluate" "compare" "pros and cons" "better" "vs"
"which is" "should I use"))
(=record . ("document" "write docs" "readme" "record" "documentation"
"write up")))
"Keywords for intent classification when GPTel is unavailable.")
(defconst ai-code--modifier-trigger-keywords
'((deep . ("thoroughly" "in detail" "comprehensive" "deeply"
"carefully" "exhaustive"))
(tdd . ("test-driven" "tdd" "write tests first" "red green"))
(challenge . ("critically" "find flaws" "what's wrong"))
(concise . ("briefly" "short" "summary" "tldr" "quickly")))
"Keywords that trigger automatic modifier suggestions.")
(defun ai-code--behaviors-repo-available-p ()
"Return non-nil if ai-behaviors repository exists."
(let ((path (expand-file-name ai-code-behaviors-repo-path)))
(and (file-directory-p path)
(file-directory-p (expand-file-name "behaviors" path)))))
(defun ai-code--ensure-behaviors-repo ()
"Ensure ai-behaviors repository is available.
Clone it if missing and `ai-code-behaviors-auto-clone' is non-nil.
Return non-nil if repo is available after this call."
(when (and (not (ai-code--behaviors-repo-available-p))
ai-code-behaviors-auto-clone)
(let* ((repo-path (directory-file-name (expand-file-name ai-code-behaviors-repo-path)))
(parent-dir (file-name-directory repo-path))
(repo-name (file-name-nondirectory repo-path)))
(unless (file-directory-p parent-dir)
(make-directory parent-dir t))
(message "Cloning ai-behaviors repository to %s..." repo-path)
(let ((default-directory parent-dir)
(result (call-process "git" nil nil nil
"clone" ai-code-behaviors-repo-url repo-name)))
(if (eq result 0)
(message "Successfully cloned ai-behaviors repository")
(message "Failed to clone ai-behaviors repository")))))
(ai-code--behaviors-repo-available-p))
(defun ai-code--behaviors-check-for-updates ()
"Check if ai-behaviors repo has updates available.
Fetches from remote first (with 5s timeout), then compares.
Return one of: `up-to-date', `updates-available', `no-remote', `no-repo', or `error'.
Note: This performs network I/O; use sparingly."
(cond
((not (ai-code--behaviors-repo-available-p)) 'no-repo)
(t
(let ((default-directory (expand-file-name ai-code-behaviors-repo-path)))
(condition-case nil
(progn
(call-process "git" nil nil nil "fetch" "--quiet")
(let* ((remote-head (ai-code--git-command-output "rev-parse" "@{u}"))
(local-head (ai-code--git-command-output "rev-parse" "HEAD")))
(cond
((or (null remote-head) (string-empty-p remote-head)) 'no-remote)
((string= local-head remote-head) 'up-to-date)
(t 'updates-available))))
(error 'error))))))
(defun ai-code--behaviors-maybe-check-updates ()
"Check for updates once per session and message if available."
(unless ai-code--behaviors-update-checked
(setq ai-code--behaviors-update-checked t)
(when (eq (ai-code--behaviors-check-for-updates) 'updates-available)
(message "ai-behaviors has updates available. Run M-x ai-code-behaviors-install to update."))))
(defun ai-code--behaviors-commit-info ()
"Return plist with current commit info for ai-behaviors repo.
Returns nil if repo not available."
(when (ai-code--behaviors-repo-available-p)
(let ((default-directory (expand-file-name ai-code-behaviors-repo-path)))
(let ((commit (ai-code--git-command-output "rev-parse" "--short" "HEAD"))
(date (ai-code--git-command-output "log" "-1" "--format=%ci" "HEAD")))
(when (and commit date)
(list :commit commit :date date))))))
(defun ai-code--behavior-file-path (behavior-name)
"Return path to prompt.md for BEHAVIOR-NAME."
(expand-file-name
(format "behaviors/%s/prompt.md" behavior-name)
(expand-file-name ai-code-behaviors-repo-path)))
(defun ai-code--behaviors-cache-get (key)
"Get cached value for KEY with TTL check.
Returns content string or nil if expired/missing."
(when-let ((entry (gethash key ai-code--behaviors-cache)))
(when (consp entry)
(let ((content (car entry))
(timestamp (cdr entry)))
(if (or (<= ai-code-behaviors-cache-ttl 0)
(< (- (float-time) timestamp) ai-code-behaviors-cache-ttl))
content
(remhash key ai-code--behaviors-cache)
nil)))))
(defun ai-code--behaviors-cache-put (key value)
"Cache VALUE for KEY with current timestamp."
(puthash key (cons value (float-time)) ai-code--behaviors-cache)
value)
(defvar ai-code--behaviors-cleanup-timer nil
"Idle timer for periodic cache cleanup.")
(defun ai-code--behaviors-cleanup-expired-caches ()
"Remove expired entries from all TTL-based caches.
Called periodically by idle timer to prevent memory growth."
(let ((now (float-time)))
(when (> ai-code-behaviors-cache-ttl 0)
(maphash
(lambda (k v)
(when (> (- now (cdr v)) ai-code-behaviors-cache-ttl)
(remhash k ai-code--behaviors-cache)))
ai-code--behaviors-cache))
(when (> ai-code-behaviors-detection-cache-ttl 0)
(maphash
(lambda (k v)
(let ((timestamp (plist-get v :timestamp)))
(when (and timestamp (> (- now timestamp) ai-code-behaviors-detection-cache-ttl))
(remhash k ai-code--detection-cache))))
ai-code--detection-cache))))
(defun ai-code--behaviors-start-cleanup-timer ()
"Start the idle timer for cache cleanup.
Timer runs every 5 minutes when Emacs is idle."
(unless ai-code--behaviors-cleanup-timer
(setq ai-code--behaviors-cleanup-timer
(run-with-idle-timer 300 t #'ai-code--behaviors-cleanup-expired-caches))))
(defun ai-code--behaviors-stop-cleanup-timer ()
"Stop the idle timer for cache cleanup."
(when ai-code--behaviors-cleanup-timer
(cancel-timer ai-code--behaviors-cleanup-timer)
(setq ai-code--behaviors-cleanup-timer nil)))
(defun ai-code--load-behavior-prompt (behavior-name)
"Load and cache the prompt content for BEHAVIOR-NAME.
Return the prompt content string, or nil if not found."
(let ((cached (ai-code--behaviors-cache-get behavior-name)))
(if cached
cached
(when (ai-code--ensure-behaviors-repo)
(ai-code--behaviors-maybe-check-updates)
(let* ((file-path (ai-code--behavior-file-path behavior-name))
(content (when (file-exists-p file-path)
(with-temp-buffer
(insert-file-contents file-path)
(buffer-string)))))
(when content
(ai-code--behaviors-cache-put behavior-name content)))))))
(defun ai-code--all-behavior-names ()
"Return list of all available behavior names including presets, constraints, and bundles."
(append (ai-code--behavior-preset-names)
(mapcar (lambda (m) (concat "#" m)) ai-code--behavior-operating-modes)
(mapcar (lambda (m) (concat "#" m)) ai-code--behavior-modifiers)
(mapcar (lambda (c) (concat "#" (car c))) ai-code--constraint-modifiers)
(ai-code--constraint-bundle-names)))
(defun ai-code--behavior-preset-names ()
"Return list of all preset names with @ prefix for completion."
(mapcar (lambda (p) (concat "@" (car p))) ai-code--behavior-presets))
(defun ai-code--constraint-bundle-names ()
"Return list of constraint bundle names with @ prefix for completion."
(mapcar (lambda (b) (concat "@" (car b))) ai-code--constraint-bundles))
(defun ai-code--behavior-preset-and-bundle-names ()
"Return list of all preset and bundle names with @ prefix for completion."
(append (ai-code--behavior-preset-names)
(ai-code--constraint-bundle-names)))
(defun ai-code--behavior-preset-capf ()
"Completion-at-point function for @preset and @bundle names.
Shows * annotation for modify presets in gptel modes."
(when (and (boundp 'major-mode)
(eq major-mode 'ai-code-prompt-mode)
(save-excursion
(skip-chars-backward "a-zA-Z0-9_-")
(eq (char-before) ?@)))
(let* ((start (1- (point)))
(end (point))
(gptel-mode-p (when (boundp 'gptel--preset)
(memq gptel--preset '(gptel-plan gptel-agent))))
(candidates (ai-code--behavior-preset-and-bundle-names))
(annotation-fn
(lambda (cand)
(let ((name (string-trim (substring cand 1))))
(cond
((and gptel-mode-p
(assoc name ai-code--behavior-presets)
(not (ai-code--behaviors-preset-readonly-p name)))
(let ((data (cdr (assoc name ai-code--behavior-presets))))
(format "* %s" (plist-get data :description))))
((assoc name ai-code--constraint-bundles)
(let ((data (cdr (assoc name ai-code--constraint-bundles))))
(format " %s" (plist-get data :description))))
((assoc name ai-code--behavior-presets)
(let ((data (cdr (assoc name ai-code--behavior-presets))))
(format " %s" (plist-get data :description))))
(t ""))))))
(list start end candidates
:annotation-function annotation-fn
:exclusive 'no))))
(defun ai-code--behavior-setup-preset-completion ()
"Add preset completion and mode-line to prompt mode buffers."
(add-hook 'completion-at-point-functions #'ai-code--behavior-preset-capf nil t)
(ai-code-behaviors-mode-line-enable))
(defun ai-code--behavior-teardown-preset-completion ()
"Remove preset completion from prompt mode buffers."
(remove-hook 'completion-at-point-functions #'ai-code--behavior-preset-capf t))
(defun ai-code--behavior-merge-preset-candidates (candidates)
"Append preset and bundle names to CANDIDATES for @ completion.
This allows preset and bundle names to appear alongside file paths in the
auto-triggered completion from `ai-code--prompt-auto-trigger-filepath-completion'."
(append candidates (ai-code--behavior-preset-and-bundle-names)))
(defun ai-code--behavior-enable-preset-in-file-completion ()
"Enable preset names in @ file completion via advice."
(advice-add 'ai-code--prompt-filepath-candidates :filter-return
#'ai-code--behavior-merge-preset-candidates))
(defun ai-code--behavior-disable-preset-in-file-completion ()
"Disable preset names in @ file completion."
(advice-remove 'ai-code--prompt-filepath-candidates
#'ai-code--behavior-merge-preset-candidates))
(defun ai-code--behavior-minibuffer-setup-hook ()
"Setup behavior completion in minibuffer."
(local-set-key (kbd "TAB") #'ai-code--behavior-minibuffer-complete))
(defun ai-code--behavior-minibuffer-complete ()
"Complete behavior hashtag at point in minibuffer."
(interactive)
(let* ((end (point))
(hash-pos (save-excursion
(skip-chars-backward "A-Za-z0-9_=-")
(when (eq (char-before) ?#)
(1- (point))))))
(if (and hash-pos (> end hash-pos))
(let* ((prefix (buffer-substring-no-properties hash-pos end))
(candidates (ai-code--all-behavior-names))