mindsystem-cc 3.17.1 → 3.18.0

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.
@@ -0,0 +1,179 @@
1
+ #!/bin/bash
2
+ #
3
+ # gather-milestone-stats.sh
4
+ # Gathers milestone readiness status and statistics from the filesystem
5
+ # and git history. Outputs structured text for the LLM to present.
6
+ #
7
+ # Usage: ./scripts/gather-milestone-stats.sh <start_phase> <end_phase>
8
+ # Example: ./scripts/gather-milestone-stats.sh 1 6
9
+
10
+ set -e
11
+
12
+ # --- Validation ---
13
+ if [ -z "$1" ] || [ -z "$2" ]; then
14
+ echo "Error: Two arguments required"
15
+ echo "Usage: $0 <start_phase> <end_phase>"
16
+ exit 1
17
+ fi
18
+
19
+ START="$1"
20
+ END="$2"
21
+
22
+ if ! [[ "$START" =~ ^[0-9]+$ ]] || ! [[ "$END" =~ ^[0-9]+$ ]]; then
23
+ echo "Error: Both arguments must be numeric"
24
+ echo "Usage: $0 <start_phase> <end_phase>"
25
+ exit 1
26
+ fi
27
+
28
+ if [ "$START" -gt "$END" ]; then
29
+ echo "Error: Start phase ($START) cannot exceed end phase ($END)"
30
+ exit 1
31
+ fi
32
+
33
+ # --- Find .planning from git root ---
34
+ GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
35
+ if [ -z "$GIT_ROOT" ]; then
36
+ echo "Error: Not in a git repository"
37
+ exit 1
38
+ fi
39
+
40
+ PHASES_DIR="$GIT_ROOT/.planning/phases"
41
+ if [ ! -d "$PHASES_DIR" ]; then
42
+ echo "Error: Phases directory not found at $PHASES_DIR"
43
+ exit 1
44
+ fi
45
+
46
+ # --- Helper: check if phase number is in range (supports decimals like 02.1) ---
47
+ in_range() {
48
+ local phase_num="$1"
49
+ echo "$phase_num" | awk -v s="$START" -v e="$END" '{
50
+ val = $1 + 0
51
+ if (val >= s && val <= e + 0.999) exit 0
52
+ else exit 1
53
+ }'
54
+ }
55
+
56
+ # ============================================================
57
+ # READINESS
58
+ # ============================================================
59
+ echo "=== Readiness ==="
60
+ echo ""
61
+
62
+ PHASE_COUNT=0
63
+ PLAN_COUNT=0
64
+ COMPLETE=0
65
+ INCOMPLETE_LIST=""
66
+ PHASE_DETAILS=""
67
+
68
+ for dir in "$PHASES_DIR"/*/; do
69
+ [ -d "$dir" ] || continue
70
+ dirname=$(basename "$dir")
71
+ phase_num="${dirname%%-*}"
72
+ phase_name="${dirname#*-}"
73
+
74
+ if in_range "$phase_num"; then
75
+ PHASE_COUNT=$((PHASE_COUNT + 1))
76
+ phase_plans=0
77
+ phase_complete=0
78
+
79
+ for plan in "$dir"/*-PLAN.md; do
80
+ [ -f "$plan" ] || continue
81
+ PLAN_COUNT=$((PLAN_COUNT + 1))
82
+ phase_plans=$((phase_plans + 1))
83
+ plan_base=$(basename "$plan" -PLAN.md)
84
+ summary="$dir/${plan_base}-SUMMARY.md"
85
+ if [ -f "$summary" ]; then
86
+ COMPLETE=$((COMPLETE + 1))
87
+ phase_complete=$((phase_complete + 1))
88
+ else
89
+ INCOMPLETE_LIST+=" $(basename "$dir")/$(basename "$plan")"$'\n'
90
+ fi
91
+ done
92
+
93
+ PHASE_DETAILS+="- Phase $phase_num: $phase_name ($phase_complete/$phase_plans plans)"$'\n'
94
+ fi
95
+ done
96
+
97
+ echo "Phases: $PHASE_COUNT (range $START-$END)"
98
+ echo "Plans: $PLAN_COUNT total, $COMPLETE complete"
99
+ echo ""
100
+ echo "$PHASE_DETAILS"
101
+
102
+ if [ "$COMPLETE" -eq "$PLAN_COUNT" ] && [ "$PLAN_COUNT" -gt 0 ]; then
103
+ echo "Status: READY"
104
+ else
105
+ INCOMPLETE=$((PLAN_COUNT - COMPLETE))
106
+ echo "Incomplete ($INCOMPLETE):"
107
+ echo "$INCOMPLETE_LIST"
108
+ echo "Status: NOT READY"
109
+ fi
110
+
111
+ # ============================================================
112
+ # GIT STATS
113
+ # ============================================================
114
+ echo ""
115
+ echo "=== Git Stats ==="
116
+ echo ""
117
+
118
+ # Collect commits matching Mindsystem phase convention: feat(XX-YY), fix(XX-YY), etc.
119
+ ALL_COMMITS=""
120
+ for i in $(seq "$START" "$END"); do
121
+ phase=$(printf "%02d" "$i")
122
+ commits=$(git log --all --format="%H %ai %s" --grep="($phase-" 2>/dev/null || true)
123
+ if [ -n "$commits" ]; then
124
+ ALL_COMMITS+="$commits"$'\n'
125
+ fi
126
+ done
127
+
128
+ # Also capture decimal phase commits (e.g., 02.1 inserted phases)
129
+ for dir in "$PHASES_DIR"/*/; do
130
+ [ -d "$dir" ] || continue
131
+ dirname=$(basename "$dir")
132
+ phase_num="${dirname%%-*}"
133
+ case "$phase_num" in
134
+ *.*) # Decimal phase — not captured by seq
135
+ if in_range "$phase_num"; then
136
+ commits=$(git log --all --format="%H %ai %s" --grep="($phase_num-" 2>/dev/null || true)
137
+ if [ -n "$commits" ]; then
138
+ ALL_COMMITS+="$commits"$'\n'
139
+ fi
140
+ fi
141
+ ;;
142
+ esac
143
+ done
144
+
145
+ # Remove empty lines, deduplicate, sort by date
146
+ ALL_COMMITS=$(echo "$ALL_COMMITS" | grep -v '^$' | sort -u -k2,3)
147
+
148
+ if [ -n "$ALL_COMMITS" ]; then
149
+ COMMIT_COUNT=$(echo "$ALL_COMMITS" | wc -l | tr -d ' ')
150
+ FIRST_LINE=$(echo "$ALL_COMMITS" | head -1)
151
+ LAST_LINE=$(echo "$ALL_COMMITS" | tail -1)
152
+ FIRST_HASH=$(echo "$FIRST_LINE" | awk '{print $1}')
153
+ LAST_HASH=$(echo "$LAST_LINE" | awk '{print $1}')
154
+ FIRST_DATE=$(echo "$FIRST_LINE" | awk '{print $2}')
155
+ LAST_DATE=$(echo "$LAST_LINE" | awk '{print $2}')
156
+ FIRST_MSG=$(echo "$FIRST_LINE" | cut -d' ' -f4-)
157
+ LAST_MSG=$(echo "$LAST_LINE" | cut -d' ' -f4-)
158
+
159
+ # Calculate days
160
+ DAYS=$(python3 -c "from datetime import date; print((date.fromisoformat('$LAST_DATE') - date.fromisoformat('$FIRST_DATE')).days)" 2>/dev/null || echo "?")
161
+
162
+ echo "Commits: $COMMIT_COUNT"
163
+ echo "Git range: ${FIRST_HASH:0:7}..${LAST_HASH:0:7}"
164
+ echo "First: $FIRST_DATE — $FIRST_MSG"
165
+ echo "Last: $LAST_DATE — $LAST_MSG"
166
+ echo "Timeline: $DAYS days ($FIRST_DATE → $LAST_DATE)"
167
+
168
+ # Diff stats for the range
169
+ DIFFSTAT=$(git diff --shortstat "${FIRST_HASH}^..${LAST_HASH}" 2>/dev/null || true)
170
+ if [ -n "$DIFFSTAT" ]; then
171
+ echo "Changes:$DIFFSTAT"
172
+ fi
173
+ else
174
+ echo "No commits found matching phase patterns (expected 'feat(XX-YY): ...')"
175
+ echo "Determine git range manually from git log"
176
+ fi
177
+
178
+ echo ""
179
+ exit 0
@@ -25,6 +25,16 @@ class Context7Client:
25
25
  ],
26
26
  )
27
27
 
28
+ @staticmethod
29
+ def _extract_error_body(response: httpx.Response) -> str:
30
+ """Extract human-readable error message from response."""
31
+ try:
32
+ data = response.json()
33
+ # Context7 style: {"error": "code", "message": "..."}
34
+ return data.get("message", data.get("error", response.text[:200]))
35
+ except Exception:
36
+ return response.text[:200]
37
+
28
38
  def resolve_library(self, library_name: str, query: str) -> dict:
29
39
  """Resolve library name to Context7 library ID.
30
40
 
@@ -50,21 +60,22 @@ class Context7Client:
50
60
  response.raise_for_status()
51
61
  data = response.json()
52
62
  except httpx.HTTPStatusError as e:
63
+ body = self._extract_error_body(e.response)
53
64
  if e.response.status_code == 401:
54
65
  raise MsLookupError(
55
66
  ErrorCode.MISSING_API_KEY,
56
- "Invalid CONTEXT7_API_KEY",
67
+ f"Invalid CONTEXT7_API_KEY — {body}",
57
68
  suggestions=["Check your API key at https://context7.com/dashboard"],
58
69
  )
59
70
  elif e.response.status_code == 429:
60
71
  raise MsLookupError(
61
72
  ErrorCode.RATE_LIMITED,
62
- "Context7 API rate limit exceeded",
73
+ f"Context7 API rate limited — {body}",
63
74
  suggestions=["Wait a moment and try again", "Use --no-cache sparingly"],
64
75
  )
65
76
  raise MsLookupError(
66
77
  ErrorCode.API_ERROR,
67
- f"Context7 API error: {e.response.status_code}",
78
+ f"Context7 API error ({e.response.status_code}): {body}",
68
79
  )
69
80
  except httpx.RequestError as e:
70
81
  raise MsLookupError(
@@ -122,6 +133,7 @@ class Context7Client:
122
133
  # Plain text/markdown response - wrap in a structure
123
134
  data = {"content": response.text, "format": "markdown"}
124
135
  except httpx.HTTPStatusError as e:
136
+ body = self._extract_error_body(e.response)
125
137
  if e.response.status_code == 404:
126
138
  raise MsLookupError(
127
139
  ErrorCode.LIBRARY_NOT_FOUND,
@@ -130,11 +142,11 @@ class Context7Client:
130
142
  elif e.response.status_code == 429:
131
143
  raise MsLookupError(
132
144
  ErrorCode.RATE_LIMITED,
133
- "Context7 API rate limit exceeded",
145
+ f"Context7 API rate limited — {body}",
134
146
  )
135
147
  raise MsLookupError(
136
148
  ErrorCode.API_ERROR,
137
- f"Context7 API error: {e.response.status_code}",
149
+ f"Context7 API error ({e.response.status_code}): {body}",
138
150
  )
139
151
  except httpx.RequestError as e:
140
152
  raise MsLookupError(
@@ -81,21 +81,22 @@ class PerplexityClient:
81
81
  response.raise_for_status()
82
82
  data = response.json()
83
83
  except httpx.HTTPStatusError as e:
84
+ body = self._extract_error_body(e.response)
84
85
  if e.response.status_code == 401:
85
86
  raise MsLookupError(
86
87
  ErrorCode.MISSING_API_KEY,
87
- "Invalid PERPLEXITY_API_KEY",
88
+ f"Invalid PERPLEXITY_API_KEY — {body}",
88
89
  suggestions=["Check your API key at https://docs.perplexity.ai/"],
89
90
  )
90
91
  elif e.response.status_code == 429:
91
92
  raise MsLookupError(
92
93
  ErrorCode.RATE_LIMITED,
93
- "Perplexity API rate limit exceeded",
94
+ f"Perplexity API rate limited — {body}",
94
95
  suggestions=["Wait a moment and try again"],
95
96
  )
96
97
  raise MsLookupError(
97
98
  ErrorCode.API_ERROR,
98
- f"Perplexity API error: {e.response.status_code}",
99
+ f"Perplexity API error ({e.response.status_code}): {body}",
99
100
  )
100
101
  except httpx.RequestError as e:
101
102
  raise MsLookupError(
@@ -105,6 +106,19 @@ class PerplexityClient:
105
106
 
106
107
  return data
107
108
 
109
+ @staticmethod
110
+ def _extract_error_body(response: httpx.Response) -> str:
111
+ """Extract human-readable error message from response."""
112
+ try:
113
+ data = response.json()
114
+ # OpenAI-style: {"error": {"message": "..."}}
115
+ if isinstance(data.get("error"), dict):
116
+ return data["error"].get("message", str(data["error"]))
117
+ # Simple: {"error": "...", "message": "..."}
118
+ return data.get("message", data.get("error", response.text[:200]))
119
+ except Exception:
120
+ return response.text[:200]
121
+
108
122
  def _strip_think_tags(self, content: str) -> str:
109
123
  """Remove <think>...</think> blocks from response."""
110
124
  # Remove think tags and their content (including multiline)
@@ -10,7 +10,7 @@ fi
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  cd "$SCRIPT_DIR/ms-lookup"
12
12
 
13
- if command -v uv &> /dev/null; then
13
+ if command -v uv > /dev/null 2>&1; then
14
14
  # Prefer uv if available (faster)
15
15
  uv sync --quiet 2>/dev/null
16
16
  uv run python -m ms_lookup "$@"
@@ -70,13 +70,13 @@ fi
70
70
  # --- Check 3 (warning): File conflicts within waves ---
71
71
  CURRENT_WAVE=""
72
72
  WAVE_COUNT=0
73
- declare -A WAVE_FILES
73
+ CURRENT_WAVE_FILES=""
74
74
 
75
75
  while IFS= read -r line; do
76
76
  if echo "$line" | grep -qE '^## Wave [0-9]+'; then
77
77
  CURRENT_WAVE=$(echo "$line" | grep -oE '[0-9]+')
78
78
  WAVE_COUNT=$((WAVE_COUNT + 1))
79
- WAVE_FILES[$CURRENT_WAVE]=""
79
+ CURRENT_WAVE_FILES=""
80
80
  elif [ -n "$CURRENT_WAVE" ]; then
81
81
  PLAN_FILE=$(echo "$line" | grep -oE '[0-9][0-9.]*-[0-9]+-PLAN\.md' || true)
82
82
  if [ -n "$PLAN_FILE" ] && [ -f "$PHASE_DIR/$PLAN_FILE" ]; then
@@ -84,11 +84,10 @@ while IFS= read -r line; do
84
84
  FILE_PATHS=$(grep -E '^\*\*Files:\*\*' "$PHASE_DIR/$PLAN_FILE" | sed 's/\*\*Files:\*\*//g' | tr ',' '\n' | sed 's/`//g; s/^[[:space:]]*//; s/[[:space:]]*$//' | grep -v '^$' || true)
85
85
  while IFS= read -r fpath; do
86
86
  [ -z "$fpath" ] && continue
87
- EXISTING="${WAVE_FILES[$CURRENT_WAVE]}"
88
- if echo "$EXISTING" | grep -qF "|$fpath|"; then
87
+ if echo "$CURRENT_WAVE_FILES" | grep -qF "|$fpath|"; then
89
88
  echo "WARNING: File '$fpath' appears in multiple plans within Wave $CURRENT_WAVE"
90
89
  else
91
- WAVE_FILES[$CURRENT_WAVE]="${EXISTING}|$fpath|"
90
+ CURRENT_WAVE_FILES="${CURRENT_WAVE_FILES}|$fpath|"
92
91
  fi
93
92
  done <<< "$FILE_PATHS"
94
93
  fi
@@ -1,68 +0,0 @@
1
- #!/bin/bash
2
- #
3
- # cleanup-phase-artifacts.sh
4
- # Deletes raw phase artifacts (CONTEXT, DESIGN, RESEARCH, SUMMARY, UAT,
5
- # VERIFICATION, EXECUTION-ORDER) from all phases in a milestone range.
6
- # Knowledge files in .planning/knowledge/ are not touched.
7
- #
8
- # Usage: ./scripts/cleanup-phase-artifacts.sh <start_phase> <end_phase>
9
- # Example: ./scripts/cleanup-phase-artifacts.sh 1 6
10
-
11
- set -e
12
-
13
- # --- Validation ---
14
- if [ -z "$1" ] || [ -z "$2" ]; then
15
- echo "Error: Two arguments required"
16
- echo "Usage: $0 <start_phase> <end_phase>"
17
- exit 1
18
- fi
19
-
20
- START="$1"
21
- END="$2"
22
-
23
- if ! [[ "$START" =~ ^[0-9]+$ ]] || ! [[ "$END" =~ ^[0-9]+$ ]]; then
24
- echo "Error: Both arguments must be numeric"
25
- echo "Usage: $0 <start_phase> <end_phase>"
26
- exit 1
27
- fi
28
-
29
- if [ "$START" -gt "$END" ]; then
30
- echo "Error: Start phase ($START) cannot exceed end phase ($END)"
31
- exit 1
32
- fi
33
-
34
- # --- Find .planning from git root ---
35
- GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
36
- if [ -z "$GIT_ROOT" ]; then
37
- echo "Error: Not in a git repository"
38
- exit 1
39
- fi
40
-
41
- PHASES_DIR="$GIT_ROOT/.planning/phases"
42
- if [ ! -d "$PHASES_DIR" ]; then
43
- echo "Error: Phases directory not found at $PHASES_DIR"
44
- exit 1
45
- fi
46
-
47
- # --- Delete artifacts from each phase in range ---
48
- DELETED=0
49
- for dir in "$PHASES_DIR"/*/; do
50
- [ -d "$dir" ] || continue
51
- dirname=$(basename "$dir")
52
- phase_num="${dirname%%-*}"
53
- # Strip leading zeros for numeric comparison
54
- phase_int=$((10#$phase_num))
55
- if [ "$phase_int" -ge "$START" ] && [ "$phase_int" -le "$END" ]; then
56
- for f in "$dir"/*-CONTEXT.md "$dir"/*-DESIGN.md "$dir"/*-RESEARCH.md \
57
- "$dir"/*-SUMMARY.md "$dir"/*-UAT.md "$dir"/*-VERIFICATION.md \
58
- "$dir"/*-EXECUTION-ORDER.md; do
59
- if [ -f "$f" ]; then
60
- rm -f "$f"
61
- DELETED=$((DELETED + 1))
62
- fi
63
- done
64
- fi
65
- done
66
-
67
- echo "Cleaned $DELETED artifact files from phases $START-$END"
68
- exit 0