ecoportal-api 0.10.16 → 0.10.17
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.
- checksums.yaml +4 -4
- data/.ai-assistance/.gitignore +2 -0
- data/.ai-assistance/bridge/.gitignore +10 -0
- data/.ai-assistance/bridge/CLAUDE.md +96 -0
- data/.ai-assistance/bridge/archive/.gitkeep +0 -0
- data/.ai-assistance/bridge/inbox/.gitkeep +0 -0
- data/.ai-assistance/bridge/outbox/.gitkeep +0 -0
- data/.ai-assistance/capabilities/assumptions-log.md +23 -0
- data/.ai-assistance/scripts/bridge-inbox-check.sh +119 -0
- data/.ai-assistance/scripts/bridge-init.sh +86 -0
- data/.ai-assistance/scripts/confine-to-subtree.sh +58 -0
- data/.ai-assistance/scripts/dirty-tree-guard.sh +96 -0
- data/.ai-assistance/scripts/distill_procedural.py +602 -0
- data/.ai-assistance/scripts/log-mcp-access.sh +24 -0
- data/.ai-assistance/scripts/log-skill-usage.sh +79 -0
- data/.ai-assistance/scripts/log_mcp_access.py +158 -0
- data/.ai-assistance/scripts/observe-session.sh +13 -0
- data/.ai-assistance/scripts/observe_session.py +287 -0
- data/.ai-assistance/scripts/protect-host-paths.sh +135 -0
- data/.ai-assistance/scripts/scrub.py +1149 -0
- data/.ai-assistance/scripts/scrub.py.sha256 +6 -0
- data/.ai-assistance/scripts/surface-procedural.sh +9 -0
- data/.ai-assistance/scripts/surface_procedural.py +101 -0
- data/.ai-assistance/skills/ep-ai-manager/SKILL.md +519 -0
- data/.ai-assistance/skills/project-self-docs/SKILL.md +259 -0
- data/.ai-assistance/skills/project-self-docs/scripts/self_docs_scan.py +378 -0
- data/.ai-assistance/standards-version.json +12 -0
- data/.ai-assistance/version.json +8 -0
- data/.claude/.gitignore +2 -0
- data/.claude/settings.json +128 -0
- data/CHANGELOG.md +8 -5
- data/CLAUDE.md +95 -71
- data/docs/self-docs/ARCHITECTURE.md +145 -0
- data/docs/self-docs/CHANGES.jsonl +7 -0
- data/docs/self-docs/COMPLIANCE.md +66 -0
- data/docs/self-docs/CONVENTIONS.md +74 -0
- data/docs/self-docs/INTEGRATIONS.md +62 -0
- data/docs/self-docs/OPERATIONS.md +64 -0
- data/docs/self-docs/OVERVIEW.md +61 -0
- data/docs/self-docs/STATUS.md +71 -0
- data/docs/self-docs/self-docs-index.json +51 -0
- data/docs/worklog.md +48 -0
- data/lib/ecoportal/api/common/client/with_retry.rb +6 -0
- data/lib/ecoportal/api/version.rb +1 -1
- metadata +40 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e6d3ab5bbf5ab30cdf4a53a91eb9727156759a19efc68754d475ecc0a8fa45ba
|
|
4
|
+
data.tar.gz: 9fcd22e6b949e5b3ea302fabeedb5b502035d322b27b8e91c8b5e4ee3980869d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a39f97efb44f84488674df6c29d0efc5f16a2f7083efedb2d2be866dff771629f29649ca4fd66809f5b53c4c700d8fd4f4350a0eca43d260a22e2babd71dd5c
|
|
7
|
+
data.tar.gz: e1931ea7266e1b32b198f1fbf0ef218b156b2b8ab2a54ccf7b9de3d95418e6154e680865fc539248147bd1a3960188166e232ced83514f400aaa309a8bca8bad
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Bridge runtime state and message files are machine-local -- never committed.
|
|
2
|
+
# Only the structural scaffold (bridge/CLAUDE.md, .gitkeep placeholders, this file) is
|
|
3
|
+
# tracked; processed pairs under archive/ stay trackable for an optional audit trail.
|
|
4
|
+
inbox/*
|
|
5
|
+
outbox/*
|
|
6
|
+
LOCK
|
|
7
|
+
STATUS
|
|
8
|
+
queue/
|
|
9
|
+
!inbox/.gitkeep
|
|
10
|
+
!outbox/.gitkeep
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Bridge Protocol -- CoWork <-> Claude Code Delegation
|
|
2
|
+
|
|
3
|
+
The bridge is a file-based task delegation system for handing work between the CoWork
|
|
4
|
+
environment and the Claude Code (CLI) environment. Use it when one environment has
|
|
5
|
+
connector access (e.g. GitLab MCP, Gemini) that the other doesn't.
|
|
6
|
+
|
|
7
|
+
**The bridge is for delegation of discrete tasks only -- not for working tree changes.**
|
|
8
|
+
All file edits happen within a single environment. The bridge passes task context and results.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Folder structure
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
.ai-assistance/bridge/
|
|
16
|
+
inbox/ ← Tasks waiting to be processed (CoWork writes; Code reads)
|
|
17
|
+
outbox/ ← Completed results (Code writes; CoWork reads)
|
|
18
|
+
archive/ ← Completed inbox/outbox pairs (audit trail)
|
|
19
|
+
CLAUDE.md ← This file — bridge protocol documentation
|
|
20
|
+
LOCK ← Concurrency control (acquired/expires/intent/connector)
|
|
21
|
+
STATUS ← Aggregated status (pending/in-progress counts)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Task lifecycle
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
PENDING → IN_PROGRESS → DONE (or FAILED)
|
|
30
|
+
CoWork writes inbox/ → Code reads + processes → Code writes outbox/ → CoWork reads + archives
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Task filename convention
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
<username>-<uuid7>-<slug>.md
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
- `oscar-01j2k3l4m5n6-gemini-review.md`
|
|
43
|
+
- `oscar-01j2k3l4m5n6-gitlab-mr-check.md`
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Task file format
|
|
48
|
+
|
|
49
|
+
```markdown
|
|
50
|
+
---
|
|
51
|
+
task_id: <uuid7>
|
|
52
|
+
status: PENDING # PENDING | IN_PROGRESS | DONE | FAILED
|
|
53
|
+
connector: gemini # which connector is needed: gemini | gitlab | rovo | local-git
|
|
54
|
+
created_by: oscar
|
|
55
|
+
created_at: 2026-06-08T01:00:00Z
|
|
56
|
+
expires_at: 2026-06-08T02:00:00Z
|
|
57
|
+
intent: >
|
|
58
|
+
One sentence describing what needs to happen.
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Context
|
|
62
|
+
|
|
63
|
+
[Everything the processing agent needs to understand and execute the task.]
|
|
64
|
+
|
|
65
|
+
## Expected output
|
|
66
|
+
|
|
67
|
+
[What should be written to outbox/ — format, length, key fields.]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## LOCK file
|
|
73
|
+
|
|
74
|
+
Prevents concurrent bridge writes. Acquired before writing to inbox; released when done.
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
AGENT: claude-code
|
|
78
|
+
USER: oscar
|
|
79
|
+
ACQUIRED: 2026-06-08T01:00:00Z
|
|
80
|
+
EXPIRES: 2026-06-08T01:30:00Z
|
|
81
|
+
INTENT: running gemini-review task oscar-01j2k3l4m5n6
|
|
82
|
+
CONNECTOR: gemini
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If LOCK is present and not expired: wait or fail. If LOCK is expired: it can be overwritten.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Autonomy zones (from conventions/permission-guidance.md)
|
|
90
|
+
|
|
91
|
+
| Zone | Operations | Approval needed |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| 1 | All reads, git status/log/diff, tests, linting | None |
|
|
94
|
+
| 2 | Edit + commit any file in repo (non-main branch) | Notify in same turn |
|
|
95
|
+
| 3 | First commit to a sibling repo | State intent, wait one exchange |
|
|
96
|
+
| 4 | git push, main branch changes, API/gemspec changes | Always confirm |
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Assumptions Log
|
|
2
|
+
|
|
3
|
+
Dated log of assumptions made during development. Items are verified, marked STALE when
|
|
4
|
+
superseded, or flagged NEEDS_REVIEW when they depend on external state that may change.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Format
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
[YYYY-MM-DD] [STATUS] Assumption text
|
|
12
|
+
Source: where this came from
|
|
13
|
+
Verified-by: how it was confirmed (or "not yet verified")
|
|
14
|
+
Notes: anything relevant
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Status values: `VERIFIED` | `NEEDS_REVIEW` | `STALE` | `REFUTED`
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Assumptions
|
|
22
|
+
|
|
23
|
+
<!-- Add entries below -- most recent first -->
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# bridge-inbox-check.sh -- session-start actuator for the bridge inbox.
|
|
3
|
+
#
|
|
4
|
+
# Closes an actuation-coverage gap: unread cross-project bridge messages used to be
|
|
5
|
+
# visible only if someone manually ran a command. This is the trigger/clock that
|
|
6
|
+
# surfaces them automatically at session start, mirroring the standards-inbox report
|
|
7
|
+
# line (see CLAUDE.md and standards/governance/standards-request-protocol.md).
|
|
8
|
+
#
|
|
9
|
+
# DETERMINISTIC and zero-LLM: it just scans .ai-assistance/bridge/inbox/ for unread
|
|
10
|
+
# messages (STATUS: PENDING) and prints one summary line plus one line per message:
|
|
11
|
+
#
|
|
12
|
+
# [bridge] N unread message(s)
|
|
13
|
+
# - {title} from {FROM}
|
|
14
|
+
#
|
|
15
|
+
# Contract (mirrors the other hook scripts):
|
|
16
|
+
# - Silent (no output) and exit 0 when the inbox is empty or has no unread messages.
|
|
17
|
+
# - NEVER fails fatally -- always exits 0, even on a malformed file or missing dir.
|
|
18
|
+
# - No side effects: read-only, does not touch or move any message.
|
|
19
|
+
#
|
|
20
|
+
# --verbose (added 2026-07-23, dashboard-cockpit-v5): when the bridge dir HAS been
|
|
21
|
+
# adopted (inbox/ exists) but there is nothing to report, prints one friendly
|
|
22
|
+
# "[bridge] inbox clear" line instead of nothing -- a manual click with no output
|
|
23
|
+
# otherwise reads as "did this even run?". Bridge-not-adopted (no inbox dir at all)
|
|
24
|
+
# stays silent even with --verbose. DEFAULT (no flag) behavior is UNCHANGED: the
|
|
25
|
+
# SessionStart hook below still calls this with no arguments.
|
|
26
|
+
#
|
|
27
|
+
# Wired as a SessionStart hook in .claude/settings.json:
|
|
28
|
+
# bash scripts/bridge-inbox-check.sh 2>/dev/null || true
|
|
29
|
+
#
|
|
30
|
+
# Manual run (same output): bash scripts/bridge-inbox-check.sh
|
|
31
|
+
# Manual run, friendly-on-empty: bash scripts/bridge-inbox-check.sh --verbose
|
|
32
|
+
set -u
|
|
33
|
+
|
|
34
|
+
VERBOSE=0
|
|
35
|
+
[ "${1:-}" = "--verbose" ] && VERBOSE=1
|
|
36
|
+
reported=0
|
|
37
|
+
|
|
38
|
+
# Resolve the inbox by walking up from this script's OWN location, so it works whether
|
|
39
|
+
# the script is deployed at <root>/scripts/ (ep-ai-standards itself) or at
|
|
40
|
+
# <root>/.ai-assistance/scripts/ (the standard fleet deploy target) -- and from any cwd.
|
|
41
|
+
# A fixed "$(dirname "$0")/.." only works for the first layout; in the deploy target it
|
|
42
|
+
# resolves to <root>/.ai-assistance/.ai-assistance/bridge/inbox and silently finds nothing.
|
|
43
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)" || exit 0
|
|
44
|
+
INBOX=""
|
|
45
|
+
d="$SCRIPT_DIR"
|
|
46
|
+
for _ in 1 2 3 4 5; do
|
|
47
|
+
if [ -d "$d/.ai-assistance/bridge/inbox" ]; then INBOX="$d/.ai-assistance/bridge/inbox"; break; fi
|
|
48
|
+
d="$(dirname "$d")"
|
|
49
|
+
done
|
|
50
|
+
|
|
51
|
+
# No inbox -> nothing to report.
|
|
52
|
+
[ -n "$INBOX" ] && [ -d "$INBOX" ] || exit 0
|
|
53
|
+
|
|
54
|
+
# An "unread" message is a *.md file whose header carries STATUS: PENDING.
|
|
55
|
+
# (PENDING = not yet picked up by the receiving agent; IN_PROGRESS/DONE/FAILED are read.)
|
|
56
|
+
unread=""
|
|
57
|
+
count=0
|
|
58
|
+
|
|
59
|
+
for f in "$INBOX"/*.md; do
|
|
60
|
+
# Guard against the literal glob when the folder is empty.
|
|
61
|
+
[ -e "$f" ] || continue
|
|
62
|
+
|
|
63
|
+
# Only inspect the header block (first ~15 lines) -- cheap and format-stable.
|
|
64
|
+
header="$(head -n 15 "$f" 2>/dev/null || true)"
|
|
65
|
+
|
|
66
|
+
# Unread == STATUS: PENDING (case-insensitive, tolerant of surrounding whitespace).
|
|
67
|
+
printf '%s\n' "$header" | grep -Eiq '^[[:space:]]*STATUS:[[:space:]]*PENDING[[:space:]]*$' || continue
|
|
68
|
+
|
|
69
|
+
# Title: the text after "# TASK:" on line 1; fall back to the filename.
|
|
70
|
+
title="$(printf '%s\n' "$header" \
|
|
71
|
+
| grep -Ei '^#[[:space:]]*TASK:' \
|
|
72
|
+
| head -n 1 \
|
|
73
|
+
| sed -E 's/^#[[:space:]]*TASK:[[:space:]]*//I' 2>/dev/null || true)"
|
|
74
|
+
[ -n "$title" ] || title="$(basename "$f" .md)"
|
|
75
|
+
|
|
76
|
+
# Sender: the text after "FROM:"; fall back to "unknown".
|
|
77
|
+
from="$(printf '%s\n' "$header" \
|
|
78
|
+
| grep -Ei '^[[:space:]]*FROM:' \
|
|
79
|
+
| head -n 1 \
|
|
80
|
+
| sed -E 's/^[[:space:]]*FROM:[[:space:]]*//I' 2>/dev/null || true)"
|
|
81
|
+
[ -n "$from" ] || from="unknown"
|
|
82
|
+
|
|
83
|
+
count=$((count + 1))
|
|
84
|
+
unread="${unread}- ${title} from ${from}
|
|
85
|
+
"
|
|
86
|
+
done
|
|
87
|
+
|
|
88
|
+
# Report unread inbox messages (if any).
|
|
89
|
+
if [ "$count" -gt 0 ]; then
|
|
90
|
+
printf '[bridge] %d unread message(s)\n' "$count"
|
|
91
|
+
printf '%s' "$unread"
|
|
92
|
+
reported=1
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# Stranded-message guard: a STATUS: PENDING file in archive/ is a lifecycle contradiction --
|
|
96
|
+
# a message consumed-and-archived without being flipped to DONE (or archived before it was
|
|
97
|
+
# processed). It is invisible to the inbox scan above, so it silently looks handled. Surface it.
|
|
98
|
+
ARCHIVE="$(dirname "$INBOX")/archive"
|
|
99
|
+
stranded=0
|
|
100
|
+
if [ -d "$ARCHIVE" ]; then
|
|
101
|
+
for f in "$ARCHIVE"/*.md; do
|
|
102
|
+
[ -e "$f" ] || continue
|
|
103
|
+
head -n 15 "$f" 2>/dev/null | grep -Eiq '^[[:space:]]*STATUS:[[:space:]]*PENDING[[:space:]]*$' || continue
|
|
104
|
+
if [ "$stranded" -eq 0 ]; then
|
|
105
|
+
printf '[bridge] WARNING: STATUS: PENDING file(s) in archive/ (stranded -- consumed without closing the loop, or archived before processing):\n'
|
|
106
|
+
fi
|
|
107
|
+
stranded=$((stranded + 1))
|
|
108
|
+
printf ' - %s\n' "$(basename "$f")"
|
|
109
|
+
reported=1
|
|
110
|
+
done
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# --verbose, nothing to report: one friendly line so a manual click doesn't read as
|
|
114
|
+
# "did this even run?".
|
|
115
|
+
if [ "$VERBOSE" -eq 1 ] && [ "$reported" -eq 0 ]; then
|
|
116
|
+
printf '[bridge] inbox clear -- no unread messages, no stranded archive entries.\n'
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
exit 0
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# bridge-init.sh
|
|
3
|
+
# Detects environment, scans bridge/inbox for pending tasks belonging to current user.
|
|
4
|
+
# Output: single JSON line for the agent to consume.
|
|
5
|
+
# Usage: source .ai-assistance/scripts/bridge-init.sh
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
BRIDGE_DIR="$SCRIPT_DIR/../bridge"
|
|
11
|
+
INBOX_DIR="$BRIDGE_DIR/inbox"
|
|
12
|
+
|
|
13
|
+
# --- Environment detection ---
|
|
14
|
+
# Prefer explicit env var; fall back to "code" (this script is most useful in Code)
|
|
15
|
+
ENV="${CLAUDE_ENV:-code}"
|
|
16
|
+
|
|
17
|
+
# --- Username detection ---
|
|
18
|
+
# Prefer explicit env var; fall back to git config
|
|
19
|
+
if [ -n "${BRIDGE_USER:-}" ]; then
|
|
20
|
+
USER_SLUG="$BRIDGE_USER"
|
|
21
|
+
else
|
|
22
|
+
GIT_USER=$(git config user.name 2>/dev/null || echo "unknown")
|
|
23
|
+
USER_SLUG=$(echo "$GIT_USER" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd '[:alnum:]-')
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# --- Stale threshold (seconds) ---
|
|
27
|
+
# IN_PROGRESS tasks older than this are flagged as stale (likely a crashed session).
|
|
28
|
+
STALE_THRESHOLD_SECONDS=3600 # 1 hour
|
|
29
|
+
|
|
30
|
+
# --- Scan inbox ---
|
|
31
|
+
pending_tasks=()
|
|
32
|
+
stale_tasks=()
|
|
33
|
+
|
|
34
|
+
if [ -d "$INBOX_DIR" ]; then
|
|
35
|
+
while IFS= read -r -d '' file; do
|
|
36
|
+
filename=$(basename "$file")
|
|
37
|
+
# Only files prefixed with this user's slug
|
|
38
|
+
if [[ "$filename" == ${USER_SLUG}-* ]]; then
|
|
39
|
+
if grep -q "^STATUS: PENDING" "$file" 2>/dev/null; then
|
|
40
|
+
pending_tasks+=("$filename")
|
|
41
|
+
elif grep -q "^STATUS: IN_PROGRESS" "$file" 2>/dev/null; then
|
|
42
|
+
# Check how old the file is
|
|
43
|
+
if command -v stat &>/dev/null; then
|
|
44
|
+
# GNU stat (Linux/Git-Bash): -c %Y gives mtime as epoch seconds
|
|
45
|
+
# macOS stat uses -f %m — try GNU first, fall back gracefully
|
|
46
|
+
mtime=$(stat -c %Y "$file" 2>/dev/null || stat -f %m "$file" 2>/dev/null || echo 0)
|
|
47
|
+
now=$(date +%s)
|
|
48
|
+
age=$(( now - mtime ))
|
|
49
|
+
if [ "$age" -ge "$STALE_THRESHOLD_SECONDS" ]; then
|
|
50
|
+
stale_tasks+=("$filename")
|
|
51
|
+
fi
|
|
52
|
+
fi
|
|
53
|
+
# If stat is unavailable we silently skip stale detection for this file
|
|
54
|
+
fi
|
|
55
|
+
fi
|
|
56
|
+
done < <(find "$INBOX_DIR" -maxdepth 1 -name "*.md" \
|
|
57
|
+
-not -name "*.draft.md" \
|
|
58
|
+
-not -name "*.local.md" \
|
|
59
|
+
-not -name ".gitkeep" \
|
|
60
|
+
-print0 2>/dev/null)
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# --- Build JSON output ---
|
|
64
|
+
count=${#pending_tasks[@]}
|
|
65
|
+
stale_count=${#stale_tasks[@]}
|
|
66
|
+
|
|
67
|
+
tasks_json="["
|
|
68
|
+
for i in "${!pending_tasks[@]}"; do
|
|
69
|
+
tasks_json+="\"${pending_tasks[$i]}\""
|
|
70
|
+
[ $i -lt $((count - 1)) ] && tasks_json+=","
|
|
71
|
+
done
|
|
72
|
+
tasks_json+="]"
|
|
73
|
+
|
|
74
|
+
stale_json="["
|
|
75
|
+
for i in "${!stale_tasks[@]}"; do
|
|
76
|
+
stale_json+="\"${stale_tasks[$i]}\""
|
|
77
|
+
[ $i -lt $((stale_count - 1)) ] && stale_json+=","
|
|
78
|
+
done
|
|
79
|
+
stale_json+="]"
|
|
80
|
+
|
|
81
|
+
output="{\"env\":\"$ENV\",\"user\":\"$USER_SLUG\",\"pending\":$count,\"tasks\":$tasks_json,\"stale\":$stale_count,\"stale_tasks\":$stale_json}"
|
|
82
|
+
|
|
83
|
+
# Write to status file so Code can read it on startup (hook stdout is not injected into context)
|
|
84
|
+
echo "$output" > "$BRIDGE_DIR/STATUS"
|
|
85
|
+
|
|
86
|
+
echo "$output"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# confine-to-subtree.sh -- PreToolUse hook (matcher: Bash) for a SUBFOLDER-SCOPED
|
|
3
|
+
# deployment. Denies Bash commands that WRITE or DELETE outside the project subtree
|
|
4
|
+
# (the cwd Claude was launched from, e.g. <core-repo>/doc/scripts), so the AI cannot
|
|
5
|
+
# modify the rest of the host repo. READS anywhere are allowed (the scripts framework
|
|
6
|
+
# needs to read the app's models); git is allowed (it manages the repo, incl the
|
|
7
|
+
# parent .git). Standard: standards/tooling/subfolder-deployment.md.
|
|
8
|
+
#
|
|
9
|
+
# Structured deny + exit 0 (holds under --dangerously-skip-permissions). Reads JSON via
|
|
10
|
+
# python -c (a heredoc would steal stdin). Fails OPEN on any parse error. Heuristic
|
|
11
|
+
# (command-string) confinement -- pair with settings.deny Edit/Write(../**), which
|
|
12
|
+
# reliably confines Claude's own file tools, for defense-in-depth.
|
|
13
|
+
PY=python3
|
|
14
|
+
command -v python3 >/dev/null 2>&1 || PY=python
|
|
15
|
+
exec "$PY" -c "
|
|
16
|
+
import sys, json, re
|
|
17
|
+
try:
|
|
18
|
+
data = json.loads(sys.stdin.read())
|
|
19
|
+
except Exception:
|
|
20
|
+
sys.exit(0)
|
|
21
|
+
if data.get(\"tool_name\") != \"Bash\":
|
|
22
|
+
sys.exit(0)
|
|
23
|
+
cmd = (data.get(\"tool_input\") or {}).get(\"command\", \"\") or \"\"
|
|
24
|
+
cwd = (data.get(\"cwd\") or \"\").replace(chr(92), \"/\").rstrip(\"/\")
|
|
25
|
+
|
|
26
|
+
def deny(reason):
|
|
27
|
+
print(json.dumps({\"hookSpecificOutput\": {\"hookEventName\": \"PreToolUse\",
|
|
28
|
+
\"permissionDecision\": \"deny\", \"permissionDecisionReason\": reason}}))
|
|
29
|
+
sys.exit(0)
|
|
30
|
+
|
|
31
|
+
# git manages the whole repo (incl the parent .git); allow it. Commit-scope discipline
|
|
32
|
+
# (git add <subtree>) is a documented convention, not enforced here.
|
|
33
|
+
if re.match(r\"^\s*git\b\", cmd):
|
|
34
|
+
sys.exit(0)
|
|
35
|
+
|
|
36
|
+
WRITE = r\"(?:\brm\b|\brmdir\b|\bmv\b|\bcp\b|\bdd\b|\btee\b|\btruncate\b|\binstall\b|\bmkdir\b|\bln\b|>>?)\"
|
|
37
|
+
if not re.search(WRITE, cmd):
|
|
38
|
+
sys.exit(0) # read-only command -> always allowed
|
|
39
|
+
|
|
40
|
+
# Signal 1: a parent-escaping relative path (../) in a write command.
|
|
41
|
+
if re.search(r\"\.\./\", cmd):
|
|
42
|
+
deny(\"Blocked: write/delete escapes the project subtree via ../ (subfolder-scoped deploy).\")
|
|
43
|
+
|
|
44
|
+
# Signal 2: an absolute path token not under cwd (and not a temp dir).
|
|
45
|
+
ALLOWED = (\"/tmp/\", \"/var/tmp/\", \"/private/tmp/\", \"/var/folders/\", \"/private/var/folders/\")
|
|
46
|
+
for raw in cmd.split():
|
|
47
|
+
t = raw.strip(chr(34)).strip(chr(39)).replace(chr(92), \"/\")
|
|
48
|
+
if not (t.startswith(\"/\") or re.match(r\"^[A-Za-z]:/\", t)):
|
|
49
|
+
continue
|
|
50
|
+
if any(t.startswith(a) for a in ALLOWED):
|
|
51
|
+
continue
|
|
52
|
+
if cwd and (t == cwd or t.startswith(cwd + \"/\")):
|
|
53
|
+
continue # inside the subtree
|
|
54
|
+
deny(\"Blocked: write/delete targets an absolute path outside the project subtree \"
|
|
55
|
+
\"(subfolder-scoped deploy): \" + raw)
|
|
56
|
+
|
|
57
|
+
sys.exit(0) # writes appear to stay within the subtree -> allow
|
|
58
|
+
"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# dirty-tree-guard.sh -- session hook that warns about a dirty tree ON THE DEFAULT BRANCH.
|
|
3
|
+
#
|
|
4
|
+
# Closes a recurring hygiene gap (Oscar-flagged, memory feedback-keep-main-tree-clean):
|
|
5
|
+
# loose, uncommitted files left behind on main/master -- often in a sibling repo after
|
|
6
|
+
# agent work. The rule is: work happens on a feature branch; the default branch stays
|
|
7
|
+
# clean. This surfaces a violation the moment it exists, at session start and stop.
|
|
8
|
+
#
|
|
9
|
+
# DETERMINISTIC and zero-LLM. It warns ONLY when BOTH are true:
|
|
10
|
+
# (a) HEAD is the repo DEFAULT branch (main or master -- DETECTED, not hardcoded), AND
|
|
11
|
+
# (b) git status --porcelain is non-empty (the tree has uncommitted changes).
|
|
12
|
+
# On a feature branch a dirty tree is normal mid-work -> stay SILENT.
|
|
13
|
+
#
|
|
14
|
+
# [dirty-tree-guard] N uncommitted change(s) on <branch> (default branch):
|
|
15
|
+
# - path/one
|
|
16
|
+
# ... (+M more)
|
|
17
|
+
# hint: move work to a feature branch (bash .ai-assistance/scripts/new-feature-branch.sh feat/<slug>) before committing.
|
|
18
|
+
#
|
|
19
|
+
# Contract (mirrors the other hook scripts):
|
|
20
|
+
# - Silent (no output) and exit 0 when clean, or when NOT on the default branch.
|
|
21
|
+
# - Safe outside a git repo: detects and exits 0 silently.
|
|
22
|
+
# - NEVER fails fatally -- always exits 0 (advisory only). Read-only, no side effects.
|
|
23
|
+
#
|
|
24
|
+
# Wired as a SessionStart AND Stop hook in .claude/settings.json:
|
|
25
|
+
# bash .ai-assistance/scripts/dirty-tree-guard.sh 2>/dev/null || true
|
|
26
|
+
#
|
|
27
|
+
# Manual run (same output): bash scripts/dirty-tree-guard.sh
|
|
28
|
+
set -u
|
|
29
|
+
|
|
30
|
+
# Cap on how many loose paths to list before summarising the remainder.
|
|
31
|
+
MAX_PATHS=15
|
|
32
|
+
|
|
33
|
+
# git present? If not, this is not a context we can (or should) inspect.
|
|
34
|
+
command -v git >/dev/null 2>&1 || exit 0
|
|
35
|
+
|
|
36
|
+
# Inside a git work tree? Run from wherever the session cwd is; git resolves the repo.
|
|
37
|
+
# Outside any repo this prints "false" (or errors) -> exit 0 silently.
|
|
38
|
+
in_tree="$(git rev-parse --is-inside-work-tree 2>/dev/null || true)"
|
|
39
|
+
[ "$in_tree" = "true" ] || exit 0
|
|
40
|
+
|
|
41
|
+
# Current branch. Detached HEAD -> empty -> treat as "not the default branch" -> silent.
|
|
42
|
+
branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
43
|
+
[ -n "$branch" ] && [ "$branch" != "HEAD" ] || exit 0
|
|
44
|
+
|
|
45
|
+
# Detect the DEFAULT branch -- never hardcode. Preference order:
|
|
46
|
+
# 1. origin/HEAD symbolic ref (the remote default, e.g. origin/main)
|
|
47
|
+
# 2. a local main, then master, if either exists
|
|
48
|
+
# Fall back to empty (no detectable default) -> silent, so we never warn on a repo
|
|
49
|
+
# whose default we could not establish.
|
|
50
|
+
default=""
|
|
51
|
+
remote_head="$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || true)"
|
|
52
|
+
if [ -n "$remote_head" ]; then
|
|
53
|
+
# Strip the leading origin/ prefix to get the bare branch name.
|
|
54
|
+
default="${remote_head#origin/}"
|
|
55
|
+
fi
|
|
56
|
+
if [ -z "$default" ]; then
|
|
57
|
+
if git show-ref --verify --quiet refs/heads/main 2>/dev/null; then
|
|
58
|
+
default="main"
|
|
59
|
+
elif git show-ref --verify --quiet refs/heads/master 2>/dev/null; then
|
|
60
|
+
default="master"
|
|
61
|
+
fi
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# Not on the default branch -> a dirty tree is expected mid-work -> silent.
|
|
65
|
+
[ -n "$default" ] && [ "$branch" = "$default" ] || exit 0
|
|
66
|
+
|
|
67
|
+
# On the default branch: is the tree dirty?
|
|
68
|
+
status="$(git status --porcelain 2>/dev/null || true)"
|
|
69
|
+
[ -n "$status" ] || exit 0
|
|
70
|
+
|
|
71
|
+
# Dirty on default. Count and list (capped).
|
|
72
|
+
total="$(printf '%s\n' "$status" | grep -c . || true)"
|
|
73
|
+
[ -n "$total" ] || total=0
|
|
74
|
+
[ "$total" -gt 0 ] || exit 0
|
|
75
|
+
|
|
76
|
+
printf '[dirty-tree-guard] %s uncommitted change(s) on %s (default branch):\n' "$total" "$branch"
|
|
77
|
+
|
|
78
|
+
shown=0
|
|
79
|
+
while IFS= read -r line; do
|
|
80
|
+
[ -n "$line" ] || continue
|
|
81
|
+
# Porcelain v1 line: 2-char status, a space, then the path. Strip the status prefix.
|
|
82
|
+
path="$(printf '%s' "$line" | cut -c4-)"
|
|
83
|
+
printf ' - %s\n' "$path"
|
|
84
|
+
shown=$((shown + 1))
|
|
85
|
+
[ "$shown" -ge "$MAX_PATHS" ] && break
|
|
86
|
+
done <<INNER_EOF
|
|
87
|
+
$status
|
|
88
|
+
INNER_EOF
|
|
89
|
+
|
|
90
|
+
if [ "$total" -gt "$shown" ]; then
|
|
91
|
+
printf ' ... (+%s more)\n' "$((total - shown))"
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
printf 'hint: move work to a feature branch (bash .ai-assistance/scripts/new-feature-branch.sh feat/<slug>) before committing, then keep %s clean.\n' "$branch"
|
|
95
|
+
|
|
96
|
+
exit 0
|