Skip to content

Commit 8512915

Browse files
authored
fix: address review comments on agent-context extension
- bash: parse init-options.json with a single python3 invocation instead of three separate read_json_field calls, for parity with the PowerShell ConvertFrom-Json approach and to avoid divergent error semantics - bash: use parameter expansion to strip PROJECT_ROOT prefix from plan path instead of sed interpolation, avoiding special-character fragility - powershell: limit Get-ChildItem to -Depth 1 so plan.md discovery matches the bash glob specs/*/plan.md (one level deep) — fixes cross-platform inconsistency with nested plan.md files - powershell: replace Substring+Length relative-path with [System.IO.Path]::GetRelativePath for robustness across case/PSDrive differences - __init__.py: move agent-context extension install to after save_init_options so init-options.json is present when hooks run - __init__.py: seed context_markers in init-options only when context_file is truthy; avoids noise for integrations without a context file - integrations/base.py: narrow blanket except Exception in _resolve_context_markers to ImportError / (OSError, ValueError) so unexpected bugs surface instead of being silently swallowed
1 parent 8e0d40e commit 8512915

4 files changed

Lines changed: 67 additions & 58 deletions

File tree

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

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,45 +26,49 @@ if [[ ! -f "$INIT_OPTIONS" ]]; then
2626
exit 0
2727
fi
2828

29-
# Use python for JSON parsing (always available in spec-kit projects).
30-
read_json_field() {
31-
# $1 = jq-style dotted path, e.g. "context_markers.start"
32-
python3 - "$INIT_OPTIONS" "$1" <<'PY'
29+
# Parse init-options.json once; emit three newline-separated fields:
30+
# context_file, context_markers.start, context_markers.end
31+
_raw_opts="$(python3 - "$INIT_OPTIONS" <<'PY'
3332
import json, sys
34-
path = sys.argv[1]
35-
key = sys.argv[2]
3633
try:
37-
with open(path, "r", encoding="utf-8") as fh:
34+
with open(sys.argv[1], "r", encoding="utf-8") as fh:
3835
data = json.load(fh)
3936
except Exception:
40-
sys.exit(0)
41-
node = data
42-
for part in key.split("."):
43-
if isinstance(node, dict) and part in node:
44-
node = node[part]
45-
else:
46-
sys.exit(0)
47-
if isinstance(node, str):
48-
sys.stdout.write(node)
37+
data = {}
38+
def get_str(obj, *keys):
39+
node = obj
40+
for k in keys:
41+
if isinstance(node, dict) and k in node:
42+
node = node[k]
43+
else:
44+
return ""
45+
return node if isinstance(node, str) else ""
46+
print(get_str(data, "context_file"))
47+
print(get_str(data, "context_markers", "start"))
48+
print(get_str(data, "context_markers", "end"))
4949
PY
50-
}
50+
)"
51+
52+
{
53+
IFS= read -r CONTEXT_FILE
54+
IFS= read -r MARKER_START
55+
IFS= read -r MARKER_END
56+
} <<< "$_raw_opts"
5157

52-
CONTEXT_FILE="$(read_json_field 'context_file' || true)"
5358
if [[ -z "$CONTEXT_FILE" ]]; then
5459
echo "agent-context: context_file not set in init-options.json; nothing to do." >&2
5560
exit 0
5661
fi
5762

58-
MARKER_START="$(read_json_field 'context_markers.start' || true)"
59-
MARKER_END="$(read_json_field 'context_markers.end' || true)"
6063
[[ -z "$MARKER_START" ]] && MARKER_START="$DEFAULT_START"
6164
[[ -z "$MARKER_END" ]] && MARKER_END="$DEFAULT_END"
6265

6366
PLAN_PATH="${1:-}"
6467
if [[ -z "$PLAN_PATH" ]]; then
6568
if compgen -G "$PROJECT_ROOT/specs/*/plan.md" > /dev/null; then
66-
# Pick the most recently modified plan.md
67-
PLAN_PATH="$(ls -1t "$PROJECT_ROOT"/specs/*/plan.md 2>/dev/null | head -1 | sed "s|$PROJECT_ROOT/||")"
69+
# Pick the most recently modified plan.md (one level deep: specs/<feature>/plan.md)
70+
_plan_abs="$(ls -1t "$PROJECT_ROOT"/specs/*/plan.md 2>/dev/null | head -1)"
71+
PLAN_PATH="${_plan_abs#"$PROJECT_ROOT/"}"
6872
fi
6973
fi
7074

extensions/agent-context/scripts/powershell/update-agent-context.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ if ($Options.context_markers) {
5252
}
5353

5454
if (-not $PlanPath) {
55-
$candidate = Get-ChildItem -Path (Join-Path $ProjectRoot 'specs') -Filter 'plan.md' -Recurse -ErrorAction SilentlyContinue |
55+
$candidate = Get-ChildItem -Path (Join-Path $ProjectRoot 'specs') -Filter 'plan.md' -Recurse -Depth 1 -ErrorAction SilentlyContinue |
5656
Sort-Object LastWriteTime -Descending |
5757
Select-Object -First 1
5858
if ($candidate) {
59-
$PlanPath = $candidate.FullName.Substring($ProjectRoot.Length).TrimStart('/', '\').Replace('\','/')
59+
$PlanPath = [System.IO.Path]::GetRelativePath($ProjectRoot, $candidate.FullName).Replace('\','/')
6060
}
6161
}
6262

src/specify_cli/__init__.py

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -870,31 +870,6 @@ def init(
870870
else:
871871
tracker.skip("git", "--no-git flag")
872872

873-
# Install bundled agent-context extension (opt-out via
874-
# `specify extension disable agent-context`). Owns the managed
875-
# section in coding agent context files (CLAUDE.md, etc.).
876-
tracker.start("agent-context")
877-
try:
878-
from .extensions import ExtensionManager as _AgentCtxMgr
879-
bundled_ac = _locate_bundled_extension("agent-context")
880-
if bundled_ac:
881-
ac_manager = _AgentCtxMgr(project_path)
882-
if ac_manager.registry.is_installed("agent-context"):
883-
tracker.complete("agent-context", "already installed")
884-
else:
885-
ac_manager.install_from_directory(
886-
bundled_ac, get_speckit_version()
887-
)
888-
tracker.complete("agent-context", "installed")
889-
else:
890-
tracker.skip("agent-context", "bundled extension not found")
891-
except Exception as ac_err:
892-
sanitized_ac = str(ac_err).replace('\n', ' ').strip()
893-
tracker.error(
894-
"agent-context",
895-
f"install failed: {sanitized_ac[:120]}",
896-
)
897-
898873
# Install bundled speckit workflow
899874
try:
900875
bundled_wf = _locate_bundled_workflow("speckit")
@@ -926,26 +901,26 @@ def init(
926901
sanitized_wf = str(wf_err).replace('\n', ' ').strip()
927902
tracker.error("workflow", f"install failed: {sanitized_wf[:120]}")
928903

929-
# Fix permissions after all installs (scripts + extensions)
930-
ensure_executable_scripts(project_path, tracker=tracker)
931-
932904
# Persist the CLI options so later operations (e.g. preset add)
933905
# can adapt their behaviour without re-scanning the filesystem.
934906
# Must be saved BEFORE preset install so _get_skills_dir() works.
907+
# Also saved BEFORE agent-context install so init-options.json is
908+
# available when the extension's hooks run.
935909
from .integrations.base import IntegrationBase
936910
init_opts = {
937911
"ai": selected_ai,
938912
"integration": resolved_integration.key,
939913
"branch_numbering": branch_numbering or "sequential",
940914
"context_file": resolved_integration.context_file,
941-
"context_markers": {
942-
"start": IntegrationBase.CONTEXT_MARKER_START,
943-
"end": IntegrationBase.CONTEXT_MARKER_END,
944-
},
945915
"here": here,
946916
"script": selected_script,
947917
"speckit_version": get_speckit_version(),
948918
}
919+
if resolved_integration.context_file:
920+
init_opts["context_markers"] = {
921+
"start": IntegrationBase.CONTEXT_MARKER_START,
922+
"end": IntegrationBase.CONTEXT_MARKER_END,
923+
}
949924
# Ensure ai_skills is set for SkillsIntegration so downstream
950925
# tools (extensions, presets) emit SKILL.md overrides correctly.
951926
# Also set for integrations running in skills mode (e.g. Copilot
@@ -955,6 +930,36 @@ def init(
955930
init_opts["ai_skills"] = True
956931
save_init_options(project_path, init_opts)
957932

933+
# Install bundled agent-context extension (opt-out via
934+
# `specify extension disable agent-context`). Owns the managed
935+
# section in coding agent context files (CLAUDE.md, etc.).
936+
# Installed AFTER init-options are saved so hooks can read
937+
# context_file / context_markers from init-options.json.
938+
tracker.start("agent-context")
939+
try:
940+
from .extensions import ExtensionManager as _AgentCtxMgr
941+
bundled_ac = _locate_bundled_extension("agent-context")
942+
if bundled_ac:
943+
ac_manager = _AgentCtxMgr(project_path)
944+
if ac_manager.registry.is_installed("agent-context"):
945+
tracker.complete("agent-context", "already installed")
946+
else:
947+
ac_manager.install_from_directory(
948+
bundled_ac, get_speckit_version()
949+
)
950+
tracker.complete("agent-context", "installed")
951+
else:
952+
tracker.skip("agent-context", "bundled extension not found")
953+
except Exception as ac_err:
954+
sanitized_ac = str(ac_err).replace('\n', ' ').strip()
955+
tracker.error(
956+
"agent-context",
957+
f"install failed: {sanitized_ac[:120]}",
958+
)
959+
960+
# Fix permissions after all installs (scripts + extensions)
961+
ensure_executable_scripts(project_path, tracker=tracker)
962+
958963
# Install preset if specified
959964
if preset:
960965
try:

src/specify_cli/integrations/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,11 +533,11 @@ def _resolve_context_markers(self, project_root: Path) -> tuple[str, str]:
533533
end = self.CONTEXT_MARKER_END
534534
try:
535535
from .. import load_init_options # local import to avoid cycles
536-
except Exception:
536+
except ImportError:
537537
return start, end
538538
try:
539539
opts = load_init_options(project_root)
540-
except Exception:
540+
except (OSError, ValueError):
541541
return start, end
542542
markers = opts.get("context_markers") if isinstance(opts, dict) else None
543543
if isinstance(markers, dict):

0 commit comments

Comments
 (0)