@codyswann/lisa 3.5.4 → 3.6.0
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 +3 -2
- package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +773 -0
- package/dist/codex/hooks-installer.d.ts.map +1 -1
- package/dist/codex/hooks-installer.js +8 -0
- package/dist/codex/hooks-installer.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +36 -16
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-catalog.d.ts.map +1 -1
- package/dist/opencode/plugin-catalog.js +5 -0
- package/dist/opencode/plugin-catalog.js.map +1 -1
- package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +210 -0
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +5 -1
- package/plugins/lisa/.codex-plugin/hooks.json +4 -0
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-github-validate-issue/SKILL.md +12 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-github-write-issue/SKILL.md +12 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-validate-ticket/SKILL.md +12 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-write-ticket/SKILL.md +12 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-validate-issue/SKILL.md +12 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-write-issue/SKILL.md +15 -2
- package/plugins/lisa/hooks/block-direct-issue-create.agy.sh +59 -0
- package/plugins/lisa/hooks/block-direct-issue-create.sh +773 -0
- package/plugins/lisa/rules/eager/derived-branch-plan.md +3 -1
- package/plugins/lisa/rules/eager/ready-role-filing.md +4 -0
- package/plugins/lisa/rules/reference/derived-branch-plan.md +34 -0
- package/plugins/lisa/rules/reference/ready-role-filing.md +27 -0
- package/plugins/lisa/skills/lisa-github-validate-issue/SKILL.md +12 -6
- package/plugins/lisa/skills/lisa-github-write-issue/SKILL.md +12 -2
- package/plugins/lisa/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa/skills/lisa-jira-validate-ticket/SKILL.md +12 -6
- package/plugins/lisa/skills/lisa-jira-write-ticket/SKILL.md +12 -2
- package/plugins/lisa/skills/lisa-linear-validate-issue/SKILL.md +12 -6
- package/plugins/lisa/skills/lisa-linear-write-issue/SKILL.md +15 -2
- package/plugins/lisa-agy/hooks/block-direct-issue-create.agy.sh +59 -0
- package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +773 -0
- package/plugins/lisa-agy/hooks.json +13 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-github-validate-issue/SKILL.md +12 -6
- package/plugins/lisa-agy/skills/lisa-github-write-issue/SKILL.md +12 -2
- package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa-agy/skills/lisa-jira-validate-ticket/SKILL.md +12 -6
- package/plugins/lisa-agy/skills/lisa-jira-write-ticket/SKILL.md +12 -2
- package/plugins/lisa-agy/skills/lisa-linear-validate-issue/SKILL.md +12 -6
- package/plugins/lisa-agy/skills/lisa-linear-write-issue/SKILL.md +15 -2
- 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 +5 -1
- package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +773 -0
- package/plugins/lisa-copilot/rules/eager/derived-branch-plan.md +3 -1
- package/plugins/lisa-copilot/rules/eager/ready-role-filing.md +4 -0
- package/plugins/lisa-copilot/rules/reference/derived-branch-plan.md +34 -0
- package/plugins/lisa-copilot/rules/reference/ready-role-filing.md +27 -0
- package/plugins/lisa-copilot/skills/lisa-github-validate-issue/SKILL.md +12 -6
- package/plugins/lisa-copilot/skills/lisa-github-write-issue/SKILL.md +12 -2
- package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa-copilot/skills/lisa-jira-validate-ticket/SKILL.md +12 -6
- package/plugins/lisa-copilot/skills/lisa-jira-write-ticket/SKILL.md +12 -2
- package/plugins/lisa-copilot/skills/lisa-linear-validate-issue/SKILL.md +12 -6
- package/plugins/lisa-copilot/skills/lisa-linear-write-issue/SKILL.md +15 -2
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +773 -0
- package/plugins/lisa-cursor/hooks/hooks.json +4 -0
- package/plugins/lisa-cursor/rules/derived-branch-plan-reference.mdc +34 -0
- package/plugins/lisa-cursor/rules/derived-branch-plan.mdc +3 -1
- package/plugins/lisa-cursor/rules/ready-role-filing-reference.mdc +27 -0
- package/plugins/lisa-cursor/rules/ready-role-filing.mdc +4 -0
- package/plugins/lisa-cursor/skills/lisa-github-validate-issue/SKILL.md +12 -6
- package/plugins/lisa-cursor/skills/lisa-github-write-issue/SKILL.md +12 -2
- package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa-cursor/skills/lisa-jira-validate-ticket/SKILL.md +12 -6
- package/plugins/lisa-cursor/skills/lisa-jira-write-ticket/SKILL.md +12 -2
- package/plugins/lisa-cursor/skills/lisa-linear-validate-issue/SKILL.md +12 -6
- package/plugins/lisa-cursor/skills/lisa-linear-write-issue/SKILL.md +15 -2
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/.claude-plugin/plugin.json +4 -0
- package/plugins/src/base/hooks/block-direct-issue-create.agy.sh +59 -0
- package/plugins/src/base/hooks/block-direct-issue-create.sh +773 -0
- package/plugins/src/base/rules/eager/derived-branch-plan.md +3 -1
- package/plugins/src/base/rules/eager/ready-role-filing.md +4 -0
- package/plugins/src/base/rules/reference/derived-branch-plan.md +34 -0
- package/plugins/src/base/rules/reference/ready-role-filing.md +27 -0
- package/plugins/src/base/skills/lisa-github-validate-issue/SKILL.md +12 -6
- package/plugins/src/base/skills/lisa-github-write-issue/SKILL.md +12 -2
- package/plugins/src/base/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/src/base/skills/lisa-jira-validate-ticket/SKILL.md +12 -6
- package/plugins/src/base/skills/lisa-jira-write-ticket/SKILL.md +12 -2
- package/plugins/src/base/skills/lisa-linear-validate-issue/SKILL.md +12 -6
- package/plugins/src/base/skills/lisa-linear-write-issue/SKILL.md +15 -2
- package/scripts/build-plugins.sh +1 -1
- package/scripts/generate-agy-plugin-artifacts.mjs +10 -0
- package/scripts/lib/per-agent-hook-filter.mjs +10 -0
- package/scripts/lisa-enforcement-fallback.sh +3 -2
|
@@ -32,7 +32,9 @@ A missing or **ambiguous** mapping, a key that is **not unique**, or a branch ab
|
|
|
32
32
|
|
|
33
33
|
## Exemption
|
|
34
34
|
|
|
35
|
-
Work that declares no runtime behavior change — `runtime_behavior_change = false`, i.e. doc-only, config-only, and type-only items —
|
|
35
|
+
Work that declares no runtime behavior change — `runtime_behavior_change = false`, i.e. doc-only, config-only, and type-only items — has nothing to derive from and **requires no branch plan**. Its **absence is correct**, not a missing section, and no gate may demand one. Containers (an Epic, or a Story/Spike still holding child work) are exempt for the same reason: they are not built directly.
|
|
36
|
+
|
|
37
|
+
But the exemption is **declared, never inferred from an absent section**. `Target Backend Environment` is rendered on every leaf, because it is where `runtime_behavior_change` is persisted: an exact configured environment key means `true`, `None — no runtime behavior change: doc-only` (or `config-only` / `type-only`) means `false`, `None — container: state rolls up from children` marks a container, and an **absent section is underivable — never `false`**. On a live item the stored declaration beats any caller assertion, and a contradicting assertion fails S8 rather than being believed. Absence used to be the only signal, which meant the flag every one of S8, S11, S14, and S19 turns on could not be checked against the item at all.
|
|
36
38
|
|
|
37
39
|
The exemption is one-way. Exempt work that *carries* a branch plan fails validation — branches asserted for work that declared it has no runtime target are hand-authored by definition, which is the second-authority failure this rule exists to prevent.
|
|
38
40
|
|
|
@@ -19,4 +19,8 @@ Filed, not ready, and no `human_gate` is the **incomplete handoff** case — wri
|
|
|
19
19
|
|
|
20
20
|
`lisa-repair-intake` sweeps for open items that are neither in the ready role nor marked `[lisa-human-gate]`, and surfaces them rather than promoting them.
|
|
21
21
|
|
|
22
|
+
## This rule is enforced
|
|
23
|
+
|
|
24
|
+
The PreToolUse guard `block-direct-issue-create.sh` refuses a direct tracker-creation command (`gh issue create`, `gh api` POST to an issues endpoint, `linear`/`jira`/`acli` creates, equivalent `curl` posts) that carries **no readiness declaration** — no configured build-ready role and no `[lisa-human-gate]` marker. It shipped because this rule as prose did not bind: 13 of 13 issues filed during the session that merged it bypassed it, several by the agent that wrote it, while the one obligation backed by a git hook was honored 50 of 50 times. It stands down where no tracker is configured, and for an operator who set `LISA_ALLOW_DIRECT_ISSUE_CREATE` **in the ambient environment** — never from an inline assignment on the refused command.
|
|
25
|
+
|
|
22
26
|
Full contract (the per-vendor before/after table, marker format, and recovery sweep): [reference/ready-role-filing.md](../reference/ready-role-filing.md).
|
|
@@ -58,6 +58,39 @@ A plan whose two fields name **two different branches** is malformed and fails v
|
|
|
58
58
|
|
|
59
59
|
**Never guess.** There is no fallback that quietly substitutes `main`, the remote default branch, or the branch of a sibling environment to keep a write from failing — a silent guess here is the exact failure this contract exists to prevent, and a stopped write is cheap next to a wrong-branch merge. The remote default branch enters only through the `Assumption:` form the environment grammar already defines, and only recorded as an assumption.
|
|
60
60
|
|
|
61
|
+
## The exemption is declared, never inferred from absence
|
|
62
|
+
|
|
63
|
+
`runtime_behavior_change` is what S8, S11, S14, and S19 all branch on, and it began life as a **caller-asserted validator input** — the three validators still list it under "Behavioral flags — caller asserts these". Nothing ever persisted it. But the live-item path says: fetch the item, *parse the body sections, derive the spec fields*, then run the gates — and this one field could not be derived from anything the writer had stored. So on re-validation the gate believed whatever the caller claimed about the item it was supposedly checking. `lisa-*-verify` could not produce a truthful verdict, and neither could a human auditor reading the item.
|
|
64
|
+
|
|
65
|
+
Absence could not stand in for the flag either, because absence is what the two opposite cases look like. An item with no `Target Backend Environment` is either correctly exempt or wrongly missing a required section, and nothing on the item told them apart.
|
|
66
|
+
|
|
67
|
+
So the flag is persisted, in the section it is already about. **`Target Backend Environment` is rendered on every leaf**, and its content carries the declaration:
|
|
68
|
+
|
|
69
|
+
| Content | `runtime_behavior_change` | Meaning |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| An exact configured `deploy.branches` key — bare, `Confirmed:`, `Inferred:`, or `Assumption:` per the environment grammar | `true` | applicable work, targeting that environment |
|
|
72
|
+
| `None — no runtime behavior change: doc-only` (or `config-only`, `type-only`) | `false` | exempt by declaration |
|
|
73
|
+
| `None — container: state rolls up from children` | `false` | an Epic/Project, or an item holding child work |
|
|
74
|
+
| The section is absent | **underivable** | a legacy item written before this contract |
|
|
75
|
+
|
|
76
|
+
The `None —` prefix is the machine discriminator; the words after it are for the human reading the item. It is deliberately **visible prose rather than an HTML comment**, for exactly the reason the provenance line is: JIRA's ADF has no comment node, so a marker that only exists in markdown cannot be a vendor-neutral discriminator.
|
|
77
|
+
|
|
78
|
+
**Persisted beats asserted.** On a live item the stored declaration is authoritative. A caller asserting a `runtime_behavior_change` that contradicts it does not override it — the disagreement fails S8, naming both values, because one of the two is wrong and silently preferring either is how an unauditable gate gets built. On a *proposed* spec nothing is persisted yet, so the caller's assertion is the input and the writer's job is to render it.
|
|
79
|
+
|
|
80
|
+
**Underivable is its own verdict, never `false`.** A legacy item with no section gets `N/A` plus a repair note from every flag-dependent gate, repaired at claim time beside the legacy branch-plan repair — the same asymmetry, for the same reason. What must never happen is reading absence as `false`, which is the silent assumption that made S8, S11, S14, and S19 undecidable on a live item in the first place.
|
|
81
|
+
|
|
82
|
+
### How strongly this binds
|
|
83
|
+
|
|
84
|
+
Stated plainly, because the session that produced this contract exists to make exactly this distinction and it would be dishonest to blur it here.
|
|
85
|
+
|
|
86
|
+
This is **not** an executable control. `ready-role-filing`'s sibling guard is a hook that exits 2 and physically refuses a tool call; nothing here refuses anything. What enforces this contract is a validator agent reading `SKILL.md` and choosing to apply it — the same rung of the ladder that failed to bind 13 filings out of 13.
|
|
87
|
+
|
|
88
|
+
What the regression suite adds is narrower than enforcement and worth naming precisely: it pins the **wording** across all six generated skill roots, so the contract cannot be silently deleted, softened, or lost in a regeneration. It cannot catch a validator that documents the rule and then ignores it at runtime.
|
|
89
|
+
|
|
90
|
+
That is the ceiling for now rather than a shortcut, because `SKILL.md` *is* the validators' execution substrate — there is no compiled artifact underneath to attach a hook to. The honest summary is: absence-as-`false` was undecidable and is now *decidable*, which is a real improvement and a strictly weaker claim than *enforced*. Treat a gate verdict resting on this contract as evidence, not proof, until a deterministic checker exists to read the declaration off a live item.
|
|
91
|
+
|
|
92
|
+
This costs nothing in derivation difficulty. A single-environment project — Lisa itself is one, `deploy.branches: { production: main }` — recomputes every applicable item to the same branch. The gate was never hard to satisfy; it was impossible to *audit*, and those are different problems with different fixes. Weakening S19 would have fixed neither.
|
|
93
|
+
|
|
61
94
|
## Gate S19 — Branch Plan derivation
|
|
62
95
|
|
|
63
96
|
Registered in all three validators as `| S19 Branch Plan derivation | technical | false |`. The gate **recomputes** the plan from current config and compares; it never reads the rendered branches as input.
|
|
@@ -66,6 +99,7 @@ Registered in all three validators as `| S19 Branch Plan derivation | technical
|
|
|
66
99
|
|---|---|
|
|
67
100
|
| Exempt work (`runtime_behavior_change = false`, or a container) with no plan | `N/A` |
|
|
68
101
|
| Exempt work **carrying** a plan | **FAIL** — hand-authored branches on work that declared no runtime target |
|
|
102
|
+
| `runtime_behavior_change` **underivable** (live legacy item, no Target Backend Environment declaration) | `N/A` with a repair note — absence is never read as `false` |
|
|
69
103
|
| Applicable work, plan present, matches recomputed plan | PASS |
|
|
70
104
|
| Applicable work, plan present, disagrees with recomputed plan | **FAIL** |
|
|
71
105
|
| Applicable work, plan present, missing the provenance line and disagreeing | **FAIL** (hand-authored) |
|
|
@@ -54,6 +54,33 @@ The sibling `e2e-coverage-gaps` skills are the contrast: a missing automated tes
|
|
|
54
54
|
|
|
55
55
|
Other legitimate human-gate filings follow the same shape — `lisa-learnings-audit` promotion/demotion tickets, `lisa-improve-harness` intervention proposals, automation-retirement proposals, and provisioning tickets for access a human must grant. Each declares its `human_gate` reason rather than relying on a default.
|
|
56
56
|
|
|
57
|
+
## This rule is enforced, not merely stated
|
|
58
|
+
|
|
59
|
+
The rule shipped as prose first, and prose did not hold. A conformance audit of the ~13 issues filed during the session that shipped it found **13 of 13 bypassed it**, with zero `lisa-track` / `lisa-tracker-write` invocations — eight of them filed *after* the rule merged, several by the agent that wrote it. Over the same window `Co-Authored-By` compliance was **50 of 50**, because a husky `commit-msg` hook enforces that one. The contrast is the whole argument: at the EAGER-RULE rung this rule did not bind even its own author, while the executable control was never once violated. `learnings-ladder` says machine-checkable knowledge belongs at EXECUTABLE-CONTROL, and this rule is machine-checkable.
|
|
60
|
+
|
|
61
|
+
The control is the PreToolUse Bash guard `block-direct-issue-create.sh`, shipped to every agent variant (Claude, Codex, Cursor, Copilot, agy, OpenCode) and to the host `scripts/lisa-hooks/` fallback. It refuses a direct tracker-creation command — `gh issue create`, `gh api` POST to an issues endpoint, `gh api graphql createIssue`, `linear issue create`, `jira issue create`, `acli … create`, and equivalent `curl` posts — when the command carries **no readiness declaration**.
|
|
62
|
+
|
|
63
|
+
What it checks is the artifact, not the caller. A Bash-level hook cannot observe call provenance: any provenance signal is settable by the very agent being governed, so a guard built on one would be theatre. So the guard asks whether the command about to run produces a correctly declared item — the configured build-ready role is applied, or a `[lisa-human-gate]` marker is present (inline, or in the `--body-file` the create is about to submit). That is precisely the machine-checkable content of this rule, and it lets the three writers through by construction, because they always stamp one.
|
|
64
|
+
|
|
65
|
+
Two ways out, both deliberate:
|
|
66
|
+
|
|
67
|
+
- **No tracker configured** — `.lisa.config.json` absent or carrying no `tracker`. There is no `lisa-tracker-write` to route through, so the guard stands down. This is the bootstrapping case, and it is *detected* rather than asserted: nobody has to remember an env var to bring up a new repo.
|
|
68
|
+
- **`LISA_ALLOW_DIRECT_ISSUE_CREATE`** in the ambient environment — the human operator's override, mirroring `LISA_ALLOW_INSTRUCTION_FILE_WRITE`. It is honored **only** from the environment the hook inherits and is refused outright when it appears as an inline assignment on the intercepted command. A tool-call shell is fresh each time and its exports never reach the hook, so the ambient variable can only have been set by a human before the session began. An escape the governed agent reaches by typing one more token in front of the command it was just refused is not an escape hatch; it is this rule's original failure with extra steps.
|
|
69
|
+
|
|
70
|
+
### Where the guard's authority ends
|
|
71
|
+
|
|
72
|
+
Two limits, stated rather than implied, because a control that documents its own boundary is worth more than one that suggests completeness it does not have.
|
|
73
|
+
|
|
74
|
+
**The declaration is checked positionally, and only where the declaration actually lives.** The build-ready role counts as the value of a `--label` / `--labels` / `--add-label` / `--status` / `--state` flag, never as free text: a role named in a title or body is a role *mentioned*, not a role *applied*, and matching it anywhere let a bug report titled `"status:ready is broken"` declare its own readiness. This is the failure mode `block-no-verify` hit from the other direction, where an allowlist added to harden the guard became its bypass because `git --config-env` reads its argument as an environment-variable name rather than a path. **The rule generalizes: before matching or exempting a token, ask what it means in every position it can occupy.** Only the long flag spellings are honored, since short flags differ per CLI (`-s` is `--state` on one tracker and `--summary` on another). The `[lisa-human-gate]` marker is matched anywhere by deliberate contrast — it is a marker with no other meaning, so its presence anywhere in the payload *is* the declaration.
|
|
75
|
+
|
|
76
|
+
**Nothing after a bare `--` can declare anything, and the trackers disagree about why that matters.** A post-`--` token is an operand, not a flag, so it never reaches the created item. Crediting one is the same error as reading the role out of a title, one position over. The two CLIs available for testing behave differently: `gh` 2.96.0 rejects a post-`--` flag outright (`unknown arguments`), so the shape is unexploitable there — but `acli` parses straight past `--` and proceeds to create the work item with the trailing `--status` **silently unapplied**, which made it a live bypass on the JIRA path until it was closed. `jira`, `linear`, `http`, and `wget` were not installed when this was written and their behavior is **unverified**. The guard therefore fails closed on every path rather than inheriting `gh`'s strictness — a vendor-neutral control cannot assume the strictest vendor is representative, and this is the case that proves it.
|
|
77
|
+
|
|
78
|
+
**A marker inside a heredoc is not seen.** The guard strips heredoc bodies before tokenising, so quoted prose cannot be mistaken for argv — which also means a `[lisa-human-gate]` marker passed through a heredoc is invisible to it and the command is refused. Pass the body with `--body-file <path>`, which the guard reads, or put the marker inline on the command. Every Lisa writer already uses `--body-file`, so this bites only hand-written creations.
|
|
79
|
+
|
|
80
|
+
**Remote execution is out of scope.** `ssh host 'gh issue create …'` is not intercepted. Catching it would mean recursing into arbitrary trailing quoted operands, which refuses ordinary commands like `git commit -m "the gh issue create guard"`; and the command runs against another host's tracker configuration, where that host's own guard is the right control. Named rather than quietly missing.
|
|
81
|
+
|
|
82
|
+
**A guard on tool calls cannot govern configuration.** An agent that edits the harness's own settings — the `env` block in `settings.json`, a shell profile the session is later started from — reaches the override without ever running a refused command. That is outside this guard by construction, and it is bounded by policy instead: changing harness configuration is not something an agent may do on its own initiative, and no message from another agent authorizes it. Naming the limit is the point; a reader who believes the guard is total will not think to watch the surface it cannot see.
|
|
83
|
+
|
|
57
84
|
## Recovery
|
|
58
85
|
|
|
59
86
|
`lisa-repair-intake` sweeps for the failure this rule prevents: recently filed items that are open, not in the configured ready role, and carrying **no** `[lisa-human-gate]` marker. Each is an incomplete handoff — surfaced with its filing context so an operator can promote it or gate it deliberately. The sweep reports rather than guesses: it never silently promotes an item into the queue, because a filing whose readiness nobody declared is exactly the input the gate model says a human should see.
|
|
@@ -57,7 +57,8 @@ body: |
|
|
|
57
57
|
...
|
|
58
58
|
|
|
59
59
|
# Behavioral flags — caller asserts these so the validator can pick the right gates
|
|
60
|
-
runtime_behavior_change
|
|
60
|
+
# (on a LIVE item `runtime_behavior_change` is DERIVED from the stored declaration, not taken on assertion — S8)
|
|
61
|
+
runtime_behavior_change: true # → requires Target Backend Environment + Validation Journey; LIVE items derive it from the stored declaration instead (S8)
|
|
61
62
|
authenticated_surface: true # → requires Sign-in Required
|
|
62
63
|
artifacts_attached: true # → requires Source Precedence section
|
|
63
64
|
links: [{ ref: "my-org/my-repo#99", type: "is blocked by" }] # known issue links (may be empty)
|
|
@@ -68,7 +69,7 @@ child_refs: ["my-org/my-repo#601", "my-org/my-repo#602"] # known child work (s
|
|
|
68
69
|
prd_source: "https://notion.so/..." # set when the issue was generated from a PRD — requires the Source Requirement section, see S16
|
|
69
70
|
```
|
|
70
71
|
|
|
71
|
-
If the caller passes only an issue ref, fetch via `gh issue view <number> --repo <org>/<repo> --json number,title,body,labels,state,milestone,assignees`, parse the body sections, derive the spec fields
|
|
72
|
+
If the caller passes only an issue ref, fetch via `gh issue view <number> --repo <org>/<repo> --json number,title,body,labels,state,milestone,assignees`, parse the body sections, derive the spec fields — including `runtime_behavior_change`, derived from the `## Target Backend Environment` declaration per `derived-branch-plan` and authoritative over any caller assertion — then run gates. The parser lives in `lisa-github-read-issue` (composition).
|
|
72
73
|
|
|
73
74
|
## Gates
|
|
74
75
|
|
|
@@ -167,7 +168,11 @@ When `issue_type ∉ {Bug, Epic}`, `parent_ref` must be set — **except for a b
|
|
|
167
168
|
|
|
168
169
|
#### S8 — Target Backend Environment
|
|
169
170
|
|
|
170
|
-
|
|
171
|
+
Every leaf work unit carries `## Target Backend Environment` in the body. The section is where `runtime_behavior_change` is persisted, so it is rendered for exempt work too — see the declaration grammar at the end of this gate. Read accepted environments from the exact configured keys of `.lisa.config.json` `deploy.branches`, never from a hardcoded list. Accept a human-confirmed bare exact configured key or `Confirmed: <env>`, automated `Inferred: <env> — evidence: <title|body|reproduction|hostname>`, automated `Assumption: <env> — remote default branch <branch>` for a unique reverse-map, or `Assumption: remote default branch <branch>` when no unique reverse-map exists. Human confirmation replaces an automated annotation with the bare key or `Confirmed: <env>`. For legacy bare values, use managed draft markers and current ticket content only; provider edit history is not required. A marker proves automation and requires re-annotation; otherwise unknown provenance plus conflicting evidence fails for confirmation. Validate the annotation shape/source and remote-default branch; validate `<env>` as an exact configured key whenever present. A valid branch-only assumption must not fail solely because its reverse-map is absent or ambiguous. Normalize built-in `prod` ↔ `production` only when exactly one of those keys is configured. No other aliases are valid.
|
|
172
|
+
|
|
173
|
+
**The section persists `runtime_behavior_change`, so it is never simply skipped.** Work that changes no runtime behavior declares the exemption in place of an environment: `None — no runtime behavior change: doc-only` (or `config-only` / `type-only`). A container (an Epic, or any issue holding child work) declares `None — container: state rolls up from children`. The `None —` prefix is the machine discriminator; the words after it are for the human reading the issue.
|
|
174
|
+
|
|
175
|
+
Derive the flag from the stored content, never from a caller's word about a live issue: an exact configured environment key means `true`, a `None —` declaration means `false`, and an **absent section means underivable** — never `false`. On a live issue the stored declaration is authoritative, so a caller asserting a `runtime_behavior_change` that contradicts it **FAILs** this gate naming both values rather than silently preferring either; one of the two is wrong, and picking one quietly is how an unauditable gate gets built. A **proposed spec** with no section **FAILs** — `lisa-github-write-issue` must render it before the write. A **live legacy issue** with no section is `N/A` with a repair note routed to claim time: the same asymmetry S19 uses, for the same reason, because failing every existing issue would turn a legacy queue red for a section no human had a way to add.
|
|
171
176
|
|
|
172
177
|
#### S9 — Sign-in Required
|
|
173
178
|
|
|
@@ -187,7 +192,7 @@ This gate is `product_relevant: false` because cross-repo work units are not a p
|
|
|
187
192
|
|
|
188
193
|
#### S11 — Validation Journey present
|
|
189
194
|
|
|
190
|
-
When `runtime_behavior_change = true`, body must contain `## Validation Journey`. Skipped for doc-only / config-only / type-only / Epic.
|
|
195
|
+
When `runtime_behavior_change = true`, body must contain `## Validation Journey`. Skipped for doc-only / config-only / type-only / Epic. When `runtime_behavior_change` is **underivable** — a live legacy issue whose body carries no Target Backend Environment declaration (S8) — this gate is `N/A` with the same repair note rather than `PASS`, because a requirement conditioned on a flag nobody recorded cannot be asserted either way.
|
|
191
196
|
|
|
192
197
|
The caller controls strictness via `journey_followup`:
|
|
193
198
|
- `auto` (default): missing section is FAIL with remediation `"Invoke lisa-github-add-journey to append the section after create"`. Callers like `lisa-github-write-issue` know to chain the followup automatically.
|
|
@@ -226,7 +231,7 @@ FAIL when the Validation Journey is present but declares zero binding `[EVIDENCE
|
|
|
226
231
|
|
|
227
232
|
Parse claiming markers by the exact `[EVIDENCE:` prefix. A cross-work-item pointer in the canonical form `[EVIDENCE-REF: <work-item-ref> | <artifact-type>: <kebab-case-name>]` is non-claiming. The Lisa 2.223.0 form `[EVIDENCE-REF: <tracker-ref>: <artifact-type>: <kebab-case-name>]` is also accepted as a legacy non-claiming alias; parse it from the right so the final two fields are type/name and a tracker URL may contain `:`. Exclude both forms from the manifest, S14's minimum-marker count, local marker type/name validation, and duplicate-name checks. Independently validate every `EVIDENCE-REF`: the native work-item reference must be non-empty and unambiguous, the artifact type must use the fixed taxonomy, and the name must be non-empty kebab-case. A malformed reference FAILs S14 as an invalid pointer but never becomes a local evidence obligation. A valid canonical or legacy reference may point to a sibling's artifact, but it never satisfies S14 for this issue. Therefore a runtime-changing leaf whose journey contains only `EVIDENCE-REF` entries FAILs S14 for zero local claiming markers, not because a valid legacy reference is malformed. Quoting or code-formatting another issue's `[EVIDENCE: ...]` marker does not make it a reference; writers must convert it to the canonical pipe form.
|
|
228
233
|
|
|
229
|
-
This gate depends on S11. It is `N/A` for containers — an **Epic**, or any item with open child work (coordination containers, not work units) — and for leaf units with `runtime_behavior_change = false` (doc-only / config-only / type-only). If S11 fails because the Validation Journey is absent, S14 also FAILs (there is no manifest to bind) with remediation pointing back to `lisa-github-add-journey`.
|
|
234
|
+
This gate depends on S11. It is `N/A` for containers — an **Epic**, or any item with open child work (coordination containers, not work units) — and for leaf units with `runtime_behavior_change = false` (doc-only / config-only / type-only). If S11 fails because the Validation Journey is absent, S14 also FAILs (there is no manifest to bind) with remediation pointing back to `lisa-github-add-journey`. It is likewise `N/A` with a repair note when `runtime_behavior_change` is **underivable** under S8 — reading an absent declaration as `false` is exactly the silent assumption that made this gate unauditable on a live issue.
|
|
230
235
|
|
|
231
236
|
#### S15 — Leaf-only build-ready
|
|
232
237
|
|
|
@@ -308,6 +313,7 @@ Enforces the `derived-branch-plan` rule: the `## Branch Plan` section is **deriv
|
|
|
308
313
|
|---|---|
|
|
309
314
|
| `runtime_behavior_change = false` (doc-only / config-only / type-only) or an Epic/container, with no plan | `N/A` — absence is correct; never demand one |
|
|
310
315
|
| Exempt work **carrying** a `## Branch Plan` | **FAIL** — hand-authored branches on work that declared no runtime target |
|
|
316
|
+
| `runtime_behavior_change` **underivable** (live legacy item, no Target Backend Environment declaration) | `N/A` with a repair note — absence is never read as `false` |
|
|
311
317
|
| Plan matches the recomputed plan | PASS |
|
|
312
318
|
| Plan conflicts with the recomputed plan | **FAIL** |
|
|
313
319
|
| Plan missing the `Derived from:` provenance line and disagreeing | **FAIL** — treated as hand-authored |
|
|
@@ -448,7 +454,7 @@ system, and never invent or ask for credentials inline.
|
|
|
448
454
|
|
|
449
455
|
## Execution
|
|
450
456
|
|
|
451
|
-
1. Parse `$ARGUMENTS`. Resolve `READY_ROLE` from merged `github.labels.build.ready` config (default `status:ready`). If the input is an issue ref, fetch via `gh issue view --json` and derive the spec fields — including `build_ready` (label set contains the resolved `READY_ROLE`, not a hard-coded label) and `child_refs` (native sub-issues plus body task-list / `Blocked by #<n>` parentage, resolved as in `lisa-github-read-issue`) so S15 can classify the issue. Otherwise parse the YAML spec and preserve the proposed `build_ready` value for S15/F4 normalization.
|
|
457
|
+
1. Parse `$ARGUMENTS`. Resolve `READY_ROLE` from merged `github.labels.build.ready` config (default `status:ready`). If the input is an issue ref, fetch via `gh issue view --json` and derive the spec fields — including `runtime_behavior_change` (from the `## Target Backend Environment` declaration: an exact configured environment key → `true`, a `None —` declaration → `false`, an absent section → **underivable**, never `false`), `build_ready` (label set contains the resolved `READY_ROLE`, not a hard-coded label) and `child_refs` (native sub-issues plus body task-list / `Blocked by #<n>` parentage, resolved as in `lisa-github-read-issue`) so S15 can classify the issue. Otherwise parse the YAML spec and preserve the proposed `build_ready` value for S15/F4 normalization.
|
|
452
458
|
2. Confirm `gh auth status` succeeds before any feasibility gate runs.
|
|
453
459
|
3. Run every Specification gate in order. Collect PASS / FAIL / N/A with a one-line reason.
|
|
454
460
|
4. Unless the caller passed `--spec-only`, run every Feasibility gate.
|
|
@@ -91,14 +91,24 @@ Scenario: <name>
|
|
|
91
91
|
[Explicit list of what this issue does NOT cover. Forces scope discipline.]
|
|
92
92
|
|
|
93
93
|
## Target Backend Environment
|
|
94
|
-
[
|
|
94
|
+
[ALWAYS required on a leaf — the SECTION is unconditional, only its
|
|
95
|
+
VALUE is conditional. It is where `runtime_behavior_change` is
|
|
96
|
+
persisted, so omitting it records nothing rather than recording "no".
|
|
97
|
+
When the issue changes runtime behavior, use an exact
|
|
95
98
|
`deploy.branches` key. A human-confirmed value is a bare key or
|
|
96
99
|
`Confirmed: <env>`. An automated evidence write is
|
|
97
100
|
`Inferred: <env> — evidence: <title|body|reproduction|hostname>`; an automated
|
|
98
101
|
generic default is `Assumption: <env> — remote default branch <branch>`.
|
|
99
102
|
Without a unique reverse-map use `Assumption: remote default branch <branch>`.
|
|
100
103
|
Human confirmation replaces the automated annotation with a bare key or
|
|
101
|
-
`Confirmed: <env>`.
|
|
104
|
+
`Confirmed: <env>`. ALWAYS render this section — it is where
|
|
105
|
+
`runtime_behavior_change` is persisted, and an absent section reads as
|
|
106
|
+
*underivable*, not exempt. Work that changes no runtime behavior declares the
|
|
107
|
+
exemption in place of an environment: `None — no runtime behavior change:
|
|
108
|
+
doc-only` (or `config-only` / `type-only`). An Epic/container declares
|
|
109
|
+
`None — container: state rolls up from children`. Visible prose, not an HTML
|
|
110
|
+
comment, for the same reason the Branch Plan provenance line is: JIRA's ADF has
|
|
111
|
+
no comment node. See the `derived-branch-plan` rule.]
|
|
102
112
|
|
|
103
113
|
## Branch Plan
|
|
104
114
|
[GENERATED, never hand-authored. Render only when the issue has a Target
|
|
@@ -112,7 +112,7 @@ Using the general-purpose agent in Team Lead session, **determine the base branc
|
|
|
112
112
|
- **Legacy (no plan)** → derive it, **write the assumption onto the ticket as a comment**, then proceed. Visible prose plus a dedupe marker, e.g. ``Branch plan derived for this item: branch from `main`, PR into `main` (Target Backend Environment: production via .lisa.config.json deploy.branches).`` followed by `<!-- [lisa-branch-plan] key=<work-item-ref>::<branch> -->` (marker-dedupe on `<work-item-ref>::<branch>`, so a re-claim adds no duplicate; where a vendor cannot host an HTML comment the visible line alone carries it). **No silent guess** — if the comment cannot be written, that is a stop, because proceeding would make the inference invisible.
|
|
113
113
|
- **Conflict with a human-confirmed environment, or with an existing open PR's base** → **stop under the existing confirmation rules** below. A branch plan never overrides the environment and never supplies the confirmation itself.
|
|
114
114
|
- **Stale (config changed since the plan was rendered)** → current config wins: re-render the section onto the ticket, record the change, and never follow the stale plan. Staleness is output falling behind its input, not a conflict to escalate.
|
|
115
|
-
- Exempt work carries no plan and needs none: `runtime_behavior_change = false` (doc-only / config-only / type-only) and containers have no
|
|
115
|
+
- Exempt work carries no plan and needs none: `runtime_behavior_change = false` (doc-only / config-only / type-only) and containers have no environment to derive from, so **absence of a plan is correct** — never demand or invent one. Read the flag off the item rather than assuming it: `## Target Backend Environment` is rendered on every leaf and carries the declaration (`derived-branch-plan`) — an exact configured environment key means `true`, `None — no runtime behavior change: <doc-only|config-only|type-only>` or `None — container: state rolls up from children` means `false`. An **absent** section is *underivable*, not exempt: derive the flag from the change under implementation, write the declaration onto the item beside the `[lisa-branch-plan]` legacy comment, and proceed. Never read absence as `false` — that is the assumption that made S8/S11/S14/S19 undecidable on a live item.
|
|
116
116
|
3. **Establish the feature branch off the latest base, conflict-free:**
|
|
117
117
|
- `git fetch origin`.
|
|
118
118
|
- Already on a feature branch with an **open PR** → reuse it. If the PR's base ≠ the resolved base branch, surface the mismatch and re-target only with confirmation — the ticket's environment is the source of truth.
|
|
@@ -55,7 +55,8 @@ description: | # Full description text — every required section
|
|
|
55
55
|
...
|
|
56
56
|
|
|
57
57
|
# Behavioral flags — caller asserts these so the validator can pick the right gates
|
|
58
|
-
runtime_behavior_change
|
|
58
|
+
# (on a LIVE item `runtime_behavior_change` is DERIVED from the stored declaration, not taken on assertion — S8)
|
|
59
|
+
runtime_behavior_change: true # → requires Target Backend Environment + Validation Journey; LIVE items derive it from the stored declaration instead (S8)
|
|
59
60
|
authenticated_surface: true # → requires Sign-in Required
|
|
60
61
|
artifacts_attached: true # → requires Source Precedence section
|
|
61
62
|
links: [{ key: "PROJ-99", type: "is blocked by" }] # known issue links (may be empty)
|
|
@@ -65,7 +66,7 @@ child_refs: ["PROJ-601", "PROJ-602"] # known child work (sub-tasks / "is block
|
|
|
65
66
|
prd_source: "https://notion.so/..." # set when the ticket was generated from a PRD — requires the Source Requirement section, see S16
|
|
66
67
|
```
|
|
67
68
|
|
|
68
|
-
If the caller passes only a ticket key, fetch the ticket via `lisa-atlassian-access` `operation: read-ticket key: <KEY>`, derive the same fields from the fetched data — including `build_ready` (label set contains `status:ready`) and `child_refs` (sub-tasks plus `is blocked by` parentage, resolved as in `lisa-jira-read-ticket`) so S15 can classify the ticket — then run gates.
|
|
69
|
+
If the caller passes only a ticket key, fetch the ticket via `lisa-atlassian-access` `operation: read-ticket key: <KEY>`, derive the same fields from the fetched data — including `runtime_behavior_change` (derived from the `Target Backend Environment` declaration per `derived-branch-plan`, and authoritative over any caller assertion), `build_ready` (label set contains `status:ready`) and `child_refs` (sub-tasks plus `is blocked by` parentage, resolved as in `lisa-jira-read-ticket`) so S15 can classify the ticket — then run gates.
|
|
69
70
|
|
|
70
71
|
## Gates
|
|
71
72
|
|
|
@@ -170,7 +171,11 @@ When `issue_type ∉ {Bug, Epic}`, `parent_key` must be set — **except for a b
|
|
|
170
171
|
|
|
171
172
|
#### S8 — Target Backend Environment
|
|
172
173
|
|
|
173
|
-
|
|
174
|
+
Every leaf work unit carries a `Target Backend Environment` section in the description (`h2.` / `##` in proposed text, or an ADF heading in live JIRA). The section is where `runtime_behavior_change` is persisted, so it is rendered for exempt work too — see the declaration grammar at the end of this gate. Read accepted environments from the exact configured keys of `.lisa.config.json` `deploy.branches`, never from a hardcoded list. Accept a human-confirmed bare exact configured key or `Confirmed: <env>`, automated `Inferred: <env> — evidence: <title|body|reproduction|hostname>`, automated `Assumption: <env> — remote default branch <branch>` for a unique reverse-map, or `Assumption: remote default branch <branch>` when no unique reverse-map exists. Human confirmation replaces an automated annotation with the bare key or `Confirmed: <env>`. For legacy bare values, use managed draft markers and current ticket content only; provider edit history is not required. A marker proves automation and requires re-annotation; otherwise unknown provenance plus conflicting evidence fails for confirmation. Validate the annotation shape/source and remote-default branch; validate `<env>` as an exact configured key whenever present. A valid branch-only assumption must not fail solely because its reverse-map is absent or ambiguous. Normalize built-in `prod` ↔ `production` only when exactly one of those keys is configured. No other aliases are valid.
|
|
175
|
+
|
|
176
|
+
**The section persists `runtime_behavior_change`, so it is never simply skipped.** Work that changes no runtime behavior declares the exemption in place of an environment: `None — no runtime behavior change: doc-only` (or `config-only` / `type-only`). A container (an Epic, or any ticket holding child work) declares `None — container: state rolls up from children`. The `None —` prefix is the machine discriminator; the words after it are for the human reading the ticket.
|
|
177
|
+
|
|
178
|
+
Derive the flag from the stored content, never from a caller's word about a live ticket: an exact configured environment key means `true`, a `None —` declaration means `false`, and an **absent section means underivable** — never `false`. On a live ticket the stored declaration is authoritative, so a caller asserting a `runtime_behavior_change` that contradicts it **FAILs** this gate naming both values rather than silently preferring either; one of the two is wrong, and picking one quietly is how an unauditable gate gets built. A **proposed spec** with no section **FAILs** — `lisa-jira-write-ticket` must render it before the write. A **live legacy ticket** with no section is `N/A` with a repair note routed to claim time: the same asymmetry S19 uses, for the same reason, because failing every existing ticket would turn a legacy queue red for a section no human had a way to add.
|
|
174
179
|
|
|
175
180
|
#### S9 — Sign-in Required
|
|
176
181
|
|
|
@@ -188,7 +193,7 @@ This gate is `product_relevant: false` because cross-repo work units are not a p
|
|
|
188
193
|
|
|
189
194
|
#### S11 — Validation Journey present
|
|
190
195
|
|
|
191
|
-
When `runtime_behavior_change = true`, description must contain a `Validation Journey` section (`h2.` / `##` in proposed text, or an ADF heading in live JIRA). Skipped for doc-only / config-only / type-only / Epic.
|
|
196
|
+
When `runtime_behavior_change = true`, description must contain a `Validation Journey` section (`h2.` / `##` in proposed text, or an ADF heading in live JIRA). Skipped for doc-only / config-only / type-only / Epic. When `runtime_behavior_change` is **underivable** — a live legacy ticket whose description carries no Target Backend Environment declaration (S8) — this gate is `N/A` with the same repair note rather than `PASS`, because a requirement conditioned on a flag nobody recorded cannot be asserted either way.
|
|
192
197
|
|
|
193
198
|
The caller controls the strictness by passing `journey_followup: "auto"` or `journey_followup: "none"` in the spec:
|
|
194
199
|
- `auto` (default): if the section is absent, return `FAIL` with remediation `"Invoke lisa-jira-add-journey to append the section after create"`. Callers like `lisa-jira-write-ticket` know to chain `lisa-jira-add-journey` automatically, so this counts as a fixable failure they can resolve in-line — they re-run validation after appending.
|
|
@@ -229,7 +234,7 @@ FAIL when the Validation Journey is present but declares zero binding `[EVIDENCE
|
|
|
229
234
|
|
|
230
235
|
Parse claiming markers by the exact `[EVIDENCE:` prefix. A cross-work-item pointer in the canonical form `[EVIDENCE-REF: <work-item-ref> | <artifact-type>: <kebab-case-name>]` is non-claiming. The Lisa 2.223.0 form `[EVIDENCE-REF: <tracker-ref>: <artifact-type>: <kebab-case-name>]` is also accepted as a legacy non-claiming alias; parse it from the right so the final two fields are type/name and a tracker URL may contain `:`. Exclude both forms from the manifest, S14's minimum-marker count, local marker type/name validation, and duplicate-name checks. Independently validate every `EVIDENCE-REF`: the native work-item reference must be non-empty and unambiguous, the artifact type must use the fixed taxonomy, and the name must be non-empty kebab-case. A malformed reference FAILs S14 as an invalid pointer but never becomes a local evidence obligation. A valid canonical or legacy reference may point to a sibling's artifact, but it never satisfies S14 for this ticket. Therefore a runtime-changing leaf whose journey contains only `EVIDENCE-REF` entries FAILs S14 for zero local claiming markers, not because a valid legacy reference is malformed. Quoting or code-formatting another ticket's `[EVIDENCE: ...]` marker does not make it a reference; writers must convert it to the canonical pipe form.
|
|
231
236
|
|
|
232
|
-
This gate depends on S11. It is `N/A` for containers — an **Epic**, or any item with open child work (coordination containers, not work units) — and for leaf units with `runtime_behavior_change = false` (doc-only / config-only / type-only). If S11 fails because the Validation Journey is absent, S14 also FAILs (there is no manifest to bind) with remediation pointing back to `lisa-jira-add-journey`.
|
|
237
|
+
This gate depends on S11. It is `N/A` for containers — an **Epic**, or any item with open child work (coordination containers, not work units) — and for leaf units with `runtime_behavior_change = false` (doc-only / config-only / type-only). If S11 fails because the Validation Journey is absent, S14 also FAILs (there is no manifest to bind) with remediation pointing back to `lisa-jira-add-journey`. It is likewise `N/A` with a repair note when `runtime_behavior_change` is **underivable** under S8 — reading an absent declaration as `false` is exactly the silent assumption that made this gate unauditable on a live ticket.
|
|
233
238
|
|
|
234
239
|
#### S15 — Leaf-only build-ready
|
|
235
240
|
|
|
@@ -310,6 +315,7 @@ Enforces the `derived-branch-plan` rule: the `Branch Plan` section (`h2.` / `##`
|
|
|
310
315
|
|---|---|
|
|
311
316
|
| `runtime_behavior_change = false` (doc-only / config-only / type-only) or an Epic/container, with no plan | `N/A` — absence is correct; never demand one |
|
|
312
317
|
| Exempt work **carrying** a `Branch Plan` | **FAIL** — hand-authored branches on work that declared no runtime target |
|
|
318
|
+
| `runtime_behavior_change` **underivable** (live legacy item, no Target Backend Environment declaration) | `N/A` with a repair note — absence is never read as `false` |
|
|
313
319
|
| Plan matches the recomputed plan | PASS |
|
|
314
320
|
| Plan conflicts with the recomputed plan | **FAIL** |
|
|
315
321
|
| Plan missing the `Derived from:` provenance line and disagreeing | **FAIL** — treated as hand-authored |
|
|
@@ -377,7 +383,7 @@ system, and never invent or ask for credentials inline.
|
|
|
377
383
|
|
|
378
384
|
## Execution
|
|
379
385
|
|
|
380
|
-
1. Parse `$ARGUMENTS`. If it's a ticket key, fetch the ticket via `lisa-atlassian-access` `operation: read-ticket` and derive the spec from the fetched fields — including `build_ready` (label set contains `status:ready`) and `child_refs` (sub-tasks plus `is blocked by` parentage, resolved as in `lisa-jira-read-ticket`) so S15 can classify the ticket. When the fetched description is ADF, walk the document tree and extract section headings from ADF `heading` nodes, then collect the text between heading nodes for section-specific gates. If the fetched description is a single paragraph containing literal Markdown/wiki heading markers, treat that as a formatting failure rather than accepting substring matches. Otherwise parse the YAML spec.
|
|
386
|
+
1. Parse `$ARGUMENTS`. If it's a ticket key, fetch the ticket via `lisa-atlassian-access` `operation: read-ticket` and derive the spec from the fetched fields — including `runtime_behavior_change` (from the `Target Backend Environment` declaration: an exact configured environment key → `true`, a `None —` declaration → `false`, an absent section → **underivable**, never `false`), `build_ready` (label set contains `status:ready`) and `child_refs` (sub-tasks plus `is blocked by` parentage, resolved as in `lisa-jira-read-ticket`) so S15 can classify the ticket. When the fetched description is ADF, walk the document tree and extract section headings from ADF `heading` nodes, then collect the text between heading nodes for section-specific gates. If the fetched description is a single paragraph containing literal Markdown/wiki heading markers, treat that as a formatting failure rather than accepting substring matches. Otherwise parse the YAML spec.
|
|
381
387
|
2. If any feasibility gate will run, invoke `lisa-atlassian-access` `operation: list-sites` once to confirm the configured site is reachable (it enforces connection match against `.lisa.config.json`).
|
|
382
388
|
3. Run every Specification gate in order. Collect PASS / FAIL / N/A with a one-line reason.
|
|
383
389
|
4. Unless the caller passed `--spec-only` (dry-run), run every Feasibility gate. Collect results.
|
|
@@ -83,14 +83,24 @@ h2. Out of Scope
|
|
|
83
83
|
[Explicit list of what this ticket does NOT cover. Forces scope discipline.]
|
|
84
84
|
|
|
85
85
|
h2. Target Backend Environment
|
|
86
|
-
[
|
|
86
|
+
[ALWAYS required on a leaf — the SECTION is unconditional, only its
|
|
87
|
+
VALUE is conditional. It is where `runtime_behavior_change` is
|
|
88
|
+
persisted, so omitting it records nothing rather than recording "no".
|
|
89
|
+
When the ticket changes runtime behavior, use an exact
|
|
87
90
|
`deploy.branches` key. A human-confirmed value is a bare key or
|
|
88
91
|
`Confirmed: <env>`. An automated evidence write is
|
|
89
92
|
`Inferred: <env> — evidence: <title|body|reproduction|hostname>`; an automated
|
|
90
93
|
generic default is `Assumption: <env> — remote default branch <branch>`.
|
|
91
94
|
Without a unique reverse-map use `Assumption: remote default branch <branch>`.
|
|
92
95
|
Human confirmation replaces the automated annotation with a bare key or
|
|
93
|
-
`Confirmed: <env>`.
|
|
96
|
+
`Confirmed: <env>`. ALWAYS render this section — it is where
|
|
97
|
+
`runtime_behavior_change` is persisted, and an absent section reads as
|
|
98
|
+
*underivable*, not exempt. Work that changes no runtime behavior declares the
|
|
99
|
+
exemption in place of an environment: `None — no runtime behavior change:
|
|
100
|
+
doc-only` (or `config-only` / `type-only`). An Epic/container declares
|
|
101
|
+
`None — container: state rolls up from children`. Visible prose, not an HTML
|
|
102
|
+
comment, for the same reason the Branch Plan provenance line is: JIRA's ADF has
|
|
103
|
+
no comment node. See the `derived-branch-plan` rule.]
|
|
94
104
|
|
|
95
105
|
h2. Branch Plan
|
|
96
106
|
[GENERATED, never hand-authored. Render only when the ticket has a Target
|
|
@@ -56,7 +56,8 @@ description: | # Full description text — every required section
|
|
|
56
56
|
...
|
|
57
57
|
|
|
58
58
|
# Behavioral flags — caller asserts these so the validator picks the right gates
|
|
59
|
-
runtime_behavior_change
|
|
59
|
+
# (on a LIVE item `runtime_behavior_change` is DERIVED from the stored declaration, not taken on assertion — S8)
|
|
60
|
+
runtime_behavior_change: true # → requires Target Backend Environment + Validation Journey; LIVE items derive it from the stored declaration instead (S8)
|
|
60
61
|
authenticated_surface: true # → requires Sign-in Required
|
|
61
62
|
artifacts_attached: true # → requires Source Precedence section
|
|
62
63
|
relations: [{ id: "ENG-99", type: "blocked_by" }] # known issue relations (may be empty)
|
|
@@ -66,7 +67,7 @@ child_refs: ["ENG-601", "ENG-602"] # known child work (sub-issues / project-me
|
|
|
66
67
|
prd_source: "https://notion.so/..." # set when the Issue was generated from a PRD — requires the Source Requirement section, see S16
|
|
67
68
|
```
|
|
68
69
|
|
|
69
|
-
If the caller passes only an identifier, fetch the item via `lisa-linear-access operation: get-issue` (Issue) or `lisa-linear-access operation: get-project` (Project), derive the same fields from the fetched data — including `build_ready` (the Issue's state is the configured `ready` state) and `child_refs` (sub-issues, project-member issues, plus `blocked_by` parentage, resolved as in `lisa-linear-read-issue`) so S15 can classify the item — then run gates.
|
|
70
|
+
If the caller passes only an identifier, fetch the item via `lisa-linear-access operation: get-issue` (Issue) or `lisa-linear-access operation: get-project` (Project), derive the same fields from the fetched data — including `runtime_behavior_change` (derived from the `## Target Backend Environment` declaration per `derived-branch-plan`, and authoritative over any caller assertion), `build_ready` (the Issue's state is the configured `ready` state) and `child_refs` (sub-issues, project-member issues, plus `blocked_by` parentage, resolved as in `lisa-linear-read-issue`) so S15 can classify the item — then run gates.
|
|
70
71
|
|
|
71
72
|
## Gates
|
|
72
73
|
|
|
@@ -175,7 +176,11 @@ A **build-ready leaf work unit** that is not part of an Epic context stands alon
|
|
|
175
176
|
|
|
176
177
|
#### S8 — Target Backend Environment
|
|
177
178
|
|
|
178
|
-
|
|
179
|
+
Every leaf work unit carries `## Target Backend Environment` in the description. The section is where `runtime_behavior_change` is persisted, so it is rendered for exempt work too — see the declaration grammar at the end of this gate. Read accepted environments from the exact configured keys of `.lisa.config.json` `deploy.branches`, never from a hardcoded list. Accept a human-confirmed bare exact configured key or `Confirmed: <env>`, automated `Inferred: <env> — evidence: <title|body|reproduction|hostname>`, automated `Assumption: <env> — remote default branch <branch>` for a unique reverse-map, or `Assumption: remote default branch <branch>` when no unique reverse-map exists. Human confirmation replaces an automated annotation with the bare key or `Confirmed: <env>`. For legacy bare values, use managed draft markers and current ticket content only; provider edit history is not required. A marker proves automation and requires re-annotation; otherwise unknown provenance plus conflicting evidence fails for confirmation. Validate the annotation shape/source and remote-default branch; validate `<env>` as an exact configured key whenever present. A valid branch-only assumption must not fail solely because its reverse-map is absent or ambiguous. Normalize built-in `prod` ↔ `production` only when exactly one of those keys is configured. No other aliases are valid.
|
|
180
|
+
|
|
181
|
+
**The section persists `runtime_behavior_change`, so it is never simply skipped.** Work that changes no runtime behavior declares the exemption in place of an environment: `None — no runtime behavior change: doc-only` (or `config-only` / `type-only`). A container (a Project, or any Issue holding child work) declares `None — container: state rolls up from children`. The `None —` prefix is the machine discriminator; the words after it are for the human reading the Issue.
|
|
182
|
+
|
|
183
|
+
Derive the flag from the stored content, never from a caller's word about a live Issue: an exact configured environment key means `true`, a `None —` declaration means `false`, and an **absent section means underivable** — never `false`. On a live Issue the stored declaration is authoritative, so a caller asserting a `runtime_behavior_change` that contradicts it **FAILs** this gate naming both values rather than silently preferring either; one of the two is wrong, and picking one quietly is how an unauditable gate gets built. A **proposed spec** with no section **FAILs** — `lisa-linear-write-issue` must render it before the write. A **live legacy Issue** with no section is `N/A` with a repair note routed to claim time: the same asymmetry S19 uses, for the same reason, because failing every existing Issue would turn a legacy queue red for a section no human had a way to add.
|
|
179
184
|
|
|
180
185
|
#### S9 — Sign-in Required
|
|
181
186
|
|
|
@@ -193,7 +198,7 @@ This gate is `product_relevant: false` because cross-repo work units are not a p
|
|
|
193
198
|
|
|
194
199
|
#### S11 — Validation Journey present
|
|
195
200
|
|
|
196
|
-
When `runtime_behavior_change = true`, description must contain `## Validation Journey`. Skipped for doc-only / config-only / type-only / Epic.
|
|
201
|
+
When `runtime_behavior_change = true`, description must contain `## Validation Journey`. Skipped for doc-only / config-only / type-only / Epic. When `runtime_behavior_change` is **underivable** — a live legacy Issue whose description carries no Target Backend Environment declaration (S8) — this gate is `N/A` with the same repair note rather than `PASS`, because a requirement conditioned on a flag nobody recorded cannot be asserted either way.
|
|
197
202
|
|
|
198
203
|
The caller controls strictness via `journey_followup: "auto"` or `"none"`:
|
|
199
204
|
- `auto` (default): missing section returns `FAIL` with remediation `"Invoke lisa-linear-add-journey to append the section after create"`. The write path auto-fixes; dry-run path leaves it as a FAIL the caller must address.
|
|
@@ -232,7 +237,7 @@ FAIL when the Validation Journey is present but declares zero binding `[EVIDENCE
|
|
|
232
237
|
|
|
233
238
|
Parse claiming markers by the exact `[EVIDENCE:` prefix. A cross-work-item pointer in the canonical form `[EVIDENCE-REF: <work-item-ref> | <artifact-type>: <kebab-case-name>]` is non-claiming. The Lisa 2.223.0 form `[EVIDENCE-REF: <tracker-ref>: <artifact-type>: <kebab-case-name>]` is also accepted as a legacy non-claiming alias; parse it from the right so the final two fields are type/name and a tracker URL may contain `:`. Exclude both forms from the manifest, S14's minimum-marker count, local marker type/name validation, and duplicate-name checks. Independently validate every `EVIDENCE-REF`: the native work-item reference must be non-empty and unambiguous, the artifact type must use the fixed taxonomy, and the name must be non-empty kebab-case. A malformed reference FAILs S14 as an invalid pointer but never becomes a local evidence obligation. A valid canonical or legacy reference may point to a sibling's artifact, but it never satisfies S14 for this item. Therefore a runtime-changing leaf whose journey contains only `EVIDENCE-REF` entries FAILs S14 for zero local claiming markers, not because a valid legacy reference is malformed. Quoting or code-formatting another item's `[EVIDENCE: ...]` marker does not make it a reference; writers must convert it to the canonical pipe form.
|
|
234
239
|
|
|
235
|
-
This gate depends on S11. It is `N/A` for containers — a **Project** (the Epic equivalent), or any item with open child work (coordination containers, not work units) — and for leaf units with `runtime_behavior_change = false` (doc-only / config-only / type-only). If S11 fails because the Validation Journey is absent, S14 also FAILs (there is no manifest to bind) with remediation pointing back to `lisa-linear-add-journey`.
|
|
240
|
+
This gate depends on S11. It is `N/A` for containers — a **Project** (the Epic equivalent), or any item with open child work (coordination containers, not work units) — and for leaf units with `runtime_behavior_change = false` (doc-only / config-only / type-only). If S11 fails because the Validation Journey is absent, S14 also FAILs (there is no manifest to bind) with remediation pointing back to `lisa-linear-add-journey`. It is likewise `N/A` with a repair note when `runtime_behavior_change` is **underivable** under S8 — reading an absent declaration as `false` is exactly the silent assumption that made this gate unauditable on a live Issue.
|
|
236
241
|
|
|
237
242
|
#### S15 — Leaf-only build-ready
|
|
238
243
|
|
|
@@ -312,6 +317,7 @@ Enforces the `derived-branch-plan` rule: the `## Branch Plan` section is **deriv
|
|
|
312
317
|
|---|---|
|
|
313
318
|
| `runtime_behavior_change = false` (doc-only / config-only / type-only) or a Project/container, with no plan | `N/A` — absence is correct; never demand one |
|
|
314
319
|
| Exempt work **carrying** a `## Branch Plan` | **FAIL** — hand-authored branches on work that declared no runtime target |
|
|
320
|
+
| `runtime_behavior_change` **underivable** (live legacy item, no Target Backend Environment declaration) | `N/A` with a repair note — absence is never read as `false` |
|
|
315
321
|
| Plan matches the recomputed plan | PASS |
|
|
316
322
|
| Plan conflicts with the recomputed plan | **FAIL** |
|
|
317
323
|
| Plan missing the `Derived from:` provenance line and disagreeing | **FAIL** — treated as hand-authored |
|
|
@@ -383,7 +389,7 @@ system, and never invent or ask for credentials inline.
|
|
|
383
389
|
|
|
384
390
|
## Execution
|
|
385
391
|
|
|
386
|
-
1. Parse `$ARGUMENTS`. If it's an identifier, fetch the item and derive the spec from the fetched fields — including `build_ready` (the Issue's state is the configured `ready` state) and `child_refs` (sub-issues, project-member issues, plus `blocked_by` parentage, resolved as in `lisa-linear-read-issue`) so S15 can classify the item. Otherwise parse the YAML spec.
|
|
392
|
+
1. Parse `$ARGUMENTS`. If it's an identifier, fetch the item and derive the spec from the fetched fields — including `runtime_behavior_change` (from the `## Target Backend Environment` declaration: an exact configured environment key → `true`, a `None —` declaration → `false`, an absent section → **underivable**, never `false`), `build_ready` (the Issue's state is the configured `ready` state) and `child_refs` (sub-issues, project-member issues, plus `blocked_by` parentage, resolved as in `lisa-linear-read-issue`) so S15 can classify the item. Otherwise parse the YAML spec.
|
|
387
393
|
2. Resolve team ID via `lisa-linear-access operation: list-teams({query: <teamKey>})` if any feasibility gate will run.
|
|
388
394
|
3. Run every Specification gate in order. Collect PASS / FAIL / N/A with a one-line reason.
|
|
389
395
|
4. Unless the caller passed `--spec-only` (dry-run), run every Feasibility gate. Collect results.
|
|
@@ -103,14 +103,27 @@ Linear descriptions are markdown (NOT Jira wiki markup — no `h2.` headings, us
|
|
|
103
103
|
[Explicit list of what this item does NOT cover. Forces scope discipline.]
|
|
104
104
|
|
|
105
105
|
## Target Backend Environment
|
|
106
|
-
[
|
|
106
|
+
[ALWAYS required on a leaf — the SECTION is unconditional, only its
|
|
107
|
+
VALUE is conditional. It is where `runtime_behavior_change` is
|
|
108
|
+
persisted, so omitting it records nothing rather than recording "no".
|
|
109
|
+
When the item changes runtime behavior, use an exact
|
|
107
110
|
`deploy.branches` key. A human-confirmed value is a bare key or
|
|
108
111
|
`Confirmed: <env>`. An automated evidence write is
|
|
109
112
|
`Inferred: <env> — evidence: <title|body|reproduction|hostname>`; an automated
|
|
110
113
|
generic default is `Assumption: <env> — remote default branch <branch>`.
|
|
111
114
|
Without a unique reverse-map use `Assumption: remote default branch <branch>`.
|
|
112
115
|
Human confirmation replaces the automated annotation with a bare key or
|
|
113
|
-
`Confirmed: <env>`.
|
|
116
|
+
`Confirmed: <env>`. ALWAYS render this section — it is where
|
|
117
|
+
`runtime_behavior_change` is persisted, and an absent section reads as
|
|
118
|
+
*underivable*, not exempt. Work that changes no runtime behavior declares the
|
|
119
|
+
exemption in place of an environment: `None — no runtime behavior change:
|
|
120
|
+
doc-only` (or `config-only` / `type-only`). A Project/container declares
|
|
121
|
+
`None — container: state rolls up from children`. Visible prose, not an HTML
|
|
122
|
+
comment, for the same reason the Branch Plan provenance line is: a marker that
|
|
123
|
+
survives in only one representation cannot be a vendor-neutral discriminator,
|
|
124
|
+
and every tracker this contract binds stores its body in some rich-text or
|
|
125
|
+
structured form that need not preserve HTML comments. A declaration a reader
|
|
126
|
+
can see is one every backend can store. See the `derived-branch-plan` rule.]
|
|
114
127
|
|
|
115
128
|
## Branch Plan
|
|
116
129
|
[GENERATED, never hand-authored. Render only when the item has a Target
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Antigravity (agy) PreToolUse adapter for Lisa's ready-role filing guard.
|
|
3
|
+
#
|
|
4
|
+
# agy sends `{toolCall:{name:"run_command",args:{CommandLine:"..."}}}` and
|
|
5
|
+
# expects an allow/deny JSON object on stdout. The canonical hook consumes the
|
|
6
|
+
# Claude Bash-hook envelope and communicates denial with exit status 2. This
|
|
7
|
+
# adapter only translates protocols; all classification stays in the canonical
|
|
8
|
+
# block-direct-issue-create.sh beside it (same delegation pattern as
|
|
9
|
+
# block-instruction-file-edits.agy.sh).
|
|
10
|
+
#
|
|
11
|
+
# No scope gap here, unlike the instruction-file adapter: the canonical guard is
|
|
12
|
+
# Bash-only by construction, and `run_command` is exactly the surface it needs.
|
|
13
|
+
# agy therefore receives the whole guard, not an arm of it.
|
|
14
|
+
#
|
|
15
|
+
# Fail-open on missing runtimes: a missed refusal costs one under-declared
|
|
16
|
+
# ticket, and `lisa-repair-intake` already sweeps for exactly that. Failing
|
|
17
|
+
# closed would block every command on a machine without jq.
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
|
|
20
|
+
allow() {
|
|
21
|
+
printf '%s\n' '{"decision":"allow"}'
|
|
22
|
+
exit 0
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
deny() {
|
|
26
|
+
local reason="$1"
|
|
27
|
+
if command -v jq >/dev/null 2>&1; then
|
|
28
|
+
jq -cn --arg reason "$reason" '{decision:"deny",reason:$reason}'
|
|
29
|
+
else
|
|
30
|
+
printf '%s\n' '{"decision":"allow"}'
|
|
31
|
+
fi
|
|
32
|
+
exit 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
input="$(cat 2>/dev/null || true)"
|
|
36
|
+
[ -z "$input" ] && allow
|
|
37
|
+
command -v jq >/dev/null 2>&1 || allow
|
|
38
|
+
|
|
39
|
+
tool_name="$(printf '%s' "$input" | jq -r '.toolCall.name // empty' 2>/dev/null || true)"
|
|
40
|
+
[ "$tool_name" != "run_command" ] && allow
|
|
41
|
+
command_str="$(printf '%s' "$input" | jq -r '.toolCall.args.CommandLine // empty' 2>/dev/null || true)"
|
|
42
|
+
[ -z "$command_str" ] && allow
|
|
43
|
+
|
|
44
|
+
hook_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
45
|
+
canonical_hook="$hook_dir/block-direct-issue-create.sh"
|
|
46
|
+
[ -r "$canonical_hook" ] || allow
|
|
47
|
+
|
|
48
|
+
canonical_input="$(jq -cn --arg command "$command_str" '{tool_name:"Bash",tool_input:{command:$command}}')"
|
|
49
|
+
canonical_output=""
|
|
50
|
+
canonical_status=0
|
|
51
|
+
if canonical_output="$(printf '%s' "$canonical_input" | /bin/bash "$canonical_hook" 2>&1)"; then
|
|
52
|
+
canonical_status=0
|
|
53
|
+
else
|
|
54
|
+
canonical_status=$?
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
[ "$canonical_status" -eq 0 ] && allow
|
|
58
|
+
[ -n "$canonical_output" ] || canonical_output="Blocked: file work items through lisa-track / lisa-tracker-write with an explicit build_ready: or human_gate:."
|
|
59
|
+
deny "$canonical_output"
|