codedebrief 0.11.0__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.
- codedebrief/__init__.py +12 -0
- codedebrief/analysis/__init__.py +16 -0
- codedebrief/analysis/common.py +527 -0
- codedebrief/analysis/discovery.py +100 -0
- codedebrief/analysis/languages/__init__.py +6 -0
- codedebrief/analysis/languages/_common.py +68 -0
- codedebrief/analysis/languages/c.py +96 -0
- codedebrief/analysis/languages/cpp.py +146 -0
- codedebrief/analysis/languages/csharp.py +137 -0
- codedebrief/analysis/languages/go.py +157 -0
- codedebrief/analysis/languages/java.py +158 -0
- codedebrief/analysis/languages/php.py +83 -0
- codedebrief/analysis/languages/ruby.py +75 -0
- codedebrief/analysis/languages/rust.py +96 -0
- codedebrief/analysis/project.py +373 -0
- codedebrief/analysis/python.py +939 -0
- codedebrief/analysis/registry.py +320 -0
- codedebrief/analysis/treesitter.py +884 -0
- codedebrief/analysis/typescript.py +1019 -0
- codedebrief/artifacts.py +49 -0
- codedebrief/cli.py +585 -0
- codedebrief/config.py +226 -0
- codedebrief/doctor.py +175 -0
- codedebrief/install.py +441 -0
- codedebrief/mcp_server.py +2720 -0
- codedebrief/model.py +189 -0
- codedebrief/py.typed +1 -0
- codedebrief/quality.py +392 -0
- codedebrief/query.py +641 -0
- codedebrief/render/__init__.py +6 -0
- codedebrief/render/assets/generated/codedebrief-viewer-runtime.iife.js +10 -0
- codedebrief/render/assets/panels.js +462 -0
- codedebrief/render/assets/shell.js +1649 -0
- codedebrief/render/assets/styles.css +1715 -0
- codedebrief/render/assets/tree.js +616 -0
- codedebrief/render/html.py +191 -0
- codedebrief/render/markdown.py +153 -0
- codedebrief/render/payload.py +326 -0
- codedebrief/render/snapshot.py +769 -0
- codedebrief/schema/codedebrief.schema.json +449 -0
- codedebrief/util.py +65 -0
- codedebrief/validation.py +214 -0
- codedebrief-0.11.0.dist-info/METADATA +426 -0
- codedebrief-0.11.0.dist-info/RECORD +48 -0
- codedebrief-0.11.0.dist-info/WHEEL +4 -0
- codedebrief-0.11.0.dist-info/entry_points.txt +2 -0
- codedebrief-0.11.0.dist-info/licenses/LICENSE +176 -0
- codedebrief-0.11.0.dist-info/licenses/NOTICE +9 -0
codedebrief/install.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
START = "<!-- codedebrief:instructions:start -->"
|
|
8
|
+
END = "<!-- codedebrief:instructions:end -->"
|
|
9
|
+
LOCAL_NOTES_START = "<!-- codedebrief:local-notes:start -->"
|
|
10
|
+
LOCAL_NOTES_END = "<!-- codedebrief:local-notes:end -->"
|
|
11
|
+
LOCAL_NOTES_HINT = (
|
|
12
|
+
"<!-- Add project-specific local notes here. This section is preserved by "
|
|
13
|
+
"`codedebrief setup-agent`. -->"
|
|
14
|
+
)
|
|
15
|
+
AGENT_INSTRUCTION_TARGETS = {
|
|
16
|
+
"codex": Path("AGENTS.md"),
|
|
17
|
+
"claude": Path("CLAUDE.md"),
|
|
18
|
+
"gemini": Path("GEMINI.md"),
|
|
19
|
+
"cursor": Path(".cursor/rules/codedebrief.mdc"),
|
|
20
|
+
}
|
|
21
|
+
AGENT_SKILL_TARGETS = {
|
|
22
|
+
"codex": Path(".agents/skills/codedebrief/SKILL.md"),
|
|
23
|
+
"claude": Path(".claude/skills/codedebrief/SKILL.md"),
|
|
24
|
+
"gemini": Path(".gemini/skills/codedebrief/SKILL.md"),
|
|
25
|
+
}
|
|
26
|
+
MCP_CONFIG_TARGETS = ("codex", "claude", "gemini", "cursor")
|
|
27
|
+
CODEX_MCP_START = "# codedebrief:mcp-config:start"
|
|
28
|
+
CODEX_MCP_END = "# codedebrief:mcp-config:end"
|
|
29
|
+
|
|
30
|
+
SKILL_DESCRIPTION = (
|
|
31
|
+
"Use when answering codebase logic, behavior, workflow/flusso, decision, "
|
|
32
|
+
"state/status, changed-code context, testing, or visual workflow/canvas questions in a "
|
|
33
|
+
"project that uses CodeDebrief. Prefer the CodeDebrief MCP agent_context tool before "
|
|
34
|
+
"broad searches, and use snapshot_slice, the canonical workflow_slice Mermaid "
|
|
35
|
+
"visual, or viewer_targets when the user asks to show, visualize, render, diagram, "
|
|
36
|
+
"canvas, workflow, flusso, or workflow_slice."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
SKILL_TEMPLATE = f"""---
|
|
40
|
+
name: codedebrief
|
|
41
|
+
description: {SKILL_DESCRIPTION}
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
# CodeDebrief
|
|
45
|
+
|
|
46
|
+
Use CodeDebrief as the first path for code-logic questions in projects with CodeDebrief
|
|
47
|
+
configured.
|
|
48
|
+
|
|
49
|
+
## Default Workflow
|
|
50
|
+
|
|
51
|
+
1. Call MCP `agent_context` before broad file-by-file search. Pass the user question plus
|
|
52
|
+
changed files, current file, selected code, flow id, symbol, or dependency path when
|
|
53
|
+
available.
|
|
54
|
+
2. Inspect `workflow_slice`. Answer from deterministic fields: presentation,
|
|
55
|
+
primary/supporting flows, ordered steps, decisions, source ranges, calls, and visuals.
|
|
56
|
+
3. Use `expand_slice`, `workflow_path`, `explain_flow`, `explain_node`, or `explain_edge`
|
|
57
|
+
only when the first slice is too narrow.
|
|
58
|
+
4. Treat CodeDebrief artifacts as part of done for workflow-relevant changes. After each
|
|
59
|
+
meaningful source, route, config, or agent-instruction change, refresh the graph before
|
|
60
|
+
finalizing: run MCP `update_codedebrief` when available, otherwise run
|
|
61
|
+
`codedebrief update`, then run `validate_artifacts` or
|
|
62
|
+
`codedebrief validate --check-sync`. Keep `codedebrief-out/codedebrief.json` and
|
|
63
|
+
`codedebrief-out/codedebrief.md` synchronized when they change.
|
|
64
|
+
|
|
65
|
+
## Visual Workflow Requests
|
|
66
|
+
|
|
67
|
+
When the user asks to show a workflow, workflow_slice, diagram, visual flow, canvas,
|
|
68
|
+
flusso, or similar code path:
|
|
69
|
+
|
|
70
|
+
1. Call `agent_context` with `include_visual=true` when available. Use a stable token
|
|
71
|
+
budget for similar requests, but inspect the full returned `workflow_slice` before
|
|
72
|
+
deciding what to show.
|
|
73
|
+
2. If the tool result is too large, saved externally, truncated, or missing the exact
|
|
74
|
+
`workflow_slice.presentation.canonical_visual.diagram`, retry with a smaller
|
|
75
|
+
`token_budget` and a narrower `flow_id`, `symbol`, `current_file`, or `scope`. Do not
|
|
76
|
+
recover by listing flows and hand-building a diagram.
|
|
77
|
+
3. If the first slice omits relevant callers, callees, branches, adjacent flows, or paths,
|
|
78
|
+
use the returned slice handles with `expand_slice` or `workflow_path` before answering.
|
|
79
|
+
4. Choose the first visible depth yourself: show the clearest useful subset of the
|
|
80
|
+
selected workflow, not every low-signal implementation node, but do not remove facts
|
|
81
|
+
needed to understand the logical path.
|
|
82
|
+
5. Render `workflow_slice.presentation.canonical_visual.diagram` exactly as the default
|
|
83
|
+
chat visual only when the client renders Mermaid blocks inline. It is the canonical
|
|
84
|
+
top-to-bottom Mermaid graph and should be preferred over SVG snapshots for repeated
|
|
85
|
+
chat answers.
|
|
86
|
+
6. Call `snapshot_slice` using `workflow_slice.id`, `workflow_slice.handle.flow_ids`, and
|
|
87
|
+
any `workflow_slice` handles returned by CodeDebrief to persist local artifacts. In
|
|
88
|
+
clients that cannot render Mermaid inline, or when Mermaid would appear as a raw code
|
|
89
|
+
block, call `snapshot_slice` with `include_svg=false` and provide
|
|
90
|
+
`artifact.mermaid_path`, `artifact.mermaid_markdown_path`, or
|
|
91
|
+
`artifact.mermaid_open_command` as the visual result before prose. Do not paste a long
|
|
92
|
+
Mermaid code block as the primary visual unless the user explicitly asks for raw or
|
|
93
|
+
copyable Mermaid.
|
|
94
|
+
7. Do not render `snapshot.svg` inline by default. SVG/HTML snapshot artifacts are for
|
|
95
|
+
explicit SVG requests or local inspection only, because their layout may differ from
|
|
96
|
+
Mermaid and can overlap text in some clients. Keep the returned `diagram_hash` visible
|
|
97
|
+
when useful. Do not synthesize a new Mermaid diagram and do not add limits, error
|
|
98
|
+
codes, branches, or service steps that are absent from the `workflow_slice` payload.
|
|
99
|
+
CodeDebrief Mermaid visuals are vertical/top-to-bottom by default. Use a horizontal
|
|
100
|
+
layout only when the user explicitly asks for a compact horizontal overview.
|
|
101
|
+
8. Do not read source files to rebuild, relabel, or extend the diagram. Source reads are
|
|
102
|
+
allowed only as follow-up explanation after the deterministic CodeDebrief visual is
|
|
103
|
+
shown, and they must not change the displayed nodes, edges, labels, or branches.
|
|
104
|
+
9. If neither exact canonical Mermaid nor a returned Mermaid artifact can be used, say
|
|
105
|
+
that the deterministic visual cannot be shown in this client and provide `viewer_targets`;
|
|
106
|
+
never create a replacement Mermaid diagram from prose or source reads.
|
|
107
|
+
10. Say that the displayed diagram is a bounded summary of the selected logic and can be
|
|
108
|
+
expanded. If the user asks for a more language-friendly version, rewrite the technical
|
|
109
|
+
block labels in simple wording using the language of the user's request. Present that
|
|
110
|
+
as a human-friendly translation derived only from returned node, edge, decision, and
|
|
111
|
+
source fields.
|
|
112
|
+
11. End with concise follow-up choices in the user's language: simplify the labels into
|
|
113
|
+
language-friendly wording, expand omitted nodes/branches/adjacent flows, or explore a
|
|
114
|
+
related area or deeper path.
|
|
115
|
+
12. Also provide the `viewer_targets` command and hash
|
|
116
|
+
target so the user can open the same visual in `codedebrief view`.
|
|
117
|
+
13. Treat `workflow_slice.presentation` as supporting context for this request, not as the
|
|
118
|
+
primary output.
|
|
119
|
+
14. Keep the textual summary short and secondary. Do not answer with raw JSON or YAML unless
|
|
120
|
+
the user explicitly asks for it.
|
|
121
|
+
|
|
122
|
+
## Guardrails
|
|
123
|
+
|
|
124
|
+
- MCP is local-first and deterministic; do not ask for provider keys for the primary
|
|
125
|
+
workflow.
|
|
126
|
+
- Treat language-friendly labels as a presentation layer derived from deterministic
|
|
127
|
+
workflow facts.
|
|
128
|
+
- Use CodeDebrief to explain modeled code logic, not to present possible defects.
|
|
129
|
+
- Use `codedebrief view` only for the human manual UI.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _instruction_block(local_notes: str = "") -> str:
|
|
134
|
+
notes = local_notes.strip() or LOCAL_NOTES_HINT
|
|
135
|
+
return f"""{START}
|
|
136
|
+
## CodeDebrief
|
|
137
|
+
|
|
138
|
+
This project uses CodeDebrief to keep decision flows synchronized with the source code.
|
|
139
|
+
|
|
140
|
+
For codebase questions about behavior, decisions, workflow structure, or changed-code context:
|
|
141
|
+
|
|
142
|
+
1. Prefer the CodeDebrief MCP `agent_context` tool before broad file-by-file searches.
|
|
143
|
+
2. Use `agent_context` for substantial changes, passing changed files, selected code,
|
|
144
|
+
current file, flow id, symbol, or dependency path when available; inspect
|
|
145
|
+
its returned `workflow_slice` before answering.
|
|
146
|
+
3. When the user asks to show a workflow, flusso, visual flow, canvas, or
|
|
147
|
+
`workflow_slice`, prefer the canonical Mermaid visual: render
|
|
148
|
+
`workflow_slice.presentation.canonical_visual.diagram` exactly as returned only when
|
|
149
|
+
the client renders Mermaid inline. If the client cannot render Mermaid inline, or if
|
|
150
|
+
Mermaid would appear as a raw code block, call `snapshot_slice` with
|
|
151
|
+
`include_svg=false` and provide `artifact.mermaid_path`,
|
|
152
|
+
`artifact.mermaid_markdown_path`, or `artifact.mermaid_open_command` as the visual
|
|
153
|
+
result before prose. Do not paste a long Mermaid code block as the primary visual
|
|
154
|
+
unless the user explicitly asks for raw or copyable Mermaid. Do not render
|
|
155
|
+
`snapshot.svg` inline by default; SVG artifacts are for explicit SVG requests or local
|
|
156
|
+
inspection because their layout can differ from Mermaid.
|
|
157
|
+
Keep CodeDebrief visuals vertical/top-to-bottom by default. Use a horizontal layout
|
|
158
|
+
only when the user explicitly asks for a compact horizontal overview.
|
|
159
|
+
Inspect the full returned `workflow_slice` before deciding what to show. Choose the
|
|
160
|
+
first visible depth yourself: show the clearest useful subset, then say that the
|
|
161
|
+
displayed diagram is a bounded summary and can be expanded.
|
|
162
|
+
If the CodeDebrief result is too large, saved externally, truncated, or missing the exact
|
|
163
|
+
canonical visual, retry with a smaller `token_budget` and narrower `flow_id`, `symbol`,
|
|
164
|
+
`current_file`, or `scope`; do not recover by listing flows and hand-building a
|
|
165
|
+
diagram.
|
|
166
|
+
Do not synthesize a new Mermaid diagram and do not add limits, error codes, branches,
|
|
167
|
+
or service steps that are absent from the `workflow_slice` payload. Do not read source
|
|
168
|
+
files to rebuild, relabel, or extend the diagram; source reads are only follow-up
|
|
169
|
+
explanation after the deterministic visual is shown and must not change displayed
|
|
170
|
+
nodes, edges, labels, or branches. If neither exact canonical Mermaid nor a returned
|
|
171
|
+
Mermaid artifact can be used, say so and provide `viewer_targets` instead of creating
|
|
172
|
+
a replacement Mermaid diagram. If the user asks for a more language-friendly version, rewrite the
|
|
173
|
+
technical block labels in simple wording using the language of the user's request. This
|
|
174
|
+
is allowed only as a separate presentation layer derived from returned node, edge,
|
|
175
|
+
decision, and source fields. End visual answers with concise options in the user's
|
|
176
|
+
language: simplify labels, expand omitted nodes/branches/adjacent flows, or explore a
|
|
177
|
+
related area. Show raw JSON or YAML only when explicitly requested.
|
|
178
|
+
4. Use `expand_slice`, `workflow_path`, `snapshot_slice`, `explain_flow`, `explain_node`,
|
|
179
|
+
or `explain_edge` only when the first slice needs more precise context.
|
|
180
|
+
5. Use `codedebrief view ...` only when a human wants the manual UI flowchart.
|
|
181
|
+
|
|
182
|
+
When helping a user set up or learn CodeDebrief:
|
|
183
|
+
|
|
184
|
+
1. Start with `codedebrief --help`, then use `codedebrief <command> --help` for the specific
|
|
185
|
+
command you plan to run or recommend.
|
|
186
|
+
2. Use `codedebrief doctor` when install, dependency, or parser capability issues are
|
|
187
|
+
unclear.
|
|
188
|
+
3. Do not ask for LLM provider keys for the primary workflow. Language-friendly labels
|
|
189
|
+
are a presentation layer derived from deterministic workflow facts.
|
|
190
|
+
4. `codedebrief setup-agent <target>` updates only that target's files. Run the command
|
|
191
|
+
separately for each agent surface you want to configure, preserving any target-specific
|
|
192
|
+
frontmatter and local notes.
|
|
193
|
+
|
|
194
|
+
After code or workflow-relevant changes:
|
|
195
|
+
|
|
196
|
+
1. Treat CodeDebrief artifacts as part of done. After every meaningful source, route,
|
|
197
|
+
config, or agent-instruction change, run `codedebrief update` before finalizing or
|
|
198
|
+
committing so MCP answers and `codedebrief view` use current graphs. Skip only changes
|
|
199
|
+
that cannot affect the modeled code logic, such as unrelated copy edits or images.
|
|
200
|
+
2. Use `codedebrief update --full` after analyzer upgrades, parser/dependency changes,
|
|
201
|
+
large refactors, or when cached file models should be ignored.
|
|
202
|
+
3. Run `codedebrief validate --check-sync`.
|
|
203
|
+
4. Commit synchronized changes to:
|
|
204
|
+
- `codedebrief-out/codedebrief.json`
|
|
205
|
+
- `codedebrief-out/codedebrief.md`
|
|
206
|
+
5. Use CodeDebrief MCP `agent_context` to inspect affected entry points and callers when
|
|
207
|
+
explaining or reviewing the change.
|
|
208
|
+
6. Ground the explanation in the returned `workflow_slice`; expand it through MCP only
|
|
209
|
+
when the initial slice omits relevant callers, callees, domain states, or paths.
|
|
210
|
+
|
|
211
|
+
For viewer/UI changes:
|
|
212
|
+
|
|
213
|
+
1. Run `npm run viewer:typecheck`, `npm run viewer:test`, and `npm run viewer:build`.
|
|
214
|
+
2. Regenerate HTML artifacts with `codedebrief update` and
|
|
215
|
+
`codedebrief view --render-only --no-open`.
|
|
216
|
+
3. Check the generated viewer with a cache-buster URL.
|
|
217
|
+
|
|
218
|
+
{LOCAL_NOTES_START}
|
|
219
|
+
{notes}
|
|
220
|
+
{LOCAL_NOTES_END}
|
|
221
|
+
|
|
222
|
+
CodeDebrief is a comprehension and navigation tool for source-grounded workflows. Use it
|
|
223
|
+
to explain modeled logic, not to present possible defects.
|
|
224
|
+
{END}
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
INSTRUCTION_BLOCK = _instruction_block()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def install_all(root: Path, platform: str = "all", mcp_config: str = "none") -> list[Path]:
|
|
232
|
+
changed = install_agent_instructions(root, platform)
|
|
233
|
+
changed.extend(install_agent_skill(root, platform))
|
|
234
|
+
if mcp_config != "none":
|
|
235
|
+
changed.extend(install_mcp_config(root, mcp_config))
|
|
236
|
+
return changed
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def install_agent_instructions(root: Path, platform: str = "all") -> list[Path]:
|
|
240
|
+
names = tuple(AGENT_INSTRUCTION_TARGETS) if platform == "all" else (platform,)
|
|
241
|
+
unknown = set(names) - set(AGENT_INSTRUCTION_TARGETS)
|
|
242
|
+
if unknown:
|
|
243
|
+
known = ", ".join(("all", *AGENT_INSTRUCTION_TARGETS))
|
|
244
|
+
raise ValueError(f"unknown agent instruction target {platform!r}; known targets: {known}")
|
|
245
|
+
targets = [root / AGENT_INSTRUCTION_TARGETS[name] for name in names]
|
|
246
|
+
|
|
247
|
+
changed: list[Path] = []
|
|
248
|
+
for target in targets:
|
|
249
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
250
|
+
existing = target.read_text(encoding="utf-8") if target.exists() else ""
|
|
251
|
+
updated = _upsert(existing, INSTRUCTION_BLOCK)
|
|
252
|
+
if target.suffix == ".mdc" and not updated.startswith("---"):
|
|
253
|
+
frontmatter = (
|
|
254
|
+
"---\ndescription: Keep CodeDebrief synchronized\nalwaysApply: true\n---\n\n"
|
|
255
|
+
)
|
|
256
|
+
updated = frontmatter + updated
|
|
257
|
+
if updated != existing:
|
|
258
|
+
target.write_text(updated, encoding="utf-8")
|
|
259
|
+
changed.append(target)
|
|
260
|
+
return changed
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def install_agent_skill(root: Path, platform: str = "all") -> list[Path]:
|
|
264
|
+
names = tuple(AGENT_SKILL_TARGETS) if platform == "all" else (platform,)
|
|
265
|
+
unknown = set(names) - set(AGENT_INSTRUCTION_TARGETS)
|
|
266
|
+
if unknown:
|
|
267
|
+
known = ", ".join(("all", *AGENT_INSTRUCTION_TARGETS))
|
|
268
|
+
raise ValueError(f"unknown agent skill target {platform!r}; known targets: {known}")
|
|
269
|
+
|
|
270
|
+
changed: list[Path] = []
|
|
271
|
+
for name in names:
|
|
272
|
+
target_path = AGENT_SKILL_TARGETS.get(name)
|
|
273
|
+
if target_path is None:
|
|
274
|
+
continue
|
|
275
|
+
target = root / target_path
|
|
276
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
277
|
+
existing = target.read_text(encoding="utf-8") if target.exists() else ""
|
|
278
|
+
if existing != SKILL_TEMPLATE:
|
|
279
|
+
target.write_text(SKILL_TEMPLATE, encoding="utf-8")
|
|
280
|
+
changed.append(target)
|
|
281
|
+
return changed
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def install_mcp_config(root: Path, target: str = "all") -> list[Path]:
|
|
285
|
+
root = root.resolve()
|
|
286
|
+
targets = MCP_CONFIG_TARGETS if target == "all" else (target,)
|
|
287
|
+
unknown = set(targets) - set(MCP_CONFIG_TARGETS)
|
|
288
|
+
if unknown:
|
|
289
|
+
known = ", ".join(("all", *MCP_CONFIG_TARGETS))
|
|
290
|
+
raise ValueError(f"unknown MCP config target {target!r}; known targets: {known}")
|
|
291
|
+
|
|
292
|
+
changed: list[Path] = []
|
|
293
|
+
for item in targets:
|
|
294
|
+
if item == "codex":
|
|
295
|
+
path = _install_codex_mcp_config(root)
|
|
296
|
+
elif item == "claude":
|
|
297
|
+
path = _install_json_mcp_config(root / ".mcp.json", root)
|
|
298
|
+
elif item == "gemini":
|
|
299
|
+
path = _install_json_mcp_config(root / ".gemini" / "settings.json", root)
|
|
300
|
+
else:
|
|
301
|
+
path = _install_json_mcp_config(root / ".cursor" / "mcp.json", root)
|
|
302
|
+
if path is not None:
|
|
303
|
+
changed.append(path)
|
|
304
|
+
return changed
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _upsert(existing: str, block: str) -> str:
|
|
308
|
+
if START in existing and END in existing:
|
|
309
|
+
before, remainder = existing.split(START, 1)
|
|
310
|
+
managed, after = remainder.split(END, 1)
|
|
311
|
+
block = _instruction_block(_extract_local_notes(managed))
|
|
312
|
+
# When the block sits at the very top (no prose before it), don't reintroduce a
|
|
313
|
+
# leading blank line - otherwise re-running `install` on a freshly created file
|
|
314
|
+
# would keep prepending whitespace instead of reaching a fixed point.
|
|
315
|
+
prefix = before.rstrip() + "\n\n" if before.strip() else ""
|
|
316
|
+
return prefix + block.rstrip() + "\n" + after.lstrip()
|
|
317
|
+
if not existing.strip():
|
|
318
|
+
# Match the fixed point the upsert branch produces for a block-only file, so a
|
|
319
|
+
# second `install` on a freshly created file is a true no-op.
|
|
320
|
+
return block.rstrip() + "\n"
|
|
321
|
+
return existing.rstrip() + "\n\n" + block.rstrip() + "\n"
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _extract_local_notes(managed: str) -> str:
|
|
325
|
+
if LOCAL_NOTES_START in managed and LOCAL_NOTES_END in managed:
|
|
326
|
+
_, remainder = managed.split(LOCAL_NOTES_START, 1)
|
|
327
|
+
notes, _ = remainder.split(LOCAL_NOTES_END, 1)
|
|
328
|
+
stripped = notes.strip()
|
|
329
|
+
return "" if stripped == LOCAL_NOTES_HINT else stripped
|
|
330
|
+
return _extract_legacy_local_notes(managed)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _extract_legacy_local_notes(managed: str) -> str:
|
|
334
|
+
marker = "For local real-world regression checks:"
|
|
335
|
+
start = managed.find(marker)
|
|
336
|
+
if start == -1:
|
|
337
|
+
return ""
|
|
338
|
+
tail = managed[start:].splitlines()
|
|
339
|
+
collected: list[str] = []
|
|
340
|
+
saw_list_item = False
|
|
341
|
+
for line in tail:
|
|
342
|
+
stripped = line.strip()
|
|
343
|
+
if not stripped:
|
|
344
|
+
collected.append(line)
|
|
345
|
+
continue
|
|
346
|
+
if not collected or stripped == marker:
|
|
347
|
+
collected.append(line)
|
|
348
|
+
continue
|
|
349
|
+
if re.match(r"^(?:\d+\.|[-*])\s+", stripped):
|
|
350
|
+
saw_list_item = True
|
|
351
|
+
collected.append(line)
|
|
352
|
+
continue
|
|
353
|
+
if saw_list_item:
|
|
354
|
+
break
|
|
355
|
+
collected.append(line)
|
|
356
|
+
return "\n".join(collected).strip()
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _install_codex_mcp_config(root: Path) -> Path | None:
|
|
360
|
+
target = root / ".codex" / "config.toml"
|
|
361
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
362
|
+
existing = target.read_text(encoding="utf-8") if target.exists() else ""
|
|
363
|
+
unmanaged = _without_managed_block(existing, CODEX_MCP_START, CODEX_MCP_END)
|
|
364
|
+
if re.search(r"(?m)^\s*\[mcp_servers\.codedebrief\]\s*$", unmanaged):
|
|
365
|
+
raise ValueError(
|
|
366
|
+
f"{target} already defines [mcp_servers.codedebrief] outside the "
|
|
367
|
+
"CodeDebrief managed block."
|
|
368
|
+
)
|
|
369
|
+
block = "\n".join(
|
|
370
|
+
[
|
|
371
|
+
CODEX_MCP_START,
|
|
372
|
+
"[mcp_servers.codedebrief]",
|
|
373
|
+
'command = "codedebrief"',
|
|
374
|
+
f"args = {_toml_array(['mcp', str(root)])}",
|
|
375
|
+
f"cwd = {json.dumps(str(root))}",
|
|
376
|
+
'default_tools_approval_mode = "approve"',
|
|
377
|
+
CODEX_MCP_END,
|
|
378
|
+
"",
|
|
379
|
+
]
|
|
380
|
+
)
|
|
381
|
+
updated = _upsert_managed_block(existing, block, CODEX_MCP_START, CODEX_MCP_END)
|
|
382
|
+
if updated != existing:
|
|
383
|
+
target.write_text(updated, encoding="utf-8")
|
|
384
|
+
return target
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _install_json_mcp_config(target: Path, root: Path) -> Path | None:
|
|
389
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
390
|
+
existing = target.read_text(encoding="utf-8") if target.exists() else ""
|
|
391
|
+
data: dict[str, object]
|
|
392
|
+
if existing.strip():
|
|
393
|
+
try:
|
|
394
|
+
loaded = json.loads(existing)
|
|
395
|
+
except json.JSONDecodeError as error:
|
|
396
|
+
raise ValueError(f"invalid JSON in {target}: {error}") from error
|
|
397
|
+
if not isinstance(loaded, dict):
|
|
398
|
+
raise ValueError(f"{target} must contain a JSON object")
|
|
399
|
+
data = loaded
|
|
400
|
+
else:
|
|
401
|
+
data = {}
|
|
402
|
+
|
|
403
|
+
servers = data.setdefault("mcpServers", {})
|
|
404
|
+
if not isinstance(servers, dict):
|
|
405
|
+
raise ValueError(f"{target} has non-object mcpServers")
|
|
406
|
+
existing_server = servers.get("codedebrief", {})
|
|
407
|
+
if existing_server is not None and not isinstance(existing_server, dict):
|
|
408
|
+
raise ValueError(f"{target} has non-object mcpServers.codedebrief")
|
|
409
|
+
server = dict(existing_server or {})
|
|
410
|
+
server.update({"command": "codedebrief", "args": ["mcp", str(root)]})
|
|
411
|
+
servers["codedebrief"] = server
|
|
412
|
+
|
|
413
|
+
updated = json.dumps(data, indent=2) + "\n"
|
|
414
|
+
if updated != existing:
|
|
415
|
+
target.write_text(updated, encoding="utf-8")
|
|
416
|
+
return target
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _without_managed_block(existing: str, start: str, end: str) -> str:
|
|
421
|
+
if start not in existing or end not in existing:
|
|
422
|
+
return existing
|
|
423
|
+
before, remainder = existing.split(start, 1)
|
|
424
|
+
_, after = remainder.split(end, 1)
|
|
425
|
+
return before + after
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _upsert_managed_block(existing: str, block: str, start: str, end: str) -> str:
|
|
429
|
+
if start in existing and end in existing:
|
|
430
|
+
before, remainder = existing.split(start, 1)
|
|
431
|
+
_, after = remainder.split(end, 1)
|
|
432
|
+
prefix = before.rstrip() + "\n\n" if before.strip() else ""
|
|
433
|
+
suffix = "\n" + after.lstrip() if after.strip() else ""
|
|
434
|
+
return prefix + block.rstrip() + "\n" + suffix
|
|
435
|
+
if not existing.strip():
|
|
436
|
+
return block.rstrip() + "\n"
|
|
437
|
+
return existing.rstrip() + "\n\n" + block.rstrip() + "\n"
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _toml_array(values: list[str]) -> str:
|
|
441
|
+
return "[" + ", ".join(json.dumps(value) for value in values) + "]"
|