Skip to content

Commit 03f71e2

Browse files
saravmajesticclaude
andcommitted
fix: team rules hook matches link-cache keys only; privacy and test docs
- The link-cache check matches a folder only as a JSON key (quoted path then `:`), escaped for the regular expression, so a folder named only as another row's `projectPath` value does not start the CLI. New fixtures: a folder named only as a value, a dot in a folder name, and a pretty-printed cache. - PRIVACY.md: team rules are given only while signed in as the account that pulled them; the rules state is in the user state directory from the altimate-code release that moves it there (earlier releases keep it in the project). Adds `rules_command`, says session events carry the session id, and names `ALTIMATE_LEARN_MODEL` ahead of `learn.model`. - CI: a workflow runs `tests/*.sh` on Linux and macOS; CONTRIBUTING lists the tests and the current layout. - The hook header says which switch gates what. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent de9d490 commit 03f71e2

6 files changed

Lines changed: 77 additions & 15 deletions

File tree

‎.github/workflows/tests.yml‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: tests
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [main]
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
shell:
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
os: [ubuntu-latest, macos-latest]
17+
runs-on: ${{ matrix.os }}
18+
steps:
19+
- uses: actions/checkout@v4
20+
- name: Install zsh
21+
if: runner.os == 'Linux'
22+
run: sudo apt-get update && sudo apt-get install -y zsh
23+
- name: Run shell tests
24+
run: |
25+
set -e
26+
for t in tests/*.sh; do
27+
echo "::group::$t"
28+
bash "$t"
29+
echo "::endgroup::"
30+
done

‎CONTRIBUTING.md‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,19 @@ altimate-codex-plugin/
2626
│ │ └── SKILL.md # dbt/SQL PR reviewer routing
2727
│ ├── commands/
2828
│ │ ├── altimate.md # /altimate-code:altimate <task>
29-
│ │ └── review.md # /altimate-code:review [scope]
29+
│ │ ├── review.md # /altimate-code:review [scope]
30+
│ │ └── rules.md # /altimate-code:rules
3031
│ ├── hooks/
31-
│ │ ├── hooks.json # SessionStart hook definition
32-
│ │ └── altimate-code-session-start.sh # SessionStart hook script
32+
│ │ ├── hooks.json # SessionStart, UserPromptSubmit and Stop hook definitions
33+
│ │ ├── altimate-code-session-start.sh # SessionStart capability description
34+
│ │ └── altimate-code-workspace-rules.sh # team rules hooks (session-start, user-prompt-submit, stop)
3335
│ ├── README.md # plugin-specific docs
3436
│ └── CHANGELOG.md # plugin-specific changelog
37+
├── .github/workflows/
38+
│ └── tests.yml # runs tests/*.sh on Linux and macOS
39+
├── tests/
40+
│ ├── resolve-cli.sh # CLI resolver block check
41+
│ └── workspace-rules-hook.sh # workspace-rules hook check
3542
├── README.md # repo overview
3643
├── PRIVACY.md
3744
├── TERMS.md
@@ -73,7 +80,12 @@ description: >-
7380

7481
## Testing changes
7582

76-
If you touch the CLI resolver block in a skill or command, run `./tests/resolve-cli.sh` — it checks every copy is identical and resolves correctly in bash and zsh.
83+
Run the shell tests before opening a PR (CI runs them too, on Linux and macOS):
84+
85+
```bash
86+
bash tests/resolve-cli.sh # the CLI resolver block: every copy identical, resolves in bash and zsh
87+
bash tests/workspace-rules-hook.sh # the workspace-rules hook against a stub CLI (needs python3 and git)
88+
```
7789

7890
Smoke-install the plugin from your working copy:
7991

@@ -100,6 +112,8 @@ codex plugin marketplace remove altimate-codex-plugin
100112
- **Command** — `plugins/altimate-code/commands/altimate.md` — `/altimate-code:altimate <task>` explicit-delegation slash command.
101113
- **Command** — `plugins/altimate-code/commands/review.md` — `/altimate-code:review [scope]` explicit-review slash command.
102114
- **Hook** — `plugins/altimate-code/hooks/hooks.json` + `plugins/altimate-code/hooks/altimate-code-session-start.sh` — SessionStart hook that publishes a static capability description of both skills so Codex can route between them when the task shape fits.
115+
- **Command** — `plugins/altimate-code/commands/rules.md` — `/altimate-code:rules` to answer proposed team rules.
116+
- **Hooks** — `plugins/altimate-code/hooks/altimate-code-workspace-rules.sh` — the team rules hooks (SessionStart, UserPromptSubmit, Stop), which call `altimate-code learn hook <event>` only in a folder altimate-code's link cache names, and never fail the session.
103117

104118
## Reporting bugs / requesting features
105119

‎PRIVACY.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,16 @@ Data leaves Codex's process only when the delegation skill, the `/altimate-code:
3232
- *Recording corrections and drafting proposed rules* also need learning on. Learning is on when you turned it on in the project (`altimate learn enable`) or, if you have made no choice of your own, when the workspace owner turned on the workspace's "Learn from sessions" setting. Your own choice (`altimate learn enable` or `altimate learn disable`) overrides the workspace setting, unless your organization enforces a policy.
3333
- In a linked folder, the per-turn hooks hand each message you send and the location of the session transcript to the local CLI whether or not learning is on. When learning is off, you are signed out or the project is not linked, the CLI discards them and nothing leaves your machine.
3434

35-
**The digest used to word a proposed rule.** After a turn with new corrections, the CLI reads the Codex session transcript (its last 8 MB) and builds a digest of the session: your prompts, the input and output of each tool call (up to 300 and 400 characters per call, so file contents, command output and warehouse query results can appear), and the final reply, at most 24,000 characters in total. Secrets in the digest are masked; no other data is removed. The digest is sent to altimate-code's own model: the model set as `learn.model` in altimate-code's configuration, otherwise altimate-code's default model, which is the Altimate LLM Gateway or another provider configured for altimate-code. This is not the model Codex uses; it is a party that does not otherwise see your Codex session. If altimate-code has no model it can use, nothing is sent, and the next session start reports that the check failed. The CLI makes at most one such model call per check round, and a few per turn at most.
35+
**The digest used to word a proposed rule.** After a turn with new corrections, the CLI reads the Codex session transcript (its last 8 MB) and builds a digest of the session: your prompts, the input and output of each tool call (up to 300 and 400 characters per call, so file contents, command output and warehouse query results can appear), and the final reply, at most 24,000 characters in total. Secrets in the digest are masked; no other data is removed. The digest is sent to altimate-code's own model: the model named by `ALTIMATE_LEARN_MODEL`, otherwise the one set as `learn.model` in altimate-code's configuration, otherwise altimate-code's default model, which is the Altimate LLM Gateway or another provider configured for altimate-code. This is not the model Codex uses; it is a party that does not otherwise see your Codex session. If altimate-code has no model it can use, nothing is sent, and the next session start reports that the check failed. The CLI makes at most one such model call per check round, and a few per turn at most.
3636

3737
| Data | Where it goes | Retention |
3838
|---|---|---|
39-
| Your workspace's approved team rules | Pulled by the local CLI from your Altimate workspace, given to Codex as session context | Mirrored locally in the project's git-ignored `.altimate-code/` directory, and given only while altimate-code's own rules state (in your state directory, `~/.local/state/altimate-code/`, outside the project) holds the same rules. That state also keeps proposals waiting to be sent and your review notices |
39+
| Your workspace's approved team rules | Pulled by the local CLI from your Altimate workspace, given to Codex as session context | Mirrored locally in the project's git-ignored `.altimate-code/` directory, and given only while altimate-code's own rules state holds the same rules and you are signed in as the account they were pulled for. That state, which also keeps proposals waiting to be sent and your review notices, is in your own state directory (`$XDG_STATE_HOME/altimate-code/workspace-rules/`, usually `~/.local/state/altimate-code/workspace-rules/`), outside the project. This needs an altimate-code release that keeps the rules state there; earlier releases keep it in the project's `.altimate-code/learn/workspace-rules/` folder |
4040
| Each message you send | Handed to the local CLI, which checks it with no model call; with learning on, a message that reads as a correction is kept (secrets redacted) | Local only, in the project's git-ignored `.altimate-code/` directory |
4141
| The session transcript, after a turn with new corrections (learning on) | Read locally by the CLI; the digest described above goes to altimate-code's own model to word one proposed rule | The transcript stays local; the model provider's handling is as for delegated tasks below |
4242
| A proposed rule, with an excerpt of your correction if your workspace allows quotes | Your Altimate workspace's review queue, only when you send or approve it in `/altimate-code:rules` or `altimate learn proposals` | Per the Altimate Privacy Policy |
4343
| Rule usage counts (sessions a rule was shown in, corrections after it) | Your Altimate workspace | Per the Altimate Privacy Policy |
44-
| Rules telemetry events: `rules_sync`, `rules_delivered`, `rule_drafted`, `rule_proposal_shown` (with the Codex session id, as `codex:<id>`), `rule_proposal_answered`, `rule_proposal_skipped`, `rule_remember_routed`. No rule text, prompts or file paths | Altimate's usage telemetry, as described in [Usage telemetry](#usage-telemetry) | As for usage telemetry; turned off with the telemetry opt-out there |
44+
| Rules telemetry events: `rules_sync`, `rules_delivered`, `rule_drafted`, `rule_proposal_shown`, `rule_proposal_answered`, `rule_proposal_skipped`, `rule_remember_routed`, and `rules_command` (the name of each `learn` subcommand you run, such as `learn proposals`; not sent for the hooks' own calls). Events about a session carry its id, `codex:<id>` for a Codex session. No rule text, prompts or file paths | Altimate's usage telemetry, as described in [Usage telemetry](#usage-telemetry) | As for usage telemetry; turned off with the telemetry opt-out there |
4545

4646
Turn off recording and drafting with `altimate learn disable` in the project, or, for members who have made no choice of their own, with the workspace's "Learn from sessions" setting. `ALTIMATE_LEARN=0` turns off all of it, including the team rules pull, and the hooks then start nothing.
4747

‎plugins/altimate-code/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ In a project linked to an Altimate workspace, the workspace's approved **team ru
4949
- **"Remember this as a team rule: …"**, said to Codex or in a task handed to altimate-code, waits as a proposal the same way. It needs the words "team rule" (or "as a rule"): "remember to run the tests" is just part of the conversation.
5050
- **`/altimate-code:rules`** shows the proposals waiting for you, the team rules, and the status of the rules you proposed. For each proposal you choose: send it to the workspace owner's review (or approve it, if you are the owner), edit the wording, save it as a memory, or "Not a rule". Nothing reaches teammates until the owner approves it.
5151

52-
Rule drafting uses altimate-code's own model (sign in to the Altimate LLM Gateway, or set a provider in altimate-code): if it has none it can use, the next session start says the check failed. The same commands work in a terminal: `altimate learn proposals`, `altimate learn proposals reply <id> --send`. Proposals from Codex sessions are answered only there or with `/altimate-code:rules`: altimate-code's own `/rules` and its VS Code chat panel don't list them. This needs an altimate-code release with workspace rules.
52+
Rule drafting uses altimate-code's own model (sign in to the Altimate LLM Gateway, or set a provider in altimate-code): if it has none it can use, the next session start says the check failed. The same commands work in a terminal: `altimate learn proposals`, `altimate learn proposals reply <id> --send` (`altimate` and `altimate-code` are two names for the same CLI; the hooks call `altimate-code learn hook`). Proposals from Codex sessions are answered only there or with `/altimate-code:rules`: altimate-code's own `/rules` and its VS Code chat panel don't list them. This needs an altimate-code release with workspace rules.
5353

5454
- **Start the session in the linked folder**, or a folder below it. If only a folder inside the repository is linked (a dbt project), a session started at the repository root gets no team rules.
5555
- **A new clone or worktree** gets no team rules until altimate-code has been linked, or run, in that checkout: the link is remembered per folder.

‎plugins/altimate-code/hooks/altimate-code-workspace-rules.sh‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
#!/usr/bin/env bash
22
# Workspace rules hook for the altimate-code plugin. In a project linked to an
33
# Altimate workspace, it brings the workspace's team rules into this Codex session
4-
# and, with learning on, lets corrections made here become proposed rules, through
5-
# `altimate-code learn hook <event>` (which applies every gate: learning and
6-
# workspaces on, signed in, linked).
4+
# and, with learning from sessions on, lets corrections made here become proposed
5+
# rules, through `altimate-code learn hook <event>` (`altimate` is the same CLI),
6+
# which applies every gate. Team rules need workspaces on, a sign-in, a link and
7+
# the learning kill switch not off (ALTIMATE_LEARN / learn.enabled). Capturing
8+
# corrections and drafting also need learning from sessions on: your own
9+
# `learn enable` / `learn disable` choice, else the workspace's "Learn from
10+
# sessions" setting.
711
# session-start team rules, proposals waiting for the person and new
812
# reviews, printed as context (one CLI call, a bounded sync)
913
# user-prompt-submit the prompt is recorded if it reads as a correction
@@ -41,8 +45,9 @@ ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
4145

4246
# linked: whether the workspace link cache has a key for the host's directory or a folder above it, up to the
4347
# repository root. This mirrors the CLI, which looks there and never below the directory it runs in. Keys are native
44-
# paths written as JSON strings, so each folder is converted (`cygpath -w` on Windows) and escaped, and matched with
45-
# both quotes, so `/x/app` never matches `/x/app-old`.
48+
# paths written as JSON strings, so each folder is converted (`cygpath -w` on Windows), JSON-escaped, then escaped for
49+
# the regular expression. It is matched with both quotes and the colon after them: `/x/app` never matches `/x/app-old`,
50+
# and a folder that appears only as a value (a row's `projectPath`) is not a key.
4651
linked() {
4752
local cache="${XDG_STATE_HOME:-$HOME/.local/state}/altimate-code/altimate-workspace-bindings.json"
4853
local top dir key
@@ -53,8 +58,8 @@ linked() {
5358
while :; do
5459
key="$dir"
5560
command -v cygpath >/dev/null 2>&1 && key="$(cygpath -w "$dir")"
56-
key="$(printf '%s' "$key" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')"
57-
grep -qF "\"$key\"" "$cache" 2>/dev/null && return 0
61+
key="$(printf '%s' "$key" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/[][\.*^$+?(){}|]/\\&/g')"
62+
grep -qE "\"$key\"[[:space:]]*:" "$cache" 2>/dev/null && return 0
5863
case "$dir" in "$top"|/) return 1 ;; esac
5964
dir="$(dirname "$dir")"
6065
done

‎tests/workspace-rules-hook.sh‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,19 @@ link "$WORK/project-old" "$WORK/projectX/dbt"
114114
reset
115115
[ "$(hook "$WORK/project" session-start "$BIN:$SYS")" = "0|" ] || fail "a sibling-prefix link printed output"
116116
! started || fail "session start ran the CLI for a link to $WORK/project-old"
117+
# A folder named only as a value (another row's `projectPath`), not as a key: not linked.
118+
python3 -c 'import json, sys; print(json.dumps({"bindings": {sys.argv[1] + "/dbt": {"datamateId": 1, "projectPath": sys.argv[1]}}}, indent=2))' "$WORK/project" > "$CACHE"
119+
reset
120+
[ "$(hook "$WORK/project" session-start "$BIN:$SYS")" = "0|" ] || fail "a folder named only as a value printed output"
121+
! started || fail "session start ran the CLI for a folder named only as a projectPath value"
122+
# Pretty-printed keys still match, and a dot in a folder name is not a regular-expression wildcard.
123+
mkdir -p "$WORK/dot.repo" && git -C "$WORK/dot.repo" init -q
124+
link "$WORK/dotXrepo"
125+
reset
126+
[ "$(hook "$WORK/dot.repo" session-start "$BIN:$SYS")" = "0|" ] && ! started || fail "a dot in a folder name matched another character"
127+
python3 -c 'import json, sys; print(json.dumps({"bindings": {sys.argv[1]: {"datamateId": 1}}}, indent=2))' "$WORK/dot.repo" > "$CACHE"
128+
reset
129+
[ "$(hook "$WORK/dot.repo" session-start "$BIN:$SYS")" = "$JSON" ] || fail "a pretty-printed link cache key was not found"
117130
# Folder names with a quote or a backslash are matched as their JSON-escaped keys.
118131
ODD="$WORK/odd\"name\\repo"
119132
mkdir -p "$ODD/sub" && git -C "$ODD" init -q

0 commit comments

Comments
 (0)