Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ Enable installation of packages from MELPA by adding an entry to package-archive
- *Context-Aware Code Actions*: The menu exposes dedicated entries for changing code (`c`), implementing TODOs (`i`), asking questions (`q`), explaining code (`x`), sending free-form commands (`<SPC>`), and refreshing AI context (`@`). Each command automatically captures the surrounding function, region, or clipboard contents (via `C-u`) to keep prompts precise.
- *Agile Development Workflows*: Use the refactoring navigator (`r`), the guided TDD cycle (`t`), and the pull/review diff helper (`v`) to keep AI-assisted work aligned with agile best practices. Prompt authoring is first-class through quick access to the prompt file (`p`), build/test helper (`b`), and AI-assisted shell/file execution (`!`). In prompt files, send the current block with `C-c C-c`.
- *Productivity & Debugging Utilities*: Initialize project navigation assets (`.`), investigate exceptions (`e`), auto-fix Flycheck issues in scope (`f`), copy or open file paths formatted for prompts (`k`, `o`), generate MCP inspector commands (`m`), capture session notes straight into Org (`n`), dictate prompts with speech-to-text (`:`), and toggle desktop notifications (`N`) to get alerted when AI responses are ready in background sessions.
- *Repo Guardrails Capture*: Derive a concise architecture guardrails file for the current repository with `C-c a A`. The command creates or updates `.ai.code.files/architecture/guardrails.org` so future AI coding sessions can reuse practical module boundaries, dependency rules, and validation expectations.
- *Seamless Prompt Management*: Open the prompt file via `ai-code-open-prompt-file` (stored under `.ai.code.files/.ai.code.prompt.org` by default), send regions with `ai-code-prompt-send-block`, and reuse prompt snippets via `yasnippet` to keep conversations organized.
- *Interactive Chat & Context Tools*: Dedicated buffers hold long-running chats, automatically enriched with file paths, diffs, and history from Magit or Git commands for richer AI responses.
- *AI-Assisted Bash Commands*: From Dired, shell, eshell, or vterm, run `C-c a !` and type natural-language commands prefixed with `:` (e.g., `:count lines of python code recursively`); the tool generates the shell command for review and executes it in a compile buffer.
Expand Down
101 changes: 101 additions & 0 deletions ai-code-discussion.el
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,107 @@ This value is used by `ai-code-take-notes' when suggesting where to store notes.
"Content of the most recent AI output"
"Default request text for `ai-code-take-notes'.")

(defconst ai-code-discussion--architecture-guardrails-file-name
"guardrails.org"
"File name for derived architecture guardrails.")

(defconst ai-code-discussion--architecture-guardrails-directory-name
"architecture"
"Directory name for derived architecture guardrails.")

(defconst ai-code-discussion--architecture-guardrails-template
(mapconcat #'identity
'("#+TITLE: Architecture Guardrails"
""
"* Purpose"
""
"* Important Modules / Areas"
""
"* Dependency Rules"
""
"* State and Ownership Rules"
""
"* AI Change Rules"
""
"* Required Validation"
""
"* Notes and Uncertainties"
"")
"\n")
"Initial Org template for architecture guardrails.")

(defun ai-code--architecture-guardrails-relative-path ()
"Return the repo-relative path for the architecture guardrails file."
(concat ai-code-files-dir-name "/"
ai-code-discussion--architecture-guardrails-directory-name "/"
ai-code-discussion--architecture-guardrails-file-name))

(defun ai-code--architecture-guardrails-file-path ()
"Return the absolute path for the architecture guardrails file."
(expand-file-name ai-code-discussion--architecture-guardrails-file-name
(expand-file-name
ai-code-discussion--architecture-guardrails-directory-name
(ai-code--ensure-files-directory))))

(defun ai-code--ensure-architecture-guardrails-file ()
"Create the architecture guardrails file with a starter template if missing."
(let ((target-file (ai-code--architecture-guardrails-file-path)))
(unless (file-directory-p (file-name-directory target-file))
(make-directory (file-name-directory target-file) t))
(unless (file-exists-p target-file)
(with-temp-file target-file
(insert ai-code-discussion--architecture-guardrails-template)))
target-file))

(defun ai-code--build-architecture-guardrails-prompt (git-root)
"Build the default prompt to derive architecture guardrails for GIT-ROOT."
(let ((relative-path (ai-code--architecture-guardrails-relative-path)))
(mapconcat
#'identity
(list "Derive a lightweight architecture guardrails document for this existing repository."
(format "Repository path: %s" git-root)
(format "Write or update @%s in Org-mode format." relative-path)
""
"Infer practical module boundaries, dependency rules, state ownership rules, and validation expectations from the current code, tests, docs, and filenames."
"Do not invent an ideal architecture."
"Do not force DDD, Hexagonal Architecture, or Clean Architecture onto the repository."
"Prefer simple, practical rules over abstract architecture theory."
"Mark uncertain conclusions clearly."
"Focus on what helps future AI coding sessions avoid breaking boundaries or introducing messy dependencies."
"Do not suggest large refactors unless clearly separated as optional future ideas."
"Keep it concise, practical, and small enough to reuse in future AI prompts."
""
"Use this Org structure:"
"#+TITLE: Architecture Guardrails"
""
"* Purpose"
"* Important Modules / Areas"
"* Dependency Rules"
"* State and Ownership Rules"
"* AI Change Rules"
"* Required Validation"
"* Notes and Uncertainties"
""
"If the file already exists, refine it instead of rewriting unrelated guidance.")
"\n")))

;;;###autoload
(defun ai-code-derive-architecture-guardrails ()
Comment on lines +766 to +767
"Ask the current AI backend to derive repository architecture guardrails."
(interactive)
(let ((git-root (ai-code--git-root)))
(unless git-root
(user-error "Not in a git repository"))
(ai-code--ensure-architecture-guardrails-file)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Delay guardrails file creation until prompt is confirmed

Calling ai-code--ensure-architecture-guardrails-file before reading user input causes a filesystem side effect even when the request is canceled (for example, aborting at the Prompt: minibuffer). In a repo that does not already have this file, ai-code-derive-architecture-guardrails will still create .ai.code.files/architecture/guardrails.org, leaving the worktree dirty despite no prompt being sent; this is unexpected for a canceled action and should be gated on successful prompt confirmation.

Useful? React with 👍 / 👎.

(if-let ((final-prompt
(ai-code-read-string
"Prompt: "
(ai-code--build-architecture-guardrails-prompt git-root))))
(progn
(ai-code--insert-prompt final-prompt)
(message "Requested architecture guardrails for %s" git-root))
(message "Architecture guardrails request cancelled"))))

(defun ai-code--get-note-candidates (default-note-file)
"Get a list of candidate note files.
DEFAULT-NOTE-FILE is included in the list. Visible org buffers are prioritized."
Expand Down
1 change: 1 addition & 0 deletions ai-code.el
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ Shows the current backend label to the right."
("P" "AI session checkpoint" ai-code-session-checkpoint)
("e" "Debug exception (C-u: clipboard)" ai-code-investigate-exception)
("f" "Fix Flycheck errors in scope" ai-code-flycheck-fix-errors-in-scope)
("A" "Derive Architecture Guardrails" ai-code-derive-architecture-guardrails)
("k" "Copy Cur File Name (C-u: full)" ai-code-copy-buffer-file-name-to-clipboard)
;; ("o" "Open recent file (C-u: insert)" ai-code-git-repo-recent-modified-files)
("p" "Open prompt history file" ai-code-open-prompt-file)
Expand Down
111 changes: 111 additions & 0 deletions test/test_ai-code-discussion.el
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,117 @@
(ai-code-take-notes t)
(should (equal captured-file "/tmp/project/.ai.code.files/test-notes.org")))))

(ert-deftest ai-code-test-derive-architecture-guardrails-creates-template-and-prompt ()
"Test `ai-code-derive-architecture-guardrails' initializes the Org file and prompt."
(let* ((tmp-root (make-temp-file "ai-code-guardrails" t))
(target-file (expand-file-name ".ai.code.files/architecture/guardrails.org" tmp-root))
captured-initial-prompt
captured-final-prompt)
(unwind-protect
(cl-letf (((symbol-function 'ai-code--git-root)
(lambda (&optional _dir)
tmp-root))
((symbol-function 'ai-code-read-string)
(lambda (prompt initial-input &optional _candidate-list)
(should (equal prompt "Prompt: "))
(setq captured-initial-prompt initial-input)
initial-input))
((symbol-function 'ai-code--insert-prompt)
(lambda (prompt)
(setq captured-final-prompt prompt))))
(ai-code-derive-architecture-guardrails)
(should (file-exists-p target-file))
(with-temp-buffer
(insert-file-contents target-file)
(should (string-match-p (regexp-quote "#+TITLE: Architecture Guardrails")
(buffer-string)))
(should (string-match-p (regexp-quote "* Dependency Rules")
(buffer-string)))
(should (string-match-p (regexp-quote "* Required Validation")
(buffer-string))))
(should (string-match-p (regexp-quote "Derive a lightweight architecture guardrails document")
captured-initial-prompt))
(should (string-match-p (regexp-quote "current code, tests, docs, and filenames")
captured-initial-prompt))
(should (string-match-p (regexp-quote "Do not invent an ideal architecture")
captured-initial-prompt))
(should (string-match-p (regexp-quote "Keep it concise")
captured-initial-prompt))
(should (string-match-p (regexp-quote "@.ai.code.files/architecture/guardrails.org")
captured-initial-prompt))
(should (string-match-p (regexp-quote "Org-mode format")
captured-initial-prompt))
(should (equal captured-final-prompt captured-initial-prompt)))
(ignore-errors (delete-directory tmp-root t)))))

(ert-deftest ai-code-test-derive-architecture-guardrails-preserves-existing-file ()
"Test `ai-code-derive-architecture-guardrails' does not overwrite an existing file."
(let* ((tmp-root (make-temp-file "ai-code-guardrails-existing" t))
(files-dir (expand-file-name ".ai.code.files/architecture" tmp-root))
(target-file (expand-file-name "guardrails.org" files-dir))
(existing-content "#+TITLE: Existing guardrails\n"))
(unwind-protect
(progn
(make-directory files-dir t)
(with-temp-file target-file
(insert existing-content))
(cl-letf (((symbol-function 'ai-code--git-root)
(lambda (&optional _dir)
tmp-root))
((symbol-function 'ai-code-read-string)
(lambda (_prompt initial-input &optional _candidate-list)
initial-input))
((symbol-function 'ai-code--insert-prompt)
(lambda (_prompt))))
(ai-code-derive-architecture-guardrails))
(with-temp-buffer
(insert-file-contents target-file)
(should (equal (buffer-string) existing-content))))
(ignore-errors (delete-directory tmp-root t)))))

(ert-deftest ai-code-test-derive-architecture-guardrails-errors-outside-git-repo ()
"Test `ai-code-derive-architecture-guardrails' requires a git repository."
(cl-letf (((symbol-function 'ai-code--git-root)
(lambda (&optional _dir)
nil)))
(should-error (ai-code-derive-architecture-guardrails)
:type 'user-error)))

(ert-deftest ai-code-test-derive-architecture-guardrails-reports-cancelled-request ()
"Test `ai-code-derive-architecture-guardrails' reports cancellation."
(let* ((tmp-root (make-temp-file "ai-code-guardrails-cancel" t))
captured-message
insert-called)
(unwind-protect
(cl-letf (((symbol-function 'ai-code--git-root)
(lambda (&optional _dir)
tmp-root))
((symbol-function 'ai-code-read-string)
(lambda (&rest _args)
nil))
((symbol-function 'ai-code--insert-prompt)
(lambda (&rest _args)
(setq insert-called t)))
((symbol-function 'message)
(lambda (format-string &rest args)
(setq captured-message
(apply #'format format-string args)))))
(ai-code-derive-architecture-guardrails)
(should-not insert-called)
(should (equal captured-message
"Architecture guardrails request cancelled")))
(ignore-errors (delete-directory tmp-root t)))))

(ert-deftest ai-code-test-menu-source-includes-derive-architecture-guardrails-entry ()
"Test the menu source exposes the architecture guardrails command."
(let ((repo-root
(file-name-directory (locate-library "ai-code-discussion"))))
(with-temp-buffer
(insert-file-contents (expand-file-name "ai-code.el" repo-root))
(should (re-search-forward
"(\"A\" \"Derive Architecture Guardrails\" ai-code-derive-architecture-guardrails)"
nil t)))))

(provide 'test_ai-code-discussion)

;;; test_ai-code-discussion.el ends here
Loading