mini-code-cli 0.0.8.4__tar.gz → 0.1.0.1__tar.gz

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 (38) hide show
  1. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/PKG-INFO +20 -2
  2. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/README.md +18 -0
  3. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli/agent_loop.py +119 -14
  4. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli/plan.py +55 -17
  5. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli/prompts.py +36 -2
  6. mini_code_cli-0.1.0.1/mini_code_cli/skills.py +365 -0
  7. mini_code_cli-0.1.0.1/mini_code_cli/subagents/builtin.py +171 -0
  8. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli/tools.py +115 -44
  9. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli/ui.py +55 -0
  10. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli.egg-info/PKG-INFO +20 -2
  11. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli.egg-info/SOURCES.txt +2 -0
  12. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/pyproject.toml +2 -2
  13. mini_code_cli-0.1.0.1/tests/test_skills.py +245 -0
  14. mini_code_cli-0.0.8.4/mini_code_cli/subagents/builtin.py +0 -79
  15. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/LICENSE +0 -0
  16. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli/__init__.py +0 -0
  17. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli/config.py +0 -0
  18. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli/permissions.py +0 -0
  19. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli/persistence.py +0 -0
  20. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli/runner.py +0 -0
  21. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli/sandbox.py +0 -0
  22. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli/shell_safety_utils.py +0 -0
  23. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli/subagents/__init__.py +0 -0
  24. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli/subagents/base.py +0 -0
  25. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli/utils.py +0 -0
  26. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli.egg-info/dependency_links.txt +0 -0
  27. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli.egg-info/entry_points.txt +0 -0
  28. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli.egg-info/requires.txt +0 -0
  29. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/mini_code_cli.egg-info/top_level.txt +0 -0
  30. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/setup.cfg +0 -0
  31. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/tests/test_agent_loop.py +0 -0
  32. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/tests/test_integration.py +0 -0
  33. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/tests/test_plan.py +0 -0
  34. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/tests/test_sandboxing.py +0 -0
  35. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/tests/test_shell_safety.py +0 -0
  36. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/tests/test_subagents.py +0 -0
  37. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/tests/test_tools.py +0 -0
  38. {mini_code_cli-0.0.8.4 → mini_code_cli-0.1.0.1}/tests/test_ui.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mini-code-cli
3
- Version: 0.0.8.4
4
- Summary: Mini Coding CLI.
3
+ Version: 0.1.0.1
4
+ Summary: Mini Coding Agent. Allows you to easily experiment with a small coding agent.
5
5
  Author-email: Nikolai Rozanov <nikolai.rozanov@gmail.com>
6
6
  License: GNU AFFERO GENERAL PUBLIC LICENSE
7
7
  Version 3, 19 November 2007
@@ -700,6 +700,9 @@ mini-code --url "https://api.deepseek.com" --model "deepseek-v4-flash"
700
700
  ```bash
701
701
  mini-code --enable-shell --auto-mode
702
702
  mini-code -s -a --sudo
703
+ mini-code -s -a -p -g -l --sudo #plan, previous session, subagents (#or short-hand the next commands)
704
+ mini-code --all
705
+ mini-code --all-last
703
706
  ```
704
707
 
705
708
  4. Advanced usage:
@@ -868,4 +871,19 @@ vllm serve ornith-ai/Ornith-1.5-397B-FP8 \
868
871
  --trust-remote-code
869
872
  ```
870
873
 
874
+ ## Citation
875
+
876
+ If you use `mini-code-cli` in your research or writing, please cite it using the BibTeX entry below:
877
+
878
+ ```bibtex
879
+ @misc{mini-code-cli,
880
+ author = {Rozanov, Nikolai},
881
+ title = {mini-code-cli: A minimal, dependency-light coding agent},
882
+ year = {2026},
883
+ version = {0.1.0.0},
884
+ howpublished = {\url{https://github.com/ai-nikolai/mini-code-cli}},
885
+ note = {A no-bloat, dependency-free agent CLI for experimenting.}
886
+ }
887
+ ```
888
+
871
889
  ## (C) Nikolai Rozanov, 2026 - Present
@@ -26,6 +26,9 @@ mini-code --url "https://api.deepseek.com" --model "deepseek-v4-flash"
26
26
  ```bash
27
27
  mini-code --enable-shell --auto-mode
28
28
  mini-code -s -a --sudo
29
+ mini-code -s -a -p -g -l --sudo #plan, previous session, subagents (#or short-hand the next commands)
30
+ mini-code --all
31
+ mini-code --all-last
29
32
  ```
30
33
 
31
34
  4. Advanced usage:
@@ -194,4 +197,19 @@ vllm serve ornith-ai/Ornith-1.5-397B-FP8 \
194
197
  --trust-remote-code
195
198
  ```
196
199
 
200
+ ## Citation
201
+
202
+ If you use `mini-code-cli` in your research or writing, please cite it using the BibTeX entry below:
203
+
204
+ ```bibtex
205
+ @misc{mini-code-cli,
206
+ author = {Rozanov, Nikolai},
207
+ title = {mini-code-cli: A minimal, dependency-light coding agent},
208
+ year = {2026},
209
+ version = {0.1.0.0},
210
+ howpublished = {\url{https://github.com/ai-nikolai/mini-code-cli}},
211
+ note = {A no-bloat, dependency-free agent CLI for experimenting.}
212
+ }
213
+ ```
214
+
197
215
  ## (C) Nikolai Rozanov, 2026 - Present
@@ -16,6 +16,7 @@ smaller, focused modules so each concern is easy to read and test:
16
16
  * :mod:`mini_code_cli.runner` -- running a tool with a wall-clock timeout.
17
17
  * :mod:`mini_code_cli.config` -- shared default settings.
18
18
  * :mod:`mini_code_cli.subagents` -- delegating a task to a sub-agent.
19
+ * :mod:`mini_code_cli.skills` -- the ``.mini`` skills feature.
19
20
 
20
21
  For backward compatibility (and the test-suite) the most commonly imported
21
22
  helpers are re-exported from here, so ``from mini_code_cli.agent_loop import ...``
@@ -32,6 +33,7 @@ from . import tools
32
33
  from . import utils
33
34
  from . import ui
34
35
  from . import subagents
36
+ from . import skills
35
37
  from . import plan as plan_utils
36
38
  from .config import (
37
39
  DEFAULT_CACHE_LOCATION,
@@ -116,14 +118,29 @@ def parse_args():
116
118
  parser.add_argument("--allowed-dir", type=str, default=".", help="Allowed directory for file operations, default='./'")
117
119
  parser.add_argument("-s", "--enable-shell", action="store_true", help="Allow shell execution. default= False")
118
120
  parser.add_argument("-g","--enable-subagents", action="store_true", help="Allow the agent to delegate work to sub-agents (exposed as extra tools). default= False")
121
+ parser.add_argument("--skills", action="store_true", help="Enable the skills feature: a local .mini directory (inside --allowed-dir) of skill.md files, skill sub-dirs, a high-level SKILLS.md summary, and executable Python scripts, exposed to the agent as list_skills/read_skill/create_skill/run_skill tools. default= False")
119
122
  parser.add_argument("--llm-timeout", type=int, default=DEFAULT_HTTP_TIMEOUT, help=f"HTTP timeout in seconds for LLM requests (reasoning models can be slow). default={DEFAULT_HTTP_TIMEOUT}")
120
123
  parser.add_argument("--tool-timeout", type=int, default=DEFAULT_TOOL_TIMEOUT, help=f"Timeout in seconds for a single tool call. default={DEFAULT_TOOL_TIMEOUT}")
121
124
  parser.add_argument("-p","--plan", action="store_true", help="Enable plan mode: the agent keeps a CURRENT_PLAN.md (its step-by-step plan, marked finished when the goal is done) and CURRENT_GOAL.md (the user's latest instruction) in the session directory. Also changes how history compaction works.")
122
125
 
126
+ parser.add_argument("--all", action="store_true", help="Enable: --plan, --subagents, --shell, --automode, --sudo")
127
+ parser.add_argument("--all-last", action="store_true", help="Enable: --plan, --subagents, --shell, --automode, --sudo, --load-last-session")
128
+
123
129
  # Output verbosity
124
130
  parser.add_argument("--verbose", action="store_true", help="Print the full, detailed output. Without it (default), only user/assistant turns, token usage and tool calls are shown.")
125
131
 
126
132
  args = parser.parse_args()
133
+
134
+ if args.all or args.all_last:
135
+ args.plan = True
136
+ args.enable_subagents = True
137
+ args.enable_shell = True
138
+ args.sudo = True
139
+ args.auto_mode = True
140
+
141
+ if args.all_last:
142
+ args.load_last_session = True
143
+
127
144
  return args
128
145
 
129
146
 
@@ -154,6 +171,10 @@ def print_help(allowed_dir, server_url, args, unique_id, cache_dir, model_name):
154
171
  ui.console.emit(ui.console.style("Sub-agents:", bold=True, fg="cyan"))
155
172
  for sa in subagents.get_registry().values():
156
173
  ui.console.emit(f" {sa.name:16} {sa.description}")
174
+ if args.enable_skills or args.verbose:
175
+ ui.console.emit(ui.console.style("Skills:", bold=True, fg="cyan"))
176
+ for name in skills.SKILL_TOOL_NAMES:
177
+ ui.console.emit(f" {name:16} (from the local .mini directory)")
157
178
  ui.console.rule(char="═", fg="cyan")
158
179
 
159
180
 
@@ -285,6 +306,19 @@ class AgentSession:
285
306
  # real function.
286
307
  self.tools_dict[sa.name] = {"function": None, "description": sa.description, "subagent": True}
287
308
 
309
+ # Skills (opt-in via --skills). Each registered skill tool is exposed to
310
+ # the model as a normal tool, but they are all sandbox-safe: the .mini
311
+ # directory lives *inside* --allowed-dir and the tools never accept a
312
+ # path from the model -- skill / script names are validated as single
313
+ # path segments and resolved under the .mini root. So skills are exposed
314
+ # even in sessions without --enable-shell (run_skill only executes a
315
+ # named Python file, never a free-form shell command).
316
+ self.enable_skills = bool(getattr(args, "enable_skills", False))
317
+ if self.enable_skills:
318
+ self.skills_tools_dict = skills.get_skills_tools_dict(self.allowed_dir)
319
+ self.llm_tools.extend(skills.get_skills_tools(self.allowed_dir))
320
+ self.tools_dict.update(self.skills_tools_dict)
321
+
288
322
  # Token accounting for the whole session.
289
323
  self.token_stats = {
290
324
  "prompt_tokens": 0,
@@ -363,7 +397,11 @@ class AgentSession:
363
397
  if self.args.system_prompt:
364
398
  system_prompt = self.args.system_prompt
365
399
  else:
366
- system_prompt = prompts.system_prompt(tools_dict=self.tools_dict, allowed_dir=self.allowed_dir, plan_mode=self.plan_mode)
400
+ system_prompt = prompts.system_prompt(
401
+ tools_dict=self.tools_dict, allowed_dir=self.allowed_dir,
402
+ plan_mode=self.plan_mode,
403
+ skills_dir=self.allowed_dir if self.enable_skills else None,
404
+ )
367
405
 
368
406
  messages = [{"role": "system", "content": system_prompt}]
369
407
 
@@ -543,6 +581,10 @@ class AgentSession:
543
581
  print_welcome(self.allowed_dir, self.server_url, self.args,
544
582
  self.unique_id, self.cache_dir, self.model)
545
583
 
584
+ # Announce the session's unique id as an assistant-styled message so the
585
+ # user can see / copy the id this conversation is being saved under.
586
+ self._print_session_id()
587
+
546
588
  # Report the outcome of --load-last-session up front, so the user sees
547
589
  # whether we resumed an earlier conversation (under a brand-new id) or
548
590
  # started fresh because no previous session was found.
@@ -611,6 +653,20 @@ class AgentSession:
611
653
  # Ctrl-C mid-loop just saves and re-prompts rather than crashing.
612
654
  self._persist()
613
655
 
656
+ def _print_session_id(self):
657
+ """Print the session's unique id as an assistant-styled message.
658
+
659
+ A short, styled announcement (in the assistant accent) so the user can
660
+ see and copy the id the conversation is saved under -- the same id shown
661
+ in the welcome banner, but rendered as a transcript turn so it is easy
662
+ to spot and grep.
663
+ """
664
+ self.console.divider()
665
+ self.console.turn(
666
+ "Session id",
667
+ f"{self.unique_id} (saved under {self.cache_dir})",
668
+ accent="green")
669
+
614
670
  # -- autonomous drive (used by sub-agents) ------------------------------
615
671
 
616
672
  def _drive_auto(self, initial_task, max_steps=None):
@@ -619,20 +675,39 @@ class AgentSession:
619
675
  Appends ``initial_task`` as a user turn, then keeps stepping until the
620
676
  model makes no further tool calls (auto-mode). ``max_steps`` caps the
621
677
  number of model calls so a misbehaving sub-agent cannot loop forever.
678
+
679
+ The sub-agent records its work in its own cache dir: every step persists
680
+ history / recorder / usage into the child's own session folder, and it
681
+ seeds CURRENT_GOAL.md / CURRENT_PLAN.md (in plan mode) so each sub-session
682
+ is self-contained and easy to inspect or resume. A final record is left
683
+ even on a mid-run failure (the seeded goal is marked finished).
622
684
  """
623
685
  self.messages.append({"role": "user", "content": initial_task})
686
+ # Seed the goal up front so the child keeps a self-contained record in
687
+ # its own cache dir (a sub-agent always records a goal, independent of
688
+ # the parent's --plan flag). We use the child's own plan_manager here so
689
+ # nothing is written to the parent's session folder.
690
+ from .plan import PlanManager
691
+ child_plan = PlanManager(os.path.join(self.cache_dir, self.unique_id), enabled=True)
692
+ child_plan.write_goal(initial_task)
624
693
  steps = 0
625
- while True:
626
- should_continue = self._step()
694
+ try:
695
+ while True:
696
+ should_continue = self._step()
697
+ self._persist()
698
+ steps += 1
699
+ if max_steps and steps >= max_steps:
700
+ self.console.note(
701
+ f"Sub-agent reached max_steps ({max_steps}); stopping.", "warn")
702
+ break
703
+ if not should_continue:
704
+ break
705
+ return self._last_assistant_text()
706
+ finally:
707
+ # Leave a clean, finished record in the child's cache dir even on an
708
+ # error / early stop, so the sub-session is never left open.
709
+ child_plan.mark_finished()
627
710
  self._persist()
628
- steps += 1
629
- if max_steps and steps >= max_steps:
630
- self.console.note(
631
- f"Sub-agent reached max_steps ({max_steps}); stopping.", "warn")
632
- break
633
- if not should_continue:
634
- break
635
- return self._last_assistant_text()
636
711
 
637
712
  def _last_assistant_text(self):
638
713
  """Return the content of the most recent assistant (non tool-call) turn."""
@@ -807,7 +882,7 @@ class AgentSession:
807
882
  self.unique_id, self.cache_dir, self.model)
808
883
  return True
809
884
  if cmd in self.CLEAR_COMMANDS:
810
- self.console.clear_screen()
885
+ self._clear_and_show_last()
811
886
  return True
812
887
  self.console.note(f"Unknown command {stripped!r}. Try /help.", "warn")
813
888
  return True
@@ -835,12 +910,42 @@ class AgentSession:
835
910
  return
836
911
  self._reset_history(target["turn"], target)
837
912
 
913
+ def _clear_and_show_last(self):
914
+ """Handle /clear: wipe the screen and re-print the last turn.
915
+
916
+ The conversation itself is unchanged; we just redraw the most recent
917
+ exchange so the user re-orients after clearing the view. If there is no
918
+ assistant reply yet, the last user turn is shown instead.
919
+ """
920
+ self.console.clear_screen()
921
+ last_role = last_text = None
922
+ for m in reversed(self.messages):
923
+ role = m.get("role")
924
+ content = m.get("content")
925
+ if role == "assistant" and not m.get("tool_calls") and content:
926
+ last_role, last_text = "Assistant", content
927
+ break
928
+ if role == "user" and content:
929
+ last_role, last_text = "You", content
930
+ break
931
+ if last_role is not None:
932
+ self.console.divider()
933
+ accent = "green" if last_role == "Assistant" else "cyan"
934
+ self.console.turn(last_role, last_text, accent=accent)
935
+
838
936
  # -- tool execution ----------------------------------------------------
839
937
 
840
938
  def _run_tool_calls(self, tool_calls):
841
939
  _make_tool_call_note(self.console, len(tool_calls))
842
- for call in tool_calls:
843
- self._run_one_tool_call(call)
940
+ # Show a "… calling tool" spinner (in-place, non-scrolling) while the
941
+ # tool(s) execute, mirroring the thinking spinner that covers the LLM
942
+ # call. Always stopped (finally) so it can never leak into the next turn.
943
+ self.console.start_tool_call()
944
+ try:
945
+ for call in tool_calls:
946
+ self._run_one_tool_call(call)
947
+ finally:
948
+ self.console.stop_tool_call()
844
949
 
845
950
  def _run_one_tool_call(self, call):
846
951
  call_id = call.get("id", "")
@@ -6,14 +6,16 @@ active goal and the plan for reaching it:
6
6
 
7
7
  * ``CURRENT_GOAL.md`` -- the user's latest instruction / goal (verbatim).
8
8
  * ``CURRENT_PLAN.md`` -- the agent's current plan for that goal, followed by
9
- a short running log (e.g. an entry recording when a goal was finished).
9
+ a short running log (e.g. an entry recording when a goal was set/changed,
10
+ and when it was finished).
10
11
 
11
12
  The plan document itself is authored by the agent (the system prompt tells it
12
13
  to, and it uses its ``write_file`` tool). This manager owns the parts the loop
13
14
  is responsible for -- seeding/reading the goal, reading the plan back so history
14
- compaction can re-inject it, and appending the "goal finished" log entry. It is
15
- deliberately free of any network or terminal dependency, and every method is a
16
- no-op when disabled, so it is trivial to unit-test in isolation.
15
+ compaction can re-inject it, and appending the "goal finished" / goal-change log
16
+ entries. It is deliberately free of any network or terminal dependency, and
17
+ every method is a no-op when disabled, so it is trivial to unit-test in
18
+ isolation.
17
19
  """
18
20
 
19
21
  import os
@@ -31,6 +33,11 @@ class PlanManager:
31
33
  ``history_{n}.json``). When ``enabled`` is False every method is a no-op, so
32
34
  callers can use the manager unconditionally and let the ``--plan`` flag
33
35
  decide whether anything actually happens.
36
+
37
+ ``session_dir`` is the session directory (``cache_dir/<unique_id>``) -- i.e.
38
+ the files always live in the *cache* dir, independent of ``--allowed-dir``.
39
+ The manager writes with plain ``open`` (not through the file tools), so the
40
+ records are created even when the allowed dir is a different location.
34
41
  """
35
42
 
36
43
  def __init__(self, session_dir, enabled=True, goal=None):
@@ -39,6 +46,10 @@ class PlanManager:
39
46
  # The last goal this manager was asked to record, so callers can tell a
40
47
  # new goal apart from the previous one. Kept in sync by write_goal().
41
48
  self.goal = goal
49
+ # The last goal (stripped) that was logged into CURRENT_PLAN.md, so
50
+ # write_goal can tell a genuine change apart from a re-stated goal and
51
+ # avoid spamming the plan with duplicate "goal updated" entries.
52
+ self._last_logged_goal = None
42
53
 
43
54
  # -- paths -------------------------------------------------------------
44
55
  @property
@@ -50,20 +61,55 @@ class PlanManager:
50
61
  return os.path.join(self.session_dir, GOAL_FILENAME)
51
62
 
52
63
  # -- writing -----------------------------------------------------------
64
+ def _append_plan_note(self, text):
65
+ """Append one dated line to CURRENT_PLAN.md (creating it if needed).
66
+
67
+ Shared by the goal-change log and ``mark_finished``. Best-effort and
68
+ never raises: a plan-tracking hiccup must never break the agent loop.
69
+ """
70
+ if not self.enabled:
71
+ return
72
+ try:
73
+ os.makedirs(self.session_dir, exist_ok=True)
74
+ if not os.path.exists(self.plan_path):
75
+ with open(self.plan_path, "w", encoding="utf-8") as f:
76
+ f.write("# Current Plan\n\n")
77
+ with open(self.plan_path, "a", encoding="utf-8") as f:
78
+ f.write(text + "\n")
79
+ except OSError:
80
+ pass
81
+
53
82
  def write_goal(self, goal):
54
83
  """Persist the user's current goal / last instruction to CURRENT_GOAL.md.
55
84
 
56
85
  Overwrites any previous goal -- the file always reflects the *latest*
57
- instruction. Best-effort and never raises: a plan-tracking hiccup must
58
- never break the agent loop.
86
+ instruction. Also makes sure ``CURRENT_PLAN.md`` exists in the cache dir
87
+ (so both tracking files are present from the very first goal), and -- in
88
+ plan mode -- logs the goal into CURRENT_PLAN.md the *first* time it is
89
+ set and again whenever it changes, so the plan records goal pivots.
90
+ Best-effort and never raises.
59
91
  """
60
92
  if not self.enabled:
61
93
  return
94
+ stripped = (goal or "").strip()
62
95
  try:
63
96
  os.makedirs(self.session_dir, exist_ok=True)
97
+ # Ensure CURRENT_PLAN.md exists in the cache dir too, so the goal
98
+ # always has a companion plan file sitting next to it.
99
+ if not os.path.exists(self.plan_path):
100
+ with open(self.plan_path, "w", encoding="utf-8") as f:
101
+ f.write("# Current Plan\n\n")
64
102
  with open(self.goal_path, "w", encoding="utf-8") as f:
65
103
  f.write("# Current Goal\n\n")
66
- f.write((goal or "").strip() + "\n")
104
+ f.write(stripped + "\n")
105
+ # Log the goal change into the plan -- but only on a genuine change
106
+ # (compared against the last goal actually logged), so re-stating the
107
+ # same goal on later turns does not spam the plan log.
108
+ if stripped and stripped != self._last_logged_goal:
109
+ verb = "Goal set to" if self._last_logged_goal is None else "Goal updated to"
110
+ stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
111
+ self._append_plan_note(f"- [{stamp}] {verb}: {stripped}")
112
+ self._last_logged_goal = stripped
67
113
  self.goal = goal
68
114
  except OSError:
69
115
  pass
@@ -82,16 +128,8 @@ class PlanManager:
82
128
  # never captured one itself, e.g. a one-shot --prompt preset), and only
83
129
  # then fall back to a generic description.
84
130
  goal = goal or self.goal or self._read(self.goal_path) or "current goal"
85
- try:
86
- os.makedirs(self.session_dir, exist_ok=True)
87
- if not os.path.exists(self.plan_path):
88
- with open(self.plan_path, "w", encoding="utf-8") as f:
89
- f.write("# Current Plan\n\n")
90
- stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
91
- with open(self.plan_path, "a", encoding="utf-8") as f:
92
- f.write(f"\n- [{stamp}] Task finished: {goal}\n")
93
- except OSError:
94
- pass
131
+ stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
132
+ self._append_plan_note(f"- [{stamp}] Task finished: {goal}")
95
133
 
96
134
  # -- reading -----------------------------------------------------------
97
135
  def read_plan(self):
@@ -1,5 +1,5 @@
1
1
  # System prompt defining available tools and compaction behavior
2
- def system_prompt(tools_dict=None, allowed_dir=None, plan_mode=False):
2
+ def system_prompt(tools_dict=None, allowed_dir=None, plan_mode=False, skills_dir=None):
3
3
  """
4
4
  Takes a list of tool names and returns a system prompt string.
5
5
 
@@ -26,6 +26,10 @@ def system_prompt(tools_dict=None, allowed_dir=None, plan_mode=False):
26
26
  if allowed_dir:
27
27
  allowed_dir_prompt = f"\nThe path you are allowed to operate in is called: {allowed_dir}"
28
28
 
29
+ skills_prompt = ""
30
+ if skills_dir is not None:
31
+ skills_prompt = "\n" + skills_instructions(skills_dir)
32
+
29
33
  plan_mode_prompt = ""
30
34
  if plan_mode:
31
35
  plan_mode_prompt = "\n" + plan_instructions()
@@ -33,7 +37,7 @@ def system_prompt(tools_dict=None, allowed_dir=None, plan_mode=False):
33
37
 
34
38
  prompt = f"""
35
39
  {base_prompt}
36
- {tool_descriptions}{end_prompt}{allowed_dir_prompt}{plan_mode_prompt}
40
+ {tool_descriptions}{end_prompt}{allowed_dir_prompt}{plan_mode_prompt}{skills_prompt}
37
41
  Important: Follow the user instruction. Do not create additional .md files (unless asked to do so). Do not create unit tests (unless asked to do so), etc.
38
42
  """
39
43
  return prompt.strip()
@@ -62,6 +66,36 @@ def plan_instructions():
62
66
  "easy to track and resume."
63
67
  )
64
68
 
69
+ def skills_instructions(skills_dir):
70
+ """The extra system-prompt block when the skills feature is enabled.
71
+
72
+ Tells the agent it has a local ``.mini`` directory of reusable skills and
73
+ explains the four skill tools (see :mod:`mini_code_cli.skills`). ``skills_dir``
74
+ is the actual ``.mini`` path so the prompt can point at it concretely.
75
+ """
76
+ from . import skills as skills_mod
77
+ return (
78
+ "SKILLS MODE: You can work with reusable skills kept in the local "
79
+ f"`.mini` directory at: {skills_mod.skills_dir(skills_dir)}.\n"
80
+ "Each skill is a sub-directory containing a `skill.md` (what the skill is "
81
+ "/ how to use it) and optional executable Python helper scripts. There is "
82
+ "also a high-level `SKILLS.md` summary of all skills.\n"
83
+ "Use these tools to work with skills:\n"
84
+ " * `list_skills` - see which skills exist (name + one-line summary).\n"
85
+ " * `read_skill` - read a skill's `skill.md` by name, to learn how to "
86
+ "use it or which scripts it provides.\n"
87
+ " * `create_skill` - create a new skill (its sub-directory and `skill.md`) "
88
+ "when you build a reusable capability. Give a clear, single-segment name "
89
+ "and a short summary. It is normal to then add Python helper scripts into "
90
+ "the skill's directory.\n"
91
+ " * `run_skill` - run one of a skill's Python scripts, passing any "
92
+ "command-line arguments via its `args`. Read the skill first to know "
93
+ "which script to run and what arguments it expects.\n"
94
+ "Prefer turning reusable helpers into skills (with a `skill.md` plus "
95
+ "scripts) so they can be reused later, rather than repeating one-off work.\n"
96
+ )
97
+
98
+
65
99
  def compaction_prompt(messages):
66
100
  """
67
101
  Takes a messages list and returns a compact prompt string summarizing the conversation.