@bookedsolid/rea 0.28.1 → 0.28.2

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.
@@ -114,6 +114,24 @@ Consumer projects may extend the roster via `.rea/agents/` and profile YAMLs, bu
114
114
  4. Delegate with full context — include file paths, constraints from policy.yaml, acceptance criteria, and the commit-discipline note above
115
115
  5. Verify outputs before reporting completion — do not trust agent summaries at face value. Read the files, check git status, confirm the build.
116
116
 
117
+ ## Self-review when the orchestrator implements directly (0.29.0+)
118
+
119
+ There are sessions where the orchestrator must implement work itself instead of dispatching:
120
+
121
+ - Subagent dispatch is unavailable (no Task tool in the current harness, exempt-subagent scenario).
122
+ - The task is narrowly scoped to a single small surface where the dispatch overhead exceeds the implementation cost.
123
+ - A codex round between specialist hand-offs is being used as the de facto specialist tier (the "Option C" iteration pattern from the 0.29.0 marathon).
124
+
125
+ In every such case, you MUST still apply the specialist discipline that delegation would have enforced. This is not optional — the structural risk of "one Opus turn implements five surfaces" is exactly the failure mode that principal-engineer review caught in the 0.28.0 cycle (manifest glob-injection P1 + cache-staleness P2, both pre-commit). Reach the same closure shape by:
126
+
127
+ 1. **Name the specialists you are channeling.** Before each surface, state which specialist's discipline applies (e.g. "shell-scripting-specialist + adversarial-test-specialist for the bash gate corpus; typescript-specialist for the CLI; platform-architect for the workflow"). State it out loud so the user can spot a mis-cast role.
128
+ 2. **Codex round between surfaces, not just at the end.** A single end-of-build codex round across 5 surfaces buries P1s in noise. One round per surface keeps the signal sharp. The 0.27.0 direct-Bash codex CLI is cheap enough at one Opus turn per round to make this routine.
129
+ 3. **Explicit threat-model framing for security-tier changes.** When patching a hook, name the bypass class, the conservative-vs-narrow reading, and the sibling shapes the class implies. Refuse to commit until the corpus enumerates every shape the class includes.
130
+ 4. **Single-commit-per-PR discipline still applies.** Squash local work before push. The pre-push gate's stateless codex review runs once against the squashed diff; granular commits multiply the review burden without surfacing new findings.
131
+ 5. **Defer ruthlessly.** Trimmed-scope greenlights from the user are a maximum, not a minimum. The marathon's 0.28.0 lesson was "principal-engineer trimmed the 11-item plate to 6 with crisp deferral reasons." Apply the same lens during direct-implementation: if surface 6 needs structural rework, defer it to the next minor with the reason in the changeset rather than ship a half-baked closure.
132
+
133
+ A self-review checkpoint after each surface (read the diff back, run the targeted tests, fire codex against the working tree) IS the specialist tier when no subagent is in the path. Skip the checkpoint and the structural lesson resets.
134
+
117
135
  ## The Plan / Build / Review Loop (default workflow)
118
136
 
119
137
  REA's default engineering workflow is three-legged, with Review performed by a different model than Build:
@@ -145,6 +145,45 @@ if [[ "$raw_has_traversal" -eq 1 ]] || [[ "$norm_has_traversal" -eq 1 ]]; then
145
145
  exit 2
146
146
  fi
147
147
 
148
+ # ── 5a-bis. Reject interior single-dot segments (0.29.0 helix-/./-class) ─────
149
+ # Parallel to the `..` guard above. `normalize_path` does NOT collapse
150
+ # interior `./` segments — that would corrupt `..` traversals — which leaves
151
+ # a bypass class. A blocked entry of `.env` does not match `foo/./.env`
152
+ # (the literal-comparison loop is byte-for-byte), so an attacker who can
153
+ # influence the file_path string can dodge the policy entry.
154
+ #
155
+ # The conservative closure (per Jake 2026-05-12): treat any interior `/./`
156
+ # segment exactly like `..`. The NORMALIZED form is the safe surface for
157
+ # the check — `normalize_path` already stripped leading `./` segments, so
158
+ # any `/./` that survives is interior by construction. A raw-form check
159
+ # would false-positive on benign `./foo` paths (codex round 1 P2: a path
160
+ # like `%2E%2Fsrc/foo.ts` decodes to `./src/foo.ts` which is the same
161
+ # leading-`./` allowed shape the comment at the top of `normalize_path`
162
+ # documents — guarding against it on the raw form would block legit
163
+ # writes under `src/` and friends).
164
+ #
165
+ # URL-encoded companion: `.%2F` / `%2E/` / `%2E%2F` decode to `./` via
166
+ # `normalize_path` (which knows `%2E` → `.` and `%2F` → `/`). After
167
+ # URL-decode + leading-`./` strip, any encoded INTERIOR form hits the
168
+ # normalized `*/./* ` check. No raw-form encoded guard is needed — the
169
+ # normalize_path path already covers every encoded shape the helper
170
+ # decodes, and shapes it doesn't decode wouldn't resolve to an interior
171
+ # `./` segment on disk either.
172
+ norm_has_dot_segment=0
173
+ case "/$NORMALIZED/" in
174
+ */./*) norm_has_dot_segment=1 ;;
175
+ esac
176
+ if [[ "$norm_has_dot_segment" -eq 1 ]]; then
177
+ {
178
+ printf 'BLOCKED PATH: interior dot-segment rejected\n'
179
+ printf '\n'
180
+ printf ' File: %s\n' "$FILE_PATH"
181
+ printf " Rule: path contains an interior '/./' segment; rewrite to a\n"
182
+ printf ' canonical project-relative path without dot segments.\n'
183
+ } >&2
184
+ exit 2
185
+ fi
186
+
148
187
  for writable in "${AGENT_WRITABLE[@]}"; do
149
188
  if [[ "$NORMALIZED" == "$writable" ]] || [[ "$NORMALIZED" == "$writable"* && "$writable" == */ ]]; then
150
189
  exit 0
@@ -128,6 +128,45 @@ if [[ "$raw_has_traversal" -eq 1 ]] || [[ "$norm_has_traversal" -eq 1 ]]; then
128
128
  exit 2
129
129
  fi
130
130
 
131
+ # ── 5a-bis. Reject interior single-dot segments (0.29.0 helix-/./-class) ─────
132
+ # Companion to the `..` guard above. The `normalize_path` helper deliberately
133
+ # does NOT collapse interior `./` segments because doing so would corrupt
134
+ # `..` traversals — but that leaves a parallel bypass class. A path like
135
+ # `.husky/./pre-push` resolves on disk to `.husky/pre-push`, yet the literal/
136
+ # prefix matchers in §6 compare against the un-collapsed `.husky/./pre-push`
137
+ # string and miss the match.
138
+ #
139
+ # Conservative reading (per Jake 2026-05-12): treat any interior `./`
140
+ # segment exactly like a `..` segment — refuse outright, force the caller
141
+ # to send a canonical path. The corpus design pairs shell-scripting-specialist
142
+ # with adversarial-test-specialist; the canonical attack shapes are:
143
+ #
144
+ # .husky/./pre-push — single segment
145
+ # .husky/././pre-push — repeated segments
146
+ # .husky/.//pre-push — `./` immediately followed by another `/`
147
+ # .claude/hooks/./_lib/halt-check.sh — inside a protected directory
148
+ # %2E%2F — percent-encoded `./`, caught after URL-decode
149
+ # .\.\pre-push — backslash variant, normalize_path → `./`
150
+ #
151
+ # Only the NORMALIZED form is checked (not the raw form) because raw `./foo`
152
+ # at start-of-string is a legitimate relative path; `normalize_path` already
153
+ # strips leading `./` segments, so anything that survives into the normalized
154
+ # form's `/./` shape is INTERIOR by construction.
155
+ norm_has_dot_segment=0
156
+ case "/$NORMALIZED/" in
157
+ */./*) norm_has_dot_segment=1 ;;
158
+ esac
159
+ if [[ "$norm_has_dot_segment" -eq 1 ]]; then
160
+ {
161
+ printf 'SETTINGS PROTECTION: interior dot-segment rejected\n'
162
+ printf '\n'
163
+ printf ' File: %s\n' "$SAFE_FILE_PATH"
164
+ printf " Rule: path contains an interior '/./' segment; rewrite to a\n"
165
+ printf ' canonical project-relative path without dot segments.\n'
166
+ } >&2
167
+ exit 2
168
+ fi
169
+
131
170
  # Compute lower-cased path early so the §5b allow-list (and §6/§6b matchers
132
171
  # below) all reference a single normalized variable.
133
172
  LOWER_NORM=$(printf '%s' "$NORMALIZED" | tr '[:upper:]' '[:lower:]')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bookedsolid/rea",
3
- "version": "0.28.1",
3
+ "version": "0.28.2",
4
4
  "description": "Agentic governance layer for Claude Code — policy enforcement, hook-based safety gates, audit logging, and Codex-integrated adversarial review for AI-assisted projects",
5
5
  "license": "MIT",
6
6
  "author": "Booked Solid Technology <oss@bookedsolid.tech> (https://bookedsolid.tech)",
@@ -167,8 +167,52 @@ trap 'rm -rf -- "$WORK"' EXIT HUP INT TERM
167
167
  # a new tarball was published. The release.yml rebuild+verify step
168
168
  # remains the catching net at publish time, so skipping here does not
169
169
  # re-open the BUG-013 attack surface for the merge-to-main path.
170
- if ! ( cd "$WORK" && npm pack "${PKG_NAME}@${PREV_VERSION}" --silent >/dev/null 2>&1 ); then
171
- log "skip npm pack ${PKG_NAME}@${PREV_VERSION} failed (network issue or registry outage)"
170
+ #
171
+ # 0.29.0: bounded retry loop for npm CDN propagation lag. The memory
172
+ # entries for 0.9.0, 0.12.0, 0.13.0, 0.28.0, and 0.28.1 all note
173
+ # "release verify flaked on npm CDN lag" — `npm view` returns the
174
+ # version metadata but `npm pack` against the same version times out
175
+ # or 404s because the tarball blob has not propagated to all CDN edges
176
+ # yet. The CI-side workflow already has a 12×10s retry (release.yml
177
+ # phase 2); this script runs locally / in PR CI where the failure
178
+ # window is shorter but still occurs.
179
+ #
180
+ # Shape: initial attempt + three retries with sleeps 2s / 8s / 30s.
181
+ # Total worst-case wait = 2 + 8 + 30 = 40s, all on the failure path.
182
+ # That covers the empirically observed CDN propagation window (cf.
183
+ # release.yml phase 2 retry loops) while bounding the local-/ PR-side
184
+ # blocking time to under a minute on a genuine outage.
185
+ NPM_PACK_OK=0
186
+ NPM_PACK_DELAYS=(2 8 30)
187
+ NPM_PACK_ATTEMPTS=$((${#NPM_PACK_DELAYS[@]} + 1))
188
+ # Codex round 1 P2-2: use bash arithmetic for-loop instead of `$(seq 1 N)`.
189
+ # `seq` is not in the preflight tool list (line 104: npm jq git shasum tar)
190
+ # and `set -e` at the top of the script would exit 127 inside the loop body
191
+ # on minimal images that lack it (Alpine, some BusyBox shells). Bash's
192
+ # arithmetic for-loop is a builtin and works on every supported version.
193
+ for ((attempt = 1; attempt <= NPM_PACK_ATTEMPTS; attempt++)); do
194
+ if ( cd "$WORK" && npm pack "${PKG_NAME}@${PREV_VERSION}" --silent >/dev/null 2>&1 ); then
195
+ if [ "$attempt" -gt 1 ]; then
196
+ log "npm pack succeeded after ${attempt} attempt(s) (CDN propagation lag)"
197
+ fi
198
+ NPM_PACK_OK=1
199
+ break
200
+ fi
201
+ # Clean up any partial artifact npm pack may have left in $WORK
202
+ # before retrying so the next attempt has a clean slate.
203
+ find "$WORK" -maxdepth 1 -type f -name '*.tgz' -delete 2>/dev/null || true
204
+ if [ "$attempt" -lt "$NPM_PACK_ATTEMPTS" ]; then
205
+ # Bash array is 0-indexed; $attempt is 1-indexed; index into
206
+ # NPM_PACK_DELAYS at $attempt-1 to read the delay AFTER this
207
+ # failed attempt (before the next try).
208
+ idx=$((attempt - 1))
209
+ delay="${NPM_PACK_DELAYS[$idx]}"
210
+ log "npm pack attempt ${attempt}/${NPM_PACK_ATTEMPTS} failed; sleeping ${delay}s for CDN propagation"
211
+ sleep "$delay"
212
+ fi
213
+ done
214
+ if [ "$NPM_PACK_OK" -ne 1 ]; then
215
+ log "skip — npm pack ${PKG_NAME}@${PREV_VERSION} failed after ${NPM_PACK_ATTEMPTS} attempts (network issue, registry outage, or persistent CDN lag)"
172
216
  exit 0
173
217
  fi
174
218