spawn-cli 0.1.7__tar.gz → 0.1.9__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 (49) hide show
  1. {spawn_cli-0.1.7/src/spawn_cli.egg-info → spawn_cli-0.1.9}/PKG-INFO +3 -3
  2. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/README.md +2 -2
  3. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/pyproject.toml +1 -1
  4. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/cli.py +2 -2
  5. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/core/high_level.py +14 -3
  6. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/core/low_level.py +276 -20
  7. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/ide/_helpers.py +8 -2
  8. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/models/config.py +10 -0
  9. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/models/skill.py +1 -0
  10. {spawn_cli-0.1.7 → spawn_cli-0.1.9/src/spawn_cli.egg-info}/PKG-INFO +3 -3
  11. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/LICENSE +0 -0
  12. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/setup.cfg +0 -0
  13. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/__init__.py +0 -0
  14. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/core/__init__.py +0 -0
  15. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/core/download.py +0 -0
  16. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/core/errors.py +0 -0
  17. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/core/scripts.py +0 -0
  18. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/errors.py +0 -0
  19. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/ide/__init__.py +0 -0
  20. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/ide/_stub.py +0 -0
  21. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/ide/_vacancy.py +0 -0
  22. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/ide/claude_code.py +0 -0
  23. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/ide/codex.py +0 -0
  24. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/ide/cursor.py +0 -0
  25. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/ide/gemini_cli.py +0 -0
  26. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/ide/github_copilot.py +0 -0
  27. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/ide/registry.py +0 -0
  28. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/ide/windsurf.py +0 -0
  29. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/io/__init__.py +0 -0
  30. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/io/json_io.py +0 -0
  31. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/io/lock.py +0 -0
  32. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/io/paths.py +0 -0
  33. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/io/text_io.py +0 -0
  34. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/io/toml_io.py +0 -0
  35. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/io/yaml_io.py +0 -0
  36. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/models/__init__.py +0 -0
  37. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/models/mcp.py +0 -0
  38. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/models/metadata.py +0 -0
  39. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/models/navigation.py +0 -0
  40. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/resources/__init__.py +0 -0
  41. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/resources/default_core_config.yaml +0 -0
  42. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli/warnings_display.py +0 -0
  43. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli.egg-info/SOURCES.txt +0 -0
  44. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli.egg-info/dependency_links.txt +0 -0
  45. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli.egg-info/entry_points.txt +0 -0
  46. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli.egg-info/requires.txt +0 -0
  47. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/src/spawn_cli.egg-info/top_level.txt +0 -0
  48. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/tests/test_cli.py +0 -0
  49. {spawn_cli-0.1.7 → spawn_cli-0.1.9}/tests/test_warnings_display.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spawn-cli
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: A minimal installable Python CLI package.
5
5
  License: MIT
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -27,7 +27,7 @@ Spawn is a framework for building **AIDD** (AI-driven development) methodologies
27
27
 
28
28
  Several methodologies in one repo stay aligned because Spawn maintains **one shared navigation index**: [`spawn/navigation.yaml`](spec/design/data-structure.md) aggregates mandatory and contextual reads from **every installed extension** plus local [`spawn/rules/`](spec/design/user-guide.md) (refreshed with `spawn rules refresh`). Agents are steered through that file instead of chasing disconnected README fragments.
29
29
 
30
- On each install or refresh, the CLI **regenerates IDE-facing artifacts** so skills and entry points stay consistent across extensions: rendered skill bodies include **paths and descriptions** wired from navigation (required reads, contextual hints, rule files), MCP snippets normalize server definitions per IDE adapter, and ignore lists merge without manual copy-paste. The net effect is that methodology files, generated skill links, and repo rules **compose** rather than contradict each other.
30
+ On each install or refresh, the CLI **regenerates IDE-facing artifacts** so skills and entry points stay consistent across extensions: rendered skill bodies include **paths and descriptions** wired from navigation (required reads, contextual hints, rule files), MCP snippets normalize server definitions per IDE adapter, and ignore lists merge without manual copy-paste. The net effect is that methodology files, generated skill links, and repo rules **compose** rather than contradict each other. Optional `hints` add short reminders to rendered skills and the Spawn-managed entry block.
31
31
 
32
32
  **Example:** [**spawn-ext-spectask**](https://github.com/noant/spawn-ext-spectask) (Spec-Tasks methodology: spec layout, skills, design index) and [**spawn-ext-creator**](https://github.com/noant/spawn-ext-creator) (authoring flows, including agent-guided extension bootstrap) can be installed in the **same** repository and still share that single index and merged IDE outputs.
33
33
 
@@ -189,7 +189,7 @@ Read **[spec/design/extension-author-guide.md](spec/design/extension-author-guid
189
189
 
190
190
  **Minimal mental model:**
191
191
 
192
- - `**extsrc/config.yaml`** — `schema`, `version`, stable `**name`** (install path `spawn/.extend/<name>/`), plus `files`, `skills`, ignores, etc.
192
+ - `**extsrc/config.yaml`** — `schema`, `version`, stable `**name`** (install path `spawn/.extend/<name>/`), plus `files`, `skills`, ignores, optional `hints`, etc.
193
193
  - `**extsrc/files/**` — template tree mirrored into the target repo according to `files:` entries.
194
194
  - `**spawn extension check . --strict**` — validate before publishing or tagging a release.
195
195
 
@@ -10,7 +10,7 @@ Spawn is a framework for building **AIDD** (AI-driven development) methodologies
10
10
 
11
11
  Several methodologies in one repo stay aligned because Spawn maintains **one shared navigation index**: [`spawn/navigation.yaml`](spec/design/data-structure.md) aggregates mandatory and contextual reads from **every installed extension** plus local [`spawn/rules/`](spec/design/user-guide.md) (refreshed with `spawn rules refresh`). Agents are steered through that file instead of chasing disconnected README fragments.
12
12
 
13
- On each install or refresh, the CLI **regenerates IDE-facing artifacts** so skills and entry points stay consistent across extensions: rendered skill bodies include **paths and descriptions** wired from navigation (required reads, contextual hints, rule files), MCP snippets normalize server definitions per IDE adapter, and ignore lists merge without manual copy-paste. The net effect is that methodology files, generated skill links, and repo rules **compose** rather than contradict each other.
13
+ On each install or refresh, the CLI **regenerates IDE-facing artifacts** so skills and entry points stay consistent across extensions: rendered skill bodies include **paths and descriptions** wired from navigation (required reads, contextual hints, rule files), MCP snippets normalize server definitions per IDE adapter, and ignore lists merge without manual copy-paste. The net effect is that methodology files, generated skill links, and repo rules **compose** rather than contradict each other. Optional `hints` add short reminders to rendered skills and the Spawn-managed entry block.
14
14
 
15
15
  **Example:** [**spawn-ext-spectask**](https://github.com/noant/spawn-ext-spectask) (Spec-Tasks methodology: spec layout, skills, design index) and [**spawn-ext-creator**](https://github.com/noant/spawn-ext-creator) (authoring flows, including agent-guided extension bootstrap) can be installed in the **same** repository and still share that single index and merged IDE outputs.
16
16
 
@@ -172,7 +172,7 @@ Read **[spec/design/extension-author-guide.md](spec/design/extension-author-guid
172
172
 
173
173
  **Minimal mental model:**
174
174
 
175
- - `**extsrc/config.yaml`** — `schema`, `version`, stable `**name`** (install path `spawn/.extend/<name>/`), plus `files`, `skills`, ignores, etc.
175
+ - `**extsrc/config.yaml`** — `schema`, `version`, stable `**name`** (install path `spawn/.extend/<name>/`), plus `files`, `skills`, ignores, optional `hints`, etc.
176
176
  - `**extsrc/files/**` — template tree mirrored into the target repo according to `files:` entries.
177
177
  - `**spawn extension check . --strict**` — validate before publishing or tagging a release.
178
178
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "spawn-cli"
7
- version = "0.1.7"
7
+ version = "0.1.9"
8
8
  description = "A minimal installable Python CLI package."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -44,8 +44,8 @@ def build_parser() -> argparse.ArgumentParser:
44
44
  "refresh",
45
45
  help="Sync core config from defaults and rebuild IDE renders",
46
46
  description=(
47
- "Merge bundled defaults into spawn/.core/config.yaml, then refresh skills, MCP, "
48
- "agent ignore, navigation, gitignore metadata, and IDE entry points."
47
+ "Replace spawn/.core/config.yaml with bundled default core config, then refresh "
48
+ "skills, MCP, agent ignore, navigation, gitignore metadata, and IDE entry points."
49
49
  ),
50
50
  )
51
51
 
@@ -169,7 +169,17 @@ def remove_mcp(target_root: Path, ide: str, extension: str) -> None:
169
169
 
170
170
  def refresh_entry_point(target_root: Path, ide: str) -> None:
171
171
  _require_init(target_root)
172
- ide_get(ide).rewrite_entry_point(target_root, SPAWN_ENTRY_POINT_PROMPT)
172
+ rollup = ll.rollup_hints_for_agents(target_root)
173
+ ll.warn_if_agents_hints_exceed_measurement(rollup)
174
+ if rollup:
175
+ prompt = (
176
+ f"{SPAWN_ENTRY_POINT_PROMPT}\nHints:\n"
177
+ + "\n".join(f"- {h}" for h in rollup)
178
+ + "\n"
179
+ )
180
+ else:
181
+ prompt = SPAWN_ENTRY_POINT_PROMPT
182
+ ide_get(ide).rewrite_entry_point(target_root, prompt)
173
183
 
174
184
 
175
185
  def refresh_extension_for_ide(target_root: Path, ide: str, extension: str) -> None:
@@ -192,6 +202,7 @@ def refresh_extension_for_ide(target_root: Path, ide: str, extension: str) -> No
192
202
  refresh_mcp(target_root, ide, extension)
193
203
  _refresh_skills_all_extensions_for_ide(target_root, ide)
194
204
  refresh_agent_ignore(target_root, ide)
205
+ refresh_navigation(target_root)
195
206
 
196
207
 
197
208
  def remove_extension_for_ide(target_root: Path, ide: str, extension: str) -> None:
@@ -223,9 +234,9 @@ def refresh_rules_navigation(target_root: Path) -> None:
223
234
 
224
235
 
225
236
  def refresh_repository(target_root: Path) -> None:
226
- """Sync core config from package defaults and rebuild all IDE-facing metadata."""
237
+ """Overwrite core config from bundled defaults, then rebuild all IDE-facing metadata."""
227
238
  _require_init(target_root)
228
- ll.merge_core_config_from_defaults(target_root)
239
+ ll.sync_core_config_from_defaults(target_root)
229
240
  needs_skill = _any_extension_has_skill_files(target_root)
230
241
  needs_mcp = _any_extension_has_mcp_servers(target_root)
231
242
  for ide in ll.list_ides(target_root):
@@ -130,6 +130,182 @@ def _load_ext_config(target_root: Path, extension: str) -> ExtensionConfig:
130
130
  return ExtensionConfig.model_validate(raw)
131
131
 
132
132
 
133
+ _EXTENSION_NAV_HINT_MAX_CODEPOINTS = 512
134
+ _SKILL_HINT_SECTION_COMBINED_MAX_CHARS = 4096
135
+
136
+
137
+ def _optional_ext_config(target_root: Path, extension: str) -> ExtensionConfig | None:
138
+ path = _config_path(target_root, extension)
139
+ if not path.is_file():
140
+ return None
141
+ raw = load_yaml(path)
142
+ if not raw:
143
+ return None
144
+ return ExtensionConfig.model_validate(raw)
145
+
146
+
147
+ def _normalized_extension_global_hints_for_navigation(cfg: ExtensionConfig) -> list[str]:
148
+ """Strip, drop empties, dedupe (first wins), warn + truncate to codepoint limit for persisted navigation."""
149
+
150
+ raw_list: list[str] = []
151
+ if cfg.hints is not None:
152
+ raw_list.extend(cfg.hints.global_)
153
+ seen: set[str] = set()
154
+ out: list[str] = []
155
+ for h in raw_list:
156
+ s = h.strip()
157
+ if not s:
158
+ continue
159
+ if s in seen:
160
+ continue
161
+ seen.add(s)
162
+ if len(s) > _EXTENSION_NAV_HINT_MAX_CODEPOINTS:
163
+ warnings.warn(
164
+ "Extension navigation hint exceeds 512 Unicode code points and was truncated.",
165
+ SpawnWarning,
166
+ )
167
+ s = s[:_EXTENSION_NAV_HINT_MAX_CODEPOINTS]
168
+ out.append(s)
169
+ return out
170
+
171
+
172
+ def _truncate_hint_codepoints(text: str, max_codepoints: int) -> tuple[str, bool]:
173
+ if len(text) <= max_codepoints:
174
+ return text, False
175
+ return text[:max_codepoints], True
176
+
177
+
178
+ def _merge_hint_streams_ordered(streams: list[list[str]]) -> list[str]:
179
+ """Dedupe stripped strings with first occurrence winning; earlier streams dominate later ones."""
180
+
181
+ seen: set[str] = set()
182
+ out: list[str] = []
183
+ for stream in streams:
184
+ for h in stream:
185
+ s = h.strip()
186
+ if not s or s in seen:
187
+ continue
188
+ seen.add(s)
189
+ out.append(s)
190
+ return out
191
+
192
+
193
+ def _navigation_read_required_rule_hints_ordered(target_root: Path) -> list[str]:
194
+ """Plain-text hints from maintainer-authored read-required ``rules`` rows only (YAML order).
195
+
196
+ Hint fields on ``read-contextual`` rules are ignored here.
197
+ """
198
+ path = _spawn(target_root) / "navigation.yaml"
199
+ raw = load_yaml(path)
200
+ if not isinstance(raw, dict):
201
+ return []
202
+ rr = raw.get("read-required")
203
+ if not isinstance(rr, list):
204
+ return []
205
+ out: list[str] = []
206
+ for grp in rr:
207
+ if not isinstance(grp, dict):
208
+ continue
209
+ rules_raw = grp.get("rules")
210
+ if not isinstance(rules_raw, list):
211
+ continue
212
+ for entry in rules_raw:
213
+ if not isinstance(entry, dict):
214
+ continue
215
+ hint_val = entry.get("hint")
216
+ if isinstance(hint_val, str):
217
+ out.append(hint_val)
218
+ return out
219
+
220
+
221
+ def _finalize_hints_for_skill_metadata(merged_hints: list[str]) -> list[str]:
222
+ """512 codepoints per hint + 4096 char ``Hints:`` section (terminal ``- ...`` when clipped)."""
223
+
224
+ trimmed: list[str] = []
225
+ for h in merged_hints:
226
+ t, clipped = _truncate_hint_codepoints(h, _EXTENSION_NAV_HINT_MAX_CODEPOINTS)
227
+ if clipped:
228
+ warnings.warn(
229
+ "Skill hint exceeds 512 Unicode code points and was truncated for skill metadata.",
230
+ SpawnWarning,
231
+ )
232
+ trimmed.append(t)
233
+
234
+ header = "Hints:\n"
235
+ warn_combined = (
236
+ "Skill hints truncated to fit combined 4096 character budget for the Hints section; "
237
+ "shorten hints and/or split skills."
238
+ )
239
+
240
+ def build_block(strings: list[str], with_ellipsis: bool) -> str:
241
+ lines = [f"- {s}" for s in strings]
242
+ if with_ellipsis:
243
+ lines.append("- ...")
244
+ if not lines:
245
+ return ""
246
+ return header + "\n".join(lines)
247
+
248
+ fitted: list[str] = []
249
+ need_ellipsis = False
250
+
251
+ for s in trimmed:
252
+ plain = build_block(fitted + [s], False)
253
+ if len(plain) <= _SKILL_HINT_SECTION_COMBINED_MAX_CHARS:
254
+ fitted.append(s)
255
+ continue
256
+ with_el = build_block(fitted + [s], True)
257
+ if len(with_el) <= _SKILL_HINT_SECTION_COMBINED_MAX_CHARS:
258
+ fitted.append(s)
259
+ need_ellipsis = True
260
+ warnings.warn(warn_combined, SpawnWarning)
261
+ break
262
+ need_ellipsis = True
263
+ warnings.warn(warn_combined, SpawnWarning)
264
+ break
265
+
266
+ if need_ellipsis:
267
+ fitted.append("...")
268
+ return fitted
269
+
270
+
271
+ def _extension_global_hint_streams_ordered(target_root: Path) -> list[list[str]]:
272
+ """One hint list per extension, in ``list_extensions`` order (sorted ext ids)."""
273
+
274
+ streams: list[list[str]] = []
275
+ for ext in list_extensions(target_root):
276
+ cfg = _load_ext_config(target_root, ext)
277
+ streams.append(list(cfg.hints.global_) if cfg.hints else [])
278
+ return streams
279
+
280
+
281
+ def rollup_hints_for_agents(target_root: Path) -> list[str]:
282
+ """Deduped ordered hints for ``AGENTS.md``: every extension ``hints.global``, then maintainer."""
283
+
284
+ streams = _extension_global_hint_streams_ordered(target_root)
285
+ streams.append(_navigation_read_required_rule_hints_ordered(target_root))
286
+ return _merge_hint_streams_ordered(streams)
287
+
288
+
289
+ def warn_if_agents_hints_exceed_measurement(hints: list[str]) -> None:
290
+ """Warn when thresholds are exceeded without mutating hints (full text still written)."""
291
+
292
+ if any(len(h) > _EXTENSION_NAV_HINT_MAX_CODEPOINTS for h in hints):
293
+ warnings.warn(
294
+ "AGENTS.md hints contain at least one hint over 512 Unicode code points "
295
+ "(full text retained); shorten hints and/or reduce installed extensions.",
296
+ SpawnWarning,
297
+ )
298
+ block = ""
299
+ if hints:
300
+ block = "Hints:\n" + "\n".join(f"- {h}" for h in hints)
301
+ if hints and len(block) > _SKILL_HINT_SECTION_COMBINED_MAX_CHARS:
302
+ warnings.warn(
303
+ "AGENTS.md hints exceed combined 4096 character measurement "
304
+ "(full text retained); shorten hints and/or reduce installed extensions.",
305
+ SpawnWarning,
306
+ )
307
+
308
+
133
309
  def _file_refs_for_global(extension: ExtensionConfig, flag_required: bool) -> list[SkillFileRef]:
134
310
  out: list[SkillFileRef] = []
135
311
  for path_str, ent in extension.files.items():
@@ -294,26 +470,82 @@ def _norm_read_path(path_str: str) -> str:
294
470
  return Path(path_str).as_posix().replace("\\", "/")
295
471
 
296
472
 
473
+ def _navigation_rules_refs_from_section(section: Any) -> list[SkillFileRef]:
474
+ out: list[SkillFileRef] = []
475
+ if not isinstance(section, list):
476
+ return out
477
+ for grp in section:
478
+ if not isinstance(grp, dict):
479
+ continue
480
+ rules_raw = grp.get("rules")
481
+ if not isinstance(rules_raw, list):
482
+ continue
483
+ for entry in rules_raw:
484
+ if not isinstance(entry, dict):
485
+ continue
486
+ path_raw = entry.get("path")
487
+ if not isinstance(path_raw, str) or not path_raw.strip():
488
+ continue
489
+ desc_raw = entry.get("description")
490
+ desc = "" if desc_raw is None else str(desc_raw)
491
+ out.append(SkillFileRef(file=path_raw.strip(), description=desc))
492
+ return out
493
+
494
+
495
+ def _navigation_yaml_rules_refs(target_root: Path) -> tuple[list[SkillFileRef], list[SkillFileRef]]:
496
+ """Return (read-required rules, read-contextual rules) from merged navigation.yaml."""
497
+ path = _spawn(target_root) / "navigation.yaml"
498
+ raw = load_yaml(path)
499
+ if not isinstance(raw, dict):
500
+ return [], []
501
+ rr = raw.get("read-required")
502
+ cq = raw.get("read-contextual")
503
+ return (
504
+ _navigation_rules_refs_from_section(rr if isinstance(rr, list) else []),
505
+ _navigation_rules_refs_from_section(cq if isinstance(cq, list) else []),
506
+ )
507
+
508
+
297
509
  def _required_read_description_for_path(
298
- p: str, merged_global: list[SkillFileRef], local_req: list[SkillFileRef], cfg_ext: ExtensionConfig
510
+ p: str,
511
+ merged_global: list[SkillFileRef],
512
+ local_req: list[SkillFileRef],
513
+ cfg_ext: ExtensionConfig,
514
+ nav_rules_required: list[SkillFileRef] | None = None,
299
515
  ) -> str:
516
+ nk = _norm_read_path(p)
517
+ if nav_rules_required:
518
+ for r in nav_rules_required:
519
+ if _norm_read_path(r.file) == nk and r.description:
520
+ return r.description
300
521
  for r in merged_global:
301
- if r.file == p and r.description:
522
+ if _norm_read_path(r.file) == nk and r.description:
302
523
  return r.description
303
524
  for r in local_req:
304
- if r.file == p and r.description:
525
+ if _norm_read_path(r.file) == nk and r.description:
305
526
  return r.description
306
527
  return _describe_path(cfg_ext, p)
307
528
 
308
529
 
309
530
  def generate_skills_metadata(target_root: Path, extension: str) -> list[SkillMetadata]:
310
531
  cfg_ext = _load_ext_config(target_root, extension)
532
+ maintainer_hints = _navigation_read_required_rule_hints_ordered(target_root)
533
+ global_streams = _extension_global_hint_streams_ordered(target_root)
534
+ local_raw = list(cfg_ext.hints.local) if cfg_ext.hints else []
535
+ merged_for_skill = _merge_hint_streams_ordered(
536
+ [*global_streams, local_raw, maintainer_hints]
537
+ )
538
+ skill_hints = (
539
+ _finalize_hints_for_skill_metadata(merged_for_skill) if merged_for_skill else []
540
+ )
541
+
311
542
  merged_global_required = _flatten_global_refs_ordered(
312
543
  target_root, get_required_read_global_all(target_root)
313
544
  )
314
545
  merged_global_auto = _flatten_global_refs_ordered(target_root, get_auto_read_global_all(target_root))
315
546
  local_required = get_required_read_ext_local(target_root, extension)
316
547
  local_auto = get_auto_read_local(target_root, extension)
548
+ nav_rules_required, nav_rules_contextual = _navigation_yaml_rules_refs(target_root)
317
549
 
318
550
  metas: list[SkillMetadata] = []
319
551
  for skill_path in list_skills(target_root, extension):
@@ -331,10 +563,19 @@ def generate_skills_metadata(target_root: Path, extension: str) -> list[SkillMet
331
563
  continue
332
564
  seen_norm.add(nk)
333
565
  required_paths.append(fp)
566
+ for ref in nav_rules_required:
567
+ nk = _norm_read_path(ref.file)
568
+ if nk in seen_norm:
569
+ continue
570
+ seen_norm.add(nk)
571
+ required_paths.append(ref.file)
572
+
334
573
  required_out = [
335
574
  SkillFileRef(
336
575
  file=p,
337
- description=_required_read_description_for_path(p, merged_global_required, local_required, cfg_ext),
576
+ description=_required_read_description_for_path(
577
+ p, merged_global_required, local_required, cfg_ext, nav_rules_required
578
+ ),
338
579
  )
339
580
  for p in required_paths
340
581
  ]
@@ -355,12 +596,19 @@ def generate_skills_metadata(target_root: Path, extension: str) -> list[SkillMet
355
596
  continue
356
597
  seen_auto.add(key)
357
598
  auto_out.append(ar)
599
+ for ar in nav_rules_contextual:
600
+ key = _norm_read_path(ar.file)
601
+ if key in required_key_set or key in seen_auto:
602
+ continue
603
+ seen_auto.add(key)
604
+ auto_out.append(ar)
358
605
 
359
606
  metas.append(
360
607
  SkillMetadata(
361
608
  name=raw.name,
362
609
  description=raw.description,
363
610
  content=raw.content,
611
+ hints=list(skill_hints),
364
612
  required_read=required_out,
365
613
  auto_read=auto_out,
366
614
  )
@@ -458,12 +706,11 @@ def get_core_agent_ignore(target_root: Path) -> list[str]:
458
706
  return list(core.agent_ignore)
459
707
 
460
708
 
461
- def merge_core_config_from_defaults(target_root: Path) -> None:
462
- """Merge bundled default core config into ``spawn/.core/config.yaml``.
709
+ def sync_core_config_from_defaults(target_root: Path) -> None:
710
+ """Overwrite ``spawn/.core/config.yaml`` with bundled default ``CoreConfig``.
463
711
 
464
- ``version`` becomes the bundled default. ``agent-ignore`` is the bundled
465
- list (in resource order) followed by any existing patterns not present in
466
- the default set.
712
+ Validates that an existing repo file parses as ``CoreConfig`` before
713
+ replacing it so corrupted files are not silently clobbered.
467
714
  """
468
715
  path = _spawn(target_root) / ".core" / "config.yaml"
469
716
  if not path.is_file():
@@ -487,16 +734,10 @@ def merge_core_config_from_defaults(target_root: Path) -> None:
487
734
  "spawn/.core/config.yaml is empty or invalid; restore it or run spawn init again."
488
735
  )
489
736
  try:
490
- current = CoreConfig.model_validate(raw_existing)
737
+ CoreConfig.model_validate(raw_existing)
491
738
  except Exception as e:
492
739
  raise SpawnError(f"invalid spawn/.core/config.yaml: {e}") from e
493
- default_set = set(default_core.agent_ignore)
494
- merged_ignore = list(default_core.agent_ignore)
495
- for g in current.agent_ignore:
496
- if g not in default_set:
497
- merged_ignore.append(g)
498
- out = CoreConfig(version=default_core.version, agent_ignore=merged_ignore)
499
- save_yaml(path, out.model_dump(by_alias=True))
740
+ save_yaml(path, default_core.model_dump(by_alias=True))
500
741
 
501
742
 
502
743
  def get_ext_agent_ignore(target_root: Path, extension: str) -> list[str]:
@@ -778,7 +1019,13 @@ def save_extension_navigation(
778
1019
  _strip_ext_sections_inplace(rr, extension)
779
1020
  _strip_ext_sections_inplace(rc, extension)
780
1021
  if read_required_files:
781
- rr.append({"ext": extension, "files": _nav_refs_to_files(read_required_files)})
1022
+ block: dict[str, Any] = {"ext": extension, "files": _nav_refs_to_files(read_required_files)}
1023
+ cfg_opt = _optional_ext_config(target_root, extension)
1024
+ if cfg_opt is not None:
1025
+ hints_nav = _normalized_extension_global_hints_for_navigation(cfg_opt)
1026
+ if hints_nav:
1027
+ block["hints"] = hints_nav
1028
+ rr.append(block)
782
1029
  if read_contextual_files:
783
1030
  req_norm = {_norm_read_path(r.file) for r in read_required_files}
784
1031
  contextual_only = [
@@ -852,7 +1099,14 @@ def save_rules_navigation(target_root: Path) -> None:
852
1099
  rk = posix_norm(str(entry['path']))
853
1100
  exists = rk in paths_on_disk and (Path(target_root) / Path(rk)).is_file()
854
1101
  if exists:
855
- out.append({'path': rk, 'description': str(entry.get('description') or 'Local rule file.')})
1102
+ row: dict[str, Any] = {
1103
+ 'path': rk,
1104
+ 'description': str(entry.get('description') or 'Local rule file.'),
1105
+ }
1106
+ hint_val = entry.get('hint')
1107
+ if isinstance(hint_val, str):
1108
+ row['hint'] = hint_val
1109
+ out.append(row)
856
1110
  else:
857
1111
  warnings.warn(
858
1112
  f"Removed missing rule path from navigation: {rk}",
@@ -929,7 +1183,7 @@ __all__ = [
929
1183
  "list_ides",
930
1184
  "list_mcp",
931
1185
  "list_skills",
932
- "merge_core_config_from_defaults",
1186
+ "sync_core_config_from_defaults",
933
1187
  "METADATA_TEMP_MAX_AGE_SECONDS",
934
1188
  "normalize_skill_name",
935
1189
  "prune_metadata_temp",
@@ -937,6 +1191,7 @@ __all__ = [
937
1191
  "remove_from_global_gitignore",
938
1192
  "remove_ide_metadata_dir",
939
1193
  "remove_ide_from_list",
1194
+ "rollup_hints_for_agents",
940
1195
  "save_agent_ignore_list",
941
1196
  "save_extension_navigation",
942
1197
  "save_git_ignore_list",
@@ -945,5 +1200,6 @@ __all__ = [
945
1200
  "save_skills_rendered",
946
1201
  "supported_ide_keys",
947
1202
  "validate_rendered_identity",
1203
+ "warn_if_agents_hints_exceed_measurement",
948
1204
  ]
949
1205
 
@@ -36,7 +36,8 @@ def _mandatory_reads_for_render(skill: SkillMetadata) -> list[SkillFileRef]:
36
36
 
37
37
  def render_skill_md(skill: SkillMetadata) -> str:
38
38
  """Render normalized skill to Markdown with frontmatter, skill body,
39
- mandatory reads (navigation path last), and contextual reads."""
39
+ hints (stable order from metadata), mandatory reads (navigation path last),
40
+ and contextual reads."""
40
41
  lines = [
41
42
  "---",
42
43
  f"name: {skill.name}",
@@ -45,8 +46,13 @@ def render_skill_md(skill: SkillMetadata) -> str:
45
46
  "",
46
47
  skill.content,
47
48
  "",
48
- "Mandatory reads:",
49
49
  ]
50
+ if skill.hints:
51
+ lines += ["Hints:"]
52
+ for h in skill.hints:
53
+ lines += [f"- {h}"]
54
+ lines += [""]
55
+ lines += ["Mandatory reads:"]
50
56
  for ref in _mandatory_reads_for_render(skill):
51
57
  lines += [f"- `{ref.file}` - {ref.description}"]
52
58
  lines += [""]
@@ -40,6 +40,15 @@ class SkillEntry(BaseModel):
40
40
  required_read: list[str] = Field(default_factory=list, alias="required-read")
41
41
 
42
42
 
43
+ class ExtensionHints(BaseModel):
44
+ """Optional extension-authored hints (plain strings only)."""
45
+
46
+ model_config = ConfigDict(populate_by_name=True)
47
+
48
+ global_: list[str] = Field(default_factory=list, alias="global")
49
+ local: list[str] = Field(default_factory=list)
50
+
51
+
43
52
  class SetupConfig(BaseModel):
44
53
  model_config = ConfigDict(populate_by_name=True)
45
54
 
@@ -62,6 +71,7 @@ class ExtensionConfig(BaseModel):
62
71
  git_ignore: list[str] = Field(default_factory=list, alias="git-ignore")
63
72
  skills: dict[str, SkillEntry] = Field(default_factory=dict)
64
73
  setup: Optional[SetupConfig] = None
74
+ hints: Optional[ExtensionHints] = None
65
75
 
66
76
 
67
77
  class CoreConfig(BaseModel):
@@ -25,5 +25,6 @@ class SkillMetadata(BaseModel):
25
25
  name: str
26
26
  description: str
27
27
  content: str
28
+ hints: list[str] = Field(default_factory=list)
28
29
  required_read: list[SkillFileRef] = Field(default_factory=list)
29
30
  auto_read: list[SkillFileRef] = Field(default_factory=list)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spawn-cli
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: A minimal installable Python CLI package.
5
5
  License: MIT
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -27,7 +27,7 @@ Spawn is a framework for building **AIDD** (AI-driven development) methodologies
27
27
 
28
28
  Several methodologies in one repo stay aligned because Spawn maintains **one shared navigation index**: [`spawn/navigation.yaml`](spec/design/data-structure.md) aggregates mandatory and contextual reads from **every installed extension** plus local [`spawn/rules/`](spec/design/user-guide.md) (refreshed with `spawn rules refresh`). Agents are steered through that file instead of chasing disconnected README fragments.
29
29
 
30
- On each install or refresh, the CLI **regenerates IDE-facing artifacts** so skills and entry points stay consistent across extensions: rendered skill bodies include **paths and descriptions** wired from navigation (required reads, contextual hints, rule files), MCP snippets normalize server definitions per IDE adapter, and ignore lists merge without manual copy-paste. The net effect is that methodology files, generated skill links, and repo rules **compose** rather than contradict each other.
30
+ On each install or refresh, the CLI **regenerates IDE-facing artifacts** so skills and entry points stay consistent across extensions: rendered skill bodies include **paths and descriptions** wired from navigation (required reads, contextual hints, rule files), MCP snippets normalize server definitions per IDE adapter, and ignore lists merge without manual copy-paste. The net effect is that methodology files, generated skill links, and repo rules **compose** rather than contradict each other. Optional `hints` add short reminders to rendered skills and the Spawn-managed entry block.
31
31
 
32
32
  **Example:** [**spawn-ext-spectask**](https://github.com/noant/spawn-ext-spectask) (Spec-Tasks methodology: spec layout, skills, design index) and [**spawn-ext-creator**](https://github.com/noant/spawn-ext-creator) (authoring flows, including agent-guided extension bootstrap) can be installed in the **same** repository and still share that single index and merged IDE outputs.
33
33
 
@@ -189,7 +189,7 @@ Read **[spec/design/extension-author-guide.md](spec/design/extension-author-guid
189
189
 
190
190
  **Minimal mental model:**
191
191
 
192
- - `**extsrc/config.yaml`** — `schema`, `version`, stable `**name`** (install path `spawn/.extend/<name>/`), plus `files`, `skills`, ignores, etc.
192
+ - `**extsrc/config.yaml`** — `schema`, `version`, stable `**name`** (install path `spawn/.extend/<name>/`), plus `files`, `skills`, ignores, optional `hints`, etc.
193
193
  - `**extsrc/files/**` — template tree mirrored into the target repo according to `files:` entries.
194
194
  - `**spawn extension check . --strict**` — validate before publishing or tagging a release.
195
195
 
File without changes
File without changes
File without changes