mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-11 10:23:44 +02:00
v1.38.1.0 fix wave: surrogate-safe page captures (#1440), Implementation Tasks across review skills (#1454), root-level artifact patterns (#1452) (#1504)
* fix(browse): sanitize lone Unicode surrogates at commandResult chokepoint + /batch envelope (#1440) Page captures with mixed-script Unicode round-trip cleanly to the Claude API. Two new utilities in browse/src/sanitize.ts: stripLoneSurrogates for raw UTF-16 strings, stripLoneSurrogateEscapes for \uXXXX JSON escape text. sanitizeBody picks the right pass based on cr.json. buildCommandResponse is extracted from handleCommand (now exported) and applies sanitization before new Response(). /batch was bypassing this chokepoint via direct JSON.stringify, so it sanitizes each cr.result before pushing AND wraps the envelope with stripLoneSurrogateEscapes. Defense in depth wraps at getCleanText, getCleanTextWithStripping, html, accessibility, and snapshot.ts return points so downstream consumers (datamarking, envelope wrapping) see sanitized text before the response is built. 25 new unit tests across sanitize.test.ts and build-command-response.test.ts. content-security.test.ts updated to accept either pre- or post-sanitize form of the snapshot scoped branch (source-level regression check). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: bug fix wave v1.36.0.0 — Implementation Tasks, allowlist patterns, surrogate-safe page captures (#1440 #1452 #1454) Three filed issues land together: #1440 — Page captures from real-world HTML hit 'API Error 400: no low surrogate in string'. Sanitizers + buildCommandResponse extraction shipped in the prior commit; this commit adds the migration script that patches existing brain-allowlist/privacy-map/gitattributes installs and the supporting tests. #1452 — Federation sync was silently skipping root-level design and test-plan docs. bin/gstack-artifacts-init adds two patterns to all three managed blocks (.brain-allowlist, .brain-privacy-map.json, .gitattributes). Idempotent migration v1.36.0.0.sh repairs existing installs in place via jq (preserves JSON validity) — no commit + push from the migration. #1454 — All four review skills (CEO/design/eng/DX) emit an Implementation Tasks markdown section AND write a jq-built JSONL artifact per phase. /autoplan reads all four files, scopes by current branch + 5-commit window, dedupes on exact (component, sorted(files), title), and renders an aggregated list in the Final Approval Gate. New tests: - browse/test/sanitize.test.ts (18 cases) - browse/test/build-command-response.test.ts (7 cases) - test/artifacts-init-migration.test.ts (7 cases) VERSION → 1.36.0.0. Skips the v1.34.x slot taken by 'gstack consumable as submodule' and the v1.35.0.0 slot taken by /document-generate. #1428 was shipped separately by v1.34.2.0 with a different approach; follow-up #1503 filed for the bare-path filesystem boundary concern surfaced during our analysis. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: bump to v1.38.1.0 VERSION + package.json + CHANGELOG header + migration filename + test reference all consistently at v1.38.1.0. Migration renamed: gstack-upgrade/migrations/v1.38.0.0.sh -> v1.38.1.0.sh. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1599,6 +1599,81 @@ noting which items are incomplete. Do not loop indefinitely.
|
||||
|
||||
## Phase 4: Final Approval Gate
|
||||
|
||||
## Implementation Tasks aggregator
|
||||
|
||||
Before rendering the Final Approval Gate output block below, aggregate the
|
||||
per-phase task lists each review skill wrote.
|
||||
|
||||
```bash
|
||||
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
|
||||
TASKS_DIR="${HOME}/.gstack/projects/${SLUG:-unknown}"
|
||||
BRANCH=$(git branch --show-current 2>/dev/null || echo unknown)
|
||||
# Commit window: last 5 commits on this branch. Drops stale standalone reviews.
|
||||
COMMITS_RECENT=$(git log --format=%H -n 5 2>/dev/null | tr '\n' '|' | sed 's/|$//')
|
||||
|
||||
AGGREGATED_TASKS=""
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
# Collect entries from all 4 phases, scoped to current branch + commit window.
|
||||
# For each phase, keep only the latest run_id. Within the surviving set,
|
||||
# dedupe by (component, sorted(files), title) — exact match only.
|
||||
# Sort by priority (P1 > P2 > P3) then by phase order.
|
||||
ALL_JSONL=$(mktemp -t autoplan-tasks.XXXXXXXX)
|
||||
for phase in ceo-review design-review eng-review devex-review; do
|
||||
# Use find instead of glob expansion — zsh nomatch errors otherwise when
|
||||
# a phase produced no JSONL files. Sorting by name keeps the order stable.
|
||||
while IFS= read -r f; do
|
||||
[ -f "$f" ] || continue
|
||||
# Filter to current branch + recent commits, then keep records for the
|
||||
# latest run_id only. (Single phase may have multiple files if the user
|
||||
# re-ran the review; aggregator takes the newest.)
|
||||
jq -c --arg branch "$BRANCH" --arg commits "$COMMITS_RECENT" \
|
||||
'select(.branch == $branch and ($commits | split("|") | index(.commit) != null))' \
|
||||
"$f" 2>/dev/null >> "$ALL_JSONL" || true
|
||||
done < <(find "$TASKS_DIR" -maxdepth 1 -name "tasks-$phase-*.jsonl" 2>/dev/null | sort)
|
||||
# Reduce to latest run_id per phase
|
||||
if [ -s "$ALL_JSONL" ]; then
|
||||
jq -sc --arg phase "$phase" \
|
||||
'[.[] | select(.phase == $phase)] | (max_by(.run_id) // null) as $latest_run | if $latest_run then map(select(.run_id == $latest_run.run_id)) else [] end | .[]' \
|
||||
"$ALL_JSONL" > "$ALL_JSONL.phase" 2>/dev/null || true
|
||||
# Replace with reduced version for this phase, accumulating others
|
||||
jq -c --arg phase "$phase" 'select(.phase != $phase)' "$ALL_JSONL" > "$ALL_JSONL.other" 2>/dev/null || true
|
||||
cat "$ALL_JSONL.other" "$ALL_JSONL.phase" > "$ALL_JSONL"
|
||||
rm -f "$ALL_JSONL.phase" "$ALL_JSONL.other"
|
||||
fi
|
||||
done
|
||||
|
||||
# Exact-match dedup by (component, sorted(files), title). Non-matches kept
|
||||
# separately with a possible-duplicate marker injected by the renderer.
|
||||
AGGREGATED_TASKS=$(jq -s \
|
||||
'group_by([.component, (.files | sort), .title])
|
||||
| map(
|
||||
# Take the highest-priority entry per group; tie-break by phase order
|
||||
sort_by({P1:0,P2:1,P3:2}[.priority] // 99, {"ceo-review":0,"design-review":1,"eng-review":2,"devex-review":3}[.phase] // 99) | .[0]
|
||||
)
|
||||
| sort_by({P1:0,P2:1,P3:2}[.priority] // 99, {"ceo-review":0,"design-review":1,"eng-review":2,"devex-review":3}[.phase] // 99)
|
||||
| if length == 0 then "_No actionable tasks emitted from any phase._" else
|
||||
map("- [ ] **\(.id) (\(.priority), human: \(.effort_human) / CC: \(.effort_cc)) — \(.component)** — \(.title)\n - Surfaced by: \(.phase) — \(.source_finding)\n - Files: \(.files | join(", "))") | join("\n")
|
||||
end' "$ALL_JSONL" 2>/dev/null | sed 's/^"//;s/"$//;s/\\n/\n/g')
|
||||
rm -f "$ALL_JSONL"
|
||||
else
|
||||
AGGREGATED_TASKS="_jq not installed — install jq to aggregate per-phase task lists. Skipping._"
|
||||
fi
|
||||
```
|
||||
|
||||
Inside the Final Approval Gate output template below, render the aggregated
|
||||
markdown in the `### Implementation Tasks (aggregated across phases)` section.
|
||||
Substitute the contents of `$AGGREGATED_TASKS` (the bash variable set above)
|
||||
before printing the message to the user. This is NOT a template placeholder
|
||||
— the agent does the substitution at runtime, not gen-skill-docs at build time.
|
||||
|
||||
If `$AGGREGATED_TASKS` is empty (no JSONL files found — none of the review
|
||||
skills ran in this session), render:
|
||||
|
||||
`_No per-phase task lists found in $TASKS_DIR for branch $BRANCH. Each review
|
||||
skill writes its own; if you ran one of them but no list appears here, check
|
||||
that jq is installed and the tasks-<phase>-*.jsonl files exist._`
|
||||
|
||||
|
||||
**STOP here and present the final state to the user.**
|
||||
|
||||
Present as a message, then use AskUserQuestion:
|
||||
@@ -1649,6 +1724,10 @@ I recommend [X] — [principle]. But [Y] is also viable:
|
||||
|
||||
### Deferred to TODOS.md
|
||||
[Items auto-deferred with reasons]
|
||||
|
||||
### Implementation Tasks (aggregated across phases)
|
||||
[Substitute the contents of $AGGREGATED_TASKS computed above. If empty:
|
||||
"_No per-phase task lists found in $TASKS_DIR for branch $BRANCH._"]
|
||||
```
|
||||
|
||||
**Cognitive load management:**
|
||||
|
||||
@@ -769,6 +769,8 @@ noting which items are incomplete. Do not loop indefinitely.
|
||||
|
||||
## Phase 4: Final Approval Gate
|
||||
|
||||
{{TASKS_SECTION_AGGREGATE}}
|
||||
|
||||
**STOP here and present the final state to the user.**
|
||||
|
||||
Present as a message, then use AskUserQuestion:
|
||||
@@ -819,6 +821,10 @@ I recommend [X] — [principle]. But [Y] is also viable:
|
||||
|
||||
### Deferred to TODOS.md
|
||||
[Items auto-deferred with reasons]
|
||||
|
||||
### Implementation Tasks (aggregated across phases)
|
||||
[Substitute the contents of $AGGREGATED_TASKS computed above. If empty:
|
||||
"_No per-phase task lists found in $TASKS_DIR for branch $BRANCH._"]
|
||||
```
|
||||
|
||||
**Cognitive load management:**
|
||||
|
||||
Reference in New Issue
Block a user