@codyswann/lisa 2.349.1 → 2.351.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 (192) hide show
  1. package/dist/cli/doctor-learnings-ledger.d.ts +23 -0
  2. package/dist/cli/doctor-learnings-ledger.d.ts.map +1 -0
  3. package/dist/cli/doctor-learnings-ledger.js +67 -0
  4. package/dist/cli/doctor-learnings-ledger.js.map +1 -0
  5. package/dist/cli/doctor.d.ts.map +1 -1
  6. package/dist/cli/doctor.js +6 -0
  7. package/dist/cli/doctor.js.map +1 -1
  8. package/dist/core/learnings-file-safety.d.ts +12 -1
  9. package/dist/core/learnings-file-safety.d.ts.map +1 -1
  10. package/dist/core/learnings-file-safety.js +20 -1
  11. package/dist/core/learnings-file-safety.js.map +1 -1
  12. package/dist/core/learnings-location.d.ts +40 -0
  13. package/dist/core/learnings-location.d.ts.map +1 -0
  14. package/dist/core/learnings-location.js +89 -0
  15. package/dist/core/learnings-location.js.map +1 -0
  16. package/dist/core/learnings-stray-ledger.d.ts +24 -0
  17. package/dist/core/learnings-stray-ledger.d.ts.map +1 -0
  18. package/dist/core/learnings-stray-ledger.js +155 -0
  19. package/dist/core/learnings-stray-ledger.js.map +1 -0
  20. package/dist/core/project-config.d.ts +1 -13
  21. package/dist/core/project-config.d.ts.map +1 -1
  22. package/dist/core/project-config.js +5 -53
  23. package/dist/core/project-config.js.map +1 -1
  24. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  25. package/dist/core/upstream-evidence-manifest.js +46 -18
  26. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  27. package/package.json +1 -1
  28. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  30. package/plugins/lisa/.codex-plugin/skills/lisa-github-read-issue/SKILL.md +1 -0
  31. package/plugins/lisa/.codex-plugin/skills/lisa-github-validate-issue/SKILL.md +22 -0
  32. package/plugins/lisa/.codex-plugin/skills/lisa-github-write-issue/SKILL.md +15 -0
  33. package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +6 -0
  34. package/plugins/lisa/.codex-plugin/skills/lisa-jira-validate-ticket/SKILL.md +22 -0
  35. package/plugins/lisa/.codex-plugin/skills/lisa-jira-write-ticket/SKILL.md +15 -0
  36. package/plugins/lisa/.codex-plugin/skills/lisa-linear-access/SKILL.md +25 -3
  37. package/plugins/lisa/.codex-plugin/skills/lisa-linear-validate-issue/SKILL.md +22 -0
  38. package/plugins/lisa/.codex-plugin/skills/lisa-linear-write-issue/SKILL.md +15 -0
  39. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +20 -2
  40. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +13 -9
  41. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/note-format.mjs +179 -0
  42. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/resolve-secret.mjs +32 -6
  43. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +6 -2
  44. package/plugins/lisa/.codex-plugin/skills/lisa-setup-atlassian/SKILL.md +17 -0
  45. package/plugins/lisa/.codex-plugin/skills/lisa-setup-linear/SKILL.md +17 -0
  46. package/plugins/lisa/.codex-plugin/skills/lisa-setup-notion/SKILL.md +17 -0
  47. package/plugins/lisa/rules/eager/derived-branch-plan.md +45 -0
  48. package/plugins/lisa/rules/reference/config-resolution.md +16 -0
  49. package/plugins/lisa/rules/reference/derived-branch-plan.md +112 -0
  50. package/plugins/lisa/skills/lisa-github-read-issue/SKILL.md +1 -0
  51. package/plugins/lisa/skills/lisa-github-validate-issue/SKILL.md +22 -0
  52. package/plugins/lisa/skills/lisa-github-write-issue/SKILL.md +15 -0
  53. package/plugins/lisa/skills/lisa-implement/SKILL.md +6 -0
  54. package/plugins/lisa/skills/lisa-jira-validate-ticket/SKILL.md +22 -0
  55. package/plugins/lisa/skills/lisa-jira-write-ticket/SKILL.md +15 -0
  56. package/plugins/lisa/skills/lisa-linear-access/SKILL.md +25 -3
  57. package/plugins/lisa/skills/lisa-linear-validate-issue/SKILL.md +22 -0
  58. package/plugins/lisa/skills/lisa-linear-write-issue/SKILL.md +15 -0
  59. package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +20 -2
  60. package/plugins/lisa/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +13 -9
  61. package/plugins/lisa/skills/lisa-secrets-access/scripts/note-format.mjs +179 -0
  62. package/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs +32 -6
  63. package/plugins/lisa/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +6 -2
  64. package/plugins/lisa/skills/lisa-setup-atlassian/SKILL.md +17 -0
  65. package/plugins/lisa/skills/lisa-setup-linear/SKILL.md +17 -0
  66. package/plugins/lisa/skills/lisa-setup-notion/SKILL.md +17 -0
  67. package/plugins/lisa-agy/plugin.json +1 -1
  68. package/plugins/lisa-agy/skills/lisa-github-read-issue/SKILL.md +1 -0
  69. package/plugins/lisa-agy/skills/lisa-github-validate-issue/SKILL.md +22 -0
  70. package/plugins/lisa-agy/skills/lisa-github-write-issue/SKILL.md +15 -0
  71. package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +6 -0
  72. package/plugins/lisa-agy/skills/lisa-jira-validate-ticket/SKILL.md +22 -0
  73. package/plugins/lisa-agy/skills/lisa-jira-write-ticket/SKILL.md +15 -0
  74. package/plugins/lisa-agy/skills/lisa-linear-access/SKILL.md +25 -3
  75. package/plugins/lisa-agy/skills/lisa-linear-validate-issue/SKILL.md +22 -0
  76. package/plugins/lisa-agy/skills/lisa-linear-write-issue/SKILL.md +15 -0
  77. package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +20 -2
  78. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +13 -9
  79. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/note-format.mjs +179 -0
  80. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/resolve-secret.mjs +32 -6
  81. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +6 -2
  82. package/plugins/lisa-agy/skills/lisa-setup-atlassian/SKILL.md +17 -0
  83. package/plugins/lisa-agy/skills/lisa-setup-linear/SKILL.md +17 -0
  84. package/plugins/lisa-agy/skills/lisa-setup-notion/SKILL.md +17 -0
  85. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  87. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  88. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-copilot/rules/eager/derived-branch-plan.md +45 -0
  92. package/plugins/lisa-copilot/rules/reference/config-resolution.md +16 -0
  93. package/plugins/lisa-copilot/rules/reference/derived-branch-plan.md +112 -0
  94. package/plugins/lisa-copilot/skills/lisa-github-read-issue/SKILL.md +1 -0
  95. package/plugins/lisa-copilot/skills/lisa-github-validate-issue/SKILL.md +22 -0
  96. package/plugins/lisa-copilot/skills/lisa-github-write-issue/SKILL.md +15 -0
  97. package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +6 -0
  98. package/plugins/lisa-copilot/skills/lisa-jira-validate-ticket/SKILL.md +22 -0
  99. package/plugins/lisa-copilot/skills/lisa-jira-write-ticket/SKILL.md +15 -0
  100. package/plugins/lisa-copilot/skills/lisa-linear-access/SKILL.md +25 -3
  101. package/plugins/lisa-copilot/skills/lisa-linear-validate-issue/SKILL.md +22 -0
  102. package/plugins/lisa-copilot/skills/lisa-linear-write-issue/SKILL.md +15 -0
  103. package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +20 -2
  104. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +13 -9
  105. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/note-format.mjs +179 -0
  106. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/resolve-secret.mjs +32 -6
  107. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +6 -2
  108. package/plugins/lisa-copilot/skills/lisa-setup-atlassian/SKILL.md +17 -0
  109. package/plugins/lisa-copilot/skills/lisa-setup-linear/SKILL.md +17 -0
  110. package/plugins/lisa-copilot/skills/lisa-setup-notion/SKILL.md +17 -0
  111. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +16 -0
  113. package/plugins/lisa-cursor/rules/derived-branch-plan-reference.mdc +117 -0
  114. package/plugins/lisa-cursor/rules/derived-branch-plan.mdc +50 -0
  115. package/plugins/lisa-cursor/skills/lisa-github-read-issue/SKILL.md +1 -0
  116. package/plugins/lisa-cursor/skills/lisa-github-validate-issue/SKILL.md +22 -0
  117. package/plugins/lisa-cursor/skills/lisa-github-write-issue/SKILL.md +15 -0
  118. package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +6 -0
  119. package/plugins/lisa-cursor/skills/lisa-jira-validate-ticket/SKILL.md +22 -0
  120. package/plugins/lisa-cursor/skills/lisa-jira-write-ticket/SKILL.md +15 -0
  121. package/plugins/lisa-cursor/skills/lisa-linear-access/SKILL.md +25 -3
  122. package/plugins/lisa-cursor/skills/lisa-linear-validate-issue/SKILL.md +22 -0
  123. package/plugins/lisa-cursor/skills/lisa-linear-write-issue/SKILL.md +15 -0
  124. package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +20 -2
  125. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +13 -9
  126. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/note-format.mjs +179 -0
  127. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/resolve-secret.mjs +32 -6
  128. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +6 -2
  129. package/plugins/lisa-cursor/skills/lisa-setup-atlassian/SKILL.md +17 -0
  130. package/plugins/lisa-cursor/skills/lisa-setup-linear/SKILL.md +17 -0
  131. package/plugins/lisa-cursor/skills/lisa-setup-notion/SKILL.md +17 -0
  132. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  133. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  134. package/plugins/lisa-expo-agy/plugin.json +1 -1
  135. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  136. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  137. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  138. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  139. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  140. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  141. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  142. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  143. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  144. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  145. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  146. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  147. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  148. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  149. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  150. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  151. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  152. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  153. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  154. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  155. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  156. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  157. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  158. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  159. package/plugins/lisa-rails-agy/plugin.json +1 -1
  160. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  161. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  162. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  163. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  164. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  165. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  166. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  167. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  168. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  169. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  170. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  171. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  172. package/plugins/src/base/rules/eager/derived-branch-plan.md +45 -0
  173. package/plugins/src/base/rules/reference/config-resolution.md +16 -0
  174. package/plugins/src/base/rules/reference/derived-branch-plan.md +112 -0
  175. package/plugins/src/base/skills/lisa-github-read-issue/SKILL.md +1 -0
  176. package/plugins/src/base/skills/lisa-github-validate-issue/SKILL.md +22 -0
  177. package/plugins/src/base/skills/lisa-github-write-issue/SKILL.md +15 -0
  178. package/plugins/src/base/skills/lisa-implement/SKILL.md +6 -0
  179. package/plugins/src/base/skills/lisa-jira-validate-ticket/SKILL.md +22 -0
  180. package/plugins/src/base/skills/lisa-jira-write-ticket/SKILL.md +15 -0
  181. package/plugins/src/base/skills/lisa-linear-access/SKILL.md +25 -3
  182. package/plugins/src/base/skills/lisa-linear-validate-issue/SKILL.md +22 -0
  183. package/plugins/src/base/skills/lisa-linear-write-issue/SKILL.md +15 -0
  184. package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +20 -2
  185. package/plugins/src/base/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +13 -9
  186. package/plugins/src/base/skills/lisa-secrets-access/scripts/note-format.mjs +179 -0
  187. package/plugins/src/base/skills/lisa-secrets-access/scripts/resolve-secret.mjs +32 -6
  188. package/plugins/src/base/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +6 -2
  189. package/plugins/src/base/skills/lisa-setup-atlassian/SKILL.md +17 -0
  190. package/plugins/src/base/skills/lisa-setup-linear/SKILL.md +17 -0
  191. package/plugins/src/base/skills/lisa-setup-notion/SKILL.md +17 -0
  192. package/scripts/github-status-check.sh +22 -7
@@ -0,0 +1,112 @@
1
+ # Derived Branch Plan — reference
2
+
3
+ The full body behind the `derived-branch-plan` eager head. It defines one vendor-neutral contract consumed by the three tracker writers (`lisa-jira-write-ticket`, `lisa-github-write-issue`, `lisa-linear-write-issue`), the three validators (`lisa-jira-validate-ticket`, `lisa-github-validate-issue`, `lisa-linear-validate-issue`) via gate **S19**, and `lisa-implement` at claim time. Each surface cites this slug rather than growing its own prose, exactly as the vendor arms cite `leaf-only-lifecycle` and `repo-scope-split`. One slug is what keeps a branch plan rejected on JIRA from being accepted on Linear.
4
+
5
+ ## The problem this solves, and the one it must not create
6
+
7
+ An operator reading a work item cannot see which branch the work starts from or which branch the PR lands in. Both facts are knowable, but only by resolving `.lisa.config.json` — which is exactly the kind of engineering step the factory model says a non-technical operator standing at the gate should not have to take.
8
+
9
+ The obvious fix — add a "source branch" field and a "PR target" field to the ticket template — is wrong, and it is worth being explicit about why, because it will look like the simple answer every time someone revisits this. Those fields would be **a second source of truth** for a fact Lisa already derives authoritatively from `## Target Backend Environment` + `deploy.branches`. Two authorities do not stay equal. They drift the first time an environment is corrected on a ticket whose branch fields nobody updated, or the first time `deploy.branches` is remapped. And the consequence of that drift is not a stale label: it is an agent branching from, and merging into, the wrong environment's branch — shipping unreviewed code to production, or shipping a production fix into a staging branch where nobody sees it.
10
+
11
+ So the contract renders the branches **for humans** and recomputes them **for machines**. The rendered section is output. It is never input.
12
+
13
+ Precedence is therefore not a negotiation: whenever a rendered plan and the resolved environment disagree, **the environment wins** — the authority wins, in every phase, on every vendor, with no exception for a plan a human edited by hand. The only question a disagreement raises is whether to re-render silently (staleness) or stop for a human (a conflict with a confirmed environment or an open PR base). It is never "which branch did they mean?"
14
+
15
+ ## The rendered section
16
+
17
+ | Vendor | Heading form |
18
+ |---|---|
19
+ | JIRA | `h2. Branch Plan` (ADF heading in live JIRA) |
20
+ | GitHub | `## Branch Plan` |
21
+ | Linear | `## Branch Plan` |
22
+
23
+ Body, identical on all three:
24
+
25
+ ```
26
+ Branch from: <branch>
27
+ PR into: <branch>
28
+ Derived from: Target Backend Environment <env> via .lisa.config.json deploy.branches
29
+ ```
30
+
31
+ ### The derivation provenance line
32
+
33
+ The third line is load-bearing. It is the discriminator between a section this contract produced and one a human typed, and it is deliberately visible prose rather than an HTML comment — the same reason the `Target Backend Environment` grammar uses visible `Inferred:` / `Assumption:` annotations: JIRA's ADF has no comment node, so a marker that only exists in markdown cannot be a vendor-neutral discriminator.
34
+
35
+ A `Branch Plan` **without the provenance line is treated as hand-authored**, and hand-authored branches are never obeyed — they are compared against the recomputed plan and, on any disagreement, fail. A provenance line naming an `<env>` that is not an exact configured `deploy.branches` key is itself malformed and fails.
36
+
37
+ ### One branch, two labels
38
+
39
+ `Branch from` and `PR into` always name the **same branch**. That is not a simplification, it is the invariant `lisa-implement` already implements: the feature branch is cut from `origin/<base>` and the PR targets `<base>`. Both labels exist because operators asked to see both questions answered, and because naming them separately gives the contract somewhere to say they are equal instead of leaving it implicit.
40
+
41
+ A plan whose two fields name **two different branches** is malformed and fails validation. The case that looks like a counter-example — a bug fixed on a non-integration environment branch that must also reach the integration branch — is handled the way `lisa-implement` already handles it: as a **linked forward cherry-pick follow-up item**, which is its own work item with its own single-branch plan. It is never a divergence inside one plan.
42
+
43
+ ## Derivation algorithm
44
+
45
+ 1. **Resolve the environment.** Use the existing grammar and precedence defined in `config-resolution` and `pre-flight-autofill`: a human-confirmed bare configured key or `Confirmed: <env>` wins; then a validated `Inferred: <env> — evidence: <title|body|reproduction|hostname>`; then `Assumption: <env> — remote default branch <branch>`, or the branch-only `Assumption: remote default branch <branch>` when the reverse-map is not unique. This rule adds no new resolution logic and no new aliases.
46
+ 2. **Map forward.** Look the exact configured key up in `deploy.branches`. The key must resolve to exactly one branch.
47
+ 3. **Prove the branch.** The mapped branch must exist on the remote.
48
+ 4. **Render.** Both fields to that branch, plus the provenance line naming the `<env>` used.
49
+
50
+ ### Stop conditions
51
+
52
+ | Condition | Behavior |
53
+ |---|---|
54
+ | No environment resolvable and the item is not exempt | Stop — the `Target Backend Environment` gate (S8) already owns this |
55
+ | Environment key absent from `deploy.branches` | Stop |
56
+ | Mapping ambiguous / not unique | Stop |
57
+ | Mapped branch missing on the remote | Stop |
58
+
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
+
61
+ ## Gate S19 — Branch Plan derivation
62
+
63
+ 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.
64
+
65
+ | Case | Verdict |
66
+ |---|---|
67
+ | Exempt work (`runtime_behavior_change = false`, or a container) with no plan | `N/A` |
68
+ | Exempt work **carrying** a plan | **FAIL** — hand-authored branches on work that declared no runtime target |
69
+ | Applicable work, plan present, matches recomputed plan | PASS |
70
+ | Applicable work, plan present, disagrees with recomputed plan | **FAIL** |
71
+ | Applicable work, plan present, missing the provenance line and disagreeing | **FAIL** (hand-authored) |
72
+ | Applicable work, two fields naming different branches | **FAIL** — malformed |
73
+ | Applicable work, derivation hits a stop condition | **FAIL** — same stop as implement |
74
+ | Applicable work, **proposed spec** (pre-write) with no plan | **FAIL** — the writer must render it before the write |
75
+ | Applicable work, **live legacy item** with no plan | `N/A` with a repair note routed to claim time |
76
+
77
+ The last two rows are the asymmetry that matters. Failing a *proposed* spec is free — the writer just renders the section. Failing every *existing* item would turn a whole legacy queue red overnight for a section no human ever had a way to add, so those are repaired at claim time instead, where the derivation is written onto the item as evidence.
78
+
79
+ ### Failure remediation
80
+
81
+ Every S19 failure names both plans (rendered and recomputed) and points the fix at **the environment, never the branches**:
82
+
83
+ > Branch Plan conflicts with the environment mapping. Rendered `Branch from: release/staging`; recomputed from `Target Backend Environment: production` via `deploy.branches` → `main`. Correct the environment, not the branch — the branches are derived. Then re-render.
84
+
85
+ The gate never silently chooses between the two. Picking one would be a guess wearing a verdict.
86
+
87
+ ## Claim-time behavior (`lisa-implement`)
88
+
89
+ At claim time the plan is recomputed against **current config and the remote** — never trusted from the item — and one of four arms runs:
90
+
91
+ - **Match** — proceed. The already-existing base-branch validation, feature-branch sync, and `target_branch=<base>` handoff are unchanged.
92
+ - **Legacy (no plan)** — derive it, **write the assumption onto the item as a comment**, then proceed. The comment carries a visible prose line plus a dedupe marker so a re-claim produces no duplicate:
93
+
94
+ ```
95
+ Branch plan derived for this item: branch from `main`, PR into `main` (Target Backend
96
+ Environment: production via .lisa.config.json deploy.branches).
97
+ <!-- [lisa-branch-plan] key=<work-item-ref>::<branch> -->
98
+ ```
99
+
100
+ Marker-dedupe is on `<work-item-ref>::<branch>`, so the comment reappears only if the derived branch actually changes. Where a vendor cannot host an HTML comment, the visible line alone carries it. If the comment cannot be written, that is a stop, not a shrug — proceeding would make it exactly the silent guess this rule forbids.
101
+ - **Conflict with a human-confirmed environment, or with an existing open PR's base** — **stop under the existing confirmation rules**. `lisa-implement` already surfaces a PR-base mismatch and re-targets only with confirmation, on the stated grounds that the ticket's environment is the source of truth; a branch plan never overrides that, and never supplies the confirmation itself.
102
+ - **Stale (config changed since the plan was rendered)** — **current config wins**. Re-render the section onto the item, record the change, and never follow the stale plan. A stale plan is not a conflict to escalate; it is output that fell behind its input.
103
+
104
+ ## Why the gate is `technical` / not product-relevant
105
+
106
+ S19 is categorized `technical`, `product_relevant: false`, matching S8 — the fact it checks is a deployment-topology fact, and a PRD-intake comment aimed at a product author cannot act on it. The operator-facing value of the section is that it is *visible on the item*, not that its failures are routed to product.
107
+
108
+ ## What this rule deliberately does not do
109
+
110
+ - It defines **no new environment resolution**, no new aliases, and no new provenance grammar. All of that stays in `config-resolution` / `pre-flight-autofill`, and this rule reads the result.
111
+ - It does **not** let a branch plan influence which environment is chosen, in any direction, at any phase.
112
+ - It does **not** introduce branch fields on PRDs. PRDs are not built directly; their generated leaves carry the plan.
@@ -48,6 +48,7 @@ Walk the markdown body and capture each top-level `## ` section by name. Standar
48
48
  - `Acceptance Criteria` (preserve the Gherkin code-fence verbatim)
49
49
  - `Out of Scope`
50
50
  - `Target Backend Environment`
51
+ - `Branch Plan` (derived output per `derived-branch-plan` — parsed so callers can *compare* it against a recomputation, never so they can use it as the base branch)
51
52
  - `Sign-in Required`
52
53
  - `Repository`
53
54
  - `Source Artifacts`
@@ -98,6 +98,7 @@ Per-type content requirements are defined once in the vendor-neutral `work-item-
98
98
  | S16 Source Requirement traceability | `product-clarity` | true |
99
99
  | S17 Improvement measurability | `acceptance-criteria` | true |
100
100
  | S18 Stateless-pickup dry-run | `product-clarity` | true |
101
+ | S19 Branch Plan derivation | `technical` | false |
101
102
  | F1 Issue type label exists in repo | `structural` | false |
102
103
  | F2 Parent sub-issue exists and is the right type | `structural` | false |
103
104
  | F3 Linked issues exist | `structural` | false |
@@ -299,6 +300,26 @@ The autonomy gate, run last, on every build-ready leaf (use the S15 classificati
299
300
 
300
301
  Zero questions → PASS. Any question → FAIL, with each question listed verbatim as its own remediation line — these are exactly the clarifying comments the caller posts to the source. The structure gates are proxies; this gate checks the readiness property itself: `ready` means a stateless agent can drive this item to its terminal state with zero human clarification (see `work-item-definition-of-ready`).
301
302
 
303
+ #### S19 — Branch Plan derivation
304
+
305
+ Enforces the `derived-branch-plan` rule: the `## Branch Plan` section is **derived only** — recompute it from current config and compare; never trust the rendered branches or read them as input. Resolve the environment under the S8 grammar, map that exact configured key forward through `.lisa.config.json` `deploy.branches`, and require the mapped branch on the remote.
306
+
307
+ | Case | Verdict |
308
+ |---|---|
309
+ | `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
+ | Exempt work **carrying** a `## Branch Plan` | **FAIL** — hand-authored branches on work that declared no runtime target |
311
+ | Plan matches the recomputed plan | PASS |
312
+ | Plan conflicts with the recomputed plan | **FAIL** |
313
+ | Plan missing the `Derived from:` provenance line and disagreeing | **FAIL** — treated as hand-authored |
314
+ | `Branch from` and `PR into` name two different branches | **FAIL** — malformed; they are the same branch by construction |
315
+ | Derivation hits a stop condition (env absent from `deploy.branches`, ambiguous / non-unique mapping, branch missing on the remote) | **FAIL** — the same stop `lisa-implement` takes; never default to `main` or the remote default |
316
+ | Proposed spec (pre-write) for applicable work with no plan | **FAIL** — `lisa-github-write-issue` must render it before the write |
317
+ | Live **legacy** issue for applicable work with no plan | `N/A` with a repair note — routed to claim time, where `lisa-implement` writes the derived assumption as a comment and proceeds |
318
+
319
+ Failing a proposed spec is free; failing every existing issue would turn a legacy queue red for a section no human had a way to add, so legacy absence is repaired at claim time instead.
320
+
321
+ FAIL names both plans (rendered and recomputed) and points the remediation at **the environment, never the branch** — e.g. `"Branch Plan conflicts with the environment mapping. Rendered 'Branch from: release/staging'; recomputed from Target Backend Environment 'production' via deploy.branches → 'main'. Correct the environment, not the branch — the branches are derived. Then re-render."` Never silently choose between the two plans.
322
+
302
323
  ### Feasibility Gates (require GitHub lookups; skip in `--spec-only`)
303
324
 
304
325
  #### F1 — Issue type label exists in repo
@@ -455,6 +476,7 @@ Output is a single fenced text block. Callers parse it; do not add free-form pro
455
476
  - [PASS|FAIL|N/A] S16 Source Requirement traceability — <one-line reason>
456
477
  - [PASS|FAIL|N/A] S17 Improvement measurability — <one-line reason>
457
478
  - [PASS|FAIL|N/A] S18 Stateless-pickup dry-run — <one-line reason>
479
+ - [PASS|FAIL|N/A] S19 Branch Plan derivation — <one-line reason>
458
480
 
459
481
  ### Feasibility Gates (omit this section when --spec-only)
460
482
  - [PASS|FAIL|N/A] F1 Issue type label exists in repo — <one-line reason>
@@ -100,6 +100,21 @@ Scenario: <name>
100
100
  Human confirmation replaces the automated annotation with a bare key or
101
101
  `Confirmed: <env>`. Skip only for doc/config/type-only issues.]
102
102
 
103
+ ## Branch Plan
104
+ [GENERATED, never hand-authored. Render only when the issue has a Target
105
+ Backend Environment; omit entirely when `runtime_behavior_change = false`
106
+ (doc-only / config-only / type-only) or for an Epic/container — absence is
107
+ correct there. Derive per the `derived-branch-plan` rule: resolve the
108
+ environment, map it forward through `.lisa.config.json` `deploy.branches`,
109
+ and prove the branch exists on the remote. Do not accept caller-supplied
110
+ branches; recompute them. Exactly three lines:
111
+ Branch from: <branch>
112
+ PR into: <branch>
113
+ Derived from: Target Backend Environment <env> via .lisa.config.json deploy.branches
114
+ Both fields name the same branch by construction. A missing, ambiguous, or
115
+ non-unique mapping, or a branch absent from the remote, STOPS the write —
116
+ never default to `main` or the remote default to keep the write alive.]
117
+
103
118
  ## Sign-in Required
104
119
  [Include this section ONLY if the work touches authenticated surfaces.
105
120
  Specify: the account/role, where to get the credentials (1Password item,
@@ -107,6 +107,12 @@ Using the general-purpose agent in Team Lead session, **determine the base branc
107
107
  - The only normalization is built-in `prod` ↔ `production`, and only when exactly one of those keys exists in `deploy.branches`; normalize to that configured key. No other aliases exist.
108
108
  - Never infer from arbitrary branch text, URL paths or query strings, or substrings inside other words or hostname labels. Multiple conflicting signals after normalization **stop** the flow. If there are no signals, resolve the remote default branch (`gh repo view --json defaultBranchRef -q .defaultBranchRef.name`, or `git remote set-head origin -a` then `origin/HEAD`). When it reverse-maps uniquely, write the env-bearing `Assumption:` form; when the reverse-map is not unique, write the branch-only form and continue on the remote default without inventing an environment or blocking solely for that ambiguity. Record the fallback assumption in the plan/tracker artifact.
109
109
  2. **Map the resolved environment to a base branch** through `.lisa.config.json` `deploy.branches` — the forward direction of the env-keyed `done` resolution. The selected exact configured key must map uniquely, and the mapped branch must exist on the remote. A missing/ambiguous mapping or remote branch **stops** the flow; never guess or silently fall back.
110
+ - **Reconcile the ticket's `## Branch Plan` (`derived-branch-plan` rule).** The rendered plan is derived output, never input: **recompute** it here from current config and the remote — the mapping you just resolved — and compare. Four arms, no fifth:
111
+ - **Match** → proceed.
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
+ - **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
+ - **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.
110
116
  3. **Establish the feature branch off the latest base, conflict-free:**
111
117
  - `git fetch origin`.
112
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.
@@ -95,6 +95,7 @@ Per-type content requirements are defined once in the vendor-neutral `work-item-
95
95
  | S16 Source Requirement traceability | `product-clarity` | true |
96
96
  | S17 Improvement measurability | `acceptance-criteria` | true |
97
97
  | S18 Stateless-pickup dry-run | `product-clarity` | true |
98
+ | S19 Branch Plan derivation | `technical` | false |
98
99
  | F1 Issue type valid in project | `structural` | false |
99
100
  | F2 Epic parent exists and is an Epic | `structural` | false |
100
101
  | F3 Linked tickets exist | `structural` | false |
@@ -301,6 +302,26 @@ The autonomy gate, run last, on every build-ready leaf (use the S15 classificati
301
302
 
302
303
  Zero questions → PASS. Any question → FAIL, with each question listed verbatim as its own remediation line — these are exactly the clarifying comments the caller posts to the source. The structure gates are proxies; this gate checks the readiness property itself: `ready` means a stateless agent can drive this item to its terminal state with zero human clarification (see `work-item-definition-of-ready`).
303
304
 
305
+ #### S19 — Branch Plan derivation
306
+
307
+ Enforces the `derived-branch-plan` rule: the `Branch Plan` section (`h2.` / `##` in proposed text, or an ADF heading in live JIRA) is **derived only** — recompute it from current config and compare; never trust the rendered branches or read them as input. Resolve the environment under the S8 grammar, map that exact configured key forward through `.lisa.config.json` `deploy.branches`, and require the mapped branch on the remote.
308
+
309
+ | Case | Verdict |
310
+ |---|---|
311
+ | `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
+ | Exempt work **carrying** a `Branch Plan` | **FAIL** — hand-authored branches on work that declared no runtime target |
313
+ | Plan matches the recomputed plan | PASS |
314
+ | Plan conflicts with the recomputed plan | **FAIL** |
315
+ | Plan missing the `Derived from:` provenance line and disagreeing | **FAIL** — treated as hand-authored |
316
+ | `Branch from` and `PR into` name two different branches | **FAIL** — malformed; they are the same branch by construction |
317
+ | Derivation hits a stop condition (env absent from `deploy.branches`, ambiguous / non-unique mapping, branch missing on the remote) | **FAIL** — the same stop `lisa-implement` takes; never default to `main` or the remote default |
318
+ | Proposed spec (pre-write) for applicable work with no plan | **FAIL** — `lisa-jira-write-ticket` must render it before the write |
319
+ | Live **legacy** ticket for applicable work with no plan | `N/A` with a repair note — routed to claim time, where `lisa-implement` writes the derived assumption as a comment and proceeds |
320
+
321
+ Failing a proposed spec is free; failing every existing ticket would turn a legacy queue red for a section no human had a way to add, so legacy absence is repaired at claim time instead.
322
+
323
+ FAIL names both plans (rendered and recomputed) and points the remediation at **the environment, never the branch** — e.g. `"Branch Plan conflicts with the environment mapping. Rendered 'Branch from: release/staging'; recomputed from Target Backend Environment 'production' via deploy.branches → 'main'. Correct the environment, not the branch — the branches are derived. Then re-render."` Never silently choose between the two plans.
324
+
304
325
  ### Feasibility Gates (require JIRA lookups; skip in dry-run if requested)
305
326
 
306
327
  #### F1 — Issue type valid in project
@@ -388,6 +409,7 @@ Output is a single fenced text block. Callers parse it; do not add free-form pro
388
409
  - [PASS|FAIL|N/A] S16 Source Requirement traceability — <one-line reason>
389
410
  - [PASS|FAIL|N/A] S17 Improvement measurability — <one-line reason>
390
411
  - [PASS|FAIL|N/A] S18 Stateless-pickup dry-run — <one-line reason>
412
+ - [PASS|FAIL|N/A] S19 Branch Plan derivation — <one-line reason>
391
413
 
392
414
  ### Feasibility Gates (omit this section when --spec-only)
393
415
  - [PASS|FAIL|N/A] F1 Issue type valid in project — <one-line reason>
@@ -92,6 +92,21 @@ h2. Target Backend Environment
92
92
  Human confirmation replaces the automated annotation with a bare key or
93
93
  `Confirmed: <env>`. Skip only for doc/config/type-only tickets.]
94
94
 
95
+ h2. Branch Plan
96
+ [GENERATED, never hand-authored. Render only when the ticket has a Target
97
+ Backend Environment; omit entirely when `runtime_behavior_change = false`
98
+ (doc-only / config-only / type-only) or for an Epic/container — absence is
99
+ correct there. Derive per the `derived-branch-plan` rule: resolve the
100
+ environment, map it forward through `.lisa.config.json` `deploy.branches`,
101
+ and prove the branch exists on the remote. Do not accept caller-supplied
102
+ branches; recompute them. Exactly three lines:
103
+ Branch from: <branch>
104
+ PR into: <branch>
105
+ Derived from: Target Backend Environment <env> via .lisa.config.json deploy.branches
106
+ Both fields name the same branch by construction. A missing, ambiguous, or
107
+ non-unique mapping, or a branch absent from the remote, STOPS the write —
108
+ never default to `main` or the remote default to keep the write alive.]
109
+
95
110
  h2. Sign-in Required
96
111
  [Include this section ONLY if the work touches authenticated surfaces.
97
112
  Specify: the account/role to sign in as, where to get the credentials
@@ -65,18 +65,40 @@ Error: no Linear access substrate available. Authenticate the Linear MCP or set
65
65
  All GraphQL calls use:
66
66
 
67
67
  ```bash
68
+ # Resolve the key through the chokepoint before giving up on the environment.
69
+ # Without this rung a project that keeps its credentials in Bitwarden, Doppler,
70
+ # or AWS has no path to the key at all — the ladder stopped at rung one, which
71
+ # is also what left `/lisa:setup:linear` reading an OS keychain directly with
72
+ # nowhere to migrate to. Mirrors `atlassian-access` and `notion-access`.
73
+ read_linear_key() {
74
+ [ -n "${LINEAR_API_KEY:-}" ] && { echo "$LINEAR_API_KEY"; return; }
75
+ local resolver
76
+ for resolver in .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs \
77
+ .agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs; do
78
+ if [ -f "$resolver" ]; then
79
+ local via_lisa
80
+ via_lisa=$(node "$resolver" get LINEAR_API_KEY 2>/dev/null) \
81
+ && [ -n "$via_lisa" ] && { echo "$via_lisa"; return; }
82
+ break
83
+ fi
84
+ done
85
+ return 1
86
+ }
87
+
68
88
  linear_graphql() {
69
89
  local query="$1"
70
90
  local variables="${2:-{}}"
71
- [ -n "$LINEAR_API_KEY" ] || {
72
- echo "Error: LINEAR_API_KEY is not set." >&2
91
+ local key
92
+ key=$(read_linear_key) || {
93
+ echo "Error: no Linear API key. Set LINEAR_API_KEY, or store it as" >&2
94
+ echo "LINEAR_API_KEY in this project's secrets provider." >&2
73
95
  return 1
74
96
  }
75
97
  jq -n --arg query "$query" --argjson variables "$variables" \
76
98
  '{query:$query, variables:$variables}' |
77
99
  curl -sS -X POST "https://api.linear.app/graphql" \
78
100
  -H "Content-Type: application/json" \
79
- -H "Authorization: '"$LINEAR_API_KEY"'" \
101
+ -H "Authorization: $key" \
80
102
  --data-binary @-
81
103
  }
82
104
  ```
@@ -96,6 +96,7 @@ Per-type content requirements are defined once in the vendor-neutral `work-item-
96
96
  | S16 Source Requirement traceability | `product-clarity` | true |
97
97
  | S17 Improvement measurability | `acceptance-criteria` | true |
98
98
  | S18 Stateless-pickup dry-run | `product-clarity` | true |
99
+ | S19 Branch Plan derivation | `technical` | false |
99
100
  | F1 Issue type valid in team | `structural` | false |
100
101
  | F2 Project parent exists and is in same team | `structural` | false |
101
102
  | F3 Linked items exist | `structural` | false |
@@ -303,6 +304,26 @@ The autonomy gate, run last, on every build-ready leaf (use the S15 classificati
303
304
 
304
305
  Zero questions → PASS. Any question → FAIL, with each question listed verbatim as its own remediation line — these are exactly the clarifying comments the caller posts to the source. The structure gates are proxies; this gate checks the readiness property itself: `ready` means a stateless agent can drive this item to its terminal state with zero human clarification (see `work-item-definition-of-ready`).
305
306
 
307
+ #### S19 — Branch Plan derivation
308
+
309
+ Enforces the `derived-branch-plan` rule: the `## Branch Plan` section is **derived only** — recompute it from current config and compare; never trust the rendered branches or read them as input. Resolve the environment under the S8 grammar, map that exact configured key forward through `.lisa.config.json` `deploy.branches`, and require the mapped branch on the remote.
310
+
311
+ | Case | Verdict |
312
+ |---|---|
313
+ | `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
+ | Exempt work **carrying** a `## Branch Plan` | **FAIL** — hand-authored branches on work that declared no runtime target |
315
+ | Plan matches the recomputed plan | PASS |
316
+ | Plan conflicts with the recomputed plan | **FAIL** |
317
+ | Plan missing the `Derived from:` provenance line and disagreeing | **FAIL** — treated as hand-authored |
318
+ | `Branch from` and `PR into` name two different branches | **FAIL** — malformed; they are the same branch by construction |
319
+ | Derivation hits a stop condition (env absent from `deploy.branches`, ambiguous / non-unique mapping, branch missing on the remote) | **FAIL** — the same stop `lisa-implement` takes; never default to `main` or the remote default |
320
+ | Proposed spec (pre-write) for applicable work with no plan | **FAIL** — `lisa-linear-write-issue` must render it before the write |
321
+ | Live **legacy** Issue for applicable work with no plan | `N/A` with a repair note — routed to claim time, where `lisa-implement` writes the derived assumption as a comment and proceeds |
322
+
323
+ Failing a proposed spec is free; failing every existing Issue would turn a legacy queue red for a section no human had a way to add, so legacy absence is repaired at claim time instead.
324
+
325
+ FAIL names both plans (rendered and recomputed) and points the remediation at **the environment, never the branch** — e.g. `"Branch Plan conflicts with the environment mapping. Rendered 'Branch from: release/staging'; recomputed from Target Backend Environment 'production' via deploy.branches → 'main'. Correct the environment, not the branch — the branches are derived. Then re-render."` Never silently choose between the two plans.
326
+
306
327
  ### Feasibility Gates (require Linear lookups; skip in dry-run if requested)
307
328
 
308
329
  #### F1 — Issue type valid in team
@@ -394,6 +415,7 @@ Output is a single fenced text block. Callers parse it; do not add free-form pro
394
415
  - [PASS|FAIL|N/A] S16 Source Requirement traceability — <one-line reason>
395
416
  - [PASS|FAIL|N/A] S17 Improvement measurability — <one-line reason>
396
417
  - [PASS|FAIL|N/A] S18 Stateless-pickup dry-run — <one-line reason>
418
+ - [PASS|FAIL|N/A] S19 Branch Plan derivation — <one-line reason>
397
419
 
398
420
  ### Feasibility Gates (omit when --spec-only)
399
421
  - [PASS|FAIL|N/A] F1 Issue type valid in team — <one-line reason>
@@ -112,6 +112,21 @@ Linear descriptions are markdown (NOT Jira wiki markup — no `h2.` headings, us
112
112
  Human confirmation replaces the automated annotation with a bare key or
113
113
  `Confirmed: <env>`. Skip only for doc/config/type-only items.]
114
114
 
115
+ ## Branch Plan
116
+ [GENERATED, never hand-authored. Render only when the item has a Target
117
+ Backend Environment; omit entirely when `runtime_behavior_change = false`
118
+ (doc-only / config-only / type-only) or for a Project/container — absence is
119
+ correct there. Derive per the `derived-branch-plan` rule: resolve the
120
+ environment, map it forward through `.lisa.config.json` `deploy.branches`,
121
+ and prove the branch exists on the remote. Do not accept caller-supplied
122
+ branches; recompute them. Exactly three lines:
123
+ Branch from: <branch>
124
+ PR into: <branch>
125
+ Derived from: Target Backend Environment <env> via .lisa.config.json deploy.branches
126
+ Both fields name the same branch by construction. A missing, ambiguous, or
127
+ non-unique mapping, or a branch absent from the remote, STOPS the write —
128
+ never default to `main` or the remote default to keep the write alive.]
129
+
115
130
  ## Sign-in Required
116
131
  [Include this section ONLY if the work touches authenticated surfaces.
117
132
  Specify: the account/role to sign in as, where to get the credentials
@@ -116,9 +116,11 @@ Every provider has a description field — Bitwarden `note`, 1Password notes, AW
116
116
 
117
117
  These are two different rules and should not be conflated:
118
118
 
119
- - **Rule A — the note must exist and be well-formed.** Universal: every secret, every provider, every surface. Enforced **statically** by `verify` and by `doctor`. Nothing to do with agents.
119
+ - **Rule A — the note must exist and be well-formed.** Universal: every secret, every provider, every surface. Enforced **statically** by `verify` and by `doctor`, and it **blocks** — a malformed note is an error, not a warning. Nothing to do with agents.
120
120
  - **Rule B — an agent must read the note before first use.** Runtime, and **only in lanes where a consumer has latitude**. An agent could do anything with a write-scoped token, and the note is what bounds it. A reviewed workflow step resolving one credential by exact name has no latitude, so gating it there is ceremony that can only fail-closed and never inform.
121
121
 
122
+ Rule A blocks because a warning enforced nothing. Both checks previously tested only whether a note was *present*, and reported a warning either way, so a vault of empty notes reported clean — which is precisely why the notes stayed empty. A check that cannot fail is a check nobody acts on.
123
+
122
124
  Format — first line prose, then `key: value` lines:
123
125
 
124
126
  ```text
@@ -129,6 +131,22 @@ ci: yes - injected by <mechanism>
129
131
  docs: <path>
130
132
  ```
131
133
 
134
+ ### What "well-formed" means, exactly
135
+
136
+ `scripts/note-format.mjs` is the one validator; `verify` and `doctor` both call it, so they cannot reach different verdicts about the same note. It **blocks** on:
137
+
138
+ | Defect | Why it blocks |
139
+ | --- | --- |
140
+ | `missing-note` | No note at all, empty, or only whitespace. |
141
+ | `no-prose` | Every line is a `key: value` line, so nothing states what the credential *is*. |
142
+ | `empty-field` | A field with nothing after the colon — it promises a fact the reader cannot find. |
143
+ | `empty-tool-line` | A `tool:` line naming no tool: it asks for an install and supplies nothing. |
144
+ | `bad-tool-name` | A `tool:` entry that is not a bare name, so the reader drops it silently. |
145
+
146
+ It **warns**, without blocking, on `stray-separator` — a trailing or doubled comma in a tool list. The reader handles it; failing a vault over punctuation would only teach operators that the check is noise.
147
+
148
+ Deliberately **not** checked: which fields a note carries, and how informative the prose is. The documented format mandates neither, and enforcing beyond the prose is the same defect as prose beyond the enforcement, pointing the other way. A line is treated as prose unless the text before its colon is a single bare lowercase token — so "Attio CRM: system of record" is a sentence, not a field.
149
+
132
150
  ### `tool:` — the CLI a secret implies
133
151
 
134
152
  One key is read by more than a human. A credential and the CLI that consumes it belong together — `SONARQUBE_CLI_TOKEN` is only useful with `sonar` — so a `tool:` (or `tools: a, b`) line declares that pairing where it cannot drift from the secret:
@@ -229,7 +247,7 @@ Cache **in-process only**. Never write a resolved value to disk except through t
229
247
 
230
248
  - Every name in `require` resolves.
231
249
  - Every key matches `^[A-Z][A-Z0-9_]*$`.
232
- - No secret has an empty note.
250
+ - Every secret's note exists and is well-formed, per the table above. This is an **error**, so a vault that was passing on warnings will newly fail until its notes are written.
233
251
  - Every name in `rotating` has a resolvable bootstrap, so its replacement could be persisted.
234
252
  - **No secret is readable from two stores.** A value present in both the provider and a local cache is not a duplicate — it is **two live credentials**, one of which is untracked. This is the check most worth having: it catches drift before a deletion turns the forgotten copy into an orphan nobody can revoke.
235
253
 
@@ -19,6 +19,7 @@
19
19
 
20
20
  import { createHash } from "node:crypto";
21
21
 
22
+ import { validateNote } from "./note-format.mjs";
22
23
  import { fetchAll, fetchRotatable } from "./providers.mjs";
23
24
  import { readMaterialized } from "./resolve-secret.mjs";
24
25
  import { readConfig } from "./surfaces.mjs";
@@ -118,20 +119,23 @@ export function checkNaming(provider, report) {
118
119
  }
119
120
 
120
121
  /**
121
- * Assert every secret carries a usage note.
122
+ * Assert every secret carries a usage note in the documented format.
123
+ *
124
+ * This blocks rather than warns, and the promotion is the point. The contract
125
+ * has always said the note "must exist and be well-formed", enforced statically
126
+ * by `doctor` — but a warning enforces nothing, so a vault of empty notes
127
+ * reported clean and stayed empty. A check that never fails is a check nobody
128
+ * acts on.
129
+ *
130
+ * The grammar lives in `note-format.mjs` so this and `resolve-secret.mjs verify`
131
+ * cannot disagree about what a well-formed note is.
122
132
  * @param {Map<string, object>} provider Provider view.
123
133
  * @param {Function} report Finding collector.
124
134
  */
125
135
  export function checkNotes(provider, report) {
126
136
  for (const [name, entry] of provider) {
127
- if (!entry.note?.trim()) {
128
- report(
129
- "warn",
130
- name,
131
- "has no usage note. An agent cannot learn this credential's scope " +
132
- "without one, and inferring it from the name is exactly the guess " +
133
- "that writes to the wrong system"
134
- );
137
+ for (const defect of validateNote(entry?.note)) {
138
+ report(defect.level, name, defect.message);
135
139
  }
136
140
  }
137
141
  }