@groundnuty/macf 0.2.0-rc.0
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.
- package/dist/.build-info.json +4 -0
- package/dist/cli/build-info.d.ts +38 -0
- package/dist/cli/build-info.d.ts.map +1 -0
- package/dist/cli/build-info.js +119 -0
- package/dist/cli/build-info.js.map +1 -0
- package/dist/cli/claude-sh.d.ts +42 -0
- package/dist/cli/claude-sh.d.ts.map +1 -0
- package/dist/cli/claude-sh.js +247 -0
- package/dist/cli/claude-sh.js.map +1 -0
- package/dist/cli/commands/cd.d.ts +6 -0
- package/dist/cli/commands/cd.d.ts.map +1 -0
- package/dist/cli/commands/cd.js +17 -0
- package/dist/cli/commands/cd.js.map +1 -0
- package/dist/cli/commands/certs.d.ts +33 -0
- package/dist/cli/commands/certs.d.ts.map +1 -0
- package/dist/cli/commands/certs.js +233 -0
- package/dist/cli/commands/certs.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +91 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +235 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/init.d.ts +37 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +279 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/list.d.ts +5 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +21 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/migrate-ca-key.d.ts +36 -0
- package/dist/cli/commands/migrate-ca-key.d.ts.map +1 -0
- package/dist/cli/commands/migrate-ca-key.js +92 -0
- package/dist/cli/commands/migrate-ca-key.js.map +1 -0
- package/dist/cli/commands/peers.d.ts +8 -0
- package/dist/cli/commands/peers.d.ts.map +1 -0
- package/dist/cli/commands/peers.js +45 -0
- package/dist/cli/commands/peers.js.map +1 -0
- package/dist/cli/commands/repo-init.d.ts +43 -0
- package/dist/cli/commands/repo-init.d.ts.map +1 -0
- package/dist/cli/commands/repo-init.js +304 -0
- package/dist/cli/commands/repo-init.js.map +1 -0
- package/dist/cli/commands/rules-refresh.d.ts +14 -0
- package/dist/cli/commands/rules-refresh.d.ts.map +1 -0
- package/dist/cli/commands/rules-refresh.js +67 -0
- package/dist/cli/commands/rules-refresh.js.map +1 -0
- package/dist/cli/commands/self-update.d.ts +14 -0
- package/dist/cli/commands/self-update.d.ts.map +1 -0
- package/dist/cli/commands/self-update.js +112 -0
- package/dist/cli/commands/self-update.js.map +1 -0
- package/dist/cli/commands/status.d.ts +9 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +90 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/update.d.ts +25 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.js +316 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/config.d.ts +103 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +224 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +245 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/plugin-fetcher.d.ts +20 -0
- package/dist/cli/plugin-fetcher.d.ts.map +1 -0
- package/dist/cli/plugin-fetcher.js +83 -0
- package/dist/cli/plugin-fetcher.js.map +1 -0
- package/dist/cli/prompt.d.ts +17 -0
- package/dist/cli/prompt.d.ts.map +1 -0
- package/dist/cli/prompt.js +109 -0
- package/dist/cli/prompt.js.map +1 -0
- package/dist/cli/registry-helper.d.ts +11 -0
- package/dist/cli/registry-helper.d.ts.map +1 -0
- package/dist/cli/registry-helper.js +18 -0
- package/dist/cli/registry-helper.js.map +1 -0
- package/dist/cli/rules.d.ts +39 -0
- package/dist/cli/rules.d.ts.map +1 -0
- package/dist/cli/rules.js +112 -0
- package/dist/cli/rules.js.map +1 -0
- package/dist/cli/settings-writer.d.ts +97 -0
- package/dist/cli/settings-writer.d.ts.map +1 -0
- package/dist/cli/settings-writer.js +270 -0
- package/dist/cli/settings-writer.js.map +1 -0
- package/dist/cli/version-resolver.d.ts +73 -0
- package/dist/cli/version-resolver.d.ts.map +1 -0
- package/dist/cli/version-resolver.js +238 -0
- package/dist/cli/version-resolver.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin/bin/macf-plugin-cli.d.ts +13 -0
- package/dist/plugin/bin/macf-plugin-cli.d.ts.map +1 -0
- package/dist/plugin/bin/macf-plugin-cli.js +127 -0
- package/dist/plugin/bin/macf-plugin-cli.js.map +1 -0
- package/dist/plugin/lib/format.d.ts +40 -0
- package/dist/plugin/lib/format.d.ts.map +1 -0
- package/dist/plugin/lib/format.js +137 -0
- package/dist/plugin/lib/format.js.map +1 -0
- package/dist/plugin/lib/health.d.ts +2 -0
- package/dist/plugin/lib/health.d.ts.map +1 -0
- package/dist/plugin/lib/health.js +6 -0
- package/dist/plugin/lib/health.js.map +1 -0
- package/dist/plugin/lib/index.d.ts +7 -0
- package/dist/plugin/lib/index.d.ts.map +1 -0
- package/dist/plugin/lib/index.js +5 -0
- package/dist/plugin/lib/index.js.map +1 -0
- package/dist/plugin/lib/registry.d.ts +18 -0
- package/dist/plugin/lib/registry.d.ts.map +1 -0
- package/dist/plugin/lib/registry.js +17 -0
- package/dist/plugin/lib/registry.js.map +1 -0
- package/dist/plugin/lib/work.d.ts +13 -0
- package/dist/plugin/lib/work.d.ts.map +1 -0
- package/dist/plugin/lib/work.js +27 -0
- package/dist/plugin/lib/work.js.map +1 -0
- package/package.json +43 -0
- package/plugin/rules/coordination.md +224 -0
- package/scripts/check-gh-token.sh +102 -0
- package/scripts/macf-gh-token.sh +130 -0
- package/scripts/macf-whoami.sh +51 -0
- package/scripts/tmux-send-to-claude.sh +51 -0
- package/scripts/write-build-info.mjs +48 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Coordination Rules (canonical, shared)
|
|
2
|
+
|
|
3
|
+
**This file is the single source of truth for cross-cutting coordination rules that apply to every MACF agent.** It is copied into each agent workspace's `.claude/rules/` by `macf init` and refreshed by `macf update`. Do not edit workspace copies directly — edit this file and re-run `macf update`.
|
|
4
|
+
|
|
5
|
+
> **Workspaces without full `macf init`** (e.g. `groundnuty/macf` itself, or any Claude Code workspace operated by a bot that isn't a MACF-registered agent) can still get these canonical rules via `macf rules refresh --dir <workspace>`. Same copy, no App credentials or registry required.
|
|
6
|
+
|
|
7
|
+
The rules here are topology-agnostic: they work whether the project uses a science-agent coordinator (like macf) or peer-to-peer agents with direct user oversight (like CV).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Issue Lifecycle
|
|
12
|
+
|
|
13
|
+
1. **The reporter owns the issue closure.** The agent who opened an issue is the only one who closes it. This rule has two failure modes — both costly, both silent. Check for both before posting a merge-handoff comment.
|
|
14
|
+
|
|
15
|
+
**Failure mode A — closing an issue you didn't open.** Two ways this happens:
|
|
16
|
+
- *Auto-close via PR keywords.* GitHub's auto-close keywords in a PR body or commit message close the referenced issue on merge, bypassing the reporter. **Never use any of these 9 variants when the issue was filed by someone else:** `Closes #N`, `Fixes #N`, `Resolves #N`, `Close #N`, `Fix #N`, `Resolve #N`, `Closed #N`, `Fixed #N`, `Resolved #N`. Use **`Refs #N`** instead.
|
|
17
|
+
- *Manual close via `gh issue close`.* Don't close someone else's issue even after merging the implementation. Post the handoff comment and stop.
|
|
18
|
+
|
|
19
|
+
**Failure mode B — waiting for yourself to close.** When the issue's reporter is YOU (you filed the issue during an audit, a follow-up split-off, or self-observed bug), there is no one else to close it. Don't post `@<other-agent> ready for you to close when verified` — no one is waiting to do that for you. After your PR merges, close the issue yourself with a verification comment. Silent stall otherwise: the queue fills with in-review issues that never clear.
|
|
20
|
+
|
|
21
|
+
**Self-check before posting any merge-handoff comment:**
|
|
22
|
+
|
|
23
|
+
gh issue view <N> --json author --jq '.author.login'
|
|
24
|
+
|
|
25
|
+
- Author is someone else (user or another agent) → post `@<author> PR #M merged, ready for you to close when verified.` and STOP.
|
|
26
|
+
- Author is YOU (your `app/<bot-name>` login) → close the issue yourself:
|
|
27
|
+
|
|
28
|
+
gh issue close <N> --reason completed --comment "Verified on main after PR #M merged. Closing as reporter."
|
|
29
|
+
|
|
30
|
+
Also self-check PR bodies before pushing:
|
|
31
|
+
|
|
32
|
+
git log -1 --pretty=%B # or the PR body draft
|
|
33
|
+
# grep for any of: Closes Fixes Resolves Close Fix Resolve Closed Fixed Resolved
|
|
34
|
+
|
|
35
|
+
If any of those appear and the referenced issue was filed by someone else, replace with `Refs #N`.
|
|
36
|
+
|
|
37
|
+
**Why this rule matters:** Reporter-owns-closure gives the reporter a chance to verify the fix matches their intent before the issue disappears from their queue. In a multi-agent workflow, the reporter often has context the implementer doesn't (why it was filed at that priority, what the acceptance criteria really meant, what adjacent work it blocks). Auto-close strips that context; reflexive handoff on self-filed issues wastes it.
|
|
38
|
+
|
|
39
|
+
2. **Work through the queue without prompting.** When an issue is complete, check your assigned-label queue and pick up the next one immediately. Do NOT ask the reporter to ping you or reply "continue" before starting. Only wait when (a) your PR is in review, or (b) the queue is empty. If an issue is ambiguous, ask clarifying questions on that issue and move to the next queued one while waiting.
|
|
40
|
+
|
|
41
|
+
3. **Never remove your own agent label.** Status labels (`in-progress`, `in-review`, `blocked`) swap as work moves; assignment labels stay.
|
|
42
|
+
|
|
43
|
+
4. **Issue body is frozen during active work.** Once an assignee has commented "picking up" / added an `in-progress` or `in-review` label / filed a PR referencing the issue, the body **is the assignee's working spec** and should not be edited. Scope corrections, additional requirements, clarifying details, regex fixes — all go as **follow-up comments** in the issue thread.
|
|
44
|
+
|
|
45
|
+
**Why:** editing the body mid-flight either changes the target under the assignee's feet (they started on spec v1, are now reading v2) or is silently lost (they don't re-fetch the body after starting). A thread comment is visible, acknowledged, and dated. Both silent failure modes are worse than the tiny friction of posting a comment.
|
|
46
|
+
|
|
47
|
+
**When body edits ARE fine:**
|
|
48
|
+
|
|
49
|
+
- Before anyone has engaged (issue just-filed, no `in-progress` label, no assignee comments)
|
|
50
|
+
- The assignee is the one editing their own issue body
|
|
51
|
+
- Fixing obvious typos or broken links (not scope)
|
|
52
|
+
|
|
53
|
+
**When body edits are NOT fine:**
|
|
54
|
+
|
|
55
|
+
- Assignee has commented "picking up" / is actively working
|
|
56
|
+
- An `in-progress` / `in-review` label is set
|
|
57
|
+
- A PR referencing the issue is open
|
|
58
|
+
|
|
59
|
+
If a correction is substantive enough that the assignee would want to re-read from scratch, consider closing the current issue and filing a replacement with a clear back-reference — rather than in-place body rewrite.
|
|
60
|
+
|
|
61
|
+
5. **Auto-opened issues break the peer-agent-reporter assumptions.** When an issue is filed by `github-actions[bot]` or any other non-human/non-agent bot, the peer-reviewer-verifies-the-fix loop that rules 1-4 assume doesn't apply — the bot-reporter can't verify, can't be routed-to as a reviewer, and can't sanity-check closure. Three specific adaptations:
|
|
62
|
+
|
|
63
|
+
- **Use `Refs #N`, not `Closes #N`, in the fix PR body.** The auto-close keyword bypasses the verification step the bot-reporter can't perform. Prefer an explicit close after the next post-merge run independently confirms the fix worked — not the PR merge itself, which fires before verification.
|
|
64
|
+
- **Route the review ping to `@macf-science-agent[bot]`** (or whichever peer agent would normally review), **not by echoing the `@<bot-reporter>` mention from the auto-open body.** The auto-open's `@mention` addresses the agent who should FIX, not the one who should REVIEW. Self-mention loops don't fire the routing workflow — the PR sits unreviewed.
|
|
65
|
+
- **Wait for the next auto-run to confirm green, then close with a comment citing the green run's SHA + URL.** If the auto-opening workflow has a self-close-on-green step (e.g., `e2e.yml` has one per #166), trust it — don't pre-empt by closing manually or via PR auto-close keyword. For workflows without a self-close step, close manually only after observing the next run green.
|
|
66
|
+
|
|
67
|
+
**Why this rule matters:** lucky timing isn't a verification gate. If a fix is incomplete, auto-close on PR merge closes the issue 2 seconds before the next run fails anew — producing a misleading closure trail. The "stays open until green" contract the auto-open body sets needs to be honored on the machine-enforced side, not relied on operator-discipline.
|
|
68
|
+
|
|
69
|
+
**Example auto-opened issues in this repo:** E2E cadence failures on `main` (#149 workflow auto-opens a `code-agent`/`blocked` issue with title prefix `ci(e2e): post-merge suite failing on main`); dependency-drift alerts; future `/sign` / cert-rotation health alarms. The pattern generalizes to any workflow that files on failure.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Communication
|
|
74
|
+
|
|
75
|
+
1. **@mention in EVERY comment.** Routing depends on it. A comment without @mention is invisible to the recipient agent.
|
|
76
|
+
|
|
77
|
+
2. **All discussion in issue comments, not PR comments.** Issue threads are visible on the Projects board and persist after PRs are merged or closed.
|
|
78
|
+
|
|
79
|
+
3. **Verify your comment actually posted — describing ≠ doing.** Writing a review / LGTM / close-comment / status-update as prose in your response is NOT the same as posting it to GitHub. Only executed `gh issue comment` / `gh pr comment` / `gh issue close` tool calls reach the repo; chat output is invisible to other agents. Treat the verification step as a **mandatory tail**, not optional, on any review-producing turn.
|
|
80
|
+
|
|
81
|
+
**After any `gh ... comment` / `gh ... close`:**
|
|
82
|
+
|
|
83
|
+
gh issue view <N> --repo <owner>/<repo> --json comments \
|
|
84
|
+
--jq '.comments[-1].author.login'
|
|
85
|
+
|
|
86
|
+
Confirms (a) the comment exists, (b) attribution is correct (bot not user — see Token & Git Hygiene below).
|
|
87
|
+
|
|
88
|
+
**Signs you may have missed the tool call:**
|
|
89
|
+
|
|
90
|
+
- Your last action was describing a review / decision / close in prose
|
|
91
|
+
- The recipient's status comment says "waiting for review" or "ready for you to close" with no reply from you visible on the thread
|
|
92
|
+
- Time has passed since you "reviewed" but no downstream activity (merge, follow-up questions, re-review request) has happened
|
|
93
|
+
|
|
94
|
+
When in doubt, run the `gh issue view` check. Cheap to verify; costly to have the assignee wait on a review that never arrived.
|
|
95
|
+
|
|
96
|
+
4. **Concise comments** — 1-3 sentences unless detail is needed.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## When You're Stuck — Escalation
|
|
101
|
+
|
|
102
|
+
1. **Treat definitive GitHub states as action signals, not wait signals.** For PR merge status, check `gh pr view <N> --json mergeStateStatus,mergeable`:
|
|
103
|
+
- `CLEAN` → merge
|
|
104
|
+
- `UNKNOWN` → GitHub is still computing; wait up to ~60s
|
|
105
|
+
- `DIRTY` / `CONFLICTING` → rebase onto main and resolve conflicts
|
|
106
|
+
- `BEHIND` → rebase onto main, force-push
|
|
107
|
+
- `BLOCKED` → check reviews / required checks / branch protection
|
|
108
|
+
- `UNSTABLE` → a required check failed; fix it
|
|
109
|
+
|
|
110
|
+
Only `UNKNOWN` means "keep waiting." Anything else means your turn to act.
|
|
111
|
+
|
|
112
|
+
**Exception — CI-completion routing (macf-actions v1.3+).** When you receive a CI-completion routing notification (`PR #N: CI SUCCESS/FAILED ...`) and `gh pr view` returns `UNKNOWN` or `UNSTABLE` immediately after, the notification was fired by one workflow's `check_suite.completed` while another workflow on the same commit is still in-flight. The rollup hasn't resolved yet. Wait ~30s and re-query; don't force-merge until the full rollup goes `CLEAN`. See `groundnuty/macf-actions#6` for background.
|
|
113
|
+
|
|
114
|
+
2. **Escalate to the issue reporter.** When you've tried to resolve and are still stuck, @mention the reporter of the issue you're working on:
|
|
115
|
+
|
|
116
|
+
GH_TOKEN=$GH_TOKEN gh issue comment <N> --repo <owner>/<repo> --body "@<reporter> blocked on <X> — tried <Y>, need <Z>."
|
|
117
|
+
|
|
118
|
+
Universal rule: an agent escalates to the entity that tasked it — which is the issue reporter. Same entity that owns closing the issue (see Issue Lifecycle rule 1). This holds across any topology:
|
|
119
|
+
- macf (code-agent → science-agent → user)
|
|
120
|
+
- CV (cv-agents → user directly)
|
|
121
|
+
- Experiments (workers → experiment orchestrator → science-agent)
|
|
122
|
+
|
|
123
|
+
The chain flows from who opened the issue. You don't need to know the topology — you just @mention the reporter.
|
|
124
|
+
|
|
125
|
+
3. **The reporter decides the next step.** They may act directly, involve a coordinator, or bring in the user. Do not reach past the reporter to the user unless the reporter has explicitly said they can't help.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Peer Dynamic
|
|
130
|
+
|
|
131
|
+
You are a peer to the agents and humans you work with, not a subordinate and not a superior.
|
|
132
|
+
|
|
133
|
+
- **Push back** when an issue has wrong scope, missing context, flawed design, or conflicts with prior decisions
|
|
134
|
+
- **Ask clarifying questions** before proceeding on ambiguous requirements — wait for answers
|
|
135
|
+
- **Defend your implementation choices** with concrete reasoning if the reviewer disagrees
|
|
136
|
+
- **Accept valid feedback** and push fixes promptly
|
|
137
|
+
- **Research before implementing** — your training data may be outdated. Look up current SDK/library/API docs (context7, WebSearch, WebFetch) before using them
|
|
138
|
+
|
|
139
|
+
The goal is correctness through dialogue, not compliance.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Submitting a Prompt to a Claude Code TUI (tmux)
|
|
144
|
+
|
|
145
|
+
When a hook or script needs to programmatically submit a prompt to a Claude Code TUI running in tmux, **always use the canonical helper**:
|
|
146
|
+
|
|
147
|
+
.claude/scripts/tmux-send-to-claude.sh <session-or-empty> "<prompt text>"
|
|
148
|
+
|
|
149
|
+
Pass `""` for the session to target the current pane.
|
|
150
|
+
|
|
151
|
+
**Never** call `tmux send-keys "<prompt>" Enter` inline. Claude Code's TUI is in multi-line input mode by default, so a single Enter inserts a newline instead of submitting — the prompt sits in the buffer unsubmitted. The helper handles the submit-quirk correctly: clear existing input with `C-u`, send the text with a first Enter, sleep 1 second (load-bearing — without it tmux batches both Enters and Claude processes them atomically as "newline + newline"), then send a second Enter that actually submits.
|
|
152
|
+
|
|
153
|
+
The helper is distributed to every agent workspace by `macf init` and refreshed by `macf update` (same mechanism as this rules file). If you're writing a new hook or automation that needs to prompt Claude, use the helper — do not re-implement the pattern.
|
|
154
|
+
|
|
155
|
+
### Canonical tmux launch pattern
|
|
156
|
+
|
|
157
|
+
**One session per agent, named `<project>@<agent>`.** For example:
|
|
158
|
+
|
|
159
|
+
tmux new-session -d -s "academic-resume@cv-architect" \
|
|
160
|
+
"cd /path/to/academic-resume && ./claude.sh"
|
|
161
|
+
|
|
162
|
+
tmux new-session -d -s "academic-resume@cv-project-archaeologist" \
|
|
163
|
+
"cd /path/to/academic-resume && ./claude.sh"
|
|
164
|
+
|
|
165
|
+
**Why this matters:** when the channel server's `tmux-wake` path (macf#185) auto-detects its own tmux target via `$TMUX_PANE` or `tmux display-message`, a **shared session with one window per agent** produces ambiguous resolution — two server processes in different windows of the same session can end up with identical auto-detected targets, and wakes land on the wrong pane. Empirically observed during the 2026-04-21 bilateral e2e smoke (chain broke when archaeologist's wake delivered to cv-architect's pane).
|
|
166
|
+
|
|
167
|
+
**One session per agent** gives each server process a deterministic `$TMUX_PANE` + one-window-per-session context where `display-message` can't be ambiguous.
|
|
168
|
+
|
|
169
|
+
**Session-name convention `<project>@<agent>`** is parseable (both human + script-friendly) and collision-free across projects — two `cv-architect` agents on the same VM (one for `academic-resume`, one for `macf-paper`) stay on separate sessions.
|
|
170
|
+
|
|
171
|
+
**Bonus**: separate sessions mean multiple terminals can attach to different agents independently — `tmux attach -t academic-resume@cv-architect` on one terminal, `...@cv-project-archaeologist` on another, without windows-switching interference.
|
|
172
|
+
|
|
173
|
+
**Migration** from a single-session multi-window setup: `tmux rename-session -t <old-name> <new-name>` per agent.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Token & Git Hygiene
|
|
178
|
+
|
|
179
|
+
1. **Refresh GH_TOKEN before every `gh` or `git push`** — tokens are 1-hour installation tokens. Use the canonical helper, and **fail loud** if it doesn't work:
|
|
180
|
+
|
|
181
|
+
GH_TOKEN=$("$MACF_WORKSPACE_DIR/.claude/scripts/macf-gh-token.sh" \
|
|
182
|
+
--app-id "$APP_ID" --install-id "$INSTALL_ID" --key "$KEY_PATH") || exit 1
|
|
183
|
+
export GH_TOKEN
|
|
184
|
+
|
|
185
|
+
**Use `$MACF_WORKSPACE_DIR/` as the path prefix, not `./`.** The relative-path form (`./.claude/scripts/...`) breaks the moment you `cd` to another repo for cross-repo work — the `$(...)` substitution returns empty, `export GH_TOKEN=""` silently succeeds, and the next `gh` call falls back to stored user auth. `$MACF_WORKSPACE_DIR` is set by `claude.sh` to the agent's workspace absolute path and resolves regardless of cwd. Same principle for `KEY_PATH`: `claude.sh` rewrites relative key paths to absolute at launch so helper invocations from any cwd still find the key.
|
|
186
|
+
|
|
187
|
+
**Why this matters:** the `#140` PreToolUse hook catches this class at tool-call time (empty `GH_TOKEN` → blocks the `gh` call before it runs). But the hook adds friction — the command aborts, the operator retries with the correct pattern. Using absolute paths from the start avoids the abort-retry loop entirely.
|
|
188
|
+
|
|
189
|
+
**Never** use the naive `export GH_TOKEN=$(gh token generate ... | jq -r '.token')` pattern — if `gh token generate` fails, jq's success masks the error (no `pipefail`), `GH_TOKEN` becomes the string `"null"`, and every subsequent `gh` operation silently falls back to the stored `gh auth login` as the user. This is the attribution trap: your PRs and comments get written as the user, not the bot, and nothing surfaces the mismatch until cross-agent routing breaks.
|
|
190
|
+
|
|
191
|
+
The helper uses `--token-only`, `set -euo pipefail`, validates the `ghs_` prefix, and emits actionable diagnostics (clock drift, missing key, bad PEM, wrong App/installation ID) on failure.
|
|
192
|
+
|
|
193
|
+
2. **Sanity-check your identity** at session start or when something feels off:
|
|
194
|
+
|
|
195
|
+
GH_TOKEN=$GH_TOKEN ./.claude/scripts/macf-whoami.sh
|
|
196
|
+
|
|
197
|
+
Bot tokens (`ghs_*`) print `bot installation token`. A user token (`ghp_*`, `gho_*`, `ghu_*`) prints the user login and exits non-zero with a warning — that's the attribution trap firing.
|
|
198
|
+
|
|
199
|
+
3. **When token generation fails, diagnose — don't work around it.** Common causes observed in practice:
|
|
200
|
+
|
|
201
|
+
- **Clock drift** — "JWT could not be decoded" usually means this machine's clock is skewed beyond GitHub's JWT tolerance. Check `timedatectl status` (expect `System clock synchronized: yes`).
|
|
202
|
+
- **Key mismatch** — `.github-app-key.pem` on disk doesn't match the App's registered public key (typically after a key rotation on GitHub without syncing locally). Compare fingerprints:
|
|
203
|
+
|
|
204
|
+
openssl rsa -in "$KEY_PATH" -pubout -outform DER 2>/dev/null | openssl dgst -sha256
|
|
205
|
+
|
|
206
|
+
against the SHA256 shown on GitHub → App settings → Private keys.
|
|
207
|
+
- **Wrong App/installation ID** — double-check `$APP_ID` and `$INSTALL_ID` in `.claude/settings.local.json`.
|
|
208
|
+
- **Missing App permission** — a 401 on a specific endpoint (e.g. `gh run list` returns 401 while `gh issue list` works) typically means the App lacks the permission for that resource. Coordinator/review agents especially need `actions: read` to debug team workflow runs — see DR-019 for the full required permission set. A missing permission is another flavor of the attribution trap: the bot call 401s, `gh` falls through to stored user auth, operations run as the user without surfacing the issue.
|
|
209
|
+
|
|
210
|
+
4. **Never bake tokens into `git remote set-url`** — use `-c url.insteadOf` for each push so tokens don't persist in remote URLs.
|
|
211
|
+
|
|
212
|
+
5. **Never leave uncommitted changes** in the working tree at the end of a turn.
|
|
213
|
+
|
|
214
|
+
6. **Never commit** `.github-app-key.pem`, tokens, or secrets. `.gitignore` should exclude them, but also verify untracked files before staging.
|
|
215
|
+
|
|
216
|
+
7. **Structural enforcement: the PreToolUse hook.** Every workspace ships with `.claude/scripts/check-gh-token.sh`, wired into `.claude/settings.json` as a PreToolUse hook on `Bash`. It intercepts every `gh` and `git push` invocation (including wrapped forms like `sudo gh ...`, `GH_TOKEN=x gh ...`, `env FOO=bar gh ...`) and blocks with `exit 2` if `GH_TOKEN` is missing or doesn't have the `ghs_` prefix. This moves enforcement from operator discipline (rules 1-3 above) to the harness itself — without it, the attribution trap recurred 5 times in a single day (see #140). If you ever need to run a knowingly user-attributed op (e.g., `gh auth login` during onboarding), set `MACF_SKIP_TOKEN_CHECK=1` for that one call. The hook is installed by `macf init`, refreshed by `macf update` and `macf rules refresh`.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## When to Read vs. Modify These Rules
|
|
221
|
+
|
|
222
|
+
- **Read:** Every session start. These rules define how you coordinate.
|
|
223
|
+
- **Modify:** Never directly in workspace copies. Edit the canonical file at `plugin/rules/coordination.md` in `groundnuty/macf`, then run `macf update` in each affected workspace.
|
|
224
|
+
- **Disagree with a rule?** Open an issue on `groundnuty/macf` proposing the change, with rationale. Peer-review applies.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# check-gh-token.sh — Claude Code PreToolUse hook that blocks `gh` and
|
|
4
|
+
# `git push` invocations when GH_TOKEN isn't a bot installation token
|
|
5
|
+
# (ghs_ prefix). Prevents the attribution trap documented in
|
|
6
|
+
# plugin/rules/coordination.md (Token & Git Hygiene) and #140.
|
|
7
|
+
#
|
|
8
|
+
# Hook contract: JSON on stdin, exit 0 = allow, exit 2 = block (stderr
|
|
9
|
+
# is fed back to Claude as the error). See groundnuty/macf#140 for
|
|
10
|
+
# design rationale; 5 recurrences in a single day drove the move from
|
|
11
|
+
# behavioral to structural enforcement.
|
|
12
|
+
#
|
|
13
|
+
# Override: MACF_SKIP_TOKEN_CHECK=1 bypasses (for intentional
|
|
14
|
+
# user-attributed ops, e.g. `gh auth login` during onboarding).
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
# Operator override first — cheapest exit. No stdin read needed.
|
|
18
|
+
if [[ "${MACF_SKIP_TOKEN_CHECK:-}" == "1" ]]; then
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Read and parse the PreToolUse JSON payload. Fall through to allow on
|
|
23
|
+
# parse error — a broken hook must not brick the harness. Claude Code
|
|
24
|
+
# emits a well-formed payload in practice; this is defense-in-depth.
|
|
25
|
+
INPUT_JSON="$(cat)"
|
|
26
|
+
COMMAND="$(jq -r '.tool_input.command // ""' <<<"$INPUT_JSON" 2>/dev/null || echo "")"
|
|
27
|
+
|
|
28
|
+
# Wrapper-aware regex: match `gh ` or `git push ` as the effective
|
|
29
|
+
# command, allowing for zero or more of: sudo, env VAR=VAL..., watch,
|
|
30
|
+
# ionice, setsid, nice, time. Also triggers when preceded by `;`, `&&`,
|
|
31
|
+
# `||`, `|` so chained forms like `make && sudo gh ...` still match.
|
|
32
|
+
# Designed per science-agent's #140 review — the naïve anchored regex
|
|
33
|
+
# was trivially bypassable by wrappers.
|
|
34
|
+
GH_PATTERN='(^|[[:space:];|&])(sudo[[:space:]]+|env[[:space:]]+([A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*|watch[[:space:]]+|ionice[[:space:]]+|setsid[[:space:]]+|nice[[:space:]]+|time[[:space:]]+|[A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*(gh[[:space:]]|git[[:space:]]+push([[:space:]]|$))'
|
|
35
|
+
|
|
36
|
+
# Shell-wrapper bypass regex: catches `bash -c "gh ..."`, `sh -c '...'`,
|
|
37
|
+
# `zsh -c`, and flag-prefixed forms like `bash -x -c`, `bash -xc`,
|
|
38
|
+
# `bash -lc`. The shell's -c flag executes its quoted argument AS A
|
|
39
|
+
# COMMAND, so `gh` inside it IS a real invocation — unlike
|
|
40
|
+
# `echo "gh is cool"` where the same text is just literal data.
|
|
41
|
+
# Without this branch, `bash -c "gh issue close"` was a trivial bypass:
|
|
42
|
+
# `bash` isn't in the wrapper allowlist, and `gh` inside the quotes
|
|
43
|
+
# isn't preceded by one of the allowed delimiters `[[:space:];|&]`.
|
|
44
|
+
# Caught in the post-#140 audit pass, 2026-04-20.
|
|
45
|
+
#
|
|
46
|
+
# Flag handling: `(-[a-zA-Z]+[[:space:]]+)*` allows zero or more
|
|
47
|
+
# separate flag groups like `-x ` or `-e `. Final flag uses
|
|
48
|
+
# `-[a-zA-Z]*c` — must end in `c` (the `-c`-ness), optional letters
|
|
49
|
+
# before it cover combined forms like `-xc`, `-lc`, `-exc`.
|
|
50
|
+
SHELL_C_PATTERN='(^|[[:space:];|&])(sudo[[:space:]]+|env[[:space:]]+([A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*|[A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*(bash|sh|zsh)[[:space:]]+(-[a-zA-Z]+[[:space:]]+)*-[a-zA-Z]*c[[:space:]]+[^[:space:]].*(gh[[:space:]]|git[[:space:]]+push([[:space:]]|$))'
|
|
51
|
+
|
|
52
|
+
if [[ ! "$COMMAND" =~ $GH_PATTERN ]] && [[ ! "$COMMAND" =~ $SHELL_C_PATTERN ]]; then
|
|
53
|
+
# Not a gh/git-push command — allow without checking token.
|
|
54
|
+
exit 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# `gh auth *` is identity-management (login, logout, status, token,
|
|
58
|
+
# refresh, setup-git) — user-attribution is correct by design here.
|
|
59
|
+
# Blanket-blocking this subcommand would put the hook directly in the
|
|
60
|
+
# onboarding path (fresh workspace → first `gh auth login` → wall of
|
|
61
|
+
# error text), which is exactly the wrong user experience. Carve it
|
|
62
|
+
# out. Wrapper forms (`sudo gh auth ...`) also match because the regex
|
|
63
|
+
# allows arbitrary wrapper prefix before `gh`.
|
|
64
|
+
if [[ "$COMMAND" =~ (^|[[:space:];|&])(sudo[[:space:]]+|env[[:space:]]+([A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*|[A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*gh[[:space:]]+auth([[:space:]]|$) ]]; then
|
|
65
|
+
exit 0
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# Check GH_TOKEN: must be present AND start with ghs_ (bot token).
|
|
69
|
+
# ghp_/gho_/ghu_ are user tokens; empty falls through to stored
|
|
70
|
+
# `gh auth login` (user). Either case fires the trap.
|
|
71
|
+
# Note: `${GH_TOKEN:-}` expansion is mandatory under `set -u`; a bare
|
|
72
|
+
# `${GH_TOKEN:0:4}` errors with "unbound variable" when the env var
|
|
73
|
+
# is unset, which is exactly the case we need to handle.
|
|
74
|
+
GH_TOKEN_VALUE="${GH_TOKEN:-}"
|
|
75
|
+
TOKEN_PREFIX="${GH_TOKEN_VALUE:0:4}"
|
|
76
|
+
if [[ -z "$GH_TOKEN_VALUE" ]] || [[ "$TOKEN_PREFIX" != "ghs_" ]]; then
|
|
77
|
+
cat >&2 <<ERR
|
|
78
|
+
BLOCKED by MACF attribution-trap hook: this command would post as the USER, not the BOT.
|
|
79
|
+
|
|
80
|
+
Command: ${COMMAND}
|
|
81
|
+
GH_TOKEN prefix: ${TOKEN_PREFIX:-(empty)}
|
|
82
|
+
|
|
83
|
+
This hook exists because behavioral controls for the GH_TOKEN attribution
|
|
84
|
+
trap recurred 5 times in a single day. See groundnuty/macf#140 and
|
|
85
|
+
.claude/rules/coordination.md (Token & Git Hygiene).
|
|
86
|
+
|
|
87
|
+
Fix — refresh the bot token via the fail-loud helper:
|
|
88
|
+
|
|
89
|
+
GH_TOKEN=\$(./.claude/scripts/macf-gh-token.sh \\
|
|
90
|
+
--app-id "\$APP_ID" --install-id "\$INSTALL_ID" --key "\$KEY_PATH") || exit 1
|
|
91
|
+
export GH_TOKEN
|
|
92
|
+
|
|
93
|
+
If the helper is missing, restore it:
|
|
94
|
+
macf rules refresh --dir .
|
|
95
|
+
|
|
96
|
+
Override (ONLY for intentional user-attributed ops like onboarding):
|
|
97
|
+
export MACF_SKIP_TOKEN_CHECK=1
|
|
98
|
+
ERR
|
|
99
|
+
exit 2
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
exit 0
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Generate a fresh GitHub App installation token and emit it on stdout.
|
|
3
|
+
#
|
|
4
|
+
# Designed to fail LOUD — not fall back silently to `gh auth login` as the
|
|
5
|
+
# user, which is what happens with the naive `$(gh token generate | jq)`
|
|
6
|
+
# pattern when `gh token generate` fails (pipefail unset by default, so
|
|
7
|
+
# jq's success masks gh's failure and GH_TOKEN ends up as "null").
|
|
8
|
+
#
|
|
9
|
+
# Callers should use `TOKEN=$(./.claude/scripts/macf-gh-token.sh ...) || exit 1`
|
|
10
|
+
# or similar — on any failure this script writes the error reason to stderr
|
|
11
|
+
# and exits non-zero, and prints NOTHING to stdout.
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# macf-gh-token.sh --app-id <id> --install-id <id> --key <path> [--hostname <host>]
|
|
15
|
+
#
|
|
16
|
+
# On success: the installation token (starts with `ghs_`) is printed on
|
|
17
|
+
# stdout with a trailing newline.
|
|
18
|
+
# On failure: error goes to stderr, exit status is non-zero, stdout is empty.
|
|
19
|
+
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
usage() {
|
|
23
|
+
cat <<USAGE >&2
|
|
24
|
+
Usage: $0 --app-id <id> --install-id <id> --key <path> [--hostname <host>]
|
|
25
|
+
|
|
26
|
+
Required:
|
|
27
|
+
--app-id <id> GitHub App ID
|
|
28
|
+
--install-id <id> GitHub App installation ID
|
|
29
|
+
--key <path> Path to the App private key (.pem)
|
|
30
|
+
|
|
31
|
+
Optional:
|
|
32
|
+
--hostname <host> GitHub Enterprise hostname (default: api.github.com)
|
|
33
|
+
|
|
34
|
+
On success the installation token (ghs_*) is printed to stdout.
|
|
35
|
+
On failure, error details go to stderr and exit is non-zero.
|
|
36
|
+
USAGE
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
app_id=""
|
|
40
|
+
install_id=""
|
|
41
|
+
key_path=""
|
|
42
|
+
hostname=""
|
|
43
|
+
|
|
44
|
+
while [ "$#" -gt 0 ]; do
|
|
45
|
+
case "$1" in
|
|
46
|
+
--app-id) app_id="${2:?--app-id requires value}"; shift 2 ;;
|
|
47
|
+
--install-id) install_id="${2:?--install-id requires value}"; shift 2 ;;
|
|
48
|
+
--key) key_path="${2:?--key requires value}"; shift 2 ;;
|
|
49
|
+
--hostname) hostname="${2:?--hostname requires value}"; shift 2 ;;
|
|
50
|
+
-h|--help) usage; exit 0 ;;
|
|
51
|
+
*) echo "Error: unknown argument: $1" >&2; usage; exit 2 ;;
|
|
52
|
+
esac
|
|
53
|
+
done
|
|
54
|
+
|
|
55
|
+
missing=()
|
|
56
|
+
[ -z "$app_id" ] && missing+=(--app-id)
|
|
57
|
+
[ -z "$install_id" ] && missing+=(--install-id)
|
|
58
|
+
[ -z "$key_path" ] && missing+=(--key)
|
|
59
|
+
if [ "${#missing[@]}" -gt 0 ]; then
|
|
60
|
+
echo "Error: missing required flag(s): ${missing[*]}" >&2
|
|
61
|
+
usage
|
|
62
|
+
exit 2
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
if [ ! -f "$key_path" ]; then
|
|
66
|
+
echo "Error: key file not found: $key_path" >&2
|
|
67
|
+
echo "Hint: check that KEY_PATH in .claude/settings.local.json points at a valid .pem file." >&2
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Capture stderr so we can grep for known failure patterns and add
|
|
72
|
+
# diagnostic hints, but keep it available to re-emit to the user.
|
|
73
|
+
err_file="$(mktemp -t macf-gh-token-err.XXXXXX)"
|
|
74
|
+
trap 'rm -f "$err_file"' EXIT
|
|
75
|
+
|
|
76
|
+
gh_args=(gh token generate
|
|
77
|
+
--app-id "$app_id"
|
|
78
|
+
--installation-id "$install_id"
|
|
79
|
+
--key "$key_path"
|
|
80
|
+
--token-only
|
|
81
|
+
)
|
|
82
|
+
[ -n "$hostname" ] && gh_args+=(--hostname "$hostname")
|
|
83
|
+
|
|
84
|
+
# Intentionally NOT piping through `jq` — --token-only gives us the bare
|
|
85
|
+
# token, so we don't need to parse JSON, and we avoid the `$(gh | jq)`
|
|
86
|
+
# exit-status-masking trap that started this whole issue.
|
|
87
|
+
if ! token="$("${gh_args[@]}" 2>"$err_file")"; then
|
|
88
|
+
echo "Error: gh token generate failed." >&2
|
|
89
|
+
if [ -s "$err_file" ]; then
|
|
90
|
+
echo "--- gh token generate stderr ---" >&2
|
|
91
|
+
cat "$err_file" >&2
|
|
92
|
+
echo "--------------------------------" >&2
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# Hint the most common causes we've observed.
|
|
96
|
+
if grep -qi "JWT could not be decoded\|JWT" "$err_file" 2>/dev/null; then
|
|
97
|
+
echo "" >&2
|
|
98
|
+
echo "Hint: 'JWT could not be decoded' typically indicates **clock drift** on this host." >&2
|
|
99
|
+
echo " Check: timedatectl status (expect: System clock synchronized: yes)" >&2
|
|
100
|
+
echo " chronyc tracking (expect: Leap status: Normal, small offset)" >&2
|
|
101
|
+
elif grep -qi "unable to read key" "$err_file" 2>/dev/null; then
|
|
102
|
+
echo "" >&2
|
|
103
|
+
echo "Hint: key file unreadable. Verify file permissions (should be user-readable)." >&2
|
|
104
|
+
elif grep -qi "unable to parse key\|PEM.*RSA" "$err_file" 2>/dev/null; then
|
|
105
|
+
echo "" >&2
|
|
106
|
+
echo "Hint: key file is not a valid PEM RSA key, or it does not match the App's registered key." >&2
|
|
107
|
+
echo " Compare your local key fingerprint to the App settings page on GitHub:" >&2
|
|
108
|
+
echo " openssl rsa -in \"$key_path\" -pubout -outform DER 2>/dev/null | openssl dgst -sha256" >&2
|
|
109
|
+
echo " (then check GitHub → App settings → Private keys → SHA256 fingerprint)" >&2
|
|
110
|
+
elif grep -qi "404\|Not Found" "$err_file" 2>/dev/null; then
|
|
111
|
+
echo "" >&2
|
|
112
|
+
echo "Hint: App or installation not found. Verify --app-id and --install-id." >&2
|
|
113
|
+
fi
|
|
114
|
+
exit 1
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# Sanity-check the result. Installation tokens start with ghs_. Anything
|
|
118
|
+
# else (ghp_ user PAT, gho_ OAuth, empty string) would be a footgun.
|
|
119
|
+
token_prefix="${token:0:4}"
|
|
120
|
+
if [ -z "$token" ]; then
|
|
121
|
+
echo "Error: gh token generate succeeded but returned an empty token." >&2
|
|
122
|
+
exit 1
|
|
123
|
+
fi
|
|
124
|
+
if [ "$token_prefix" != "ghs_" ]; then
|
|
125
|
+
echo "Error: generated token has prefix '${token_prefix}' — expected 'ghs_' (installation token)." >&2
|
|
126
|
+
echo " Refusing to emit a non-installation token to avoid mis-attribution." >&2
|
|
127
|
+
exit 1
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
printf '%s\n' "$token"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Print the identity associated with the current GH_TOKEN, and flag it
|
|
3
|
+
# LOUDLY if the token is the wrong type (e.g., a user PAT where a bot
|
|
4
|
+
# installation token is expected).
|
|
5
|
+
#
|
|
6
|
+
# This exists because of the silent-fallback trap: when `gh token generate`
|
|
7
|
+
# fails, `gh` falls back to the locally stored `gh auth login` token,
|
|
8
|
+
# and subsequent `gh` operations silently run as the user — but nothing
|
|
9
|
+
# surfaces the identity mismatch until cross-agent routing breaks.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# macf-whoami.sh # prints identity to stdout; non-zero exit on trouble
|
|
13
|
+
#
|
|
14
|
+
# Token prefixes (per GitHub docs):
|
|
15
|
+
# ghs_ — server-to-server (App installation token) → /user returns 403
|
|
16
|
+
# ghp_ — personal access token
|
|
17
|
+
# gho_ — OAuth user token
|
|
18
|
+
# ghu_ — user-to-server (GitHub App user-access) token
|
|
19
|
+
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
if [ -z "${GH_TOKEN:-}" ]; then
|
|
23
|
+
echo "Error: GH_TOKEN is unset." >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
prefix="${GH_TOKEN:0:4}"
|
|
28
|
+
|
|
29
|
+
case "$prefix" in
|
|
30
|
+
ghs_)
|
|
31
|
+
# Installation token — /user is 403. This is the EXPECTED prefix for a
|
|
32
|
+
# healthy bot operation.
|
|
33
|
+
echo "bot installation token (prefix=ghs_)"
|
|
34
|
+
;;
|
|
35
|
+
ghp_|gho_|ghu_)
|
|
36
|
+
user="$(gh api user --jq '.login' 2>/dev/null || echo '<unknown>')"
|
|
37
|
+
echo "user token: $user (prefix=$prefix)"
|
|
38
|
+
echo "" >&2
|
|
39
|
+
echo "WARNING: this is NOT a bot installation token. All gh/git-push" >&2
|
|
40
|
+
echo "operations will be attributed to '$user', not to the bot." >&2
|
|
41
|
+
echo "If you expected a bot token, run:" >&2
|
|
42
|
+
echo " .claude/scripts/macf-gh-token.sh --app-id \$APP_ID --install-id \$INSTALL_ID --key \$KEY_PATH" >&2
|
|
43
|
+
echo "and look at its stderr diagnostics." >&2
|
|
44
|
+
exit 2
|
|
45
|
+
;;
|
|
46
|
+
*)
|
|
47
|
+
echo "unknown token type (prefix=$prefix, first chars only shown)" >&2
|
|
48
|
+
echo "Expected ghs_ (bot installation), ghp_ (user PAT), gho_ (OAuth), or ghu_ (user-to-server)." >&2
|
|
49
|
+
exit 3
|
|
50
|
+
;;
|
|
51
|
+
esac
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Send a prompt to a Claude Code TUI running in a tmux session.
|
|
3
|
+
#
|
|
4
|
+
# Claude Code's TUI is in multi-line input mode by default, so a single
|
|
5
|
+
# Enter inserts a newline instead of submitting. Sending the prompt and
|
|
6
|
+
# a single Enter leaves the text stuck in the input buffer until someone
|
|
7
|
+
# manually presses Enter again.
|
|
8
|
+
#
|
|
9
|
+
# The correct pattern is:
|
|
10
|
+
# 1. C-u — clear any stale input
|
|
11
|
+
# 2. "<prompt>" + Enter — type the prompt and a first Enter
|
|
12
|
+
# 3. sleep 1 — let tmux deliver those as one read event
|
|
13
|
+
# (without the sleep, tmux may batch both
|
|
14
|
+
# Enters together and Claude processes them
|
|
15
|
+
# atomically as "newline + newline", never
|
|
16
|
+
# submitting)
|
|
17
|
+
# 4. Enter — the second Enter that actually submits
|
|
18
|
+
#
|
|
19
|
+
# Usage:
|
|
20
|
+
# tmux-send-to-claude.sh <session> <prompt>
|
|
21
|
+
#
|
|
22
|
+
# <session> — tmux target session/window/pane (e.g. "agent:0"), or
|
|
23
|
+
# empty string "" to send to the current pane (useful
|
|
24
|
+
# when the caller is already inside the target tmux).
|
|
25
|
+
# <prompt> — the prompt text to submit.
|
|
26
|
+
#
|
|
27
|
+
# This script is the ONLY sanctioned way to programmatically submit a
|
|
28
|
+
# prompt to a Claude Code TUI. Never inline `tmux send-keys ... Enter`
|
|
29
|
+
# for prompt submission — the quirk above is easy to forget.
|
|
30
|
+
|
|
31
|
+
set -euo pipefail
|
|
32
|
+
|
|
33
|
+
if [ "$#" -lt 2 ]; then
|
|
34
|
+
echo "usage: $0 <session-or-empty> <prompt>" >&2
|
|
35
|
+
exit 2
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
session="$1"
|
|
39
|
+
prompt="$2"
|
|
40
|
+
|
|
41
|
+
if [ -n "$session" ]; then
|
|
42
|
+
tmux send-keys -t "$session" C-u
|
|
43
|
+
tmux send-keys -t "$session" "$prompt" Enter
|
|
44
|
+
sleep 1
|
|
45
|
+
tmux send-keys -t "$session" Enter
|
|
46
|
+
else
|
|
47
|
+
tmux send-keys C-u
|
|
48
|
+
tmux send-keys "$prompt" Enter
|
|
49
|
+
sleep 1
|
|
50
|
+
tmux send-keys Enter
|
|
51
|
+
fi
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Postbuild: write `dist/.build-info.json` so the installed CLI can
|
|
4
|
+
* detect a stale-dist situation at runtime (per #144).
|
|
5
|
+
*
|
|
6
|
+
* The file records the git HEAD at build time + an ISO timestamp.
|
|
7
|
+
* `macf update` + `macf self-update` compare this against the source
|
|
8
|
+
* repo's current HEAD to decide whether the operator's linked CLI is
|
|
9
|
+
* behind main.
|
|
10
|
+
*
|
|
11
|
+
* Fail-soft on `git` errors: on a shallow clone or npm tarball
|
|
12
|
+
* install where `.git/` isn't available, write `commit: "unknown"`
|
|
13
|
+
* so the CLI still ships cleanly. The stale-detect path treats
|
|
14
|
+
* `unknown` as "don't know, don't warn."
|
|
15
|
+
*/
|
|
16
|
+
import { execFileSync } from 'node:child_process';
|
|
17
|
+
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
18
|
+
import { dirname, join } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const REPO_ROOT = dirname(SCRIPT_DIR);
|
|
23
|
+
const DIST_DIR = join(REPO_ROOT, 'dist');
|
|
24
|
+
const TARGET = join(DIST_DIR, '.build-info.json');
|
|
25
|
+
|
|
26
|
+
function gitCommit() {
|
|
27
|
+
try {
|
|
28
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
29
|
+
cwd: REPO_ROOT,
|
|
30
|
+
encoding: 'utf-8',
|
|
31
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
32
|
+
}).trim();
|
|
33
|
+
} catch {
|
|
34
|
+
return 'unknown';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!existsSync(DIST_DIR)) {
|
|
39
|
+
mkdirSync(DIST_DIR, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const info = {
|
|
43
|
+
commit: gitCommit(),
|
|
44
|
+
built_at: new Date().toISOString(),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
writeFileSync(TARGET, JSON.stringify(info, null, 2) + '\n');
|
|
48
|
+
console.log(`Wrote ${TARGET}: commit=${info.commit.slice(0, 7)} built_at=${info.built_at}`);
|