Skip to content

Commit abcaecc

Browse files
committed
fix(agent-context): support multiple context files safely
1 parent 1b0556c commit abcaecc

15 files changed

Lines changed: 644 additions & 187 deletions

File tree

extensions/agent-context/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Not every Spec Kit user wants Spec Kit to write into the coding agent's context
1010

1111
- **Opt out** entirely with `specify extension disable agent-context` — Spec Kit will then never create or modify the agent context file.
1212
- **Customize the markers** by editing `.specify/extensions/agent-context/agent-context-config.yml` — both the Python layer and the bundled scripts honor the same `context_markers` value.
13+
- **Synchronize multiple agent anchors** by setting `context_files` when a project intentionally uses more than one coding agent context file, such as `AGENTS.md` and `CLAUDE.md`.
1314
- **Refresh on demand** with `/speckit.agent-context.update`, or automatically through the hooks declared in `extension.yml` (`after_specify`, `after_plan`).
1415

1516
## Commands
@@ -27,13 +28,20 @@ All configuration flows through the extension's own config file at
2728
# Path to the coding agent context file managed by this extension
2829
context_file: CLAUDE.md
2930

31+
# Optional list of coding agent context files to manage together.
32+
# When non-empty, this takes precedence over context_file.
33+
context_files:
34+
- AGENTS.md
35+
- CLAUDE.md
36+
3037
# Delimiters for the managed Spec Kit section
3138
context_markers:
3239
start: "<!-- SPECKIT START -->"
3340
end: "<!-- SPECKIT END -->"
3441
```
3542
3643
- `context_file` — the project-relative path to the coding agent context file, written by `specify init` and `specify integration install`.
44+
- `context_files` — optional project-relative paths to multiple coding agent context files. When non-empty, the list takes precedence over `context_file`. Absolute paths, backslash separators, and `..` path segments are rejected.
3745
- `context_markers.start` / `.end` — the delimiters around the managed section. Edit these to use custom markers.
3846

3947
## Requirements
@@ -55,3 +63,4 @@ specify extension disable agent-context
5563
```
5664

5765
When disabled, Spec Kit skips context file creation, updates, and removal (the gates are inside `upsert_context_section()` and `remove_context_section()`).
66+
Disabled projects also ignore stale `context_files` values during command rendering so disabling the extension remains a complete opt-out.

extensions/agent-context/agent-context-config.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22
# These values are populated automatically by `specify init` and
33
# `specify integration use` / `specify integration install`.
44

5-
# Path (relative to the project root) to the coding agent context file
5+
# Path (relative to the project root) to the default coding agent context file
66
# managed by this extension (e.g. CLAUDE.md, AGENTS.md,
77
# .github/copilot-instructions.md). Set automatically from the active
88
# integration and regenerated during `specify init` or integration switches.
99
context_file: ""
1010

11+
# Optional list of project-relative coding agent context files managed by this
12+
# extension. When non-empty, this list takes precedence over `context_file`.
13+
# Use this for projects that intentionally keep multiple agent anchors in sync.
14+
context_files: []
15+
1116
# Delimiters for the managed Spec Kit section.
1217
# Edit these to use custom markers.
1318
context_markers:

extensions/agent-context/commands/speckit.agent-context.update.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ The script reads the agent-context extension config at
1212
`.specify/extensions/agent-context/agent-context-config.yml` to discover:
1313

1414
- `context_file` — the path of the coding agent context file to manage.
15+
- `context_files` — optional project-relative paths for multiple coding agent context files. When non-empty, the script updates each listed file and the list takes precedence over `context_file`.
1516
- `context_markers.start` / `.end` — the delimiters surrounding the managed section. Defaults to `<!-- SPECKIT START -->` and `<!-- SPECKIT END -->` when the field is missing.
1617

1718
It then creates, replaces, or appends the managed block so that the section points at the most recent plan path when one can be discovered (`specs/<feature>/plan.md`).
1819

19-
If `context_file` is empty or the file cannot be located, the command reports nothing to do and exits successfully.
20+
If `context_files` and `context_file` are empty, the command reports nothing to do and exits successfully. Context file paths must stay project-relative; absolute paths and `..` path segments are rejected.
2021

2122
## Execution
2223

extensions/agent-context/scripts/bash/update-agent-context.sh

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
#!/usr/bin/env bash
22
# update-agent-context.sh
33
#
4-
# Refresh the managed Spec Kit section in the coding agent's context file
4+
# Refresh the managed Spec Kit section in the coding agent's context file(s)
55
# (e.g. CLAUDE.md, .github/copilot-instructions.md, AGENTS.md).
66
#
7-
# Reads `context_file` and `context_markers.{start,end}` from the
7+
# Reads `context_files` or `context_file`, plus `context_markers.{start,end}`, from the
88
# agent-context extension config:
99
# .specify/extensions/agent-context/agent-context-config.yml
1010
#
@@ -39,9 +39,10 @@ if [[ -z "$_python" ]]; then
3939
exit 0
4040
fi
4141

42-
# Parse extension config once; emit three newline-separated fields:
43-
# context_file, context_markers.start, context_markers.end
42+
# Parse extension config once; emit three JSON lines:
43+
# context files array, context_markers.start, context_markers.end
4444
if ! _raw_opts="$("$_python" - "$EXT_CONFIG" <<'PY'
45+
import json
4546
import sys
4647
try:
4748
import yaml
@@ -73,7 +74,17 @@ def get_str(obj, *keys):
7374
else:
7475
return ""
7576
return node if isinstance(node, str) else ""
76-
print(get_str(data, "context_file"))
77+
context_files = []
78+
raw_files = data.get("context_files")
79+
if isinstance(raw_files, list):
80+
for value in raw_files:
81+
if isinstance(value, str) and value.strip() and value.strip() not in context_files:
82+
context_files.append(value.strip())
83+
if not context_files:
84+
raw_file = get_str(data, "context_file")
85+
if raw_file:
86+
context_files.append(raw_file)
87+
print(json.dumps(context_files))
7788
print(get_str(data, "context_markers", "start"))
7889
print(get_str(data, "context_markers", "end"))
7990
PY
@@ -87,33 +98,58 @@ while IFS= read -r _line || [[ -n "$_line" ]]; do
8798
_opts_lines+=("$_line")
8899
done < <(printf '%s\n' "$_raw_opts")
89100
if (( ${#_opts_lines[@]} < 3 )); then
90-
echo "agent-context: malformed config parser output; expected 3 lines (context_file, marker_start, marker_end), got ${#_opts_lines[@]}; skipping update." >&2
101+
echo "agent-context: malformed config parser output; expected 3 lines (context_files, marker_start, marker_end), got ${#_opts_lines[@]}; skipping update." >&2
91102
exit 0
92103
fi
93-
CONTEXT_FILE="${_opts_lines[0]}"
104+
CONTEXT_FILES_JSON="${_opts_lines[0]}"
94105
MARKER_START="${_opts_lines[1]}"
95106
MARKER_END="${_opts_lines[2]}"
96107

97-
if [[ -z "$CONTEXT_FILE" ]]; then
98-
echo "agent-context: context_file not set in extension config; nothing to do." >&2
108+
if ! _context_files_raw="$("$_python" - "$CONTEXT_FILES_JSON" <<'PY'
109+
import json
110+
import sys
111+
try:
112+
data = json.loads(sys.argv[1])
113+
except Exception:
114+
data = []
115+
if not isinstance(data, list):
116+
data = []
117+
for value in data:
118+
if isinstance(value, str) and value:
119+
print(value)
120+
PY
121+
)"; then
122+
echo "agent-context: malformed context_files parser output; skipping update." >&2
99123
exit 0
100124
fi
101125

102-
# Reject absolute paths, backslash separators, and '..' path segments in context_file
103-
if [[ "$CONTEXT_FILE" == /* ]] || [[ "$CONTEXT_FILE" =~ ^[A-Za-z]: ]]; then
104-
echo "agent-context: context_file must be a project-relative path; got '$CONTEXT_FILE'." >&2
105-
exit 1
106-
fi
107-
if [[ "$CONTEXT_FILE" == *\\* ]]; then
108-
echo "agent-context: context_file must not contain backslash separators; got '$CONTEXT_FILE'." >&2
109-
exit 1
126+
CONTEXT_FILES=()
127+
while IFS= read -r _line || [[ -n "$_line" ]]; do
128+
[[ -n "$_line" ]] && CONTEXT_FILES+=("$_line")
129+
done < <(printf '%s\n' "$_context_files_raw")
130+
131+
if (( ${#CONTEXT_FILES[@]} == 0 )); then
132+
echo "agent-context: context_files/context_file not set in extension config; nothing to do." >&2
133+
exit 0
110134
fi
111-
IFS='/' read -ra _cf_parts <<< "$CONTEXT_FILE"
112-
for _seg in "${_cf_parts[@]}"; do
113-
if [[ "$_seg" == ".." ]]; then
114-
echo "agent-context: context_file must not contain '..' path segments; got '$CONTEXT_FILE'." >&2
135+
136+
for CONTEXT_FILE in "${CONTEXT_FILES[@]}"; do
137+
# Reject absolute paths, backslash separators, and '..' path segments in context files
138+
if [[ "$CONTEXT_FILE" == /* ]] || [[ "$CONTEXT_FILE" =~ ^[A-Za-z]: ]]; then
139+
echo "agent-context: context files must be project-relative paths; got '$CONTEXT_FILE'." >&2
140+
exit 1
141+
fi
142+
if [[ "$CONTEXT_FILE" == *\\* ]]; then
143+
echo "agent-context: context files must not contain backslash separators; got '$CONTEXT_FILE'." >&2
115144
exit 1
116145
fi
146+
IFS='/' read -ra _cf_parts <<< "$CONTEXT_FILE"
147+
for _seg in "${_cf_parts[@]}"; do
148+
if [[ "$_seg" == ".." ]]; then
149+
echo "agent-context: context files must not contain '..' path segments; got '$CONTEXT_FILE'." >&2
150+
exit 1
151+
fi
152+
done
117153
done
118154
unset _cf_parts _seg
119155

@@ -142,9 +178,6 @@ PY
142178
fi
143179
fi
144180

145-
CTX_PATH="$PROJECT_ROOT/$CONTEXT_FILE"
146-
mkdir -p "$(dirname "$CTX_PATH")"
147-
148181
# Build the managed section
149182
TMP_SECTION="$(mktemp)"
150183
trap 'rm -f "$TMP_SECTION"' EXIT
@@ -158,7 +191,11 @@ trap 'rm -f "$TMP_SECTION"' EXIT
158191
echo "$MARKER_END"
159192
} > "$TMP_SECTION"
160193

161-
"$_python" - "$CTX_PATH" "$MARKER_START" "$MARKER_END" "$TMP_SECTION" <<'PY'
194+
for CONTEXT_FILE in "${CONTEXT_FILES[@]}"; do
195+
CTX_PATH="$PROJECT_ROOT/$CONTEXT_FILE"
196+
mkdir -p "$(dirname "$CTX_PATH")"
197+
198+
"$_python" - "$CTX_PATH" "$MARKER_START" "$MARKER_END" "$TMP_SECTION" <<'PY'
162199
import sys, os
163200
ctx_path, start, end, section_path = sys.argv[1:5]
164201
with open(section_path, "r", encoding="utf-8") as fh:
@@ -197,4 +234,5 @@ with open(ctx_path, "wb") as fh:
197234
fh.write(new_content.encode("utf-8"))
198235
PY
199236

200-
echo "agent-context: updated $CONTEXT_FILE"
237+
echo "agent-context: updated $CONTEXT_FILE"
238+
done

0 commit comments

Comments
 (0)