@lumoai/cli 1.27.0 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/skill/SKILL.md +15 -15
- package/assets/skill/references/criteria.md +25 -4
- package/assets/skill/references/sessions.md +6 -3
- package/assets/skill/references/verify.md +25 -1
- package/dist/cli/src/index.js +14 -6
- package/dist/cli/src/lib/git-task.js +9 -17
- package/dist/shared/src/task-identifier.js +51 -0
- package/package.json +1 -1
package/assets/skill/SKILL.md
CHANGED
|
@@ -19,20 +19,20 @@ which lumo && lumo whoami
|
|
|
19
19
|
|
|
20
20
|
The command catalog below is a **map**: it lists every command grouped by domain with a one-line summary. **Detailed flags, examples, output formats, and "when to suggest" guidance live in the `references/` files** — when a user request lands in a domain, **Read the matching reference file before composing the command**. Don't run a command from memory if its flags/edge-cases matter; open the reference first.
|
|
21
21
|
|
|
22
|
-
| Domain
|
|
23
|
-
|
|
|
24
|
-
| `setup`, `auth login/logout`, `whoami`, `update`
|
|
25
|
-
| `task context`, retrieval (`slack/web/figma context`, `comments list`, `pr show`)
|
|
26
|
-
| `task create/update/list/show/comment`, `next`
|
|
27
|
-
| `task artifact*`, `task figma*`
|
|
28
|
-
| `task criteria set/list`, drafting the acceptance contract
|
|
22
|
+
| Domain | Read this reference |
|
|
23
|
+
| --------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
24
|
+
| `setup`, `auth login/logout`, `whoami`, `update` | [references/onboarding.md](references/onboarding.md) |
|
|
25
|
+
| `task context`, retrieval (`slack/web/figma context`, `comments list`, `pr show`) | [references/task-context.md](references/task-context.md) |
|
|
26
|
+
| `task create/update/list/show/comment`, `next` | [references/tasks.md](references/tasks.md) |
|
|
27
|
+
| `task artifact*`, `task figma*` | [references/artifacts-figma.md](references/artifacts-figma.md) |
|
|
28
|
+
| `task criteria set/list`, drafting the acceptance contract | [references/criteria.md](references/criteria.md) |
|
|
29
29
|
| `verify`, `task status` — machine verification loop, claim-done flow, self-check/resume | [references/verify.md](references/verify.md) |
|
|
30
|
-
| `project list`, `milestone*`
|
|
31
|
-
| `doc*`
|
|
32
|
-
| `sprint*`
|
|
33
|
-
| `task/project memory`, `memory promote/rm`
|
|
34
|
-
| `session attach/status/detach/wrap`, git-suggest on start, Layer-2 review
|
|
35
|
-
| `worktree add/rm/list` (local dev tooling)
|
|
30
|
+
| `project list`, `milestone*` | [references/milestones.md](references/milestones.md) |
|
|
31
|
+
| `doc*` | [references/docs.md](references/docs.md) |
|
|
32
|
+
| `sprint*` | [references/sprints.md](references/sprints.md) |
|
|
33
|
+
| `task/project memory`, `memory promote/rm` | [references/memory.md](references/memory.md) |
|
|
34
|
+
| `session attach/status/detach/wrap`, git-suggest on start, Layer-2 review | [references/sessions.md](references/sessions.md) |
|
|
35
|
+
| `worktree add/rm/list` (local dev tooling) | [references/worktree.md](references/worktree.md) |
|
|
36
36
|
|
|
37
37
|
## Command catalog
|
|
38
38
|
|
|
@@ -151,10 +151,10 @@ Typical flow when a user says "help me with LUM-42":
|
|
|
151
151
|
1. `lumo session attach LUM-42` — bind this session
|
|
152
152
|
2. `lumo task context LUM-42` — load background
|
|
153
153
|
3. Review unresolved items, PR-review todos, and the task description
|
|
154
|
-
4. **If the task has no acceptance criteria** (context shows the draft reminder instead of a contract): draft 3–7
|
|
154
|
+
4. **If the task has no acceptance criteria** (context shows the draft reminder instead of a contract): draft outcome-level criteria sized to the task (3–7 for a typical multi-file task; 1–2 for a micro task) and submit them with `lumo task criteria set` **before writing the first line of code** — see [criteria.md](references/criteria.md) for the drafting guide
|
|
155
155
|
5. Begin working on the task
|
|
156
156
|
6. **Before claiming the work is done: run `lumo verify`** — the machine half of the acceptance loop. Fix failures and re-run (round cap 3). On all-pass the task moves to IN_REVIEW and you stop; never set DONE yourself after a verify loop — that adjudication is human-only. See [verify.md](references/verify.md)
|
|
157
157
|
|
|
158
158
|
**Status-first recovery:** when you pick a task back up — a new session resuming earlier work, or a task that came back after a rejected verification round / review findings — run `lumo task status` **before** re-reading code or planning. It tells you where the loop stands (current round, what passed, what's unmet and why, any REVIEW_ADDED criteria appended during review) so you don't redo finished work or miss the reason it bounced. See [verify.md](references/verify.md)
|
|
159
159
|
|
|
160
|
-
**Git-suggest at start:** when the session is unbound, session-start may infer the task from the git branch / recent commits and print a suggestion — `Detected LUM-N (from branch name). Run lumo session attach LUM-N to bind.` (or `from recent commits`) — **without** binding. Confirm it's the right task, then run `lumo session attach <LUM-N>` yourself (binding only happens on an explicit attach). See [sessions.md](references/sessions.md) for the full session-start behavior.
|
|
160
|
+
**Git-suggest at start:** when the session is unbound, session-start may infer the task from the git branch / recent commits (any team prefix, e.g. `SPEC-12`, not just `LUM-N`) and print a suggestion — `Detected LUM-N (from branch name). Run lumo session attach LUM-N to bind.` (or `from recent commits`) — **without** binding. Confirm it's the right task, then run `lumo session attach <LUM-N>` yourself (binding only happens on an explicit attach). See [sessions.md](references/sessions.md) for the full session-start behavior.
|
|
@@ -17,8 +17,10 @@ contract, you MUST draft and submit criteria before starting implementation:
|
|
|
17
17
|
1. Read the task description, comments, linked resources, and memory first —
|
|
18
18
|
the contract distills what "done" means, so understand the task before
|
|
19
19
|
writing it.
|
|
20
|
-
2. Draft **3–7 criteria** (soft range — the
|
|
21
|
-
rejects; if you genuinely need more,
|
|
20
|
+
2. Draft **3–7 criteria** for a typical multi-file task (soft range — the
|
|
21
|
+
server warns outside it but never rejects; if you genuinely need more,
|
|
22
|
+
merge related checks instead). **Scale the count down for small tasks** —
|
|
23
|
+
see "Scale the contract to the task size" below.
|
|
22
24
|
3. Submit with `lumo task criteria set <task> --file <criteria.json>`.
|
|
23
25
|
Submission **locks the contract for agent edits** — get it right before
|
|
24
26
|
submitting, because afterwards only human revisions (`--human`) can change it.
|
|
@@ -27,6 +29,23 @@ Once you (the agent) have started work, you can NOT change your own contract.
|
|
|
27
29
|
That's by design: the contract is what your work gets judged against, not a
|
|
28
30
|
to-do list you trim to fit what you built.
|
|
29
31
|
|
|
32
|
+
## Scale the contract to the task size
|
|
33
|
+
|
|
34
|
+
The 3–7 range is calibrated for typical multi-file tasks. The criterion count
|
|
35
|
+
must track the size of the work — it is not a fixed ritual:
|
|
36
|
+
|
|
37
|
+
- **Micro task** (expected to fit a single session, blast radius of one or
|
|
38
|
+
two files — a docs tweak, a copy change, a small targeted fix): **1–2
|
|
39
|
+
criteria IS a complete contract** — one targeted MACHINE criterion plus at
|
|
40
|
+
most one HUMAN criterion.
|
|
41
|
+
- **Never pad toward the soft floor.** A filler criterion (a restated
|
|
42
|
+
baseline, a third angle on the same check) dilutes the contract and taxes
|
|
43
|
+
every verification round. For a micro task the below-minimum warning is
|
|
44
|
+
expected — ignore it.
|
|
45
|
+
- **Shrink the contract, never skip it.** Every task still drafts and locks a
|
|
46
|
+
contract before the first line of code; task size only changes how many
|
|
47
|
+
criteria that takes.
|
|
48
|
+
|
|
30
49
|
## How to write good criteria
|
|
31
50
|
|
|
32
51
|
- **Outcome-level definition of done, not micro-steps.** A criterion states a
|
|
@@ -129,8 +148,10 @@ before a `--human` revision. Empty contract prints a drafting pointer.
|
|
|
129
148
|
binding.
|
|
130
149
|
- **`lumo task context`**: the `## Acceptance criteria (contract)` section appears after the
|
|
131
150
|
task description, before memory.
|
|
132
|
-
- Review-time gap findings
|
|
133
|
-
|
|
151
|
+
- Review-time gap findings are appended at the round they surface and show up
|
|
152
|
+
in the contract automatically — `REVIEW_ADDED` provenance via human review
|
|
153
|
+
paths; findings a human raises in conversation are transcribed with
|
|
154
|
+
`--human` + `--cause` (see verify.md "Review-time drift habits").
|
|
134
155
|
|
|
135
156
|
## After the contract: the verification loop
|
|
136
157
|
|
|
@@ -10,15 +10,18 @@ infer the task from local git so it can **suggest** one — it never binds for y
|
|
|
10
10
|
|
|
11
11
|
- It reads the **current branch name** first (e.g. `lumo/LUM-145-...`), then the
|
|
12
12
|
**most recent commit subjects** (e.g. `... [LUM-145]`), extracting the first
|
|
13
|
-
|
|
13
|
+
task identifier. Detection is **prefix-agnostic** (LUM-419): any team prefix
|
|
14
|
+
matches — `SPEC-12` as much as `LUM-145` — using the same pattern the server
|
|
15
|
+
uses to link PR branches to tasks. Well-known acronym-number tokens
|
|
16
|
+
(`UTF-8`, `SHA-256`, `ISO-8601`, …) are skipped, never suggested.
|
|
14
17
|
- On a hit it prints a single suggestion line and stops — the session stays
|
|
15
18
|
**unbound** and no context is injected yet:
|
|
16
19
|
`Detected LUM-145 (from branch name). Run lumo session attach LUM-145 to bind.`
|
|
17
20
|
(the basis reads `from recent commits` when the hit came from commit subjects instead of the branch name)
|
|
18
21
|
No task title is shown here because nothing was fetched; the title, memory,
|
|
19
22
|
and PR-review todos appear only once you actually attach.
|
|
20
|
-
- No match (detached HEAD, a
|
|
21
|
-
repo) → it degrades to the normal unbound prompt.
|
|
23
|
+
- No match (detached HEAD, a branch with no identifier and no tagged commits,
|
|
24
|
+
not a git repo) → it degrades to the normal unbound prompt.
|
|
22
25
|
|
|
23
26
|
**Agent guidance:** when you see a suggestion line, confirm the inferred task is
|
|
24
27
|
the one the user wants, then run `lumo session attach <LUM-N>` (followed by
|
|
@@ -71,6 +71,30 @@ the failures — re-running without changes burns a round and (at round 3)
|
|
|
71
71
|
pages a human. A FAIL round never changes task status; only an all-pass round
|
|
72
72
|
moves it (to IN_REVIEW, never further).
|
|
73
73
|
|
|
74
|
+
## Review-time drift habits (gap findings)
|
|
75
|
+
|
|
76
|
+
A problem discovered during acceptance/review that the contract does NOT
|
|
77
|
+
cover is a **gap finding** — record it in the contract, never just fix it
|
|
78
|
+
silently:
|
|
79
|
+
|
|
80
|
+
1. **Append it on the spot.** Transcribe the human's finding as a criterion
|
|
81
|
+
via `lumo task criteria set <task> --file <desired-final-list> --human` —
|
|
82
|
+
the review-added semantics: the gap surfaced at review time, at the
|
|
83
|
+
current round.
|
|
84
|
+
2. **Tag why the contract drifted** with `--cause
|
|
85
|
+
<NEW_INFO|SCOPE_CHANGE|DRAFT_BLIND_SPOT|GRANULARITY|OTHER>`. Gap findings
|
|
86
|
+
are usually `DRAFT_BLIND_SPOT` (the draft missed it) or `NEW_INFO`
|
|
87
|
+
(information that didn't exist at drafting time).
|
|
88
|
+
3. **Then bounce.** The appended criterion shows up in `lumo task status`
|
|
89
|
+
nextActions and the next verify round picks it up automatically — no
|
|
90
|
+
side-channel to-do list.
|
|
91
|
+
|
|
92
|
+
How to read drift: information-lag and requirement-movement drift
|
|
93
|
+
(`NEW_INFO`, `SCOPE_CHANGE`) is healthy — don't optimize it away.
|
|
94
|
+
`DRAFT_BLIND_SPOT` clusters feed back into the drafting guide. **Zero drift
|
|
95
|
+
across many tasks is a red flag, not a trophy** — it usually means contracts
|
|
96
|
+
are too thin or state only sure-win clauses that can never be found wanting.
|
|
97
|
+
|
|
74
98
|
## lumo task status — the read half (self-check entry point)
|
|
75
99
|
|
|
76
100
|
`lumo task status [task] [--json]` is the read-only counterpart of the loop
|
|
@@ -102,7 +126,7 @@ what's unmet and why (the exact failure tails), and how many rounds are left.
|
|
|
102
126
|
- Header: task identifier/title/status + `verification round N/3` (round 0 =
|
|
103
127
|
never verified) + an escalation warning when the machine loop is exhausted.
|
|
104
128
|
- **Criteria** — every criterion as `<glyph> <id> [TYPE] SOURCE@rN
|
|
105
|
-
|
|
129
|
+
statement` (✓ latest verdict passed / ✗ failed / ○ no verdict yet) with its
|
|
106
130
|
checkpointer and latest verdict line (evidence pointer on pass, failure
|
|
107
131
|
tail on fail). `REVIEW_ADDED@rN` provenance is visible per row.
|
|
108
132
|
- **History** — one line per recorded round: `rN · timestamp · X PASS / Y FAIL`.
|
package/dist/cli/src/index.js
CHANGED
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
};
|
|
35
35
|
})();
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.program = void 0;
|
|
37
38
|
const fs = __importStar(require("fs"));
|
|
38
39
|
const os = __importStar(require("os"));
|
|
39
40
|
const path = __importStar(require("path"));
|
|
@@ -123,9 +124,16 @@ const worktree_rm_1 = require("./commands/worktree-rm");
|
|
|
123
124
|
const worktree_list_1 = require("./commands/worktree-list");
|
|
124
125
|
const update_check_1 = require("./lib/update-check");
|
|
125
126
|
const sanitize_1 = require("./lib/sanitize");
|
|
127
|
+
// Introspection mode: tooling (scripts/analysis grammar extraction) imports
|
|
128
|
+
// this module to walk the registered commander tree without running the CLI.
|
|
129
|
+
// Skips package.json resolution (which assumes the compiled dist/ layout),
|
|
130
|
+
// update checks, local-state cleanup, and parseAsync. Never set for real runs.
|
|
131
|
+
const isIntrospect = process.env.LUMO_CLI_INTROSPECT === '1';
|
|
126
132
|
// Resolve package.json relative to __dirname so this works regardless of how
|
|
127
133
|
// deep the compiled output ends up (flat dist/ or nested dist/cli/src/).
|
|
128
|
-
const pkg =
|
|
134
|
+
const pkg = isIntrospect
|
|
135
|
+
? { name: '@lumoai/cli', version: '0.0.0-introspect' }
|
|
136
|
+
: require(path.resolve(__dirname, '../../..', 'package.json'));
|
|
129
137
|
// Detached background-refresh worker: re-entry point for the spawn() in
|
|
130
138
|
// maybeRefreshInBackground(). Fetches latest, writes cache, exits — skipping
|
|
131
139
|
// commander.parseAsync() below.
|
|
@@ -135,7 +143,7 @@ if (isUpdateCheckWorker) {
|
|
|
135
143
|
.catch(() => { })
|
|
136
144
|
.finally(() => process.exit(0));
|
|
137
145
|
}
|
|
138
|
-
else {
|
|
146
|
+
else if (!isIntrospect) {
|
|
139
147
|
(0, update_check_1.printUpdateNoticeIfAny)(pkg.name, pkg.version);
|
|
140
148
|
(0, update_check_1.maybeRefreshInBackground)(pkg.name);
|
|
141
149
|
}
|
|
@@ -143,8 +151,7 @@ else {
|
|
|
143
151
|
// is now server-authoritative; the legacy global pointer and the
|
|
144
152
|
// per-session sentinel directory are both obsolete. Failures are
|
|
145
153
|
// swallowed so a permission glitch doesn't prevent CLI invocation.
|
|
146
|
-
|
|
147
|
-
(() => {
|
|
154
|
+
if (!isIntrospect) {
|
|
148
155
|
const dir = process.env.LUMO_CONFIG_DIR || path.join(os.homedir(), '.lumo');
|
|
149
156
|
try {
|
|
150
157
|
fs.rmSync(path.join(dir, 'current-task.json'), { force: true });
|
|
@@ -154,7 +161,7 @@ else {
|
|
|
154
161
|
fs.rmSync(path.join(dir, 'sessions'), { recursive: true, force: true });
|
|
155
162
|
}
|
|
156
163
|
catch { }
|
|
157
|
-
}
|
|
164
|
+
}
|
|
158
165
|
const collect = (val, acc) => [...acc, val];
|
|
159
166
|
function wrap(fn) {
|
|
160
167
|
return async (...args) => {
|
|
@@ -179,6 +186,7 @@ const program = new commander_1.Command()
|
|
|
179
186
|
// created via .command() inherit these settings.
|
|
180
187
|
.showSuggestionAfterError(true)
|
|
181
188
|
.showHelpAfterError('(run the command with --help to list its valid flags and arguments)');
|
|
189
|
+
exports.program = program;
|
|
182
190
|
const auth = program.command('auth').description('Manage Lumo authentication');
|
|
183
191
|
auth
|
|
184
192
|
.command('login')
|
|
@@ -883,7 +891,7 @@ for (const [slug, description] of HOOK_SUBCOMMANDS) {
|
|
|
883
891
|
.option('--agent <token>', 'Coding agent that owns this session (e.g. claude-code, codex). Baked in by `lumo setup --agent`.')
|
|
884
892
|
.action(wrap((opts) => (0, hook_1.hookCommand)(slug, opts.agent)));
|
|
885
893
|
}
|
|
886
|
-
if (!isUpdateCheckWorker) {
|
|
894
|
+
if (!isUpdateCheckWorker && !isIntrospect) {
|
|
887
895
|
program.parseAsync(process.argv).catch(err => {
|
|
888
896
|
const msg = err instanceof Error ? err.message : String(err);
|
|
889
897
|
console.error(`Error: ${(0, sanitize_1.sanitizeField)(msg)}`);
|
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.matchTaskIdentifier =
|
|
3
|
+
exports.matchTaskIdentifier = void 0;
|
|
4
4
|
exports.extractTaskFromGit = extractTaskFromGit;
|
|
5
5
|
const child_process_1 = require("child_process");
|
|
6
|
+
const task_identifier_1 = require("../../../shared/src/task-identifier");
|
|
7
|
+
Object.defineProperty(exports, "matchTaskIdentifier", { enumerable: true, get: function () { return task_identifier_1.matchTaskIdentifier; } });
|
|
6
8
|
/**
|
|
7
9
|
* How many recent commit subjects to scan when the branch name carries no
|
|
8
10
|
* task id (e.g. on a generic feature branch or detached HEAD).
|
|
9
11
|
*/
|
|
10
12
|
const DEFAULT_COMMIT_DEPTH = 20;
|
|
11
|
-
const TASK_RE = /LUM-(\d+)/i;
|
|
12
|
-
/**
|
|
13
|
-
* Pull the first `LUM-<n>` token out of arbitrary text (a branch name or a
|
|
14
|
-
* commit subject) and normalize it to upper case. Returns null when no task
|
|
15
|
-
* id is present. The leading `-` in the pattern means the bare word `lumo`
|
|
16
|
-
* never matches.
|
|
17
|
-
*/
|
|
18
|
-
function matchTaskIdentifier(text) {
|
|
19
|
-
const m = text.match(TASK_RE);
|
|
20
|
-
return m ? `LUM-${m[1]}` : null;
|
|
21
|
-
}
|
|
22
13
|
/**
|
|
23
14
|
* Run a git subcommand and return trimmed stdout, or '' on any failure
|
|
24
15
|
* (non-zero exit, spawn error, not a repository, timeout). Never throws.
|
|
@@ -41,17 +32,18 @@ function gitOutput(args, cwd) {
|
|
|
41
32
|
/**
|
|
42
33
|
* Infer the task to work on from local git: prefer the current branch name
|
|
43
34
|
* (e.g. `lumo/LUM-145-...`), then fall back to the most recent commit
|
|
44
|
-
* subjects (e.g. `... [LUM-145]`).
|
|
45
|
-
*
|
|
46
|
-
*
|
|
35
|
+
* subjects (e.g. `... [LUM-145]`). Any team prefix matches (SPEC-12 as much
|
|
36
|
+
* as LUM-145). Returns null when nothing matches — detached HEAD (branch
|
|
37
|
+
* reads `HEAD`), a branch with no identifier and no tagged commits, or a
|
|
38
|
+
* directory that is not a git repository all degrade to null.
|
|
47
39
|
*/
|
|
48
40
|
function extractTaskFromGit(cwd, commitDepth = DEFAULT_COMMIT_DEPTH) {
|
|
49
41
|
const branch = gitOutput(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
50
|
-
const fromBranch = matchTaskIdentifier(branch);
|
|
42
|
+
const fromBranch = (0, task_identifier_1.matchTaskIdentifier)(branch);
|
|
51
43
|
if (fromBranch)
|
|
52
44
|
return { identifier: fromBranch, source: 'branch' };
|
|
53
45
|
const log = gitOutput(['log', '-n', String(commitDepth), '--format=%s'], cwd);
|
|
54
|
-
const fromLog = matchTaskIdentifier(log);
|
|
46
|
+
const fromLog = (0, task_identifier_1.matchTaskIdentifier)(log);
|
|
55
47
|
if (fromLog)
|
|
56
48
|
return { identifier: fromLog, source: 'commit' };
|
|
57
49
|
return null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TASK_IDENTIFIER_PATTERN = void 0;
|
|
4
|
+
exports.matchTaskIdentifier = matchTaskIdentifier;
|
|
5
|
+
/**
|
|
6
|
+
* Single source of truth for the task-identifier pattern (LUM-419).
|
|
7
|
+
*
|
|
8
|
+
* Consumed by both sides so they cannot drift:
|
|
9
|
+
* - server: lib/integrations/github/branch.ts (webhook branch → task linking)
|
|
10
|
+
* - CLI: cli/src/lib/git-task.ts (session-start git-suggest)
|
|
11
|
+
*
|
|
12
|
+
* A task identifier is `<TEAM>-<n>` where TEAM is the team's configured
|
|
13
|
+
* prefix (`Team.identifier`, 1–10 letters) — e.g. LUM-419, SPEC-123. The
|
|
14
|
+
* prefix is NOT hardcoded to any one team.
|
|
15
|
+
*/
|
|
16
|
+
exports.TASK_IDENTIFIER_PATTERN = String.raw `\b([A-Za-z]{1,10}-\d+)\b`;
|
|
17
|
+
/**
|
|
18
|
+
* Acronym-number tokens that match the identifier shape but are never task
|
|
19
|
+
* ids. Commit subjects routinely contain these ("fix UTF-8 handling",
|
|
20
|
+
* "use SHA-256"), so the matcher skips them instead of suggesting a bogus
|
|
21
|
+
* session attach. Compared against the uppercased prefix segment.
|
|
22
|
+
*/
|
|
23
|
+
const NON_TASK_PREFIXES = new Set([
|
|
24
|
+
'AES',
|
|
25
|
+
'CVE',
|
|
26
|
+
'GPT',
|
|
27
|
+
'HTTP',
|
|
28
|
+
'ISO',
|
|
29
|
+
'RFC',
|
|
30
|
+
'RSA',
|
|
31
|
+
'SHA',
|
|
32
|
+
'TLS',
|
|
33
|
+
'UTF',
|
|
34
|
+
]);
|
|
35
|
+
/**
|
|
36
|
+
* Extract the first task identifier from arbitrary text (a branch name or a
|
|
37
|
+
* commit subject), normalized to upper case. Tokens whose prefix is a known
|
|
38
|
+
* non-task acronym (UTF-8, SHA-256, …) are skipped; later genuine matches in
|
|
39
|
+
* the same text still win. Returns null when nothing matches.
|
|
40
|
+
*/
|
|
41
|
+
function matchTaskIdentifier(text) {
|
|
42
|
+
const re = new RegExp(exports.TASK_IDENTIFIER_PATTERN, 'g');
|
|
43
|
+
for (const m of text.matchAll(re)) {
|
|
44
|
+
const identifier = m[1].toUpperCase();
|
|
45
|
+
const prefix = identifier.slice(0, identifier.lastIndexOf('-'));
|
|
46
|
+
if (NON_TASK_PREFIXES.has(prefix))
|
|
47
|
+
continue;
|
|
48
|
+
return identifier;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|