@event4u/agent-config 1.16.0 → 1.17.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.
Files changed (203) hide show
  1. package/.agent-src/commands/{agents-audit.md → agents/audit.md} +4 -3
  2. package/.agent-src/commands/{agents-cleanup.md → agents/cleanup.md} +12 -6
  3. package/.agent-src/commands/{agents-prepare.md → agents/prepare.md} +4 -3
  4. package/.agent-src/commands/agents.md +46 -0
  5. package/.agent-src/commands/{chat-history-checkpoint.md → chat-history/checkpoint.md} +4 -4
  6. package/.agent-src/commands/{chat-history-clear.md → chat-history/clear.md} +4 -4
  7. package/.agent-src/commands/{chat-history-resume.md → chat-history/resume.md} +4 -4
  8. package/.agent-src/commands/chat-history/show.md +107 -0
  9. package/.agent-src/commands/chat-history.md +33 -89
  10. package/.agent-src/commands/{commit-in-chunks.md → commit/in-chunks.md} +15 -13
  11. package/.agent-src/commands/commit.md +22 -2
  12. package/.agent-src/commands/{context-create.md → context/create.md} +4 -3
  13. package/.agent-src/commands/{context-refactor.md → context/refactor.md} +4 -3
  14. package/.agent-src/commands/context.md +44 -0
  15. package/.agent-src/commands/{copilot-agents-init.md → copilot-agents/init.md} +4 -3
  16. package/.agent-src/commands/{copilot-agents-optimize.md → copilot-agents/optimize.md} +4 -3
  17. package/.agent-src/commands/copilot-agents.md +44 -0
  18. package/.agent-src/commands/council/default.md +221 -0
  19. package/.agent-src/commands/{council-design.md → council/design.md} +6 -5
  20. package/.agent-src/commands/{council-optimize.md → council/optimize.md} +7 -6
  21. package/.agent-src/commands/{council-pr.md → council/pr.md} +6 -5
  22. package/.agent-src/commands/council.md +47 -212
  23. package/.agent-src/commands/{create-pr-description.md → create-pr/description-only.md} +4 -2
  24. package/.agent-src/commands/create-pr.md +26 -5
  25. package/.agent-src/commands/{feature-dev.md → feature/dev.md} +5 -10
  26. package/.agent-src/commands/{feature-explore.md → feature/explore.md} +4 -8
  27. package/.agent-src/commands/{feature-plan.md → feature/plan.md} +4 -8
  28. package/.agent-src/commands/{feature-refactor.md → feature/refactor.md} +4 -8
  29. package/.agent-src/commands/{feature-roadmap.md → feature/roadmap.md} +6 -10
  30. package/.agent-src/commands/feature.md +6 -12
  31. package/.agent-src/commands/{fix-ci.md → fix/ci.md} +4 -8
  32. package/.agent-src/commands/{fix-portability.md → fix/portability.md} +4 -8
  33. package/.agent-src/commands/{fix-pr-bot-comments.md → fix/pr-bots.md} +4 -8
  34. package/.agent-src/commands/{fix-pr-developer-comments.md → fix/pr-developers.md} +4 -8
  35. package/.agent-src/commands/{fix-pr-comments.md → fix/pr.md} +7 -11
  36. package/.agent-src/commands/{fix-references.md → fix/refs.md} +4 -8
  37. package/.agent-src/commands/{fix-seeder.md → fix/seeder.md} +4 -8
  38. package/.agent-src/commands/fix.md +7 -13
  39. package/.agent-src/commands/{do-and-judge.md → judge/on-diff.md} +4 -3
  40. package/.agent-src/commands/judge/solo.md +90 -0
  41. package/.agent-src/commands/{do-in-steps.md → judge/steps.md} +4 -3
  42. package/.agent-src/commands/judge.md +35 -70
  43. package/.agent-src/commands/{memory-add.md → memory/add.md} +4 -3
  44. package/.agent-src/commands/{memory-full.md → memory/load.md} +4 -3
  45. package/.agent-src/commands/{memory-promote.md → memory/promote.md} +4 -3
  46. package/.agent-src/commands/{propose-memory.md → memory/propose.md} +4 -3
  47. package/.agent-src/commands/memory.md +48 -0
  48. package/.agent-src/commands/{module-create.md → module/create.md} +4 -3
  49. package/.agent-src/commands/{module-explore.md → module/explore.md} +4 -3
  50. package/.agent-src/commands/module.md +44 -0
  51. package/.agent-src/commands/{optimize-agents.md → optimize/agents.md} +4 -8
  52. package/.agent-src/commands/{optimize-augmentignore.md → optimize/augmentignore.md} +4 -9
  53. package/.agent-src/commands/{optimize-rtk-filters.md → optimize/rtk.md} +4 -8
  54. package/.agent-src/commands/{optimize-skills.md → optimize/skills.md} +4 -8
  55. package/.agent-src/commands/optimize.md +4 -10
  56. package/.agent-src/commands/{override-create.md → override/create.md} +4 -3
  57. package/.agent-src/commands/{override-manage.md → override/manage.md} +4 -3
  58. package/.agent-src/commands/override.md +44 -0
  59. package/.agent-src/commands/{roadmap-create.md → roadmap/create.md} +4 -3
  60. package/.agent-src/commands/{roadmap-execute.md → roadmap/execute.md} +4 -3
  61. package/.agent-src/commands/roadmap.md +44 -0
  62. package/.agent-src/commands/{tests-create.md → tests/create.md} +4 -3
  63. package/.agent-src/commands/{tests-execute.md → tests/execute.md} +4 -3
  64. package/.agent-src/commands/tests.md +44 -0
  65. package/.agent-src/contexts/communication/rules-auto/artifact-engagement-recording-mechanics.md +72 -0
  66. package/.agent-src/contexts/communication/rules-auto/augment-portability-mechanics.md +79 -0
  67. package/.agent-src/contexts/communication/rules-auto/augment-source-of-truth-mechanics.md +98 -0
  68. package/.agent-src/contexts/communication/rules-auto/cli-output-handling-mechanics.md +87 -0
  69. package/.agent-src/contexts/communication/rules-auto/command-suggestion-policy-mechanics.md +62 -0
  70. package/.agent-src/contexts/communication/rules-auto/docs-sync-mechanics.md +78 -0
  71. package/.agent-src/contexts/communication/rules-auto/package-ci-checks-mechanics.md +85 -0
  72. package/.agent-src/contexts/communication/rules-auto/review-routing-awareness-mechanics.md +65 -0
  73. package/.agent-src/contexts/communication/rules-auto/roadmap-progress-sync-mechanics.md +78 -0
  74. package/.agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +62 -0
  75. package/.agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +55 -0
  76. package/.agent-src/contexts/communication/rules-auto/ui-audit-gate-mechanics.md +53 -0
  77. package/.agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +77 -0
  78. package/.agent-src/contexts/judges/no-consolidate-rationale.md +102 -0
  79. package/.agent-src/contexts/judges/persona-voice-rubric.md +140 -0
  80. package/.agent-src/rules/artifact-engagement-recording.md +13 -69
  81. package/.agent-src/rules/ask-when-uncertain.md +27 -42
  82. package/.agent-src/rules/augment-portability.md +15 -61
  83. package/.agent-src/rules/augment-source-of-truth.md +27 -93
  84. package/.agent-src/rules/cli-output-handling.md +10 -76
  85. package/.agent-src/rules/command-suggestion-policy.md +18 -59
  86. package/.agent-src/rules/commit-conventions.md +17 -14
  87. package/.agent-src/rules/direct-answers.md +34 -49
  88. package/.agent-src/rules/docker-commands.md +5 -5
  89. package/.agent-src/rules/docs-sync.md +15 -69
  90. package/.agent-src/rules/language-and-tone.md +48 -72
  91. package/.agent-src/rules/missing-tool-handling.md +28 -22
  92. package/.agent-src/rules/no-cheap-questions.md +45 -52
  93. package/.agent-src/rules/no-roadmap-references.md +73 -0
  94. package/.agent-src/rules/package-ci-checks.md +21 -61
  95. package/.agent-src/rules/preservation-guard.md +64 -29
  96. package/.agent-src/rules/review-routing-awareness.md +24 -43
  97. package/.agent-src/rules/roadmap-progress-sync.md +10 -71
  98. package/.agent-src/rules/security-sensitive-stop.md +8 -8
  99. package/.agent-src/rules/skill-quality.md +16 -48
  100. package/.agent-src/rules/slash-command-routing-policy.md +7 -4
  101. package/.agent-src/rules/think-before-action.md +52 -42
  102. package/.agent-src/rules/tool-safety.md +19 -16
  103. package/.agent-src/rules/ui-audit-gate.md +24 -38
  104. package/.agent-src/rules/user-interaction.md +13 -68
  105. package/.agent-src/skills/ai-council/SKILL.md +2 -0
  106. package/.agent-src/skills/api-testing/SKILL.md +1 -1
  107. package/.agent-src/skills/check-refs/SKILL.md +59 -40
  108. package/.agent-src/skills/conventional-commits-writing/SKILL.md +86 -28
  109. package/.agent-src/skills/copilot-agents-optimization/SKILL.md +5 -5
  110. package/.agent-src/skills/developer-like-execution/SKILL.md +4 -4
  111. package/.agent-src/skills/finishing-a-development-branch/SKILL.md +101 -65
  112. package/.agent-src/skills/flux/SKILL.md +30 -10
  113. package/.agent-src/skills/github-ci/SKILL.md +2 -2
  114. package/.agent-src/skills/judge-code-quality/SKILL.md +7 -8
  115. package/.agent-src/skills/judge-security-auditor/SKILL.md +4 -5
  116. package/.agent-src/skills/judge-test-coverage/SKILL.md +3 -4
  117. package/.agent-src/skills/lint-skills/SKILL.md +57 -39
  118. package/.agent-src/skills/md-language-check/SKILL.md +61 -39
  119. package/.agent-src/skills/override-management/SKILL.md +5 -5
  120. package/.agent-src/skills/quality-tools/SKILL.md +2 -2
  121. package/.agent-src/skills/react-shadcn-ui/SKILL.md +116 -43
  122. package/.agent-src/skills/readme-reviewer/SKILL.md +30 -29
  123. package/.agent-src/skills/readme-writing/SKILL.md +78 -53
  124. package/.agent-src/skills/readme-writing-package/SKILL.md +50 -47
  125. package/.agent-src/skills/receiving-code-review/SKILL.md +52 -47
  126. package/.agent-src/skills/refine-prompt/SKILL.md +0 -1
  127. package/.agent-src/skills/requesting-code-review/SKILL.md +35 -30
  128. package/.agent-src/skills/security/SKILL.md +7 -2
  129. package/.agent-src/skills/security-audit/SKILL.md +7 -3
  130. package/.agent-src/skills/systematic-debugging/SKILL.md +68 -60
  131. package/.agent-src/skills/test-driven-development/SKILL.md +59 -57
  132. package/.agent-src/skills/test-performance/SKILL.md +0 -1
  133. package/.agent-src/skills/traefik/SKILL.md +4 -4
  134. package/.agent-src/skills/verify-completion-evidence/SKILL.md +28 -26
  135. package/.claude-plugin/marketplace.json +22 -11
  136. package/AGENTS.md +2 -2
  137. package/CHANGELOG.md +90 -1
  138. package/README.md +18 -17
  139. package/docs/architecture.md +4 -6
  140. package/docs/catalog.md +67 -39
  141. package/docs/contracts/STABILITY.md +13 -7
  142. package/docs/contracts/adr-chat-history-split.md +1 -3
  143. package/docs/contracts/adr-command-suggestion.md +0 -2
  144. package/docs/contracts/adr-implement-ticket-runtime.md +1 -2
  145. package/docs/contracts/adr-product-ui-track.md +3 -6
  146. package/docs/contracts/adr-prompt-driven-execution.md +3 -4
  147. package/docs/contracts/agent-memory-contract.md +6 -11
  148. package/docs/contracts/artifact-engagement-flow.md +6 -9
  149. package/docs/contracts/command-clusters.md +56 -46
  150. package/docs/contracts/command-suggestion-flow.md +1 -3
  151. package/docs/contracts/context-paths.md +99 -0
  152. package/docs/contracts/file-ownership-matrix.json +6722 -0
  153. package/docs/contracts/file-ownership-matrix.md +134 -0
  154. package/docs/contracts/implement-ticket-flow.md +6 -9
  155. package/docs/contracts/linear-ai-rules-inclusion.md +0 -1
  156. package/docs/contracts/linear-ai-three-layers.md +0 -2
  157. package/docs/contracts/load-context-budget-model.md +178 -0
  158. package/docs/contracts/load-context-schema.md +1 -3
  159. package/docs/contracts/rule-interactions.md +0 -1
  160. package/docs/contracts/rule-priority-hierarchy.md +1 -1
  161. package/docs/contracts/ui-track-flow.md +7 -17
  162. package/docs/customization.md +2 -0
  163. package/docs/getting-started.md +5 -4
  164. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +100 -0
  165. package/package.json +1 -1
  166. package/scripts/_one_off_phase4_dispatch_latency.py +108 -0
  167. package/scripts/_one_off_phase6_trigger_jaccard.py +92 -0
  168. package/scripts/_phase2_shim_helper.py +109 -0
  169. package/scripts/agent-config +10 -0
  170. package/scripts/ai_council/_one_off_2a4_acceptance.py +208 -0
  171. package/scripts/ai_council/_one_off_context_layer_v1_estimate.py +67 -0
  172. package/scripts/ai_council/_one_off_context_layer_v1_review.py +292 -0
  173. package/scripts/ai_council/_one_off_followups_review.py +259 -0
  174. package/scripts/ai_council/_one_off_nondestructive_inline_audit.py +209 -0
  175. package/scripts/ai_council/_one_off_phase_2a_budget_rebalance.py +257 -0
  176. package/scripts/ai_council/_one_off_phase_2a_post_revert.py +197 -0
  177. package/scripts/ai_council/_one_off_rule_hardening_v1.py +251 -0
  178. package/scripts/ai_council/_one_off_structural_open_questions.py +232 -0
  179. package/scripts/ai_council/_one_off_structural_optimization.py +144 -0
  180. package/scripts/ai_council/_one_off_structural_v3_gaps.py +252 -0
  181. package/scripts/ai_council/_one_off_structural_v3_review.py +240 -0
  182. package/scripts/check_always_budget.py +363 -45
  183. package/scripts/check_cluster_patterns.py +159 -0
  184. package/scripts/check_command_count_messaging.py +14 -7
  185. package/scripts/check_context_paths.py +201 -0
  186. package/scripts/check_no_roadmap_refs.py +155 -0
  187. package/scripts/check_phase_coupling.py +148 -0
  188. package/scripts/check_portability.py +2 -0
  189. package/scripts/check_references.py +29 -2
  190. package/scripts/check_safety_floor_untouched.py +125 -0
  191. package/scripts/command_suggester/loader.py +4 -1
  192. package/scripts/compress.py +59 -13
  193. package/scripts/generate_index.py +6 -2
  194. package/scripts/generate_ownership_matrix.py +323 -0
  195. package/scripts/hooks/augment-roadmap-progress.sh +57 -0
  196. package/scripts/install.py +49 -28
  197. package/scripts/lint_no_new_atomic_commands.py +12 -11
  198. package/scripts/requirements-evals.txt +1 -0
  199. package/scripts/roadmap_progress_hook.py +159 -0
  200. package/scripts/schemas/command.schema.json +4 -3
  201. package/scripts/skill_linter.py +1 -0
  202. package/scripts/sync_agent_settings.py +25 -2
  203. package/scripts/update_counts.py +7 -0
@@ -26,7 +26,10 @@ def load_commands(commands_dir: Path) -> list[CommandSpec]:
26
26
  this loader.
27
27
  """
28
28
  specs: list[CommandSpec] = []
29
- for path in sorted(commands_dir.glob("*.md")):
29
+ for path in sorted(commands_dir.rglob("*.md")):
30
+ # Skip cluster authoring docs — not commands.
31
+ if path.name == "AGENTS.md":
32
+ continue
30
33
  text = path.read_text(encoding="utf-8")
31
34
  data, _offset = parse_frontmatter(text)
32
35
  if data is None:
@@ -312,6 +312,29 @@ def generate_gemini_md() -> None:
312
312
  print(" ✅ Created GEMINI.md → AGENTS.md symlink")
313
313
 
314
314
 
315
+ def _command_slug(source_file: Path) -> str:
316
+ """Return the flat .claude/skills/ slug for a command source file.
317
+
318
+ Top-level commands keep their stem (`commit.md` → `commit`). Nested
319
+ commands flatten the relative path with `-` (`council/default.md` →
320
+ `council-default`). Keeps slug collisions out of `.claude/skills/`
321
+ while preserving native nested invocation in `.agent-src/commands/`.
322
+ """
323
+ rel = source_file.relative_to(COMMANDS_SOURCE)
324
+ return "-".join(rel.with_suffix("").parts)
325
+
326
+
327
+ def _iter_commands():
328
+ """Yield (source_file, slug) for every command .md file (recursive)."""
329
+ if not COMMANDS_SOURCE.exists():
330
+ return
331
+ for source_file in sorted(COMMANDS_SOURCE.rglob("*.md")):
332
+ # Skip the cluster AGENTS.md authoring doc (not a command).
333
+ if source_file.name == "AGENTS.md":
334
+ continue
335
+ yield source_file, _command_slug(source_file)
336
+
337
+
315
338
  def generate_claude_skills() -> None:
316
339
  """Create .claude/skills/ symlinks for ALL skills in .agent-src/skills/.
317
340
  """
@@ -321,16 +344,14 @@ def generate_claude_skills() -> None:
321
344
 
322
345
  # All skill directories in .agent-src/skills/
323
346
  skills = sorted([d.name for d in SKILLS_SOURCE.iterdir() if d.is_dir()])
324
- # All command names (to protect from stale cleanup)
325
- command_names = set()
326
- if COMMANDS_SOURCE.exists():
327
- command_names = {f.stem for f in COMMANDS_SOURCE.glob("*.md")}
347
+ # All command slugs (to protect from stale cleanup)
348
+ command_slugs = {slug for _, slug in _iter_commands()}
328
349
 
329
350
  CLAUDE_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
330
351
 
331
352
  # Clean stale symlinks (but not converted commands or README)
332
353
  for item in CLAUDE_SKILLS_DIR.iterdir():
333
- if item.is_symlink() and item.name not in skills and item.name not in command_names and item.name != "README.md":
354
+ if item.is_symlink() and item.name not in skills and item.name not in command_slugs and item.name != "README.md":
334
355
  item.unlink()
335
356
 
336
357
  count = 0
@@ -357,11 +378,15 @@ def extract_description_from_md(content: str) -> str:
357
378
 
358
379
 
359
380
  def generate_claude_commands() -> None:
360
- """Create .claude/skills/{name}/SKILL.md symlinks for ALL Augment commands.
381
+ """Create .claude/skills/{slug}/SKILL.md symlinks for ALL Augment commands.
361
382
 
362
383
  Commands in .agent-src/commands/ are the single source of truth.
363
384
  They must include name: and disable-model-invocation: true in frontmatter
364
385
  (added once, then maintained as part of the command file).
386
+
387
+ Top-level commands use their filename stem as the slug. Nested
388
+ cluster commands (e.g. `commands/council/default.md`) are flattened
389
+ to `council-default` so directories never collide in `.claude/skills/`.
365
390
  """
366
391
  if not COMMANDS_SOURCE.exists():
367
392
  print(" ⚠️ .agent-src/commands/ not found — skipping commands")
@@ -374,32 +399,53 @@ def generate_claude_commands() -> None:
374
399
  if SKILLS_SOURCE.exists():
375
400
  skill_names = {d.name for d in SKILLS_SOURCE.iterdir() if d.is_dir()}
376
401
 
402
+ # Track current command slugs for stale-directory cleanup
403
+ current_slugs: set[str] = set()
377
404
  count = 0
378
405
  skipped = 0
379
- for source_file in sorted(COMMANDS_SOURCE.glob("*.md")):
380
- name = source_file.stem
381
-
406
+ for source_file, slug in _iter_commands():
382
407
  # Skip if a real skill with the same name exists — skill takes priority
383
- if name in skill_names:
408
+ if slug in skill_names:
384
409
  skipped += 1
385
410
  continue
386
411
 
412
+ current_slugs.add(slug)
413
+
387
414
  # Create skill directory (real dir, symlinked SKILL.md inside)
388
- skill_dir = CLAUDE_SKILLS_DIR / name
415
+ skill_dir = CLAUDE_SKILLS_DIR / slug
389
416
  skill_dir.mkdir(parents=True, exist_ok=True)
390
417
 
391
418
  skill_file = skill_dir / "SKILL.md"
392
419
  if skill_file.exists() or skill_file.is_symlink():
393
420
  skill_file.unlink()
394
421
 
395
- # Symlink: .claude/skills/{name}/SKILL.md → ../../../.agent-src/commands/{name}.md
396
- rel_target = Path("../../../.agent-src/commands") / source_file.name
422
+ # Symlink: .claude/skills/{slug}/SKILL.md → ../../../.agent-src/commands/<rel-path>
423
+ rel_path = source_file.relative_to(COMMANDS_SOURCE)
424
+ rel_target = Path("../../../.agent-src/commands") / rel_path
397
425
  skill_file.symlink_to(rel_target)
398
426
  count += 1
399
427
 
428
+ # Clean stale command skill directories — real dirs from removed commands.
429
+ # Only delete if the directory contains exactly the SKILL.md symlink we created.
430
+ removed_dirs = 0
431
+ for item in CLAUDE_SKILLS_DIR.iterdir():
432
+ if not item.is_dir() or item.is_symlink():
433
+ continue
434
+ if item.name in skill_names or item.name in current_slugs:
435
+ continue
436
+ skill_md = item / "SKILL.md"
437
+ if skill_md.is_symlink():
438
+ entries = list(item.iterdir())
439
+ if len(entries) == 1 and entries[0].name == "SKILL.md":
440
+ skill_md.unlink()
441
+ item.rmdir()
442
+ removed_dirs += 1
443
+
400
444
  msg = f" ✅ Created {count} command symlinks in .claude/skills/"
401
445
  if skipped:
402
446
  msg += f" ({skipped} skipped — same-name skill exists)"
447
+ if removed_dirs:
448
+ msg += f" ({removed_dirs} stale dirs removed)"
403
449
  print(msg)
404
450
 
405
451
 
@@ -95,7 +95,10 @@ def _collect_rules() -> list[Entry]:
95
95
 
96
96
  def _collect_commands() -> list[Entry]:
97
97
  out = []
98
- for cmd_md in sorted((SRC / "commands").glob("*.md")):
98
+ cmd_dir = SRC / "commands"
99
+ for cmd_md in sorted(cmd_dir.rglob("*.md")):
100
+ if cmd_md.name == "AGENTS.md":
101
+ continue
99
102
  fm = _parse_frontmatter(cmd_md.read_text(encoding="utf-8"))
100
103
  is_shim = bool(fm.get("superseded_by"))
101
104
  extra = ""
@@ -103,12 +106,13 @@ def _collect_commands() -> list[Entry]:
103
106
  extra = f"shim → /{fm['superseded_by']}"
104
107
  elif fm.get("cluster"):
105
108
  extra = f"cluster: {fm['cluster']}"
109
+ rel = cmd_md.relative_to(cmd_dir)
106
110
  out.append(Entry(
107
111
  kind="shim" if is_shim else "command",
108
112
  name=fm.get("name") or cmd_md.stem,
109
113
  description=_truncate(fm.get("description", "")),
110
114
  extra=extra,
111
- path=f".agent-src.uncompressed/commands/{cmd_md.name}",
115
+ path=f".agent-src.uncompressed/commands/{rel}",
112
116
  ))
113
117
  return out
114
118
 
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env python3
2
+ """Generate the file-ownership matrix.
3
+
4
+ Produces:
5
+
6
+ * docs/contracts/file-ownership-matrix.json (machine, internal-locked)
7
+ * agents/contexts/structural/file-ownership-matrix.md (human-readable)
8
+
9
+ Walks `.agent-src.uncompressed/{rules,skills,commands,contexts,personas}/`,
10
+ parses frontmatter for `load_context:` / `load_context_eager:`, scans
11
+ markdown bodies for inline links to `.md` files inside the scanned roots,
12
+ and emits READ_ONLY edges plus depth-2 transitive closure of load_context
13
+ chains. Depth-3 chains abort the build (matches the 0.2.4 nesting cap).
14
+
15
+ Contract: docs/contracts/file-ownership-matrix.md
16
+ Roadmap: road-to-structural-optimization.md § 0.1
17
+
18
+ Modes:
19
+ --check Regenerate to memory and diff against committed JSON.
20
+ Exit 0 if identical, 1 if drifted.
21
+ (default) Regenerate JSON + MD in place; exit 0 on success.
22
+
23
+ Exit codes: 0 = ok, 1 = drift (--check), 2 = depth-3 chain, 3 = internal.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import json
29
+ import re
30
+ import sys
31
+ from dataclasses import dataclass, field
32
+ from pathlib import Path
33
+ from typing import Iterable
34
+
35
+ import yaml
36
+
37
+ ROOT = Path(__file__).resolve().parent.parent
38
+ SRC_ROOT = ROOT / ".agent-src.uncompressed"
39
+
40
+ SCAN_DIRS = ("rules", "skills", "commands", "contexts", "personas")
41
+
42
+ JSON_OUT = ROOT / "docs" / "contracts" / "file-ownership-matrix.json"
43
+ MD_OUT = ROOT / "agents" / "contexts" / "structural" / "file-ownership-matrix.md"
44
+
45
+ LINK_RE = re.compile(r"\]\(([^)]+\.md)(?:#[^)]*)?\)")
46
+
47
+
48
+ @dataclass
49
+ class FileEntry:
50
+ path: str
51
+ kind: str
52
+ rule_type: str | None = None
53
+ load_context: list[str] = field(default_factory=list)
54
+ load_context_eager: list[str] = field(default_factory=list)
55
+
56
+
57
+ @dataclass
58
+ class Edge:
59
+ source: str
60
+ target: str
61
+ type: str
62
+ via: str
63
+ depth: int
64
+
65
+
66
+ def _rel(p: Path) -> str:
67
+ return p.relative_to(ROOT).as_posix()
68
+
69
+
70
+ def _kind_for(rel: str) -> str:
71
+ parts = rel.split("/")
72
+ if len(parts) >= 3 and parts[0] == ".agent-src.uncompressed":
73
+ return parts[1].rstrip("s") if parts[1] != "personas" else "persona"
74
+ return "unknown"
75
+
76
+
77
+ def _parse_frontmatter(p: Path) -> dict:
78
+ text = p.read_text(encoding="utf-8")
79
+ if not text.startswith("---\n"):
80
+ return {}
81
+ end = text.find("\n---\n", 4)
82
+ if end == -1:
83
+ return {}
84
+ try:
85
+ data = yaml.safe_load(text[4:end])
86
+ except yaml.YAMLError:
87
+ return {}
88
+ return data if isinstance(data, dict) else {}
89
+
90
+
91
+ def _collect_files(src_root: Path) -> list[Path]:
92
+ out: list[Path] = []
93
+ for sub in SCAN_DIRS:
94
+ d = src_root / sub
95
+ if d.exists():
96
+ out.extend(sorted(d.rglob("*.md")))
97
+ return out
98
+
99
+
100
+ def _resolve(target: str, src_root: Path) -> Path | None:
101
+ """Resolve a path string (repo-relative or short) into an absolute Path
102
+ under src_root or the repo root. Return None if not under a scanned root."""
103
+ cand = src_root.parent / target if "/" in target else src_root / target
104
+ try:
105
+ rel = cand.resolve().relative_to(src_root.parent)
106
+ except ValueError:
107
+ return None
108
+ parts = rel.parts
109
+ if len(parts) >= 3 and parts[0] == ".agent-src.uncompressed" and parts[1] in SCAN_DIRS:
110
+ return cand if cand.exists() else None
111
+ return None
112
+
113
+
114
+ def build_matrix(src_root: Path) -> tuple[dict[str, FileEntry], list[Edge], list[str]]:
115
+ """Build the file map + edge list. Returns (files, edges, depth3_chains).
116
+
117
+ depth3_chains is non-empty iff the depth invariant is violated; the
118
+ caller must abort with exit code 2.
119
+ """
120
+ files: dict[str, FileEntry] = {}
121
+ for f in _collect_files(src_root):
122
+ rel = f.relative_to(src_root.parent).as_posix()
123
+ fm = _parse_frontmatter(f)
124
+ rtype = fm.get("type")
125
+ if isinstance(rtype, str):
126
+ rtype = rtype.strip('"').strip("'")
127
+ else:
128
+ rtype = None
129
+ lazy = fm.get("load_context") or []
130
+ eager = fm.get("load_context_eager") or []
131
+ if not isinstance(lazy, list):
132
+ lazy = []
133
+ if not isinstance(eager, list):
134
+ eager = []
135
+ files[rel] = FileEntry(
136
+ path=rel,
137
+ kind=_kind_for(rel),
138
+ rule_type=rtype,
139
+ load_context=[str(x) for x in lazy if isinstance(x, str)],
140
+ load_context_eager=[str(x) for x in eager if isinstance(x, str)],
141
+ )
142
+
143
+ edges: list[Edge] = []
144
+ for rel, entry in files.items():
145
+ for tgt in entry.load_context:
146
+ edges.append(Edge(rel, tgt, "READ_ONLY", "load_context", 1))
147
+ for tgt in entry.load_context_eager:
148
+ edges.append(Edge(rel, tgt, "READ_ONLY", "load_context_eager", 1))
149
+
150
+ # Body markdown links — only count edges to files we know about
151
+ for rel, entry in files.items():
152
+ body = (src_root.parent / rel).read_text(encoding="utf-8")
153
+ body = body.split("\n---\n", 1)[-1] if body.startswith("---\n") else body
154
+ seen_targets: set[str] = set()
155
+ for m in LINK_RE.finditer(body):
156
+ href = m.group(1).strip()
157
+ if href.startswith("http"):
158
+ continue
159
+ resolved = _resolve_link(rel, href, src_root)
160
+ if resolved is None or resolved == rel or resolved in seen_targets:
161
+ continue
162
+ if resolved in files:
163
+ seen_targets.add(resolved)
164
+ edges.append(Edge(rel, resolved, "READ_ONLY", "body_link", 1))
165
+
166
+ # Transitive closure on load_context* edges, depth 2; depth 3 aborts.
167
+ lc_edges_by_src: dict[str, list[str]] = {}
168
+ for e in edges:
169
+ if e.via in ("load_context", "load_context_eager"):
170
+ lc_edges_by_src.setdefault(e.source, []).append(e.target)
171
+
172
+ transitive: list[Edge] = []
173
+ depth3: list[str] = []
174
+ for src, lvl1_targets in lc_edges_by_src.items():
175
+ for t1 in lvl1_targets:
176
+ for t2 in lc_edges_by_src.get(t1, []):
177
+ if t2 == src or t2 == t1:
178
+ continue
179
+ transitive.append(Edge(src, t2, "READ_ONLY", "load_context_transitive", 2))
180
+ # depth-3 probe
181
+ for t3 in lc_edges_by_src.get(t2, []):
182
+ if t3 in (src, t1, t2):
183
+ continue
184
+ depth3.append(f"{src} → {t1} → {t2} → {t3}")
185
+
186
+ edges.extend(transitive)
187
+ for rel in files:
188
+ edges.append(Edge(rel, rel, "WRITE", "self", 0))
189
+
190
+ edges.sort(key=lambda e: (e.source, e.target, e.via, e.depth))
191
+ return files, edges, depth3
192
+
193
+
194
+ def _resolve_link(source_rel: str, href: str, src_root: Path) -> str | None:
195
+ """Resolve a markdown link href (relative to source file) to a repo-relative
196
+ path inside a scanned root, or None."""
197
+ if href.startswith(".agent-src.uncompressed/") or href.startswith("agents/"):
198
+ cand = (src_root.parent / href).resolve()
199
+ else:
200
+ base = (src_root.parent / source_rel).parent
201
+ cand = (base / href).resolve()
202
+ try:
203
+ rel = cand.relative_to(src_root.parent).as_posix()
204
+ except ValueError:
205
+ return None
206
+ parts = rel.split("/")
207
+ if len(parts) >= 3 and parts[0] == ".agent-src.uncompressed" and parts[1] in SCAN_DIRS:
208
+ return rel if cand.exists() else None
209
+ return None
210
+
211
+
212
+ def _to_json(files: dict[str, FileEntry], edges: list[Edge]) -> dict:
213
+ return {
214
+ "version": 1,
215
+ "generated_by": "scripts/generate_ownership_matrix.py",
216
+ "source_of_truth": ".agent-src.uncompressed/",
217
+ "files": {
218
+ rel: {
219
+ "kind": e.kind,
220
+ "rule_type": e.rule_type,
221
+ "load_context": e.load_context,
222
+ "load_context_eager": e.load_context_eager,
223
+ }
224
+ for rel, e in sorted(files.items())
225
+ },
226
+ "edges": [
227
+ {
228
+ "source": e.source,
229
+ "target": e.target,
230
+ "type": e.type,
231
+ "via": e.via,
232
+ "depth": e.depth,
233
+ }
234
+ for e in edges
235
+ ],
236
+ }
237
+
238
+
239
+ def _to_markdown(payload: dict) -> str:
240
+ lines: list[str] = [
241
+ "# File-ownership matrix (regenerated)",
242
+ "",
243
+ "> **Do not edit.** Regenerated by `scripts/generate_ownership_matrix.py`.",
244
+ "> Schema: [`docs/contracts/file-ownership-matrix.md`](../../../docs/contracts/file-ownership-matrix.md).",
245
+ "",
246
+ f"- Schema version: `{payload['version']}`",
247
+ f"- Source of truth: `{payload['source_of_truth']}`",
248
+ f"- Files indexed: **{len(payload['files'])}**",
249
+ f"- Edges (incl. self-WRITE): **{len(payload['edges'])}**",
250
+ "",
251
+ "## READ_ONLY edges",
252
+ "",
253
+ "| Source | Target | Via | Depth |",
254
+ "|---|---|---|---:|",
255
+ ]
256
+ ro = [e for e in payload["edges"] if e["type"] == "READ_ONLY"]
257
+ for e in ro:
258
+ lines.append(f"| `{e['source']}` | `{e['target']}` | `{e['via']}` | {e['depth']} |")
259
+ if not ro:
260
+ lines.append("| _(none)_ | | | |")
261
+ lines += [
262
+ "",
263
+ "## Files by kind",
264
+ "",
265
+ "| Kind | Count |",
266
+ "|---|---:|",
267
+ ]
268
+ counts: dict[str, int] = {}
269
+ for f in payload["files"].values():
270
+ counts[f["kind"]] = counts.get(f["kind"], 0) + 1
271
+ for k in sorted(counts):
272
+ lines.append(f"| `{k}` | {counts[k]} |")
273
+ lines.append("")
274
+ return "\n".join(lines)
275
+
276
+
277
+ def _write_outputs(payload: dict, json_out: Path, md_out: Path) -> None:
278
+ json_out.parent.mkdir(parents=True, exist_ok=True)
279
+ md_out.parent.mkdir(parents=True, exist_ok=True)
280
+ json_out.write_text(json.dumps(payload, indent=2, sort_keys=False) + "\n", encoding="utf-8")
281
+ md_out.write_text(_to_markdown(payload) + "\n", encoding="utf-8")
282
+
283
+
284
+ def main(argv: Iterable[str] | None = None) -> int:
285
+ ap = argparse.ArgumentParser(description=__doc__)
286
+ ap.add_argument("--check", action="store_true",
287
+ help="Regenerate to memory and diff against committed JSON.")
288
+ args = ap.parse_args(list(argv) if argv is not None else None)
289
+
290
+ if not SRC_ROOT.is_dir():
291
+ print(f"❌ source dir missing: {SRC_ROOT}", file=sys.stderr)
292
+ return 3
293
+
294
+ files, edges, depth3 = build_matrix(SRC_ROOT)
295
+ if depth3:
296
+ print("❌ load_context depth-3 chain detected (limit is 2):", file=sys.stderr)
297
+ for chain in depth3:
298
+ print(f" 🔴 {chain}", file=sys.stderr)
299
+ return 2
300
+
301
+ payload = _to_json(files, edges)
302
+
303
+ if args.check:
304
+ if not JSON_OUT.exists():
305
+ print(f"❌ {JSON_OUT.relative_to(ROOT)} not committed; run `task generate-ownership-matrix`",
306
+ file=sys.stderr)
307
+ return 1
308
+ committed = json.loads(JSON_OUT.read_text(encoding="utf-8"))
309
+ if committed != payload:
310
+ print("❌ ownership matrix is stale — run `task generate-ownership-matrix` and commit",
311
+ file=sys.stderr)
312
+ return 1
313
+ print(f"✅ ownership matrix in sync ({len(files)} files, {len(edges)} edges)")
314
+ return 0
315
+
316
+ _write_outputs(payload, JSON_OUT, MD_OUT)
317
+ print(f"✅ wrote {JSON_OUT.relative_to(ROOT)} ({len(files)} files, {len(edges)} edges)")
318
+ print(f"✅ wrote {MD_OUT.relative_to(ROOT)}")
319
+ return 0
320
+
321
+
322
+ if __name__ == "__main__":
323
+ sys.exit(main())
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ # Augment Code lifecycle-hook trampoline for roadmap-progress-sync.
3
+ #
4
+ # Augment requires hook scripts to use the .sh extension and live at
5
+ # either a system path (/etc/augment/...) or user scope
6
+ # (~/.augment/...). This trampoline lives at user scope and dispatches
7
+ # every PostToolUse event to whichever workspace fired it, so a single
8
+ # install covers every project that has ./agent-config available.
9
+ #
10
+ # Behaviour:
11
+ # - Read the JSON event from stdin into a buffer.
12
+ # - Extract workspace_roots[0]; bail silently when missing.
13
+ # - cd into that workspace; bail silently when it is not a directory
14
+ # or does not contain ./agent-config.
15
+ # - Re-pipe the original JSON into
16
+ # ./agent-config roadmap-progress:hook --platform augment
17
+ # so roadmap_progress_hook.py runs the path filter and decides
18
+ # whether to regenerate the dashboard.
19
+ # - Always exit 0 — PostToolUse hooks must never block.
20
+
21
+ set -u
22
+
23
+ EVENT_DATA="$(cat)"
24
+
25
+ # Extract workspace_roots[0] using whichever JSON tool is available.
26
+ WORKSPACE=""
27
+ if command -v jq >/dev/null 2>&1; then
28
+ WORKSPACE="$(printf '%s' "$EVENT_DATA" \
29
+ | jq -r '.workspace_roots[0] // empty' 2>/dev/null)"
30
+ elif command -v python3 >/dev/null 2>&1; then
31
+ WORKSPACE="$(printf '%s' "$EVENT_DATA" | python3 -c '
32
+ import json, sys
33
+ try:
34
+ data = json.load(sys.stdin)
35
+ except Exception:
36
+ sys.exit(0)
37
+ roots = data.get("workspace_roots") or []
38
+ if roots:
39
+ print(roots[0])
40
+ ' 2>/dev/null)"
41
+ fi
42
+
43
+ if [ -z "$WORKSPACE" ] || [ ! -d "$WORKSPACE" ]; then
44
+ exit 0
45
+ fi
46
+
47
+ cd "$WORKSPACE" 2>/dev/null || exit 0
48
+
49
+ if [ ! -x ./agent-config ]; then
50
+ exit 0
51
+ fi
52
+
53
+ printf '%s' "$EVENT_DATA" \
54
+ | ./agent-config roadmap-progress:hook --platform augment \
55
+ >/dev/null 2>&1 || true
56
+
57
+ exit 0
@@ -464,44 +464,65 @@ def ensure_augment_bridge(project_root: Path, force: bool) -> None:
464
464
  # .augment/settings.json is plugin enablement, not hooks.
465
465
  AUGMENT_USER_DIR = Path.home() / ".augment"
466
466
  AUGMENT_USER_HOOKS_DIR = AUGMENT_USER_DIR / "hooks"
467
- AUGMENT_TRAMPOLINE_NAME = "augment-chat-history.sh"
468
- AUGMENT_HOOK_EVENTS = ("SessionStart", "SessionEnd", "Stop", "PostToolUse")
467
+ AUGMENT_CHAT_HISTORY_TRAMPOLINE = "augment-chat-history.sh"
468
+ AUGMENT_ROADMAP_PROGRESS_TRAMPOLINE = "augment-roadmap-progress.sh"
469
+ # (trampoline name, list of events it should fire on). Each trampoline
470
+ # is a self-contained workspace router; mapping them per-event keeps the
471
+ # wiring explicit and lets a future hook bind to a different surface
472
+ # without touching the chat-history one.
473
+ AUGMENT_HOOK_BINDINGS = (
474
+ (AUGMENT_CHAT_HISTORY_TRAMPOLINE,
475
+ ("SessionStart", "SessionEnd", "Stop", "PostToolUse")),
476
+ (AUGMENT_ROADMAP_PROGRESS_TRAMPOLINE,
477
+ ("PostToolUse",)),
478
+ )
479
+
480
+
481
+ def _deploy_augment_trampoline(package_root: Path, name: str, force: bool) -> Path | None:
482
+ src = package_root / "scripts" / "hooks" / name
483
+ if not src.exists():
484
+ skip(f"augment trampoline missing in package: {src}")
485
+ return None
486
+ AUGMENT_USER_HOOKS_DIR.mkdir(parents=True, exist_ok=True)
487
+ dst = AUGMENT_USER_HOOKS_DIR / name
488
+ src_text = src.read_text(encoding="utf-8")
489
+ if dst.exists() and dst.read_text(encoding="utf-8") == src_text and not force:
490
+ skip(f"~/.augment/hooks/{name} already up to date")
491
+ else:
492
+ dst.write_text(src_text, encoding="utf-8")
493
+ dst.chmod(0o755)
494
+ success(f"~/.augment/hooks/{name} installed")
495
+ return dst
469
496
 
470
497
 
471
498
  def ensure_augment_user_hooks(package_root: Path, force: bool) -> None:
472
- """Deploy the Augment lifecycle-hook trampoline at user scope.
499
+ """Deploy the Augment lifecycle-hook trampolines at user scope.
473
500
 
474
501
  Augment hook scripts must use the .sh extension and be referenced by
475
502
  absolute path; user scope is the only surface that fires for both the
476
503
  CLI and the IDE plugins. This installs once per developer (not per
477
- project) — the trampoline reads workspace_roots from the event payload
478
- and dispatches into whichever project is active at hook-fire time.
504
+ project) — each trampoline reads workspace_roots from the event
505
+ payload and dispatches into whichever project is active at hook-fire
506
+ time.
507
+
508
+ Two trampolines are deployed:
509
+ - augment-chat-history.sh → SessionStart/SessionEnd/Stop/PostToolUse
510
+ - augment-roadmap-progress.sh → PostToolUse (path-filtered to
511
+ agents/roadmaps/ — see scripts/roadmap_progress_hook.py)
479
512
  """
480
- src = package_root / "scripts" / "hooks" / AUGMENT_TRAMPOLINE_NAME
481
- if not src.exists():
482
- skip(f"augment trampoline missing in package: {src}")
483
- return
484
-
485
- AUGMENT_USER_HOOKS_DIR.mkdir(parents=True, exist_ok=True)
486
- dst = AUGMENT_USER_HOOKS_DIR / AUGMENT_TRAMPOLINE_NAME
513
+ per_event: dict[str, list] = {}
514
+ for name, events in AUGMENT_HOOK_BINDINGS:
515
+ dst = _deploy_augment_trampoline(package_root, name, force)
516
+ if dst is None:
517
+ continue
518
+ entry = {"hooks": [{"type": "command", "command": str(dst)}]}
519
+ for event in events:
520
+ per_event.setdefault(event, []).append(entry)
487
521
 
488
- src_text = src.read_text(encoding="utf-8")
489
- if dst.exists() and dst.read_text(encoding="utf-8") == src_text and not force:
490
- skip(f"~/.augment/hooks/{AUGMENT_TRAMPOLINE_NAME} already up to date")
491
- else:
492
- dst.write_text(src_text, encoding="utf-8")
493
- dst.chmod(0o755)
494
- success(f"~/.augment/hooks/{AUGMENT_TRAMPOLINE_NAME} installed")
522
+ if not per_event:
523
+ return
495
524
 
496
- hook_entry = {
497
- "hooks": [
498
- {
499
- "type": "command",
500
- "command": str(dst),
501
- },
502
- ],
503
- }
504
- settings_patch: dict = {"hooks": {event: [hook_entry] for event in AUGMENT_HOOK_EVENTS}}
525
+ settings_patch: dict = {"hooks": per_event}
505
526
  merge_json_file(
506
527
  AUGMENT_USER_DIR / "settings.json",
507
528
  settings_patch,