@codyswann/lisa 2.341.3 → 2.341.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +14 -1
- package/all/copy-overwrite/scripts/lisa-hooks/block-instruction-file-edits.sh +47 -7
- package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +49 -5
- package/all/copy-overwrite/scripts/lisa-hooks/block-shell-json-parsing.sh +20 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +12 -10
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-linear/SKILL.md +13 -6
- package/plugins/lisa/commands/setup/linear.md +1 -1
- package/plugins/lisa/hooks/block-instruction-file-edits.sh +47 -7
- package/plugins/lisa/hooks/block-no-verify.sh +49 -5
- package/plugins/lisa/hooks/block-shell-json-parsing.sh +20 -1
- package/plugins/lisa/skills/lisa-setup-linear/SKILL.md +14 -7
- package/plugins/lisa-agy/commands/lisa/setup/linear.md +1 -1
- package/plugins/lisa-agy/hooks/block-instruction-file-edits.sh +47 -7
- package/plugins/lisa-agy/hooks/block-shell-json-parsing.sh +20 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-setup-linear/SKILL.md +14 -7
- 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/commands/lisa/setup/linear.md +1 -1
- package/plugins/lisa-copilot/hooks/block-instruction-file-edits.sh +47 -7
- package/plugins/lisa-copilot/hooks/block-no-verify.sh +49 -5
- package/plugins/lisa-copilot/hooks/block-shell-json-parsing.sh +20 -1
- package/plugins/lisa-copilot/skills/lisa-setup-linear/SKILL.md +14 -7
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/commands/lisa/setup/linear.md +1 -1
- package/plugins/lisa-cursor/hooks/block-instruction-file-edits.sh +47 -7
- package/plugins/lisa-cursor/hooks/block-no-verify.sh +49 -5
- package/plugins/lisa-cursor/hooks/block-shell-json-parsing.sh +20 -1
- package/plugins/lisa-cursor/skills/lisa-setup-linear/SKILL.md +14 -7
- 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/commands/setup/linear.md +1 -1
- package/plugins/src/base/hooks/block-instruction-file-edits.sh +47 -7
- package/plugins/src/base/hooks/block-no-verify.sh +49 -5
- package/plugins/src/base/hooks/block-shell-json-parsing.sh +20 -1
- package/plugins/src/base/skills/lisa-setup-linear/SKILL.md +14 -7
- package/scripts/lisa-enforcement-fallback.sh +14 -1
package/package.json
CHANGED
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
}
|
|
121
121
|
},
|
|
122
122
|
"name": "@codyswann/lisa",
|
|
123
|
-
"version": "2.341.
|
|
123
|
+
"version": "2.341.5",
|
|
124
124
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
125
125
|
"main": "dist/index.js",
|
|
126
126
|
"exports": {
|
|
@@ -6,9 +6,16 @@ allowed-tools: ["Bash", "Read", "Write", "Edit", "Skill", "AskUserQuestion", "mc
|
|
|
6
6
|
|
|
7
7
|
# Setup Linear: $ARGUMENTS
|
|
8
8
|
|
|
9
|
-
Make Linear a tracker, a PRD source, or both for this project. After this skill, `.lisa.config.json` contains `linear.workspace` (+ `linear.teamKey` when Linear is the tracker), the team carries the lifecycle label namespaces lisa needs, and (optionally) `tracker` / `source` point at Linear.
|
|
9
|
+
Make Linear a tracker, a PRD source, or both for this project. After this skill, `.lisa.config.json` contains `linear.workspace` (+ `linear.teamKey` when Linear is the tracker), the team carries the lifecycle states and label namespaces lisa needs, and (optionally) `tracker` / `source` point at Linear.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
The two lifecycles run on different primitives, and conflating them is the most common setup error:
|
|
12
|
+
|
|
13
|
+
- **Build queue → native workflow STATES**, read from `linear.workflow.*`. Not labels. See "Why Linear uses states, not labels" in `config-resolution`, and Step 3a below. `lisa-linear-build-intake` reads only these.
|
|
14
|
+
- **PRD lifecycle → PROJECT labels** (`prd-*`), because a PRD is a Linear Project.
|
|
15
|
+
|
|
16
|
+
Project labels and issue labels are distinct namespaces in Linear and are NOT interchangeable — creating an issue label named `prd-ready` will not work for the PRD flow. The one issue label this skill creates is the sentinel feedback marker, which belongs to the PRD flow despite being an issue label (Linear's MCP has no project-level comments — see `linear-prd-intake`).
|
|
17
|
+
|
|
18
|
+
**A `status:*` issue-label namespace is no longer scaffolded or read.** It was the pre-state-model build lane; see "Migrating a project that predates the state model" below for what to do with a config that still carries it.
|
|
12
19
|
|
|
13
20
|
## Workflow
|
|
14
21
|
|
|
@@ -28,10 +35,10 @@ Ask two things via `AskUserQuestion`.
|
|
|
28
35
|
|
|
29
36
|
> What should lisa use Linear for?
|
|
30
37
|
>
|
|
31
|
-
> 1. **Destination tracker** — lisa writes Epics→Projects, Stories→Issues, Sub-tasks→Sub-issues; the build queue runs off
|
|
38
|
+
> 1. **Destination tracker** — lisa writes Epics→Projects, Stories→Issues, Sub-tasks→Sub-issues; the build queue runs off native workflow **states** (`linear.workflow`), not labels. Sets `tracker: "linear"`. (Requires a team key.)
|
|
32
39
|
> 2. **PRD source** — humans flag Linear **projects** with `prd-ready`; `/lisa:intake` scans and ticketes them off the `prd-*` project-label namespace. Sets `source: "linear"`.
|
|
33
40
|
|
|
34
|
-
The role answer drives Step 3 (
|
|
41
|
+
The role answer drives Step 3 (states for the tracker lane, `prd-*` project labels for the PRD lane) and whether `teamKey` is required (tracker → yes).
|
|
35
42
|
|
|
36
43
|
### Step 1 — Establish Linear access
|
|
37
44
|
|
|
@@ -139,7 +146,7 @@ echo "Linear key validated. Org: $(echo "$VIEWER" | jq -r '.data.organization.ur
|
|
|
139
146
|
- **Workspace slug**: honor `--workspace=<slug>`. Otherwise derive from the validated identity — the GraphQL `organization.urlKey` (API path) or the team list's workspace (MCP path). Confirm with the user; this slug is the keychain `account` key and the multi-workspace disambiguator.
|
|
140
147
|
- **Team key** (required when Linear is the **tracker**): honor `--team=<KEY>`. Otherwise enumerate teams via `lisa-linear-access operation: list-teams({})` (or the GraphQL `teams` query) and present them via `AskUserQuestion` (label = team key, description = team name) for the user to pick the team that owns lisa's destination Issues. If Linear is source-only, `teamKey` is optional — skip unless the user wants to pin a team scope.
|
|
141
148
|
|
|
142
|
-
### Step 3 — Scaffold the lifecycle
|
|
149
|
+
### Step 3 — Scaffold the lifecycle namespaces
|
|
143
150
|
|
|
144
151
|
Read role → label with the default-fallback ladder the intake skills use, so scaffolded labels match exactly what they query.
|
|
145
152
|
|
|
@@ -280,7 +287,7 @@ jq -e '.linear.workspace' .lisa.config.json >/dev/null
|
|
|
280
287
|
[ "$(jq -r '.tracker // empty' .lisa.config.json)" = "linear" ] && jq -e '.linear.teamKey' .lisa.config.json >/dev/null
|
|
281
288
|
```
|
|
282
289
|
|
|
283
|
-
Confirm
|
|
290
|
+
Confirm what was scaffolded is present: `list-workflow-states` for every build role when Linear is the tracker, `list_project_labels` for `prd-*` (including the terminal `prd-verified`) and `list_issue_labels` for the sentinel when Linear is the PRD source. Do NOT expect a `status:*` namespace — it is not part of this model. Report success with the resolved workspace, team key (if any), which namespaces were scaffolded (created vs. already existed), any non-default overrides, and whether `tracker` / `source` were set. Direct the user to `/lisa:intake` to test.
|
|
284
291
|
|
|
285
292
|
## Idempotency
|
|
286
293
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Set up Linear as the tracker and/or PRD source for this project. Verifies Linear access (MCP OAuth or a personal API key in keychain), resolves the workspace slug and team key, scaffolds the
|
|
2
|
+
description: "Set up Linear as the tracker and/or PRD source for this project. Verifies Linear access (MCP OAuth or a personal API key in keychain), resolves the workspace slug and team key, scaffolds the build-queue workflow states (`linear.workflow`) and/or `prd-*` project-label (PRD) namespaces, writes the `linear` section of `.lisa.config.json`, and offers to set top-level `tracker: \"linear\"` / `source: \"linear\"`. No /lisa:setup:atlassian prerequisite."
|
|
3
3
|
allowed-tools: ["Skill"]
|
|
4
4
|
argument-hint: "[--workspace=<slug>] [--team=<KEY>]"
|
|
5
5
|
---
|
|
@@ -42,12 +42,48 @@ fi
|
|
|
42
42
|
tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty')"
|
|
43
43
|
[ -n "$tool_name" ] || exit 0
|
|
44
44
|
|
|
45
|
-
# Lisa's own bounded bridges write marked regions
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
# Lisa's own bounded bridges write marked regions — the agy project-learnings
|
|
46
|
+
# bridge and cross-pollinate's rule section. The premise of their exemption, as
|
|
47
|
+
# originally stated, is that they "replace in place and cannot grow the file".
|
|
48
|
+
# This enforces that premise rather than trusting it.
|
|
49
|
+
#
|
|
50
|
+
# Both sides have to be a marked region and nothing else:
|
|
51
|
+
#
|
|
52
|
+
# - old_string bounded proves the region really is on disk. Scanning the whole
|
|
53
|
+
# payload was trivially forgeable — any caller could put `<!-- LISA_` in the
|
|
54
|
+
# content it was WRITING and walk past the guard.
|
|
55
|
+
# - new_string bounded is what makes it a replacement rather than an append.
|
|
56
|
+
# old_string alone does not authorize anything: a caller can read a genuine
|
|
57
|
+
# marked region, echo it back verbatim, and still smuggle unbounded prose in
|
|
58
|
+
# after the closing marker. Requiring the written text to be exactly one
|
|
59
|
+
# marked region — whitespace aside — leaves nowhere to put it.
|
|
60
|
+
#
|
|
61
|
+
# Every edit in a MultiEdit must qualify; one unbounded edit taints the batch.
|
|
62
|
+
# Write is absent by construction: it has no old_string and clobbers the whole
|
|
63
|
+
# file, which is precisely the unbounded case.
|
|
64
|
+
case "$tool_name" in
|
|
65
|
+
Edit | MultiEdit)
|
|
66
|
+
if printf '%s' "$input" |
|
|
67
|
+
jq -e '
|
|
68
|
+
# Exactly ONE marked region and nothing else. The inner
|
|
69
|
+
# `(?!<!-- LISA_)` is what makes it one: a plain `[\s\S]*` anchors only
|
|
70
|
+
# the FIRST opening marker and the LAST closing marker, so
|
|
71
|
+
# `region + prose + region` satisfies it and the prose between the two
|
|
72
|
+
# regions rides along outside any marked block.
|
|
73
|
+
def bounded:
|
|
74
|
+
type == "string"
|
|
75
|
+
and test("^\\s*<!-- LISA_[A-Z_]+ -->(?:(?!<!-- LISA_)[\\s\\S])*<!-- LISA_[A-Z_]+ -->\\s*$");
|
|
76
|
+
def replacement_pairs:
|
|
77
|
+
if .tool_input.edits? then [.tool_input.edits[]?]
|
|
78
|
+
else [.tool_input] end;
|
|
79
|
+
replacement_pairs
|
|
80
|
+
| length > 0
|
|
81
|
+
and all(.[]; (.old_string | bounded) and (.new_string | bounded))
|
|
82
|
+
' >/dev/null 2>&1; then
|
|
83
|
+
exit 0
|
|
84
|
+
fi
|
|
85
|
+
;;
|
|
86
|
+
esac
|
|
51
87
|
|
|
52
88
|
# Basename match, case-insensitively: `agents.md` and `AGENTS.md` are the same
|
|
53
89
|
# file on the macOS checkouts this fleet runs on.
|
|
@@ -130,9 +166,13 @@ EOF
|
|
|
130
166
|
# Write signatures only. A bare mention (`cat AGENTS.md`, `rg x AGENTS.md`)
|
|
131
167
|
# is a read and must stay allowed — the filename has to appear as the target
|
|
132
168
|
# of a redirection, a tee, or an in-place sed.
|
|
169
|
+
# `>>?\|?` covers `>`, `>>`, and the noclobber override `>|`. The bare-`>`
|
|
170
|
+
# branch already catches explicit fd forms such as `1>` / `2>>` because the
|
|
171
|
+
# pattern is unanchored and matches the `>` inside them; `>|` was the real
|
|
172
|
+
# gap, since `|` is excluded by the target class and so terminated the match.
|
|
133
173
|
write_target='[^ |;&]*(agents|claude|copilot-instructions)\.md'
|
|
134
174
|
if printf '%s' "$command_str" |
|
|
135
|
-
grep -Eqi "
|
|
175
|
+
grep -Eqi ">>?\|?[[:space:]]*['\"]?$write_target|tee([[:space:]]+-[a-z]+)*[[:space:]]+['\"]?$write_target|sed[[:space:]]+[^|;&]*-i[^|;&]*$write_target"; then
|
|
136
176
|
refuse "$(printf '%s' "$command_str" |
|
|
137
177
|
grep -Eoi "$write_target" | head -1)"
|
|
138
178
|
fi
|
|
@@ -97,17 +97,61 @@ except ValueError:
|
|
|
97
97
|
|
|
98
98
|
normalized_tokens = [token.strip("();|&") for token in tokens]
|
|
99
99
|
|
|
100
|
+
# The only relocations that keep a repo's own hooks in play. `.husky` is the
|
|
101
|
+
# husky convention this fleet runs; `.githooks` is the common hand-rolled one.
|
|
102
|
+
# Anything else — including "" and /dev/null, which are simply the two most
|
|
103
|
+
# obvious members of the blocked set rather than special cases — is refused.
|
|
104
|
+
PERMITTED_HOOKS_PATHS = {".husky", ".githooks"}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def is_permitted_hooks_path(value):
|
|
108
|
+
"""Whether a core.hooksPath value relocates hooks rather than disabling them.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
value: The raw core.hooksPath value as it appeared on the command line.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
True if the path is an established in-repo hooks directory.
|
|
115
|
+
"""
|
|
116
|
+
cleaned = value.strip().strip("'\"")
|
|
117
|
+
if cleaned.startswith("./"):
|
|
118
|
+
cleaned = cleaned[2:]
|
|
119
|
+
return cleaned.rstrip("/") in PERMITTED_HOOKS_PATHS
|
|
120
|
+
|
|
100
121
|
for i, token in enumerate(normalized_tokens):
|
|
101
122
|
if token == "--no-verify":
|
|
102
123
|
sys.exit(1)
|
|
103
124
|
if token == "HUSKY=0" or token.startswith("HUSKY_SKIP_HOOKS="):
|
|
104
125
|
sys.exit(1)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
126
|
+
# Allowlist the destinations, do not denylist the disabling ones.
|
|
127
|
+
#
|
|
128
|
+
# This used to block only "" and /dev/null. But hooks are disabled just as
|
|
129
|
+
# completely by pointing hooksPath at any directory that happens to contain
|
|
130
|
+
# none — `-c core.hooksPath=/tmp/empty` bypassed every hook while reading as
|
|
131
|
+
# an ordinary path — so a denylist of "obviously disabling" values can
|
|
132
|
+
# always be stepped around by naming a third thing. The set of paths that
|
|
133
|
+
# DISABLE hooks is unbounded; the set that legitimately relocates them is
|
|
134
|
+
# tiny and known, so the allowlist is the only side that can be enumerated.
|
|
135
|
+
#
|
|
136
|
+
# Matched case-insensitively because git config variable names are:
|
|
137
|
+
# `CORE.HOOKSPATH=/x` and `core.hookspath=/x` are the same setting to git,
|
|
138
|
+
# so a case-sensitive check is bypassed by holding down shift.
|
|
139
|
+
lowered = token.lower()
|
|
140
|
+
if lowered.startswith("core.hookspath="):
|
|
141
|
+
if not is_permitted_hooks_path(token.split("=", 1)[1]):
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
if lowered == "core.hookspath" and i + 1 < len(normalized_tokens):
|
|
144
|
+
if not is_permitted_hooks_path(normalized_tokens[i + 1]):
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
# `git --config-env=core.hooksPath=SOMEVAR` sets the same config, reading
|
|
147
|
+
# the value out of the named environment variable. The path therefore is
|
|
148
|
+
# not in the command at all, so there is nothing to allowlist against —
|
|
149
|
+
# SOMEVAR can hold anything by the time git reads it. Any core.hooksPath
|
|
150
|
+
# routed through --config-env is refused outright.
|
|
151
|
+
if lowered.startswith("--config-env="):
|
|
152
|
+
spec = token.split("=", 1)[1]
|
|
153
|
+
if spec.split("=", 1)[0].strip().lower() == "core.hookspath":
|
|
108
154
|
sys.exit(1)
|
|
109
|
-
if token == "core.hooksPath" and i + 1 < len(normalized_tokens) and normalized_tokens[i + 1] in ("", "/dev/null"):
|
|
110
|
-
sys.exit(1)
|
|
111
155
|
|
|
112
156
|
sys.exit(0)
|
|
113
157
|
PY
|
|
@@ -24,6 +24,25 @@ set -euo pipefail
|
|
|
24
24
|
|
|
25
25
|
input="$(cat)"
|
|
26
26
|
|
|
27
|
+
# Probe the interpreters before the first use. Under `set -euo pipefail` an
|
|
28
|
+
# absent jq does not merely skip the parse — the assignment below dies with
|
|
29
|
+
# 127, and 127 is not a refusal, so the guard vanishes and the command runs.
|
|
30
|
+
# Worse, that 127 used to outrank a sibling guard's 2 in
|
|
31
|
+
# lisa-enforcement-fallback.sh, taking a real refusal down with it.
|
|
32
|
+
#
|
|
33
|
+
# Degrading to "allow" stays right: a hook that cannot parse its input cannot
|
|
34
|
+
# tell a bypass from an ordinary command, and failing closed would block every
|
|
35
|
+
# Bash call on a machine missing an interpreter. Doing it QUIETLY is what is
|
|
36
|
+
# wrong — a guard that is silently absent reads exactly like a guard that is
|
|
37
|
+
# passing. Mirrors block-no-verify.sh, which already had this.
|
|
38
|
+
for required in jq python3; do
|
|
39
|
+
if ! command -v "$required" >/dev/null 2>&1; then
|
|
40
|
+
printf 'block-shell-json-parsing: %s not found; JSON-parsing protection is NOT active\n' \
|
|
41
|
+
"$required" >&2
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
done
|
|
45
|
+
|
|
27
46
|
tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty')"
|
|
28
47
|
if [ "$tool_name" != "Bash" ]; then
|
|
29
48
|
exit 0
|
|
@@ -40,7 +59,7 @@ case "$command_str" in
|
|
|
40
59
|
*) exit 0 ;;
|
|
41
60
|
esac
|
|
42
61
|
|
|
43
|
-
|
|
62
|
+
# python3 is probed with jq at the top now, announced rather than silent.
|
|
44
63
|
|
|
45
64
|
if ! BLOCK_SHELL_JSON_COMMAND="$command_str" python3 - <<'PY'
|
|
46
65
|
import os
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lisa-setup-linear
|
|
3
|
-
description: "Configure Linear as the destination tracker and/or the PRD source for this project. Verifies Linear access (MCP OAuth or a personal API key in OS keychain), resolves the workspace slug and team key, scaffolds the build-queue
|
|
3
|
+
description: "Configure Linear as the destination tracker and/or the PRD source for this project. Verifies Linear access (MCP OAuth or a personal API key in OS keychain), resolves the workspace slug and team key, scaffolds the build-queue **workflow states** (`linear.workflow`) when Linear is the tracker and/or the PRD-lifecycle project-label namespace (`prd-*` + issue-level sentinel) when Linear is the PRD source, writes the `linear` section into `.lisa.config.json`, and offers to set top-level `tracker: \"linear\"` and/or `source: \"linear\"`. Idempotent — re-running updates the existing section and reuses existing labels. No /lisa:setup:atlassian prerequisite."
|
|
4
4
|
allowed-tools: ["Bash", "Read", "Write", "Edit", "Skill", "AskUserQuestion", "mcp__linear-server__authenticate", "mcp__linear-server__complete_authentication"]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Setup Linear: $ARGUMENTS
|
|
8
8
|
|
|
9
|
-
Make Linear a tracker, a PRD source, or both for this project. After this skill, `.lisa.config.json` contains `linear.workspace` (+ `linear.teamKey` when Linear is the tracker), the team carries the lifecycle label namespaces lisa needs, and (optionally) `tracker` / `source` point at Linear.
|
|
9
|
+
Make Linear a tracker, a PRD source, or both for this project. After this skill, `.lisa.config.json` contains `linear.workspace` (+ `linear.teamKey` when Linear is the tracker), the team carries the lifecycle states and label namespaces lisa needs, and (optionally) `tracker` / `source` point at Linear.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
The two lifecycles run on different primitives, and conflating them is the most common setup error:
|
|
12
|
+
|
|
13
|
+
- **Build queue → native workflow STATES**, read from `linear.workflow.*`. Not labels. See "Why Linear uses states, not labels" in `config-resolution`, and Step 3a below. `lisa-linear-build-intake` reads only these.
|
|
14
|
+
- **PRD lifecycle → PROJECT labels** (`prd-*`), because a PRD is a Linear Project.
|
|
15
|
+
|
|
16
|
+
Project labels and issue labels are distinct namespaces in Linear and are NOT interchangeable — creating an issue label named `prd-ready` will not work for the PRD flow. The one issue label this skill creates is the sentinel feedback marker, which belongs to the PRD flow despite being an issue label (Linear's MCP has no project-level comments — see `linear-prd-intake`).
|
|
17
|
+
|
|
18
|
+
**A `status:*` issue-label namespace is no longer scaffolded or read.** It was the pre-state-model build lane; see "Migrating a project that predates the state model" below for what to do with a config that still carries it.
|
|
12
19
|
|
|
13
20
|
## Workflow
|
|
14
21
|
|
|
@@ -28,10 +35,10 @@ Ask two things via `AskUserQuestion`.
|
|
|
28
35
|
|
|
29
36
|
> What should lisa use Linear for?
|
|
30
37
|
>
|
|
31
|
-
> 1. **Destination tracker** — lisa writes Epics→Projects, Stories→Issues, Sub-tasks→Sub-issues; the build queue runs off
|
|
38
|
+
> 1. **Destination tracker** — lisa writes Epics→Projects, Stories→Issues, Sub-tasks→Sub-issues; the build queue runs off native workflow **states** (`linear.workflow`), not labels. Sets `tracker: "linear"`. (Requires a team key.)
|
|
32
39
|
> 2. **PRD source** — humans flag Linear **projects** with `prd-ready`; `/lisa:intake` scans and ticketes them off the `prd-*` project-label namespace. Sets `source: "linear"`.
|
|
33
40
|
|
|
34
|
-
The role answer drives Step 3 (
|
|
41
|
+
The role answer drives Step 3 (states for the tracker lane, `prd-*` project labels for the PRD lane) and whether `teamKey` is required (tracker → yes).
|
|
35
42
|
|
|
36
43
|
### Step 1 — Establish Linear access
|
|
37
44
|
|
|
@@ -139,7 +146,7 @@ echo "Linear key validated. Org: $(echo "$VIEWER" | jq -r '.data.organization.ur
|
|
|
139
146
|
- **Workspace slug**: honor `--workspace=<slug>`. Otherwise derive from the validated identity — the GraphQL `organization.urlKey` (API path) or the team list's workspace (MCP path). Confirm with the user; this slug is the keychain `account` key and the multi-workspace disambiguator.
|
|
140
147
|
- **Team key** (required when Linear is the **tracker**): honor `--team=<KEY>`. Otherwise enumerate teams via `lisa-linear-access operation: list-teams({})` (or the GraphQL `teams` query) and present them via `AskUserQuestion` (label = team key, description = team name) for the user to pick the team that owns lisa's destination Issues. If Linear is source-only, `teamKey` is optional — skip unless the user wants to pin a team scope.
|
|
141
148
|
|
|
142
|
-
### Step 3 — Scaffold the lifecycle
|
|
149
|
+
### Step 3 — Scaffold the lifecycle namespaces
|
|
143
150
|
|
|
144
151
|
Read role → label with the default-fallback ladder the intake skills use, so scaffolded labels match exactly what they query.
|
|
145
152
|
|
|
@@ -280,7 +287,7 @@ jq -e '.linear.workspace' .lisa.config.json >/dev/null
|
|
|
280
287
|
[ "$(jq -r '.tracker // empty' .lisa.config.json)" = "linear" ] && jq -e '.linear.teamKey' .lisa.config.json >/dev/null
|
|
281
288
|
```
|
|
282
289
|
|
|
283
|
-
Confirm
|
|
290
|
+
Confirm what was scaffolded is present: `list-workflow-states` for every build role when Linear is the tracker, `list_project_labels` for `prd-*` (including the terminal `prd-verified`) and `list_issue_labels` for the sentinel when Linear is the PRD source. Do NOT expect a `status:*` namespace — it is not part of this model. Report success with the resolved workspace, team key (if any), which namespaces were scaffolded (created vs. already existed), any non-default overrides, and whether `tracker` / `source` were set. Direct the user to `/lisa:intake` to test.
|
|
284
291
|
|
|
285
292
|
## Idempotency
|
|
286
293
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Set up Linear as the tracker and/or PRD source for this project. Verifies Linear access (MCP OAuth or a personal API key in keychain), resolves the workspace slug and team key, scaffolds the
|
|
2
|
+
description: "Set up Linear as the tracker and/or PRD source for this project. Verifies Linear access (MCP OAuth or a personal API key in keychain), resolves the workspace slug and team key, scaffolds the build-queue workflow states (`linear.workflow`) and/or `prd-*` project-label (PRD) namespaces, writes the `linear` section of `.lisa.config.json`, and offers to set top-level `tracker: \"linear\"` / `source: \"linear\"`. No /lisa:setup:atlassian prerequisite."
|
|
3
3
|
allowed-tools: ["Skill"]
|
|
4
4
|
argument-hint: "[--workspace=<slug>] [--team=<KEY>]"
|
|
5
5
|
---
|
|
@@ -42,12 +42,48 @@ fi
|
|
|
42
42
|
tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty')"
|
|
43
43
|
[ -n "$tool_name" ] || exit 0
|
|
44
44
|
|
|
45
|
-
# Lisa's own bounded bridges write marked regions
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
# Lisa's own bounded bridges write marked regions — the agy project-learnings
|
|
46
|
+
# bridge and cross-pollinate's rule section. The premise of their exemption, as
|
|
47
|
+
# originally stated, is that they "replace in place and cannot grow the file".
|
|
48
|
+
# This enforces that premise rather than trusting it.
|
|
49
|
+
#
|
|
50
|
+
# Both sides have to be a marked region and nothing else:
|
|
51
|
+
#
|
|
52
|
+
# - old_string bounded proves the region really is on disk. Scanning the whole
|
|
53
|
+
# payload was trivially forgeable — any caller could put `<!-- LISA_` in the
|
|
54
|
+
# content it was WRITING and walk past the guard.
|
|
55
|
+
# - new_string bounded is what makes it a replacement rather than an append.
|
|
56
|
+
# old_string alone does not authorize anything: a caller can read a genuine
|
|
57
|
+
# marked region, echo it back verbatim, and still smuggle unbounded prose in
|
|
58
|
+
# after the closing marker. Requiring the written text to be exactly one
|
|
59
|
+
# marked region — whitespace aside — leaves nowhere to put it.
|
|
60
|
+
#
|
|
61
|
+
# Every edit in a MultiEdit must qualify; one unbounded edit taints the batch.
|
|
62
|
+
# Write is absent by construction: it has no old_string and clobbers the whole
|
|
63
|
+
# file, which is precisely the unbounded case.
|
|
64
|
+
case "$tool_name" in
|
|
65
|
+
Edit | MultiEdit)
|
|
66
|
+
if printf '%s' "$input" |
|
|
67
|
+
jq -e '
|
|
68
|
+
# Exactly ONE marked region and nothing else. The inner
|
|
69
|
+
# `(?!<!-- LISA_)` is what makes it one: a plain `[\s\S]*` anchors only
|
|
70
|
+
# the FIRST opening marker and the LAST closing marker, so
|
|
71
|
+
# `region + prose + region` satisfies it and the prose between the two
|
|
72
|
+
# regions rides along outside any marked block.
|
|
73
|
+
def bounded:
|
|
74
|
+
type == "string"
|
|
75
|
+
and test("^\\s*<!-- LISA_[A-Z_]+ -->(?:(?!<!-- LISA_)[\\s\\S])*<!-- LISA_[A-Z_]+ -->\\s*$");
|
|
76
|
+
def replacement_pairs:
|
|
77
|
+
if .tool_input.edits? then [.tool_input.edits[]?]
|
|
78
|
+
else [.tool_input] end;
|
|
79
|
+
replacement_pairs
|
|
80
|
+
| length > 0
|
|
81
|
+
and all(.[]; (.old_string | bounded) and (.new_string | bounded))
|
|
82
|
+
' >/dev/null 2>&1; then
|
|
83
|
+
exit 0
|
|
84
|
+
fi
|
|
85
|
+
;;
|
|
86
|
+
esac
|
|
51
87
|
|
|
52
88
|
# Basename match, case-insensitively: `agents.md` and `AGENTS.md` are the same
|
|
53
89
|
# file on the macOS checkouts this fleet runs on.
|
|
@@ -130,9 +166,13 @@ EOF
|
|
|
130
166
|
# Write signatures only. A bare mention (`cat AGENTS.md`, `rg x AGENTS.md`)
|
|
131
167
|
# is a read and must stay allowed — the filename has to appear as the target
|
|
132
168
|
# of a redirection, a tee, or an in-place sed.
|
|
169
|
+
# `>>?\|?` covers `>`, `>>`, and the noclobber override `>|`. The bare-`>`
|
|
170
|
+
# branch already catches explicit fd forms such as `1>` / `2>>` because the
|
|
171
|
+
# pattern is unanchored and matches the `>` inside them; `>|` was the real
|
|
172
|
+
# gap, since `|` is excluded by the target class and so terminated the match.
|
|
133
173
|
write_target='[^ |;&]*(agents|claude|copilot-instructions)\.md'
|
|
134
174
|
if printf '%s' "$command_str" |
|
|
135
|
-
grep -Eqi "
|
|
175
|
+
grep -Eqi ">>?\|?[[:space:]]*['\"]?$write_target|tee([[:space:]]+-[a-z]+)*[[:space:]]+['\"]?$write_target|sed[[:space:]]+[^|;&]*-i[^|;&]*$write_target"; then
|
|
136
176
|
refuse "$(printf '%s' "$command_str" |
|
|
137
177
|
grep -Eoi "$write_target" | head -1)"
|
|
138
178
|
fi
|
|
@@ -24,6 +24,25 @@ set -euo pipefail
|
|
|
24
24
|
|
|
25
25
|
input="$(cat)"
|
|
26
26
|
|
|
27
|
+
# Probe the interpreters before the first use. Under `set -euo pipefail` an
|
|
28
|
+
# absent jq does not merely skip the parse — the assignment below dies with
|
|
29
|
+
# 127, and 127 is not a refusal, so the guard vanishes and the command runs.
|
|
30
|
+
# Worse, that 127 used to outrank a sibling guard's 2 in
|
|
31
|
+
# lisa-enforcement-fallback.sh, taking a real refusal down with it.
|
|
32
|
+
#
|
|
33
|
+
# Degrading to "allow" stays right: a hook that cannot parse its input cannot
|
|
34
|
+
# tell a bypass from an ordinary command, and failing closed would block every
|
|
35
|
+
# Bash call on a machine missing an interpreter. Doing it QUIETLY is what is
|
|
36
|
+
# wrong — a guard that is silently absent reads exactly like a guard that is
|
|
37
|
+
# passing. Mirrors block-no-verify.sh, which already had this.
|
|
38
|
+
for required in jq python3; do
|
|
39
|
+
if ! command -v "$required" >/dev/null 2>&1; then
|
|
40
|
+
printf 'block-shell-json-parsing: %s not found; JSON-parsing protection is NOT active\n' \
|
|
41
|
+
"$required" >&2
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
done
|
|
45
|
+
|
|
27
46
|
tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty')"
|
|
28
47
|
if [ "$tool_name" != "Bash" ]; then
|
|
29
48
|
exit 0
|
|
@@ -40,7 +59,7 @@ case "$command_str" in
|
|
|
40
59
|
*) exit 0 ;;
|
|
41
60
|
esac
|
|
42
61
|
|
|
43
|
-
|
|
62
|
+
# python3 is probed with jq at the top now, announced rather than silent.
|
|
44
63
|
|
|
45
64
|
if ! BLOCK_SHELL_JSON_COMMAND="$command_str" python3 - <<'PY'
|
|
46
65
|
import os
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lisa-setup-linear
|
|
3
|
-
description: "Configure Linear as the destination tracker and/or the PRD source for this project. Verifies Linear access (MCP OAuth or a personal API key in OS keychain), resolves the workspace slug and team key, scaffolds the build-queue
|
|
3
|
+
description: "Configure Linear as the destination tracker and/or the PRD source for this project. Verifies Linear access (MCP OAuth or a personal API key in OS keychain), resolves the workspace slug and team key, scaffolds the build-queue **workflow states** (`linear.workflow`) when Linear is the tracker and/or the PRD-lifecycle project-label namespace (`prd-*` + issue-level sentinel) when Linear is the PRD source, writes the `linear` section into `.lisa.config.json`, and offers to set top-level `tracker: \"linear\"` and/or `source: \"linear\"`. Idempotent — re-running updates the existing section and reuses existing labels. No /lisa:setup:atlassian prerequisite."
|
|
4
4
|
allowed-tools: ["Bash", "Read", "Write", "Edit", "Skill", "AskUserQuestion", "mcp__linear-server__authenticate", "mcp__linear-server__complete_authentication"]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Setup Linear: $ARGUMENTS
|
|
8
8
|
|
|
9
|
-
Make Linear a tracker, a PRD source, or both for this project. After this skill, `.lisa.config.json` contains `linear.workspace` (+ `linear.teamKey` when Linear is the tracker), the team carries the lifecycle label namespaces lisa needs, and (optionally) `tracker` / `source` point at Linear.
|
|
9
|
+
Make Linear a tracker, a PRD source, or both for this project. After this skill, `.lisa.config.json` contains `linear.workspace` (+ `linear.teamKey` when Linear is the tracker), the team carries the lifecycle states and label namespaces lisa needs, and (optionally) `tracker` / `source` point at Linear.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
The two lifecycles run on different primitives, and conflating them is the most common setup error:
|
|
12
|
+
|
|
13
|
+
- **Build queue → native workflow STATES**, read from `linear.workflow.*`. Not labels. See "Why Linear uses states, not labels" in `config-resolution`, and Step 3a below. `lisa-linear-build-intake` reads only these.
|
|
14
|
+
- **PRD lifecycle → PROJECT labels** (`prd-*`), because a PRD is a Linear Project.
|
|
15
|
+
|
|
16
|
+
Project labels and issue labels are distinct namespaces in Linear and are NOT interchangeable — creating an issue label named `prd-ready` will not work for the PRD flow. The one issue label this skill creates is the sentinel feedback marker, which belongs to the PRD flow despite being an issue label (Linear's MCP has no project-level comments — see `linear-prd-intake`).
|
|
17
|
+
|
|
18
|
+
**A `status:*` issue-label namespace is no longer scaffolded or read.** It was the pre-state-model build lane; see "Migrating a project that predates the state model" below for what to do with a config that still carries it.
|
|
12
19
|
|
|
13
20
|
## Workflow
|
|
14
21
|
|
|
@@ -28,10 +35,10 @@ Ask two things via `AskUserQuestion`.
|
|
|
28
35
|
|
|
29
36
|
> What should lisa use Linear for?
|
|
30
37
|
>
|
|
31
|
-
> 1. **Destination tracker** — lisa writes Epics→Projects, Stories→Issues, Sub-tasks→Sub-issues; the build queue runs off
|
|
38
|
+
> 1. **Destination tracker** — lisa writes Epics→Projects, Stories→Issues, Sub-tasks→Sub-issues; the build queue runs off native workflow **states** (`linear.workflow`), not labels. Sets `tracker: "linear"`. (Requires a team key.)
|
|
32
39
|
> 2. **PRD source** — humans flag Linear **projects** with `prd-ready`; `/lisa:intake` scans and ticketes them off the `prd-*` project-label namespace. Sets `source: "linear"`.
|
|
33
40
|
|
|
34
|
-
The role answer drives Step 3 (
|
|
41
|
+
The role answer drives Step 3 (states for the tracker lane, `prd-*` project labels for the PRD lane) and whether `teamKey` is required (tracker → yes).
|
|
35
42
|
|
|
36
43
|
### Step 1 — Establish Linear access
|
|
37
44
|
|
|
@@ -139,7 +146,7 @@ echo "Linear key validated. Org: $(echo "$VIEWER" | jq -r '.data.organization.ur
|
|
|
139
146
|
- **Workspace slug**: honor `--workspace=<slug>`. Otherwise derive from the validated identity — the GraphQL `organization.urlKey` (API path) or the team list's workspace (MCP path). Confirm with the user; this slug is the keychain `account` key and the multi-workspace disambiguator.
|
|
140
147
|
- **Team key** (required when Linear is the **tracker**): honor `--team=<KEY>`. Otherwise enumerate teams via `lisa-linear-access operation: list-teams({})` (or the GraphQL `teams` query) and present them via `AskUserQuestion` (label = team key, description = team name) for the user to pick the team that owns lisa's destination Issues. If Linear is source-only, `teamKey` is optional — skip unless the user wants to pin a team scope.
|
|
141
148
|
|
|
142
|
-
### Step 3 — Scaffold the lifecycle
|
|
149
|
+
### Step 3 — Scaffold the lifecycle namespaces
|
|
143
150
|
|
|
144
151
|
Read role → label with the default-fallback ladder the intake skills use, so scaffolded labels match exactly what they query.
|
|
145
152
|
|
|
@@ -280,7 +287,7 @@ jq -e '.linear.workspace' .lisa.config.json >/dev/null
|
|
|
280
287
|
[ "$(jq -r '.tracker // empty' .lisa.config.json)" = "linear" ] && jq -e '.linear.teamKey' .lisa.config.json >/dev/null
|
|
281
288
|
```
|
|
282
289
|
|
|
283
|
-
Confirm
|
|
290
|
+
Confirm what was scaffolded is present: `list-workflow-states` for every build role when Linear is the tracker, `list_project_labels` for `prd-*` (including the terminal `prd-verified`) and `list_issue_labels` for the sentinel when Linear is the PRD source. Do NOT expect a `status:*` namespace — it is not part of this model. Report success with the resolved workspace, team key (if any), which namespaces were scaffolded (created vs. already existed), any non-default overrides, and whether `tracker` / `source` were set. Direct the user to `/lisa:intake` to test.
|
|
284
291
|
|
|
285
292
|
## Idempotency
|
|
286
293
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Set up Linear as the tracker and/or PRD source for this project. Verifies Linear access (MCP OAuth or a personal API key in keychain), resolves the workspace slug and team key, scaffolds the
|
|
2
|
+
description: "Set up Linear as the tracker and/or PRD source for this project. Verifies Linear access (MCP OAuth or a personal API key in keychain), resolves the workspace slug and team key, scaffolds the build-queue workflow states (`linear.workflow`) and/or `prd-*` project-label (PRD) namespaces, writes the `linear` section of `.lisa.config.json`, and offers to set top-level `tracker: \"linear\"` / `source: \"linear\"`. No /lisa:setup:atlassian prerequisite."
|
|
3
3
|
allowed-tools: ["Skill"]
|
|
4
4
|
argument-hint: "[--workspace=<slug>] [--team=<KEY>]"
|
|
5
5
|
---
|