agentdir-cli 0.7.5__py3-none-any.whl

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.
agentdir/skills.py ADDED
@@ -0,0 +1,685 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from .git import git_root
8
+ from .store import AgentDirError, init_root, paths_for
9
+
10
+ MANAGED_SKILL_MARKER = "<!-- agentdir-managed-skill -->"
11
+ GENERIC_GUIDANCE_START = "<!-- agentdir-managed-generic:start -->"
12
+ GENERIC_GUIDANCE_END = "<!-- agentdir-managed-generic:end -->"
13
+ INTEGRATION_NAMES = ("generic", "codex", "claude", "copilot", "cursor", "windsurf")
14
+ BROAD_PROJECT_INTEGRATIONS = ("generic", "claude", "copilot", "cursor", "windsurf")
15
+
16
+ CODEX_SKILL = f"""---
17
+ name: agentdir
18
+ description: Automatically record coding-agent sessions with AgentDir when working in a software repository.
19
+ ---
20
+ {MANAGED_SKILL_MARKER}
21
+
22
+ # AgentDir
23
+
24
+ Use AgentDir as the local flight recorder for coding-agent work.
25
+
26
+ The user should not have to run AgentDir commands during normal coding work.
27
+ Treat AgentDir as agent-owned background instrumentation once it is installed.
28
+ Run the commands yourself when they are useful, and only mention AgentDir to the
29
+ user when reporting evidence, blockers, or setup problems.
30
+
31
+ ## Start
32
+
33
+ - At the start of a coding task in a repository, run `agentdir work start "<short task>" --emit-context`.
34
+ - If `.agentdir` is missing and the task is non-trivial, run `agentdir adopt` once for the repository before work begins.
35
+ - Prefer the default project store. It writes to the nearest repo `.agentdir`.
36
+ - Do not record secrets, private keys, raw environment dumps, or credential-bearing command output.
37
+ - If `doctor` reports secret-like persisted bodies, do not print the bodies.
38
+ Use `agentdir secrets scan` for path-only triage and
39
+ `agentdir secrets redact --apply` when cleanup is approved.
40
+ - Use `agentdir status` when you need one health view for the active session,
41
+ evidence, context pack, memory index, doctor result, and registered roots.
42
+ - Use `agentdir evidence --brief` and `agentdir timeline` when you need a
43
+ compact skim of what has been recorded.
44
+
45
+ ## Tool Calls
46
+
47
+ - Run evidence-bearing commands through `agentdir run -- <command>`.
48
+ - Evidence-bearing commands include tests, lint, typecheck, build, release checks, reproduced failures, and diagnostics that support a final claim.
49
+ - Do not wrap routine exploration commands such as `rg`, `sed`, `nl`, `cat`, `ls`, `find`, or quick read-only `git status` checks.
50
+ - Use plain shell commands while reading files, mapping code, or gathering low-level context.
51
+ - `agentdir run` records both the tool call and the tool result, while still streaming command output to the terminal.
52
+ - Do not wrap command chains unless the whole chain is evidence worth preserving.
53
+ - If a command must not be wrapped, emit the evidence afterward with `agentdir emit`.
54
+
55
+ ## During Work
56
+
57
+ - `agentdir work start "<task>" --emit-context` is the normal entry point. Use
58
+ lower-level context commands only when you need finer control.
59
+ - Use `agentdir context build "<task>" --emit` when the retrieved context should become an auditable context pack.
60
+ - Use `agentdir context consume --pack <pack-id> --source <source-id> --purpose plan|tool|answer|handoff` when you rely on retrieved context.
61
+ - Use `agentdir context cite --pack <pack-id>` or `agentdir audit context --pack <pack-id>` when reporting source lineage.
62
+ - Use `agentdir memory search "<task, error, or subsystem>"` and `agentdir memory explain "<same query>"` when you need to inspect retrieval.
63
+ - Use `agentdir roots suggest` and `agentdir roots doctor` to inspect available
64
+ cross-repo memory without mutating registrations.
65
+ - Use `agentdir roots register <root-or-repo>` only when cross-repo memory has
66
+ been explicitly requested or is clearly part of the task.
67
+ - Prefer root groups for repeated cross-repo work, then use
68
+ `agentdir memory search --group <name> "<query>"` or
69
+ `agentdir work start "<task>" --group <name> --emit-context`.
70
+ - Emit important plans, blockers, diffs, review decisions, and final handoffs as immutable events.
71
+ - Use `agentdir memory daemon status` to inspect warm indexing when repeated
72
+ large-store or cross-repo work depends on fresh memory.
73
+ - Use `agentdir memory embeddings configure fastembed` or
74
+ `agentdir memory backend configure sqlite-vec` only when optional semantic
75
+ extras are intentionally installed for this environment.
76
+ - Use `agentdir index rebuild` if query, replay, memory, summarize, or evidence output looks stale.
77
+ - Use `agentdir evidence --brief` before claiming tests or checks passed.
78
+
79
+ ## Finish
80
+
81
+ - Before the final response, run `agentdir work finish --json` when practical.
82
+ It emits a final report, checks evidence and context lineage, runs doctor, and
83
+ ends the active session.
84
+ - Use `agentdir report final --format json` to preview the same agent handoff
85
+ without ending the session.
86
+ - Read the `agent_handoff` object before making final verification claims.
87
+ - Use `agentdir evidence --brief` when the final response claims tests, builds,
88
+ hooks, or release checks passed.
89
+ - Use lower-level `agentdir summarize`, `agentdir evidence`, `agentdir doctor`,
90
+ and `agentdir session end` only when the workbench command is not appropriate.
91
+ """
92
+
93
+ GENERIC_GUIDANCE_BLOCK = f"""{GENERIC_GUIDANCE_START}
94
+ # AgentDir
95
+
96
+ Use AgentDir as the local flight recorder for coding-agent work in this repository.
97
+
98
+ The engineer should not have to run AgentDir commands during normal coding work.
99
+ The agent owns the background recording flow:
100
+
101
+ - Start non-trivial coding work with `agentdir work start "<short task>" --emit-context`.
102
+ - If `.agentdir` is missing, run `agentdir adopt` once before work begins.
103
+ - Run evidence-bearing commands through `agentdir run -- <command>`.
104
+ - Evidence-bearing commands include tests, lint, typecheck, build, release checks,
105
+ reproduced failures, and diagnostics that support final claims.
106
+ - Do not wrap routine exploration commands such as `rg`, `sed`, `nl`, `cat`, `ls`,
107
+ `find`, or quick read-only `git status` checks.
108
+ - Use `agentdir status` for a single view of session, evidence, memory, context,
109
+ registered roots, and doctor health.
110
+ - Use `agentdir evidence --brief` and `agentdir timeline` to skim recorded work.
111
+ - Use `agentdir audit session` and `agentdir audit claims --text <path|->` before
112
+ final claims when evidence support matters.
113
+ - Before the final response, run `agentdir work finish --json` when practical.
114
+ Use `agentdir report final --format json` to preview the same agent handoff
115
+ without ending the session.
116
+ - Read the `agent_handoff` object before making final verification claims.
117
+ - Do not record secrets, private keys, raw environment dumps, or credential-bearing
118
+ command output.
119
+ {GENERIC_GUIDANCE_END}
120
+ """
121
+
122
+
123
+ @dataclass(frozen=True)
124
+ class InstalledSkill:
125
+ path: Path
126
+ target: str
127
+ updated: bool = False
128
+ backup_path: Path | None = None
129
+
130
+
131
+ def install_codex_skill(
132
+ root: str | Path,
133
+ *,
134
+ target: str = "user",
135
+ force: bool = False,
136
+ cwd: str | Path | None = None,
137
+ ) -> InstalledSkill:
138
+ destination = codex_skill_path(root, target=target, cwd=cwd)
139
+ updated = False
140
+ backup_path: Path | None = None
141
+ if destination.exists() and not force:
142
+ existing = destination.read_text(encoding="utf-8", errors="ignore")
143
+ if existing != CODEX_SKILL:
144
+ if not is_agentdir_managed_skill(existing):
145
+ raise AgentDirError(f"Refusing to overwrite existing Codex skill: {destination}")
146
+ backup_path = destination.with_suffix(destination.suffix + ".bak")
147
+ backup_path.write_text(existing, encoding="utf-8")
148
+ updated = True
149
+ else:
150
+ return InstalledSkill(path=destination, target=target)
151
+ elif destination.exists() and force:
152
+ existing = destination.read_text(encoding="utf-8", errors="ignore")
153
+ updated = existing != CODEX_SKILL
154
+ destination.parent.mkdir(parents=True, exist_ok=True)
155
+ destination.write_text(CODEX_SKILL, encoding="utf-8")
156
+ return InstalledSkill(path=destination, target=target, updated=updated, backup_path=backup_path)
157
+
158
+
159
+ def install_generic_guidance(
160
+ root: str | Path,
161
+ *,
162
+ target: str = "store",
163
+ force: bool = False,
164
+ cwd: str | Path | None = None,
165
+ ) -> InstalledSkill:
166
+ destination = generic_guidance_path(root, target=target, cwd=cwd)
167
+ updated = False
168
+ backup_path: Path | None = None
169
+ if target == "project":
170
+ existing = destination.read_text(encoding="utf-8") if destination.exists() else ""
171
+ updated_text = merge_generic_guidance(existing)
172
+ updated = existing != updated_text
173
+ if destination.exists() and updated and force:
174
+ backup_path = destination.with_suffix(destination.suffix + ".bak")
175
+ backup_path.write_text(existing, encoding="utf-8")
176
+ destination.parent.mkdir(parents=True, exist_ok=True)
177
+ destination.write_text(updated_text, encoding="utf-8")
178
+ return InstalledSkill(path=destination, target=target, updated=updated, backup_path=backup_path)
179
+
180
+ if target == "store":
181
+ existing = destination.read_text(encoding="utf-8", errors="ignore") if destination.exists() else ""
182
+ if destination.exists() and existing != GENERIC_GUIDANCE_BLOCK:
183
+ if not force and not is_agentdir_managed_generic(existing):
184
+ raise AgentDirError(f"Refusing to overwrite existing generic guidance: {destination}")
185
+ updated = True
186
+ if force:
187
+ backup_path = destination.with_suffix(destination.suffix + ".bak")
188
+ backup_path.write_text(existing, encoding="utf-8")
189
+ destination.parent.mkdir(parents=True, exist_ok=True)
190
+ destination.write_text(GENERIC_GUIDANCE_BLOCK, encoding="utf-8")
191
+ return InstalledSkill(path=destination, target=target, updated=updated, backup_path=backup_path)
192
+
193
+ raise AgentDirError("Unknown generic guidance target; expected project or store")
194
+
195
+
196
+ def is_agentdir_managed_skill(text: str) -> bool:
197
+ if MANAGED_SKILL_MARKER in text:
198
+ return True
199
+ return "name: agentdir" in text and "# AgentDir" in text
200
+
201
+
202
+ def is_agentdir_managed_generic(text: str) -> bool:
203
+ return GENERIC_GUIDANCE_START in text and GENERIC_GUIDANCE_END in text
204
+
205
+
206
+ def merge_generic_guidance(existing: str) -> str:
207
+ return merge_managed_block(existing, GENERIC_GUIDANCE_BLOCK, GENERIC_GUIDANCE_START, GENERIC_GUIDANCE_END)
208
+
209
+
210
+ def codex_skill_path(root: str | Path, *, target: str, cwd: str | Path | None = None) -> Path:
211
+ if target == "user":
212
+ return Path.home().expanduser() / ".codex" / "skills" / "agentdir" / "SKILL.md"
213
+ if target == "store":
214
+ init_root(root)
215
+ return paths_for(root).integrations / "codex" / "skills" / "agentdir" / "SKILL.md"
216
+ if target == "project":
217
+ project = git_root(cwd)
218
+ if project is None:
219
+ raise AgentDirError("Project skill target requires a git repository")
220
+ return project / ".agents" / "skills" / "agentdir" / "SKILL.md"
221
+ raise AgentDirError("Unknown Codex skill target; expected user, project, or store")
222
+
223
+
224
+ def generic_guidance_path(root: str | Path, *, target: str, cwd: str | Path | None = None) -> Path:
225
+ if target == "store":
226
+ init_root(root)
227
+ return paths_for(root).integrations / "generic" / "AGENTS.md"
228
+ if target == "project":
229
+ project = git_root(cwd)
230
+ if project is None:
231
+ raise AgentDirError("Project generic guidance target requires a git repository")
232
+ return project / "AGENTS.md"
233
+ raise AgentDirError("Unknown generic guidance target; expected project or store")
234
+
235
+
236
+ def install_integrations(
237
+ root: str | Path,
238
+ names: list[str],
239
+ *,
240
+ target: str,
241
+ force: bool = False,
242
+ cwd: str | Path | None = None,
243
+ ) -> list[dict[str, Any]]:
244
+ installed: list[dict[str, Any]] = []
245
+ for name in expand_integration_names(names):
246
+ if name == "generic":
247
+ generic = install_generic_guidance(root, target=target, force=force, cwd=cwd)
248
+ installed.append(_skill_result(name, generic))
249
+ continue
250
+ if name == "codex":
251
+ codex_target = "store" if target == "store" else "project"
252
+ skill = install_codex_skill(root, target=codex_target, force=force, cwd=cwd)
253
+ installed.append(_skill_result(name, skill))
254
+ continue
255
+ installed.append(_install_guidance_integration(root, name, target=target, force=force, cwd=cwd))
256
+ return installed
257
+
258
+
259
+ def integration_plan(
260
+ root: str | Path,
261
+ names: list[str],
262
+ *,
263
+ target: str,
264
+ force: bool = False,
265
+ cwd: str | Path | None = None,
266
+ ) -> list[dict[str, Any]]:
267
+ plan: list[dict[str, Any]] = []
268
+ for name in expand_integration_names(names):
269
+ destination = integration_path(root, name, target=target, cwd=cwd, create=False)
270
+ expected = integration_expected_text(name)
271
+ exists = destination.exists()
272
+ existing = destination.read_text(encoding="utf-8", errors="ignore") if exists else ""
273
+ state = integration_state(root, name, target=target, cwd=cwd)
274
+ action = "none"
275
+ if not exists:
276
+ action = "create"
277
+ elif state["state"] == "installed":
278
+ action = "none"
279
+ elif state["state"] == "stale":
280
+ action = "update"
281
+ elif state["state"] == "missing" and _can_merge_guidance(name, target):
282
+ action = "update"
283
+ elif state["state"] == "conflict":
284
+ action = "overwrite" if force else "refuse"
285
+ backup = None
286
+ if exists and action in {"update", "overwrite"} and (force or not _can_merge_guidance(name, target)):
287
+ backup = str(destination.with_suffix(destination.suffix + ".bak"))
288
+ plan.append(
289
+ {
290
+ "name": name,
291
+ "target": target,
292
+ "path": str(destination),
293
+ "action": action,
294
+ "state": state["state"],
295
+ "exists": exists,
296
+ "managed": state.get("managed", False),
297
+ "backup_path": backup,
298
+ "would_write": action in {"create", "update", "overwrite"},
299
+ "would_refuse": action == "refuse",
300
+ "expected_bytes": len(expected.encode("utf-8")),
301
+ "existing_bytes": len(existing.encode("utf-8")) if exists else 0,
302
+ }
303
+ )
304
+ return plan
305
+
306
+
307
+ def integration_doctor(
308
+ root: str | Path,
309
+ names: list[str] | None = None,
310
+ *,
311
+ target: str = "project",
312
+ cwd: str | Path | None = None,
313
+ ) -> dict[str, Any]:
314
+ selected = expand_integration_names(names or ["all"])
315
+ checks = [integration_state(root, name, target=target, cwd=cwd) for name in selected]
316
+ return {
317
+ "target": target,
318
+ "ok": all(item["state"] in {"installed", "missing"} for item in checks),
319
+ "checks": checks,
320
+ }
321
+
322
+
323
+ def uninstall_integrations(
324
+ root: str | Path,
325
+ names: list[str] | None = None,
326
+ *,
327
+ target: str = "project",
328
+ apply: bool = False,
329
+ cwd: str | Path | None = None,
330
+ ) -> list[dict[str, Any]]:
331
+ results: list[dict[str, Any]] = []
332
+ for name in expand_integration_names(names or ["all"]):
333
+ destination = integration_path(root, name, target=target, cwd=cwd, create=False)
334
+ exists = destination.exists()
335
+ existing = destination.read_text(encoding="utf-8", errors="ignore") if exists else ""
336
+ managed = _integration_managed(name, existing)
337
+ removed = False
338
+ action = "none"
339
+ if exists and managed:
340
+ action = "remove"
341
+ if apply:
342
+ if _can_merge_guidance(name, target):
343
+ updated = remove_managed_block(existing, *_integration_markers(name)).strip()
344
+ if updated:
345
+ destination.write_text(updated + "\n", encoding="utf-8")
346
+ else:
347
+ destination.unlink()
348
+ removed = True
349
+ else:
350
+ destination.unlink()
351
+ removed = True
352
+ elif exists:
353
+ action = "preserve-unmanaged"
354
+ results.append(
355
+ {
356
+ "name": name,
357
+ "target": target,
358
+ "path": str(destination),
359
+ "action": action,
360
+ "managed": managed,
361
+ "removed": removed,
362
+ "applied": apply,
363
+ }
364
+ )
365
+ return results
366
+
367
+
368
+ def integration_state(
369
+ root: str | Path,
370
+ name: str,
371
+ *,
372
+ target: str = "project",
373
+ cwd: str | Path | None = None,
374
+ ) -> dict[str, Any]:
375
+ destination = integration_path(root, name, target=target, cwd=cwd, create=False)
376
+ expected = integration_expected_text(name)
377
+ exists = destination.exists()
378
+ existing = destination.read_text(encoding="utf-8", errors="ignore") if exists else ""
379
+ managed = _integration_managed(name, existing)
380
+ if not exists:
381
+ if name == "codex" and target == "project":
382
+ effective = _effective_codex_install(root, cwd=cwd)
383
+ if effective:
384
+ return {
385
+ "name": name,
386
+ "target": target,
387
+ "path": str(destination),
388
+ "state": effective["state"],
389
+ "installed": effective["state"] == "installed",
390
+ "managed": effective["managed"],
391
+ "effective_target": effective["target"],
392
+ "effective_path": str(effective["path"]),
393
+ "message": f"Codex skill is installed at {effective['target']} target",
394
+ }
395
+ state = "missing"
396
+ elif name == "codex":
397
+ state = "installed" if existing == CODEX_SKILL else "stale" if managed else "conflict"
398
+ elif _can_merge_guidance(name, target):
399
+ current = extract_managed_block(existing, *_integration_markers(name))
400
+ state = "installed" if current == expected else "stale" if managed else "missing"
401
+ else:
402
+ state = "installed" if existing == expected else "stale" if managed else "conflict"
403
+ return {
404
+ "name": name,
405
+ "target": target,
406
+ "path": str(destination),
407
+ "state": state,
408
+ "installed": state == "installed",
409
+ "managed": managed,
410
+ }
411
+
412
+
413
+ def _effective_codex_install(
414
+ root: str | Path,
415
+ *,
416
+ cwd: str | Path | None = None,
417
+ ) -> dict[str, Any] | None:
418
+ for effective_target in ("user", "store"):
419
+ path = codex_skill_path_no_create(root, target=effective_target, cwd=cwd, create=False)
420
+ if not path.exists():
421
+ continue
422
+ text = path.read_text(encoding="utf-8", errors="ignore")
423
+ if text == CODEX_SKILL:
424
+ state = "installed"
425
+ elif is_agentdir_managed_skill(text):
426
+ state = "stale"
427
+ else:
428
+ state = "conflict"
429
+ return {
430
+ "target": effective_target,
431
+ "path": path,
432
+ "state": state,
433
+ "managed": is_agentdir_managed_skill(text),
434
+ }
435
+ return None
436
+
437
+
438
+ def integration_path(
439
+ root: str | Path,
440
+ name: str,
441
+ *,
442
+ target: str,
443
+ cwd: str | Path | None = None,
444
+ create: bool = True,
445
+ ) -> Path:
446
+ name = normalize_integration_name(name)
447
+ if target not in {"project", "store"}:
448
+ raise AgentDirError("Unknown integration target; expected project or store")
449
+ if name == "generic":
450
+ return generic_guidance_path_no_create(root, target=target, cwd=cwd, create=create)
451
+ if name == "codex":
452
+ return codex_skill_path_no_create(root, target="store" if target == "store" else "project", cwd=cwd, create=create)
453
+ if target == "store":
454
+ if create:
455
+ init_root(root)
456
+ filename = _project_integration_relative_path(name).name
457
+ return paths_for(root).integrations / name / filename
458
+ project = git_root(cwd)
459
+ if project is None:
460
+ raise AgentDirError("Project integration target requires a git repository")
461
+ return project / _project_integration_relative_path(name)
462
+
463
+
464
+ def integration_expected_text(name: str) -> str:
465
+ name = normalize_integration_name(name)
466
+ if name == "generic":
467
+ return GENERIC_GUIDANCE_BLOCK.rstrip() + "\n"
468
+ if name == "codex":
469
+ return CODEX_SKILL
470
+ return _guidance_block(name)
471
+
472
+
473
+ def expand_integration_names(names: list[str]) -> list[str]:
474
+ expanded: list[str] = []
475
+ for name in names:
476
+ if name == "all":
477
+ expanded.extend(INTEGRATION_NAMES)
478
+ else:
479
+ expanded.append(normalize_integration_name(name))
480
+ deduped: list[str] = []
481
+ for name in expanded:
482
+ if name not in deduped:
483
+ deduped.append(name)
484
+ return deduped
485
+
486
+
487
+ def normalize_integration_name(name: str) -> str:
488
+ if name not in (*INTEGRATION_NAMES, "all"):
489
+ raise AgentDirError(f"Unknown integration {name!r}; expected one of {', '.join(INTEGRATION_NAMES)} or all")
490
+ return name
491
+
492
+
493
+ def merge_managed_block(existing: str, block: str, start: str, end: str) -> str:
494
+ block = block.rstrip() + "\n"
495
+ if not existing.strip():
496
+ return block
497
+ start_index = existing.find(start)
498
+ end_index = existing.find(end)
499
+ if start_index != -1 and end_index != -1 and end_index >= start_index:
500
+ end_index += len(end)
501
+ return _join_guidance_sections(existing[:start_index], block, existing[end_index:])
502
+ return _join_guidance_sections(existing, block, "")
503
+
504
+
505
+ def _join_guidance_sections(*sections: str) -> str:
506
+ normalized = [section.strip() for section in sections if section.strip()]
507
+ return "\n\n".join(normalized) + "\n"
508
+
509
+
510
+ def remove_managed_block(existing: str, start: str, end: str) -> str:
511
+ start_index = existing.find(start)
512
+ end_index = existing.find(end)
513
+ if start_index == -1 or end_index == -1 or end_index < start_index:
514
+ return existing
515
+ end_index += len(end)
516
+ return existing[:start_index].rstrip() + "\n\n" + existing[end_index:].lstrip()
517
+
518
+
519
+ def extract_managed_block(existing: str, start: str, end: str) -> str | None:
520
+ start_index = existing.find(start)
521
+ end_index = existing.find(end)
522
+ if start_index == -1 or end_index == -1 or end_index < start_index:
523
+ return None
524
+ end_index += len(end)
525
+ return existing[start_index:end_index].rstrip() + "\n"
526
+
527
+
528
+ def codex_skill_path_no_create(
529
+ root: str | Path,
530
+ *,
531
+ target: str,
532
+ cwd: str | Path | None = None,
533
+ create: bool = True,
534
+ ) -> Path:
535
+ if create:
536
+ return codex_skill_path(root, target=target, cwd=cwd)
537
+ if target == "user":
538
+ return Path.home().expanduser() / ".codex" / "skills" / "agentdir" / "SKILL.md"
539
+ if target == "store":
540
+ return paths_for(root).integrations / "codex" / "skills" / "agentdir" / "SKILL.md"
541
+ if target == "project":
542
+ project = git_root(cwd)
543
+ if project is None:
544
+ raise AgentDirError("Project skill target requires a git repository")
545
+ return project / ".agents" / "skills" / "agentdir" / "SKILL.md"
546
+ raise AgentDirError("Unknown Codex skill target; expected user, project, or store")
547
+
548
+
549
+ def generic_guidance_path_no_create(
550
+ root: str | Path,
551
+ *,
552
+ target: str,
553
+ cwd: str | Path | None = None,
554
+ create: bool = True,
555
+ ) -> Path:
556
+ if create:
557
+ return generic_guidance_path(root, target=target, cwd=cwd)
558
+ if target == "store":
559
+ return paths_for(root).integrations / "generic" / "AGENTS.md"
560
+ if target == "project":
561
+ project = git_root(cwd)
562
+ if project is None:
563
+ raise AgentDirError("Project generic guidance target requires a git repository")
564
+ return project / "AGENTS.md"
565
+ raise AgentDirError("Unknown generic guidance target; expected project or store")
566
+
567
+
568
+ def _install_guidance_integration(
569
+ root: str | Path,
570
+ name: str,
571
+ *,
572
+ target: str,
573
+ force: bool,
574
+ cwd: str | Path | None,
575
+ ) -> dict[str, Any]:
576
+ destination = integration_path(root, name, target=target, cwd=cwd)
577
+ expected = integration_expected_text(name)
578
+ existing = destination.read_text(encoding="utf-8", errors="ignore") if destination.exists() else ""
579
+ updated = existing != expected
580
+ backup_path: Path | None = None
581
+ if _can_merge_guidance(name, target):
582
+ merged = merge_managed_block(existing, expected, *_integration_markers(name))
583
+ updated = existing != merged
584
+ if destination.exists() and force and updated:
585
+ backup_path = destination.with_suffix(destination.suffix + ".bak")
586
+ backup_path.write_text(existing, encoding="utf-8")
587
+ destination.parent.mkdir(parents=True, exist_ok=True)
588
+ destination.write_text(merged, encoding="utf-8")
589
+ return {
590
+ "name": name,
591
+ "target": target,
592
+ "path": str(destination),
593
+ "updated": updated,
594
+ "backup_path": str(backup_path) if backup_path else None,
595
+ }
596
+ if destination.exists() and existing != expected:
597
+ if not _integration_managed(name, existing) and not force:
598
+ raise AgentDirError(f"Refusing to overwrite existing {name} integration: {destination}")
599
+ backup_path = destination.with_suffix(destination.suffix + ".bak")
600
+ backup_path.write_text(existing, encoding="utf-8")
601
+ destination.parent.mkdir(parents=True, exist_ok=True)
602
+ destination.write_text(expected, encoding="utf-8")
603
+ return {
604
+ "name": name,
605
+ "target": target,
606
+ "path": str(destination),
607
+ "updated": updated,
608
+ "backup_path": str(backup_path) if backup_path else None,
609
+ }
610
+
611
+
612
+ def _skill_result(name: str, installed: InstalledSkill) -> dict[str, Any]:
613
+ return {
614
+ "name": name,
615
+ "target": installed.target,
616
+ "path": str(installed.path),
617
+ "updated": installed.updated,
618
+ "backup_path": str(installed.backup_path) if installed.backup_path else None,
619
+ }
620
+
621
+
622
+ def _project_integration_relative_path(name: str) -> Path:
623
+ if name == "claude":
624
+ return Path("CLAUDE.md")
625
+ if name == "copilot":
626
+ return Path(".github") / "copilot-instructions.md"
627
+ if name == "cursor":
628
+ return Path(".cursor") / "rules" / "agentdir.mdc"
629
+ if name == "windsurf":
630
+ return Path(".windsurf") / "rules" / "agentdir.md"
631
+ raise AgentDirError(f"Integration {name!r} does not have a project guidance path")
632
+
633
+
634
+ def _can_merge_guidance(name: str, target: str) -> bool:
635
+ if target == "store":
636
+ return False
637
+ return name in {"generic", "claude", "copilot"}
638
+
639
+
640
+ def _integration_managed(name: str, text: str) -> bool:
641
+ if not text:
642
+ return False
643
+ if name == "generic":
644
+ return is_agentdir_managed_generic(text)
645
+ if name == "codex":
646
+ return is_agentdir_managed_skill(text)
647
+ start, end = _integration_markers(name)
648
+ return start in text and end in text
649
+
650
+
651
+ def _integration_markers(name: str) -> tuple[str, str]:
652
+ if name == "generic":
653
+ return GENERIC_GUIDANCE_START, GENERIC_GUIDANCE_END
654
+ return f"<!-- agentdir-managed-{name}:start -->", f"<!-- agentdir-managed-{name}:end -->"
655
+
656
+
657
+ def _guidance_block(name: str) -> str:
658
+ start, end = _integration_markers(name)
659
+ title = {
660
+ "claude": "Claude Code",
661
+ "copilot": "GitHub Copilot",
662
+ "cursor": "Cursor",
663
+ "windsurf": "Windsurf",
664
+ }[name]
665
+ frontmatter = ""
666
+ if name == "cursor":
667
+ frontmatter = "---\ndescription: Use AgentDir as invisible evidence capture for agentic engineering.\nalwaysApply: true\n---\n\n"
668
+ elif name == "windsurf":
669
+ frontmatter = "---\ntrigger: always_on\n---\n\n"
670
+ return f"""{frontmatter}{start}
671
+ # AgentDir for {title}
672
+
673
+ AgentDir is agent-owned background instrumentation for this repository.
674
+ The engineer installs or adopts it once; agents operate it during normal work.
675
+
676
+ - Start non-trivial coding work with `agentdir work start "<short task>" --emit-context`.
677
+ - If `.agentdir` is missing, run `agentdir adopt` once before starting.
678
+ - Wrap evidence-bearing commands with `agentdir run -- <command>`.
679
+ - Evidence includes tests, lint, typecheck, builds, doctor checks, release checks, reproduced failures, and diagnostics used in final claims.
680
+ - Do not wrap routine exploration such as `rg`, `sed`, `nl`, `cat`, `ls`, `find`, or quick read-only `git status`.
681
+ - Use `agentdir evidence --brief` and `agentdir timeline` to skim what happened.
682
+ - Use `agentdir report final --format json` or `agentdir work finish --json` for the agent handoff object before final claims when practical.
683
+ - Do not record secrets, private keys, raw environment dumps, or credential-bearing command output.
684
+ {end}
685
+ """