Skip to content

Commit 8e1f33a

Browse files
Bordaclaude[bot]
andcommitted
fix(plugins): resolve residual audit findings across 6 plugins
Fixes surfaced by /audit plugins --adversarial and its follow-up residual-critical/non-critical passes, plus a routing-links checker bug found while validating this commit. - Fix unrehydrated shell-variable and sentinel-newline bugs across cc_develop, cc_foundry, cc_oss, cc_research, codemap-py — fresh-shell state loss between separate Bash tool calls (CODEX_AVAILABLE, RANGE, SCAN_STATE_FILE and others) - Harden symlink-unsafe temp-file writes in codemap-py (resolve_index_env.py, parse_deprecate_args.py, setup_scan_env.sh) via O_NOFOLLOW / mktemp+atomic-rename instead of plain redirect - Add containment guard to codemap-py's locate_scan_query.py Tier-3 cache glob, mirroring the existing Tier-2 guard - Wire Glob-based file resolution into codemap-py's debrief-coding Step 2 (previously referenced shell vars unresolvable across tool calls and unreadable by the Read tool) - Fix cross-plugin path-resolution bug in cc_foundry's check_routing_links.py: relative_to() against an absolute plugins_dir always raised ValueError, silently falling back to the source plugin instead of the referenced target plugin - Fix resolve_skill_subdir.py tier-precedence bug so --local correctly overrides CLAUDE_PLUGIN_ROOT instead of being a silent no-op - Various smaller fixes: BSD-incompatible grep -oP, sentinel project-scoping collision, stale .js hook references, cicd-steward unmarked write, Confidence block format, orphan-risk markers - Bump versions: cc_develop 0.18.3, cc_foundry 0.38.4, cc_oss 0.24.3, cc_research 0.12.6, codemap-py 0.28.4 (.claude-plugin + .codex-plugin), codex-rig 0.4.2 --- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
1 parent 002d822 commit 8e1f33a

158 files changed

Lines changed: 3289 additions & 1000 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

benchmarks/tests/test_run_all.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def batch_env(tmp_path: Path) -> tuple[dict[str, str], Path]:
107107
exit 0
108108
fi
109109
if [[ "$*" == *"prepare-codex-index.py"* && "$*" == *"--verify"* ]]; then
110-
if grep -q '"scan_version": {LOCKED_INDEX_SCAN_VERSION}' "$3" && grep -q '"modules": \[\]' "$3"; then
110+
if grep -q '"scan_version": {LOCKED_INDEX_SCAN_VERSION}' "$3" && grep -q '"modules": \\[\\]' "$3"; then
111111
printf "verified: %s\\n" "$3"
112112
exit 0
113113
fi

plugins/cc_develop/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"license": "Apache-2.0",
88
"name": "develop",
99
"repository": "https://github.com/Borda/AI-Rig",
10-
"version": "0.18.2"
10+
"version": "0.18.3"
1111
}

plugins/cc_develop/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ ______________________________________________________________________
377377

378378
| Flag | Description |
379379
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
380+
| `--issue <N>` | Force issue mode — fetch GitHub issue `<N>` instead of inferring mode from a bare numeric argument. Value form only; `--issue=123` is not supported by design (mode-detect matches the bare token). |
380381
| `--repo <owner/repo>` | Route issue fetch to upstream repo. Use when working in fork and issue on original repo (e.g. `--repo owner/my-project`). |
381382
| `--team` | Spawn 2-3 `foundry:sw-engineer` teammates, each investigating distinct root-cause hypothesis independently. Use when root cause unclear after initial analysis, or failure spans 3+ modules |
382383
| `--worktree` | Run the investigation in an isolated git worktree (base: HEAD) so reproduction attempts never touch main sources. Diagnosis file is written to the **main tree** so `/develop:fix` can read it. |
@@ -646,7 +647,7 @@ claude plugin install foundry@borda-ai-rig
646647

647648
### A question is blocked with "develop:review report gate"
648649

649-
`enforce-review-header.js` denied an `AskUserQuestion` call because `.reports/review/<timestamp>/review-report.md` does not exist — the review reached agent launch but never consolidated its findings into a report. Finish the consolidation step and print the report `---` header; the question then goes through. The gate deactivates two hours after a run starts, so an aborted review never blocks later questions permanently.
650+
`enforce-review-header.js` denied an `AskUserQuestion` call because `.reports/review/<timestamp>/review-report.md` does not exist — the review reached agent launch but never consolidated its findings into a report. Finish the consolidation step and print the report `---` header; the question then goes through. The gate deactivates two hours after a run starts, so an aborted review never blocks later questions permanently. When no review is actually in flight and an aborted run simply left its sentinel behind, there is no need to wait out that window: the denial message names the sentinel file, so `rm -f`-ing that path and re-issuing the question clears the block immediately.
650651

651652
### Demo gate passes (exit 0) when it should fail
652653

plugins/cc_develop/bin/dev_parse_args.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -459,11 +459,11 @@ def write_skill_files(skill: str, arguments: str, tmp_dir: Path | None = None) -
459459

460460
for spec, legacy in entries:
461461
value = values[spec.var]
462-
(target_dir / _per_skill_filename(skill, spec)).write_text(value)
462+
(target_dir / _per_skill_filename(skill, spec)).write_text(f"{value}\n")
463463
if legacy is not None:
464-
(target_dir / f"{legacy}-{_csid()}").write_text(value)
464+
(target_dir / f"{legacy}-{_csid()}").write_text(f"{value}\n")
465465
# Write CLEAN_ARGS (flags stripped) to a per-skill file so callers avoid eval
466-
(target_dir / f"dev-{skill}-clean-args-{_csid()}").write_text(_clean)
466+
(target_dir / f"dev-{skill}-clean-args-{_csid()}").write_text(f"{_clean}\n")
467467
return values
468468

469469

plugins/cc_develop/bin/dev_run_dir.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
With ``--sentinel <name>``, also touches ``<sentinel-dir>/<name>-<ts>``.
55
Sentinel name is sanitized to ``[a-zA-Z0-9_-]+`` to prevent path traversal.
66
7-
Sentinel dir mirrors JS getSentinelDir(): ``/tmp`` on POSIX,
8-
``tempfile.gettempdir()`` on Windows — matches commit-guard.js and task-log.js.
7+
Sentinel dir honors ``$TMPDIR`` when set, else ``tempfile.gettempdir()`` — the Python
8+
equivalent of the shell ``${TMPDIR:-/tmp}`` idiom callers use to poll the sentinel.
99
1010
Usage:
1111
python dev_run_dir.py [--sentinel <name>]
@@ -21,6 +21,7 @@
2121
from __future__ import annotations
2222

2323
import argparse
24+
import os
2425
import re
2526
import sys
2627
import tempfile
@@ -31,12 +32,14 @@
3132

3233

3334
def _sentinel_dir() -> Path:
34-
"""Return ``/tmp`` on POSIX or ``tempfile.gettempdir()`` on Windows.
35+
"""Return ``$TMPDIR`` when set, else ``tempfile.gettempdir()``.
3536
36-
Mirrors JS ``getSentinelDir()`` so sentinel paths match hook expectations
37-
on all platforms while preserving the existing ``/tmp`` path on POSIX.
37+
Matches the shell ``${TMPDIR:-/tmp}`` idiom used by callers that poll this sentinel,
38+
so both sides resolve to the same directory on every platform. ``os.environ`` is read
39+
first because ``tempfile.gettempdir()`` caches its result on first call and would not
40+
observe a later ``TMPDIR`` change.
3841
"""
39-
return Path(tempfile.gettempdir()) if sys.platform == "win32" else Path("/tmp")
42+
return Path(os.environ.get("TMPDIR") or tempfile.gettempdir())
4043

4144

4245
def main(argv: list[str] | None = None) -> int:
@@ -67,7 +70,16 @@ def main(argv: list[str] | None = None) -> int:
6770
if args.sentinel:
6871
sentinel_name = _SAFE_NAME_RE.sub("", args.sentinel)
6972
if sentinel_name:
70-
(_sentinel_dir() / f"{sentinel_name}-{ts}").touch()
73+
sentinel_path = _sentinel_dir() / f"{sentinel_name}-{ts}"
74+
# O_NOFOLLOW refuses a pre-planted symlink in the world-writable temp dir;
75+
# getattr keeps this working on native Windows, where the flag is absent.
76+
flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY | getattr(os, "O_NOFOLLOW", 0)
77+
try:
78+
fd = os.open(sentinel_path, flags, 0o600)
79+
except OSError:
80+
pass # sentinel skipped — never abort the caller's always-exit-0 contract
81+
else:
82+
os.close(fd)
7183

7284
sys.stdout.write(run_dir.as_posix() + "\n")
7385
return 0

plugins/cc_develop/bin/setup_worktree.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
With ``--sentinel <name>``, also touches ``<sentinel-dir>/<name>-<ts>``.
88
Sentinel name is sanitized to ``[a-zA-Z0-9_-]+`` to prevent path traversal.
99
10-
Sentinel dir mirrors JS getSentinelDir(): ``/tmp`` on POSIX,
11-
``tempfile.gettempdir()`` on Windows.
10+
Sentinel dir honors ``$TMPDIR`` when set, else ``tempfile.gettempdir()`` — the Python
11+
equivalent of the shell ``${TMPDIR:-/tmp}`` idiom callers use to poll the sentinel.
1212
1313
``--sentinel`` and its name are parsed by a direct ``argv`` check rather than argparse's
1414
matcher so that the legacy always-exit-0 contract is preserved — a missing name, an unknown
@@ -29,6 +29,7 @@
2929
from __future__ import annotations
3030

3131
import argparse
32+
import os
3233
import re
3334
import sys
3435
import tempfile
@@ -39,12 +40,14 @@
3940

4041

4142
def _sentinel_dir() -> Path:
42-
"""Return ``/tmp`` on POSIX or ``tempfile.gettempdir()`` on Windows.
43+
"""Return ``$TMPDIR`` when set, else ``tempfile.gettempdir()``.
4344
44-
Mirrors JS ``getSentinelDir()`` so sentinel paths match hook expectations
45-
on all platforms while preserving the existing ``/tmp`` path on POSIX.
45+
Matches the shell ``${TMPDIR:-/tmp}`` idiom used by callers that poll this sentinel,
46+
so both sides resolve to the same directory on every platform. ``os.environ`` is read
47+
first because ``tempfile.gettempdir()`` caches its result on first call and would not
48+
observe a later ``TMPDIR`` change.
4649
"""
47-
return Path(tempfile.gettempdir()) if sys.platform == "win32" else Path("/tmp")
50+
return Path(os.environ.get("TMPDIR") or tempfile.gettempdir())
4851

4952

5053
def main(argv: list[str] | None = None) -> int:
@@ -80,7 +83,16 @@ def main(argv: list[str] | None = None) -> int:
8083
if len(args) >= 2 and args[0] == "--sentinel" and args[1]:
8184
sentinel_name = _SAFE_NAME_RE.sub("", args[1])
8285
if sentinel_name:
83-
(_sentinel_dir() / f"{sentinel_name}-{ts}").touch()
86+
sentinel_path = _sentinel_dir() / f"{sentinel_name}-{ts}"
87+
# O_NOFOLLOW refuses a pre-planted symlink in the world-writable temp dir;
88+
# getattr keeps this working on native Windows, where the flag is absent.
89+
flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY | getattr(os, "O_NOFOLLOW", 0)
90+
try:
91+
fd = os.open(sentinel_path, flags, 0o600)
92+
except OSError:
93+
pass # sentinel skipped — never abort the caller's always-exit-0 contract
94+
else:
95+
os.close(fd)
8496

8597
sys.stdout.write(f"{ts}\n{run_dir.as_posix()}\n")
8698
return 0

plugins/cc_develop/hooks/enforce-review-header.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ const REPORT_FILENAME = "review-report.md";
6565
// Step 2 always builds "$_REPORT_BASE/.reports/review/$TIMESTAMP" ($_REPORT_BASE
6666
// is the main tree even under --worktree); requiring the marker keeps the hook
6767
// from acting on a sentinel holding anything else.
68-
const REPORT_DIR_MARKER = "/.reports/review/";
68+
const REPORT_DIR_PARTS = [".reports", "review"];
6969
// Enforcement window measured from the sentinel's mtime (see KNOWN LIMITATION).
7070
const STALE_MS = 2 * 60 * 60 * 1000;
7171

@@ -109,9 +109,30 @@ function findSentinel(dir, csids) {
109109
return null;
110110
}
111111

112+
/** Select the path implementation that matches one absolute report path. */
113+
function reportPathApi(value) {
114+
return typeof value === "string" && (/^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\"))
115+
? path.win32
116+
: path.posix;
117+
}
118+
119+
/** True when an absolute path's normalized components contain a report directory. */
120+
function isReportPath(value, reportParts) {
121+
if (typeof value !== "string") return false;
122+
const api = reportPathApi(value);
123+
if (!api.isAbsolute(value)) return false;
124+
const parts = api.normalize(value).split(api.sep).filter(Boolean);
125+
const normalize = api === path.win32 ? (part) => part.toLowerCase() : (part) => part;
126+
const marker = reportParts.map(normalize);
127+
return parts.some(
128+
(_, index) =>
129+
index + marker.length < parts.length && marker.every((part, offset) => normalize(parts[index + offset]) === part),
130+
);
131+
}
132+
112133
/** True when `value` has the shape Step 2 writes: absolute path under .reports/review/. */
113134
function isReviewReportDir(value) {
114-
return typeof value === "string" && path.isAbsolute(value) && value.includes(REPORT_DIR_MARKER);
135+
return isReportPath(value, REPORT_DIR_PARTS);
115136
}
116137

117138
/**
@@ -154,7 +175,9 @@ function denyReason(sentinelPath, now) {
154175
"(print report header) have not completed. Go back: spawn the consolidator agent and let it write " +
155176
`${REPORT_FILENAME}, then Read that file and print its \`---\` header block to the terminal. Call ` +
156177
"AskUserQuestion only after that header has actually appeared in your response. If the consolidator " +
157-
"genuinely cannot run, report that failure and stop instead of asking the user."
178+
"genuinely cannot run, report that failure and stop instead of asking the user. If no develop:review is " +
179+
"actually in flight (an aborted run left this sentinel behind), clear it with: " +
180+
`\`rm -f ${sentinelPath}\` — then re-issue the question.`
158181
);
159182
}
160183

plugins/cc_develop/rules/quality-gates.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ paths:
66

77
## Confidence Block (required on all analysis tasks)
88

9+
<!-- policy-sibling: plugins/cc_oss/rules/quality-gates.md, plugins/cc_foundry/rules/quality-gates.md, plugins/cc_research/rules/quality-gates.md -->
10+
911
Every analysis agent **must** end with:
1012

1113
```markdown
1214
## Confidence
13-
**Score**: 0.N — [high ≥0.9 | moderate 0.8–0.9 | low <0.8]
15+
**Score**: 0.N — [high ≥0.9 | moderate 0.85–0.9 | low <0.85]
1416
**Gaps**:
1517
- [specific limitation]
1618
← blank line required; Refinements is a peer field, not a sub-bullet
@@ -22,7 +24,7 @@ Every analysis agent **must** end with:
2224
2325
- Omit **Refinements** if 0 passes — omit **Gaps** bullets if none, keep **Gaps** header
2426
- **Score**, **Gaps**, **Refinements** = peer top-level fields — never nest Refinements under Gaps; blank line before **Refinements** required
25-
- Score < 0.8 → ⚠ on score line AND next line: "orchestrator may re-run with the specific gap addressed"
27+
- Score < 0.85 → ⚠ on score line AND next line: "orchestrator may re-run with the specific gap addressed"
2628
- Gaps = primary signal — surface implicit limitations for re-run decisions
2729

2830
## Internal Quality Loop (analysis tasks only)

plugins/cc_develop/skills/_shared/runner-detection.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,39 @@
33
Detect test runner once at skill start:
44

55
```bash
6+
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
67
if [ -f "uv.lock" ] || grep -q '\[tool\.uv\]' pyproject.toml 2>/dev/null; then TEST_CMD="uv run pytest"
78
elif [ -f "poetry.lock" ] || grep -q '\[tool\.poetry\]' pyproject.toml 2>/dev/null; then TEST_CMD="poetry run pytest"
89
elif [ -f "tox.ini" ]; then TEST_CMD="tox -q" # avoids hard-coded py3 env name
910
elif [ -f "Makefile" ] && grep -q '^test:' Makefile 2>/dev/null; then TEST_CMD="make test"
1011
else TEST_CMD="python -m pytest"; fi
12+
echo "$TEST_CMD" > "${TMPDIR:-/tmp}/dev-test-cmd-${CSID}"
1113
```
1214

1315
Use `$TEST_CMD` for full suite runs.
1416

1517
```bash
1618
# tox/make reject --tb and ::node selectors — derive unwrapped PYTEST_CMD
19+
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
20+
IFS= read -r TEST_CMD < "${TMPDIR:-/tmp}/dev-test-cmd-${CSID}" 2>/dev/null || TEST_CMD="python -m pytest"
1721
case "$TEST_CMD" in
1822
tox*|"make test")
1923
if command -v uv >/dev/null 2>&1; then PYTEST_CMD="uv run pytest"
2024
else PYTEST_CMD="python -m pytest"; fi ;;
2125
*) PYTEST_CMD="$TEST_CMD" ;;
2226
esac
27+
echo "$PYTEST_CMD" > "${TMPDIR:-/tmp}/dev-pytest-cmd-${CSID}"
28+
echo "TEST_CMD=$TEST_CMD PYTEST_CMD=$PYTEST_CMD"
2329
```
2430

2531
Use `$PYTEST_CMD` for single test file/node with pytest-specific flags (`--tb`, `::test_name`); `$TEST_CMD` for full suite.
32+
33+
**Both values are persisted, and every later block must re-read them** — bash state is lost between Bash() calls, so a bare `$PYTEST_CMD` in a later block expands to the empty string and the command silently becomes `--tb=... -v``command not found` → exit 127, which downstream exit-code checks misread as a genuine test failure. Read back with:
34+
35+
```bash
36+
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
37+
IFS= read -r PYTEST_CMD < "${TMPDIR:-/tmp}/dev-pytest-cmd-${CSID}" 2>/dev/null || PYTEST_CMD=""
38+
IFS= read -r TEST_CMD < "${TMPDIR:-/tmp}/dev-test-cmd-${CSID}" 2>/dev/null || TEST_CMD=""
39+
```
40+
41+
Guard on emptiness before running — never let an unresolved command reach the shell.

0 commit comments

Comments
 (0)