Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions extensions/agent-context/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Not every Spec Kit user wants Spec Kit to write into the coding agent's context

- **Opt out** entirely with `specify extension disable agent-context` — Spec Kit will then never create or modify the agent context file.
- **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.
- **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`.
- **Refresh on demand** with `/speckit.agent-context.update`, or automatically through the hooks declared in `extension.yml` (`after_specify`, `after_plan`).

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

# Optional list of coding agent context files to manage together.
# When non-empty, this takes precedence over context_file.
context_files:
- AGENTS.md
- CLAUDE.md

# Delimiters for the managed Spec Kit section
context_markers:
start: "<!-- SPECKIT START -->"
end: "<!-- SPECKIT END -->"
```

- `context_file` — the project-relative path to the coding agent context file, written by `specify init` and `specify integration install`.
- `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.
- `context_markers.start` / `.end` — the delimiters around the managed section. Edit these to use custom markers.

## Requirements
Expand All @@ -55,3 +63,4 @@ specify extension disable agent-context
```

When disabled, Spec Kit skips context file creation, updates, and removal (the gates are inside `upsert_context_section()` and `remove_context_section()`).
Disabled projects also ignore stale `context_files` values during command rendering so disabling the extension remains a complete opt-out.
7 changes: 6 additions & 1 deletion extensions/agent-context/agent-context-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
# These values are populated automatically by `specify init` and
# `specify integration use` / `specify integration install`.

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

# Optional list of project-relative coding agent context files managed by this
# extension. When non-empty, this list takes precedence over `context_file`.
# Use this for projects that intentionally keep multiple agent anchors in sync.
context_files: []

# Delimiters for the managed Spec Kit section.
# Edit these to use custom markers.
context_markers:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ The script reads the agent-context extension config at
`.specify/extensions/agent-context/agent-context-config.yml` to discover:

- `context_file` — the path of the coding agent context file to manage.
- `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`.
- `context_markers.start` / `.end` — the delimiters surrounding the managed section. Defaults to `<!-- SPECKIT START -->` and `<!-- SPECKIT END -->` when the field is missing.

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`).

If `context_file` is empty or the file cannot be located, the command reports nothing to do and exits successfully.
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.

## Execution

Expand Down
90 changes: 64 additions & 26 deletions extensions/agent-context/scripts/bash/update-agent-context.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#!/usr/bin/env bash
# update-agent-context.sh
#
# Refresh the managed Spec Kit section in the coding agent's context file
# Refresh the managed Spec Kit section in the coding agent's context file(s)
# (e.g. CLAUDE.md, .github/copilot-instructions.md, AGENTS.md).
#
# Reads `context_file` and `context_markers.{start,end}` from the
# Reads `context_files` or `context_file`, plus `context_markers.{start,end}`, from the
# agent-context extension config:
# .specify/extensions/agent-context/agent-context-config.yml
#
Expand Down Expand Up @@ -39,9 +39,10 @@ if [[ -z "$_python" ]]; then
exit 0
fi

# Parse extension config once; emit three newline-separated fields:
# context_file, context_markers.start, context_markers.end
# Parse extension config once; emit three JSON lines:
# context files array, context_markers.start, context_markers.end
if ! _raw_opts="$("$_python" - "$EXT_CONFIG" <<'PY'
import json
import sys
try:
import yaml
Expand Down Expand Up @@ -73,7 +74,17 @@ def get_str(obj, *keys):
else:
return ""
return node if isinstance(node, str) else ""
print(get_str(data, "context_file"))
context_files = []
raw_files = data.get("context_files")
if isinstance(raw_files, list):
for value in raw_files:
if isinstance(value, str) and value.strip() and value.strip() not in context_files:
context_files.append(value.strip())
if not context_files:
raw_file = get_str(data, "context_file")
if raw_file:
context_files.append(raw_file)
print(json.dumps(context_files))
print(get_str(data, "context_markers", "start"))
print(get_str(data, "context_markers", "end"))
PY
Expand All @@ -87,33 +98,58 @@ while IFS= read -r _line || [[ -n "$_line" ]]; do
_opts_lines+=("$_line")
done < <(printf '%s\n' "$_raw_opts")
if (( ${#_opts_lines[@]} < 3 )); then
echo "agent-context: malformed config parser output; expected 3 lines (context_file, marker_start, marker_end), got ${#_opts_lines[@]}; skipping update." >&2
echo "agent-context: malformed config parser output; expected 3 lines (context_files, marker_start, marker_end), got ${#_opts_lines[@]}; skipping update." >&2
exit 0
fi
CONTEXT_FILE="${_opts_lines[0]}"
CONTEXT_FILES_JSON="${_opts_lines[0]}"
MARKER_START="${_opts_lines[1]}"
MARKER_END="${_opts_lines[2]}"

if [[ -z "$CONTEXT_FILE" ]]; then
echo "agent-context: context_file not set in extension config; nothing to do." >&2
if ! _context_files_raw="$("$_python" - "$CONTEXT_FILES_JSON" <<'PY'
import json
import sys
try:
data = json.loads(sys.argv[1])
except Exception:
data = []
if not isinstance(data, list):
data = []
for value in data:
if isinstance(value, str) and value:
print(value)
PY
)"; then
echo "agent-context: malformed context_files parser output; skipping update." >&2
exit 0
fi

# Reject absolute paths, backslash separators, and '..' path segments in context_file
if [[ "$CONTEXT_FILE" == /* ]] || [[ "$CONTEXT_FILE" =~ ^[A-Za-z]: ]]; then
echo "agent-context: context_file must be a project-relative path; got '$CONTEXT_FILE'." >&2
exit 1
fi
if [[ "$CONTEXT_FILE" == *\\* ]]; then
echo "agent-context: context_file must not contain backslash separators; got '$CONTEXT_FILE'." >&2
exit 1
CONTEXT_FILES=()
while IFS= read -r _line || [[ -n "$_line" ]]; do
[[ -n "$_line" ]] && CONTEXT_FILES+=("$_line")
done < <(printf '%s\n' "$_context_files_raw")

if (( ${#CONTEXT_FILES[@]} == 0 )); then
echo "agent-context: context_files/context_file not set in extension config; nothing to do." >&2
exit 0
fi
IFS='/' read -ra _cf_parts <<< "$CONTEXT_FILE"
for _seg in "${_cf_parts[@]}"; do
if [[ "$_seg" == ".." ]]; then
echo "agent-context: context_file must not contain '..' path segments; got '$CONTEXT_FILE'." >&2

for CONTEXT_FILE in "${CONTEXT_FILES[@]}"; do
# Reject absolute paths, backslash separators, and '..' path segments in context files
if [[ "$CONTEXT_FILE" == /* ]] || [[ "$CONTEXT_FILE" =~ ^[A-Za-z]: ]]; then
echo "agent-context: context files must be project-relative paths; got '$CONTEXT_FILE'." >&2
exit 1
fi
if [[ "$CONTEXT_FILE" == *\\* ]]; then
echo "agent-context: context files must not contain backslash separators; got '$CONTEXT_FILE'." >&2
exit 1
fi
IFS='/' read -ra _cf_parts <<< "$CONTEXT_FILE"
for _seg in "${_cf_parts[@]}"; do
if [[ "$_seg" == ".." ]]; then
echo "agent-context: context files must not contain '..' path segments; got '$CONTEXT_FILE'." >&2
exit 1
fi
done
done
unset _cf_parts _seg

Expand Down Expand Up @@ -142,9 +178,6 @@ PY
fi
fi

CTX_PATH="$PROJECT_ROOT/$CONTEXT_FILE"
mkdir -p "$(dirname "$CTX_PATH")"

# Build the managed section
TMP_SECTION="$(mktemp)"
trap 'rm -f "$TMP_SECTION"' EXIT
Expand All @@ -158,7 +191,11 @@ trap 'rm -f "$TMP_SECTION"' EXIT
echo "$MARKER_END"
} > "$TMP_SECTION"

"$_python" - "$CTX_PATH" "$MARKER_START" "$MARKER_END" "$TMP_SECTION" <<'PY'
for CONTEXT_FILE in "${CONTEXT_FILES[@]}"; do
CTX_PATH="$PROJECT_ROOT/$CONTEXT_FILE"
mkdir -p "$(dirname "$CTX_PATH")"

"$_python" - "$CTX_PATH" "$MARKER_START" "$MARKER_END" "$TMP_SECTION" <<'PY'
Comment on lines +209 to +213
import sys, os
ctx_path, start, end, section_path = sys.argv[1:5]
with open(section_path, "r", encoding="utf-8") as fh:
Expand Down Expand Up @@ -197,4 +234,5 @@ with open(ctx_path, "wb") as fh:
fh.write(new_content.encode("utf-8"))
PY

echo "agent-context: updated $CONTEXT_FILE"
echo "agent-context: updated $CONTEXT_FILE"
done
Loading