mirror of
https://github.com/github/spec-kit.git
synced 2026-07-03 12:28:06 +08:00
* docs: add Spec Kit spec for agent-context full opt-in Use Spec Kit's own specify workflow to author the spec that makes the agent-context extension a full opt-in, removing all agent-context configuration/support from the Python codebase and removing the deprecation message. Force-added despite specs/ being gitignored; the generated artifact will be purged prior to merge. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: add Spec Kit plan artifacts for agent-context full opt-in Phase 0/1 of the SDD plan workflow: plan.md, research.md, data-model.md, quickstart.md, and contracts/cli-behavior.md. Constitution Check is a documented no-op (repo has no ratified constitution). Force-added despite specs/ being gitignored; generated artifacts will be purged prior to merge. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: correct Constitution Check against ratified v1.0.0 Earlier draft wrongly treated the gate as a no-op; the fork's main is 16 commits behind upstream/main, which carries .specify/memory/constitution.md. Re-evaluate the feature against Principles I-V (all PASS) and note that Principle I mandates keeping context_file as a declared class attribute, validating the R1 metadata decision. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: refresh plan artifacts against synced upstream/main After syncing fork main to upstream and rebasing, re-scan the current agent-context surface. Upstream generalized the single context_file into a plural context_files concept with new resolver helpers (_resolve_context_files, _resolve_context_file_values, _format_context_file_values) and upsert/remove now loop over multiple files. Update research.md, data-model.md, contracts, quickstart grep guards, and the plan summary to cover the expanded removal scope. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: add Spec Kit tasks for agent-context full opt-in Phase 2 of SDD: dependency-ordered tasks.md (30 tasks) organized by the three user stories, with mandatory test tasks (Constitution Principle II) and a foundational phase decoupling __CONTEXT_FILE__ resolution from the extension config. Includes the extension self-seeding task (T015) and a static guard test (T002) enforcing zero agent-context references in the CLI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat!: remove agent-context lifecycle from the Specify CLI Make the agent-context extension a full opt-in. The CLI no longer installs the extension during init, writes agent-context-config.yml, or creates/updates/removes the managed Spec Kit section in agent context files. Context-section upsert/remove, marker resolution, extension-enabled gating, the config helpers, and the obsolete inline deprecation warning are all removed. Integration context_file stays as inert metadata; __CONTEXT_FILE__ now resolves from registry metadata. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(agent-context): self-seed context file from the active integration When agent-context-config.yml has no context_file/context_files, the bundled bash and PowerShell update scripts now resolve the context file from the active integration in .specify/init-options.json via the integration registry, so the extension no longer depends on the CLI writing its config. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test+docs: update suite and docs for agent-context opt-in Update integration/extension tests to expect no agent-context install, config, or context-section writes during init. Add a static guard test (test_agent_context_cli_free.py) asserting the CLI source is free of agent-context lifecycle symbols, plus backward-compatibility tests for legacy projects. Refresh AGENTS.md, the extension README, and add a CHANGELOG entry describing the opt-in behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(agent-context): warn on self-seed failure, correct docs, speed up guard test Address PR review feedback: - Self-seed scripts (bash + PowerShell) now emit an actionable warning when an active integration is configured but specify_cli cannot be imported by the chosen Python (e.g. pipx installs), or when the integration declares no context file, instead of silently falling through to 'nothing to do'. - Correct the extension README disable note: command rendering never reads the extension config; __CONTEXT_FILE__ is always substituted from integration metadata, so a stale context_files value cannot affect rendering. - Cache CLI source reads in the static guard test via a module-scoped fixture so the directory walk happens once instead of once per forbidden symbol. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(agent-context): ship self-owned per-agent context-file defaults The extension now bundles agent-context-defaults.json (key→context_file map) and self-seeds from it, dropping any dependency on the Specify CLI registry. Both the bash and PowerShell update scripts read the bundled JSON map keyed by the active integration from init-options.json. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat!: remove all agent-context state from the Specify CLI Strip every context_file reference from the CLI: the field on all 35 integration classes, the IntegrationBase plumbing (process_template param/step, _context_file_display, docstrings), the __CONTEXT_FILE__ resolution in agents.py, the legacy context_file/context_markers popping in _helpers.py, and the context_file template in integration_scaffold.py. Also drop the Agent context update step and __CONTEXT_FILE__ placeholder from templates/commands/plan.md. The agent-context extension now solely owns all context-file knowledge, including the per-agent default mapping. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: drop context_file coverage and guard against CLI reintroduction Remove CONTEXT_FILE attrs and context_file assertions across the base mixins, all 35 per-integration test files, shared integration tests, and conftest stubs. Rewrite the base-mixin context tests to assert no managed section is written and no __CONTEXT_FILE__ placeholder survives. Extend the CLI-free static guard to forbid context_file, __CONTEXT_FILE__, and _context_file_display in src/specify_cli, and have the extension tests copy the bundled defaults JSON so self-seed runs without the CLI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: reflect full removal of agent-context state from the CLI Update AGENTS.md (integration examples, required-fields table, context behavior section, pitfalls), CHANGELOG, and the SDD spec artifacts (FR-007, SC-002, data-model) to state that the CLI carries no context_file and the extension fully owns the per-agent default mapping via agent-context-defaults.json. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: align SDD artifacts with full context_file removal Update research.md (R1, R2, R4, summary table), contracts/cli-behavior.md (C3, C5), tasks.md (Phase 2, T026, notes), plan.md (Principle I, source map), and checklists/requirements.md so the spec artifacts reflect the implemented decision: the CLI carries no context_file attribute or __CONTEXT_FILE__ resolution, and the per-agent defaults map lives in the extension. Resolves PR review #4548130110. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: scrub stale context-file mentions from CLI docstrings Update the multi_install_safe docstring (drop the removed "context file" invariant), the RovoDev setup docstring (no longer upserts a context section), the Copilot module docstring (drop the context-file line), and tighten the _update_init_options_for_integration note. Pure docstring changes — no behavioral impact. Resolves PR review #4548237085. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test+docs: harden agent-context test helper and fix stale docs - base.py: document multi_install_safe as an optional subclass attribute in the IntegrationBase docstring. - test_cli.py: clarify the init-options assertion is guarding against leftover legacy agent-context keys, not relocation. - test_extension_agent_context.py: _install_agent_context_config now asserts the bundled agent-context-defaults.json exists and always copies it, so self-seeding tests fail loudly instead of silently skipping when the map is missing. - test_integration_cursor_agent.py: drop Path/IntegrationManifest imports left unused after removing the context-section frontmatter tests. Resolves PR review #4548293116. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: remove gitignored SDD artifacts from specs/ The specs/001-agent-context-full-optin/ artifacts were force-added for dogfooding visibility, but specs/ is gitignored and these were always intended to be purged before merge. Remove them so merging does not add an intentionally-untracked directory to repo history. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: keep CHANGELOG.md identical to upstream CHANGELOG.md is auto-generated at release time, so the branch should not carry a manual entry. Restore it to match upstream/main exactly. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: preserve Cursor .mdc frontmatter in agent-context updater scripts The bundled agent-context updater scripts wrote the managed section as plain text. For Cursor-style `.mdc` targets this dropped the required `---\nalwaysApply: true\n---` frontmatter, reintroducing the rule-loading bug originally fixed in #1699. Port the `_ensure_mdc_frontmatter` logic into both the bash and PowerShell updaters: prepend frontmatter when missing, repair `alwaysApply` when set to the wrong value, and leave non-`.mdc` targets untouched. Add regression tests covering both shells. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: scope CLI-free guard to agent-context-specific symbols Drop the bare "context_file" substring from FORBIDDEN_SYMBOLS so the guard no longer fails on unrelated future CLI fields named context_file. The list still covers agent-context-specific identifiers (__CONTEXT_FILE__, _context_file_display, _resolve_context_files, _resolve_context_file_values). Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: harden agent-context bash self-seed against malformed init JSON Two robustness fixes in the embedded Python self-seed logic: - Coerce the integration value from init-options.json to a string only when it is actually a string; otherwise treat it as unset so a corrupted dict/list value degrades to the existing nothing-to-do behavior instead of breaking the agents-map lookup. - Normalize agent-context-defaults.json: only use 'agents' when both the JSON root and the 'agents' value are dicts, so a wrong-shaped (but valid) JSON falls back to the warning path instead of raising on .get. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: correct PowerShell hyphenated key lookup and regex replace count - Self-seed now reads the defaults mapping via $defaults.agents.PSObject.Properties[$integrationKey].Value instead of member access ($defaults.agents.$integrationKey), which parsed hyphenated keys like 'cursor-agent'/'kiro-cli' as subtraction and failed to resolve. - Replace the static [regex]::Replace(..., 1) call, whose trailing 1 was interpreted as RegexOptions.IgnoreCase rather than a replacement count, with an instance Regex whose Replace(input, replacement, 1) limits to the first match as intended. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: make bash .mdc frontmatter guard case-insensitive The bash updater only injected Cursor .mdc frontmatter when ctx_path ended in lowercase '.mdc', so a mixed/upper-case extension (e.g. specify-rules.MDC) was skipped and Cursor would not auto-load the rule file. Compare against the casefolded path. The PowerShell variant already uses -match, which is case-insensitive by default, so no change is needed there. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: document separator-agnostic agent-context update invocation The README hard-coded the dot-notation slash command (/speckit.agent-context.update), which hyphen-separator agents like Forge and Cline do not recognize. Document the canonical command ID plus both slash invocations so users copy the form their agent accepts. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
430 lines
14 KiB
Bash
Executable File
430 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# update-agent-context.sh
|
|
#
|
|
# 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_files` or `context_file`, plus `context_markers.{start,end}`, from the
|
|
# agent-context extension config:
|
|
# .specify/extensions/agent-context/agent-context-config.yml
|
|
#
|
|
# Usage: update-agent-context.sh [plan_path]
|
|
#
|
|
# When `plan_path` is omitted, the script derives it from `.specify/feature.json`
|
|
# (written by /speckit-specify). Falls back to the most recently modified
|
|
# `specs/*/plan.md` only when feature.json is absent or its plan does not exist yet.
|
|
|
|
set -euo pipefail
|
|
|
|
PROJECT_ROOT="$(pwd)"
|
|
EXT_CONFIG="$PROJECT_ROOT/.specify/extensions/agent-context/agent-context-config.yml"
|
|
DEFAULT_START="<!-- SPECKIT START -->"
|
|
DEFAULT_END="<!-- SPECKIT END -->"
|
|
|
|
if [[ ! -f "$EXT_CONFIG" ]]; then
|
|
echo "agent-context: $EXT_CONFIG not found; nothing to do." >&2
|
|
exit 0
|
|
fi
|
|
|
|
# Locate a Python 3 interpreter with PyYAML available.
|
|
_python=""
|
|
_python_candidates=()
|
|
[[ -n "${SPECKIT_PYTHON:-}" ]] && _python_candidates+=("$SPECKIT_PYTHON")
|
|
_python_candidates+=("python3" "python")
|
|
for _candidate in "${_python_candidates[@]}"; do
|
|
if command -v "$_candidate" >/dev/null 2>&1 \
|
|
&& "$_candidate" - <<'PY' >/dev/null 2>&1
|
|
import sys
|
|
try:
|
|
import yaml # noqa: F401
|
|
except ImportError:
|
|
sys.exit(1)
|
|
sys.exit(0 if sys.version_info[0] == 3 else 1)
|
|
PY
|
|
then
|
|
_python="$_candidate"
|
|
break
|
|
fi
|
|
done
|
|
unset _candidate _python_candidates
|
|
|
|
if [[ -z "$_python" ]]; then
|
|
echo "agent-context: Python 3 with PyYAML not found on PATH; skipping update." >&2
|
|
echo " To resolve: pip install pyyaml (or install it into the environment used by python3)." >&2
|
|
exit 0
|
|
fi
|
|
_case_insensitive_context_files=0
|
|
case "$(uname -s 2>/dev/null || true)" in
|
|
MINGW*|MSYS*|CYGWIN*) _case_insensitive_context_files=1 ;;
|
|
esac
|
|
|
|
# Parse extension config once; emit context files as JSON, followed by marker strings.
|
|
if ! _raw_opts="$("$_python" - "$EXT_CONFIG" "$_case_insensitive_context_files" "$PROJECT_ROOT" <<'PY'
|
|
import json
|
|
import sys
|
|
try:
|
|
import yaml
|
|
except ImportError:
|
|
print(
|
|
"agent-context: PyYAML is required to parse extension config but is not available "
|
|
"in the current Python environment.\n"
|
|
" To resolve: pip install pyyaml (or install it into the environment used by python3).\n"
|
|
" Context file will not be updated until PyYAML is importable.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(2)
|
|
try:
|
|
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
data = yaml.safe_load(fh)
|
|
except Exception as exc:
|
|
print(
|
|
f"agent-context: unable to parse {sys.argv[1]} ({exc}); cannot update context.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(2)
|
|
if not isinstance(data, dict):
|
|
data = {}
|
|
def get_str(obj, *keys):
|
|
node = obj
|
|
for k in keys:
|
|
if isinstance(node, dict) and k in node:
|
|
node = node[k]
|
|
else:
|
|
return ""
|
|
return node if isinstance(node, str) else ""
|
|
context_files = []
|
|
seen_context_files = set()
|
|
case_insensitive = sys.argv[2] == "1" or sys.platform.startswith(("win32", "cygwin"))
|
|
def add_context_file(value):
|
|
if not isinstance(value, str):
|
|
return
|
|
candidate = value.strip()
|
|
if not candidate:
|
|
return
|
|
key = candidate.casefold() if case_insensitive else candidate
|
|
if key in seen_context_files:
|
|
return
|
|
context_files.append(candidate)
|
|
seen_context_files.add(key)
|
|
raw_files = data.get("context_files")
|
|
if isinstance(raw_files, list):
|
|
for value in raw_files:
|
|
add_context_file(value)
|
|
if not context_files:
|
|
add_context_file(get_str(data, "context_file"))
|
|
if not context_files:
|
|
# Self-seed: the agent-context extension owns its lifecycle, so when its
|
|
# own config declares no target it derives one from the active integration
|
|
# recorded in init-options.json, using the extension's OWN bundled mapping
|
|
# (agent-context-defaults.json). This is independent of the Specify CLI by
|
|
# design — nothing here imports specify_cli.
|
|
project_root = sys.argv[3] if len(sys.argv) > 3 else "."
|
|
integration_key = ""
|
|
try:
|
|
with open(
|
|
f"{project_root}/.specify/init-options.json", "r", encoding="utf-8"
|
|
) as fh:
|
|
opts = json.load(fh)
|
|
if isinstance(opts, dict):
|
|
value = opts.get("integration") or opts.get("ai") or ""
|
|
integration_key = value if isinstance(value, str) else ""
|
|
except Exception:
|
|
integration_key = ""
|
|
if integration_key:
|
|
defaults_path = (
|
|
f"{project_root}/.specify/extensions/agent-context/"
|
|
"agent-context-defaults.json"
|
|
)
|
|
mapping = {}
|
|
try:
|
|
with open(defaults_path, "r", encoding="utf-8") as fh:
|
|
loaded = json.load(fh)
|
|
agents = loaded.get("agents", {}) if isinstance(loaded, dict) else {}
|
|
mapping = agents if isinstance(agents, dict) else {}
|
|
except Exception:
|
|
print(
|
|
"agent-context: unable to read %s; cannot self-seed the context "
|
|
"file. Set 'context_file' in the extension config." % defaults_path,
|
|
file=sys.stderr,
|
|
)
|
|
mapping = {}
|
|
add_context_file(mapping.get(integration_key, "") or "")
|
|
if not context_files:
|
|
print(
|
|
"agent-context: no default context file is known for integration "
|
|
"'%s'. Set 'context_file' in the extension config to choose one."
|
|
% integration_key,
|
|
file=sys.stderr,
|
|
)
|
|
print(json.dumps(context_files))
|
|
print(get_str(data, "context_markers", "start"))
|
|
print(get_str(data, "context_markers", "end"))
|
|
PY
|
|
)"; then
|
|
echo "agent-context: skipping update (see above for details)." >&2
|
|
exit 0
|
|
fi
|
|
|
|
_opts_lines=()
|
|
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_files, marker_start, marker_end), got ${#_opts_lines[@]}; skipping update." >&2
|
|
exit 0
|
|
fi
|
|
CONTEXT_FILES_JSON="${_opts_lines[0]}"
|
|
MARKER_START="${_opts_lines[1]}"
|
|
MARKER_END="${_opts_lines[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
|
|
|
|
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
|
|
|
|
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
|
|
if ! "$_python" - "$PROJECT_ROOT" "$CONTEXT_FILE" <<'PY'
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
root = Path(sys.argv[1]).resolve()
|
|
target = (root / sys.argv[2]).resolve(strict=False)
|
|
try:
|
|
target.relative_to(root)
|
|
except ValueError:
|
|
sys.exit(1)
|
|
PY
|
|
then
|
|
echo "agent-context: context file path resolves outside the project root; got '$CONTEXT_FILE'." >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
unset _cf_parts _seg
|
|
|
|
[[ -z "$MARKER_START" ]] && MARKER_START="$DEFAULT_START"
|
|
[[ -z "$MARKER_END" ]] && MARKER_END="$DEFAULT_END"
|
|
|
|
PLAN_PATH="${1:-}"
|
|
if [[ -z "$PLAN_PATH" ]]; then
|
|
# Prefer .specify/feature.json (written by /speckit-specify) over mtime heuristic.
|
|
_feature_json="$PROJECT_ROOT/.specify/feature.json"
|
|
if [[ -f "$_feature_json" ]]; then
|
|
_feature_dir="$("$_python" - "$_feature_json" <<'PY'
|
|
import sys, json
|
|
try:
|
|
with open(sys.argv[1], encoding="utf-8") as fh:
|
|
d = json.load(fh)
|
|
val = d.get("feature_directory", "")
|
|
print(val if isinstance(val, str) else "")
|
|
except Exception:
|
|
print("")
|
|
PY
|
|
)"
|
|
# Normalize backslashes (written by PS on Windows) to forward slashes before path ops.
|
|
_feature_dir="$(printf '%s' "$_feature_dir" | tr '\\' '/')"
|
|
_feature_dir="${_feature_dir%/}"
|
|
if [[ -n "$_feature_dir" ]]; then
|
|
# feature_directory may be relative or absolute (absolute paths outside PROJECT_ROOT
|
|
# are preserved as-is by _persist_feature_json in common.sh).
|
|
# Also match drive-qualified paths (C:/...) written by PowerShell on Windows.
|
|
if [[ "$_feature_dir" == /* ]] || [[ "$_feature_dir" =~ ^[A-Za-z]:/ ]]; then
|
|
_candidate="$_feature_dir/plan.md"
|
|
else
|
|
_candidate="$PROJECT_ROOT/$_feature_dir/plan.md"
|
|
fi
|
|
if [[ -f "$_candidate" ]]; then
|
|
# Resolve symlinks before comparing so paths like /var/… vs /private/var/…
|
|
# (macOS) are treated as equivalent. Mirrors the mtime-fallback approach.
|
|
PLAN_PATH="$("$_python" - "$PROJECT_ROOT" "$_candidate" <<'PY'
|
|
import sys
|
|
from pathlib import Path
|
|
root = Path(sys.argv[1]).resolve()
|
|
cand = Path(sys.argv[2]).resolve()
|
|
try:
|
|
print(cand.relative_to(root).as_posix())
|
|
except ValueError:
|
|
# Outside project root: emit the resolved path in POSIX form.
|
|
# as_posix() converts backslashes correctly on native Windows Python.
|
|
print(cand.as_posix())
|
|
PY
|
|
)"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Fall back to mtime only when feature.json is absent or its plan does not exist yet.
|
|
# Python emits a project-relative POSIX path directly to avoid bash prefix-strip
|
|
# issues with backslash paths on Windows (Git bash / MSYS2).
|
|
if [[ -z "$PLAN_PATH" ]]; then
|
|
_plan_rel="$("$_python" - "$PROJECT_ROOT" <<'PY'
|
|
import sys
|
|
from pathlib import Path
|
|
root = Path(sys.argv[1]).resolve()
|
|
specs = root / "specs"
|
|
plans = sorted(
|
|
specs.glob("*/plan.md"),
|
|
key=lambda p: p.stat().st_mtime,
|
|
reverse=True,
|
|
)
|
|
if plans:
|
|
try:
|
|
print(plans[0].relative_to(root).as_posix())
|
|
except ValueError:
|
|
print("")
|
|
else:
|
|
print("")
|
|
PY
|
|
)"
|
|
if [[ -n "$_plan_rel" ]]; then
|
|
PLAN_PATH="$_plan_rel"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Build the managed section
|
|
TMP_SECTION="$(mktemp)"
|
|
trap 'rm -f "$TMP_SECTION"' EXIT
|
|
{
|
|
echo "$MARKER_START"
|
|
echo "For additional context about technologies to be used, project structure,"
|
|
echo "shell commands, and other important information, read the current plan"
|
|
if [[ -n "$PLAN_PATH" ]]; then
|
|
echo "at $PLAN_PATH"
|
|
fi
|
|
echo "$MARKER_END"
|
|
} > "$TMP_SECTION"
|
|
|
|
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'
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
ctx_path, start, end, section_path = sys.argv[1:5]
|
|
with open(section_path, "r", encoding="utf-8") as fh:
|
|
section = fh.read().rstrip("\n") + "\n"
|
|
|
|
|
|
def ensure_mdc_frontmatter(content):
|
|
"""Ensure ``.mdc`` content has YAML frontmatter with ``alwaysApply: true``.
|
|
|
|
Cursor only auto-loads ``.mdc`` rule files that carry frontmatter with
|
|
``alwaysApply: true``. Prepend it when missing, or repair the value while
|
|
preserving any existing frontmatter comments/formatting.
|
|
"""
|
|
leading_ws = len(content) - len(content.lstrip())
|
|
leading = content[:leading_ws]
|
|
stripped = content[leading_ws:]
|
|
|
|
if not stripped.startswith("---"):
|
|
return "---\nalwaysApply: true\n---\n\n" + content
|
|
|
|
match = re.match(
|
|
r"^(---[ \t]*\r?\n)(.*?)(\r?\n---[ \t]*)(\r?\n|$)(.*)",
|
|
stripped,
|
|
re.DOTALL,
|
|
)
|
|
if not match:
|
|
return "---\nalwaysApply: true\n---\n\n" + content
|
|
|
|
opening, fm_text, closing, sep, rest = match.groups()
|
|
newline = "\r\n" if "\r\n" in opening else "\n"
|
|
|
|
if re.search(r"(?m)^[ \t]*alwaysApply[ \t]*:[ \t]*true[ \t]*(?:#.*)?$", fm_text):
|
|
return content
|
|
|
|
if re.search(r"(?m)^[ \t]*alwaysApply[ \t]*:", fm_text):
|
|
fm_text = re.sub(
|
|
r"(?m)^([ \t]*)alwaysApply[ \t]*:.*?([ \t]*(?:#.*)?)$",
|
|
r"\1alwaysApply: true\2",
|
|
fm_text,
|
|
count=1,
|
|
)
|
|
elif fm_text.strip():
|
|
fm_text = fm_text + newline + "alwaysApply: true"
|
|
else:
|
|
fm_text = "alwaysApply: true"
|
|
|
|
return f"{leading}{opening}{fm_text}{closing}{sep}{rest}"
|
|
|
|
|
|
if os.path.exists(ctx_path):
|
|
with open(ctx_path, "r", encoding="utf-8-sig") as fh:
|
|
content = fh.read()
|
|
s = content.find(start)
|
|
e = content.find(end, s if s != -1 else 0)
|
|
if s != -1 and e != -1 and e > s:
|
|
end_of_marker = e + len(end)
|
|
if end_of_marker < len(content) and content[end_of_marker] == "\r":
|
|
end_of_marker += 1
|
|
if end_of_marker < len(content) and content[end_of_marker] == "\n":
|
|
end_of_marker += 1
|
|
new_content = content[:s] + section + content[end_of_marker:]
|
|
elif s != -1:
|
|
new_content = content[:s] + section
|
|
elif e != -1:
|
|
end_of_marker = e + len(end)
|
|
if end_of_marker < len(content) and content[end_of_marker] == "\r":
|
|
end_of_marker += 1
|
|
if end_of_marker < len(content) and content[end_of_marker] == "\n":
|
|
end_of_marker += 1
|
|
new_content = section + content[end_of_marker:]
|
|
else:
|
|
if content and not content.endswith("\n"):
|
|
content += "\n"
|
|
new_content = (content + "\n" + section) if content else section
|
|
else:
|
|
new_content = section
|
|
|
|
new_content = new_content.replace("\r\n", "\n").replace("\r", "\n")
|
|
if ctx_path.casefold().endswith(".mdc"):
|
|
new_content = ensure_mdc_frontmatter(new_content)
|
|
with open(ctx_path, "wb") as fh:
|
|
fh.write(new_content.encode("utf-8"))
|
|
PY
|
|
|
|
echo "agent-context: updated $CONTEXT_FILE"
|
|
done
|