@softspark/ai-toolkit 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +1 -1
- package/app/.claude-plugin/plugin.json +1 -1
- package/bin/ai-toolkit.js +13 -0
- package/kb/{planning/enterprise-config-inheritance-plan.md → history/completed/enterprise-config-inheritance-plan-20260412.md} +39 -37
- package/kb/reference/enterprise-config-guide.md +329 -0
- package/llms-full.txt +2518 -2181
- package/llms.txt +2 -1
- package/manifest.json +9 -1
- package/package.json +1 -1
- package/scripts/config_cli.py +537 -0
- package/scripts/config_lock.py +154 -0
- package/scripts/config_merger.py +455 -0
- package/scripts/config_resolver.py +507 -0
- package/scripts/config_scaffold.py +266 -0
- package/scripts/config_validator.py +389 -0
- package/scripts/install.py +149 -1
- package/scripts/install_steps/ai_tools.py +101 -1
- package/scripts/install_steps/install_state.py +24 -0
- package/scripts/schemas/ai-toolkit-config.schema.json +163 -0
package/scripts/install.py
CHANGED
|
@@ -58,6 +58,16 @@ from install_steps.install_state import (
|
|
|
58
58
|
)
|
|
59
59
|
from install_steps.detect_language import detect_languages
|
|
60
60
|
|
|
61
|
+
# Config inheritance (extends system)
|
|
62
|
+
from config_resolver import (
|
|
63
|
+
ConfigResolverError,
|
|
64
|
+
load_project_config,
|
|
65
|
+
resolve_extends,
|
|
66
|
+
)
|
|
67
|
+
from config_merger import ConfigMergeError, merge_config_chain
|
|
68
|
+
from config_validator import validate_project_config
|
|
69
|
+
from config_lock import save_lock_file
|
|
70
|
+
|
|
61
71
|
|
|
62
72
|
# ---------------------------------------------------------------------------
|
|
63
73
|
# Manifest helpers
|
|
@@ -199,6 +209,8 @@ def parse_args(argv: list[str]) -> dict:
|
|
|
199
209
|
"status": False,
|
|
200
210
|
"lang": "",
|
|
201
211
|
"editors": "",
|
|
212
|
+
"config": "",
|
|
213
|
+
"refresh_base": False,
|
|
202
214
|
}
|
|
203
215
|
i = 0
|
|
204
216
|
while i < len(argv):
|
|
@@ -248,6 +260,13 @@ def parse_args(argv: list[str]) -> dict:
|
|
|
248
260
|
elif arg == "--editors":
|
|
249
261
|
i += 1
|
|
250
262
|
cfg["editors"] = argv[i] if i < len(argv) else ""
|
|
263
|
+
elif arg.startswith("--config="):
|
|
264
|
+
cfg["config"] = arg.split("=", 1)[1]
|
|
265
|
+
elif arg == "--config":
|
|
266
|
+
i += 1
|
|
267
|
+
cfg["config"] = argv[i] if i < len(argv) else ""
|
|
268
|
+
elif arg == "--refresh-base":
|
|
269
|
+
cfg["refresh_base"] = True
|
|
251
270
|
elif arg.startswith("-"):
|
|
252
271
|
print(f"Unknown option: {arg}")
|
|
253
272
|
sys.exit(1)
|
|
@@ -442,6 +461,117 @@ def install_strict_git_hooks(profile: str, local: bool, dry_run: bool) -> None:
|
|
|
442
461
|
# Version helper
|
|
443
462
|
# ---------------------------------------------------------------------------
|
|
444
463
|
|
|
464
|
+
def resolve_extends_config(
|
|
465
|
+
project_dir: Path,
|
|
466
|
+
config_path: str = "",
|
|
467
|
+
refresh: bool = False,
|
|
468
|
+
) -> dict | None:
|
|
469
|
+
"""Resolve .ai-toolkit.json extends and return merged config.
|
|
470
|
+
|
|
471
|
+
Returns None if no .ai-toolkit.json or no extends field.
|
|
472
|
+
Prints warnings/errors and exits on fatal errors.
|
|
473
|
+
"""
|
|
474
|
+
if config_path:
|
|
475
|
+
config_file = Path(config_path)
|
|
476
|
+
if not config_file.is_file():
|
|
477
|
+
print(f" Error: config file not found: {config_path}")
|
|
478
|
+
sys.exit(1)
|
|
479
|
+
import json as _json
|
|
480
|
+
with open(config_file, encoding="utf-8") as f:
|
|
481
|
+
project_config = _json.load(f)
|
|
482
|
+
config_root = config_file.parent
|
|
483
|
+
else:
|
|
484
|
+
project_config = load_project_config(project_dir)
|
|
485
|
+
config_root = project_dir
|
|
486
|
+
|
|
487
|
+
if project_config is None:
|
|
488
|
+
return None
|
|
489
|
+
|
|
490
|
+
# Validate project config schema
|
|
491
|
+
errors = validate_project_config(project_config, config_root)
|
|
492
|
+
if errors:
|
|
493
|
+
print(" Config validation errors:")
|
|
494
|
+
for e in errors:
|
|
495
|
+
print(f" ✗ {e}")
|
|
496
|
+
sys.exit(1)
|
|
497
|
+
|
|
498
|
+
extends = project_config.get("extends")
|
|
499
|
+
if not extends:
|
|
500
|
+
# Config without extends — just use its settings directly
|
|
501
|
+
return project_config
|
|
502
|
+
|
|
503
|
+
print(f" Resolving extends: {extends}...")
|
|
504
|
+
|
|
505
|
+
try:
|
|
506
|
+
result = resolve_extends(extends, config_root, refresh=refresh)
|
|
507
|
+
except ConfigResolverError as e:
|
|
508
|
+
print(f" ✗ Resolution failed: {e}")
|
|
509
|
+
sys.exit(1)
|
|
510
|
+
|
|
511
|
+
for w in result.warnings:
|
|
512
|
+
print(f" ⚠ {w}")
|
|
513
|
+
|
|
514
|
+
# Merge
|
|
515
|
+
try:
|
|
516
|
+
base_datas = [c.data for c in result.configs]
|
|
517
|
+
merge_result = merge_config_chain(base_datas, project_config)
|
|
518
|
+
except ConfigMergeError as e:
|
|
519
|
+
print(f" ✗ Merge failed: {e}")
|
|
520
|
+
sys.exit(1)
|
|
521
|
+
|
|
522
|
+
for c in result.configs:
|
|
523
|
+
version_str = f" v{c.version}" if c.version else ""
|
|
524
|
+
print(f" ✓ Resolved: {c.name}{version_str}")
|
|
525
|
+
|
|
526
|
+
# Attach resolution metadata for state.json recording
|
|
527
|
+
config_metas = [
|
|
528
|
+
{
|
|
529
|
+
"source": c.source,
|
|
530
|
+
"name": c.name,
|
|
531
|
+
"version": c.version,
|
|
532
|
+
"integrity": c.integrity,
|
|
533
|
+
"root": str(c.root),
|
|
534
|
+
}
|
|
535
|
+
for c in result.configs
|
|
536
|
+
]
|
|
537
|
+
merge_result.merged["_extends_meta"] = {
|
|
538
|
+
"source": extends,
|
|
539
|
+
"configs": config_metas,
|
|
540
|
+
"overrides_applied": merge_result.overrides_applied,
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
# Generate lock file
|
|
544
|
+
lock_path = save_lock_file(
|
|
545
|
+
config_root,
|
|
546
|
+
config_metas,
|
|
547
|
+
ai_toolkit_version=_get_toolkit_version(),
|
|
548
|
+
)
|
|
549
|
+
print(f" Saved: {lock_path.name}")
|
|
550
|
+
|
|
551
|
+
return merge_result.merged
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _apply_merged_config(
|
|
555
|
+
merged: dict,
|
|
556
|
+
cfg: dict,
|
|
557
|
+
) -> dict:
|
|
558
|
+
"""Apply merged config settings back into the install cfg dict.
|
|
559
|
+
|
|
560
|
+
Overrides profile and modules based on merged config.
|
|
561
|
+
Returns the modified cfg.
|
|
562
|
+
"""
|
|
563
|
+
# Profile from merged config
|
|
564
|
+
merged_profile = merged.get("profile")
|
|
565
|
+
if merged_profile and not cfg["profile"]:
|
|
566
|
+
cfg["profile"] = merged_profile
|
|
567
|
+
|
|
568
|
+
# Agents: filter based on merged enabled/disabled lists
|
|
569
|
+
# (stored for use by install_local_project)
|
|
570
|
+
cfg["_merged_config"] = merged
|
|
571
|
+
|
|
572
|
+
return cfg
|
|
573
|
+
|
|
574
|
+
|
|
445
575
|
def _get_toolkit_version() -> str:
|
|
446
576
|
"""Read version from package.json."""
|
|
447
577
|
pkg = toolkit_dir / "package.json"
|
|
@@ -522,10 +652,21 @@ def main() -> None:
|
|
|
522
652
|
|
|
523
653
|
if local:
|
|
524
654
|
# --local: project-local only, no global install
|
|
655
|
+
# Check for .ai-toolkit.json extends system
|
|
656
|
+
config_path_arg: str = cfg["config"]
|
|
657
|
+
refresh_base: bool = cfg["refresh_base"]
|
|
658
|
+
merged_config = resolve_extends_config(
|
|
659
|
+
project_dir, config_path=config_path_arg, refresh=refresh_base,
|
|
660
|
+
)
|
|
661
|
+
if merged_config:
|
|
662
|
+
cfg = _apply_merged_config(merged_config, cfg)
|
|
663
|
+
profile = cfg["profile"]
|
|
664
|
+
|
|
525
665
|
lang_modules = [m for m in (resolved_modules or []) if m.startswith("rules-")]
|
|
526
666
|
editors_arg: str = cfg["editors"]
|
|
527
667
|
install_local_project(rules_dir, dry_run, reset, lang_modules or None,
|
|
528
|
-
editors=editors_arg
|
|
668
|
+
editors=editors_arg,
|
|
669
|
+
merged_config=merged_config)
|
|
529
670
|
install_strict_git_hooks(profile, local, dry_run)
|
|
530
671
|
else:
|
|
531
672
|
# Global install
|
|
@@ -549,11 +690,18 @@ def main() -> None:
|
|
|
549
690
|
# Legacy mode: infer modules from profile/only
|
|
550
691
|
record_modules = _infer_modules_from_legacy(profile, only)
|
|
551
692
|
|
|
693
|
+
# Extract extends metadata if available
|
|
694
|
+
extends_info = None
|
|
695
|
+
merged = cfg.get("_merged_config")
|
|
696
|
+
if merged and merged.get("_extends_meta"):
|
|
697
|
+
extends_info = merged["_extends_meta"]
|
|
698
|
+
|
|
552
699
|
record_install(
|
|
553
700
|
version=_get_toolkit_version(),
|
|
554
701
|
modules=record_modules,
|
|
555
702
|
profile=profile or "standard",
|
|
556
703
|
auto_detected=auto_detected,
|
|
704
|
+
extends_info=extends_info,
|
|
557
705
|
)
|
|
558
706
|
|
|
559
707
|
print_summary(local=local)
|
|
@@ -172,7 +172,8 @@ def _resolve_editors(editors_arg: str, cwd: Path) -> list[str]:
|
|
|
172
172
|
|
|
173
173
|
def install_local_project(rules_dir: Path, dry_run: bool, reset: bool,
|
|
174
174
|
language_modules: list[str] | None = None,
|
|
175
|
-
editors: str = ""
|
|
175
|
+
editors: str = "",
|
|
176
|
+
merged_config: dict | None = None) -> None:
|
|
176
177
|
"""Install project-local configs.
|
|
177
178
|
|
|
178
179
|
Claude Code configs (CLAUDE.md, settings, constitution) are always installed.
|
|
@@ -180,12 +181,18 @@ def install_local_project(rules_dir: Path, dry_run: bool, reset: bool,
|
|
|
180
181
|
- ``--editors all``: install all editors
|
|
181
182
|
- ``--editors cursor,aider``: install only these
|
|
182
183
|
- (empty): auto-detect from existing project files, install only those
|
|
184
|
+
|
|
185
|
+
If ``merged_config`` is provided (from .ai-toolkit.json extends resolution),
|
|
186
|
+
additional rules and constitution amendments from the base config are injected.
|
|
183
187
|
"""
|
|
184
188
|
cwd = Path.cwd()
|
|
185
189
|
resolved_editors = _resolve_editors(editors, cwd)
|
|
186
190
|
|
|
187
191
|
print()
|
|
188
192
|
print(f"## Project-local ({cwd})")
|
|
193
|
+
if merged_config and merged_config.get("_extends_meta"):
|
|
194
|
+
meta = merged_config["_extends_meta"]
|
|
195
|
+
print(f" Config: .ai-toolkit.json (extends: {meta['source']})")
|
|
189
196
|
if reset:
|
|
190
197
|
print(" Mode: RESET (all local configs will be wiped and recreated)")
|
|
191
198
|
if resolved_editors:
|
|
@@ -198,6 +205,8 @@ def install_local_project(rules_dir: Path, dry_run: bool, reset: bool,
|
|
|
198
205
|
_install_local_dry_run(reset, resolved_editors)
|
|
199
206
|
if language_modules:
|
|
200
207
|
print(f" Would inject language rules: {', '.join(language_modules)}")
|
|
208
|
+
if merged_config:
|
|
209
|
+
print(f" Would apply merged config from extends")
|
|
201
210
|
return
|
|
202
211
|
|
|
203
212
|
(cwd / ".claude").mkdir(parents=True, exist_ok=True)
|
|
@@ -223,6 +232,10 @@ def install_local_project(rules_dir: Path, dry_run: bool, reset: bool,
|
|
|
223
232
|
)
|
|
224
233
|
print(" Injected: .claude/constitution.md")
|
|
225
234
|
|
|
235
|
+
# Apply extends: inject base rules and constitution amendments
|
|
236
|
+
if merged_config:
|
|
237
|
+
_apply_extends_config(cwd, merged_config)
|
|
238
|
+
|
|
226
239
|
# Inject language-specific rules into project CLAUDE.md
|
|
227
240
|
_inject_language_rules(cwd, language_modules)
|
|
228
241
|
|
|
@@ -231,6 +244,93 @@ def install_local_project(rules_dir: Path, dry_run: bool, reset: bool,
|
|
|
231
244
|
language_modules=language_modules)
|
|
232
245
|
|
|
233
246
|
|
|
247
|
+
def _apply_extends_config(cwd: Path, merged: dict) -> None:
|
|
248
|
+
"""Apply merged extends config — inject base rules and constitution amendments."""
|
|
249
|
+
import json as _json
|
|
250
|
+
|
|
251
|
+
# Inject base rules into CLAUDE.md
|
|
252
|
+
rules = merged.get("rules", {})
|
|
253
|
+
inject_rules = rules.get("inject", [])
|
|
254
|
+
if inject_rules:
|
|
255
|
+
claude_md = cwd / ".claude" / "CLAUDE.md"
|
|
256
|
+
if claude_md.is_file():
|
|
257
|
+
content = claude_md.read_text(encoding="utf-8")
|
|
258
|
+
else:
|
|
259
|
+
content = ""
|
|
260
|
+
|
|
261
|
+
# Add extends rules section if not already present
|
|
262
|
+
marker_start = "<!-- TOOLKIT:extends-rules START -->"
|
|
263
|
+
marker_end = "<!-- TOOLKIT:extends-rules END -->"
|
|
264
|
+
|
|
265
|
+
rules_block = f"\n{marker_start}\n"
|
|
266
|
+
rules_block += "# Inherited Rules (from base config)\n\n"
|
|
267
|
+
for rule_path in inject_rules:
|
|
268
|
+
rules_block += f"- Rule: `{rule_path}`\n"
|
|
269
|
+
rules_block += f"{marker_end}\n"
|
|
270
|
+
|
|
271
|
+
if marker_start in content:
|
|
272
|
+
# Replace existing section
|
|
273
|
+
import re
|
|
274
|
+
content = re.sub(
|
|
275
|
+
f"{re.escape(marker_start)}.*?{re.escape(marker_end)}",
|
|
276
|
+
f"{marker_start}\n# Inherited Rules (from base config)\n\n"
|
|
277
|
+
+ "".join(f"- Rule: `{r}`\n" for r in inject_rules)
|
|
278
|
+
+ marker_end,
|
|
279
|
+
content,
|
|
280
|
+
flags=re.DOTALL,
|
|
281
|
+
)
|
|
282
|
+
else:
|
|
283
|
+
content += rules_block
|
|
284
|
+
|
|
285
|
+
claude_md.write_text(content, encoding="utf-8")
|
|
286
|
+
print(f" Injected: {len(inject_rules)} rule(s) from base config")
|
|
287
|
+
|
|
288
|
+
# Inject constitution amendments
|
|
289
|
+
amendments = merged.get("constitution", {}).get("amendments", [])
|
|
290
|
+
# Filter to non-toolkit articles (6+)
|
|
291
|
+
custom_amendments = [a for a in amendments if a.get("article", 0) >= 6]
|
|
292
|
+
if custom_amendments:
|
|
293
|
+
constitution_file = cwd / ".claude" / "constitution.md"
|
|
294
|
+
if constitution_file.is_file():
|
|
295
|
+
content = constitution_file.read_text(encoding="utf-8")
|
|
296
|
+
else:
|
|
297
|
+
content = ""
|
|
298
|
+
|
|
299
|
+
marker_start = "<!-- TOOLKIT:extends-constitution START -->"
|
|
300
|
+
marker_end = "<!-- TOOLKIT:extends-constitution END -->"
|
|
301
|
+
|
|
302
|
+
amendments_block = f"\n{marker_start}\n"
|
|
303
|
+
for a in custom_amendments:
|
|
304
|
+
amendments_block += f"\n## Article {a['article']}: {a['title']}\n\n"
|
|
305
|
+
amendments_block += f"{a['text']}\n"
|
|
306
|
+
amendments_block += f"\n{marker_end}\n"
|
|
307
|
+
|
|
308
|
+
if marker_start in content:
|
|
309
|
+
import re
|
|
310
|
+
new_inner = ""
|
|
311
|
+
for a in custom_amendments:
|
|
312
|
+
new_inner += f"\n## Article {a['article']}: {a['title']}\n\n"
|
|
313
|
+
new_inner += f"{a['text']}\n"
|
|
314
|
+
content = re.sub(
|
|
315
|
+
f"{re.escape(marker_start)}.*?{re.escape(marker_end)}",
|
|
316
|
+
f"{marker_start}{new_inner}\n{marker_end}",
|
|
317
|
+
content,
|
|
318
|
+
flags=re.DOTALL,
|
|
319
|
+
)
|
|
320
|
+
else:
|
|
321
|
+
content += amendments_block
|
|
322
|
+
|
|
323
|
+
constitution_file.write_text(content, encoding="utf-8")
|
|
324
|
+
print(f" Injected: {len(custom_amendments)} constitution amendment(s) from base config")
|
|
325
|
+
|
|
326
|
+
# Record merged config summary
|
|
327
|
+
meta = merged.get("_extends_meta")
|
|
328
|
+
if meta:
|
|
329
|
+
state_file = cwd / ".ai-toolkit-extends.json"
|
|
330
|
+
state_file.write_text(_json.dumps(meta, indent=2) + "\n", encoding="utf-8")
|
|
331
|
+
print(f" Saved: .ai-toolkit-extends.json (resolution metadata)")
|
|
332
|
+
|
|
333
|
+
|
|
234
334
|
def _inject_language_rules(cwd: Path, language_modules: list[str] | None) -> None:
|
|
235
335
|
"""Inject language-specific rule summary into project's .claude/CLAUDE.md.
|
|
236
336
|
|
|
@@ -68,11 +68,15 @@ def record_install(
|
|
|
68
68
|
modules: list[str],
|
|
69
69
|
profile: str,
|
|
70
70
|
auto_detected: list[str] | None = None,
|
|
71
|
+
extends_info: dict | None = None,
|
|
71
72
|
) -> None:
|
|
72
73
|
"""Record a successful install in state.json.
|
|
73
74
|
|
|
74
75
|
If state already exists, preserves ``installed_at`` and updates
|
|
75
76
|
``last_updated``. Otherwise sets both timestamps.
|
|
77
|
+
|
|
78
|
+
``extends_info`` (optional) records config inheritance metadata:
|
|
79
|
+
source, version, resolved_at, hash, overrides_applied.
|
|
76
80
|
"""
|
|
77
81
|
state = load_state()
|
|
78
82
|
now = _now_iso()
|
|
@@ -86,6 +90,17 @@ def record_install(
|
|
|
86
90
|
if auto_detected is not None:
|
|
87
91
|
state["auto_detected_languages"] = sorted(auto_detected)
|
|
88
92
|
|
|
93
|
+
if extends_info is not None:
|
|
94
|
+
state["extends"] = {
|
|
95
|
+
"source": extends_info.get("source", ""),
|
|
96
|
+
"configs": extends_info.get("configs", []),
|
|
97
|
+
"resolved_at": now,
|
|
98
|
+
"overrides_applied": extends_info.get("overrides_applied", []),
|
|
99
|
+
}
|
|
100
|
+
elif "extends" in state:
|
|
101
|
+
# Clear extends if no longer using it
|
|
102
|
+
del state["extends"]
|
|
103
|
+
|
|
89
104
|
save_state(state)
|
|
90
105
|
|
|
91
106
|
# Clear version check cache (version may have changed)
|
|
@@ -121,6 +136,15 @@ def print_status() -> None:
|
|
|
121
136
|
langs = [m.replace("rules-", "") for m in detected]
|
|
122
137
|
print(f" Detected: {', '.join(langs)}")
|
|
123
138
|
|
|
139
|
+
extends = state.get("extends")
|
|
140
|
+
if extends:
|
|
141
|
+
print(f" Extends: {extends.get('source', 'unknown')}")
|
|
142
|
+
for cfg in extends.get("configs", []):
|
|
143
|
+
version_str = f" v{cfg['version']}" if cfg.get("version") else ""
|
|
144
|
+
print(f" → {cfg.get('name', cfg.get('source', '?'))}{version_str}")
|
|
145
|
+
if extends.get("resolved_at"):
|
|
146
|
+
print(f" Resolved: {extends['resolved_at']}")
|
|
147
|
+
|
|
124
148
|
# Check for updates
|
|
125
149
|
try:
|
|
126
150
|
import sys as _sys
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://softspark.github.io/ai-toolkit/schemas/ai-toolkit-config.json",
|
|
4
|
+
"title": "ai-toolkit project configuration",
|
|
5
|
+
"description": "Project-level configuration for ai-toolkit with optional inheritance via extends.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "JSON Schema reference (ignored at runtime)."
|
|
12
|
+
},
|
|
13
|
+
"extends": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Base config to inherit from. npm package name (@scope/pkg or @scope/pkg@version), git URL (git+https://...), or local path (./path or ../path).",
|
|
16
|
+
"examples": [
|
|
17
|
+
"@mycompany/ai-toolkit-config",
|
|
18
|
+
"@mycompany/ai-toolkit-config@^2.0.0",
|
|
19
|
+
"git+https://github.com/myco/ai-config.git",
|
|
20
|
+
"../shared-config"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"name": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Config identity (required for base configs published as npm packages)."
|
|
26
|
+
},
|
|
27
|
+
"version": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "Semantic version (required for base configs).",
|
|
30
|
+
"pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.]+)?(\\+[a-zA-Z0-9.]+)?$"
|
|
31
|
+
},
|
|
32
|
+
"description": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"description": "Human-readable description of this configuration."
|
|
35
|
+
},
|
|
36
|
+
"profile": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"enum": ["minimal", "standard", "strict", "full", "offline-slm"],
|
|
39
|
+
"default": "standard",
|
|
40
|
+
"description": "Installation profile controlling which modules are installed."
|
|
41
|
+
},
|
|
42
|
+
"agents": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"additionalProperties": false,
|
|
45
|
+
"description": "Agent configuration — enable, disable, or add custom agents.",
|
|
46
|
+
"properties": {
|
|
47
|
+
"enabled": {
|
|
48
|
+
"type": "array",
|
|
49
|
+
"items": { "type": "string" },
|
|
50
|
+
"description": "Agent names to enable (merged with base)."
|
|
51
|
+
},
|
|
52
|
+
"disabled": {
|
|
53
|
+
"type": "array",
|
|
54
|
+
"items": { "type": "string" },
|
|
55
|
+
"description": "Agent names to disable (blocked if base enforces them)."
|
|
56
|
+
},
|
|
57
|
+
"custom": {
|
|
58
|
+
"type": "array",
|
|
59
|
+
"items": { "type": "string" },
|
|
60
|
+
"description": "Paths to custom agent .md files (relative to config root)."
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"rules": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"description": "Rule injection configuration.",
|
|
68
|
+
"properties": {
|
|
69
|
+
"inject": {
|
|
70
|
+
"type": "array",
|
|
71
|
+
"items": { "type": "string" },
|
|
72
|
+
"description": "Paths to rule .md files to inject into CLAUDE.md."
|
|
73
|
+
},
|
|
74
|
+
"remove": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"items": { "type": "string" },
|
|
77
|
+
"description": "Rule names to remove from base config."
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"constitution": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"additionalProperties": false,
|
|
84
|
+
"description": "Constitution amendments. Articles I-V are immutable. Base articles are immutable. Projects can only ADD new articles.",
|
|
85
|
+
"properties": {
|
|
86
|
+
"amendments": {
|
|
87
|
+
"type": "array",
|
|
88
|
+
"items": {
|
|
89
|
+
"type": "object",
|
|
90
|
+
"required": ["article", "title", "text"],
|
|
91
|
+
"additionalProperties": false,
|
|
92
|
+
"properties": {
|
|
93
|
+
"article": {
|
|
94
|
+
"type": "integer",
|
|
95
|
+
"minimum": 1,
|
|
96
|
+
"description": "Article number. 1-5 are reserved (immutable). Base articles are also immutable."
|
|
97
|
+
},
|
|
98
|
+
"title": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"description": "Article title."
|
|
101
|
+
},
|
|
102
|
+
"text": {
|
|
103
|
+
"type": "string",
|
|
104
|
+
"description": "Article text."
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"enforce": {
|
|
112
|
+
"type": "object",
|
|
113
|
+
"additionalProperties": false,
|
|
114
|
+
"description": "Non-overridable constraints (base configs only). Projects inheriting this config must comply.",
|
|
115
|
+
"properties": {
|
|
116
|
+
"minHookProfile": {
|
|
117
|
+
"type": "string",
|
|
118
|
+
"enum": ["minimal", "standard", "strict"],
|
|
119
|
+
"description": "Minimum hook profile — projects cannot go below this."
|
|
120
|
+
},
|
|
121
|
+
"requiredPlugins": {
|
|
122
|
+
"type": "array",
|
|
123
|
+
"items": { "type": "string" },
|
|
124
|
+
"description": "Plugins that must be installed in all inheriting projects."
|
|
125
|
+
},
|
|
126
|
+
"forbidOverride": {
|
|
127
|
+
"type": "array",
|
|
128
|
+
"items": { "type": "string" },
|
|
129
|
+
"description": "Components that cannot be overridden by inheriting projects."
|
|
130
|
+
},
|
|
131
|
+
"requiredAgents": {
|
|
132
|
+
"type": "array",
|
|
133
|
+
"items": { "type": "string" },
|
|
134
|
+
"description": "Agents that must be enabled in all inheriting projects."
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"overrides": {
|
|
139
|
+
"type": "object",
|
|
140
|
+
"additionalProperties": {
|
|
141
|
+
"type": "object",
|
|
142
|
+
"required": ["override", "justification"],
|
|
143
|
+
"properties": {
|
|
144
|
+
"override": {
|
|
145
|
+
"type": "boolean",
|
|
146
|
+
"const": true,
|
|
147
|
+
"description": "Must be true to confirm intentional override."
|
|
148
|
+
},
|
|
149
|
+
"justification": {
|
|
150
|
+
"type": "string",
|
|
151
|
+
"minLength": 20,
|
|
152
|
+
"description": "Explanation for why this override is needed (min 20 chars)."
|
|
153
|
+
},
|
|
154
|
+
"replacement": {
|
|
155
|
+
"type": "string",
|
|
156
|
+
"description": "Replacement value or 'skip' to disable."
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
"description": "Explicit overrides of base config components. Each override requires override:true + justification."
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|