@codyswann/lisa 4.38.0 → 4.38.2
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-direct-issue-create.sh +140 -4
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +209 -8
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +10 -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 +13 -6
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +9 -0
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +19 -5
- package/plugins/lisa/.codex-plugin/hooks.json +18 -4
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-git-submit-pr/SKILL.md +8 -3
- package/plugins/lisa/hooks/block-direct-issue-create.agy.sh +13 -3
- package/plugins/lisa/hooks/block-direct-issue-create.sh +140 -4
- package/plugins/lisa/hooks/discharge-work-item-gates.sh +54 -0
- package/plugins/lisa/skills/lisa-git-submit-pr/SKILL.md +8 -3
- package/plugins/lisa-agy/hooks/block-direct-issue-create.agy.sh +13 -3
- package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +140 -4
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-git-submit-pr/SKILL.md +8 -3
- 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 +19 -5
- package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +140 -4
- package/plugins/lisa-copilot/hooks/discharge-work-item-gates.sh +54 -0
- package/plugins/lisa-copilot/skills/lisa-git-submit-pr/SKILL.md +8 -3
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +140 -4
- package/plugins/lisa-cursor/hooks/discharge-work-item-gates.sh +54 -0
- package/plugins/lisa-cursor/hooks/hooks.json +7 -4
- package/plugins/lisa-cursor/skills/lisa-git-submit-pr/SKILL.md +8 -3
- 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/.claude-plugin/plugin.json +18 -4
- package/plugins/src/base/hooks/block-direct-issue-create.agy.sh +13 -3
- package/plugins/src/base/hooks/block-direct-issue-create.sh +140 -4
- package/plugins/src/base/hooks/discharge-work-item-gates.sh +54 -0
- package/plugins/src/base/skills/lisa-git-submit-pr/SKILL.md +8 -3
- package/scripts/lib/per-agent-hook-filter.mjs +19 -0
|
@@ -57,6 +57,7 @@ Add provider-appropriate linkage to the PR title and/or body without changing th
|
|
|
57
57
|
- **GitHub Issues**:
|
|
58
58
|
- If `work_item_ref` is a GitHub issue URL, `org/repo#<n>`, or `#<n>`, add a dedicated issue reference line to the PR body.
|
|
59
59
|
- Always use a non-closing reference such as `Refs #<n>`, so the merge cannot close the issue before the post-merge deploy, remote verification, health check, and terminal `done` label.
|
|
60
|
+
- **This rule is what makes the managed backlink mandatory, and neither rule says so on its own.** `Refs` creates no native development link, so gate 5 has nothing to defer to and the `[lisa-pr-link]` comment becomes the only backlink there is. Anyone following the non-closing rule alone will hit gate 5. Separately, `Refs #<n>` does **not** satisfy gate 4 either: the body needs its own `Work-Item: <ref>` line, and `Closes`/`Fixes` do not satisfy it. One `Refs` line, one `Work-Item:` line, one backlink comment — three different requirements that look like one. `discharge-pr-gates` (below) is what checks the last two together.
|
|
60
61
|
- **A non-closing reference does not populate the issue's Development / linked pull requests surface, and no non-closing form does.** That surface *is* the closing-reference mechanism: a closing keyword populates the PR's `closingIssuesReferences`, while `Refs` yields only a `CrossReferencedEvent`. Measured on this repository — a `Refs`-only PR reports `closingIssuesReferences: 0`; a `Closes` PR reports 1. So the ticket-side backlink cannot be delegated to GitHub: the managed `[lisa-pr-link]` comment written by `node scripts/lisa-work-item.mjs backlink` — the one producer, which `lisa-github-sync` and the other vendor sync skills call rather than reimplement — is the **required** backlink under this rule, not a fallback for when native linkage happens to be absent. Two-way linkage (`lisa-implement` step 7a) depends on that comment, and so does the Work-Item Traceability check wherever the project declares `workItem.verify: "full"` — there, a PR carrying a correct `Refs` line still fails the check without it. Post it either way: under the default `trailer` level the check does not read the tracker, but the two-way linkage a human follows is still worth having, and a project can raise its level at any time.
|
|
61
62
|
- For cross-repo issue refs, use the fully qualified non-closing form, for example `Refs CodySwannGT/lisa#614`.
|
|
62
63
|
- **Linear**:
|
|
@@ -76,13 +77,17 @@ When updating an existing PR, preserve any existing linkage line unless the new
|
|
|
76
77
|
After creating or updating the PR, always make the reverse link durable on the source work item when `work_item_ref` is available:
|
|
77
78
|
|
|
78
79
|
1. Resolve the live PR URL with `gh pr view <pr-number> --json url --jq .url`.
|
|
79
|
-
2.
|
|
80
|
+
2. Discharge the two gates the push could not check. Gate 4 (the `Work-Item:` line in the PR BODY) and gate 5 (the backlink) are both properties of a pull request, so a push before the PR existed reported them as `UNRESOLVED` rather than checked. One command closes both out at the first moment they *are* checkable:
|
|
80
81
|
|
|
81
82
|
```bash
|
|
82
|
-
node scripts/lisa-work-item.mjs
|
|
83
|
+
node scripts/lisa-work-item.mjs discharge-pr-gates
|
|
83
84
|
```
|
|
84
85
|
|
|
85
|
-
It
|
|
86
|
+
It posts (or refreshes) the managed `[lisa-pr-link]` comment on every item the range names — so gate 5 passes without anyone remembering to — and then evaluates gate 4 against the live body, failing here rather than one CI cycle later. Exit 3 means there is no pull request for this branch yet, which is not a violation; exit 1 is a real unmet requirement and the message names it.
|
|
87
|
+
|
|
88
|
+
It writes no PR body. Gate 4 is the author's declaration of what the change is for; a command that inserted the line would be answering its own question.
|
|
89
|
+
|
|
90
|
+
The narrower `node scripts/lisa-work-item.mjs backlink --ref <work_item_ref> --pr-url <url>` is what it wraps, and remains the right call when you hold a ref and a URL but no checkout of the PR's branch (a sync skill updating a backlink after the fact). Both are idempotent, refuse loudly for a tracker they cannot write, and never silently no-op. Do not hand-post the comment, and do not describe the posting procedure anywhere: the same file that writes it is the file that checks it, which is what keeps producer and consumer from drifting.
|
|
86
91
|
3. Invoke `lisa-tracker-sync` with the original work item ref, milestone `pr-ready`, `pr_url=<url>`, and `tracker_provider=<provider>` when known. That is the progress-note and status side; it is not what satisfies the traceability check.
|
|
87
92
|
4. When the PR later merges, invoke `lisa-tracker-sync` again with milestone `pr-merged`, the same `pr_url`, and the merge SHA when available.
|
|
88
93
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa",
|
|
3
|
-
"version": "4.38.
|
|
3
|
+
"version": "4.38.2",
|
|
4
4
|
"description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -36,6 +36,15 @@
|
|
|
36
36
|
}
|
|
37
37
|
]
|
|
38
38
|
},
|
|
39
|
+
{
|
|
40
|
+
"matcher": "Bash",
|
|
41
|
+
"hooks": [
|
|
42
|
+
{
|
|
43
|
+
"type": "command",
|
|
44
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/discharge-work-item-gates.sh"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
39
48
|
{
|
|
40
49
|
"matcher": "Edit|Write|NotebookEdit|Bash",
|
|
41
50
|
"hooks": [
|
|
@@ -75,10 +84,6 @@
|
|
|
75
84
|
"type": "command",
|
|
76
85
|
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/block-instruction-file-edits.sh"
|
|
77
86
|
},
|
|
78
|
-
{
|
|
79
|
-
"type": "command",
|
|
80
|
-
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/block-direct-issue-create.sh"
|
|
81
|
-
},
|
|
82
87
|
{
|
|
83
88
|
"type": "command",
|
|
84
89
|
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/block-blind-automerge.sh"
|
|
@@ -106,6 +111,15 @@
|
|
|
106
111
|
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/enforce-verification-gate.sh"
|
|
107
112
|
}
|
|
108
113
|
]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"matcher": "",
|
|
117
|
+
"hooks": [
|
|
118
|
+
{
|
|
119
|
+
"type": "command",
|
|
120
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/block-direct-issue-create.sh"
|
|
121
|
+
}
|
|
122
|
+
]
|
|
109
123
|
}
|
|
110
124
|
],
|
|
111
125
|
"agentStop": [
|
|
@@ -79,6 +79,36 @@
|
|
|
79
79
|
# settings env block, CI config). An escape the governed agent reaches by
|
|
80
80
|
# typing one more token in front of the command it was just refused is not an
|
|
81
81
|
# escape hatch — it is the prose problem with extra steps.
|
|
82
|
+
#
|
|
83
|
+
# ## What this hook costs, and the number that would change the decision
|
|
84
|
+
#
|
|
85
|
+
# Registered `matcher: ""` since CodySwannGT/lisa#3753 — it runs on EVERY tool
|
|
86
|
+
# call, not only Bash. Measured on a developer machine at load ~8-26, 50
|
|
87
|
+
# invocations per row:
|
|
88
|
+
#
|
|
89
|
+
# 16.7 ms enforce-team-first.sh already `matcher: ""` today
|
|
90
|
+
# 17.5 ms enforce-verification-gate.sh already `matcher: ""` today
|
|
91
|
+
# 11.5 ms this guard, non-Bash payload what the broad matcher adds
|
|
92
|
+
# 112.3 ms this guard, Bash payload what Bash calls already paid
|
|
93
|
+
#
|
|
94
|
+
# So the broad registration was not novel — two hooks were already there, this
|
|
95
|
+
# one is cheaper than either, and it raises an existing ~34 ms per-call baseline
|
|
96
|
+
# by about a third.
|
|
97
|
+
#
|
|
98
|
+
# THIS IS A THRESHOLD, NOT A PRECEDENT. The argument is about the TOTAL cost of
|
|
99
|
+
# the broad set, so it does not transfer to the next guard: four more hooks
|
|
100
|
+
# moved to `matcher: ""` would put ~90 ms on every tool call and the answer
|
|
101
|
+
# flips. **Before widening any other hook's matcher, re-run the measurement and
|
|
102
|
+
# reconsider if the broad set totals more than ~100 ms per tool call, or if any
|
|
103
|
+
# single broad hook exceeds ~50 ms.** Do not cite these numbers for a fifth
|
|
104
|
+
# hook; measure again, because the number that matters is the sum.
|
|
105
|
+
#
|
|
106
|
+
# The cost is also irreducible rather than sloppy. A 9-line script containing
|
|
107
|
+
# only this file's preamble measured 11.7 ms, and padding it to this file's
|
|
108
|
+
# length changed nothing (11.5 ms) — it is process spawn plus one `jq`, not
|
|
109
|
+
# parsing. Moving the early exit further up the file buys nothing; the exit is
|
|
110
|
+
# already ahead of config resolution, which is what keeps the figure at 11.5
|
|
111
|
+
# rather than at the 112 ms the Bash path pays.
|
|
82
112
|
set -euo pipefail
|
|
83
113
|
|
|
84
114
|
input="$(cat)"
|
|
@@ -99,13 +129,72 @@ for required in jq python3; do
|
|
|
99
129
|
done
|
|
100
130
|
|
|
101
131
|
tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)"
|
|
132
|
+
|
|
133
|
+
# ── The two substrates ────────────────────────────────────────────────────
|
|
134
|
+
#
|
|
135
|
+
# A creation reaches a tracker two ways, and until CodySwannGT/lisa#3753 this
|
|
136
|
+
# guard saw only one of them. It was registered `matcher: "Bash"` AND gated
|
|
137
|
+
# here on `tool_name != "Bash"`, so an MCP tool call was refused entry twice
|
|
138
|
+
# over. MEASURED before the change, driving the guard with synthetic payloads:
|
|
139
|
+
#
|
|
140
|
+
# ALLOW mcp__linear-server__create_issue, undeclared <- the defect
|
|
141
|
+
# ALLOW mcp__github__create_issue, undeclared <- the defect
|
|
142
|
+
# REFUSE the same creation as `gh issue create` shell text <- control
|
|
143
|
+
# ALLOW an ordinary Read tool call <- control
|
|
144
|
+
#
|
|
145
|
+
# The two controls are what make those ALLOWs mean anything: the refusal proves
|
|
146
|
+
# the guard is live, the Read proves it is not simply refusing everything.
|
|
147
|
+
#
|
|
148
|
+
# **Widening the matcher alone would have changed nothing** — the call would
|
|
149
|
+
# arrive and this gate would still exit 0. Anyone who "fixed" #3753 by editing
|
|
150
|
+
# `plugin.json` would have seen no test fail and shipped a guard that still
|
|
151
|
+
# allows every MCP filing. The registration and this gate had to move together.
|
|
152
|
+
#
|
|
153
|
+
# The SHELL path below is untouched. A structured payload has no command line
|
|
154
|
+
# to tokenise, so it gets its own small classifier rather than being forced
|
|
155
|
+
# through a parser built for shell text.
|
|
156
|
+
structured_call=""
|
|
102
157
|
if [ "$tool_name" != "Bash" ]; then
|
|
103
|
-
|
|
158
|
+
# The cheap shape gate, deliberately BEFORE config resolution.
|
|
159
|
+
#
|
|
160
|
+
# This hook now runs on EVERY tool call, so the cost of the path that does
|
|
161
|
+
# nothing is the cost of the whole change. Config resolution spawns two jq
|
|
162
|
+
# processes per lookup and is what makes the Bash path ~112 ms; putting it
|
|
163
|
+
# ahead of this gate would charge that to every Read and Grep. So the
|
|
164
|
+
# not-a-creation exit happens here, on a shell `case` with no subprocess.
|
|
165
|
+
#
|
|
166
|
+
# Matched on SHAPE, not on an enumerated list of server tool names. Server
|
|
167
|
+
# names are supplied by whoever wrote the server and change when one is
|
|
168
|
+
# added or renamed, so a list would pass every row anyone thought of and
|
|
169
|
+
# miss the first one nobody did — the failure mode this repository's guard
|
|
170
|
+
# suites are written to defeat.
|
|
171
|
+
#
|
|
172
|
+
# RESIDUAL, stated rather than hidden: a server whose creation tool is named
|
|
173
|
+
# without a create-verb or without a tracker noun (`mcp__x__file_work`) is
|
|
174
|
+
# not recognised and is allowed. That is a fail-open, and it is the honest
|
|
175
|
+
# cost of refusing to enumerate. Add shapes here as they are measured.
|
|
176
|
+
case "$tool_name" in
|
|
177
|
+
Bash | Read | Write | Edit | MultiEdit | Glob | Grep | Task | TodoWrite)
|
|
178
|
+
exit 0
|
|
179
|
+
;;
|
|
180
|
+
esac
|
|
181
|
+
case "$tool_name" in
|
|
182
|
+
*[Cc]reate* | *[Nn]ew* | *[Aa]dd* | *[Ff]ile*) ;;
|
|
183
|
+
*) exit 0 ;;
|
|
184
|
+
esac
|
|
185
|
+
case "$tool_name" in
|
|
186
|
+
*[Ii]ssue* | *[Tt]icket* | *[Tt]ask* | *[Ss]tory* | *[Bb]ug* | *[Ee]pic* | *[Ww]ork*) ;;
|
|
187
|
+
*) exit 0 ;;
|
|
188
|
+
esac
|
|
189
|
+
structured_call="1"
|
|
104
190
|
fi
|
|
105
191
|
|
|
106
|
-
command_str="
|
|
107
|
-
if [ -z "$
|
|
108
|
-
|
|
192
|
+
command_str=""
|
|
193
|
+
if [ -z "$structured_call" ]; then
|
|
194
|
+
command_str="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
|
|
195
|
+
if [ -z "$command_str" ]; then
|
|
196
|
+
exit 0
|
|
197
|
+
fi
|
|
109
198
|
fi
|
|
110
199
|
|
|
111
200
|
project_dir="${CLAUDE_PROJECT_DIR:-}"
|
|
@@ -1859,6 +1948,53 @@ print("ALLOW")
|
|
|
1859
1948
|
PY
|
|
1860
1949
|
|
|
1861
1950
|
set +e
|
|
1951
|
+
# ── The structured substrate ──────────────────────────────────────────────
|
|
1952
|
+
#
|
|
1953
|
+
# An MCP call carries named fields, not a command line, so `scan()` — which
|
|
1954
|
+
# tokenises shell text — has nothing to parse. The declaration is read from the
|
|
1955
|
+
# payload instead, and the recogniser is deliberately small: this path decides
|
|
1956
|
+
# ONE question (is a build-ready role or a human-gate marker present anywhere
|
|
1957
|
+
# in the submitted fields), where the shell path has to answer "which of these
|
|
1958
|
+
# tokens is a client, an endpoint, a flag value, or a file it executes".
|
|
1959
|
+
#
|
|
1960
|
+
# Every string in the payload is searched, at any depth, because a role lands
|
|
1961
|
+
# in a different field on every tracker — `labels[]` on GitHub, `stateId` or a
|
|
1962
|
+
# workflow state name on Linear, a transition `id` on JIRA — and enumerating
|
|
1963
|
+
# field names per vendor is the same brittleness as enumerating tool names.
|
|
1964
|
+
# Over-collecting is safe here: the values are compared against ONE configured
|
|
1965
|
+
# role string, so an unrelated field cannot accidentally satisfy it.
|
|
1966
|
+
if [ -n "$structured_call" ]; then
|
|
1967
|
+
# The operator's ambient escape works on both substrates. There is no inline
|
|
1968
|
+
# form to disqualify it here — a structured call has no shell in which to
|
|
1969
|
+
# assign one — so the override is simply honoured.
|
|
1970
|
+
if [ -n "$ambient_override" ]; then
|
|
1971
|
+
exit 0
|
|
1972
|
+
fi
|
|
1973
|
+
|
|
1974
|
+
structured_declaration="$(
|
|
1975
|
+
printf '%s' "$input" |
|
|
1976
|
+
jq -r --arg role "$ready_role" '
|
|
1977
|
+
[(.tool_input // {}) | .. | strings] as $values
|
|
1978
|
+
| if ($values | index($role)) then "role"
|
|
1979
|
+
elif ($values | map(select(contains("[lisa-human-gate]"))) | length) > 0 then "gate"
|
|
1980
|
+
else "" end
|
|
1981
|
+
' 2>/dev/null || printf 'UNREADABLE'
|
|
1982
|
+
)"
|
|
1983
|
+
|
|
1984
|
+
# Fail closed on a creation-shaped call whose payload cannot be read. Silence
|
|
1985
|
+
# about a filing the guard could not inspect is the defect this whole ticket
|
|
1986
|
+
# is about, one substrate over.
|
|
1987
|
+
if [ "$structured_declaration" = "UNREADABLE" ]; then
|
|
1988
|
+
refuse "an unreadable $tool_name payload that reads as a tracker creation" \
|
|
1989
|
+
"$ready_role" ""
|
|
1990
|
+
fi
|
|
1991
|
+
|
|
1992
|
+
if [ -z "$structured_declaration" ]; then
|
|
1993
|
+
refuse "a tracker creation through $tool_name" "$ready_role" ""
|
|
1994
|
+
fi
|
|
1995
|
+
exit 0
|
|
1996
|
+
fi
|
|
1997
|
+
|
|
1862
1998
|
verdict="$(
|
|
1863
1999
|
printf '%s' "$classifier" |
|
|
1864
2000
|
LISA_GUARD_COMMAND="$command_str" \
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# This file is managed by Lisa.
|
|
3
|
+
# Do not edit directly — changes will be overwritten on the next `lisa` run.
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Discharge Deferred Work-Item Gates Hook (PostToolUse - Bash)
|
|
6
|
+
# =============================================================================
|
|
7
|
+
# Two of the five Work-Item Traceability gates live OUTSIDE the commits: gate 4
|
|
8
|
+
# is the `Work-Item:` line in the pull-request BODY, and gate 5 is the managed
|
|
9
|
+
# `[lisa-pr-link]` backlink on the item. A push cannot check either one —
|
|
10
|
+
# both are properties of a pull request, and the push is what makes the pull
|
|
11
|
+
# request possible — so until this hook existed the next thing that looked was
|
|
12
|
+
# CI, one cycle later (CodySwannGT/lisa#3791).
|
|
13
|
+
#
|
|
14
|
+
# This fires the moment a pull request is created or its body edited, which is
|
|
15
|
+
# the FIRST moment both gates are checkable. It evaluates them, and posts the
|
|
16
|
+
# backlink that gate 5 needs, so neither waits for a red CI run to be revealed.
|
|
17
|
+
#
|
|
18
|
+
# Exit 2 blocks and hands the output back to the agent, which is the point: a
|
|
19
|
+
# non-blocking notice here would be the same defect the discharge exists to
|
|
20
|
+
# fix — a finding delivered somewhere nobody has to act on it.
|
|
21
|
+
# =============================================================================
|
|
22
|
+
set -uo pipefail
|
|
23
|
+
|
|
24
|
+
JSON_INPUT="$(cat)"
|
|
25
|
+
|
|
26
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
27
|
+
command -v node >/dev/null 2>&1 || exit 0
|
|
28
|
+
|
|
29
|
+
COMMAND="$(printf '%s' "$JSON_INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
|
|
30
|
+
[ -n "$COMMAND" ] || exit 0
|
|
31
|
+
|
|
32
|
+
# `gh pr create` opens one; `gh pr edit` is the other way a body comes to carry
|
|
33
|
+
# — or lose — its declaration. Both change the answer to gate 4.
|
|
34
|
+
case "$COMMAND" in
|
|
35
|
+
*"gh pr create"* | *"gh pr edit"*) ;;
|
|
36
|
+
*) exit 0 ;;
|
|
37
|
+
esac
|
|
38
|
+
|
|
39
|
+
repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
|
|
40
|
+
[ -f "$repo_root/scripts/lisa-work-item.mjs" ] || exit 0
|
|
41
|
+
|
|
42
|
+
output="$(cd "$repo_root" && node scripts/lisa-work-item.mjs discharge-pr-gates 2>&1)"
|
|
43
|
+
status=$?
|
|
44
|
+
|
|
45
|
+
# 0 is a clean discharge. 3 is the validator's own answer for "no pull request
|
|
46
|
+
# to check yet", which PostToolUse reaches on every `gh pr create` that FAILED
|
|
47
|
+
# — the tool call still ran, so the hook still fires. Collapsing 3 into the
|
|
48
|
+
# blocking arm would report a work-item violation on somebody's typo.
|
|
49
|
+
case "$status" in
|
|
50
|
+
0 | 3) exit 0 ;;
|
|
51
|
+
esac
|
|
52
|
+
|
|
53
|
+
printf '%s\n' "$output" >&2
|
|
54
|
+
exit 2
|
|
@@ -57,6 +57,7 @@ Add provider-appropriate linkage to the PR title and/or body without changing th
|
|
|
57
57
|
- **GitHub Issues**:
|
|
58
58
|
- If `work_item_ref` is a GitHub issue URL, `org/repo#<n>`, or `#<n>`, add a dedicated issue reference line to the PR body.
|
|
59
59
|
- Always use a non-closing reference such as `Refs #<n>`, so the merge cannot close the issue before the post-merge deploy, remote verification, health check, and terminal `done` label.
|
|
60
|
+
- **This rule is what makes the managed backlink mandatory, and neither rule says so on its own.** `Refs` creates no native development link, so gate 5 has nothing to defer to and the `[lisa-pr-link]` comment becomes the only backlink there is. Anyone following the non-closing rule alone will hit gate 5. Separately, `Refs #<n>` does **not** satisfy gate 4 either: the body needs its own `Work-Item: <ref>` line, and `Closes`/`Fixes` do not satisfy it. One `Refs` line, one `Work-Item:` line, one backlink comment — three different requirements that look like one. `discharge-pr-gates` (below) is what checks the last two together.
|
|
60
61
|
- **A non-closing reference does not populate the issue's Development / linked pull requests surface, and no non-closing form does.** That surface *is* the closing-reference mechanism: a closing keyword populates the PR's `closingIssuesReferences`, while `Refs` yields only a `CrossReferencedEvent`. Measured on this repository — a `Refs`-only PR reports `closingIssuesReferences: 0`; a `Closes` PR reports 1. So the ticket-side backlink cannot be delegated to GitHub: the managed `[lisa-pr-link]` comment written by `node scripts/lisa-work-item.mjs backlink` — the one producer, which `lisa-github-sync` and the other vendor sync skills call rather than reimplement — is the **required** backlink under this rule, not a fallback for when native linkage happens to be absent. Two-way linkage (`lisa-implement` step 7a) depends on that comment, and so does the Work-Item Traceability check wherever the project declares `workItem.verify: "full"` — there, a PR carrying a correct `Refs` line still fails the check without it. Post it either way: under the default `trailer` level the check does not read the tracker, but the two-way linkage a human follows is still worth having, and a project can raise its level at any time.
|
|
61
62
|
- For cross-repo issue refs, use the fully qualified non-closing form, for example `Refs CodySwannGT/lisa#614`.
|
|
62
63
|
- **Linear**:
|
|
@@ -76,13 +77,17 @@ When updating an existing PR, preserve any existing linkage line unless the new
|
|
|
76
77
|
After creating or updating the PR, always make the reverse link durable on the source work item when `work_item_ref` is available:
|
|
77
78
|
|
|
78
79
|
1. Resolve the live PR URL with `gh pr view <pr-number> --json url --jq .url`.
|
|
79
|
-
2.
|
|
80
|
+
2. Discharge the two gates the push could not check. Gate 4 (the `Work-Item:` line in the PR BODY) and gate 5 (the backlink) are both properties of a pull request, so a push before the PR existed reported them as `UNRESOLVED` rather than checked. One command closes both out at the first moment they *are* checkable:
|
|
80
81
|
|
|
81
82
|
```bash
|
|
82
|
-
node scripts/lisa-work-item.mjs
|
|
83
|
+
node scripts/lisa-work-item.mjs discharge-pr-gates
|
|
83
84
|
```
|
|
84
85
|
|
|
85
|
-
It
|
|
86
|
+
It posts (or refreshes) the managed `[lisa-pr-link]` comment on every item the range names — so gate 5 passes without anyone remembering to — and then evaluates gate 4 against the live body, failing here rather than one CI cycle later. Exit 3 means there is no pull request for this branch yet, which is not a violation; exit 1 is a real unmet requirement and the message names it.
|
|
87
|
+
|
|
88
|
+
It writes no PR body. Gate 4 is the author's declaration of what the change is for; a command that inserted the line would be answering its own question.
|
|
89
|
+
|
|
90
|
+
The narrower `node scripts/lisa-work-item.mjs backlink --ref <work_item_ref> --pr-url <url>` is what it wraps, and remains the right call when you hold a ref and a URL but no checkout of the PR's branch (a sync skill updating a backlink after the fact). Both are idempotent, refuse loudly for a tracker they cannot write, and never silently no-op. Do not hand-post the comment, and do not describe the posting procedure anywhere: the same file that writes it is the file that checks it, which is what keeps producer and consumer from drifting.
|
|
86
91
|
3. Invoke `lisa-tracker-sync` with the original work item ref, milestone `pr-ready`, `pr_url=<url>`, and `tracker_provider=<provider>` when known. That is the progress-note and status side; it is not what satisfies the traceability check.
|
|
87
92
|
4. When the PR later merges, invoke `lisa-tracker-sync` again with milestone `pr-merged`, the same `pr_url`, and the merge SHA when available.
|
|
88
93
|
|
|
@@ -79,6 +79,36 @@
|
|
|
79
79
|
# settings env block, CI config). An escape the governed agent reaches by
|
|
80
80
|
# typing one more token in front of the command it was just refused is not an
|
|
81
81
|
# escape hatch — it is the prose problem with extra steps.
|
|
82
|
+
#
|
|
83
|
+
# ## What this hook costs, and the number that would change the decision
|
|
84
|
+
#
|
|
85
|
+
# Registered `matcher: ""` since CodySwannGT/lisa#3753 — it runs on EVERY tool
|
|
86
|
+
# call, not only Bash. Measured on a developer machine at load ~8-26, 50
|
|
87
|
+
# invocations per row:
|
|
88
|
+
#
|
|
89
|
+
# 16.7 ms enforce-team-first.sh already `matcher: ""` today
|
|
90
|
+
# 17.5 ms enforce-verification-gate.sh already `matcher: ""` today
|
|
91
|
+
# 11.5 ms this guard, non-Bash payload what the broad matcher adds
|
|
92
|
+
# 112.3 ms this guard, Bash payload what Bash calls already paid
|
|
93
|
+
#
|
|
94
|
+
# So the broad registration was not novel — two hooks were already there, this
|
|
95
|
+
# one is cheaper than either, and it raises an existing ~34 ms per-call baseline
|
|
96
|
+
# by about a third.
|
|
97
|
+
#
|
|
98
|
+
# THIS IS A THRESHOLD, NOT A PRECEDENT. The argument is about the TOTAL cost of
|
|
99
|
+
# the broad set, so it does not transfer to the next guard: four more hooks
|
|
100
|
+
# moved to `matcher: ""` would put ~90 ms on every tool call and the answer
|
|
101
|
+
# flips. **Before widening any other hook's matcher, re-run the measurement and
|
|
102
|
+
# reconsider if the broad set totals more than ~100 ms per tool call, or if any
|
|
103
|
+
# single broad hook exceeds ~50 ms.** Do not cite these numbers for a fifth
|
|
104
|
+
# hook; measure again, because the number that matters is the sum.
|
|
105
|
+
#
|
|
106
|
+
# The cost is also irreducible rather than sloppy. A 9-line script containing
|
|
107
|
+
# only this file's preamble measured 11.7 ms, and padding it to this file's
|
|
108
|
+
# length changed nothing (11.5 ms) — it is process spawn plus one `jq`, not
|
|
109
|
+
# parsing. Moving the early exit further up the file buys nothing; the exit is
|
|
110
|
+
# already ahead of config resolution, which is what keeps the figure at 11.5
|
|
111
|
+
# rather than at the 112 ms the Bash path pays.
|
|
82
112
|
set -euo pipefail
|
|
83
113
|
|
|
84
114
|
input="$(cat)"
|
|
@@ -99,13 +129,72 @@ for required in jq python3; do
|
|
|
99
129
|
done
|
|
100
130
|
|
|
101
131
|
tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)"
|
|
132
|
+
|
|
133
|
+
# ── The two substrates ────────────────────────────────────────────────────
|
|
134
|
+
#
|
|
135
|
+
# A creation reaches a tracker two ways, and until CodySwannGT/lisa#3753 this
|
|
136
|
+
# guard saw only one of them. It was registered `matcher: "Bash"` AND gated
|
|
137
|
+
# here on `tool_name != "Bash"`, so an MCP tool call was refused entry twice
|
|
138
|
+
# over. MEASURED before the change, driving the guard with synthetic payloads:
|
|
139
|
+
#
|
|
140
|
+
# ALLOW mcp__linear-server__create_issue, undeclared <- the defect
|
|
141
|
+
# ALLOW mcp__github__create_issue, undeclared <- the defect
|
|
142
|
+
# REFUSE the same creation as `gh issue create` shell text <- control
|
|
143
|
+
# ALLOW an ordinary Read tool call <- control
|
|
144
|
+
#
|
|
145
|
+
# The two controls are what make those ALLOWs mean anything: the refusal proves
|
|
146
|
+
# the guard is live, the Read proves it is not simply refusing everything.
|
|
147
|
+
#
|
|
148
|
+
# **Widening the matcher alone would have changed nothing** — the call would
|
|
149
|
+
# arrive and this gate would still exit 0. Anyone who "fixed" #3753 by editing
|
|
150
|
+
# `plugin.json` would have seen no test fail and shipped a guard that still
|
|
151
|
+
# allows every MCP filing. The registration and this gate had to move together.
|
|
152
|
+
#
|
|
153
|
+
# The SHELL path below is untouched. A structured payload has no command line
|
|
154
|
+
# to tokenise, so it gets its own small classifier rather than being forced
|
|
155
|
+
# through a parser built for shell text.
|
|
156
|
+
structured_call=""
|
|
102
157
|
if [ "$tool_name" != "Bash" ]; then
|
|
103
|
-
|
|
158
|
+
# The cheap shape gate, deliberately BEFORE config resolution.
|
|
159
|
+
#
|
|
160
|
+
# This hook now runs on EVERY tool call, so the cost of the path that does
|
|
161
|
+
# nothing is the cost of the whole change. Config resolution spawns two jq
|
|
162
|
+
# processes per lookup and is what makes the Bash path ~112 ms; putting it
|
|
163
|
+
# ahead of this gate would charge that to every Read and Grep. So the
|
|
164
|
+
# not-a-creation exit happens here, on a shell `case` with no subprocess.
|
|
165
|
+
#
|
|
166
|
+
# Matched on SHAPE, not on an enumerated list of server tool names. Server
|
|
167
|
+
# names are supplied by whoever wrote the server and change when one is
|
|
168
|
+
# added or renamed, so a list would pass every row anyone thought of and
|
|
169
|
+
# miss the first one nobody did — the failure mode this repository's guard
|
|
170
|
+
# suites are written to defeat.
|
|
171
|
+
#
|
|
172
|
+
# RESIDUAL, stated rather than hidden: a server whose creation tool is named
|
|
173
|
+
# without a create-verb or without a tracker noun (`mcp__x__file_work`) is
|
|
174
|
+
# not recognised and is allowed. That is a fail-open, and it is the honest
|
|
175
|
+
# cost of refusing to enumerate. Add shapes here as they are measured.
|
|
176
|
+
case "$tool_name" in
|
|
177
|
+
Bash | Read | Write | Edit | MultiEdit | Glob | Grep | Task | TodoWrite)
|
|
178
|
+
exit 0
|
|
179
|
+
;;
|
|
180
|
+
esac
|
|
181
|
+
case "$tool_name" in
|
|
182
|
+
*[Cc]reate* | *[Nn]ew* | *[Aa]dd* | *[Ff]ile*) ;;
|
|
183
|
+
*) exit 0 ;;
|
|
184
|
+
esac
|
|
185
|
+
case "$tool_name" in
|
|
186
|
+
*[Ii]ssue* | *[Tt]icket* | *[Tt]ask* | *[Ss]tory* | *[Bb]ug* | *[Ee]pic* | *[Ww]ork*) ;;
|
|
187
|
+
*) exit 0 ;;
|
|
188
|
+
esac
|
|
189
|
+
structured_call="1"
|
|
104
190
|
fi
|
|
105
191
|
|
|
106
|
-
command_str="
|
|
107
|
-
if [ -z "$
|
|
108
|
-
|
|
192
|
+
command_str=""
|
|
193
|
+
if [ -z "$structured_call" ]; then
|
|
194
|
+
command_str="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
|
|
195
|
+
if [ -z "$command_str" ]; then
|
|
196
|
+
exit 0
|
|
197
|
+
fi
|
|
109
198
|
fi
|
|
110
199
|
|
|
111
200
|
project_dir="${CLAUDE_PROJECT_DIR:-}"
|
|
@@ -1859,6 +1948,53 @@ print("ALLOW")
|
|
|
1859
1948
|
PY
|
|
1860
1949
|
|
|
1861
1950
|
set +e
|
|
1951
|
+
# ── The structured substrate ──────────────────────────────────────────────
|
|
1952
|
+
#
|
|
1953
|
+
# An MCP call carries named fields, not a command line, so `scan()` — which
|
|
1954
|
+
# tokenises shell text — has nothing to parse. The declaration is read from the
|
|
1955
|
+
# payload instead, and the recogniser is deliberately small: this path decides
|
|
1956
|
+
# ONE question (is a build-ready role or a human-gate marker present anywhere
|
|
1957
|
+
# in the submitted fields), where the shell path has to answer "which of these
|
|
1958
|
+
# tokens is a client, an endpoint, a flag value, or a file it executes".
|
|
1959
|
+
#
|
|
1960
|
+
# Every string in the payload is searched, at any depth, because a role lands
|
|
1961
|
+
# in a different field on every tracker — `labels[]` on GitHub, `stateId` or a
|
|
1962
|
+
# workflow state name on Linear, a transition `id` on JIRA — and enumerating
|
|
1963
|
+
# field names per vendor is the same brittleness as enumerating tool names.
|
|
1964
|
+
# Over-collecting is safe here: the values are compared against ONE configured
|
|
1965
|
+
# role string, so an unrelated field cannot accidentally satisfy it.
|
|
1966
|
+
if [ -n "$structured_call" ]; then
|
|
1967
|
+
# The operator's ambient escape works on both substrates. There is no inline
|
|
1968
|
+
# form to disqualify it here — a structured call has no shell in which to
|
|
1969
|
+
# assign one — so the override is simply honoured.
|
|
1970
|
+
if [ -n "$ambient_override" ]; then
|
|
1971
|
+
exit 0
|
|
1972
|
+
fi
|
|
1973
|
+
|
|
1974
|
+
structured_declaration="$(
|
|
1975
|
+
printf '%s' "$input" |
|
|
1976
|
+
jq -r --arg role "$ready_role" '
|
|
1977
|
+
[(.tool_input // {}) | .. | strings] as $values
|
|
1978
|
+
| if ($values | index($role)) then "role"
|
|
1979
|
+
elif ($values | map(select(contains("[lisa-human-gate]"))) | length) > 0 then "gate"
|
|
1980
|
+
else "" end
|
|
1981
|
+
' 2>/dev/null || printf 'UNREADABLE'
|
|
1982
|
+
)"
|
|
1983
|
+
|
|
1984
|
+
# Fail closed on a creation-shaped call whose payload cannot be read. Silence
|
|
1985
|
+
# about a filing the guard could not inspect is the defect this whole ticket
|
|
1986
|
+
# is about, one substrate over.
|
|
1987
|
+
if [ "$structured_declaration" = "UNREADABLE" ]; then
|
|
1988
|
+
refuse "an unreadable $tool_name payload that reads as a tracker creation" \
|
|
1989
|
+
"$ready_role" ""
|
|
1990
|
+
fi
|
|
1991
|
+
|
|
1992
|
+
if [ -z "$structured_declaration" ]; then
|
|
1993
|
+
refuse "a tracker creation through $tool_name" "$ready_role" ""
|
|
1994
|
+
fi
|
|
1995
|
+
exit 0
|
|
1996
|
+
fi
|
|
1997
|
+
|
|
1862
1998
|
verdict="$(
|
|
1863
1999
|
printf '%s' "$classifier" |
|
|
1864
2000
|
LISA_GUARD_COMMAND="$command_str" \
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# This file is managed by Lisa.
|
|
3
|
+
# Do not edit directly — changes will be overwritten on the next `lisa` run.
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Discharge Deferred Work-Item Gates Hook (PostToolUse - Bash)
|
|
6
|
+
# =============================================================================
|
|
7
|
+
# Two of the five Work-Item Traceability gates live OUTSIDE the commits: gate 4
|
|
8
|
+
# is the `Work-Item:` line in the pull-request BODY, and gate 5 is the managed
|
|
9
|
+
# `[lisa-pr-link]` backlink on the item. A push cannot check either one —
|
|
10
|
+
# both are properties of a pull request, and the push is what makes the pull
|
|
11
|
+
# request possible — so until this hook existed the next thing that looked was
|
|
12
|
+
# CI, one cycle later (CodySwannGT/lisa#3791).
|
|
13
|
+
#
|
|
14
|
+
# This fires the moment a pull request is created or its body edited, which is
|
|
15
|
+
# the FIRST moment both gates are checkable. It evaluates them, and posts the
|
|
16
|
+
# backlink that gate 5 needs, so neither waits for a red CI run to be revealed.
|
|
17
|
+
#
|
|
18
|
+
# Exit 2 blocks and hands the output back to the agent, which is the point: a
|
|
19
|
+
# non-blocking notice here would be the same defect the discharge exists to
|
|
20
|
+
# fix — a finding delivered somewhere nobody has to act on it.
|
|
21
|
+
# =============================================================================
|
|
22
|
+
set -uo pipefail
|
|
23
|
+
|
|
24
|
+
JSON_INPUT="$(cat)"
|
|
25
|
+
|
|
26
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
27
|
+
command -v node >/dev/null 2>&1 || exit 0
|
|
28
|
+
|
|
29
|
+
COMMAND="$(printf '%s' "$JSON_INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
|
|
30
|
+
[ -n "$COMMAND" ] || exit 0
|
|
31
|
+
|
|
32
|
+
# `gh pr create` opens one; `gh pr edit` is the other way a body comes to carry
|
|
33
|
+
# — or lose — its declaration. Both change the answer to gate 4.
|
|
34
|
+
case "$COMMAND" in
|
|
35
|
+
*"gh pr create"* | *"gh pr edit"*) ;;
|
|
36
|
+
*) exit 0 ;;
|
|
37
|
+
esac
|
|
38
|
+
|
|
39
|
+
repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
|
|
40
|
+
[ -f "$repo_root/scripts/lisa-work-item.mjs" ] || exit 0
|
|
41
|
+
|
|
42
|
+
output="$(cd "$repo_root" && node scripts/lisa-work-item.mjs discharge-pr-gates 2>&1)"
|
|
43
|
+
status=$?
|
|
44
|
+
|
|
45
|
+
# 0 is a clean discharge. 3 is the validator's own answer for "no pull request
|
|
46
|
+
# to check yet", which PostToolUse reaches on every `gh pr create` that FAILED
|
|
47
|
+
# — the tool call still ran, so the hook still fires. Collapsing 3 into the
|
|
48
|
+
# blocking arm would report a work-item violation on somebody's typo.
|
|
49
|
+
case "$status" in
|
|
50
|
+
0 | 3) exit 0 ;;
|
|
51
|
+
esac
|
|
52
|
+
|
|
53
|
+
printf '%s\n' "$output" >&2
|
|
54
|
+
exit 2
|