aes-cli 0.7.0__tar.gz → 0.12.0__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 (99) hide show
  1. {aes_cli-0.7.0 → aes_cli-0.12.0}/PKG-INFO +1 -1
  2. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/__init__.py +1 -1
  3. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/__main__.py +9 -2
  4. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/commands/init.py +104 -0
  5. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/commands/inspect.py +44 -1
  6. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/commands/sync.py +88 -0
  7. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/config.py +12 -0
  8. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/domains.py +39 -12
  9. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/global_config.py +17 -0
  10. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/i18n/_messages.py +1 -1
  11. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/i18n/ja.py +1 -1
  12. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/registry.py +4 -0
  13. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/agent.yaml.jinja +2 -2
  14. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/bom.yaml.jinja +1 -1
  15. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/instructions.md.jinja +6 -4
  16. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/ja/instructions.md.jinja +2 -0
  17. aes_cli-0.12.0/aes/scaffold/ja/memory_command.md.jinja +195 -0
  18. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/ja/operations.md.jinja +5 -4
  19. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/ja/orchestrator.md.jinja +4 -3
  20. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/ja/setup.md.jinja +20 -2
  21. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/ja/workflow_command.md.jinja +8 -5
  22. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/local.example.yaml.jinja +2 -0
  23. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/local.yaml.jinja +2 -0
  24. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/memory_command.md.jinja +98 -2
  25. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/operations.md.jinja +5 -4
  26. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/orchestrator.md.jinja +4 -3
  27. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/permissions.yaml.jinja +2 -2
  28. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/setup.md.jinja +71 -9
  29. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/skill.yaml.jinja +2 -2
  30. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/workflow.yaml.jinja +1 -1
  31. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/workflow_command.md.jinja +8 -5
  32. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/schemas/agent.schema.json +20 -0
  33. aes_cli-0.12.0/aes/schemas/instinct.schema.json +137 -0
  34. aes_cli-0.12.0/aes/schemas/learning-config.schema.json +129 -0
  35. aes_cli-0.12.0/aes/schemas/lifecycle.schema.json +196 -0
  36. aes_cli-0.12.0/aes/schemas/rules-config.schema.json +55 -0
  37. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/targets/__init__.py +2 -0
  38. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/targets/_base.py +5 -0
  39. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/targets/_composer.py +246 -2
  40. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/targets/claude.py +51 -13
  41. aes_cli-0.12.0/aes/targets/codex.py +138 -0
  42. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/targets/copilot.py +30 -1
  43. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/targets/cursor.py +30 -1
  44. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/targets/openclaw.py +64 -5
  45. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/targets/windsurf.py +30 -1
  46. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/validator.py +83 -1
  47. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes_cli.egg-info/PKG-INFO +1 -1
  48. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes_cli.egg-info/SOURCES.txt +10 -0
  49. {aes_cli-0.7.0 → aes_cli-0.12.0}/pyproject.toml +1 -1
  50. aes_cli-0.12.0/tests/test_codex_target.py +304 -0
  51. {aes_cli-0.7.0 → aes_cli-0.12.0}/tests/test_init.py +1 -1
  52. aes_cli-0.12.0/tests/test_learning_validation.py +344 -0
  53. aes_cli-0.12.0/tests/test_lifecycle_validation.py +234 -0
  54. aes_cli-0.12.0/tests/test_project_locale.py +94 -0
  55. aes_cli-0.12.0/tests/test_rules_validation.py +146 -0
  56. {aes_cli-0.7.0 → aes_cli-0.12.0}/tests/test_sync.py +6 -6
  57. aes_cli-0.7.0/aes/scaffold/ja/memory_command.md.jinja +0 -99
  58. {aes_cli-0.7.0 → aes_cli-0.12.0}/README.md +0 -0
  59. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/analyzer.py +0 -0
  60. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/commands/__init__.py +0 -0
  61. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/commands/bom.py +0 -0
  62. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/commands/install.py +0 -0
  63. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/commands/publish.py +0 -0
  64. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/commands/search.py +0 -0
  65. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/commands/status.py +0 -0
  66. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/commands/upgrade.py +0 -0
  67. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/commands/validate.py +0 -0
  68. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/frameworks.py +0 -0
  69. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/i18n/__init__.py +0 -0
  70. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/i18n/domains_ja.py +0 -0
  71. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/mcp_server.py +0 -0
  72. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/migrations.py +0 -0
  73. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/agentignore.jinja +0 -0
  74. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/ja/skill.md.jinja +0 -0
  75. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/scaffold/skill.md.jinja +0 -0
  76. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/schemas/bom.schema.json +0 -0
  77. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/schemas/decision-record.schema.json +0 -0
  78. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/schemas/permissions.schema.json +0 -0
  79. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/schemas/registry.schema.json +0 -0
  80. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/schemas/skill.schema.json +0 -0
  81. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes/schemas/workflow.schema.json +0 -0
  82. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes_cli.egg-info/dependency_links.txt +0 -0
  83. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes_cli.egg-info/entry_points.txt +0 -0
  84. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes_cli.egg-info/requires.txt +0 -0
  85. {aes_cli-0.7.0 → aes_cli-0.12.0}/aes_cli.egg-info/top_level.txt +0 -0
  86. {aes_cli-0.7.0 → aes_cli-0.12.0}/setup.cfg +0 -0
  87. {aes_cli-0.7.0 → aes_cli-0.12.0}/tests/test_analyzer.py +0 -0
  88. {aes_cli-0.7.0 → aes_cli-0.12.0}/tests/test_bom.py +0 -0
  89. {aes_cli-0.7.0 → aes_cli-0.12.0}/tests/test_frameworks.py +0 -0
  90. {aes_cli-0.7.0 → aes_cli-0.12.0}/tests/test_inspect.py +0 -0
  91. {aes_cli-0.7.0 → aes_cli-0.12.0}/tests/test_install.py +0 -0
  92. {aes_cli-0.7.0 → aes_cli-0.12.0}/tests/test_mcp_server.py +0 -0
  93. {aes_cli-0.7.0 → aes_cli-0.12.0}/tests/test_openclaw_target.py +0 -0
  94. {aes_cli-0.7.0 → aes_cli-0.12.0}/tests/test_publish.py +0 -0
  95. {aes_cli-0.7.0 → aes_cli-0.12.0}/tests/test_registry.py +0 -0
  96. {aes_cli-0.7.0 → aes_cli-0.12.0}/tests/test_search.py +0 -0
  97. {aes_cli-0.7.0 → aes_cli-0.12.0}/tests/test_status.py +0 -0
  98. {aes_cli-0.7.0 → aes_cli-0.12.0}/tests/test_upgrade.py +0 -0
  99. {aes_cli-0.7.0 → aes_cli-0.12.0}/tests/test_validate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aes-cli
3
- Version: 0.7.0
3
+ Version: 0.12.0
4
4
  Summary: CLI tool for the Agentic Engineering Standard
5
5
  Author: Hiro
6
6
  License: Apache-2.0
@@ -2,4 +2,4 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.7.0"
5
+ __version__ = "0.12.0"
@@ -53,7 +53,7 @@ def cli(lang: Optional[str] = None) -> None:
53
53
  """
54
54
  from aes.i18n import init_locale
55
55
 
56
- # Priority: --lang flag > AES_LANG env > ~/.aes/config.yaml > first-run prompt
56
+ # Priority: --lang flag > AES_LANG env > .agent/local.yaml > ~/.aes/config.yaml > first-run prompt
57
57
  if lang:
58
58
  init_locale(lang)
59
59
  return
@@ -63,7 +63,14 @@ def cli(lang: Optional[str] = None) -> None:
63
63
  init_locale(env_lang)
64
64
  return
65
65
 
66
- from aes.global_config import get_locale
66
+ from pathlib import Path
67
+ from aes.global_config import get_locale, get_project_locale
68
+
69
+ project_locale = get_project_locale(Path.cwd())
70
+ if project_locale:
71
+ init_locale(project_locale)
72
+ return
73
+
67
74
  saved = get_locale()
68
75
  if saved is None and sys.stdin.isatty():
69
76
  _prompt_language()
@@ -22,9 +22,17 @@ from aes.config import (
22
22
  AGENTIGNORE_FILE,
23
23
  BOM_FILE,
24
24
  DECISIONS_DIR,
25
+ INSTINCTS_DIR,
26
+ LEARNING_CONFIG_FILE,
27
+ LEARNING_DIR,
28
+ LIFECYCLE_FILE,
25
29
  LOCAL_EXAMPLE_FILE,
26
30
  LOCAL_FILE,
31
+ LOGS_DIR,
32
+ RULES_CONFIG_FILE,
33
+ RULES_DIR,
27
34
  SCAFFOLD_DIR,
35
+ SCRIPTS_DIR,
28
36
  SKILLS_DIR,
29
37
  REGISTRY_DIR,
30
38
  WORKFLOWS_DIR,
@@ -451,6 +459,8 @@ def _print_post_init_summary(
451
459
  ("Cursor", ".cursorrules"),
452
460
  ("Copilot", ".github/copilot-instructions.md"),
453
461
  ("Windsurf", ".windsurfrules"),
462
+ ("Codex", "AGENTS.md"),
463
+ ("OpenClaw", ".openclaw/openclaw.json"),
454
464
  ]
455
465
  for tool_name, file_name in sync_targets:
456
466
  if (project_root / file_name).exists():
@@ -735,6 +745,100 @@ def init_cmd(
735
745
  content = _render_template(env, "operations.md.jinja", ops_context)
736
746
  (agent_dir / MEMORY_DIR / "operations.md").write_text(content)
737
747
 
748
+ # Lifecycle hooks (all domains)
749
+ if domain_config.scaffold_lifecycle:
750
+ (agent_dir / SCRIPTS_DIR).mkdir(exist_ok=True)
751
+ (agent_dir / LOGS_DIR).mkdir(exist_ok=True)
752
+ lifecycle_content = (
753
+ "# .agent/lifecycle.yaml — Lifecycle Hooks\n"
754
+ "apiVersion: aes/v1\n"
755
+ "kind: Lifecycle\n"
756
+ f"\nprofile: {domain_config.lifecycle_profile}\n"
757
+ "\nhooks:\n"
758
+ " on_session_start:\n"
759
+ " - name: restore-context\n"
760
+ " description: Load previous session summary into context\n"
761
+ " profile: minimal\n"
762
+ " action: script\n"
763
+ " command: node .agent/scripts/restore-context.js\n"
764
+ " timeout_seconds: 10\n"
765
+ " async: false\n"
766
+ " fail_strategy: warn\n"
767
+ "\n"
768
+ " on_session_end:\n"
769
+ " - name: persist-summary\n"
770
+ " description: Save session summary for next session\n"
771
+ " profile: minimal\n"
772
+ " action: script\n"
773
+ " command: node .agent/scripts/persist-summary.js\n"
774
+ " timeout_seconds: 15\n"
775
+ " async: true\n"
776
+ " fail_strategy: warn\n"
777
+ )
778
+ (agent_dir / LIFECYCLE_FILE).write_text(lifecycle_content)
779
+
780
+ # Continuous learning (agent-integrated domains only)
781
+ if domain_config.scaffold_learning:
782
+ (agent_dir / LEARNING_DIR).mkdir(exist_ok=True)
783
+ (agent_dir / INSTINCTS_DIR / "active").mkdir(parents=True, exist_ok=True)
784
+ (agent_dir / INSTINCTS_DIR / "candidates").mkdir(exist_ok=True)
785
+ (agent_dir / INSTINCTS_DIR / "archived").mkdir(exist_ok=True)
786
+ learning_content = (
787
+ "# .agent/learning/config.yaml\n"
788
+ "apiVersion: aes/v1\n"
789
+ "kind: LearningConfig\n"
790
+ "\nextraction:\n"
791
+ " enabled: true\n"
792
+ " auto_extract: true\n"
793
+ " min_session_length: 5\n"
794
+ " max_candidates_per_session: 3\n"
795
+ "\nconfidence:\n"
796
+ " initial_score: 0.4\n"
797
+ " promotion_threshold: 0.6\n"
798
+ " promotion_min_validations: 3\n"
799
+ " decay_rate_per_week: 0.01\n"
800
+ " min_score: 0.3\n"
801
+ "\ncontext_loading:\n"
802
+ " max_instincts_in_context: 10\n"
803
+ " sort_by: confidence_score\n"
804
+ " token_budget: 2000\n"
805
+ " format: compact\n"
806
+ )
807
+ (agent_dir / LEARNING_CONFIG_FILE).write_text(learning_content)
808
+
809
+ # Rules & conventions (all domains)
810
+ if domain_config.scaffold_rules:
811
+ (agent_dir / RULES_DIR).mkdir(exist_ok=True)
812
+ (agent_dir / RULES_DIR / "common").mkdir(exist_ok=True)
813
+ rules_content = (
814
+ "# .agent/rules/rules.yaml\n"
815
+ "apiVersion: aes/v1\n"
816
+ "kind: RulesConfig\n"
817
+ "\nloading:\n"
818
+ " always: [common]\n"
819
+ )
820
+ (agent_dir / RULES_CONFIG_FILE).write_text(rules_content)
821
+
822
+ # Starter rule
823
+ starter_rule = (
824
+ "---\n"
825
+ "name: testing\n"
826
+ "scope: common\n"
827
+ "priority: high\n"
828
+ "---\n\n"
829
+ "# Testing Standards\n\n"
830
+ "## Requirements\n"
831
+ "- All public functions must have unit tests\n"
832
+ "- No test should take longer than 5 seconds\n\n"
833
+ "## Patterns\n"
834
+ "- Use Arrange-Act-Assert structure\n"
835
+ "- Mock external dependencies\n\n"
836
+ "## Anti-patterns\n"
837
+ "- Never test implementation details\n"
838
+ "- Never share state between tests\n"
839
+ )
840
+ (agent_dir / RULES_DIR / "common" / "testing.md").write_text(starter_rule)
841
+
738
842
  # Auto-sync: generate tool-specific config files
739
843
  synced_files = run_sync(project_root, force=True, quiet=True)
740
844
  _write_mcp_config(project_root)
@@ -12,7 +12,11 @@ import yaml
12
12
  from rich.console import Console
13
13
  from rich.table import Table
14
14
 
15
- from aes.config import AGENT_DIR, BOM_FILE, DECISIONS_DIR
15
+ from aes.config import (
16
+ AGENT_DIR, BOM_FILE, DECISIONS_DIR,
17
+ INSTINCTS_DIR, LEARNING_CONFIG_FILE, LIFECYCLE_FILE,
18
+ RULES_CONFIG_FILE, RULES_DIR,
19
+ )
16
20
  from aes.i18n import t
17
21
  from aes.registry import (
18
22
  fetch_index,
@@ -246,6 +250,45 @@ def _inspect_local(path: str) -> None:
246
250
  console.print(f" {t('inspect.decisions_count', count=dr_count)}")
247
251
  console.print()
248
252
 
253
+ # Lifecycle hooks
254
+ lifecycle_path = agent_dir / LIFECYCLE_FILE
255
+ if lifecycle_path.exists():
256
+ lc = _load_yaml(lifecycle_path)
257
+ hooks = lc.get("hooks", {}) or {}
258
+ hook_count = sum(
259
+ len(v) if isinstance(v, list) else (1 if isinstance(v, dict) else 0)
260
+ for v in hooks.values()
261
+ )
262
+ profile = lc.get("profile", "standard")
263
+ console.print(f"[bold]Lifecycle[/]")
264
+ console.print(f" Profile: {profile} | Hooks: {hook_count}")
265
+ console.print()
266
+
267
+ # Learning / instincts
268
+ learning_config_path = agent_dir / LEARNING_CONFIG_FILE
269
+ if learning_config_path.exists():
270
+ lconf = _load_yaml(learning_config_path)
271
+ extraction = lconf.get("extraction", {}).get("enabled", True)
272
+ instincts_base = agent_dir / INSTINCTS_DIR
273
+ active = len(list((instincts_base / "active").glob("*.instinct.yaml"))) if (instincts_base / "active").exists() else 0
274
+ candidates = len(list((instincts_base / "candidates").glob("*.instinct.yaml"))) if (instincts_base / "candidates").exists() else 0
275
+ archived = len(list((instincts_base / "archived").glob("*.instinct.yaml"))) if (instincts_base / "archived").exists() else 0
276
+ console.print(f"[bold]Learning[/]")
277
+ console.print(f" Extraction: {'enabled' if extraction else 'disabled'} | Active: {active} | Candidates: {candidates} | Archived: {archived}")
278
+ console.print()
279
+
280
+ # Rules
281
+ rules_config_path = agent_dir / RULES_CONFIG_FILE
282
+ if rules_config_path.exists():
283
+ rconf = _load_yaml(rules_config_path)
284
+ languages = rconf.get("languages", [])
285
+ rules_base = agent_dir / RULES_DIR
286
+ rule_count = len(list(rules_base.glob("**/*.md"))) if rules_base.exists() else 0
287
+ lang_str = ", ".join(languages) if languages else "auto-detect"
288
+ console.print(f"[bold]Rules[/]")
289
+ console.print(f" Languages: {lang_str} | Rule files: {rule_count}")
290
+ console.print()
291
+
249
292
  # Summary
250
293
  console.print(f"[bold]{t('inspect.summary')}[/]")
251
294
  console.print(f" {t('inspect.skills_count', count=len(skills))}")
@@ -16,10 +16,15 @@ from rich.console import Console
16
16
  from aes.config import (
17
17
  AGENT_DIR,
18
18
  COMMANDS_DIR,
19
+ INSTINCTS_DIR,
20
+ LEARNING_CONFIG_FILE,
21
+ LIFECYCLE_FILE,
19
22
  LOCAL_FILE,
20
23
  MANIFEST_FILE,
21
24
  MEMORY_DIR,
22
25
  PERMISSIONS_FILE,
26
+ RULES_CONFIG_FILE,
27
+ RULES_DIR,
23
28
  SKILLS_DIR,
24
29
  )
25
30
  from aes.i18n import t
@@ -224,6 +229,84 @@ def _load_agent_context(project_root: Path) -> AgentContext:
224
229
  if local_perms and permissions:
225
230
  permissions = _deep_merge(permissions, local_perms)
226
231
 
232
+ # Load lifecycle.yaml
233
+ lifecycle: Optional[dict] = None
234
+ lifecycle_path = agent_dir / LIFECYCLE_FILE
235
+ if lifecycle_path.exists():
236
+ with open(lifecycle_path) as f:
237
+ lifecycle = yaml.safe_load(f) or {}
238
+
239
+ # Load learning config and active instincts
240
+ learning_config: Optional[dict] = None
241
+ active_instincts: List[dict] = []
242
+ learning_config_path = agent_dir / LEARNING_CONFIG_FILE
243
+ if learning_config_path.exists():
244
+ with open(learning_config_path) as f:
245
+ learning_config = yaml.safe_load(f) or {}
246
+
247
+ instincts_active_dir = agent_dir / INSTINCTS_DIR / "active"
248
+ if instincts_active_dir.exists():
249
+ max_instincts = 10
250
+ if learning_config:
251
+ max_instincts = (
252
+ learning_config.get("context_loading", {})
253
+ .get("max_instincts_in_context", 10)
254
+ )
255
+ raw_instincts = []
256
+ for inst_file in sorted(instincts_active_dir.glob("*.instinct.yaml")):
257
+ with open(inst_file) as f:
258
+ inst_data = yaml.safe_load(f)
259
+ if inst_data:
260
+ raw_instincts.append(inst_data)
261
+ # Sort by confidence score descending
262
+ raw_instincts.sort(
263
+ key=lambda i: i.get("confidence", {}).get("score", 0),
264
+ reverse=True,
265
+ )
266
+ active_instincts = raw_instincts[:max_instincts]
267
+
268
+ # Load rules config and rule files
269
+ rules_config: Optional[dict] = None
270
+ rules_files: Dict[str, str] = {}
271
+ rules_config_path = agent_dir / RULES_CONFIG_FILE
272
+ if rules_config_path.exists():
273
+ with open(rules_config_path) as f:
274
+ rules_config = yaml.safe_load(f) or {}
275
+
276
+ rules_base = agent_dir / RULES_DIR
277
+ overrides = rules_config.get("overrides", {})
278
+
279
+ # Determine which language directories to load
280
+ languages = rules_config.get("languages", [])
281
+ if not languages:
282
+ # Auto-detect from project root
283
+ detection = rules_config.get("detection", {})
284
+ for lang, patterns in detection.items():
285
+ for pattern in patterns:
286
+ if list(project_root.glob(pattern)):
287
+ languages.append(lang)
288
+ break
289
+
290
+ # Always-load directories (default: common)
291
+ always = rules_config.get("loading", {}).get("always", ["common"])
292
+ dirs_to_load = list(always) + languages
293
+
294
+ for dir_name in dirs_to_load:
295
+ rule_dir = rules_base / dir_name
296
+ if not rule_dir.exists():
297
+ continue
298
+ for md_file in sorted(rule_dir.glob("*.md")):
299
+ content = md_file.read_text()
300
+ # Resolve ${variable} placeholders from overrides
301
+ rule_name = md_file.stem
302
+ rule_overrides = overrides.get(rule_name, {})
303
+ for var_name, var_value in rule_overrides.items():
304
+ content = content.replace(
305
+ f"${{{var_name}}}", str(var_value)
306
+ )
307
+ key = f"{dir_name}/{md_file.name}"
308
+ rules_files[key] = content
309
+
227
310
  return AgentContext(
228
311
  project_root=project_root,
229
312
  agent_dir=agent_dir,
@@ -236,6 +319,11 @@ def _load_agent_context(project_root: Path) -> AgentContext:
236
319
  memory_project=memory_project,
237
320
  skill_metadata=skill_metadata,
238
321
  local_config=local_config,
322
+ lifecycle=lifecycle,
323
+ learning_config=learning_config,
324
+ active_instincts=active_instincts,
325
+ rules_config=rules_config,
326
+ rules_files=rules_files,
239
327
  )
240
328
 
241
329
 
@@ -34,6 +34,14 @@ OVERRIDES_DIR = "overrides"
34
34
  LOCAL_FILE = "local.yaml"
35
35
  LOCAL_EXAMPLE_FILE = "local.example.yaml"
36
36
  BOM_FILE = "bom.yaml"
37
+ LIFECYCLE_FILE = "lifecycle.yaml"
38
+ LEARNING_DIR = "learning"
39
+ LEARNING_CONFIG_FILE = "learning/config.yaml"
40
+ INSTINCTS_DIR = "learning/instincts"
41
+ RULES_DIR = "rules"
42
+ RULES_CONFIG_FILE = "rules/rules.yaml"
43
+ SCRIPTS_DIR = "scripts"
44
+ LOGS_DIR = "logs"
37
45
 
38
46
  # Schema file mapping
39
47
  SCHEMA_MAP = {
@@ -44,4 +52,8 @@ SCHEMA_MAP = {
44
52
  "permissions": "permissions.schema.json",
45
53
  "bom": "bom.schema.json",
46
54
  "decision-record": "decision-record.schema.json",
55
+ "lifecycle": "lifecycle.schema.json",
56
+ "instinct": "instinct.schema.json",
57
+ "learning-config": "learning-config.schema.json",
58
+ "rules-config": "rules-config.schema.json",
47
59
  }
@@ -154,6 +154,12 @@ class DomainConfig:
154
154
  heartbeat_checklist: str = ""
155
155
  channels: Dict[str, Dict[str, str]] = field(default_factory=dict)
156
156
 
157
+ # Lifecycle / Learning / Rules scaffolding
158
+ scaffold_lifecycle: bool = True
159
+ scaffold_learning: bool = False # True for agent-integrated domains
160
+ scaffold_rules: bool = True
161
+ lifecycle_profile: str = "standard"
162
+
157
163
 
158
164
  # ---------------------------------------------------------------------------
159
165
  # ML domain config — drawn from examples/ml-pipeline
@@ -529,6 +535,7 @@ ML_CONFIG = DomainConfig(
529
535
  {"name": "OPTUNA_TIMEOUT", "default": "300", "description": "Seconds per model for HPO"},
530
536
  {"name": "OPTUNA_N_TRIALS", "default": "50", "description": "Max trials per model"},
531
537
  ],
538
+ scaffold_learning=True,
532
539
  )
533
540
 
534
541
 
@@ -1338,6 +1345,7 @@ RESEARCH_CONFIG = DomainConfig(
1338
1345
  {"name": "SEMANTIC_SCHOLAR_API_KEY", "default": "", "description": "Semantic Scholar API key (optional, increases rate limits)"},
1339
1346
  {"name": "MAX_ITEMS_PER_RUN", "default": "50", "description": "Maximum items to process per pipeline run"},
1340
1347
  ],
1348
+ scaffold_learning=True,
1341
1349
  )
1342
1350
 
1343
1351
 
@@ -1387,6 +1395,7 @@ AGENT_INTEGRATED_BASE_CONFIG = DomainConfig(
1387
1395
  ),
1388
1396
  ],
1389
1397
  instructions_description="", # empty -> falls through to TODO scaffolding in template
1398
+ scaffold_learning=True,
1390
1399
  )
1391
1400
 
1392
1401
 
@@ -1476,23 +1485,40 @@ _ASSISTANT_SKILLS = [
1476
1485
  ),
1477
1486
  ]
1478
1487
 
1479
- _ASSISTANT_CONVERSE_COMMAND = CommandDef(
1480
- id="converse",
1481
- trigger="/converse",
1482
- description="Start or resume a conversation session with the assistant",
1483
- runbook_purpose="Run the assistant's main conversational loop. The agent listens across configured channels, responds to messages, executes skills on demand, and proactively checks heartbeat tasks.",
1484
- worker_specialty="Multi-platform conversational AI assistant",
1488
+ _ASSISTANT_BUILD_COMMAND = CommandDef(
1489
+ id="build",
1490
+ trigger="/build",
1491
+ description="Build the assistant project from scratch: identity, channels, skills, integrations, permissions, tests",
1492
+ runbook_purpose="Construct the complete assistant project. The agent sets up project structure, defines identity and model config, wires channel integrations, implements skills, configures security and permissions, and verifies with tests.",
1493
+ worker_specialty="Constructing assistant projects — identity, channels, skills, integrations",
1494
+ runbook_phases=[
1495
+ {"title": "Project Structure", "content": "Create directory layout matching the AES assistant convention: skills/, config/, scripts/, tests/. Set up pyproject.toml, environment files, and dependency management."},
1496
+ {"title": "Identity & Model", "content": "Configure identity (persona, name, emoji) in agent.yaml. Set up model provider, API key environment variables, and sandbox settings. Create USER.md template for user profile."},
1497
+ {"title": "Channel Integration", "content": "Wire messaging channels (Telegram, Discord, etc.). Set up bot token environment variables, configure channel-specific settings, and implement message routing stubs."},
1498
+ {"title": "Skill Wiring", "content": "Implement skill manifests and runbooks for each configured skill. Wire MCP servers if needed. Set up auto-activation rules and skill dependencies."},
1499
+ {"title": "Security & Permissions", "content": "Configure permissions.yaml with shell confirmation rules, action confirmations, and channel-specific security policies. Set up credential management via environment variables."},
1500
+ {"title": "Tests & Verification", "content": "Write unit tests for skill execution, channel routing, and permission checks. Add integration test that validates the full config. Verify all environment variables are documented."},
1501
+ ],
1502
+ )
1503
+
1504
+ _ASSISTANT_RUN_COMMAND = CommandDef(
1505
+ id="run",
1506
+ trigger="/run",
1507
+ description="Start the autonomous assistant loop: initialize, monitor channels, execute skills, maintain heartbeat",
1508
+ runbook_purpose="Run the assistant's autonomous agent loop. The agent initializes from stored state, monitors connected channels for messages, executes skills on demand or automatically, persists learnings to memory, and maintains the heartbeat cycle for proactive tasks.",
1509
+ worker_specialty="Running autonomous assistant agents — channel monitoring, skill execution, heartbeat maintenance",
1485
1510
  runbook_phases=[
1486
- {"title": "Session Setup", "content": "Load user profile from USER.md. Check HEARTBEAT.md for pending tasks. Review recent conversation history for context continuity."},
1487
- {"title": "Message Processing", "content": "Receive messages from connected channels. Determine intent — is this a greeting, a question, a skill invocation, or a general conversation? Route accordingly."},
1488
- {"title": "Skill Execution", "content": "When a skill is triggered (explicitly via command or automatically via context match), execute it and return results to the user."},
1489
- {"title": "Memory & Wrap-up", "content": "Persist important learnings to MEMORY.md. Update AGENTS.md if the user's preferences or context changed."},
1511
+ {"title": "Initialization", "content": "Load user profile from USER.md. Restore session state from MEMORY.md. Check HEARTBEAT.md for pending tasks. Review recent activity log for context continuity."},
1512
+ {"title": "Channel Monitoring", "content": "Connect to configured channels (Telegram, Discord, etc.). Listen for incoming messages. Determine intent — greeting, skill invocation, general conversation, or system command. Route accordingly."},
1513
+ {"title": "Skill Execution", "content": "When a skill is triggered (explicitly via command or automatically via context match), execute it within permission boundaries. Return results to the originating channel. Handle errors gracefully with user-facing messages."},
1514
+ {"title": "Memory & Learning", "content": "Persist important learnings to MEMORY.md. Update active instincts based on interaction patterns. Record conversation context for future sessions."},
1515
+ {"title": "Heartbeat & Maintenance", "content": "Execute heartbeat checklist at configured intervals. Proactively surface important updates (calendar, PRs, unread messages). Respect quiet hours. Log heartbeat results to activity log."},
1490
1516
  ],
1491
1517
  )
1492
1518
 
1493
1519
  ASSISTANT_CONFIG = DomainConfig(
1494
1520
  mode="agent-integrated",
1495
- workflow_commands=[_ASSISTANT_CONVERSE_COMMAND],
1521
+ workflow_commands=[_ASSISTANT_BUILD_COMMAND, _ASSISTANT_RUN_COMMAND],
1496
1522
 
1497
1523
  # Identity defaults for scaffold
1498
1524
  identity_persona=(
@@ -1518,7 +1544,7 @@ ASSISTANT_CONFIG = DomainConfig(
1518
1544
  "discord": {"enabled": "true", "bot_token_env": "DISCORD_BOT_TOKEN"},
1519
1545
  },
1520
1546
 
1521
- instructions_description="Personal AI assistant connected to messaging platforms via OpenClaw. Runs 24/7, responds across channels, and executes skills on demand.",
1547
+ instructions_description="Autonomous AI assistant powered by OpenClaw. Runs 24/7, monitors channels, executes skills, and maintains proactive heartbeat tasks.",
1522
1548
  instructions_quick_ref=(
1523
1549
  "```bash\n"
1524
1550
  "aes sync -t openclaw # generate .openclaw/ config\n"
@@ -1571,6 +1597,7 @@ ASSISTANT_CONFIG = DomainConfig(
1571
1597
  {"name": "TELEGRAM_BOT_TOKEN", "description": "Bot token for Telegram integration"},
1572
1598
  {"name": "DISCORD_BOT_TOKEN", "description": "Bot token for Discord integration"},
1573
1599
  ],
1600
+ scaffold_learning=True,
1574
1601
  )
1575
1602
 
1576
1603
 
@@ -27,6 +27,23 @@ def save_global_config(config: dict) -> None:
27
27
  yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
28
28
 
29
29
 
30
+ def get_project_locale(project_path: Path) -> Optional[str]:
31
+ """Return locale from .agent/local.yaml in project, or None."""
32
+ local_yaml = project_path / ".agent" / "local.yaml"
33
+ if not local_yaml.exists():
34
+ return None
35
+ try:
36
+ with open(local_yaml) as f:
37
+ data = yaml.safe_load(f)
38
+ if isinstance(data, dict):
39
+ locale = data.get("locale")
40
+ if isinstance(locale, str) and locale:
41
+ return locale
42
+ except (OSError, yaml.YAMLError):
43
+ pass
44
+ return None
45
+
46
+
30
47
  def get_locale() -> Optional[str]:
31
48
  """Return configured locale or None."""
32
49
  return load_global_config().get("locale")
@@ -178,7 +178,7 @@ MESSAGES: dict = {
178
178
  "init.type_skip": "Skip",
179
179
  "init.type_ml": "ML pipeline",
180
180
  "init.type_research": "Research / Content pipeline",
181
- "init.type_assistant": "Assistant (24/7 AI on messaging platforms)",
181
+ "init.type_assistant": "Assistant (24/7 autonomous agent)",
182
182
  "init.type_custom": "Custom",
183
183
  "init.overwrite_warning": "{agent_dir}/ already exists at {path}",
184
184
  "init.overwrite_confirm": "Overwrite existing files?",
@@ -178,7 +178,7 @@ MESSAGES: dict = {
178
178
  "init.type_skip": "スキップ",
179
179
  "init.type_ml": "ML パイプライン",
180
180
  "init.type_research": "研究 / コンテンツパイプライン",
181
- "init.type_assistant": "アシスタント(24時間メッセージングAI)",
181
+ "init.type_assistant": "アシスタント(24時間自律エージェント)",
182
182
  "init.type_custom": "カスタム",
183
183
  "init.overwrite_warning": "{path} に {agent_dir}/ が既に存在します",
184
184
  "init.overwrite_confirm": "既存のファイルを上書きしますか?",
@@ -15,6 +15,7 @@ from typing import Dict, List, Optional, Tuple
15
15
  REGISTRY_URL = os.environ.get("AES_REGISTRY_URL", "https://registry.aes-official.com")
16
16
  INDEX_PATH = "index.json"
17
17
  PACKAGES_PATH = "packages"
18
+ _USER_AGENT = "aes-cli/0.11.0"
18
19
 
19
20
 
20
21
  def _validate_registry_url(url: str) -> str:
@@ -131,6 +132,7 @@ def fetch_index(registry_url: Optional[str] = None) -> dict:
131
132
  url = f"{base.rstrip('/')}/{INDEX_PATH}"
132
133
 
133
134
  req = urllib.request.Request(url)
135
+ req.add_header("User-Agent", _USER_AGENT)
134
136
  token = os.environ.get("AES_REGISTRY_KEY")
135
137
  if token:
136
138
  req.add_header("Authorization", f"Bearer {token}")
@@ -158,6 +160,7 @@ def download_package(
158
160
  tarball_path = dest / f"{name}-{version}.tar.gz"
159
161
 
160
162
  req = urllib.request.Request(url)
163
+ req.add_header("User-Agent", _USER_AGENT)
161
164
  token = os.environ.get("AES_REGISTRY_KEY")
162
165
  if token:
163
166
  req.add_header("Authorization", f"Bearer {token}")
@@ -206,6 +209,7 @@ def upload_package(
206
209
  # Upload tarball — server auto-updates index from X-AES-* headers
207
210
  upload_url = f"{base.rstrip('/')}/{PACKAGES_PATH}/{name}/{version}.tar.gz"
208
211
  req = urllib.request.Request(upload_url, data=tarball_data, method="PUT")
212
+ req.add_header("User-Agent", _USER_AGENT)
209
213
  req.add_header("Authorization", f"Bearer {token}")
210
214
  req.add_header("Content-Type", "application/gzip")
211
215
  req.add_header("X-AES-Description", description)
@@ -1,6 +1,6 @@
1
1
  {% if domain_config %}
2
2
  # .agent/agent.yaml — AES Manifest
3
- aes: "1.2"
3
+ aes: "1.4"
4
4
 
5
5
  name: "{{ name }}"
6
6
  version: "0.1.0"
@@ -121,7 +121,7 @@ environment:
121
121
  {% endif %}
122
122
  {% else %}
123
123
  # .agent/agent.yaml — AES Manifest
124
- aes: "1.2"
124
+ aes: "1.4"
125
125
 
126
126
  name: "{{ name }}"
127
127
  version: "0.1.0"
@@ -1,7 +1,7 @@
1
1
  # .agent/bom.yaml — Agent Bill of Materials (AI-BOM)
2
2
  # Tracks all components this agent depends on.
3
3
  # See: https://aes-official.com/spec/11-bom
4
- aes_bom: "1.2"
4
+ aes_bom: "1.4"
5
5
 
6
6
  models: []
7
7
  # - name: "claude-sonnet-4-20250514"
@@ -20,6 +20,7 @@
20
20
  {% for rule in domain_config.instructions_rules %}
21
21
  {{ loop.index }}. {{ rule }}
22
22
  {% endfor %}
23
+ {{ domain_config.instructions_rules|length + 1 }}. **Memory discipline** — update `.agent/memory/operations.md` after every significant action. Run `/memory` at session end. Next session starts blind without this.
23
24
 
24
25
  ## Primary Workflow
25
26
 
@@ -51,7 +52,7 @@ When creating or editing `.agent/` files, follow these formats exactly.
51
52
  ### Skill manifest (`.skill.yaml`)
52
53
 
53
54
  ```yaml
54
- aes_skill: "1.2"
55
+ aes_skill: "1.4"
55
56
 
56
57
  id: "my-skill"
57
58
  name: "My Skill"
@@ -100,7 +101,7 @@ tags:
100
101
  ### Workflow (`.yaml`)
101
102
 
102
103
  ```yaml
103
- aes_workflow: "1.2"
104
+ aes_workflow: "1.4"
104
105
 
105
106
  id: "my-workflow"
106
107
  entity: "item"
@@ -165,6 +166,7 @@ Key rules: `aes_skill`/`aes_workflow` version key is **required**. `inputs` is a
165
166
  Example: "Python 3.9+ — use `from __future__ import annotations` everywhere." -->
166
167
 
167
168
  1. **Fail graceful** — Each item wrapped in try/except, log error, continue.
169
+ 2. **Memory discipline** — update `.agent/memory/operations.md` after every significant action. Run `/memory` at session end.
168
170
 
169
171
  ## Domain Model
170
172
 
@@ -226,7 +228,7 @@ When creating or editing `.agent/` files, follow these formats exactly.
226
228
  ### Skill manifest (`.skill.yaml`)
227
229
 
228
230
  ```yaml
229
- aes_skill: "1.2"
231
+ aes_skill: "1.4"
230
232
 
231
233
  id: "my-skill"
232
234
  name: "My Skill"
@@ -275,7 +277,7 @@ tags:
275
277
  ### Workflow (`.yaml`)
276
278
 
277
279
  ```yaml
278
- aes_workflow: "1.2"
280
+ aes_workflow: "1.4"
279
281
 
280
282
  id: "my-workflow"
281
283
  entity: "item"
@@ -20,6 +20,7 @@
20
20
  {% for rule in domain_config.instructions_rules %}
21
21
  {{ loop.index }}. {{ rule }}
22
22
  {% endfor %}
23
+ {{ domain_config.instructions_rules|length + 1 }}. **メモリ規律** — 重要なアクションの後に `.agent/memory/operations.md` を必ず更新してください。セッション終了時に `/memory` を実行してください。これを怠ると次のセッションが盲目的に開始されます。
23
24
 
24
25
  ## 主要ワークフロー
25
26
 
@@ -165,6 +166,7 @@ idempotency:
165
166
  例: "Python 3.9+ — `from __future__ import annotations` を全ファイルで使用。" -->
166
167
 
167
168
  1. **グレースフルフェイル** — 各アイテムをtry/exceptでラップし、エラーをログして続行。
169
+ 2. **メモリ規律** — 重要なアクションの後に `.agent/memory/operations.md` を必ず更新。セッション終了時に `/memory` を実行。
168
170
 
169
171
  ## ドメインモデル
170
172