@codyswann/lisa 4.34.19 → 4.34.21
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/check-npm-publish-landed.mjs +4 -0
- package/all/copy-overwrite/scripts/lib/kill-marks.mjs +6 -6
- package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +22 -1
- package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +37 -2
- package/all/copy-overwrite/scripts/lisa-run-gates.mjs +6 -2
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +1 -5
- package/dist/cli/doctor-worktree-work-at-risk.d.ts.map +1 -1
- package/dist/cli/doctor-worktree-work-at-risk.js +5 -2
- package/dist/cli/doctor-worktree-work-at-risk.js.map +1 -1
- package/dist/cli/worktree-prune-policy.d.ts.map +1 -1
- package/dist/cli/worktree-prune-policy.js +2 -1
- package/dist/cli/worktree-prune-policy.js.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +14 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +11 -9
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +37 -2
- package/dist/strategies/package-lisa.js +2 -2
- package/package.json +9 -9
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/block-direct-issue-create.sh +22 -1
- package/plugins/lisa/hooks/parity-safety-net.sh +37 -2
- package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +22 -1
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +37 -2
- package/plugins/lisa-agy/plugin.json +1 -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-direct-issue-create.sh +22 -1
- package/plugins/lisa-copilot/hooks/parity-safety-net.sh +37 -2
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +22 -1
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +37 -2
- 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-direct-issue-create.sh +22 -1
- package/plugins/src/base/hooks/parity-safety-net.sh +37 -2
- package/scripts/check-plugins-sync.sh +6 -2
|
@@ -162,6 +162,37 @@ describes yours:
|
|
|
162
162
|
EOF
|
|
163
163
|
)"
|
|
164
164
|
|
|
165
|
+
# How to preserve uncommitted work before an operation that would discard it.
|
|
166
|
+
#
|
|
167
|
+
# This is appended to the reasons that refuse a discarding operation, and its
|
|
168
|
+
# whole point is what it does NOT say (issue #3722). The obvious advice is
|
|
169
|
+
# "stash it first", and on a single-checkout repository that is fine. Here it
|
|
170
|
+
# is the most dangerous thing an agent can be told: the stash is ONE STACK PER
|
|
171
|
+
# CLONE, shared by every worktree, so with concurrent agents a sibling's push
|
|
172
|
+
# can shift your entry and a sibling's pop can consume it. `lint-staged` pushes
|
|
173
|
+
# one on every commit too, so agent and tooling entries interleave.
|
|
174
|
+
#
|
|
175
|
+
# A remedy fires at the exact moment an agent is holding the work most worth
|
|
176
|
+
# protecting, and a guard's own output reads as authoritative — so pointing it
|
|
177
|
+
# at shared mutable state there is worse than the refusal it accompanies.
|
|
178
|
+
#
|
|
179
|
+
# The patch file is per-worktree: `mktemp` names it, so nothing has to invent a
|
|
180
|
+
# filename, and it lives in this agent's own TMPDIR where no sibling can reach
|
|
181
|
+
# it. Untracked files are unaffected by both refused operations, so `git diff`
|
|
182
|
+
# captures everything at risk.
|
|
183
|
+
PRESERVE_GUIDANCE="$(
|
|
184
|
+
cat <<'EOF'
|
|
185
|
+
Preserve the work first, per-worktree:
|
|
186
|
+
|
|
187
|
+
git diff [-- <path>] > "$(mktemp "${TMPDIR:-/tmp}/lisa-preserve-XXXXXX.patch")"
|
|
188
|
+
|
|
189
|
+
and restore it later with `git apply <that file>`.
|
|
190
|
+
|
|
191
|
+
Do NOT reach for `git stash`. One stash stack is shared by every worktree of a
|
|
192
|
+
clone, so a concurrently running agent can consume the entry you just pushed.
|
|
193
|
+
EOF
|
|
194
|
+
)"
|
|
195
|
+
|
|
165
196
|
# block() prints the reason to stderr (surfaced to the model) and exits 2 so the
|
|
166
197
|
# Bash tool call is denied. $1 = human-readable reason for the block.
|
|
167
198
|
#
|
|
@@ -1574,7 +1605,9 @@ done <<<"$push_segments"
|
|
|
1574
1605
|
if matches "${GIT_CMD}"'reset\b.*--(hard|merge)\b'; then
|
|
1575
1606
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
|
1576
1607
|
&& [ -n "$(git status --porcelain 2>/dev/null)" ]; then
|
|
1577
|
-
block "git reset --hard/--merge on a dirty working tree would discard uncommitted changes (
|
|
1608
|
+
block "git reset --hard/--merge on a dirty working tree would discard uncommitted changes (commit first, or preserve as below)" "$PRESERVE_GUIDANCE
|
|
1609
|
+
|
|
1610
|
+
$DESTRUCTIVE_GUIDANCE"
|
|
1578
1611
|
fi
|
|
1579
1612
|
fi
|
|
1580
1613
|
|
|
@@ -1624,7 +1657,9 @@ if matches "${GIT_CHECKOUT}${GIT_TOKENS}"'--([[:space:]]|$)' \
|
|
|
1624
1657
|
|| matches "${GIT_CHECKOUT}${GIT_TOKENS}"'(-[[:alnum:]]*f[[:alnum:]]*|--force)([[:space:]]|=|$)' \
|
|
1625
1658
|
|| matches "${GIT_CHECKOUT}"'[[:space:]][^;&|]*--pathspec-from-file' \
|
|
1626
1659
|
|| matches "${GIT_CHECKOUT}"'[[:space:]]+\.(/)?([[:space:]]|$)'; then
|
|
1627
|
-
block "git checkout discarding local changes (--, -f/--force, --pathspec-from-file, or bare .)
|
|
1660
|
+
block "git checkout discarding local changes (--, -f/--force, --pathspec-from-file, or bare .)" "$PRESERVE_GUIDANCE
|
|
1661
|
+
|
|
1662
|
+
$DESTRUCTIVE_GUIDANCE"
|
|
1628
1663
|
fi
|
|
1629
1664
|
|
|
1630
1665
|
# 5. `git switch` discards: `--discard-changes` and its `-f/--force` aliases.
|
|
@@ -723,8 +723,8 @@ function locateLisaPin(packageJson) {
|
|
|
723
723
|
* applied version and the INSTALLED version are two halves of one thing. When
|
|
724
724
|
* they drift, a config file calls an export the installed package does not have
|
|
725
725
|
* and every run of the tool that loads it dies at config load — while the apply
|
|
726
|
-
* itself reports success, and `postinstall`'s `
|
|
727
|
-
*
|
|
726
|
+
* itself reports success, and `postinstall`'s `tsc || true` swallows the only
|
|
727
|
+
* local signal. The failure then surfaces at the next
|
|
728
728
|
* lint run, detached from the apply that caused it (#2953).
|
|
729
729
|
*
|
|
730
730
|
* A range is rewritten as readily as an exact pin, and deliberately so: a caret
|
package/package.json
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
"build": "bun run build:dist && bun run build:plugins",
|
|
4
4
|
"build:dist": "node scripts/clean-dist.mjs && tsc && node scripts/copy-codex-scripts.mjs && node scripts/copy-opencode-plugin-templates.mjs",
|
|
5
5
|
"build:dist:in-place": "tsc && node scripts/copy-codex-scripts.mjs && node scripts/copy-opencode-plugin-templates.mjs",
|
|
6
|
-
"lisa-test-run": "
|
|
6
|
+
"lisa-test-run": "bun run build && node dist/cli/lisa-test-run.js --profile lisa",
|
|
7
7
|
"test:integration": "$npm_execpath run lisa-test-run -- --adapter vitest -- vitest run tests/integration",
|
|
8
8
|
"test:integration:push": "$npm_execpath run lisa-test-run -- --adapter vitest -- vitest run tests/integration --exclude='**/mutation-gate-bite.test.ts' --exclude='**/mutation-gate-diff-bite.test.ts' --exclude='**/mutation-sigterm-control.test.ts' --exclude='**/mutation-performance-fixture.test.ts' --exclude='**/mutation-performance-packed-apply.test.ts'",
|
|
9
|
-
"postinstall": "bash ./scripts/install-claude-plugins.sh || true; node ./scripts/install-generated-artifact-merge-driver.mjs || true;
|
|
10
|
-
"pretest": "
|
|
9
|
+
"postinstall": "bash ./scripts/install-claude-plugins.sh || true; node ./scripts/install-generated-artifact-merge-driver.mjs || true; tsc || true",
|
|
10
|
+
"pretest": "bun run build",
|
|
11
11
|
"test": "$npm_execpath run lisa-test-run -- --adapter vitest -- vitest run",
|
|
12
12
|
"test:unit": "$npm_execpath run lisa-test-run -- --adapter vitest -- vitest run --exclude='**/integration/**' --exclude='**/*.integration.*'",
|
|
13
13
|
"test:cov": "$npm_execpath run lisa-test-run -- --adapter vitest -- vitest run --coverage",
|
|
14
14
|
"test:cov:unit": "LISA_COVERAGE_SCOPE=unit $npm_execpath run lisa-test-run -- --adapter vitest -- vitest run --coverage --exclude='**/integration/**' --exclude='**/*.integration.*'",
|
|
15
|
-
"pretest:mutation": "
|
|
15
|
+
"pretest:mutation": "bun run build",
|
|
16
16
|
"test:mutation": "LISA_TEST_SCRATCH_PREFIXES='[\"lisa-mutation-\",\"worker-\"]' $npm_execpath run lisa-test-run -- --adapter direct -- node scripts/lisa-mutation.mjs",
|
|
17
17
|
"test:mutation:full": "LISA_TEST_SCRATCH_PREFIXES='[\"lisa-mutation-\",\"worker-\"]' $npm_execpath run lisa-test-run -- --adapter direct -- node scripts/lisa-mutation.mjs --all",
|
|
18
18
|
"lint": "oxlint --disable-nested-config && eslint . --quiet",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"check:rules-pairing": "bash scripts/check-rules-pairing.sh",
|
|
60
60
|
"check:pipeline-status-reads": "node scripts/check-pipeline-status-reads.mjs",
|
|
61
61
|
"check:whole-output-guards": "node scripts/check-whole-output-guards.mjs",
|
|
62
|
-
"check:shell-guard-refusals": "
|
|
62
|
+
"check:shell-guard-refusals": "bun run build && $npm_execpath run lisa-test-run -- --adapter direct -- node scripts/check-shell-guard-refusal-coverage.mjs",
|
|
63
63
|
"check:workflow-inputs": "node scripts/detect-stale-workflow-inputs.mjs --project . --project typescript/create-only --project rails/create-only --json",
|
|
64
64
|
"check:conflict-markers": "node all/copy-overwrite/scripts/check-conflict-markers.mjs",
|
|
65
65
|
"check:vacuous-required-checks": "node typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs --vacuity",
|
|
@@ -166,7 +166,7 @@
|
|
|
166
166
|
"zod-validation-error": "^4.0.0"
|
|
167
167
|
},
|
|
168
168
|
"name": "@codyswann/lisa",
|
|
169
|
-
"version": "4.34.
|
|
169
|
+
"version": "4.34.21",
|
|
170
170
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
171
171
|
"main": "dist/index.js",
|
|
172
172
|
"exports": {
|
|
@@ -314,7 +314,7 @@
|
|
|
314
314
|
"test": "tests"
|
|
315
315
|
},
|
|
316
316
|
"types": "./dist/index.d.ts",
|
|
317
|
-
"lisaReleaseCommit": "
|
|
318
|
-
"gitHead": "
|
|
319
|
-
"lisaReleaseTag": "v4.34.
|
|
317
|
+
"lisaReleaseCommit": "f65b16b98f562acb7a06c20b6e01a2cc6c37e200",
|
|
318
|
+
"gitHead": "f65b16b98f562acb7a06c20b6e01a2cc6c37e200",
|
|
319
|
+
"lisaReleaseTag": "v4.34.21"
|
|
320
320
|
}
|
|
@@ -1685,7 +1685,28 @@ def scan(text, depth, from_file=False):
|
|
|
1685
1685
|
# waved through: an unbalanced quote inside a script is the identical
|
|
1686
1686
|
# two-character trick moved one file away, and skipping it would hand
|
|
1687
1687
|
# the bypass straight back.
|
|
1688
|
-
|
|
1688
|
+
#
|
|
1689
|
+
# The declaration check is NOT gated on `from_file`, and that is the
|
|
1690
|
+
# #3727 fix. Tokenisation is what feeds every argv-based check below,
|
|
1691
|
+
# so when it throws, a TYPED command was previously judged with no
|
|
1692
|
+
# declaration consulted at all — not its `--body-file`, not even a
|
|
1693
|
+
# plain `--label <ready role>` sitting in argv. A correctly declared
|
|
1694
|
+
# filing was refused for the unrelated reason that its command happened
|
|
1695
|
+
# not to lex, and told "this filing declares no readiness", which was
|
|
1696
|
+
# false. Reading the text here is the only check available once `shlex`
|
|
1697
|
+
# has failed.
|
|
1698
|
+
#
|
|
1699
|
+
# It cannot reopen the bypass the paragraph above defends. That bypass
|
|
1700
|
+
# is an UNDECLARED create hiding behind a quote, and an undeclared
|
|
1701
|
+
# create has no marker and no `--label <ready role>` for
|
|
1702
|
+
# `text_declares_readiness` to match — which is asserted, not assumed,
|
|
1703
|
+
# by the rejection controls covering a bare create, the role mentioned
|
|
1704
|
+
# in prose, and the role appearing inside a URL.
|
|
1705
|
+
#
|
|
1706
|
+
# A marker in a SEPARATE `--body-file` stays refused, because finding
|
|
1707
|
+
# that operand needs the tokenisation that just failed. That case is
|
|
1708
|
+
# reachable by declaring inline instead, which this change makes work.
|
|
1709
|
+
if text_declares_readiness(text):
|
|
1689
1710
|
return None
|
|
1690
1711
|
if UNPARSEABLE_CREATION.search(text):
|
|
1691
1712
|
return (
|
|
@@ -162,6 +162,37 @@ describes yours:
|
|
|
162
162
|
EOF
|
|
163
163
|
)"
|
|
164
164
|
|
|
165
|
+
# How to preserve uncommitted work before an operation that would discard it.
|
|
166
|
+
#
|
|
167
|
+
# This is appended to the reasons that refuse a discarding operation, and its
|
|
168
|
+
# whole point is what it does NOT say (issue #3722). The obvious advice is
|
|
169
|
+
# "stash it first", and on a single-checkout repository that is fine. Here it
|
|
170
|
+
# is the most dangerous thing an agent can be told: the stash is ONE STACK PER
|
|
171
|
+
# CLONE, shared by every worktree, so with concurrent agents a sibling's push
|
|
172
|
+
# can shift your entry and a sibling's pop can consume it. `lint-staged` pushes
|
|
173
|
+
# one on every commit too, so agent and tooling entries interleave.
|
|
174
|
+
#
|
|
175
|
+
# A remedy fires at the exact moment an agent is holding the work most worth
|
|
176
|
+
# protecting, and a guard's own output reads as authoritative — so pointing it
|
|
177
|
+
# at shared mutable state there is worse than the refusal it accompanies.
|
|
178
|
+
#
|
|
179
|
+
# The patch file is per-worktree: `mktemp` names it, so nothing has to invent a
|
|
180
|
+
# filename, and it lives in this agent's own TMPDIR where no sibling can reach
|
|
181
|
+
# it. Untracked files are unaffected by both refused operations, so `git diff`
|
|
182
|
+
# captures everything at risk.
|
|
183
|
+
PRESERVE_GUIDANCE="$(
|
|
184
|
+
cat <<'EOF'
|
|
185
|
+
Preserve the work first, per-worktree:
|
|
186
|
+
|
|
187
|
+
git diff [-- <path>] > "$(mktemp "${TMPDIR:-/tmp}/lisa-preserve-XXXXXX.patch")"
|
|
188
|
+
|
|
189
|
+
and restore it later with `git apply <that file>`.
|
|
190
|
+
|
|
191
|
+
Do NOT reach for `git stash`. One stash stack is shared by every worktree of a
|
|
192
|
+
clone, so a concurrently running agent can consume the entry you just pushed.
|
|
193
|
+
EOF
|
|
194
|
+
)"
|
|
195
|
+
|
|
165
196
|
# block() prints the reason to stderr (surfaced to the model) and exits 2 so the
|
|
166
197
|
# Bash tool call is denied. $1 = human-readable reason for the block.
|
|
167
198
|
#
|
|
@@ -1574,7 +1605,9 @@ done <<<"$push_segments"
|
|
|
1574
1605
|
if matches "${GIT_CMD}"'reset\b.*--(hard|merge)\b'; then
|
|
1575
1606
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
|
1576
1607
|
&& [ -n "$(git status --porcelain 2>/dev/null)" ]; then
|
|
1577
|
-
block "git reset --hard/--merge on a dirty working tree would discard uncommitted changes (
|
|
1608
|
+
block "git reset --hard/--merge on a dirty working tree would discard uncommitted changes (commit first, or preserve as below)" "$PRESERVE_GUIDANCE
|
|
1609
|
+
|
|
1610
|
+
$DESTRUCTIVE_GUIDANCE"
|
|
1578
1611
|
fi
|
|
1579
1612
|
fi
|
|
1580
1613
|
|
|
@@ -1624,7 +1657,9 @@ if matches "${GIT_CHECKOUT}${GIT_TOKENS}"'--([[:space:]]|$)' \
|
|
|
1624
1657
|
|| matches "${GIT_CHECKOUT}${GIT_TOKENS}"'(-[[:alnum:]]*f[[:alnum:]]*|--force)([[:space:]]|=|$)' \
|
|
1625
1658
|
|| matches "${GIT_CHECKOUT}"'[[:space:]][^;&|]*--pathspec-from-file' \
|
|
1626
1659
|
|| matches "${GIT_CHECKOUT}"'[[:space:]]+\.(/)?([[:space:]]|$)'; then
|
|
1627
|
-
block "git checkout discarding local changes (--, -f/--force, --pathspec-from-file, or bare .)
|
|
1660
|
+
block "git checkout discarding local changes (--, -f/--force, --pathspec-from-file, or bare .)" "$PRESERVE_GUIDANCE
|
|
1661
|
+
|
|
1662
|
+
$DESTRUCTIVE_GUIDANCE"
|
|
1628
1663
|
fi
|
|
1629
1664
|
|
|
1630
1665
|
# 5. `git switch` discards: `--discard-changes` and its `-f/--force` aliases.
|
|
@@ -1685,7 +1685,28 @@ def scan(text, depth, from_file=False):
|
|
|
1685
1685
|
# waved through: an unbalanced quote inside a script is the identical
|
|
1686
1686
|
# two-character trick moved one file away, and skipping it would hand
|
|
1687
1687
|
# the bypass straight back.
|
|
1688
|
-
|
|
1688
|
+
#
|
|
1689
|
+
# The declaration check is NOT gated on `from_file`, and that is the
|
|
1690
|
+
# #3727 fix. Tokenisation is what feeds every argv-based check below,
|
|
1691
|
+
# so when it throws, a TYPED command was previously judged with no
|
|
1692
|
+
# declaration consulted at all — not its `--body-file`, not even a
|
|
1693
|
+
# plain `--label <ready role>` sitting in argv. A correctly declared
|
|
1694
|
+
# filing was refused for the unrelated reason that its command happened
|
|
1695
|
+
# not to lex, and told "this filing declares no readiness", which was
|
|
1696
|
+
# false. Reading the text here is the only check available once `shlex`
|
|
1697
|
+
# has failed.
|
|
1698
|
+
#
|
|
1699
|
+
# It cannot reopen the bypass the paragraph above defends. That bypass
|
|
1700
|
+
# is an UNDECLARED create hiding behind a quote, and an undeclared
|
|
1701
|
+
# create has no marker and no `--label <ready role>` for
|
|
1702
|
+
# `text_declares_readiness` to match — which is asserted, not assumed,
|
|
1703
|
+
# by the rejection controls covering a bare create, the role mentioned
|
|
1704
|
+
# in prose, and the role appearing inside a URL.
|
|
1705
|
+
#
|
|
1706
|
+
# A marker in a SEPARATE `--body-file` stays refused, because finding
|
|
1707
|
+
# that operand needs the tokenisation that just failed. That case is
|
|
1708
|
+
# reachable by declaring inline instead, which this change makes work.
|
|
1709
|
+
if text_declares_readiness(text):
|
|
1689
1710
|
return None
|
|
1690
1711
|
if UNPARSEABLE_CREATION.search(text):
|
|
1691
1712
|
return (
|
|
@@ -162,6 +162,37 @@ describes yours:
|
|
|
162
162
|
EOF
|
|
163
163
|
)"
|
|
164
164
|
|
|
165
|
+
# How to preserve uncommitted work before an operation that would discard it.
|
|
166
|
+
#
|
|
167
|
+
# This is appended to the reasons that refuse a discarding operation, and its
|
|
168
|
+
# whole point is what it does NOT say (issue #3722). The obvious advice is
|
|
169
|
+
# "stash it first", and on a single-checkout repository that is fine. Here it
|
|
170
|
+
# is the most dangerous thing an agent can be told: the stash is ONE STACK PER
|
|
171
|
+
# CLONE, shared by every worktree, so with concurrent agents a sibling's push
|
|
172
|
+
# can shift your entry and a sibling's pop can consume it. `lint-staged` pushes
|
|
173
|
+
# one on every commit too, so agent and tooling entries interleave.
|
|
174
|
+
#
|
|
175
|
+
# A remedy fires at the exact moment an agent is holding the work most worth
|
|
176
|
+
# protecting, and a guard's own output reads as authoritative — so pointing it
|
|
177
|
+
# at shared mutable state there is worse than the refusal it accompanies.
|
|
178
|
+
#
|
|
179
|
+
# The patch file is per-worktree: `mktemp` names it, so nothing has to invent a
|
|
180
|
+
# filename, and it lives in this agent's own TMPDIR where no sibling can reach
|
|
181
|
+
# it. Untracked files are unaffected by both refused operations, so `git diff`
|
|
182
|
+
# captures everything at risk.
|
|
183
|
+
PRESERVE_GUIDANCE="$(
|
|
184
|
+
cat <<'EOF'
|
|
185
|
+
Preserve the work first, per-worktree:
|
|
186
|
+
|
|
187
|
+
git diff [-- <path>] > "$(mktemp "${TMPDIR:-/tmp}/lisa-preserve-XXXXXX.patch")"
|
|
188
|
+
|
|
189
|
+
and restore it later with `git apply <that file>`.
|
|
190
|
+
|
|
191
|
+
Do NOT reach for `git stash`. One stash stack is shared by every worktree of a
|
|
192
|
+
clone, so a concurrently running agent can consume the entry you just pushed.
|
|
193
|
+
EOF
|
|
194
|
+
)"
|
|
195
|
+
|
|
165
196
|
# block() prints the reason to stderr (surfaced to the model) and exits 2 so the
|
|
166
197
|
# Bash tool call is denied. $1 = human-readable reason for the block.
|
|
167
198
|
#
|
|
@@ -1574,7 +1605,9 @@ done <<<"$push_segments"
|
|
|
1574
1605
|
if matches "${GIT_CMD}"'reset\b.*--(hard|merge)\b'; then
|
|
1575
1606
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
|
1576
1607
|
&& [ -n "$(git status --porcelain 2>/dev/null)" ]; then
|
|
1577
|
-
block "git reset --hard/--merge on a dirty working tree would discard uncommitted changes (
|
|
1608
|
+
block "git reset --hard/--merge on a dirty working tree would discard uncommitted changes (commit first, or preserve as below)" "$PRESERVE_GUIDANCE
|
|
1609
|
+
|
|
1610
|
+
$DESTRUCTIVE_GUIDANCE"
|
|
1578
1611
|
fi
|
|
1579
1612
|
fi
|
|
1580
1613
|
|
|
@@ -1624,7 +1657,9 @@ if matches "${GIT_CHECKOUT}${GIT_TOKENS}"'--([[:space:]]|$)' \
|
|
|
1624
1657
|
|| matches "${GIT_CHECKOUT}${GIT_TOKENS}"'(-[[:alnum:]]*f[[:alnum:]]*|--force)([[:space:]]|=|$)' \
|
|
1625
1658
|
|| matches "${GIT_CHECKOUT}"'[[:space:]][^;&|]*--pathspec-from-file' \
|
|
1626
1659
|
|| matches "${GIT_CHECKOUT}"'[[:space:]]+\.(/)?([[:space:]]|$)'; then
|
|
1627
|
-
block "git checkout discarding local changes (--, -f/--force, --pathspec-from-file, or bare .)
|
|
1660
|
+
block "git checkout discarding local changes (--, -f/--force, --pathspec-from-file, or bare .)" "$PRESERVE_GUIDANCE
|
|
1661
|
+
|
|
1662
|
+
$DESTRUCTIVE_GUIDANCE"
|
|
1628
1663
|
fi
|
|
1629
1664
|
|
|
1630
1665
|
# 5. `git switch` discards: `--discard-changes` and its `-f/--force` aliases.
|
|
@@ -1685,7 +1685,28 @@ def scan(text, depth, from_file=False):
|
|
|
1685
1685
|
# waved through: an unbalanced quote inside a script is the identical
|
|
1686
1686
|
# two-character trick moved one file away, and skipping it would hand
|
|
1687
1687
|
# the bypass straight back.
|
|
1688
|
-
|
|
1688
|
+
#
|
|
1689
|
+
# The declaration check is NOT gated on `from_file`, and that is the
|
|
1690
|
+
# #3727 fix. Tokenisation is what feeds every argv-based check below,
|
|
1691
|
+
# so when it throws, a TYPED command was previously judged with no
|
|
1692
|
+
# declaration consulted at all — not its `--body-file`, not even a
|
|
1693
|
+
# plain `--label <ready role>` sitting in argv. A correctly declared
|
|
1694
|
+
# filing was refused for the unrelated reason that its command happened
|
|
1695
|
+
# not to lex, and told "this filing declares no readiness", which was
|
|
1696
|
+
# false. Reading the text here is the only check available once `shlex`
|
|
1697
|
+
# has failed.
|
|
1698
|
+
#
|
|
1699
|
+
# It cannot reopen the bypass the paragraph above defends. That bypass
|
|
1700
|
+
# is an UNDECLARED create hiding behind a quote, and an undeclared
|
|
1701
|
+
# create has no marker and no `--label <ready role>` for
|
|
1702
|
+
# `text_declares_readiness` to match — which is asserted, not assumed,
|
|
1703
|
+
# by the rejection controls covering a bare create, the role mentioned
|
|
1704
|
+
# in prose, and the role appearing inside a URL.
|
|
1705
|
+
#
|
|
1706
|
+
# A marker in a SEPARATE `--body-file` stays refused, because finding
|
|
1707
|
+
# that operand needs the tokenisation that just failed. That case is
|
|
1708
|
+
# reachable by declaring inline instead, which this change makes work.
|
|
1709
|
+
if text_declares_readiness(text):
|
|
1689
1710
|
return None
|
|
1690
1711
|
if UNPARSEABLE_CREATION.search(text):
|
|
1691
1712
|
return (
|
|
@@ -162,6 +162,37 @@ describes yours:
|
|
|
162
162
|
EOF
|
|
163
163
|
)"
|
|
164
164
|
|
|
165
|
+
# How to preserve uncommitted work before an operation that would discard it.
|
|
166
|
+
#
|
|
167
|
+
# This is appended to the reasons that refuse a discarding operation, and its
|
|
168
|
+
# whole point is what it does NOT say (issue #3722). The obvious advice is
|
|
169
|
+
# "stash it first", and on a single-checkout repository that is fine. Here it
|
|
170
|
+
# is the most dangerous thing an agent can be told: the stash is ONE STACK PER
|
|
171
|
+
# CLONE, shared by every worktree, so with concurrent agents a sibling's push
|
|
172
|
+
# can shift your entry and a sibling's pop can consume it. `lint-staged` pushes
|
|
173
|
+
# one on every commit too, so agent and tooling entries interleave.
|
|
174
|
+
#
|
|
175
|
+
# A remedy fires at the exact moment an agent is holding the work most worth
|
|
176
|
+
# protecting, and a guard's own output reads as authoritative — so pointing it
|
|
177
|
+
# at shared mutable state there is worse than the refusal it accompanies.
|
|
178
|
+
#
|
|
179
|
+
# The patch file is per-worktree: `mktemp` names it, so nothing has to invent a
|
|
180
|
+
# filename, and it lives in this agent's own TMPDIR where no sibling can reach
|
|
181
|
+
# it. Untracked files are unaffected by both refused operations, so `git diff`
|
|
182
|
+
# captures everything at risk.
|
|
183
|
+
PRESERVE_GUIDANCE="$(
|
|
184
|
+
cat <<'EOF'
|
|
185
|
+
Preserve the work first, per-worktree:
|
|
186
|
+
|
|
187
|
+
git diff [-- <path>] > "$(mktemp "${TMPDIR:-/tmp}/lisa-preserve-XXXXXX.patch")"
|
|
188
|
+
|
|
189
|
+
and restore it later with `git apply <that file>`.
|
|
190
|
+
|
|
191
|
+
Do NOT reach for `git stash`. One stash stack is shared by every worktree of a
|
|
192
|
+
clone, so a concurrently running agent can consume the entry you just pushed.
|
|
193
|
+
EOF
|
|
194
|
+
)"
|
|
195
|
+
|
|
165
196
|
# block() prints the reason to stderr (surfaced to the model) and exits 2 so the
|
|
166
197
|
# Bash tool call is denied. $1 = human-readable reason for the block.
|
|
167
198
|
#
|
|
@@ -1574,7 +1605,9 @@ done <<<"$push_segments"
|
|
|
1574
1605
|
if matches "${GIT_CMD}"'reset\b.*--(hard|merge)\b'; then
|
|
1575
1606
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
|
1576
1607
|
&& [ -n "$(git status --porcelain 2>/dev/null)" ]; then
|
|
1577
|
-
block "git reset --hard/--merge on a dirty working tree would discard uncommitted changes (
|
|
1608
|
+
block "git reset --hard/--merge on a dirty working tree would discard uncommitted changes (commit first, or preserve as below)" "$PRESERVE_GUIDANCE
|
|
1609
|
+
|
|
1610
|
+
$DESTRUCTIVE_GUIDANCE"
|
|
1578
1611
|
fi
|
|
1579
1612
|
fi
|
|
1580
1613
|
|
|
@@ -1624,7 +1657,9 @@ if matches "${GIT_CHECKOUT}${GIT_TOKENS}"'--([[:space:]]|$)' \
|
|
|
1624
1657
|
|| matches "${GIT_CHECKOUT}${GIT_TOKENS}"'(-[[:alnum:]]*f[[:alnum:]]*|--force)([[:space:]]|=|$)' \
|
|
1625
1658
|
|| matches "${GIT_CHECKOUT}"'[[:space:]][^;&|]*--pathspec-from-file' \
|
|
1626
1659
|
|| matches "${GIT_CHECKOUT}"'[[:space:]]+\.(/)?([[:space:]]|$)'; then
|
|
1627
|
-
block "git checkout discarding local changes (--, -f/--force, --pathspec-from-file, or bare .)
|
|
1660
|
+
block "git checkout discarding local changes (--, -f/--force, --pathspec-from-file, or bare .)" "$PRESERVE_GUIDANCE
|
|
1661
|
+
|
|
1662
|
+
$DESTRUCTIVE_GUIDANCE"
|
|
1628
1663
|
fi
|
|
1629
1664
|
|
|
1630
1665
|
# 5. `git switch` discards: `--discard-changes` and its `-f/--force` aliases.
|
|
@@ -1685,7 +1685,28 @@ def scan(text, depth, from_file=False):
|
|
|
1685
1685
|
# waved through: an unbalanced quote inside a script is the identical
|
|
1686
1686
|
# two-character trick moved one file away, and skipping it would hand
|
|
1687
1687
|
# the bypass straight back.
|
|
1688
|
-
|
|
1688
|
+
#
|
|
1689
|
+
# The declaration check is NOT gated on `from_file`, and that is the
|
|
1690
|
+
# #3727 fix. Tokenisation is what feeds every argv-based check below,
|
|
1691
|
+
# so when it throws, a TYPED command was previously judged with no
|
|
1692
|
+
# declaration consulted at all — not its `--body-file`, not even a
|
|
1693
|
+
# plain `--label <ready role>` sitting in argv. A correctly declared
|
|
1694
|
+
# filing was refused for the unrelated reason that its command happened
|
|
1695
|
+
# not to lex, and told "this filing declares no readiness", which was
|
|
1696
|
+
# false. Reading the text here is the only check available once `shlex`
|
|
1697
|
+
# has failed.
|
|
1698
|
+
#
|
|
1699
|
+
# It cannot reopen the bypass the paragraph above defends. That bypass
|
|
1700
|
+
# is an UNDECLARED create hiding behind a quote, and an undeclared
|
|
1701
|
+
# create has no marker and no `--label <ready role>` for
|
|
1702
|
+
# `text_declares_readiness` to match — which is asserted, not assumed,
|
|
1703
|
+
# by the rejection controls covering a bare create, the role mentioned
|
|
1704
|
+
# in prose, and the role appearing inside a URL.
|
|
1705
|
+
#
|
|
1706
|
+
# A marker in a SEPARATE `--body-file` stays refused, because finding
|
|
1707
|
+
# that operand needs the tokenisation that just failed. That case is
|
|
1708
|
+
# reachable by declaring inline instead, which this change makes work.
|
|
1709
|
+
if text_declares_readiness(text):
|
|
1689
1710
|
return None
|
|
1690
1711
|
if UNPARSEABLE_CREATION.search(text):
|
|
1691
1712
|
return (
|
|
@@ -162,6 +162,37 @@ describes yours:
|
|
|
162
162
|
EOF
|
|
163
163
|
)"
|
|
164
164
|
|
|
165
|
+
# How to preserve uncommitted work before an operation that would discard it.
|
|
166
|
+
#
|
|
167
|
+
# This is appended to the reasons that refuse a discarding operation, and its
|
|
168
|
+
# whole point is what it does NOT say (issue #3722). The obvious advice is
|
|
169
|
+
# "stash it first", and on a single-checkout repository that is fine. Here it
|
|
170
|
+
# is the most dangerous thing an agent can be told: the stash is ONE STACK PER
|
|
171
|
+
# CLONE, shared by every worktree, so with concurrent agents a sibling's push
|
|
172
|
+
# can shift your entry and a sibling's pop can consume it. `lint-staged` pushes
|
|
173
|
+
# one on every commit too, so agent and tooling entries interleave.
|
|
174
|
+
#
|
|
175
|
+
# A remedy fires at the exact moment an agent is holding the work most worth
|
|
176
|
+
# protecting, and a guard's own output reads as authoritative — so pointing it
|
|
177
|
+
# at shared mutable state there is worse than the refusal it accompanies.
|
|
178
|
+
#
|
|
179
|
+
# The patch file is per-worktree: `mktemp` names it, so nothing has to invent a
|
|
180
|
+
# filename, and it lives in this agent's own TMPDIR where no sibling can reach
|
|
181
|
+
# it. Untracked files are unaffected by both refused operations, so `git diff`
|
|
182
|
+
# captures everything at risk.
|
|
183
|
+
PRESERVE_GUIDANCE="$(
|
|
184
|
+
cat <<'EOF'
|
|
185
|
+
Preserve the work first, per-worktree:
|
|
186
|
+
|
|
187
|
+
git diff [-- <path>] > "$(mktemp "${TMPDIR:-/tmp}/lisa-preserve-XXXXXX.patch")"
|
|
188
|
+
|
|
189
|
+
and restore it later with `git apply <that file>`.
|
|
190
|
+
|
|
191
|
+
Do NOT reach for `git stash`. One stash stack is shared by every worktree of a
|
|
192
|
+
clone, so a concurrently running agent can consume the entry you just pushed.
|
|
193
|
+
EOF
|
|
194
|
+
)"
|
|
195
|
+
|
|
165
196
|
# block() prints the reason to stderr (surfaced to the model) and exits 2 so the
|
|
166
197
|
# Bash tool call is denied. $1 = human-readable reason for the block.
|
|
167
198
|
#
|
|
@@ -1574,7 +1605,9 @@ done <<<"$push_segments"
|
|
|
1574
1605
|
if matches "${GIT_CMD}"'reset\b.*--(hard|merge)\b'; then
|
|
1575
1606
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
|
1576
1607
|
&& [ -n "$(git status --porcelain 2>/dev/null)" ]; then
|
|
1577
|
-
block "git reset --hard/--merge on a dirty working tree would discard uncommitted changes (
|
|
1608
|
+
block "git reset --hard/--merge on a dirty working tree would discard uncommitted changes (commit first, or preserve as below)" "$PRESERVE_GUIDANCE
|
|
1609
|
+
|
|
1610
|
+
$DESTRUCTIVE_GUIDANCE"
|
|
1578
1611
|
fi
|
|
1579
1612
|
fi
|
|
1580
1613
|
|
|
@@ -1624,7 +1657,9 @@ if matches "${GIT_CHECKOUT}${GIT_TOKENS}"'--([[:space:]]|$)' \
|
|
|
1624
1657
|
|| matches "${GIT_CHECKOUT}${GIT_TOKENS}"'(-[[:alnum:]]*f[[:alnum:]]*|--force)([[:space:]]|=|$)' \
|
|
1625
1658
|
|| matches "${GIT_CHECKOUT}"'[[:space:]][^;&|]*--pathspec-from-file' \
|
|
1626
1659
|
|| matches "${GIT_CHECKOUT}"'[[:space:]]+\.(/)?([[:space:]]|$)'; then
|
|
1627
|
-
block "git checkout discarding local changes (--, -f/--force, --pathspec-from-file, or bare .)
|
|
1660
|
+
block "git checkout discarding local changes (--, -f/--force, --pathspec-from-file, or bare .)" "$PRESERVE_GUIDANCE
|
|
1661
|
+
|
|
1662
|
+
$DESTRUCTIVE_GUIDANCE"
|
|
1628
1663
|
fi
|
|
1629
1664
|
|
|
1630
1665
|
# 5. `git switch` discards: `--discard-changes` and its `-f/--force` aliases.
|