@codyswann/lisa 4.22.6 → 4.22.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all/copy-overwrite/scripts/check-release-package-identity.mjs +319 -0
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +6 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +28 -14
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/sync/lifecycle-defaults.d.ts +21 -0
- package/dist/sync/lifecycle-defaults.d.ts.map +1 -1
- package/dist/sync/lifecycle-defaults.js +21 -1
- package/dist/sync/lifecycle-defaults.js.map +1 -1
- package/dist/sync/registry.d.ts.map +1 -1
- package/dist/sync/registry.js +7 -1
- package/dist/sync/registry.js.map +1 -1
- package/package.json +3 -2
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +6 -5
- package/plugins/lisa/.codex-plugin/skills/lisa-github-prd-intake/SKILL.md +6 -5
- package/plugins/lisa/.codex-plugin/skills/lisa-github-sync/SKILL.md +2 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-github-write-prd/SKILL.md +7 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-build-intake/SKILL.md +6 -5
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-sync/SKILL.md +3 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +6 -5
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-evidence/SKILL.md +12 -11
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-prd-intake/SKILL.md +6 -5
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-sync/SKILL.md +36 -20
- package/plugins/lisa/.codex-plugin/skills/lisa-tracker-sync/SKILL.md +22 -2
- package/plugins/lisa/rules/reference/config-resolution.md +30 -3
- package/plugins/lisa/scripts/resolve-lifecycle-role.mjs +340 -0
- package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +6 -5
- package/plugins/lisa/skills/lisa-github-prd-intake/SKILL.md +6 -5
- package/plugins/lisa/skills/lisa-github-sync/SKILL.md +2 -0
- package/plugins/lisa/skills/lisa-github-write-prd/SKILL.md +7 -3
- package/plugins/lisa/skills/lisa-jira-build-intake/SKILL.md +6 -5
- package/plugins/lisa/skills/lisa-jira-sync/SKILL.md +3 -1
- package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +6 -5
- package/plugins/lisa/skills/lisa-linear-evidence/SKILL.md +12 -11
- package/plugins/lisa/skills/lisa-linear-prd-intake/SKILL.md +6 -5
- package/plugins/lisa/skills/lisa-linear-sync/SKILL.md +36 -20
- package/plugins/lisa/skills/lisa-tracker-sync/SKILL.md +22 -2
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/resolve-lifecycle-role.mjs +340 -0
- package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +6 -5
- package/plugins/lisa-agy/skills/lisa-github-prd-intake/SKILL.md +6 -5
- package/plugins/lisa-agy/skills/lisa-github-sync/SKILL.md +2 -0
- package/plugins/lisa-agy/skills/lisa-github-write-prd/SKILL.md +7 -3
- package/plugins/lisa-agy/skills/lisa-jira-build-intake/SKILL.md +6 -5
- package/plugins/lisa-agy/skills/lisa-jira-sync/SKILL.md +3 -1
- package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +6 -5
- package/plugins/lisa-agy/skills/lisa-linear-evidence/SKILL.md +12 -11
- package/plugins/lisa-agy/skills/lisa-linear-prd-intake/SKILL.md +6 -5
- package/plugins/lisa-agy/skills/lisa-linear-sync/SKILL.md +36 -20
- package/plugins/lisa-agy/skills/lisa-tracker-sync/SKILL.md +22 -2
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/rules/reference/config-resolution.md +30 -3
- package/plugins/lisa-copilot/scripts/resolve-lifecycle-role.mjs +340 -0
- package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +6 -5
- package/plugins/lisa-copilot/skills/lisa-github-prd-intake/SKILL.md +6 -5
- package/plugins/lisa-copilot/skills/lisa-github-sync/SKILL.md +2 -0
- package/plugins/lisa-copilot/skills/lisa-github-write-prd/SKILL.md +7 -3
- package/plugins/lisa-copilot/skills/lisa-jira-build-intake/SKILL.md +6 -5
- package/plugins/lisa-copilot/skills/lisa-jira-sync/SKILL.md +3 -1
- package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +6 -5
- package/plugins/lisa-copilot/skills/lisa-linear-evidence/SKILL.md +12 -11
- package/plugins/lisa-copilot/skills/lisa-linear-prd-intake/SKILL.md +6 -5
- package/plugins/lisa-copilot/skills/lisa-linear-sync/SKILL.md +36 -20
- package/plugins/lisa-copilot/skills/lisa-tracker-sync/SKILL.md +22 -2
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +30 -3
- package/plugins/lisa-cursor/scripts/resolve-lifecycle-role.mjs +340 -0
- package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +6 -5
- package/plugins/lisa-cursor/skills/lisa-github-prd-intake/SKILL.md +6 -5
- package/plugins/lisa-cursor/skills/lisa-github-sync/SKILL.md +2 -0
- package/plugins/lisa-cursor/skills/lisa-github-write-prd/SKILL.md +7 -3
- package/plugins/lisa-cursor/skills/lisa-jira-build-intake/SKILL.md +6 -5
- package/plugins/lisa-cursor/skills/lisa-jira-sync/SKILL.md +3 -1
- package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +6 -5
- package/plugins/lisa-cursor/skills/lisa-linear-evidence/SKILL.md +12 -11
- package/plugins/lisa-cursor/skills/lisa-linear-prd-intake/SKILL.md +6 -5
- package/plugins/lisa-cursor/skills/lisa-linear-sync/SKILL.md +36 -20
- package/plugins/lisa-cursor/skills/lisa-tracker-sync/SKILL.md +22 -2
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/rules/reference/config-resolution.md +30 -3
- package/plugins/src/base/scripts/resolve-lifecycle-role.mjs +340 -0
- package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +6 -5
- package/plugins/src/base/skills/lisa-github-prd-intake/SKILL.md +6 -5
- package/plugins/src/base/skills/lisa-github-sync/SKILL.md +2 -0
- package/plugins/src/base/skills/lisa-github-write-prd/SKILL.md +7 -3
- package/plugins/src/base/skills/lisa-jira-build-intake/SKILL.md +6 -5
- package/plugins/src/base/skills/lisa-jira-sync/SKILL.md +3 -1
- package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +6 -5
- package/plugins/src/base/skills/lisa-linear-evidence/SKILL.md +12 -11
- package/plugins/src/base/skills/lisa-linear-prd-intake/SKILL.md +6 -5
- package/plugins/src/base/skills/lisa-linear-sync/SKILL.md +36 -20
- package/plugins/src/base/skills/lisa-tracker-sync/SKILL.md +22 -2
- package/scripts/check-release-package-identity.mjs +319 -0
- package/scripts/two-channel-couplings.json +17 -4
- package/typescript/copy-overwrite/.prettierignore +1 -0
|
@@ -16,14 +16,25 @@ Reads `linear.workspace`, `linear.teamKey` from `.lisa.config.json` (with `.loca
|
|
|
16
16
|
|
|
17
17
|
Callers (planning skills, lifecycle skills) invoke this skill at:
|
|
18
18
|
|
|
19
|
-
| Milestone | What to post |
|
|
20
|
-
|
|
21
|
-
| Plan created | Plan contents (sections + ordered tasks) as a comment
|
|
22
|
-
| Implementation in progress | Branch URL + first commit
|
|
23
|
-
| PR ready for review | PR URL + summary, the implementation handoff comment,
|
|
24
|
-
| PR merged | Merge SHA + deploy environment (if known)
|
|
19
|
+
| Milestone | What to post | Suggested role |
|
|
20
|
+
|-----------|--------------|----------------|
|
|
21
|
+
| Plan created | Plan contents (sections + ordered tasks) as a comment | `ready` |
|
|
22
|
+
| Implementation in progress | Branch URL + first commit | `claimed` |
|
|
23
|
+
| PR ready for review | PR URL + summary, the implementation handoff comment | `review` — **optional; omitted by most projects, in which case there is no transition to suggest** |
|
|
24
|
+
| PR merged | Merge SHA + deploy environment (if known) | env-keyed `done`, **then run Phase 4b — mandatory when the merge target is a non-terminal env branch** |
|
|
25
|
+
|
|
26
|
+
**Roles, never literal state names.** Resolve each through the shared resolver; the state names above are whatever the project configured, and a role the project did not configure has no suggestion at all:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
resolve() {
|
|
30
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/resolve-lifecycle-role.mjs" \
|
|
31
|
+
--role "$1" --vendor linear --intent read 2>/dev/null
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**This skill SUGGESTS transitions. It never writes the lane.** That matches `lisa-jira-sync` ("suggest, but don't automatically perform") and `lisa-github-sync` ("this skill never relabels"), and it is what this skill's own dispatcher already advertises: `lisa-tracker-sync`'s description reads *"Suggests (never auto-transitions) the next status."* Lane writes belong to the build-intake / agent owner, which is already true on the other two trackers.
|
|
25
36
|
|
|
26
|
-
|
|
37
|
+
`--rollup` is the one exception and is documented in Phase 4b — parent derivation is its entire purpose and is separately gated.
|
|
27
38
|
|
|
28
39
|
## Input
|
|
29
40
|
|
|
@@ -82,20 +93,24 @@ When `$ARGUMENTS` includes `pr_url=<url>` for `pr-ready` or `pr-merged`, ensure
|
|
|
82
93
|
|
|
83
94
|
The PR branch/title/body identifier is the PR -> Linear side. This phase is the required Linear -> PR side.
|
|
84
95
|
|
|
85
|
-
## Phase 4 —
|
|
96
|
+
## Phase 4 — Suggest the Next State (never write it)
|
|
97
|
+
|
|
98
|
+
Name the suggested next state in the milestone comment and stop. **This skill does not set `stateId`.** The lane write belongs to `lisa-linear-build-intake` / `lisa-linear-agent`, exactly as the `status:*` write belongs to `lisa-github-build-intake` on GitHub.
|
|
86
99
|
|
|
87
|
-
|
|
100
|
+
Resolve the suggestion by role, and **say nothing when the role is unset**:
|
|
88
101
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
- `
|
|
92
|
-
|
|
102
|
+
| Milestone | Role to resolve | When the role is unset |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `plan-created` | `ready` | required — report a setup defect |
|
|
105
|
+
| `implementation-in-progress` | `claimed` | required — report a setup defect |
|
|
106
|
+
| `pr-ready` | `review` | **optional — suggest no transition; the Issue stays in `claimed`** |
|
|
107
|
+
| `pr-merged` | `done` for the PR's target env (via `deploy.branches`) | required — report a setup defect |
|
|
93
108
|
|
|
94
|
-
If
|
|
109
|
+
If a resolved state doesn't exist on the team, that is a setup defect — report it and point at `/lisa:setup:linear`. Never create a workflow state here: a state carries a `type` and a board position, and guessing either puts the Issue somewhere no human sanctioned. Equally, never *find* one: resolving a role by scanning the team for a state whose name or `type` looks right picks by board position, not intent, and the states that surface that way are the human-only lanes a project deliberately left out of its config.
|
|
95
110
|
|
|
96
|
-
|
|
111
|
+
> **Removed: `--update-state`.** This flag previously let a caller make Phase 4 write the state. It was the only lane-write path in any sync skill, and combined with a defaulted `review` role it moved Issues into review states that projects had never configured. Callers that relied on it should let the build-intake owner make the transition; `--rollup` is unaffected.
|
|
97
112
|
|
|
98
|
-
|
|
113
|
+
No single-lane verification is needed: an Issue holds exactly one workflow state by construction, so the two-lanes-at-once corruption the old label-driven lane could produce is unrepresentable. (It was not hypothetical — 16 issues carried two `status:*` labels at the time of the migration.)
|
|
99
114
|
|
|
100
115
|
## Phase 4b — Reconcile Native Auto-Close (Linear-specific)
|
|
101
116
|
|
|
@@ -116,13 +131,13 @@ When the caller passes `--rollup`, this skill **derives a parent/container's wor
|
|
|
116
131
|
|
|
117
132
|
**Resolve the child set the same way `lisa-linear-read-issue` does** — `lisa-linear-access operation: list-issues({project: <id>})` for a Project's Issues, or `lisa-linear-access operation: get-issue` per child for an Issue's sub-Issues (via `parentId`). Capture each child's workflow state. If the item has **no** children it is a leaf — rollup is N/A; behave as a normal milestone sync.
|
|
118
133
|
|
|
119
|
-
**Evaluate the required children over the env ladder `in-progress < dev < staging < production` (the ordered keys of the Linear env-keyed `done` map
|
|
134
|
+
**Evaluate the required children over the env ladder `in-progress < dev < staging < production` (the ordered keys of the Linear env-keyed `done` map) and take the first match** (canonical roles from `config-resolution`; the Linear state map is the configured `blocked`, `claimed`, optional `review`, and env-keyed `done` — resolve each by role, and treat an unset `review` as simply having no such rung):
|
|
120
135
|
|
|
121
136
|
| If among the required child leaves… | Derived parent role | Linear state |
|
|
122
137
|
|---|---|---|
|
|
123
138
|
| any child carries `Blocked` | `blocked` | `Blocked` |
|
|
124
139
|
| else **every** required child has shipped to some env (each at a `done`-map state, e.g. `On Dev`/`On Stg`/`Done`) | `done[min-env]` | the **least-advanced** env state among them (all `On Stg` → `On Stg`; mixed dev+staging → `On Dev`; all production → `Done`) |
|
|
125
|
-
| else any child has **started** (
|
|
140
|
+
| else any child has **started** (at the `claimed` state, at the `review` state where the project configures one, or shipped to an env while a sibling has not) | `claimed` | the configured `claimed` state |
|
|
126
141
|
| else (children exist, none started) | — | unchanged — parent keeps its non-ready container state |
|
|
127
142
|
|
|
128
143
|
- **Blocked dominates** — one blocked child surfaces `Blocked` on the parent even while siblings progress. It never says *which* child or *which kind* of hold; run `scripts/rollup-blocker-classification.mjs` over the resolved child graph and carry its per-class report — blocking leaf, path, and who must act — into the rollup note. A non-zero exit means it classified nothing; that is a failure to report, never an all-clear. See `leaf-only-lifecycle` → **Classifying a hold**.
|
|
@@ -131,13 +146,14 @@ When the caller passes `--rollup`, this skill **derives a parent/container's wor
|
|
|
131
146
|
- **Recursive** — a Project reaches an env only when its Issues have themselves rolled up to at least that env. Evaluate bottom-up.
|
|
132
147
|
- **Never roll a parent into the `ready` state** — `ready` is leaf-only. Rollup only moves the parent between non-ready container states.
|
|
133
148
|
|
|
134
|
-
**Single-environment collapse (this repo).** The env rungs resolve via the env-keyed `done` logic in `config-resolution`. In this repo `deploy.branches` declares only `production: main`, so `done` collapses to the single `Done` state, the only env rung is production, and the lifecycle is `
|
|
149
|
+
**Single-environment collapse (this repo).** The env rungs resolve via the env-keyed `done` logic in `config-resolution`. In this repo `deploy.branches` declares only `production: main`, so `done` collapses to the single `Done` state, the only env rung is production, and the lifecycle is `ready → claimed → done` with **no** dev/staging promotion hops and no configured `review` rung; the rollup never resolves a dev or staging `done`. Multi-environment projects keep the env-keyed map and roll a parent up to the intermediate env states.
|
|
135
150
|
|
|
136
151
|
**Apply the derived state** via `lisa-linear-access operation: save-issue` (Project or Issue), setting the parent's `stateId` to the derived role. Post an idempotent rollup comment naming the derived state and the child tally. Because the terminal `done` state is itself typed `completed`, a parent rolled to terminal is natively closed by the same write — there is no second closure step. **Safe default:** if the derived terminal cannot be resolved (ambiguous required-set or unresolvable env `done`), do not guess — post the derived suggestion as a comment and leave the parent's state untouched.
|
|
137
152
|
|
|
138
153
|
## Rules
|
|
139
154
|
|
|
140
|
-
- Never transition
|
|
155
|
+
- Never transition a leaf Issue's workflow `state` from this skill at all. `--rollup` (parent derivation, per the `leaf-only-lifecycle` rule) is the only write path that remains; every other milestone SUGGESTS a transition in its comment and nothing more. The state is the lifecycle lane, so a write here would move the item in the build queue.
|
|
156
|
+
- Never resolve a role by searching the team's states. Only a name the project configured may be written; a `type`- or position-derived match may inform a read and must never supply a write target (`config-resolution`, R2).
|
|
141
157
|
- Rollup derives a *parent's* workflow state from its children and never rolls a parent into the human-owned ready lane (never `$READY`). It cites the `leaf-only-lifecycle` rule by slug rather than restating the state machine.
|
|
142
158
|
- Never post empty or minimal comments — if a milestone has no meaningful content, skip the post.
|
|
143
159
|
- Do not delete prior milestone comments. They are the audit trail.
|
|
@@ -21,7 +21,27 @@ See the `config-resolution` rule for configuration and dispatch table.
|
|
|
21
21
|
- Anything else → stop and report `"Unknown tracker '<value>' in .lisa.config.json. Expected 'jira', 'github', or 'linear'."`
|
|
22
22
|
3. Pass through the output.
|
|
23
23
|
|
|
24
|
-
`$ARGUMENTS` is forwarded verbatim, including the optional `--rollup` flag (see "Parent status rollup" below),
|
|
24
|
+
`$ARGUMENTS` is forwarded verbatim, including the optional `--rollup` flag (see "Parent status rollup" below), `pr_url=<url>`, and `merge_sha=<sha>`. The shim never interprets these — the vendor skill does.
|
|
25
|
+
|
|
26
|
+
> **There is no per-milestone lane-write flag on any vendor.** This paragraph previously documented `--update-label` on GitHub and `--update-state` on Linear. `--update-label` was never implemented — `lisa-github-sync` says flatly *"This skill never relabels"* — so a caller passing it got a silent no-op on GitHub and a real write on Linear for the equivalent flag. Both are gone. `--rollup` is the only write path a sync skill has.
|
|
27
|
+
|
|
28
|
+
## Tracker status vocabulary — binding on all three arms
|
|
29
|
+
|
|
30
|
+
Every suggested or performed transition names **only a role the project configured**, never a status, state or label discovered from the tracker's live workflow (transition lists, board columns, `type`-derived matches, other tickets). This binds a lead performing tracker writes exactly as it binds a subagent.
|
|
31
|
+
|
|
32
|
+
Two consequences the vendor arms must not restate differently:
|
|
33
|
+
|
|
34
|
+
- **A milestone whose role is unset gets a comment, not a transition.** `review` and the `qa.*` roles are optional and carry **no default** — omitting one means the project does not run that step, and the lifecycle skips it. "Unset" must never resolve to a built-in name; that is the difference between "not customized" and "we deliberately don't do this".
|
|
35
|
+
- **A fallback may inform a read; it may never supply a write target.** Where a vendor exposes one (Linear resolves states by `type`), it selects by board position rather than intent — so on a board carrying more than one plausible state it returns whichever sits earliest, which is exactly the human-only lane a project left out of its config on purpose.
|
|
36
|
+
|
|
37
|
+
Resolve roles through the shared resolver rather than an inlined helper:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/resolve-lifecycle-role.mjs" \
|
|
41
|
+
--role <role> --vendor <jira|linear|github> --intent <read|write> [--env <env>]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Exit `0` with a value means configured; exit `0` with **empty** output means an optional role is unset — skip the transition; exit `2` means a required role is unset or a write was refused a fallback value.
|
|
25
45
|
|
|
26
46
|
If `$ARGUMENTS` is empty, all vendor skills auto-detect a ticket reference from the active plan file (most recently modified `.md` in `plans/`).
|
|
27
47
|
|
|
@@ -65,6 +85,6 @@ This is the reverse half of `lisa-git-submit-pr`'s PR body linkage. A PR that me
|
|
|
65
85
|
## Rules
|
|
66
86
|
|
|
67
87
|
- Idempotent updates — running sync at the same milestone twice should not produce duplicate comments. Vendor skills enforce this.
|
|
68
|
-
- Never
|
|
88
|
+
- Never transition a leaf's lane from a sync milestone on any vendor. The canonical signal differs — the `status:*` label on GitHub, the workflow status on JIRA, the native workflow state on Linear — but the owner is the same everywhere: build-intake / the vendor agent. Every arm only suggests. `--rollup` (parent derivation) is the sole exception, and it derives the parent from its children rather than advancing a leaf.
|
|
69
89
|
- Parent rollup derives state from children per the `leaf-only-lifecycle` rule; it never sets a parent to `ready` and never resolves a dev/staging `done` in this single-environment repo.
|
|
70
90
|
- Pull request backlinks are mandatory when `pr_url=<url>` is present: native first, managed-comment fallback, never silently dropped.
|
|
@@ -352,7 +352,7 @@ When `github.projects.v2` is present, later setup/doctor and writer preflight va
|
|
|
352
352
|
|-------|---------------|-------|
|
|
353
353
|
| `linear.workspace` | `tracker = "linear"`, `source = "linear"`, or any `linear-*` skill is invoked | Linear workspace slug (e.g. `acme`). |
|
|
354
354
|
| `linear.teamKey` | `tracker = "linear"` | Linear team key (e.g. `ENG`). The team owns the destination Issues. For source mode, projects are workspace-scoped or team-scoped per the URL passed. |
|
|
355
|
-
| `linear.workflow` | `tracker = "linear"` | Workflow **state** name per build lifecycle role — the Linear analogue of `jira.workflow`, not of `github.labels`. Defaults `{ ready: "Ready", claimed: "In Progress",
|
|
355
|
+
| `linear.workflow` | `tracker = "linear"` | Workflow **state** name per build lifecycle role — the Linear analogue of `jira.workflow`, not of `github.labels`. Defaults `{ ready: "Ready", claimed: "In Progress", blocked: "Blocked", done: { dev: "On Dev", staging: "On Stg", production: "Done" } }` — **no `review` key**, because it is optional and a default would be materialized into the project's config by `lisa sync`. **`ready` is a dedicated state, never the team's default** — see below. Resolve and verify with `/lisa:setup:linear`. |
|
|
356
356
|
| `linear.labels` | `tracker = "linear"` or `source = "linear"` | **Markers and the PRD lane only.** `build.human_needed` (default `human-needed`) and the `prd.*` map. The build lifecycle does **not** live here — see `linear.workflow`. |
|
|
357
357
|
|
|
358
358
|
##### Why Linear uses states, not labels
|
|
@@ -439,13 +439,40 @@ Every lifecycle skill operates on a fixed set of **roles** (`ready`, `claimed`,
|
|
|
439
439
|
|---|---|---|---|---|
|
|
440
440
|
| `ready` | Human signal "this is buildable; agent may claim" | `Ready` (status) | `Ready` (state) | `status:ready` (label) |
|
|
441
441
|
| `claimed` | Agent has picked the item up | `In Progress` (status) | `In Progress` (state) | `status:in-progress` (label) |
|
|
442
|
-
| `review` | Optional post-build review hold, when a tracker/project still uses one |
|
|
442
|
+
| `review` | Optional post-build review hold, when a tracker/project still uses one | **no default** | **no default** | **no default** |
|
|
443
443
|
| `blocked` | Agent stopped on triage ambiguities or external blocker | `Blocked` (status) | `Blocked` (state) | `status:blocked` (label) |
|
|
444
444
|
| `done` | Terminal state for this work, **env-keyed** | map of env → status | map of env → state | map of env → label |
|
|
445
445
|
|
|
446
446
|
**JIRA and Linear resolve roles to native workflow statuses/states** (`jira.workflow`, `linear.workflow`); **GitHub resolves them to labels** (`github.labels.build`) because GitHub Issues has no workflow-state field at all. A role transition is therefore a *state move* on JIRA and Linear, and a *label swap* on GitHub. Skills operate on roles and never hardcode either form.
|
|
447
447
|
|
|
448
|
-
|
|
448
|
+
### R1 — an absent OPTIONAL role means SKIP, never a default
|
|
449
|
+
|
|
450
|
+
`ready`, `claimed`, `blocked` and `done` are **required**; omitting one is a setup defect and a skill must say so. `review`, the `qa.*` roles and `human_needed` are **optional and carry no built-in default on any vendor**. Omitting one means the project does not run that step, and every lifecycle skill skips the transition — the item simply stays in its current role.
|
|
451
|
+
|
|
452
|
+
A default on an optional role breaks this twice over. Vendor default maps are `defaultValue`s in `sync/registry`, so `lisa sync` **materializes** them into a project's `.lisa.config.json` — a project that deliberately omitted `review` finds it written back in. And as a resolution fallback, a default makes "unset" indistinguishable from "not customized", so no project can express *"we have no agent review step."*
|
|
453
|
+
|
|
454
|
+
Measured downstream: a project bound no `review` role — its policy was that a PR-open ticket stays in `claimed` until it reaches an environment — and agents moved two issues into a human-only review state regardless, because every layer that could have honoured the omission supplied a default instead.
|
|
455
|
+
|
|
456
|
+
GitHub has always been correct here (`BUILD_LABEL_DEFAULTS` seeds no `review`), and `lisa-jira-evidence/scripts/post-evidence.sh` has always implemented it correctly (`REVIEW=""`, explicit skip branch). The default maps for JIRA and Linear now agree with them.
|
|
457
|
+
|
|
458
|
+
### R2 — a fallback may inform a READ, never supply a WRITE target
|
|
459
|
+
|
|
460
|
+
Where a vendor exposes a fallback for a missing role — Linear resolves states by `type`, since Linear states carry one — it may be used to *read*. It must **never** supply a value to write.
|
|
461
|
+
|
|
462
|
+
Such a fallback selects by **board position**, not intent. On a board carrying more than one plausible state it returns whichever sits earliest, and the states that surface that way are precisely the human-only lanes a project kept out of its config on purpose. Measured on one board: with `blocked` and `claimed` already bound, the lowest-position unbound `started` state was a human-only review lane, and that is exactly where two issues landed.
|
|
463
|
+
|
|
464
|
+
`ready` has no fallback at all, in either direction — see below.
|
|
465
|
+
|
|
466
|
+
### The single resolver
|
|
467
|
+
|
|
468
|
+
Every skill resolves roles through `${CLAUDE_PLUGIN_ROOT}/scripts/resolve-lifecycle-role.mjs`, never an inlined `read_role()` helper. Twelve skills previously inlined one and produced **eleven distinct implementations**; that copy-paste is what let the vendors drift apart on R1.
|
|
469
|
+
|
|
470
|
+
```bash
|
|
471
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/resolve-lifecycle-role.mjs" \
|
|
472
|
+
--role <role> --vendor <jira|linear|github> --intent <read|write> [--env <env>]
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Exit `0` with a value → configured. Exit `0` with **empty** output → an optional role is unset; skip the transition. Exit `2` → a required role is unset, or a write was refused a fallback value.
|
|
449
476
|
|
|
450
477
|
**`ready` must never resolve to the team's DEFAULT state.** `Todo` is where Linear puts a brand-new issue, so using it for `ready` inverts the gate: the lane stops meaning "a human flipped this to build-ready" and starts meaning "nobody has touched this". Measured on the first team migrated: 20 issues in the lane, only 8 ever explicitly marked ready. JIRA avoids this because `jira.workflow.ready` is a dedicated `Ready` status while a fresh ticket lands in the project default.
|
|
451
478
|
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Lifecycle-role resolution — the single resolver for every vendor.
|
|
4
|
+
*
|
|
5
|
+
* ## The defect this replaces
|
|
6
|
+
*
|
|
7
|
+
* Twelve skills inlined their own `read_role()` bash helper. Hashing the
|
|
8
|
+
* function bodies produced ELEVEN distinct implementations — only
|
|
9
|
+
* `lisa-linear-build-intake` and `lisa-linear-evidence` agreed. Copy-paste with
|
|
10
|
+
* no shared source is why the three vendors drifted apart on the same contract.
|
|
11
|
+
*
|
|
12
|
+
* The drift was not cosmetic. `lisa-linear-evidence` resolved
|
|
13
|
+
* `read_role review "In Review"` — a NON-EMPTY default — while
|
|
14
|
+
* `lisa-jira-evidence/scripts/post-evidence.sh` resolved the same role with
|
|
15
|
+
* `REVIEW=""` and an explicit skip branch. Same role, opposite behaviour, on the
|
|
16
|
+
* question of whether an unconfigured review step exists at all.
|
|
17
|
+
*
|
|
18
|
+
* ## R1 — absent means SKIP, never default
|
|
19
|
+
*
|
|
20
|
+
* `ready`, `claimed`, `blocked` and `done` are REQUIRED: a project that omits
|
|
21
|
+
* them is misconfigured and the caller should say so. `review` and the `qa.*`
|
|
22
|
+
* roles are OPTIONAL and have NO built-in default, so omitting one means the
|
|
23
|
+
* lifecycle skips that transition entirely.
|
|
24
|
+
*
|
|
25
|
+
* This is the difference between "not customized" and "we don't do this step",
|
|
26
|
+
* which a defaulting resolver cannot express. Measured downstream: a project
|
|
27
|
+
* deliberately bound no `review` role — a PR-open ticket was meant to stay in
|
|
28
|
+
* `claimed` until it reached an environment — and agents moved issues into a
|
|
29
|
+
* human-only review state anyway, because every layer that could have honoured
|
|
30
|
+
* the omission supplied a default instead.
|
|
31
|
+
*
|
|
32
|
+
* ## R2 — a fallback may inform a READ, never supply a WRITE target
|
|
33
|
+
*
|
|
34
|
+
* Linear states carry a machine-readable `type`, so a skill may fall back to
|
|
35
|
+
* resolving a role by type when the configured name is missing — the
|
|
36
|
+
* `config-resolution` rule permits this "but only to *read* … it must never
|
|
37
|
+
* invent a state to write into."
|
|
38
|
+
*
|
|
39
|
+
* Nothing enforced that sentence. The fallback selects the lowest-position
|
|
40
|
+
* `started` state, and on the board where this was measured the states ranked
|
|
41
|
+
* `blocked` (-1989.26), `claimed` (-1478.50), then two unbound human-only review
|
|
42
|
+
* states at -1209.69 and -1079.70. With `blocked` and `claimed` already taken,
|
|
43
|
+
* the fallback returned the first unbound one — precisely where two issues
|
|
44
|
+
* landed. Not fuzzy matching; position ordering, used for a write.
|
|
45
|
+
*
|
|
46
|
+
* `--intent=write` therefore refuses to return a fallback value and reports a
|
|
47
|
+
* setup defect instead. `--intent=read` may use one, and always reports which.
|
|
48
|
+
*
|
|
49
|
+
* ## Why a script rather than a shared bash snippet
|
|
50
|
+
*
|
|
51
|
+
* A snippet is copy-paste, which is the disease. Skills shell out to
|
|
52
|
+
* `${CLAUDE_PLUGIN_ROOT}/scripts/…` for exactly this reason elsewhere
|
|
53
|
+
* (`lifecycle-label-trust.mjs`, `automation-run-record.mjs`), and a real process
|
|
54
|
+
* boundary means the resolution can be unit-tested rather than re-read.
|
|
55
|
+
* @module scripts/resolve-lifecycle-role
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
59
|
+
import { fileURLToPath } from "node:url";
|
|
60
|
+
|
|
61
|
+
/** Config file consulted first; its keys override the committed file. */
|
|
62
|
+
export const LOCAL_CONFIG = ".lisa.config.local.json";
|
|
63
|
+
|
|
64
|
+
/** The committed config file. */
|
|
65
|
+
export const GLOBAL_CONFIG = ".lisa.config.json";
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Roles a project MUST bind. Omitting one is a setup defect, not a policy
|
|
69
|
+
* choice — there is no coherent lifecycle without a ready lane, a claimed lane,
|
|
70
|
+
* a blocked lane and a terminal.
|
|
71
|
+
*/
|
|
72
|
+
export const REQUIRED_ROLES = Object.freeze([
|
|
73
|
+
"ready",
|
|
74
|
+
"claimed",
|
|
75
|
+
"blocked",
|
|
76
|
+
"done",
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Roles a project MAY bind. Omitting one means the lifecycle skips that
|
|
81
|
+
* transition. These have NO built-in default — that is the whole point of R1.
|
|
82
|
+
*/
|
|
83
|
+
export const OPTIONAL_ROLES = Object.freeze([
|
|
84
|
+
"review",
|
|
85
|
+
"qa.queue",
|
|
86
|
+
"qa.certified",
|
|
87
|
+
"human_needed",
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
/** Where each vendor keeps its build lifecycle role map. */
|
|
91
|
+
export const VENDOR_ROOTS = Object.freeze({
|
|
92
|
+
jira: "jira.workflow",
|
|
93
|
+
linear: "linear.workflow",
|
|
94
|
+
github: "github.labels.build",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/** Resolution outcomes, reported so a caller never has to guess what happened. */
|
|
98
|
+
export const OUTCOMES = Object.freeze({
|
|
99
|
+
CONFIGURED: "configured",
|
|
100
|
+
UNSET_OPTIONAL: "unset-optional",
|
|
101
|
+
UNSET_REQUIRED: "unset-required",
|
|
102
|
+
FALLBACK_REFUSED: "fallback-refused-for-write",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Read a dotted path out of a parsed config object.
|
|
107
|
+
*
|
|
108
|
+
* @param {unknown} source parsed config
|
|
109
|
+
* @param {string} path dotted key path, e.g. `linear.workflow.review`
|
|
110
|
+
* @returns {unknown} the value, or undefined when any segment is missing
|
|
111
|
+
*/
|
|
112
|
+
export const readPath = (source, path) => {
|
|
113
|
+
let cursor = source;
|
|
114
|
+
for (const segment of path.split(".")) {
|
|
115
|
+
if (cursor === null || typeof cursor !== "object" || !(segment in cursor))
|
|
116
|
+
return undefined;
|
|
117
|
+
cursor = cursor[segment];
|
|
118
|
+
}
|
|
119
|
+
return cursor;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Parse a config file, treating an unreadable or malformed file as absent.
|
|
124
|
+
*
|
|
125
|
+
* A malformed local override must not silently mask the committed file, so the
|
|
126
|
+
* caller is told which files actually parsed via the returned `sources`.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} file path to read
|
|
129
|
+
* @returns {{ value: object | undefined, error: string | undefined }} parse result
|
|
130
|
+
*/
|
|
131
|
+
export const parseConfig = file => {
|
|
132
|
+
let raw;
|
|
133
|
+
try {
|
|
134
|
+
raw = readFileSync(file, "utf8");
|
|
135
|
+
} catch {
|
|
136
|
+
return { value: undefined, error: undefined };
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
return { value: JSON.parse(raw), error: undefined };
|
|
140
|
+
} catch (cause) {
|
|
141
|
+
return {
|
|
142
|
+
value: undefined,
|
|
143
|
+
error: `${file} is not valid JSON: ${cause.message}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Resolve one lifecycle role for one vendor.
|
|
150
|
+
*
|
|
151
|
+
* Resolution order is local config → global config → (nothing). There is no
|
|
152
|
+
* built-in default tier: see R1 in the module docblock.
|
|
153
|
+
*
|
|
154
|
+
* @param {object} options resolution inputs
|
|
155
|
+
* @param {string} options.role role name (`ready`, `review`, `qa.queue`, …)
|
|
156
|
+
* @param {string} options.vendor one of `jira` | `linear` | `github`
|
|
157
|
+
* @param {"read" | "write"} [options.intent] what the caller will do with it
|
|
158
|
+
* @param {string} [options.env] env key when resolving the env-keyed `done` map
|
|
159
|
+
* @param {object} [options.local] pre-parsed local config (tests)
|
|
160
|
+
* @param {object} [options.global] pre-parsed global config (tests)
|
|
161
|
+
* @param {string} [options.fallback] a value a vendor fallback would supply
|
|
162
|
+
* @returns {{ value: string, outcome: string, source: string, message: string }} resolution
|
|
163
|
+
*/
|
|
164
|
+
export const resolveRole = ({
|
|
165
|
+
role,
|
|
166
|
+
vendor,
|
|
167
|
+
intent = "read",
|
|
168
|
+
env,
|
|
169
|
+
local,
|
|
170
|
+
global: globalConfig,
|
|
171
|
+
fallback,
|
|
172
|
+
}) => {
|
|
173
|
+
const root = VENDOR_ROOTS[vendor];
|
|
174
|
+
if (!root) {
|
|
175
|
+
return {
|
|
176
|
+
value: "",
|
|
177
|
+
outcome: OUTCOMES.UNSET_REQUIRED,
|
|
178
|
+
source: "none",
|
|
179
|
+
message: `Unknown vendor '${vendor}'. Expected one of: ${Object.keys(VENDOR_ROOTS).join(", ")}.`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// `done` is env-keyed on every vendor; an env selects one rung of the map.
|
|
184
|
+
const path =
|
|
185
|
+
env && role === "done" ? `${root}.done.${env}` : `${root}.${role}`;
|
|
186
|
+
|
|
187
|
+
for (const [label, source] of [
|
|
188
|
+
["local", local],
|
|
189
|
+
["global", globalConfig],
|
|
190
|
+
]) {
|
|
191
|
+
const found = readPath(source, path);
|
|
192
|
+
if (typeof found === "string" && found.length > 0) {
|
|
193
|
+
return {
|
|
194
|
+
value: found,
|
|
195
|
+
outcome: OUTCOMES.CONFIGURED,
|
|
196
|
+
source: label,
|
|
197
|
+
message: `${path} = ${found} (from ${label} config)`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const optional = OPTIONAL_ROLES.includes(role);
|
|
203
|
+
|
|
204
|
+
// R2 — a fallback may inform a read, never supply a write target.
|
|
205
|
+
if (fallback && intent === "write") {
|
|
206
|
+
return {
|
|
207
|
+
value: "",
|
|
208
|
+
outcome: OUTCOMES.FALLBACK_REFUSED,
|
|
209
|
+
source: "none",
|
|
210
|
+
message:
|
|
211
|
+
`Refusing to write to '${fallback}': it was derived by fallback, not named at ${path}. ` +
|
|
212
|
+
`A fallback may inform a read but must never supply a write target (config-resolution). ` +
|
|
213
|
+
`Bind ${path} explicitly, or run /lisa:setup:${vendor}.`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (fallback) {
|
|
217
|
+
return {
|
|
218
|
+
value: fallback,
|
|
219
|
+
outcome: OUTCOMES.CONFIGURED,
|
|
220
|
+
source: "fallback",
|
|
221
|
+
message: `${path} is unset; using fallback '${fallback}' for a READ only.`,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (optional) {
|
|
226
|
+
return {
|
|
227
|
+
value: "",
|
|
228
|
+
outcome: OUTCOMES.UNSET_OPTIONAL,
|
|
229
|
+
source: "none",
|
|
230
|
+
message:
|
|
231
|
+
`No ${path} configured; skipping the ${role} transition. ` +
|
|
232
|
+
`This is a supported configuration, not an error — the item stays in its current role.`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
value: "",
|
|
238
|
+
outcome: OUTCOMES.UNSET_REQUIRED,
|
|
239
|
+
source: "none",
|
|
240
|
+
message: `Required role '${role}' is not configured at ${path}. Run /lisa:setup:${vendor}.`,
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Parse `--key=value` / `--key value` argv into a plain object.
|
|
246
|
+
*
|
|
247
|
+
* @param {string[]} argv raw arguments
|
|
248
|
+
* @returns {Record<string, string>} parsed flags
|
|
249
|
+
*/
|
|
250
|
+
export const parseArgs = argv => {
|
|
251
|
+
const out = {};
|
|
252
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
253
|
+
const token = argv[index];
|
|
254
|
+
if (!token.startsWith("--")) continue;
|
|
255
|
+
const [key, inline] = token.slice(2).split("=");
|
|
256
|
+
if (inline !== undefined) {
|
|
257
|
+
out[key] = inline;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const next = argv[index + 1];
|
|
261
|
+
if (next && !next.startsWith("--")) {
|
|
262
|
+
out[key] = next;
|
|
263
|
+
index += 1;
|
|
264
|
+
} else {
|
|
265
|
+
out[key] = "true";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* CLI entry point.
|
|
273
|
+
*
|
|
274
|
+
* Exit codes are the contract callers branch on:
|
|
275
|
+
* - `0` with a value on stdout — the role is configured (or a read fallback).
|
|
276
|
+
* - `0` with EMPTY stdout — an optional role is unset; skip the transition.
|
|
277
|
+
* - `2` — a required role is unset, or a write was refused a fallback value.
|
|
278
|
+
*
|
|
279
|
+
* The distinction between the two `0` cases is deliberate: an unset optional
|
|
280
|
+
* role is a supported configuration, so it must not look like a failure.
|
|
281
|
+
*
|
|
282
|
+
* @param {string[]} argv arguments after the script name
|
|
283
|
+
* @returns {number} process exit code
|
|
284
|
+
*/
|
|
285
|
+
export const main = argv => {
|
|
286
|
+
const args = parseArgs(argv);
|
|
287
|
+
if (!args.role || !args.vendor) {
|
|
288
|
+
process.stderr.write(
|
|
289
|
+
"usage: resolve-lifecycle-role.mjs --role <name> --vendor <jira|linear|github> " +
|
|
290
|
+
"[--intent read|write] [--env dev|staging|production] [--fallback <name>]\n"
|
|
291
|
+
);
|
|
292
|
+
return 2;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const local = parseConfig(args.local ?? LOCAL_CONFIG);
|
|
296
|
+
const globalConfig = parseConfig(args.config ?? GLOBAL_CONFIG);
|
|
297
|
+
for (const parsed of [local, globalConfig]) {
|
|
298
|
+
if (parsed.error) {
|
|
299
|
+
process.stderr.write(`${parsed.error}\n`);
|
|
300
|
+
return 2;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const result = resolveRole({
|
|
305
|
+
role: args.role,
|
|
306
|
+
vendor: args.vendor,
|
|
307
|
+
intent: args.intent ?? "read",
|
|
308
|
+
env: args.env,
|
|
309
|
+
fallback: args.fallback,
|
|
310
|
+
local: local.value,
|
|
311
|
+
global: globalConfig.value,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
if (args.json === "true") {
|
|
315
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
316
|
+
} else if (result.value) {
|
|
317
|
+
process.stdout.write(`${result.value}\n`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (result.outcome === OUTCOMES.CONFIGURED) return 0;
|
|
321
|
+
if (result.outcome === OUTCOMES.UNSET_OPTIONAL) {
|
|
322
|
+
process.stderr.write(`${result.message}\n`);
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
process.stderr.write(`${result.message}\n`);
|
|
326
|
+
return 2;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const invokedDirectly = (() => {
|
|
330
|
+
try {
|
|
331
|
+
return (
|
|
332
|
+
realpathSync(process.argv[1] ?? "") ===
|
|
333
|
+
realpathSync(fileURLToPath(import.meta.url))
|
|
334
|
+
);
|
|
335
|
+
} catch {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
})();
|
|
339
|
+
|
|
340
|
+
if (invokedDirectly) process.exit(main(process.argv.slice(2)));
|
|
@@ -38,11 +38,12 @@ Build-queue label names are read from `.lisa.config.json` `github.labels.build.*
|
|
|
38
38
|
```bash
|
|
39
39
|
# Read role with default fallback. Local overrides global per-key.
|
|
40
40
|
read_role() {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
# Single resolver — see config-resolution "The single resolver".
|
|
42
|
+
# Exit 0 + empty output means an OPTIONAL role is unset: skip that transition,
|
|
43
|
+
# never substitute the second argument. A default on an optional role is what
|
|
44
|
+
# made "unset" indistinguishable from "not customized".
|
|
45
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/resolve-lifecycle-role.mjs" \
|
|
46
|
+
--role "$1" --vendor github --intent "${3:-read}" 2>/dev/null
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
READY=$(read_role ready "status:ready")
|
|
@@ -21,11 +21,12 @@ PRD label names are read from `.lisa.config.json` `github.labels.prd.*`, falling
|
|
|
21
21
|
```bash
|
|
22
22
|
# Read role with default fallback. Local overrides global per-key.
|
|
23
23
|
read_role() {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
# Single resolver — see config-resolution "The single resolver".
|
|
25
|
+
# Exit 0 + empty output means an OPTIONAL role is unset: skip that transition,
|
|
26
|
+
# never substitute the second argument. A default on an optional role is what
|
|
27
|
+
# made "unset" indistinguishable from "not customized".
|
|
28
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/resolve-lifecycle-role.mjs" \
|
|
29
|
+
--role "$1" --vendor github --intent "${3:-read}" 2>/dev/null
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
READY=$(read_role ready "prd-ready")
|