@codyswann/lisa 2.332.3 → 2.332.5
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/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +27 -4
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +6 -4
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +53 -18
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +83 -1
- package/plugins/lisa/hooks/block-no-verify.sh +27 -4
- package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +53 -18
- package/plugins/lisa/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +83 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +53 -18
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +83 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/block-no-verify.sh +27 -4
- package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +53 -18
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +83 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-no-verify.sh +27 -4
- package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +53 -18
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +83 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/block-no-verify.sh +27 -4
- package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +53 -18
- package/plugins/src/base/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +83 -1
|
@@ -21,11 +21,15 @@
|
|
|
21
21
|
|
|
22
22
|
import {
|
|
23
23
|
chmodSync,
|
|
24
|
+
existsSync,
|
|
24
25
|
mkdirSync,
|
|
26
|
+
readFileSync,
|
|
25
27
|
renameSync,
|
|
26
28
|
rmSync,
|
|
27
29
|
writeFileSync,
|
|
28
30
|
} from "node:fs";
|
|
31
|
+
import { homedir } from "node:os";
|
|
32
|
+
import { join } from "node:path";
|
|
29
33
|
|
|
30
34
|
import { deriveAwsEnvironment } from "./aws-bootstrap.mjs";
|
|
31
35
|
import { renderEnv, renderNotes } from "./envfile.mjs";
|
|
@@ -58,6 +62,70 @@ function writeAtomic(destination, contents) {
|
|
|
58
62
|
* @param {object} [cfg] Resolved configuration.
|
|
59
63
|
* @returns {{count: number, dir: string}} What was written, and where.
|
|
60
64
|
*/
|
|
65
|
+
/** Marks the block this owns, so it is replaced rather than appended twice. */
|
|
66
|
+
const PROFILE_MARKER = "# >>> lisa secrets (managed) >>>";
|
|
67
|
+
|
|
68
|
+
/** Closes the managed block. */
|
|
69
|
+
const PROFILE_END = "# <<< lisa secrets (managed) <<<";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Make every shell in this container load the materialized secrets.
|
|
73
|
+
*
|
|
74
|
+
* `set -a` so the values are exported rather than merely set as shell
|
|
75
|
+
* variables, and sourced LAST so they win over anything the host injected —
|
|
76
|
+
* environment variables outrank profile files in AWS's credential chain, which
|
|
77
|
+
* is the whole reason a valid credential was being ignored.
|
|
78
|
+
*
|
|
79
|
+
* Guarded on the file existing, so a shell still starts cleanly before the
|
|
80
|
+
* first materialization or if the file is removed. Written to both `.bashrc`
|
|
81
|
+
* and `.profile` because which one a given shell reads depends on whether it is
|
|
82
|
+
* interactive or a login shell, and an agent's tool calls are not reliably
|
|
83
|
+
* either.
|
|
84
|
+
* @param {string} valuesFile Absolute path to the materialized env file.
|
|
85
|
+
* @param {object} [options] Home directory and file seams, for tests.
|
|
86
|
+
* @returns {string[]} The profile files that now source it.
|
|
87
|
+
*/
|
|
88
|
+
export function installProfileSourcing(valuesFile, options = {}) {
|
|
89
|
+
const {
|
|
90
|
+
home = process.env.HOME || homedir(),
|
|
91
|
+
exists = existsSync,
|
|
92
|
+
read = readFileSync,
|
|
93
|
+
write = writeFileSync,
|
|
94
|
+
} = options;
|
|
95
|
+
|
|
96
|
+
const block = [
|
|
97
|
+
PROFILE_MARKER,
|
|
98
|
+
`if [ -f "${valuesFile}" ]; then`,
|
|
99
|
+
` set -a`,
|
|
100
|
+
` . "${valuesFile}"`,
|
|
101
|
+
` set +a`,
|
|
102
|
+
`fi`,
|
|
103
|
+
PROFILE_END,
|
|
104
|
+
].join("\n");
|
|
105
|
+
|
|
106
|
+
const updated = [];
|
|
107
|
+
for (const name of [".bashrc", ".profile"]) {
|
|
108
|
+
const file = join(home, name);
|
|
109
|
+
const current = exists(file) ? String(read(file, "utf8")) : "";
|
|
110
|
+
|
|
111
|
+
// Replace an existing managed block rather than appending another: this
|
|
112
|
+
// runs on every session, and an appended-forever profile is its own bug.
|
|
113
|
+
const start = current.indexOf(PROFILE_MARKER);
|
|
114
|
+
const next =
|
|
115
|
+
start === -1
|
|
116
|
+
? `${current}${current.endsWith("\n") || !current ? "" : "\n"}\n${block}\n`
|
|
117
|
+
: `${current.slice(0, start)}${block}${current.slice(
|
|
118
|
+
current.indexOf(PROFILE_END, start) + PROFILE_END.length
|
|
119
|
+
)}`;
|
|
120
|
+
|
|
121
|
+
if (next !== current) {
|
|
122
|
+
write(file, next, { mode: 0o600 });
|
|
123
|
+
updated.push(file);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return updated;
|
|
127
|
+
}
|
|
128
|
+
|
|
61
129
|
export function materialize(cfg = readConfig()) {
|
|
62
130
|
if (!cfg.capabilities.mayWriteValues) {
|
|
63
131
|
throw new Error(
|
|
@@ -88,7 +156,21 @@ export function materialize(cfg = readConfig()) {
|
|
|
88
156
|
chmodSync(dir, 0o700);
|
|
89
157
|
writeAtomic(valuesFile, renderEnv(selected));
|
|
90
158
|
writeAtomic(notesFile, renderNotes(selected));
|
|
91
|
-
|
|
159
|
+
|
|
160
|
+
// Writing the file is not the same as the values being in effect.
|
|
161
|
+
//
|
|
162
|
+
// A materialized secrets.env that nothing sources changes nothing: the agent's
|
|
163
|
+
// shell starts from the container's own environment, so a host-injected
|
|
164
|
+
// AWS_ACCESS_KEY_ID keeps winning and every call fails with
|
|
165
|
+
// InvalidClientTokenId while the correct credential sits on disk, correct and
|
|
166
|
+
// unused. Deriving the variables (above) only fixes precedence WITHIN the
|
|
167
|
+
// file — something still has to load the file.
|
|
168
|
+
//
|
|
169
|
+
// So the shell profile sources it. That is what makes "the credential is
|
|
170
|
+
// materialized" and "the credential is usable" the same statement.
|
|
171
|
+
const sourced = installProfileSourcing(valuesFile);
|
|
172
|
+
|
|
173
|
+
return { count: selected.size, derived: derived.size, dir, sourced };
|
|
92
174
|
}
|
|
93
175
|
|
|
94
176
|
function main() {
|
|
@@ -20,18 +20,41 @@ set -euo pipefail
|
|
|
20
20
|
|
|
21
21
|
input="$(cat)"
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
# Both interpreters must be probed BEFORE they are used, and a missing one must
|
|
24
|
+
# be announced rather than swallowed.
|
|
25
|
+
#
|
|
26
|
+
# `jq` used to be called unguarded here while `python3` two lines below was
|
|
27
|
+
# guarded. Under `set -e` an absent jq aborted the script with 127, and Claude
|
|
28
|
+
# Code treats any non-2 exit as a NON-BLOCKING hook error — so the hook that
|
|
29
|
+
# enforces "never --no-verify" silently permitted the very thing it exists to
|
|
30
|
+
# stop. It was not hypothetical: agent containers routinely ship no jq (that is
|
|
31
|
+
# why jq is now a pinned toolchain entry). The Codex variant of this script has
|
|
32
|
+
# always had the guard, so this was a parity gap rather than a design choice.
|
|
33
|
+
#
|
|
34
|
+
# Degrading to "allow" is still the right behaviour — a hook that cannot parse
|
|
35
|
+
# its input cannot tell a bypass from an ordinary command, and failing closed
|
|
36
|
+
# would block every Bash call on a machine missing an interpreter. What is NOT
|
|
37
|
+
# right is doing it quietly, so the operator gets one line on stderr saying the
|
|
38
|
+
# protection is off. A guard that is silently absent reads exactly like a guard
|
|
39
|
+
# that is passing.
|
|
40
|
+
for required in jq python3; do
|
|
41
|
+
if ! command -v "$required" >/dev/null 2>&1; then
|
|
42
|
+
printf 'block-no-verify: %s not found; --no-verify protection is NOT active\n' \
|
|
43
|
+
"$required" >&2
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)"
|
|
24
49
|
if [ "$tool_name" != "Bash" ]; then
|
|
25
50
|
exit 0
|
|
26
51
|
fi
|
|
27
52
|
|
|
28
|
-
command_str="$(printf '%s' "$input" | jq -r '.tool_input.command // empty')"
|
|
53
|
+
command_str="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
|
|
29
54
|
if [ -z "$command_str" ]; then
|
|
30
55
|
exit 0
|
|
31
56
|
fi
|
|
32
57
|
|
|
33
|
-
command -v python3 >/dev/null 2>&1 || exit 0
|
|
34
|
-
|
|
35
58
|
if ! BLOCK_NO_VERIFY_COMMAND="$command_str" python3 - <<'PY'
|
|
36
59
|
import os
|
|
37
60
|
import re
|
|
@@ -126,13 +126,50 @@ auto-merge against a stale head you have not verified.
|
|
|
126
126
|
`gh pr merge <pr> --auto --<merge_method>`. Enabling auto-merge is **not terminal**
|
|
127
127
|
— continue the loop below until the PR is actually `MERGED` or `CLOSED`.
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
**With `auto_merge=true`, leave the latch ARMED — never disable auto-merge.**
|
|
130
|
+
(Under `auto_merge=false` the deliberate disarm above still applies: that mode
|
|
131
|
+
must leave the PR open for a human, so a pre-existing latch is removed on
|
|
132
|
+
purpose. Everything below is the `auto_merge=true` path.)
|
|
133
|
+
|
|
134
|
+
Once a fix is PUSHED the latch is safe: GitHub evaluates required checks against
|
|
135
|
+
the PR's current head, so a new commit whose checks have not reported leaves the
|
|
136
|
+
PR blocked. Auto-merge cannot ship a commit nothing has verified.
|
|
137
|
+
|
|
138
|
+
The only window a disarm ever protected is the gap between deciding to fix
|
|
139
|
+
something and that fix landing — during which the PR is genuinely green and
|
|
140
|
+
genuinely mergeable, and auto-merge firing is GitHub behaving correctly. Two
|
|
141
|
+
merges in this repo's history are attributed to that window (#1392, and the
|
|
142
|
+
release that shipped the `./hooks/` Cursor bug). Both were fixed forward within
|
|
143
|
+
minutes; one was a one-line docs inconsistency.
|
|
144
|
+
|
|
145
|
+
That evidence is also weaker than it looks: **auto-merge attributes the merge to
|
|
146
|
+
whoever enabled it**, so an auto-merge and a human pressing Merge are
|
|
147
|
+
indistinguishable in the timeline. The record cannot tell us those PRs were not
|
|
148
|
+
simply merged by hand while the latch happened to be armed.
|
|
149
|
+
|
|
150
|
+
Against that, disarming costs something certain. Disabling is a durable state
|
|
151
|
+
change on GitHub; re-enabling is one more step the run has to reach. When a run
|
|
152
|
+
ends in between — turns exhausted, job timeout, or you concluding the work while
|
|
153
|
+
checks are still pending — the latch stays off and nothing restores it. The PR is
|
|
154
|
+
left WORSE OFF THAN IF THIS SKILL HAD NEVER RUN: it has lost the mechanism that
|
|
155
|
+
merges it while no agent is watching, and the run reports success. Measured on
|
|
156
|
+
`gunnertech/frontend#282`, the latch went off 14s before the fix commit and the
|
|
157
|
+
PR sat 26 minutes after going green, against ~3 minutes for PRs this skill never
|
|
158
|
+
touched.
|
|
159
|
+
|
|
160
|
+
So the trade is a rare, unproven miss that costs a fix-forward PR, against a
|
|
161
|
+
frequent, silent stall on every PR this skill repairs. Take the rare one.
|
|
162
|
+
|
|
163
|
+
What still applies on a push: immediately re-read `headRefOid` and reset
|
|
164
|
+
`verify_commit` to the pushed head, so the shipped-verification in step 3 checks
|
|
165
|
+
what you actually pushed rather than the commit you replaced. That ancestry check
|
|
166
|
+
is what CATCHES a raced merge — it fails loudly when the fix SHA is not an
|
|
167
|
+
ancestor of the base branch — so the rare miss is detected rather than silent.
|
|
168
|
+
|
|
169
|
+
**Invariant:** never terminate having left auto-merge OFF on an open PR when
|
|
170
|
+
`auto_merge=true`. If some future path does disable it, restoring it is a
|
|
171
|
+
terminal obligation on EVERY exit — including give-up, budget-exhausted and error
|
|
172
|
+
paths — not a later step in a sequence.
|
|
136
173
|
|
|
137
174
|
- **Capability fallback** (`auto_merge=true` only): if the repo disallows
|
|
138
175
|
auto-merge, do not fail. Keep watching; once checks are green, the review gate
|
|
@@ -215,10 +252,9 @@ registered you will instead see real conflict markers — run
|
|
|
215
252
|
### c. Failing CI / deploy checks (`statusCheckRollup` has FAILURE)
|
|
216
253
|
Inspect the failing check's logs (`gh pr checks <pr>`, `gh run view <run> --log-failed`).
|
|
217
254
|
Fix the underlying code inline — **never lower thresholds, skip tests, or disable
|
|
218
|
-
checks** to force green.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
auto-merge. When the root cause is an upstream Lisa template/postinstall bug
|
|
255
|
+
checks** to force green. Leave auto-merge armed across the push (section 1);
|
|
256
|
+
after it, re-read the PR head and update `verify_commit` to that exact SHA so the
|
|
257
|
+
shipped-verification checks what you pushed. When the root cause is an upstream Lisa template/postinstall bug
|
|
222
258
|
rather than this project's code, fix it upstream and propagate down rather than
|
|
223
259
|
patching only here.
|
|
224
260
|
|
|
@@ -227,10 +263,9 @@ Delegate to the `pull-request-review` skill with the PR number. It owns the whol
|
|
|
227
263
|
comment cycle: fetch every unresolved human + bot thread (with resolution state via
|
|
228
264
|
GraphQL), implement valid feedback (commit + push), reply to invalid feedback, and
|
|
229
265
|
resolve every thread via `resolveReviewThread` so the branch-protection
|
|
230
|
-
thread-resolution gate clears. If that skill needs to push a commit,
|
|
231
|
-
|
|
232
|
-
`verify_commit` to the returned/pushed head,
|
|
233
|
-
then re-enable auto-merge and continue. Do not re-implement review handling here
|
|
266
|
+
thread-resolution gate clears. If that skill needs to push a commit, leave
|
|
267
|
+
auto-merge armed (section 1); when it returns, re-read `headRefOid` and reset
|
|
268
|
+
`verify_commit` to the returned/pushed head, then continue. Do not re-implement review handling here
|
|
234
269
|
— it is the single source of truth for review-thread handling.
|
|
235
270
|
|
|
236
271
|
### e. Review gate stall (`reviewDecision == CHANGES_REQUESTED`)
|
|
@@ -252,9 +287,9 @@ branch (the CI auto-fix workflow engaged before this session took the lease),
|
|
|
252
287
|
adjudicate it: merge it into the head branch if the fix is correct and still
|
|
253
288
|
needed, otherwise close it and delete the side branch. Never leave it dangling
|
|
254
289
|
— it represents a competing writer's pending work. Merging it mutates the
|
|
255
|
-
driven branch, so treat it like any other push:
|
|
256
|
-
re-read `headRefOid
|
|
257
|
-
head
|
|
290
|
+
driven branch, so treat it like any other push: leave auto-merge armed
|
|
291
|
+
(section 1), then re-read `headRefOid` and reset `verify_commit` to the merged
|
|
292
|
+
head. In
|
|
258
293
|
`on_blocker=report` mode this whole step is off-limits (diagnose-only): do not
|
|
259
294
|
merge, close, or delete anything — return `blocked:pending-auto-fix`.
|
|
260
295
|
|
|
@@ -21,11 +21,15 @@
|
|
|
21
21
|
|
|
22
22
|
import {
|
|
23
23
|
chmodSync,
|
|
24
|
+
existsSync,
|
|
24
25
|
mkdirSync,
|
|
26
|
+
readFileSync,
|
|
25
27
|
renameSync,
|
|
26
28
|
rmSync,
|
|
27
29
|
writeFileSync,
|
|
28
30
|
} from "node:fs";
|
|
31
|
+
import { homedir } from "node:os";
|
|
32
|
+
import { join } from "node:path";
|
|
29
33
|
|
|
30
34
|
import { deriveAwsEnvironment } from "./aws-bootstrap.mjs";
|
|
31
35
|
import { renderEnv, renderNotes } from "./envfile.mjs";
|
|
@@ -58,6 +62,70 @@ function writeAtomic(destination, contents) {
|
|
|
58
62
|
* @param {object} [cfg] Resolved configuration.
|
|
59
63
|
* @returns {{count: number, dir: string}} What was written, and where.
|
|
60
64
|
*/
|
|
65
|
+
/** Marks the block this owns, so it is replaced rather than appended twice. */
|
|
66
|
+
const PROFILE_MARKER = "# >>> lisa secrets (managed) >>>";
|
|
67
|
+
|
|
68
|
+
/** Closes the managed block. */
|
|
69
|
+
const PROFILE_END = "# <<< lisa secrets (managed) <<<";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Make every shell in this container load the materialized secrets.
|
|
73
|
+
*
|
|
74
|
+
* `set -a` so the values are exported rather than merely set as shell
|
|
75
|
+
* variables, and sourced LAST so they win over anything the host injected —
|
|
76
|
+
* environment variables outrank profile files in AWS's credential chain, which
|
|
77
|
+
* is the whole reason a valid credential was being ignored.
|
|
78
|
+
*
|
|
79
|
+
* Guarded on the file existing, so a shell still starts cleanly before the
|
|
80
|
+
* first materialization or if the file is removed. Written to both `.bashrc`
|
|
81
|
+
* and `.profile` because which one a given shell reads depends on whether it is
|
|
82
|
+
* interactive or a login shell, and an agent's tool calls are not reliably
|
|
83
|
+
* either.
|
|
84
|
+
* @param {string} valuesFile Absolute path to the materialized env file.
|
|
85
|
+
* @param {object} [options] Home directory and file seams, for tests.
|
|
86
|
+
* @returns {string[]} The profile files that now source it.
|
|
87
|
+
*/
|
|
88
|
+
export function installProfileSourcing(valuesFile, options = {}) {
|
|
89
|
+
const {
|
|
90
|
+
home = process.env.HOME || homedir(),
|
|
91
|
+
exists = existsSync,
|
|
92
|
+
read = readFileSync,
|
|
93
|
+
write = writeFileSync,
|
|
94
|
+
} = options;
|
|
95
|
+
|
|
96
|
+
const block = [
|
|
97
|
+
PROFILE_MARKER,
|
|
98
|
+
`if [ -f "${valuesFile}" ]; then`,
|
|
99
|
+
` set -a`,
|
|
100
|
+
` . "${valuesFile}"`,
|
|
101
|
+
` set +a`,
|
|
102
|
+
`fi`,
|
|
103
|
+
PROFILE_END,
|
|
104
|
+
].join("\n");
|
|
105
|
+
|
|
106
|
+
const updated = [];
|
|
107
|
+
for (const name of [".bashrc", ".profile"]) {
|
|
108
|
+
const file = join(home, name);
|
|
109
|
+
const current = exists(file) ? String(read(file, "utf8")) : "";
|
|
110
|
+
|
|
111
|
+
// Replace an existing managed block rather than appending another: this
|
|
112
|
+
// runs on every session, and an appended-forever profile is its own bug.
|
|
113
|
+
const start = current.indexOf(PROFILE_MARKER);
|
|
114
|
+
const next =
|
|
115
|
+
start === -1
|
|
116
|
+
? `${current}${current.endsWith("\n") || !current ? "" : "\n"}\n${block}\n`
|
|
117
|
+
: `${current.slice(0, start)}${block}${current.slice(
|
|
118
|
+
current.indexOf(PROFILE_END, start) + PROFILE_END.length
|
|
119
|
+
)}`;
|
|
120
|
+
|
|
121
|
+
if (next !== current) {
|
|
122
|
+
write(file, next, { mode: 0o600 });
|
|
123
|
+
updated.push(file);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return updated;
|
|
127
|
+
}
|
|
128
|
+
|
|
61
129
|
export function materialize(cfg = readConfig()) {
|
|
62
130
|
if (!cfg.capabilities.mayWriteValues) {
|
|
63
131
|
throw new Error(
|
|
@@ -88,7 +156,21 @@ export function materialize(cfg = readConfig()) {
|
|
|
88
156
|
chmodSync(dir, 0o700);
|
|
89
157
|
writeAtomic(valuesFile, renderEnv(selected));
|
|
90
158
|
writeAtomic(notesFile, renderNotes(selected));
|
|
91
|
-
|
|
159
|
+
|
|
160
|
+
// Writing the file is not the same as the values being in effect.
|
|
161
|
+
//
|
|
162
|
+
// A materialized secrets.env that nothing sources changes nothing: the agent's
|
|
163
|
+
// shell starts from the container's own environment, so a host-injected
|
|
164
|
+
// AWS_ACCESS_KEY_ID keeps winning and every call fails with
|
|
165
|
+
// InvalidClientTokenId while the correct credential sits on disk, correct and
|
|
166
|
+
// unused. Deriving the variables (above) only fixes precedence WITHIN the
|
|
167
|
+
// file — something still has to load the file.
|
|
168
|
+
//
|
|
169
|
+
// So the shell profile sources it. That is what makes "the credential is
|
|
170
|
+
// materialized" and "the credential is usable" the same statement.
|
|
171
|
+
const sourced = installProfileSourcing(valuesFile);
|
|
172
|
+
|
|
173
|
+
return { count: selected.size, derived: derived.size, dir, sourced };
|
|
92
174
|
}
|
|
93
175
|
|
|
94
176
|
function main() {
|
|
@@ -20,18 +20,41 @@ set -euo pipefail
|
|
|
20
20
|
|
|
21
21
|
input="$(cat)"
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
# Both interpreters must be probed BEFORE they are used, and a missing one must
|
|
24
|
+
# be announced rather than swallowed.
|
|
25
|
+
#
|
|
26
|
+
# `jq` used to be called unguarded here while `python3` two lines below was
|
|
27
|
+
# guarded. Under `set -e` an absent jq aborted the script with 127, and Claude
|
|
28
|
+
# Code treats any non-2 exit as a NON-BLOCKING hook error — so the hook that
|
|
29
|
+
# enforces "never --no-verify" silently permitted the very thing it exists to
|
|
30
|
+
# stop. It was not hypothetical: agent containers routinely ship no jq (that is
|
|
31
|
+
# why jq is now a pinned toolchain entry). The Codex variant of this script has
|
|
32
|
+
# always had the guard, so this was a parity gap rather than a design choice.
|
|
33
|
+
#
|
|
34
|
+
# Degrading to "allow" is still the right behaviour — a hook that cannot parse
|
|
35
|
+
# its input cannot tell a bypass from an ordinary command, and failing closed
|
|
36
|
+
# would block every Bash call on a machine missing an interpreter. What is NOT
|
|
37
|
+
# right is doing it quietly, so the operator gets one line on stderr saying the
|
|
38
|
+
# protection is off. A guard that is silently absent reads exactly like a guard
|
|
39
|
+
# that is passing.
|
|
40
|
+
for required in jq python3; do
|
|
41
|
+
if ! command -v "$required" >/dev/null 2>&1; then
|
|
42
|
+
printf 'block-no-verify: %s not found; --no-verify protection is NOT active\n' \
|
|
43
|
+
"$required" >&2
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)"
|
|
24
49
|
if [ "$tool_name" != "Bash" ]; then
|
|
25
50
|
exit 0
|
|
26
51
|
fi
|
|
27
52
|
|
|
28
|
-
command_str="$(printf '%s' "$input" | jq -r '.tool_input.command // empty')"
|
|
53
|
+
command_str="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
|
|
29
54
|
if [ -z "$command_str" ]; then
|
|
30
55
|
exit 0
|
|
31
56
|
fi
|
|
32
57
|
|
|
33
|
-
command -v python3 >/dev/null 2>&1 || exit 0
|
|
34
|
-
|
|
35
58
|
if ! BLOCK_NO_VERIFY_COMMAND="$command_str" python3 - <<'PY'
|
|
36
59
|
import os
|
|
37
60
|
import re
|
|
@@ -126,13 +126,50 @@ auto-merge against a stale head you have not verified.
|
|
|
126
126
|
`gh pr merge <pr> --auto --<merge_method>`. Enabling auto-merge is **not terminal**
|
|
127
127
|
— continue the loop below until the PR is actually `MERGED` or `CLOSED`.
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
**With `auto_merge=true`, leave the latch ARMED — never disable auto-merge.**
|
|
130
|
+
(Under `auto_merge=false` the deliberate disarm above still applies: that mode
|
|
131
|
+
must leave the PR open for a human, so a pre-existing latch is removed on
|
|
132
|
+
purpose. Everything below is the `auto_merge=true` path.)
|
|
133
|
+
|
|
134
|
+
Once a fix is PUSHED the latch is safe: GitHub evaluates required checks against
|
|
135
|
+
the PR's current head, so a new commit whose checks have not reported leaves the
|
|
136
|
+
PR blocked. Auto-merge cannot ship a commit nothing has verified.
|
|
137
|
+
|
|
138
|
+
The only window a disarm ever protected is the gap between deciding to fix
|
|
139
|
+
something and that fix landing — during which the PR is genuinely green and
|
|
140
|
+
genuinely mergeable, and auto-merge firing is GitHub behaving correctly. Two
|
|
141
|
+
merges in this repo's history are attributed to that window (#1392, and the
|
|
142
|
+
release that shipped the `./hooks/` Cursor bug). Both were fixed forward within
|
|
143
|
+
minutes; one was a one-line docs inconsistency.
|
|
144
|
+
|
|
145
|
+
That evidence is also weaker than it looks: **auto-merge attributes the merge to
|
|
146
|
+
whoever enabled it**, so an auto-merge and a human pressing Merge are
|
|
147
|
+
indistinguishable in the timeline. The record cannot tell us those PRs were not
|
|
148
|
+
simply merged by hand while the latch happened to be armed.
|
|
149
|
+
|
|
150
|
+
Against that, disarming costs something certain. Disabling is a durable state
|
|
151
|
+
change on GitHub; re-enabling is one more step the run has to reach. When a run
|
|
152
|
+
ends in between — turns exhausted, job timeout, or you concluding the work while
|
|
153
|
+
checks are still pending — the latch stays off and nothing restores it. The PR is
|
|
154
|
+
left WORSE OFF THAN IF THIS SKILL HAD NEVER RUN: it has lost the mechanism that
|
|
155
|
+
merges it while no agent is watching, and the run reports success. Measured on
|
|
156
|
+
`gunnertech/frontend#282`, the latch went off 14s before the fix commit and the
|
|
157
|
+
PR sat 26 minutes after going green, against ~3 minutes for PRs this skill never
|
|
158
|
+
touched.
|
|
159
|
+
|
|
160
|
+
So the trade is a rare, unproven miss that costs a fix-forward PR, against a
|
|
161
|
+
frequent, silent stall on every PR this skill repairs. Take the rare one.
|
|
162
|
+
|
|
163
|
+
What still applies on a push: immediately re-read `headRefOid` and reset
|
|
164
|
+
`verify_commit` to the pushed head, so the shipped-verification in step 3 checks
|
|
165
|
+
what you actually pushed rather than the commit you replaced. That ancestry check
|
|
166
|
+
is what CATCHES a raced merge — it fails loudly when the fix SHA is not an
|
|
167
|
+
ancestor of the base branch — so the rare miss is detected rather than silent.
|
|
168
|
+
|
|
169
|
+
**Invariant:** never terminate having left auto-merge OFF on an open PR when
|
|
170
|
+
`auto_merge=true`. If some future path does disable it, restoring it is a
|
|
171
|
+
terminal obligation on EVERY exit — including give-up, budget-exhausted and error
|
|
172
|
+
paths — not a later step in a sequence.
|
|
136
173
|
|
|
137
174
|
- **Capability fallback** (`auto_merge=true` only): if the repo disallows
|
|
138
175
|
auto-merge, do not fail. Keep watching; once checks are green, the review gate
|
|
@@ -215,10 +252,9 @@ registered you will instead see real conflict markers — run
|
|
|
215
252
|
### c. Failing CI / deploy checks (`statusCheckRollup` has FAILURE)
|
|
216
253
|
Inspect the failing check's logs (`gh pr checks <pr>`, `gh run view <run> --log-failed`).
|
|
217
254
|
Fix the underlying code inline — **never lower thresholds, skip tests, or disable
|
|
218
|
-
checks** to force green.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
auto-merge. When the root cause is an upstream Lisa template/postinstall bug
|
|
255
|
+
checks** to force green. Leave auto-merge armed across the push (section 1);
|
|
256
|
+
after it, re-read the PR head and update `verify_commit` to that exact SHA so the
|
|
257
|
+
shipped-verification checks what you pushed. When the root cause is an upstream Lisa template/postinstall bug
|
|
222
258
|
rather than this project's code, fix it upstream and propagate down rather than
|
|
223
259
|
patching only here.
|
|
224
260
|
|
|
@@ -227,10 +263,9 @@ Delegate to the `pull-request-review` skill with the PR number. It owns the whol
|
|
|
227
263
|
comment cycle: fetch every unresolved human + bot thread (with resolution state via
|
|
228
264
|
GraphQL), implement valid feedback (commit + push), reply to invalid feedback, and
|
|
229
265
|
resolve every thread via `resolveReviewThread` so the branch-protection
|
|
230
|
-
thread-resolution gate clears. If that skill needs to push a commit,
|
|
231
|
-
|
|
232
|
-
`verify_commit` to the returned/pushed head,
|
|
233
|
-
then re-enable auto-merge and continue. Do not re-implement review handling here
|
|
266
|
+
thread-resolution gate clears. If that skill needs to push a commit, leave
|
|
267
|
+
auto-merge armed (section 1); when it returns, re-read `headRefOid` and reset
|
|
268
|
+
`verify_commit` to the returned/pushed head, then continue. Do not re-implement review handling here
|
|
234
269
|
— it is the single source of truth for review-thread handling.
|
|
235
270
|
|
|
236
271
|
### e. Review gate stall (`reviewDecision == CHANGES_REQUESTED`)
|
|
@@ -252,9 +287,9 @@ branch (the CI auto-fix workflow engaged before this session took the lease),
|
|
|
252
287
|
adjudicate it: merge it into the head branch if the fix is correct and still
|
|
253
288
|
needed, otherwise close it and delete the side branch. Never leave it dangling
|
|
254
289
|
— it represents a competing writer's pending work. Merging it mutates the
|
|
255
|
-
driven branch, so treat it like any other push:
|
|
256
|
-
re-read `headRefOid
|
|
257
|
-
head
|
|
290
|
+
driven branch, so treat it like any other push: leave auto-merge armed
|
|
291
|
+
(section 1), then re-read `headRefOid` and reset `verify_commit` to the merged
|
|
292
|
+
head. In
|
|
258
293
|
`on_blocker=report` mode this whole step is off-limits (diagnose-only): do not
|
|
259
294
|
merge, close, or delete anything — return `blocked:pending-auto-fix`.
|
|
260
295
|
|