@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
|
@@ -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.
|
|
@@ -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")
|
|
@@ -79,6 +79,8 @@ Based on the milestone, suggest (but do NOT automatically perform) a label trans
|
|
|
79
79
|
|
|
80
80
|
The actual `status:in-progress` flip is owned by `lisa-github-build-intake` (claim) and `lisa-github-agent`. The configured `done` flip is owned by the build-intake owner after a successful build and evidence post. This skill never relabels.
|
|
81
81
|
|
|
82
|
+
Every suggested transition is bound by the **Tracker status vocabulary** section of `lisa-tracker-sync` — cite it, do not restate the policy. GitHub has **no review lane** (`BUILD_LABEL_DEFAULTS` seeds no `review` key, and `config-resolution` records "no default review label"), so the `pr-ready` milestone suggests the configured `done` label directly; there is no intermediate hop to skip. Resolve every label through the shared resolver rather than an inlined helper.
|
|
83
|
+
|
|
82
84
|
### Step 5: Parent Status Rollup (`--rollup`)
|
|
83
85
|
|
|
84
86
|
When invoked with `--rollup`, this skill **derives a parent/container issue's `status:*` label from the roll-up of its child sub-issues** instead of posting a milestone update on a leaf. This implements the GitHub sub-issue-completion arm of the **Parent status rollup (the state machine)** section of the `leaf-only-lifecycle` rule — cite that rule, do not restate the policy. It is the sync-side complement to the write-time labeling (`lisa-github-write-issue`), the validate-time S15 gate (`lisa-github-validate-issue`), and the claim-time gate (`lisa-github-build-intake`); all four cite the same rule so the classification never drifts.
|
|
@@ -20,9 +20,13 @@ ORG=$(jq -r '.github.org // empty' .lisa.config.local.json 2>/dev/null); ORG="${
|
|
|
20
20
|
REPO=$(jq -r '.github.repo // empty' .lisa.config.local.json 2>/dev/null); REPO="${REPO:-$(jq -r '.github.repo // empty' .lisa.config.json)}"
|
|
21
21
|
[ -z "$ORG" ] || [ -z "$REPO" ] && { echo "Error: github.org / github.repo not set in .lisa.config.json."; exit 1; }
|
|
22
22
|
|
|
23
|
-
read_role() {
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
read_role() {
|
|
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
|
|
26
30
|
}
|
|
27
31
|
# Resolve the FULL PRD lifecycle vocabulary from config (never hard-code names) — needed so the
|
|
28
32
|
# "exactly one role" reconcile and the past-ready check work for projects that renamed any label.
|
|
@@ -22,11 +22,12 @@ Status names are read from `.lisa.config.json` `jira.workflow.*`, falling back t
|
|
|
22
22
|
```bash
|
|
23
23
|
# Read role with default fallback. Local overrides global per-key.
|
|
24
24
|
read_role() {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
# Single resolver — see config-resolution "The single resolver".
|
|
26
|
+
# Exit 0 + empty output means an OPTIONAL role is unset: skip that transition,
|
|
27
|
+
# never substitute the second argument. A default on an optional role is what
|
|
28
|
+
# made "unset" indistinguishable from "not customized".
|
|
29
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/resolve-lifecycle-role.mjs" \
|
|
30
|
+
--role "$1" --vendor jira --intent "${3:-read}" 2>/dev/null
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
READY=$(read_role ready "Ready")
|
|
@@ -75,7 +75,9 @@ Based on the milestone, suggest (but don't automatically perform) a status trans
|
|
|
75
75
|
| PR ready | configured `jira.workflow.review` status, or no transition when unconfigured |
|
|
76
76
|
| PR merged | configured `jira.workflow.done` status for the PR's target environment (env-keyed `done` resolved via `deploy.branches`), or no transition when unconfigured |
|
|
77
77
|
|
|
78
|
-
Every suggested
|
|
78
|
+
Every suggested transition is bound by the **Tracker status vocabulary** section of `lisa-tracker-sync` — cite it, do not restate the policy. It is shared by all three vendor arms so the bar cannot drift between them, and it carries the two consequences that matter here: a milestone whose role is unset gets a comment rather than a transition, and a fallback may inform a read but never supply a write target.
|
|
79
|
+
|
|
80
|
+
`review` is optional on JIRA and has no default. Resolve it through the shared resolver rather than an inlined helper — `lisa-jira-evidence/scripts/post-evidence.sh` already models the correct behaviour (empty default, explicit skip branch, `leaving <ID> in its current (claimed) status`).
|
|
79
81
|
|
|
80
82
|
### Step 5: Parent Status Rollup (`--rollup`)
|
|
81
83
|
|
|
@@ -23,11 +23,12 @@ Build-queue **workflow state** names are read from `.lisa.config.json` `linear.w
|
|
|
23
23
|
```bash
|
|
24
24
|
# Read role with default fallback. Local overrides global per-key.
|
|
25
25
|
read_role() {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
# Single resolver — see config-resolution "The single resolver".
|
|
27
|
+
# Exit 0 + empty output means an OPTIONAL role is unset: skip that transition,
|
|
28
|
+
# never substitute the second argument. A default on an optional role is what
|
|
29
|
+
# made "unset" indistinguishable from "not customized".
|
|
30
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/resolve-lifecycle-role.mjs" \
|
|
31
|
+
--role "$1" --vendor linear --intent "${3:-read}" 2>/dev/null
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
READY=$(read_role ready "Ready")
|
|
@@ -12,21 +12,20 @@ Post verification evidence to a Linear Issue and transition it from the configur
|
|
|
12
12
|
|
|
13
13
|
## Workflow resolution
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Resolve every lifecycle role through the shared resolver — never an inlined `read_role` helper. Twelve skills used to inline their own and produced **eleven different implementations**, which is how the vendors drifted apart on this exact contract.
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
local_v=$(jq -r ".linear.workflow.${role} // empty" .lisa.config.local.json 2>/dev/null)
|
|
22
|
-
global_v=$(jq -r ".linear.workflow.${role} // empty" .lisa.config.json 2>/dev/null)
|
|
23
|
-
echo "${local_v:-${global_v:-$default}}"
|
|
18
|
+
resolve() { # role, intent -> value on stdout; empty when an optional role is unset
|
|
19
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/resolve-lifecycle-role.mjs" \
|
|
20
|
+
--role "$1" --vendor linear --intent "${2:-read}" 2>/dev/null
|
|
24
21
|
}
|
|
25
22
|
|
|
26
|
-
CLAIMED=$(
|
|
27
|
-
REVIEW=$(
|
|
23
|
+
CLAIMED=$(resolve claimed write)
|
|
24
|
+
REVIEW=$(resolve review write)
|
|
28
25
|
```
|
|
29
26
|
|
|
27
|
+
**`review` is OPTIONAL and has no default.** An empty `REVIEW` means the project does not run an agent review step, and this skill **skips the transition entirely**, leaving the Issue in `claimed` — the same behaviour `lisa-jira-evidence/scripts/post-evidence.sh` has always had. Do not substitute `In Review`, and do not fall back to a state resolved by `type` or board position: a fallback may inform a read but must never supply a write target (`config-resolution`, R2). Resolving a state nobody configured is how agents reach human-only review lanes.
|
|
28
|
+
|
|
30
29
|
## Configuration
|
|
31
30
|
|
|
32
31
|
Reads `linear.workspace`, `linear.teamKey`, and `linear.workflow.*` from `.lisa.config.json` (with `.local` override).
|
|
@@ -93,9 +92,11 @@ Linear comments support markdown including `<details>` collapsibles, fenced code
|
|
|
93
92
|
|
|
94
93
|
## Phase 5 — Transition Status
|
|
95
94
|
|
|
96
|
-
|
|
95
|
+
**If `$REVIEW` is empty, skip this phase entirely** — post the evidence comment and leave the Issue in `$CLAIMED`. Report it plainly (`No linear.workflow.review configured; leaving <ID> in its current (claimed) state`) so the skip is visible rather than silent. This is a supported configuration, not a failure.
|
|
96
|
+
|
|
97
|
+
When `$REVIEW` is non-empty, update labels via `lisa-linear-access operation: save-issue` to remove `$CLAIMED` and add `$REVIEW`. Resolve label IDs first via `lisa-linear-access operation: list-issue-labels` (create the label via `create_issue_label` if it doesn't exist on the team).
|
|
97
98
|
|
|
98
|
-
The native Linear `state` field is
|
|
99
|
+
The native Linear `state` field is updated to `$REVIEW` **only when the resolver returned it from config**. Never resolve the state by searching the team for something review-shaped: on a board with more than one review-ish state that picks by position, not by intent, and the states most likely to be found that way are the human-only ones a project deliberately kept out of its config.
|
|
99
100
|
|
|
100
101
|
## Phase 6 — Report
|
|
101
102
|
|
|
@@ -21,11 +21,12 @@ PRD label names are read from `.lisa.config.json` `linear.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 linear --intent "${3:-read}" 2>/dev/null
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
READY=$(read_role ready "prd-ready")
|