@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.
Files changed (137) hide show
  1. package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +3 -2
  2. package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +773 -0
  3. package/dist/codex/hooks-installer.d.ts.map +1 -1
  4. package/dist/codex/hooks-installer.js +8 -0
  5. package/dist/codex/hooks-installer.js.map +1 -1
  6. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  7. package/dist/core/upstream-evidence-manifest.js +36 -16
  8. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  9. package/dist/opencode/plugin-catalog.d.ts.map +1 -1
  10. package/dist/opencode/plugin-catalog.js +5 -0
  11. package/dist/opencode/plugin-catalog.js.map +1 -1
  12. package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +210 -0
  13. package/package.json +1 -1
  14. package/plugins/lisa/.claude-plugin/plugin.json +5 -1
  15. package/plugins/lisa/.codex-plugin/hooks.json +4 -0
  16. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  17. package/plugins/lisa/.codex-plugin/skills/lisa-github-validate-issue/SKILL.md +12 -6
  18. package/plugins/lisa/.codex-plugin/skills/lisa-github-write-issue/SKILL.md +12 -2
  19. package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +1 -1
  20. package/plugins/lisa/.codex-plugin/skills/lisa-jira-validate-ticket/SKILL.md +12 -6
  21. package/plugins/lisa/.codex-plugin/skills/lisa-jira-write-ticket/SKILL.md +12 -2
  22. package/plugins/lisa/.codex-plugin/skills/lisa-linear-validate-issue/SKILL.md +12 -6
  23. package/plugins/lisa/.codex-plugin/skills/lisa-linear-write-issue/SKILL.md +15 -2
  24. package/plugins/lisa/hooks/block-direct-issue-create.agy.sh +59 -0
  25. package/plugins/lisa/hooks/block-direct-issue-create.sh +773 -0
  26. package/plugins/lisa/rules/eager/derived-branch-plan.md +3 -1
  27. package/plugins/lisa/rules/eager/ready-role-filing.md +4 -0
  28. package/plugins/lisa/rules/reference/derived-branch-plan.md +34 -0
  29. package/plugins/lisa/rules/reference/ready-role-filing.md +27 -0
  30. package/plugins/lisa/skills/lisa-github-validate-issue/SKILL.md +12 -6
  31. package/plugins/lisa/skills/lisa-github-write-issue/SKILL.md +12 -2
  32. package/plugins/lisa/skills/lisa-implement/SKILL.md +1 -1
  33. package/plugins/lisa/skills/lisa-jira-validate-ticket/SKILL.md +12 -6
  34. package/plugins/lisa/skills/lisa-jira-write-ticket/SKILL.md +12 -2
  35. package/plugins/lisa/skills/lisa-linear-validate-issue/SKILL.md +12 -6
  36. package/plugins/lisa/skills/lisa-linear-write-issue/SKILL.md +15 -2
  37. package/plugins/lisa-agy/hooks/block-direct-issue-create.agy.sh +59 -0
  38. package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +773 -0
  39. package/plugins/lisa-agy/hooks.json +13 -0
  40. package/plugins/lisa-agy/plugin.json +1 -1
  41. package/plugins/lisa-agy/skills/lisa-github-validate-issue/SKILL.md +12 -6
  42. package/plugins/lisa-agy/skills/lisa-github-write-issue/SKILL.md +12 -2
  43. package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +1 -1
  44. package/plugins/lisa-agy/skills/lisa-jira-validate-ticket/SKILL.md +12 -6
  45. package/plugins/lisa-agy/skills/lisa-jira-write-ticket/SKILL.md +12 -2
  46. package/plugins/lisa-agy/skills/lisa-linear-validate-issue/SKILL.md +12 -6
  47. package/plugins/lisa-agy/skills/lisa-linear-write-issue/SKILL.md +15 -2
  48. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  51. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-copilot/.claude-plugin/plugin.json +5 -1
  54. package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +773 -0
  55. package/plugins/lisa-copilot/rules/eager/derived-branch-plan.md +3 -1
  56. package/plugins/lisa-copilot/rules/eager/ready-role-filing.md +4 -0
  57. package/plugins/lisa-copilot/rules/reference/derived-branch-plan.md +34 -0
  58. package/plugins/lisa-copilot/rules/reference/ready-role-filing.md +27 -0
  59. package/plugins/lisa-copilot/skills/lisa-github-validate-issue/SKILL.md +12 -6
  60. package/plugins/lisa-copilot/skills/lisa-github-write-issue/SKILL.md +12 -2
  61. package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +1 -1
  62. package/plugins/lisa-copilot/skills/lisa-jira-validate-ticket/SKILL.md +12 -6
  63. package/plugins/lisa-copilot/skills/lisa-jira-write-ticket/SKILL.md +12 -2
  64. package/plugins/lisa-copilot/skills/lisa-linear-validate-issue/SKILL.md +12 -6
  65. package/plugins/lisa-copilot/skills/lisa-linear-write-issue/SKILL.md +15 -2
  66. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +773 -0
  68. package/plugins/lisa-cursor/hooks/hooks.json +4 -0
  69. package/plugins/lisa-cursor/rules/derived-branch-plan-reference.mdc +34 -0
  70. package/plugins/lisa-cursor/rules/derived-branch-plan.mdc +3 -1
  71. package/plugins/lisa-cursor/rules/ready-role-filing-reference.mdc +27 -0
  72. package/plugins/lisa-cursor/rules/ready-role-filing.mdc +4 -0
  73. package/plugins/lisa-cursor/skills/lisa-github-validate-issue/SKILL.md +12 -6
  74. package/plugins/lisa-cursor/skills/lisa-github-write-issue/SKILL.md +12 -2
  75. package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +1 -1
  76. package/plugins/lisa-cursor/skills/lisa-jira-validate-ticket/SKILL.md +12 -6
  77. package/plugins/lisa-cursor/skills/lisa-jira-write-ticket/SKILL.md +12 -2
  78. package/plugins/lisa-cursor/skills/lisa-linear-validate-issue/SKILL.md +12 -6
  79. package/plugins/lisa-cursor/skills/lisa-linear-write-issue/SKILL.md +15 -2
  80. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  82. package/plugins/lisa-expo-agy/plugin.json +1 -1
  83. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  87. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  88. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  92. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  93. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  97. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  98. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  100. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  101. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  102. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  103. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  104. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  105. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  106. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  107. package/plugins/lisa-rails-agy/plugin.json +1 -1
  108. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  109. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  110. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  111. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  112. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  113. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  114. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  115. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  116. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  117. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  118. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  119. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  120. package/plugins/src/base/.claude-plugin/plugin.json +4 -0
  121. package/plugins/src/base/hooks/block-direct-issue-create.agy.sh +59 -0
  122. package/plugins/src/base/hooks/block-direct-issue-create.sh +773 -0
  123. package/plugins/src/base/rules/eager/derived-branch-plan.md +3 -1
  124. package/plugins/src/base/rules/eager/ready-role-filing.md +4 -0
  125. package/plugins/src/base/rules/reference/derived-branch-plan.md +34 -0
  126. package/plugins/src/base/rules/reference/ready-role-filing.md +27 -0
  127. package/plugins/src/base/skills/lisa-github-validate-issue/SKILL.md +12 -6
  128. package/plugins/src/base/skills/lisa-github-write-issue/SKILL.md +12 -2
  129. package/plugins/src/base/skills/lisa-implement/SKILL.md +1 -1
  130. package/plugins/src/base/skills/lisa-jira-validate-ticket/SKILL.md +12 -6
  131. package/plugins/src/base/skills/lisa-jira-write-ticket/SKILL.md +12 -2
  132. package/plugins/src/base/skills/lisa-linear-validate-issue/SKILL.md +12 -6
  133. package/plugins/src/base/skills/lisa-linear-write-issue/SKILL.md +15 -2
  134. package/scripts/build-plugins.sh +1 -1
  135. package/scripts/generate-agy-plugin-artifacts.mjs +10 -0
  136. package/scripts/lib/per-agent-hook-filter.mjs +10 -0
  137. package/scripts/lisa-enforcement-fallback.sh +3 -2
@@ -1 +1 @@
1
- {"version":3,"file":"plugin-catalog.d.ts","sourceRoot":"","sources":["../../src/opencode/plugin-catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,iDAAiD;AACjD,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,0EAA0E;IAC1E,QAAQ,CAAC,eAAe,EAAE,SAAS,CAAC,WAAW,GAAG,GAAG,CAAC,EAAE,CAAC;CAC1D;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,SAAS,kBAAkB,EAyCvD,CAAC"}
1
+ {"version":3,"file":"plugin-catalog.d.ts","sourceRoot":"","sources":["../../src/opencode/plugin-catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,iDAAiD;AACjD,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,0EAA0E;IAC1E,QAAQ,CAAC,eAAe,EAAE,SAAS,CAAC,WAAW,GAAG,GAAG,CAAC,EAAE,CAAC;CAC1D;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,SAAS,kBAAkB,EA8CvD,CAAC"}
@@ -18,6 +18,11 @@ export const PLUGIN_CATALOG = [
18
18
  templateFilename: "lisa-block-instruction-file-edits.ts",
19
19
  forProjectTypes: ["*"],
20
20
  },
21
+ {
22
+ id: "block-direct-issue-create",
23
+ templateFilename: "lisa-block-direct-issue-create.ts",
24
+ forProjectTypes: ["*"],
25
+ },
21
26
  {
22
27
  id: "block-suppress-directives",
23
28
  templateFilename: "lisa-block-suppress-directives.ts",
@@ -1 +1 @@
1
- {"version":3,"file":"plugin-catalog.js","sourceRoot":"","sources":["../../src/opencode/plugin-catalog.ts"],"names":[],"mappings":"AAqBA;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAkC;IAC3D;QACE,EAAE,EAAE,mBAAmB;QACvB,gBAAgB,EAAE,2BAA2B;QAC7C,eAAe,EAAE,CAAC,GAAG,CAAC;KACvB;IACD;QACE,EAAE,EAAE,mBAAmB;QACvB,gBAAgB,EAAE,2BAA2B;QAC7C,eAAe,EAAE,CAAC,GAAG,CAAC;KACvB;IACD;QACE,EAAE,EAAE,8BAA8B;QAClC,gBAAgB,EAAE,sCAAsC;QACxD,eAAe,EAAE,CAAC,GAAG,CAAC;KACvB;IACD;QACE,EAAE,EAAE,2BAA2B;QAC/B,gBAAgB,EAAE,mCAAmC;QACrD,eAAe,EAAE,CAAC,YAAY,CAAC;KAChC;IACD;QACE,EAAE,EAAE,cAAc;QAClB,gBAAgB,EAAE,sBAAsB;QACxC,eAAe,EAAE,CAAC,YAAY,CAAC;KAChC;IACD;QACE,EAAE,EAAE,iBAAiB;QACrB,gBAAgB,EAAE,yBAAyB;QAC3C,eAAe,EAAE,CAAC,YAAY,EAAE,OAAO,CAAC;KACzC;IACD;QACE,EAAE,EAAE,uBAAuB;QAC3B,gBAAgB,EAAE,+BAA+B;QACjD,eAAe,EAAE,CAAC,QAAQ,CAAC;KAC5B;IACD;QACE,EAAE,EAAE,iBAAiB;QACrB,gBAAgB,EAAE,yBAAyB;QAC3C,eAAe,EAAE,CAAC,OAAO,CAAC;KAC3B;CACF,CAAC"}
1
+ {"version":3,"file":"plugin-catalog.js","sourceRoot":"","sources":["../../src/opencode/plugin-catalog.ts"],"names":[],"mappings":"AAqBA;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAkC;IAC3D;QACE,EAAE,EAAE,mBAAmB;QACvB,gBAAgB,EAAE,2BAA2B;QAC7C,eAAe,EAAE,CAAC,GAAG,CAAC;KACvB;IACD;QACE,EAAE,EAAE,mBAAmB;QACvB,gBAAgB,EAAE,2BAA2B;QAC7C,eAAe,EAAE,CAAC,GAAG,CAAC;KACvB;IACD;QACE,EAAE,EAAE,8BAA8B;QAClC,gBAAgB,EAAE,sCAAsC;QACxD,eAAe,EAAE,CAAC,GAAG,CAAC;KACvB;IACD;QACE,EAAE,EAAE,2BAA2B;QAC/B,gBAAgB,EAAE,mCAAmC;QACrD,eAAe,EAAE,CAAC,GAAG,CAAC;KACvB;IACD;QACE,EAAE,EAAE,2BAA2B;QAC/B,gBAAgB,EAAE,mCAAmC;QACrD,eAAe,EAAE,CAAC,YAAY,CAAC;KAChC;IACD;QACE,EAAE,EAAE,cAAc;QAClB,gBAAgB,EAAE,sBAAsB;QACxC,eAAe,EAAE,CAAC,YAAY,CAAC;KAChC;IACD;QACE,EAAE,EAAE,iBAAiB;QACrB,gBAAgB,EAAE,yBAAyB;QAC3C,eAAe,EAAE,CAAC,YAAY,EAAE,OAAO,CAAC;KACzC;IACD;QACE,EAAE,EAAE,uBAAuB;QAC3B,gBAAgB,EAAE,+BAA+B;QACjD,eAAe,EAAE,CAAC,QAAQ,CAAC;KAC5B;IACD;QACE,EAAE,EAAE,iBAAiB;QACrB,gBAAgB,EAAE,yBAAyB;QAC3C,eAAe,EAAE,CAAC,OAAO,CAAC;KAC3B;CACF,CAAC"}
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Lisa-managed OpenCode plugin (tool.execute.before).
3
+ *
4
+ * Refuses a direct tracker-creation command that declares no readiness. The
5
+ * `ready-role-filing` rule says every filing carries either `build_ready: true`
6
+ * or an explicit `human_gate:` reason and goes through `lisa-track` /
7
+ * `lisa-tracker-write`. An audit of one working session found 13 of 13 issues
8
+ * filed in violation of it, several by the agent that wrote the rule — while
9
+ * the one obligation backed by a git hook was honored 50 out of 50 times. This
10
+ * is that rule promoted from prose to an executable control.
11
+ *
12
+ * Port of Lisa's canonical hook `block-direct-issue-create.sh`. OpenCode
13
+ * exposes the shell as the `bash` tool, so the command arrives on
14
+ * `output.args.command`. Throwing in `tool.execute.before` cancels the tool
15
+ * call and surfaces the message to the agent.
16
+ *
17
+ * The port matches on the raw command text rather than tokenising it, which
18
+ * makes it naturally immune to the prefix and tokenisation bypass classes the
19
+ * shell guard had to be restructured to close — an unrecognised wrapper is just
20
+ * more text before the CLI name. Two deliberate differences remain, both in the
21
+ * permissive direction so this port can never refuse something the canonical
22
+ * guard would allow:
23
+ * - `--body-file` contents are not read, so an OpenCode caller declaring a
24
+ * human gate puts the `[lisa-human-gate]` marker on the command line;
25
+ * - remote execution (`ssh host '…'`) is not intercepted, matching the shell
26
+ * guard's documented limit.
27
+ *
28
+ * NOTE: This file is a template Lisa copies verbatim into a host project's
29
+ * `.opencode/plugin/`. It is intentionally excluded from this repo's tsconfig
30
+ * and eslint config — it runs under OpenCode's Bun runtime, not here.
31
+ */
32
+ export /**
33
+ *
34
+ */
35
+ const LisaBlockDirectIssueCreate = async () => {
36
+ const HUMAN_GATE_MARKER = "[lisa-human-gate]";
37
+ /**
38
+ * A label / workflow-state assignment and its value.
39
+ *
40
+ * Long forms only, deliberately: short flags are per-CLI (`-s` is `--state`
41
+ * on one and `--summary` on another), so accepting them would re-open the
42
+ * free-text hole one letter smaller. Every Lisa writer emits the long form.
43
+ */
44
+ const LABEL_FLAG =
45
+ /--(?:label|labels|add-label|status|state)(?:=|\s+)(['"]?)([^'"\s]+)\1/g;
46
+ const CREATION_SIGNATURES: readonly {
47
+ readonly re: RegExp;
48
+ readonly name: string;
49
+ }[] = [
50
+ {
51
+ re: /(^|[;&|("'\s])gh\s+issue\s+(?:--?\S+(?:[= ]\S+)?\s+)*create(\s|$)/,
52
+ name: "gh issue create",
53
+ },
54
+ {
55
+ re: /(^|[;&|("'\s])linear\s+issue\s+(?:--?\S+(?:[= ]\S+)?\s+)*create(\s|$)/,
56
+ name: "linear issue create",
57
+ },
58
+ {
59
+ re: /(^|[;&|("'\s])jira\s+issue\s+(?:--?\S+(?:[= ]\S+)?\s+)*create(\s|$)/,
60
+ name: "jira issue create",
61
+ },
62
+ {
63
+ re: /(^|[;&|("'\s])acli\s+[^;&|]*\b(workitem|issue)s?\s+create\b/,
64
+ name: "acli … create",
65
+ },
66
+ {
67
+ // Scoped to a tracker API call on purpose. A bare mutation NAME is just a
68
+ // word: `git commit -m "fix issueCreate typo"` and `rg issueCreate` are
69
+ // ordinary commands, and matching them made the guard refuse work it has
70
+ // no business refusing. The mutation only means a creation when it is
71
+ // being SENT, so `gh api` or an HTTP write to Linear's endpoint must
72
+ // appear on the same command.
73
+ re: /(?:(^|[;&|("'\s])gh\s+[^;&|]*\bapi\b|api\.linear\.app\/graphql)[^;&|]*\b(createIssue|issueCreate)\b/,
74
+ name: "a GraphQL issue-creation mutation",
75
+ },
76
+ {
77
+ re: /repos\/[^/\s]+\/[^/\s]+\/issues\b[^;&|]*(-X\s*POST|--method\s+POST|\s-[fF]\s|--input\b|--data\b)/,
78
+ name: "a POST to the issues endpoint",
79
+ },
80
+ {
81
+ re: /atlassian\.net\/rest\/api\/[^/\s]+\/issue\b[^;&|]*(-X\s*POST|--request\s+POST|--data\b)/,
82
+ name: "a POST to the JIRA issue endpoint",
83
+ },
84
+ ];
85
+
86
+ interface LisaConfig {
87
+ tracker?: string;
88
+ github?: { labels?: { build?: { ready?: string } } };
89
+ jira?: { workflow?: { ready?: string } };
90
+ linear?: { workflow?: { ready?: string } };
91
+ }
92
+
93
+ /**
94
+ * Read one config file, tolerating absence.
95
+ * @param file Path relative to the project root.
96
+ * @returns The parsed config, or an empty object.
97
+ */
98
+ const readConfig = async (file: string): Promise<LisaConfig> => {
99
+ try {
100
+ return JSON.parse(await Bun.file(file).text()) as LisaConfig;
101
+ } catch {
102
+ return {};
103
+ }
104
+ };
105
+
106
+ /**
107
+ * The configured build-ready role, or undefined when no tracker is set.
108
+ *
109
+ * Keyed off the resolved `tracker` rather than provider precedence: reading
110
+ * whichever provider block happened to appear first could hand a GitHub label
111
+ * to a Linear project, so the guard and the writer would disagree about what
112
+ * a declaration even looks like. The local overlay is layered over the base
113
+ * with field-level precedence, matching how `lisa-tracker-read` and
114
+ * `lisa-tracker-write` resolve it — a project that overrides its tracker only
115
+ * in `.lisa.config.local.json` was previously invisible here.
116
+ *
117
+ * Resolved once per session at plugin init. That is a deliberate snapshot: a
118
+ * config edit mid-session needs a session restart to take effect, which is
119
+ * the same lifetime as the rest of this plugin's state.
120
+ * @returns The role token, or undefined.
121
+ */
122
+ const resolveReadyRole = async (): Promise<string | undefined> => {
123
+ const base = await readConfig(".lisa.config.json");
124
+ const local = await readConfig(".lisa.config.local.json");
125
+ const tracker = local.tracker ?? base.tracker;
126
+ // No configured tracker means no `lisa-tracker-write` to route through —
127
+ // the bootstrapping case, detected rather than asserted.
128
+ if (!tracker) return undefined;
129
+ const pick = (config: LisaConfig): string | undefined => {
130
+ if (tracker === "github") return config.github?.labels?.build?.ready;
131
+ if (tracker === "jira") return config.jira?.workflow?.ready;
132
+ if (tracker === "linear") return config.linear?.workflow?.ready;
133
+ return undefined;
134
+ };
135
+ return pick(local) ?? pick(base) ?? "status:ready";
136
+ };
137
+
138
+ const readyRole = await resolveReadyRole();
139
+
140
+ return {
141
+ "tool.execute.before": async (
142
+ input: { tool: string },
143
+ output: { args?: { command?: string } }
144
+ ) => {
145
+ if (input.tool !== "bash") return;
146
+ if (readyRole === undefined) return;
147
+ const command = String(output.args?.command ?? "");
148
+ if (!command) return;
149
+ if (/--help\b|\s-h(\s|$)/.test(command)) return;
150
+ // The override is honored only from the ambient environment. An inline
151
+ // assignment is the agent granting itself the exemption, so it
152
+ // disqualifies the override rather than supplying it.
153
+ const inlineOverride = /LISA_ALLOW_DIRECT_ISSUE_CREATE=/.test(command);
154
+ if (process.env["LISA_ALLOW_DIRECT_ISSUE_CREATE"] && !inlineOverride)
155
+ return;
156
+ const signature = CREATION_SIGNATURES.find(entry =>
157
+ entry.re.test(command)
158
+ );
159
+ if (!signature) return;
160
+ // The build-ready role counts ONLY as the value of a label / state flag.
161
+ // A free-text scan of the command let a bug report's own title declare
162
+ // readiness — `gh issue create --title "status:ready is broken"` — which
163
+ // is the same position-blind matching that turned #2469's hardening
164
+ // allowlist into a bypass. The human-gate marker is matched anywhere by
165
+ // contrast, because it is a marker with no other meaning.
166
+ // Everything after a bare `--` is an operand and cannot reach the
167
+ // created item, so no declaration may be read from there. gh rejects
168
+ // post-`--` flags outright; acli parses straight past them and creates
169
+ // the item with the flag silently unapplied (verified). Fails closed.
170
+ const declarable = command.split(/(?:^|\s)--(?:\s|$)/)[0] ?? command;
171
+ const declaresRole = [...declarable.matchAll(LABEL_FLAG)].some(match =>
172
+ (match[2] ?? "")
173
+ .split(",")
174
+ .map(part => part.trim())
175
+ .includes(readyRole)
176
+ );
177
+ if (declaresRole || declarable.includes(HUMAN_GATE_MARKER)) return;
178
+ throw new Error(
179
+ [
180
+ `block-direct-issue-create: refusing ${signature.name} — this filing declares no readiness.`,
181
+ "",
182
+ "WHY: a work item filed without the build-ready role is an incomplete",
183
+ "handoff. Build-intake scans the ready lane and nothing else, so nothing",
184
+ "will ever pick it up: the write succeeds and the work still dies.",
185
+ "",
186
+ "FILE IT THE SANCTIONED WAY — one of these two, always explicit:",
187
+ "",
188
+ '1. Complete enough to build? Run /lisa:track "<what needs building>",',
189
+ " which resolves or creates exactly one live leaf through",
190
+ " lisa-tracker-write with build_ready: true, validates it before the",
191
+ " write, and claims it.",
192
+ "2. A human product call is pending? Route the same way but pass",
193
+ ' human_gate: "<why a human must judge this first>", which stamps',
194
+ ` ${HUMAN_GATE_MARKER} on the item so the hold is auditable.`,
195
+ "",
196
+ "Filed, not ready, and no human_gate is the incomplete-handoff case. See",
197
+ "the ready-role-filing rule for the full contract.",
198
+ "",
199
+ "If you must run the CLI directly, the command has to carry one of the",
200
+ `two declarations itself: the configured build-ready role "${readyRole}",`,
201
+ `or a ${HUMAN_GATE_MARKER} marker in the body it submits.`,
202
+ "",
203
+ "OPERATOR ESCAPE: a human can export LISA_ALLOW_DIRECT_ISSUE_CREATE=1 in",
204
+ "the environment before starting the session. Setting it inline on this",
205
+ "command is deliberately refused.",
206
+ ].join("\n")
207
+ );
208
+ },
209
+ };
210
+ };
package/package.json CHANGED
@@ -120,7 +120,7 @@
120
120
  }
121
121
  },
122
122
  "name": "@codyswann/lisa",
123
- "version": "3.5.4",
123
+ "version": "3.6.0",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.5.4",
3
+ "version": "3.6.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -110,6 +110,10 @@
110
110
  {
111
111
  "type": "command",
112
112
  "command": "${CLAUDE_PLUGIN_ROOT}/hooks/block-instruction-file-edits.sh"
113
+ },
114
+ {
115
+ "type": "command",
116
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/block-direct-issue-create.sh"
113
117
  }
114
118
  ]
115
119
  },
@@ -50,6 +50,10 @@
50
50
  {
51
51
  "type": "command",
52
52
  "command": "${PLUGIN_ROOT}/hooks/block-instruction-file-edits.sh"
53
+ },
54
+ {
55
+ "type": "command",
56
+ "command": "${PLUGIN_ROOT}/hooks/block-direct-issue-create.sh"
53
57
  }
54
58
  ]
55
59
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.5.4",
3
+ "version": "3.6.0",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -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: true # requires Target Backend Environment + Validation Journey
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, then run gates. The parser lives in `lisa-github-read-issue` (composition).
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
- When `runtime_behavior_change = true`, the body must contain `## Target Backend Environment`. 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. Skipped for doc-only / config-only / type-only / Epic.
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
- [Required when the issue changes runtime behavior. Use an exact
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>`. Skip only for doc/config/type-only issues.]
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 `## Target Backend Environment` to derive from, so **absence is correct** — never demand or invent one.
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: true # requires Target Backend Environment + Validation Journey
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
- When `runtime_behavior_change = true`, the description must contain a `Target Backend Environment` section (`h2.` / `##` in proposed text, or an ADF heading in live JIRA). 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. Skipped for doc-only / config-only / type-only / Epic.
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
- [Required when the ticket changes runtime behavior. Use an exact
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>`. Skip only for doc/config/type-only tickets.]
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: true # requires Target Backend Environment + Validation Journey
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
- When `runtime_behavior_change = true`, the description must contain `## Target Backend Environment`. 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. Skipped for doc-only / config-only / type-only / Epic.
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.