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/__init__.py +3 -0
- agentdir/__main__.py +6 -0
- agentdir/actors.py +50 -0
- agentdir/artifacts.py +56 -0
- agentdir/audit.py +339 -0
- agentdir/capture.py +187 -0
- agentdir/cli.py +1900 -0
- agentdir/context.py +654 -0
- agentdir/control.py +729 -0
- agentdir/daemon.py +253 -0
- agentdir/doctor.py +115 -0
- agentdir/envelope.py +147 -0
- agentdir/events.py +72 -0
- agentdir/federation.py +530 -0
- agentdir/git.py +45 -0
- agentdir/gitignore.py +106 -0
- agentdir/hooks.py +215 -0
- agentdir/index.py +362 -0
- agentdir/mailbox.py +84 -0
- agentdir/memory.py +1274 -0
- agentdir/query.py +65 -0
- agentdir/redaction.py +42 -0
- agentdir/rendering.py +89 -0
- agentdir/replay.py +31 -0
- agentdir/retention.py +319 -0
- agentdir/review.py +288 -0
- agentdir/secrets.py +158 -0
- agentdir/sessions.py +171 -0
- agentdir/skills.py +685 -0
- agentdir/store.py +270 -0
- agentdir/upgrade.py +252 -0
- agentdir_cli-0.7.5.dist-info/METADATA +393 -0
- agentdir_cli-0.7.5.dist-info/RECORD +37 -0
- agentdir_cli-0.7.5.dist-info/WHEEL +5 -0
- agentdir_cli-0.7.5.dist-info/entry_points.txt +2 -0
- agentdir_cli-0.7.5.dist-info/licenses/LICENSE +21 -0
- agentdir_cli-0.7.5.dist-info/top_level.txt +1 -0
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
|
+
"""
|