@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
|
@@ -82,6 +82,36 @@
|
|
|
82
82
|
# settings env block, CI config). An escape the governed agent reaches by
|
|
83
83
|
# typing one more token in front of the command it was just refused is not an
|
|
84
84
|
# escape hatch — it is the prose problem with extra steps.
|
|
85
|
+
#
|
|
86
|
+
# ## What this hook costs, and the number that would change the decision
|
|
87
|
+
#
|
|
88
|
+
# Registered `matcher: ""` since CodySwannGT/lisa#3753 — it runs on EVERY tool
|
|
89
|
+
# call, not only Bash. Measured on a developer machine at load ~8-26, 50
|
|
90
|
+
# invocations per row:
|
|
91
|
+
#
|
|
92
|
+
# 16.7 ms enforce-team-first.sh already `matcher: ""` today
|
|
93
|
+
# 17.5 ms enforce-verification-gate.sh already `matcher: ""` today
|
|
94
|
+
# 11.5 ms this guard, non-Bash payload what the broad matcher adds
|
|
95
|
+
# 112.3 ms this guard, Bash payload what Bash calls already paid
|
|
96
|
+
#
|
|
97
|
+
# So the broad registration was not novel — two hooks were already there, this
|
|
98
|
+
# one is cheaper than either, and it raises an existing ~34 ms per-call baseline
|
|
99
|
+
# by about a third.
|
|
100
|
+
#
|
|
101
|
+
# THIS IS A THRESHOLD, NOT A PRECEDENT. The argument is about the TOTAL cost of
|
|
102
|
+
# the broad set, so it does not transfer to the next guard: four more hooks
|
|
103
|
+
# moved to `matcher: ""` would put ~90 ms on every tool call and the answer
|
|
104
|
+
# flips. **Before widening any other hook's matcher, re-run the measurement and
|
|
105
|
+
# reconsider if the broad set totals more than ~100 ms per tool call, or if any
|
|
106
|
+
# single broad hook exceeds ~50 ms.** Do not cite these numbers for a fifth
|
|
107
|
+
# hook; measure again, because the number that matters is the sum.
|
|
108
|
+
#
|
|
109
|
+
# The cost is also irreducible rather than sloppy. A 9-line script containing
|
|
110
|
+
# only this file's preamble measured 11.7 ms, and padding it to this file's
|
|
111
|
+
# length changed nothing (11.5 ms) — it is process spawn plus one `jq`, not
|
|
112
|
+
# parsing. Moving the early exit further up the file buys nothing; the exit is
|
|
113
|
+
# already ahead of config resolution, which is what keeps the figure at 11.5
|
|
114
|
+
# rather than at the 112 ms the Bash path pays.
|
|
85
115
|
set -euo pipefail
|
|
86
116
|
|
|
87
117
|
input="$(cat)"
|
|
@@ -102,13 +132,72 @@ for required in jq python3; do
|
|
|
102
132
|
done
|
|
103
133
|
|
|
104
134
|
tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)"
|
|
135
|
+
|
|
136
|
+
# ── The two substrates ────────────────────────────────────────────────────
|
|
137
|
+
#
|
|
138
|
+
# A creation reaches a tracker two ways, and until CodySwannGT/lisa#3753 this
|
|
139
|
+
# guard saw only one of them. It was registered `matcher: "Bash"` AND gated
|
|
140
|
+
# here on `tool_name != "Bash"`, so an MCP tool call was refused entry twice
|
|
141
|
+
# over. MEASURED before the change, driving the guard with synthetic payloads:
|
|
142
|
+
#
|
|
143
|
+
# ALLOW mcp__linear-server__create_issue, undeclared <- the defect
|
|
144
|
+
# ALLOW mcp__github__create_issue, undeclared <- the defect
|
|
145
|
+
# REFUSE the same creation as `gh issue create` shell text <- control
|
|
146
|
+
# ALLOW an ordinary Read tool call <- control
|
|
147
|
+
#
|
|
148
|
+
# The two controls are what make those ALLOWs mean anything: the refusal proves
|
|
149
|
+
# the guard is live, the Read proves it is not simply refusing everything.
|
|
150
|
+
#
|
|
151
|
+
# **Widening the matcher alone would have changed nothing** — the call would
|
|
152
|
+
# arrive and this gate would still exit 0. Anyone who "fixed" #3753 by editing
|
|
153
|
+
# `plugin.json` would have seen no test fail and shipped a guard that still
|
|
154
|
+
# allows every MCP filing. The registration and this gate had to move together.
|
|
155
|
+
#
|
|
156
|
+
# The SHELL path below is untouched. A structured payload has no command line
|
|
157
|
+
# to tokenise, so it gets its own small classifier rather than being forced
|
|
158
|
+
# through a parser built for shell text.
|
|
159
|
+
structured_call=""
|
|
105
160
|
if [ "$tool_name" != "Bash" ]; then
|
|
106
|
-
|
|
161
|
+
# The cheap shape gate, deliberately BEFORE config resolution.
|
|
162
|
+
#
|
|
163
|
+
# This hook now runs on EVERY tool call, so the cost of the path that does
|
|
164
|
+
# nothing is the cost of the whole change. Config resolution spawns two jq
|
|
165
|
+
# processes per lookup and is what makes the Bash path ~112 ms; putting it
|
|
166
|
+
# ahead of this gate would charge that to every Read and Grep. So the
|
|
167
|
+
# not-a-creation exit happens here, on a shell `case` with no subprocess.
|
|
168
|
+
#
|
|
169
|
+
# Matched on SHAPE, not on an enumerated list of server tool names. Server
|
|
170
|
+
# names are supplied by whoever wrote the server and change when one is
|
|
171
|
+
# added or renamed, so a list would pass every row anyone thought of and
|
|
172
|
+
# miss the first one nobody did — the failure mode this repository's guard
|
|
173
|
+
# suites are written to defeat.
|
|
174
|
+
#
|
|
175
|
+
# RESIDUAL, stated rather than hidden: a server whose creation tool is named
|
|
176
|
+
# without a create-verb or without a tracker noun (`mcp__x__file_work`) is
|
|
177
|
+
# not recognised and is allowed. That is a fail-open, and it is the honest
|
|
178
|
+
# cost of refusing to enumerate. Add shapes here as they are measured.
|
|
179
|
+
case "$tool_name" in
|
|
180
|
+
Bash | Read | Write | Edit | MultiEdit | Glob | Grep | Task | TodoWrite)
|
|
181
|
+
exit 0
|
|
182
|
+
;;
|
|
183
|
+
esac
|
|
184
|
+
case "$tool_name" in
|
|
185
|
+
*[Cc]reate* | *[Nn]ew* | *[Aa]dd* | *[Ff]ile*) ;;
|
|
186
|
+
*) exit 0 ;;
|
|
187
|
+
esac
|
|
188
|
+
case "$tool_name" in
|
|
189
|
+
*[Ii]ssue* | *[Tt]icket* | *[Tt]ask* | *[Ss]tory* | *[Bb]ug* | *[Ee]pic* | *[Ww]ork*) ;;
|
|
190
|
+
*) exit 0 ;;
|
|
191
|
+
esac
|
|
192
|
+
structured_call="1"
|
|
107
193
|
fi
|
|
108
194
|
|
|
109
|
-
command_str="
|
|
110
|
-
if [ -z "$
|
|
111
|
-
|
|
195
|
+
command_str=""
|
|
196
|
+
if [ -z "$structured_call" ]; then
|
|
197
|
+
command_str="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
|
|
198
|
+
if [ -z "$command_str" ]; then
|
|
199
|
+
exit 0
|
|
200
|
+
fi
|
|
112
201
|
fi
|
|
113
202
|
|
|
114
203
|
project_dir="${CLAUDE_PROJECT_DIR:-}"
|
|
@@ -1862,6 +1951,53 @@ print("ALLOW")
|
|
|
1862
1951
|
PY
|
|
1863
1952
|
|
|
1864
1953
|
set +e
|
|
1954
|
+
# ── The structured substrate ──────────────────────────────────────────────
|
|
1955
|
+
#
|
|
1956
|
+
# An MCP call carries named fields, not a command line, so `scan()` — which
|
|
1957
|
+
# tokenises shell text — has nothing to parse. The declaration is read from the
|
|
1958
|
+
# payload instead, and the recogniser is deliberately small: this path decides
|
|
1959
|
+
# ONE question (is a build-ready role or a human-gate marker present anywhere
|
|
1960
|
+
# in the submitted fields), where the shell path has to answer "which of these
|
|
1961
|
+
# tokens is a client, an endpoint, a flag value, or a file it executes".
|
|
1962
|
+
#
|
|
1963
|
+
# Every string in the payload is searched, at any depth, because a role lands
|
|
1964
|
+
# in a different field on every tracker — `labels[]` on GitHub, `stateId` or a
|
|
1965
|
+
# workflow state name on Linear, a transition `id` on JIRA — and enumerating
|
|
1966
|
+
# field names per vendor is the same brittleness as enumerating tool names.
|
|
1967
|
+
# Over-collecting is safe here: the values are compared against ONE configured
|
|
1968
|
+
# role string, so an unrelated field cannot accidentally satisfy it.
|
|
1969
|
+
if [ -n "$structured_call" ]; then
|
|
1970
|
+
# The operator's ambient escape works on both substrates. There is no inline
|
|
1971
|
+
# form to disqualify it here — a structured call has no shell in which to
|
|
1972
|
+
# assign one — so the override is simply honoured.
|
|
1973
|
+
if [ -n "$ambient_override" ]; then
|
|
1974
|
+
exit 0
|
|
1975
|
+
fi
|
|
1976
|
+
|
|
1977
|
+
structured_declaration="$(
|
|
1978
|
+
printf '%s' "$input" |
|
|
1979
|
+
jq -r --arg role "$ready_role" '
|
|
1980
|
+
[(.tool_input // {}) | .. | strings] as $values
|
|
1981
|
+
| if ($values | index($role)) then "role"
|
|
1982
|
+
elif ($values | map(select(contains("[lisa-human-gate]"))) | length) > 0 then "gate"
|
|
1983
|
+
else "" end
|
|
1984
|
+
' 2>/dev/null || printf 'UNREADABLE'
|
|
1985
|
+
)"
|
|
1986
|
+
|
|
1987
|
+
# Fail closed on a creation-shaped call whose payload cannot be read. Silence
|
|
1988
|
+
# about a filing the guard could not inspect is the defect this whole ticket
|
|
1989
|
+
# is about, one substrate over.
|
|
1990
|
+
if [ "$structured_declaration" = "UNREADABLE" ]; then
|
|
1991
|
+
refuse "an unreadable $tool_name payload that reads as a tracker creation" \
|
|
1992
|
+
"$ready_role" ""
|
|
1993
|
+
fi
|
|
1994
|
+
|
|
1995
|
+
if [ -z "$structured_declaration" ]; then
|
|
1996
|
+
refuse "a tracker creation through $tool_name" "$ready_role" ""
|
|
1997
|
+
fi
|
|
1998
|
+
exit 0
|
|
1999
|
+
fi
|
|
2000
|
+
|
|
1865
2001
|
verdict="$(
|
|
1866
2002
|
printf '%s' "$classifier" |
|
|
1867
2003
|
LISA_GUARD_COMMAND="$command_str" \
|
|
@@ -82,6 +82,14 @@ const NOT_A_DEPLOY_BRANCH = "not a deploy branch";
|
|
|
82
82
|
* for a producer that, until this command existed, was prose in a SKILL.md.
|
|
83
83
|
*/
|
|
84
84
|
const BACKLINK_COMMAND = "node scripts/lisa-work-item.mjs backlink";
|
|
85
|
+
/**
|
|
86
|
+
* The subcommand that closes out the two gates a push had to defer.
|
|
87
|
+
*
|
|
88
|
+
* Named in the push report rather than described, because the push report is
|
|
89
|
+
* the only place an operator learns that two requirements are still open, and
|
|
90
|
+
* a list of open work with no command against it is a complaint.
|
|
91
|
+
*/
|
|
92
|
+
const DISCHARGE_COMMAND = "discharge-pr-gates";
|
|
85
93
|
/**
|
|
86
94
|
* The prefix only — an anchored, fixed-length literal with no quantifier, so
|
|
87
95
|
* it cannot backtrack. The ReDoS was never here; it was in the `\s*(.+?)\s*$`
|
|
@@ -3053,11 +3061,24 @@ function currentRepository() {
|
|
|
3053
3061
|
return safeJson(result.stdout, "GitHub repository").nameWithOwner;
|
|
3054
3062
|
}
|
|
3055
3063
|
|
|
3056
|
-
function currentPullRequest(
|
|
3064
|
+
function currentPullRequest(
|
|
3065
|
+
number,
|
|
3066
|
+
repository = currentRepository(),
|
|
3067
|
+
fields = "url,body,state"
|
|
3068
|
+
) {
|
|
3057
3069
|
const args = ["pr", "view"];
|
|
3058
3070
|
if (number) args.push(String(number));
|
|
3059
|
-
|
|
3060
|
-
|
|
3071
|
+
// `--repo` ONLY alongside a number, because `gh pr view --repo <r>` with no
|
|
3072
|
+
// positional argument is a USAGE ERROR — "argument required when using the
|
|
3073
|
+
// --repo flag" — not a lookup that returns nothing. It exits 1, this
|
|
3074
|
+
// function reads that as "no pull request exists", and the push then takes
|
|
3075
|
+
// the deferral branch on every branch that HAS one. Measured on this
|
|
3076
|
+
// repository: an open pull request, and the push reporting gates 4 and 5 as
|
|
3077
|
+
// unchecked because the flag combination could never have found it. Without
|
|
3078
|
+
// a number `gh` resolves the pull request from the current branch's remote,
|
|
3079
|
+
// which is the answer this caller wants and the only one it can get.
|
|
3080
|
+
if (number && repository) args.push("--repo", repository);
|
|
3081
|
+
args.push("--json", fields);
|
|
3061
3082
|
const result = run("gh", args, { allowFailure: true });
|
|
3062
3083
|
return result.status === 0
|
|
3063
3084
|
? safeJson(result.stdout, "GitHub pull request")
|
|
@@ -4158,6 +4179,61 @@ function pushDeferralNote(result) {
|
|
|
4158
4179
|
return `no pull request exists yet, so gates 4 and 5 could not be checked here — CI will verify both.${spanning}`;
|
|
4159
4180
|
}
|
|
4160
4181
|
|
|
4182
|
+
/**
|
|
4183
|
+
* The two unchecked gates as open work, addressed to whoever runs the push.
|
|
4184
|
+
*
|
|
4185
|
+
* The wording is the whole point of this function, so it is worth saying what
|
|
4186
|
+
* it is deliberately NOT: `WORK_ITEM_TRACKING_OK`. This same text used to be
|
|
4187
|
+
* appended to that token, which made a finding arrive inside a success — and a
|
|
4188
|
+
* finding inside a success is not a finding. Nobody reads an unmet requirement
|
|
4189
|
+
* out of a line that opens by declaring the check passed, so the gate did the
|
|
4190
|
+
* hard part (it knows exactly which two requirements it could not reach, and
|
|
4191
|
+
* says so) and then threw the answer away in its own headline
|
|
4192
|
+
* (CodySwannGT/lisa#3791).
|
|
4193
|
+
*
|
|
4194
|
+
* The exit code stays 0, and that is not the same compromise. A push is the
|
|
4195
|
+
* only way to make a branch exist on the remote, and a pull request cannot be
|
|
4196
|
+
* opened for a branch the remote has never seen — so refusing here would be a
|
|
4197
|
+
* refusal with no remedy, and the first push of every branch would be
|
|
4198
|
+
* impossible. What is available at this moment is the OTHER half of a control:
|
|
4199
|
+
* name the unmet requirements as unmet, and name the command that discharges
|
|
4200
|
+
* them the moment they become checkable. `discharge-pr-gates` is that command,
|
|
4201
|
+
* and it is what makes this list closeable rather than merely honest.
|
|
4202
|
+
*
|
|
4203
|
+
* Gate 5 is stated as inapplicable rather than unresolved under the `trailer`
|
|
4204
|
+
* level, because a run that contacted no tracker has not left a backlink
|
|
4205
|
+
* unposted — it is not required at all there. Listing it as open work would
|
|
4206
|
+
* ask an operator to resolve something no check will ever look at.
|
|
4207
|
+
* Exported so the report can be asserted in-process. The push path reaches it
|
|
4208
|
+
* only through a spawned child, where a mutation run records no coverage and
|
|
4209
|
+
* scores every line of this text as unreached — so the subprocess cases prove
|
|
4210
|
+
* the WIRING and the direct cases prove the WORDS.
|
|
4211
|
+
* @param {object} result Commit-side result.
|
|
4212
|
+
* @param {string} label Prefix naming the ref, empty for a single-ref push.
|
|
4213
|
+
* @returns {string} The whole report, replacing the success line.
|
|
4214
|
+
*/
|
|
4215
|
+
export function unresolvedPushReport(result, label) {
|
|
4216
|
+
const contract = result.contract;
|
|
4217
|
+
const refs = result.refs ?? [];
|
|
4218
|
+
const declaration = refs.map(ref => `Work-Item: ${ref}`).join("\n ");
|
|
4219
|
+
const backlinkLine =
|
|
4220
|
+
contract.verify === "full"
|
|
4221
|
+
? ` UNRESOLVED gate 5 — each item needs a managed \`${MARKER}\` backlink comment pointing at the pull request. \`discharge-pr-gates\` posts it; nothing else has to remember to.`
|
|
4222
|
+
: ` n/a gate 5 — the tracker backlink is not required here: workItem.verify is "trailer".`;
|
|
4223
|
+
return [
|
|
4224
|
+
`WORK_ITEM_TRACKING_INCOMPLETE ${label}${result.relevant} commit(s)${alreadyTraced(result)}: gates 1-3 proved here, 2 of 5 gates NOT CHECKED.`,
|
|
4225
|
+
` This push is not a clean bill of health for ${refs.join(", ")}. ${pushDeferralNote(result)}`,
|
|
4226
|
+
` UNRESOLVED gate 4 — the pull-request BODY must carry, on its own line${refs.length > 1 ? "s" : ""}:`,
|
|
4227
|
+
` ${declaration}`,
|
|
4228
|
+
` \`Refs #n\` and \`Closes #n\` do NOT satisfy it.`,
|
|
4229
|
+
backlinkLine,
|
|
4230
|
+
` Discharge both the moment the pull request exists — it evaluates them and`,
|
|
4231
|
+
` posts what it can, so neither waits for CI to reveal it:`,
|
|
4232
|
+
` node scripts/lisa-work-item.mjs ${DISCHARGE_COMMAND}`,
|
|
4233
|
+
gateSummary(contract),
|
|
4234
|
+
].join("\n");
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4161
4237
|
/**
|
|
4162
4238
|
* Report one pushed ref's outcome, refusing when it did not prove out.
|
|
4163
4239
|
* @param {{result?: object, error?: Error}} outcome Commit-side outcome.
|
|
@@ -4177,9 +4253,20 @@ function reportPushGroup(outcome, pr, label) {
|
|
|
4177
4253
|
// CI — so the checklist goes out either way. Saying nothing is precisely
|
|
4178
4254
|
// what made those two gates separate CI-cycle surprises (#2681).
|
|
4179
4255
|
if (outcome.error) throw withGateSummary(outcome.error);
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4256
|
+
// A range naming no work item is a different fact from a range whose
|
|
4257
|
+
// declaration nobody checked, and the two must not share a sentence. Nothing
|
|
4258
|
+
// here is deferred: a release commit and a back-merge have no item for a
|
|
4259
|
+
// pull-request body to declare or a tracker to link, so gates 4 and 5 have
|
|
4260
|
+
// NOTHING to check rather than something unchecked. Reporting those as open
|
|
4261
|
+
// work would put an unresolvable item on every release push, which is how a
|
|
4262
|
+
// checklist teaches the reader to skip it.
|
|
4263
|
+
if ((outcome.result.refs ?? []).length === 0) {
|
|
4264
|
+
console.log(
|
|
4265
|
+
`WORK_ITEM_TRACKING_OK ${label}${outcome.result.relevant} commit(s)${alreadyTraced(outcome.result)}; this range names no work item, so gates 4 and 5 have nothing to check here.`
|
|
4266
|
+
);
|
|
4267
|
+
return;
|
|
4268
|
+
}
|
|
4269
|
+
console.log(unresolvedPushReport(outcome.result, label));
|
|
4183
4270
|
}
|
|
4184
4271
|
|
|
4185
4272
|
function validatePush(args) {
|
|
@@ -4202,6 +4289,116 @@ function validatePush(args) {
|
|
|
4202
4289
|
}
|
|
4203
4290
|
}
|
|
4204
4291
|
|
|
4292
|
+
/**
|
|
4293
|
+
* Refuse, but as "there is nothing here to check" rather than "this failed".
|
|
4294
|
+
*
|
|
4295
|
+
* Its own exit code because the two answers ask opposite things of whoever is
|
|
4296
|
+
* reading. `1` says a requirement is unmet and the change must not proceed;
|
|
4297
|
+
* `3` says this command was run somewhere it has no subject — before the pull
|
|
4298
|
+
* request exists, or on a branch that has none. A caller that fires this
|
|
4299
|
+
* automatically (the PostToolUse hook does, after any `gh pr` command,
|
|
4300
|
+
* including ones that failed) must be able to tell them apart without parsing
|
|
4301
|
+
* prose, or every failed `gh pr create` would report a work-item violation
|
|
4302
|
+
* that did not happen.
|
|
4303
|
+
* Exported so the code and the wording can be asserted without a tracker: an
|
|
4304
|
+
* error object is a value, and the whole content of this one is which number
|
|
4305
|
+
* it carries.
|
|
4306
|
+
* @returns {Error} The refusal, carrying its own exit code.
|
|
4307
|
+
*/
|
|
4308
|
+
export function noPullRequestToDischarge() {
|
|
4309
|
+
const error = new TrackingError(
|
|
4310
|
+
`${DISCHARGE_COMMAND} found no pull request for this branch, so gates 4 ` +
|
|
4311
|
+
`and 5 have no subject yet. Open the pull request, then run it again.`
|
|
4312
|
+
);
|
|
4313
|
+
error.exitCode = 3;
|
|
4314
|
+
error.selfExplanatory = true;
|
|
4315
|
+
return error;
|
|
4316
|
+
}
|
|
4317
|
+
|
|
4318
|
+
/**
|
|
4319
|
+
* Close out gates 4 and 5 at the first moment they can be checked.
|
|
4320
|
+
*
|
|
4321
|
+
* A push cannot check them: both are properties of a pull request, and the
|
|
4322
|
+
* push is what makes the pull request possible. Until this command existed the
|
|
4323
|
+
* next thing that looked was CI, one cycle later — so a body missing its
|
|
4324
|
+
* `Work-Item:` line, or an item missing its backlink, cost a full red run to
|
|
4325
|
+
* discover something that was knowable the second the pull request opened
|
|
4326
|
+
* (CodySwannGT/lisa#3791).
|
|
4327
|
+
*
|
|
4328
|
+
* It does not merely CHECK gate 5, it satisfies it. The managed backlink is
|
|
4329
|
+
* something a producer can write and no human should have to remember, and the
|
|
4330
|
+
* reason it has to be written at all is a coupling neither rule states on its
|
|
4331
|
+
* own: the convention is to reference an item with `Refs #n` rather than
|
|
4332
|
+
* `Closes #n`, so the merge cannot close it before deploy and verification —
|
|
4333
|
+
* and `Refs` populates no native development link for gate 5 to read. The
|
|
4334
|
+
* non-closing rule is what MAKES the managed comment mandatory.
|
|
4335
|
+
*
|
|
4336
|
+
* Gate 4 is checked and never written. The body is the author's declaration of
|
|
4337
|
+
* what this change is for; a command that inserted the line would be answering
|
|
4338
|
+
* its own question, and the gate would be proving that this command can write.
|
|
4339
|
+
* @param {string[]} args Command arguments.
|
|
4340
|
+
*/
|
|
4341
|
+
function dischargePrGates(args) {
|
|
4342
|
+
const remote = option(args, "--remote", "LISA_PR_REMOTE") || "origin";
|
|
4343
|
+
const pr = currentPullRequest(
|
|
4344
|
+
option(args, "--pr-number", "LISA_PR_NUMBER"),
|
|
4345
|
+
undefined,
|
|
4346
|
+
"url,body,state,commits"
|
|
4347
|
+
);
|
|
4348
|
+
if (!pr) throw noPullRequestToDischarge();
|
|
4349
|
+
const configRef = remoteDefaultRef(remote);
|
|
4350
|
+
const commits = (pr.commits ?? []).map(commit => commit.oid).filter(Boolean);
|
|
4351
|
+
const first = commitOutcome(commits, configRef, remote);
|
|
4352
|
+
const contract = first.result?.contract ?? trackerContract();
|
|
4353
|
+
const refs = first.result?.refs ?? [];
|
|
4354
|
+
const wrote = postDischargeBacklinks(refs, pr.url, contract);
|
|
4355
|
+
// A SECOND read when anything actually changed, and the reason is the defect
|
|
4356
|
+
// this whole file exists to avoid. `validatePrData` reads gate 5 out of the
|
|
4357
|
+
// issue payload the FIRST pass cached — fetched before the comment above was
|
|
4358
|
+
// posted — so validating against it would report a backlink missing that
|
|
4359
|
+
// this command had just written, and the operator would be sent to fix
|
|
4360
|
+
// something already fixed. Nothing is re-read when every backlink came back
|
|
4361
|
+
// `unchanged`, because then the cached payload and the tracker agree.
|
|
4362
|
+
const outcome = wrote ? commitOutcome(commits, configRef, remote) : first;
|
|
4363
|
+
validatePrData(outcome, pr.url, pr.body);
|
|
4364
|
+
console.log(
|
|
4365
|
+
`WORK_ITEM_TRACKING_OK ${outcome.result.relevant} commit(s)${alreadyTraced(outcome.result)}, ${provedHere(contract)} — gates 4 and 5 discharged at the pull request, not deferred to CI`
|
|
4366
|
+
);
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4369
|
+
/**
|
|
4370
|
+
* Establish the managed backlink for every item this pull request carries.
|
|
4371
|
+
*
|
|
4372
|
+
* The writer is a parameter for the same reason `assertBacklink` takes its
|
|
4373
|
+
* issue payload as one: it is the only part that touches a tracker, and
|
|
4374
|
+
* without the seam the answer this function returns — did anything actually
|
|
4375
|
+
* change, which is what decides whether the verification re-reads — can only
|
|
4376
|
+
* be observed through a network round trip.
|
|
4377
|
+
* @param {string[]} refs Work items the range names.
|
|
4378
|
+
* @param {string} prUrl Pull request URL.
|
|
4379
|
+
* @param {object} contract Resolved tracker contract.
|
|
4380
|
+
* @param {Function} [post] Backlink writer, defaulting to the real one.
|
|
4381
|
+
* @returns {boolean} True when any tracker write actually changed something.
|
|
4382
|
+
*/
|
|
4383
|
+
export function postDischargeBacklinks(
|
|
4384
|
+
refs,
|
|
4385
|
+
prUrl,
|
|
4386
|
+
contract,
|
|
4387
|
+
post = postBacklink
|
|
4388
|
+
) {
|
|
4389
|
+
// `trailer` contacts no tracker anywhere else in this file, and it may not
|
|
4390
|
+
// start here: a project that keeps no tracker credentials cannot satisfy a
|
|
4391
|
+
// write, and gate 5 is not asked of it.
|
|
4392
|
+
if (contract.verify !== "full") return false;
|
|
4393
|
+
let changed = false;
|
|
4394
|
+
for (const ref of refs) {
|
|
4395
|
+
const outcome = post(ref, prUrl, contract);
|
|
4396
|
+
changed ||= outcome !== "unchanged";
|
|
4397
|
+
console.log(`work-item backlink ${outcome} on ${ref}: ${MARKER} ${prUrl}`);
|
|
4398
|
+
}
|
|
4399
|
+
return changed;
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4205
4402
|
function validatePr(args) {
|
|
4206
4403
|
const base = option(args, "--base", "LISA_PR_BASE_SHA");
|
|
4207
4404
|
const head = option(args, "--head", "LISA_PR_HEAD_SHA") || "HEAD";
|
|
@@ -4317,8 +4514,9 @@ function main() {
|
|
|
4317
4514
|
if (command === "validate-push-destination")
|
|
4318
4515
|
return validatePushDestination(args);
|
|
4319
4516
|
if (command === "validate-pr") return validatePr(args);
|
|
4517
|
+
if (command === DISCHARGE_COMMAND) return dischargePrGates(args);
|
|
4320
4518
|
throw new TrackingError(
|
|
4321
|
-
"Usage: lisa-work-item.mjs link|current|attach-branch|clear|verify-level|contract-version|backlink|complete|sweep|prepare-commit-msg|validate-commit|validate-push|validate-push-destination|validate-pr" +
|
|
4519
|
+
"Usage: lisa-work-item.mjs link|current|attach-branch|clear|verify-level|contract-version|backlink|complete|sweep|prepare-commit-msg|validate-commit|validate-push|validate-push-destination|validate-pr|discharge-pr-gates" +
|
|
4322
4520
|
"\n(`bind` is accepted as an alias for `link`, but some agent harnesses refuse the token `bind` in a command line.)"
|
|
4323
4521
|
);
|
|
4324
4522
|
}
|
|
@@ -4343,7 +4541,10 @@ export function runCli() {
|
|
|
4343
4541
|
? `\n❌ ${detail}\n`
|
|
4344
4542
|
: `\n❌ Work-item tracking blocked this operation: ${detail}\n\n${GUIDANCE}\n`
|
|
4345
4543
|
);
|
|
4346
|
-
|
|
4544
|
+
// A refusal may name its own exit code, and one does: `discharge-pr-gates`
|
|
4545
|
+
// answers 3 for "no pull request to check yet", which is not a violation
|
|
4546
|
+
// and must not read as one to a caller that only has the status to go on.
|
|
4547
|
+
process.exitCode = error?.exitCode ?? 1;
|
|
4347
4548
|
}
|
|
4348
4549
|
}
|
|
4349
4550
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lisa-owned-hash-ledger.d.ts","sourceRoot":"","sources":["../../src/core/lisa-owned-hash-ledger.ts"],"names":[],"mappings":"AACA,6EAA6E;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAC3C,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"lisa-owned-hash-ledger.d.ts","sourceRoot":"","sources":["../../src/core/lisa-owned-hash-ledger.ts"],"names":[],"mappings":"AACA,6EAA6E;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAC3C,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAy/BjC,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,+BAA+B,EAAE,QAAQ,CACpD,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CA45BjC,CAAC"}
|
|
@@ -598,11 +598,13 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
598
598
|
"22184f1dd1b96e818741bd4ae659446299535d7ce594817155edebac4172a7ba",
|
|
599
599
|
"2832037d6c8d7930a111519dee0cca0ed0110deba3988f8e713bb9203de254df",
|
|
600
600
|
"2e030d5a394d3edb48c391db49661fb3d45545c85f88ea03d1ab07ad3787c089",
|
|
601
|
+
"412209ea2329a64737eeffa4ffc9b6f9371f14cf86419fd0626361a99188a3f8",
|
|
601
602
|
"5b3bd8a3460524457435f51a0994fb12009c97fffb921375bc79db3798acbabf",
|
|
602
603
|
"5c5a9f1c39f99e493a575784f70379eb857c4963da5d400b6ee86f0e69fc5402",
|
|
603
604
|
"60a4f94ec671b2372f7492680ab327d97aff686588ce85791f54e0fbd098b0ca",
|
|
604
605
|
"63c2f9b54ea30997905bc6e17709dd1300436204ac82ad16717d7cc215af262b",
|
|
605
606
|
"698131242f9ea27ea00e864d4b302f4ccc4a24d1383cb8ca9f0b17b565796197",
|
|
607
|
+
"6edfeec55433192c89b955a026ca3b5a25ade8e04c3267542d1cd884cd0bb3bb",
|
|
606
608
|
"73e94b73830d8961026947d99bf4ac5f959b5bca025203b913d8a6363cbe2b9f",
|
|
607
609
|
"803752f85a9f013014be5d04e444c65f7d3eea476569c5d7048d4d13b8e0bb9e",
|
|
608
610
|
"847dfca48823a495fc3de47dfdb4b26e49d6808c6570c46d024a99793df1d370",
|
|
@@ -879,6 +881,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
879
881
|
"2c678c75d6de9b029f27fa6eec45b8421217a4fb5ef3412ad39883e10b213987",
|
|
880
882
|
"2d9ca0841db9e75ddfd95e3a4d485fd01aa639a512ef648996f565b7991d8e2b",
|
|
881
883
|
"3a9ec1ff863693a0fa0fef0ade6fa5534485d0249fb78302d37cdac3781348f1",
|
|
884
|
+
"3f8c59e0f4ad43211f6964b4889f3fc673bee0496ec3613bdbdc4544424340f6",
|
|
882
885
|
"41b967f736d7fcf9d8928ad7c8cc767c5beb9544e2fbfa3317c65734aa02fcfd",
|
|
883
886
|
"4372872a448d3b943dea5541167ed5be6155703b57b9f8224d19ddefd3b80898",
|
|
884
887
|
"475cb9ef86c2ac54c8fd8cb6d63e9a55ea8cd0017e26efbe3c44391a4aeeab41",
|
|
@@ -892,6 +895,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
892
895
|
"637741a9e5d2904aed24d48efd6bec1326c5573f718a13152807aa0e4b42f0a1",
|
|
893
896
|
"653252b0bbfd19bdffa046a2173c053e566ac6bf605c8d9060f9cf71ad7386de",
|
|
894
897
|
"6a8be8577242588f14ab52b25b2a22b6def53aefa9513b4d3d7c49f4d8027079",
|
|
898
|
+
"6afe796f5a69a68a026c79a64aa451eb2bf7907a0ace0d9079f8b4add16b886b",
|
|
895
899
|
"6dadc065016e741568b6ae1ebcd0378085005c076fdb93346bad7c1fd9e3b8b8",
|
|
896
900
|
"71ff42c2cb8ac7022e0066132476764dd745fb92bf19ccd395000b1fe319abc7",
|
|
897
901
|
"7573b5170fcba45ccd6123cd0712baba8faa52f063b639140be829d9ce87327d",
|
|
@@ -916,6 +920,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
916
920
|
"bdeab1cb7cbb427a69bc8450075b5d36d364a7b71e69666fef6966097084b0a5",
|
|
917
921
|
"c78df9d5f0f3fc388ff60a7db5898478a2a4d6d7325833f10384b5a8c62bb34a",
|
|
918
922
|
"cdea136456d2b9880c3abd2bbd402fb08051822b9e92484c66cd184ec7ed7d0f",
|
|
923
|
+
"ce1520dba31d32856d470cf744700e0636518dd3a767627d42ac0a77007d6813",
|
|
919
924
|
"ce5ff040b1f75925bc286027dcb893649edfb408021916920b07105baa2d9949",
|
|
920
925
|
"d0321aa197d748813a2451eaa1c509cc74aced19a7f887b14a4bb15a7a73a525",
|
|
921
926
|
"d1b07166efb5534c1d9b0fd09032e311854477160244f5318925035392318d36",
|
|
@@ -1559,11 +1564,13 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1559
1564
|
"22184f1dd1b96e818741bd4ae659446299535d7ce594817155edebac4172a7ba",
|
|
1560
1565
|
"2832037d6c8d7930a111519dee0cca0ed0110deba3988f8e713bb9203de254df",
|
|
1561
1566
|
"2e030d5a394d3edb48c391db49661fb3d45545c85f88ea03d1ab07ad3787c089",
|
|
1567
|
+
"412209ea2329a64737eeffa4ffc9b6f9371f14cf86419fd0626361a99188a3f8",
|
|
1562
1568
|
"5b3bd8a3460524457435f51a0994fb12009c97fffb921375bc79db3798acbabf",
|
|
1563
1569
|
"5c5a9f1c39f99e493a575784f70379eb857c4963da5d400b6ee86f0e69fc5402",
|
|
1564
1570
|
"60a4f94ec671b2372f7492680ab327d97aff686588ce85791f54e0fbd098b0ca",
|
|
1565
1571
|
"63c2f9b54ea30997905bc6e17709dd1300436204ac82ad16717d7cc215af262b",
|
|
1566
1572
|
"698131242f9ea27ea00e864d4b302f4ccc4a24d1383cb8ca9f0b17b565796197",
|
|
1573
|
+
"6edfeec55433192c89b955a026ca3b5a25ade8e04c3267542d1cd884cd0bb3bb",
|
|
1567
1574
|
"73e94b73830d8961026947d99bf4ac5f959b5bca025203b913d8a6363cbe2b9f",
|
|
1568
1575
|
"803752f85a9f013014be5d04e444c65f7d3eea476569c5d7048d4d13b8e0bb9e",
|
|
1569
1576
|
"847dfca48823a495fc3de47dfdb4b26e49d6808c6570c46d024a99793df1d370",
|
|
@@ -1817,6 +1824,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1817
1824
|
"2c678c75d6de9b029f27fa6eec45b8421217a4fb5ef3412ad39883e10b213987",
|
|
1818
1825
|
"2d9ca0841db9e75ddfd95e3a4d485fd01aa639a512ef648996f565b7991d8e2b",
|
|
1819
1826
|
"3a9ec1ff863693a0fa0fef0ade6fa5534485d0249fb78302d37cdac3781348f1",
|
|
1827
|
+
"3f8c59e0f4ad43211f6964b4889f3fc673bee0496ec3613bdbdc4544424340f6",
|
|
1820
1828
|
"41b967f736d7fcf9d8928ad7c8cc767c5beb9544e2fbfa3317c65734aa02fcfd",
|
|
1821
1829
|
"4372872a448d3b943dea5541167ed5be6155703b57b9f8224d19ddefd3b80898",
|
|
1822
1830
|
"475cb9ef86c2ac54c8fd8cb6d63e9a55ea8cd0017e26efbe3c44391a4aeeab41",
|
|
@@ -1829,6 +1837,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1829
1837
|
"62f53156d425198609564e996f3844f52ab110e63f99f22e8338fcf7a83f316d",
|
|
1830
1838
|
"653252b0bbfd19bdffa046a2173c053e566ac6bf605c8d9060f9cf71ad7386de",
|
|
1831
1839
|
"6a8be8577242588f14ab52b25b2a22b6def53aefa9513b4d3d7c49f4d8027079",
|
|
1840
|
+
"6afe796f5a69a68a026c79a64aa451eb2bf7907a0ace0d9079f8b4add16b886b",
|
|
1832
1841
|
"6dadc065016e741568b6ae1ebcd0378085005c076fdb93346bad7c1fd9e3b8b8",
|
|
1833
1842
|
"71ff42c2cb8ac7022e0066132476764dd745fb92bf19ccd395000b1fe319abc7",
|
|
1834
1843
|
"7573b5170fcba45ccd6123cd0712baba8faa52f063b639140be829d9ce87327d",
|
|
@@ -1853,6 +1862,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1853
1862
|
"bdeab1cb7cbb427a69bc8450075b5d36d364a7b71e69666fef6966097084b0a5",
|
|
1854
1863
|
"c78df9d5f0f3fc388ff60a7db5898478a2a4d6d7325833f10384b5a8c62bb34a",
|
|
1855
1864
|
"cdea136456d2b9880c3abd2bbd402fb08051822b9e92484c66cd184ec7ed7d0f",
|
|
1865
|
+
"ce1520dba31d32856d470cf744700e0636518dd3a767627d42ac0a77007d6813",
|
|
1856
1866
|
"ce5ff040b1f75925bc286027dcb893649edfb408021916920b07105baa2d9949",
|
|
1857
1867
|
"d0321aa197d748813a2451eaa1c509cc74aced19a7f887b14a4bb15a7a73a525",
|
|
1858
1868
|
"d1b07166efb5534c1d9b0fd09032e311854477160244f5318925035392318d36",
|