@lumoai/cli 1.49.0 → 1.50.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 +3 -1
- package/assets/skill/references/milestones.md +4 -0
- package/assets/skill/references/sessions.md +30 -2
- package/dist/cli/src/commands/milestone-show.js +9 -2
- package/dist/cli/src/commands/session-attach.js +111 -11
- package/dist/cli/src/commands/session-status.js +8 -0
- package/dist/cli/src/index.js +4 -3
- package/dist/shared/src/gate-map.js +133 -0
- package/package.json +1 -1
package/assets/skill/SKILL.md
CHANGED
|
@@ -157,7 +157,8 @@ lumo fidelity show LUM-42 --json
|
|
|
157
157
|
**Sessions** — see [sessions.md](references/sessions.md)
|
|
158
158
|
|
|
159
159
|
- `lumo session attach <id>` — bind this session to a task (then run `task context`). **Lifetime lock**: re-attaching to the same task is a no-op; attaching to a _different_ task is refused with 409 — start a new Claude Code session instead. No `--force`, no `session detach`.
|
|
160
|
-
- `lumo session
|
|
160
|
+
- `lumo session attach --steward <milestone>` — bind this session to a **milestone** as a **STEWARD (governance) session** (LUM-640): plan & accept only. Governance passes (task create/update, criteria set incl. `--human`, deps confirm/dismiss, `verdict --fail` with session provenance, milestone add/remove, all reads); implementation is gated (`lumo verify` → 409 **without burning a round**; moving a task to in_progress assigned to yourself → 409); commits/PRs produced anyway are recorded as milestone-scoped boundary crossings (fail-closed; disposition stays web+human-only, no CLI clear path). Same lifetime lock: same milestone idempotent, any other attach (a task, or a different milestone) → 409; multiple stewards per milestone are fine. See [sessions.md](references/sessions.md)
|
|
161
|
+
- `lumo session status` — show current binding (a task, or the milestone + STEWARD role)
|
|
161
162
|
- End-of-session housekeeping is fully automatic (no command). On DONE the server runs three best-effort silent passes: Layer-1 memory curation, fragment-usage audit, and blocked-tag automation. See [sessions.md](references/sessions.md)
|
|
162
163
|
- Git-suggest at session start (suggests `session attach`, never auto-binds) + Layer-2 project-memory review — see the reference
|
|
163
164
|
|
|
@@ -192,6 +193,7 @@ Don't guess these:
|
|
|
192
193
|
|
|
193
194
|
- No `lumo session start` — binding is `lumo session attach <LUM-N>`
|
|
194
195
|
- No `lumo session detach` — the session↔task binding is a lifetime lock; to work on a different task, start a new Claude Code session
|
|
196
|
+
- No steward "role switch" or un-steward — `session attach --steward` is the same lifetime lock on a milestone; a steward session never converts to an implementation session (and there is no CLI path to clear its boundary crossings)
|
|
195
197
|
- No `lumo task delete` — tasks can't be deleted from the CLI (web UI only)
|
|
196
198
|
- No `lumo task artifact edit` — it's `lumo task artifact update`
|
|
197
199
|
- No `lumo auth status` — identity check is `lumo whoami`
|
|
@@ -73,6 +73,8 @@ Accepts UUID or name. With a name, `--project <ref>` is required when the worksp
|
|
|
73
73
|
|
|
74
74
|
Prints a key:value header (name, status, **health**, dates, project, description), task counts, and the full task table under the milestone. The `Health:` line shows the same target-date risk light as `milestone list` (`ON-TRACK` / `AT-RISK` / `OVERDUE`, or `-` when none applies).
|
|
75
75
|
|
|
76
|
+
**Staleness hints (LUM-624):** the `Status:` line is annotated `(stale — task progress indicates <STATUS>; auto-updating)` when the stored status lags the task-derived one (the server self-heals the drift in the background — a re-read shows the corrected status), and the `Target:` line is annotated `(no target date — schedule health unavailable)` for an in-flight milestone with no target date.
|
|
77
|
+
|
|
76
78
|
It also prints a **Sprint coverage** section (above the task table) listing which
|
|
77
79
|
sprints the milestone's tasks span — each row shows the sprint number, status, name,
|
|
78
80
|
and `done/total` progress — plus an `Unscheduled` line counting milestone tasks not in any
|
|
@@ -107,6 +109,8 @@ lumo milestone show 11111111-2222-3333-4444-555555555555
|
|
|
107
109
|
|
|
108
110
|
At least one field required.
|
|
109
111
|
|
|
112
|
+
**Status rolls up automatically (LUM-624):** milestone status follows task completion — PLANNED → ACTIVE when any task starts, → COMPLETED when every task is DONE (stamps `completedAt`, triggers the retro summary), and COMPLETED → ACTIVE when a non-DONE task (re)appears. A manual `--status planned/active/completed` that contradicts task reality gets reconciled back on the next task event or detail read; `--status cancelled` and archived milestones are human terminal choices the rollup never touches. You normally don't need `--status completed` by hand anymore.
|
|
113
|
+
|
|
110
114
|
```bash
|
|
111
115
|
lumo milestone update "Q3 Launch" --status active
|
|
112
116
|
lumo milestone update "Q3 Launch" --target 2026-09-15
|
|
@@ -113,12 +113,40 @@ A successful `session attach` also runs a **best-effort team-memory downsync** f
|
|
|
113
113
|
Error: this session is permanently bound to LUM-7 "Other task". A session works one task for its lifetime — start a new Claude Code session to work on LUM-42.
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
There is no `--force` and no `session detach`. To work on a different task, open a new terminal / Claude Code session and run `lumo session attach <new-task>` there.
|
|
116
|
+
There is no `--force` and no `session detach`. To work on a different task, open a new terminal / Claude Code session and run `lumo session attach <new-task>` there. A session already bound as **STEWARD** of a milestone (below) also 409s here — it can never attach a task.
|
|
117
117
|
|
|
118
118
|
#### When to suggest
|
|
119
119
|
|
|
120
120
|
If `session attach` returns 409, do not retry or look for a workaround — start a fresh Claude Code session for the target task.
|
|
121
121
|
|
|
122
|
+
### `lumo session attach --steward <milestone>` — bind as a STEWARD (governance) session (LUM-640)
|
|
123
|
+
|
|
124
|
+
The **second legal session form**: instead of a task, the session binds a **milestone** and becomes a governance role — it plans and accepts, it never implements.
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
lumo session attach --steward "Agent-Native Milestones" # by name (case-insensitive)
|
|
128
|
+
lumo session attach --steward agent-native-milestones # by slug
|
|
129
|
+
lumo session attach --steward cmxyz123... # by milestone id
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- **Resolution:** exact id → case-insensitive name/slug across the workspace's active milestones. An **ambiguous** name is refused with 400 listing the candidates (retry with the id); no match is 404.
|
|
133
|
+
- **Lifetime lock, same semantics as the task form:** re-attach to the **same** milestone = idempotent no-op; attach a **task** after a steward bind → 409; attach a **different milestone** → 409; a task-bound session running `--steward` → 409. No `--force`, no detach — a different binding needs a new Claude Code session.
|
|
134
|
+
- **Multiple stewards per milestone are allowed** (acceptance is multi-round by nature); sessions don't conflict.
|
|
135
|
+
|
|
136
|
+
**Allowed (governance — pass through unchanged, with the session recorded as provenance):** `task create` / `task update`, `task criteria set` (incl. `--human`), `task deps confirm/dismiss`, `verdict --fail` (the send-back rows carry `sourceSessionId`), `milestone add/remove`, and **all read operations**.
|
|
137
|
+
|
|
138
|
+
**Blocked (implementation — first layer, API gate):**
|
|
139
|
+
|
|
140
|
+
- `lumo verify` → **409, no round burned, no VerificationRun written**. The error names the milestone and the remedy (start an implementation session and `lumo session attach <task>`). Do not retry from the steward session.
|
|
141
|
+
- Moving a task to `in_progress` with **yourself as the effective assignee** (whether already assigned to you or assigned in the same call) → 409. Dispatching to _someone else_ is planning and passes.
|
|
142
|
+
- Red line unchanged: a steward writes no PASS of any kind; DONE stays human-only.
|
|
143
|
+
|
|
144
|
+
**Backstop (second layer, detector):** commits / PR creations produced during a steward session anyway (the CLI can't stop local git) are recorded as **`STEWARD_IMPLEMENTATION_ARTIFACT` boundary crossings against the milestone** — fail-closed (an unparseable Bash payload records an _unconfirmed_ crossing rather than nothing), disposition stays **web + human-only**, and there is **no CLI clear path** (`crossing explain` remains an appeal, never a clear).
|
|
145
|
+
|
|
146
|
+
#### When to suggest
|
|
147
|
+
|
|
148
|
+
The user asks to "review/accept/plan a milestone", to act as reviewer/steward across tasks, or wants a governance session that must not implement. If a steward session needs code changed, dispatch the task to an implementation owner (or tell the user to open a new implementation session) — never work around the gate.
|
|
149
|
+
|
|
122
150
|
### Parallel sessions
|
|
123
151
|
|
|
124
152
|
Each Claude Code session has its own `CLAUDE_CODE_SESSION_ID`. Two terminals running `claude code` and binding to different tasks will not interfere — bindings are scoped per session row server-side.
|
|
@@ -129,7 +157,7 @@ Each Claude Code session has its own `CLAUDE_CODE_SESSION_ID`. Two terminals run
|
|
|
129
157
|
lumo session status
|
|
130
158
|
```
|
|
131
159
|
|
|
132
|
-
Prints
|
|
160
|
+
Prints what the current Claude Code session is bound to — a task, a milestone (`Role: STEWARD (governance — plan & accept only)`), or "(no task)" if none. Requires `$CLAUDE_CODE_SESSION_ID` (must run inside Claude Code).
|
|
133
161
|
|
|
134
162
|
#### When to suggest
|
|
135
163
|
|
|
@@ -44,13 +44,19 @@ function formatMilestoneShow(m, tasks) {
|
|
|
44
44
|
m.taskCounts.IN_PROGRESS +
|
|
45
45
|
m.taskCounts.IN_REVIEW +
|
|
46
46
|
m.taskCounts.DONE;
|
|
47
|
+
const statusLine = m.staleness?.statusDrift
|
|
48
|
+
? `Status: ${m.status} (stale — task progress indicates ${(0, sanitize_1.sanitizeField)(m.staleness.statusDrift)}; auto-updating)`
|
|
49
|
+
: `Status: ${m.status}`;
|
|
50
|
+
const targetLine = m.staleness?.datesMissing
|
|
51
|
+
? `Target: ${fmtDate(m.targetDate)} (no target date — schedule health unavailable)`
|
|
52
|
+
: `Target: ${fmtDate(m.targetDate)}`;
|
|
47
53
|
const lines = [
|
|
48
54
|
`Milestone: ${(0, sanitize_1.sanitizeField)(m.name)}`,
|
|
49
|
-
|
|
55
|
+
statusLine,
|
|
50
56
|
`Archived: ${m.archivedAt ? m.archivedAt.slice(0, 10) : 'no'}`,
|
|
51
57
|
`Health: ${fmtHealth(m.health)}`,
|
|
52
58
|
`Start: ${fmtDate(m.startDate)}`,
|
|
53
|
-
|
|
59
|
+
targetLine,
|
|
54
60
|
`Project: ${(0, sanitize_1.sanitizeField)(m.projectName)}`,
|
|
55
61
|
`Description:`,
|
|
56
62
|
` ${m.description && m.description.length > 0 ? (0, sanitize_1.sanitizeField)(m.description) : '-'}`,
|
|
@@ -136,6 +142,7 @@ async function milestoneShow(identifier, opts) {
|
|
|
136
142
|
projectName,
|
|
137
143
|
taskCounts: milestone.taskCounts,
|
|
138
144
|
health: milestone.health,
|
|
145
|
+
staleness: milestone.staleness,
|
|
139
146
|
sprintCoverage: milestone.sprintCoverage,
|
|
140
147
|
}, tasks) + '\n');
|
|
141
148
|
}
|
|
@@ -7,21 +7,30 @@ const sanitize_1 = require("../lib/sanitize");
|
|
|
7
7
|
const resolve_project_1 = require("../lib/resolve-project");
|
|
8
8
|
const memory_auto_1 = require("../lib/memory-auto");
|
|
9
9
|
/**
|
|
10
|
-
* `lumo session attach <identifier
|
|
11
|
-
* Claude Code session to a task
|
|
10
|
+
* `lumo session attach <identifier> [--steward]` — bind the currently-running
|
|
11
|
+
* Claude Code session to a task, or (with `--steward`) to a MILESTONE as a
|
|
12
|
+
* STEWARD (governance) session (LUM-640).
|
|
12
13
|
*
|
|
13
14
|
* Required environment: `CLAUDE_CODE_SESSION_ID` (set automatically by
|
|
14
15
|
* Claude Code). Must be invoked from inside a Claude Code session.
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
-
* Re-attaching to the *same* task is an idempotent
|
|
18
|
-
* Attaching to a
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* Both forms are a **lifetime lock** (LUM-459/LUM-640): the binding is
|
|
18
|
+
* write-once. Re-attaching to the *same* task/milestone is an idempotent
|
|
19
|
+
* no-op. Attaching to anything else — a different task, a different
|
|
20
|
+
* milestone, a task after a steward bind, a milestone after a task bind —
|
|
21
|
+
* is refused with HTTP 409; there is no `--force` and no `session detach`;
|
|
22
|
+
* a different binding requires a brand-new Claude Code session.
|
|
23
|
+
*
|
|
24
|
+
* A STEWARD session plans and accepts only: task create/update, criteria
|
|
25
|
+
* set, deps confirm/dismiss, verdict --fail, milestone add/remove and all
|
|
26
|
+
* reads pass; `lumo verify` and self-dispatch are refused with 409, and
|
|
27
|
+
* implementation artifacts (commits/PRs) are recorded as boundary crossings.
|
|
21
28
|
*/
|
|
22
|
-
async function sessionAttach(identifier) {
|
|
29
|
+
async function sessionAttach(identifier, options) {
|
|
23
30
|
if (!identifier) {
|
|
24
|
-
console.error(
|
|
31
|
+
console.error(options?.steward
|
|
32
|
+
? 'Error: missing <identifier>. Usage: lumo session attach --steward <milestone id | name | slug>'
|
|
33
|
+
: 'Error: missing <identifier>. Usage: lumo session attach <LUM-42>');
|
|
25
34
|
return 1;
|
|
26
35
|
}
|
|
27
36
|
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
@@ -35,6 +44,14 @@ async function sessionAttach(identifier) {
|
|
|
35
44
|
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
36
45
|
return 1;
|
|
37
46
|
}
|
|
47
|
+
if (options?.steward) {
|
|
48
|
+
return stewardAttach({
|
|
49
|
+
identifier,
|
|
50
|
+
sessionId,
|
|
51
|
+
apiUrl: (0, api_1.resolveAuthedApiUrl)(creds.apiUrl),
|
|
52
|
+
token: creds.token,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
38
55
|
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
39
56
|
const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/sessions/${encodeURIComponent(sessionId)}/bind-task`;
|
|
40
57
|
let res;
|
|
@@ -70,12 +87,17 @@ async function sessionAttach(identifier) {
|
|
|
70
87
|
console.error(`Error: ${(0, sanitize_1.sanitizeField)(message)}`);
|
|
71
88
|
return 1;
|
|
72
89
|
}
|
|
73
|
-
// Lifetime lock (LUM-459): the session is permanently bound to another task
|
|
90
|
+
// Lifetime lock (LUM-459): the session is permanently bound to another task —
|
|
91
|
+
// or (LUM-640) it is a STEWARD session bound to a milestone.
|
|
74
92
|
if (res.status === 409) {
|
|
75
93
|
let current = 'another task';
|
|
94
|
+
let stewardMilestone = null;
|
|
76
95
|
try {
|
|
77
96
|
const data = (await res.json());
|
|
78
|
-
if (data.
|
|
97
|
+
if (data.currentMilestoneName) {
|
|
98
|
+
stewardMilestone = (0, sanitize_1.sanitizeField)(data.currentMilestoneName);
|
|
99
|
+
}
|
|
100
|
+
else if (data.currentTaskIdentifier) {
|
|
79
101
|
current = data.currentTaskTitle
|
|
80
102
|
? `${data.currentTaskIdentifier} "${(0, sanitize_1.sanitizeField)(data.currentTaskTitle)}"`
|
|
81
103
|
: data.currentTaskIdentifier;
|
|
@@ -84,6 +106,12 @@ async function sessionAttach(identifier) {
|
|
|
84
106
|
catch {
|
|
85
107
|
// fall through with the generic phrasing
|
|
86
108
|
}
|
|
109
|
+
if (stewardMilestone) {
|
|
110
|
+
console.error(`Error: this session is a STEWARD (governance) session for milestone "${stewardMilestone}" — ` +
|
|
111
|
+
'it cannot attach a task. Start a new Claude Code session to ' +
|
|
112
|
+
`implement ${identifier}.`);
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
87
115
|
console.error(`Error: this session is permanently bound to ${current}. ` +
|
|
88
116
|
'A session works one task for its lifetime — start a new Claude Code ' +
|
|
89
117
|
`session to work on ${identifier}.`);
|
|
@@ -139,3 +167,75 @@ async function sessionAttach(identifier) {
|
|
|
139
167
|
// best-effort — the bind already succeeded; never surface a sync error here
|
|
140
168
|
}
|
|
141
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* LUM-640: the `--steward` form — bind this session to a MILESTONE as a
|
|
172
|
+
* governance session. Same lifetime-lock error surface as the task form; the
|
|
173
|
+
* success output spells out the allowed/blocked action split so the agent
|
|
174
|
+
* knows its lane without a round-trip to the docs.
|
|
175
|
+
*/
|
|
176
|
+
async function stewardAttach(args) {
|
|
177
|
+
const { identifier, sessionId, apiUrl, token } = args;
|
|
178
|
+
const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/sessions/${encodeURIComponent(sessionId)}/bind-milestone`;
|
|
179
|
+
let res;
|
|
180
|
+
try {
|
|
181
|
+
res = await fetch(url, {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
headers: {
|
|
184
|
+
'Content-Type': 'application/json',
|
|
185
|
+
Authorization: `Bearer ${token}`,
|
|
186
|
+
},
|
|
187
|
+
body: JSON.stringify({ milestoneRef: identifier }),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
192
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
if (res.status === 401) {
|
|
196
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
197
|
+
return 1;
|
|
198
|
+
}
|
|
199
|
+
// 400 (ambiguous reference) and 404 (no match) carry actionable server
|
|
200
|
+
// messages — print them verbatim (sanitized).
|
|
201
|
+
if (res.status === 400 || res.status === 404) {
|
|
202
|
+
let message = res.status === 404 ? 'Not found' : 'Bad request';
|
|
203
|
+
try {
|
|
204
|
+
const data = (await res.json());
|
|
205
|
+
if (data.error)
|
|
206
|
+
message = data.error;
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// fall through
|
|
210
|
+
}
|
|
211
|
+
console.error(`Error: ${(0, sanitize_1.sanitizeField)(message)}`);
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
// Lifetime lock: already task-bound, or steward of a different milestone.
|
|
215
|
+
if (res.status === 409) {
|
|
216
|
+
let message = 'this session is already permanently bound';
|
|
217
|
+
try {
|
|
218
|
+
const data = (await res.json());
|
|
219
|
+
if (data.error)
|
|
220
|
+
message = data.error;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// fall through with the generic phrasing
|
|
224
|
+
}
|
|
225
|
+
console.error(`Error: ${(0, sanitize_1.sanitizeField)(message)} ` +
|
|
226
|
+
'A session holds one binding for its lifetime — start a new Claude ' +
|
|
227
|
+
'Code session for a different task or milestone.');
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
if (!res.ok) {
|
|
231
|
+
console.error(`Error: bind-milestone failed (HTTP ${res.status})`);
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
const body = (await res.json());
|
|
235
|
+
console.log(`Attached session ${sessionId} as STEWARD of milestone "${(0, sanitize_1.sanitizeField)(body.milestoneName)}"`);
|
|
236
|
+
console.log('');
|
|
237
|
+
console.log('Governance role — plan & accept only (lifetime lock):');
|
|
238
|
+
console.log(' allowed: task create/update · criteria set (incl. --human) · deps confirm/dismiss · verdict --fail · milestone add/remove · all reads');
|
|
239
|
+
console.log(' blocked: lumo verify (409, no round burned) · moving a task to in_progress assigned to yourself · attaching a task');
|
|
240
|
+
console.log(' note: commits/PRs produced in this session are recorded as boundary crossings (human-reviewed on the web)');
|
|
241
|
+
}
|
|
@@ -45,6 +45,14 @@ async function sessionStatus() {
|
|
|
45
45
|
return 1;
|
|
46
46
|
}
|
|
47
47
|
const data = (await res.json());
|
|
48
|
+
// LUM-640: the steward form — milestone-bound governance session.
|
|
49
|
+
if (data.role === 'STEWARD' && data.milestoneName) {
|
|
50
|
+
process.stdout.write(`Session ${sessionId}\n` +
|
|
51
|
+
` Role: STEWARD (governance — plan & accept only)\n` +
|
|
52
|
+
` Bound to: milestone "${(0, sanitize_1.sanitizeField)(data.milestoneName)}"\n` +
|
|
53
|
+
` Events: ${data.eventCount}\n`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
48
56
|
if (data.taskIdentifier && data.taskTitle) {
|
|
49
57
|
process.stdout.write(`Session ${sessionId}\n` +
|
|
50
58
|
` Bound to: ${data.taskIdentifier} "${(0, sanitize_1.sanitizeField)(data.taskTitle)}"\n` +
|
package/dist/cli/src/index.js
CHANGED
|
@@ -299,11 +299,12 @@ const session = program
|
|
|
299
299
|
.description('Manage per-terminal coding-session context');
|
|
300
300
|
session
|
|
301
301
|
.command('attach <identifier>')
|
|
302
|
-
.description('Attach the currently-running Claude Code session (CLAUDE_CODE_SESSION_ID) to a task.
|
|
303
|
-
.
|
|
302
|
+
.description('Attach the currently-running Claude Code session (CLAUDE_CODE_SESSION_ID) to a task — or, with --steward, to a MILESTONE as a governance session (LUM-640: plan/accept only). Either binding is a lifetime lock: re-attaching the same target is a no-op, anything else is refused — start a new session instead.')
|
|
303
|
+
.option('--steward', 'Bind as a STEWARD (governance) session to a milestone (id, name, or slug) instead of a task. Steward sessions plan and accept only: lumo verify is refused (409, no round burned), self-dispatch to in_progress is refused, and implementation commits/PRs are recorded as boundary crossings.')
|
|
304
|
+
.action(wrap((identifier, options) => (0, session_attach_1.sessionAttach)(identifier, options)));
|
|
304
305
|
session
|
|
305
306
|
.command('status')
|
|
306
|
-
.description('Show
|
|
307
|
+
.description('Show what this Claude Code session is bound to: a task, a milestone (STEWARD governance session), or nothing.')
|
|
307
308
|
.action(wrap(() => (0, session_status_1.sessionStatus)()));
|
|
308
309
|
const task = program
|
|
309
310
|
.command('task')
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Static gate map (LUM-614) — the single source of truth that classifies the
|
|
4
|
+
* engineer-facing mechanical gates into a stable key set, splits them by how
|
|
5
|
+
* their signal is produced, and (for execution gates) carries the command
|
|
6
|
+
* patterns that recognize an executed check in the hook-event stream.
|
|
7
|
+
*
|
|
8
|
+
* Shared by the read-model that derives a delivery's gate audit (server) and
|
|
9
|
+
* the delivery-card panel that renders it (app). It deliberately carries NO
|
|
10
|
+
* status — status is derived per delivery from HookEvent rows, never stored.
|
|
11
|
+
*
|
|
12
|
+
* The class → provenance split seeds the fidelity independence axis: a detection
|
|
13
|
+
* gate is something Lumo derives itself from the tool-intent stream it already
|
|
14
|
+
* ingests (independent of the agent). An execution gate's signal can come from
|
|
15
|
+
* EITHER source: the agent's own tool runs (self-reported — it chose to run the
|
|
16
|
+
* check and could skip it) OR, as of LUM-616, an independent CI run read back
|
|
17
|
+
* from GitHub (check-runs / commit-statuses), which the agent cannot skip or
|
|
18
|
+
* fake. So execution-gate provenance is no longer a pure function of class — it
|
|
19
|
+
* is carried per produced signal (see NormalizedGateEvent.provenance in the
|
|
20
|
+
* gate-audit read-model); `provenanceForClass` is only the not-run/not-configured
|
|
21
|
+
* default. Detection-gate provenance is still class-fixed.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.GATE_DEFINITIONS = exports.ALL_GATE_KEYS = void 0;
|
|
25
|
+
exports.provenanceForClass = provenanceForClass;
|
|
26
|
+
exports.gateDefinition = gateDefinition;
|
|
27
|
+
exports.classifyCommand = classifyCommand;
|
|
28
|
+
exports.classifyGateName = classifyGateName;
|
|
29
|
+
/** Provenance is a pure function of class — the only place the mapping lives. */
|
|
30
|
+
function provenanceForClass(cls) {
|
|
31
|
+
return cls === 'detection' ? 'independent_harness' : 'agent_self_reported';
|
|
32
|
+
}
|
|
33
|
+
const RAW_DEFINITIONS = [
|
|
34
|
+
// 'parse' (node --check) was dropped (LUM-616 follow-up): it's a strict subset
|
|
35
|
+
// of typecheck/build (which both parse before they can run), so a separate
|
|
36
|
+
// parse gate carried no independent information and was never sourced — it sat
|
|
37
|
+
// perpetually not_run. Removed rather than fabricated-green.
|
|
38
|
+
{
|
|
39
|
+
key: 'typecheck',
|
|
40
|
+
class: 'execution',
|
|
41
|
+
// No build-exclusion lookahead: classification is one-command → N gates
|
|
42
|
+
// (LUM-629), so `tsc && npm run build` reports typecheck AND build instead
|
|
43
|
+
// of needing build to win a single-choice tie.
|
|
44
|
+
commandPatterns: [/\btsc\b/, /typecheck/],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: 'build',
|
|
48
|
+
class: 'execution',
|
|
49
|
+
commandPatterns: [/\bnext\s+build\b/, /\bnpm\s+run\s+build\b/],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: 'test',
|
|
53
|
+
class: 'execution',
|
|
54
|
+
commandPatterns: [/\bjest\b/, /\bvitest\b/, /\bnpm\s+(run\s+)?test\b/],
|
|
55
|
+
},
|
|
56
|
+
// Detection-class gates were all removed (LUM-616 follow-up): detection belongs
|
|
57
|
+
// in boundary crossings, not audit-only gates.
|
|
58
|
+
// - 'git-hygiene' (committed secrets) ≡ the HARDCODED_SECRET crossing, which
|
|
59
|
+
// is detected server-side, surfaced, AND blocks DONE — strictly stronger.
|
|
60
|
+
// - 'diff-scope' (out-of-declared-scope edits) has no source of truth yet
|
|
61
|
+
// (Lumo has no "declared scope"); the right home is a future
|
|
62
|
+
// OUT_OF_SCOPE_EDIT crossing, not a perpetually not_configured gate.
|
|
63
|
+
// The 'detection' GateClass stays as scaffolding for a clean future re-add.
|
|
64
|
+
];
|
|
65
|
+
const DEFINITIONS = RAW_DEFINITIONS.reduce((acc, raw) => {
|
|
66
|
+
acc[raw.key] = { ...raw, provenance: provenanceForClass(raw.class) };
|
|
67
|
+
return acc;
|
|
68
|
+
}, {});
|
|
69
|
+
exports.ALL_GATE_KEYS = RAW_DEFINITIONS.map(d => d.key);
|
|
70
|
+
exports.GATE_DEFINITIONS = exports.ALL_GATE_KEYS.map(k => DEFINITIONS[k]);
|
|
71
|
+
/** Look up a gate's definition by key. */
|
|
72
|
+
function gateDefinition(key) {
|
|
73
|
+
return DEFINITIONS[key];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Strip double- and single-quoted substrings from a command line, leaving only
|
|
77
|
+
* the unquoted invocation tokens. This is what stops a gate keyword that merely
|
|
78
|
+
* appears inside an *argument* — e.g. a `lumo task create "…parse/typecheck/
|
|
79
|
+
* build/test…"` description, or `echo "…run the test…"` — from being mistaken
|
|
80
|
+
* for an invocation of that gate (the LUM-614 prod false-positive, where a
|
|
81
|
+
* task-create description was classified as a passed typecheck and overwrote
|
|
82
|
+
* the real `tsc` signal). A real gate run names its tool unquoted, so it
|
|
83
|
+
* survives the strip.
|
|
84
|
+
*/
|
|
85
|
+
function unquotedInvocation(command) {
|
|
86
|
+
return command.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, ' ');
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Classify an executed shell command as execution gates — EVERY gate whose
|
|
90
|
+
* invocation tokens the command hits, in declaration order; empty when none.
|
|
91
|
+
* One command → N gates (LUM-629): agents routinely chain checks in a single
|
|
92
|
+
* shell line (`npx jest …; npx tsc --noEmit`), and single-choice classification
|
|
93
|
+
* silently dropped all but one gate's signal (the LUM-620 #770 test=not_run
|
|
94
|
+
* under-report). Matching still runs against the unquoted invocation only, so
|
|
95
|
+
* a gate keyword buried in quoted argument text never produces a false signal
|
|
96
|
+
* (LUM-614). Detection gates are never returned here — they're not derived
|
|
97
|
+
* from commands.
|
|
98
|
+
*/
|
|
99
|
+
function classifyCommand(command) {
|
|
100
|
+
const invocation = unquotedInvocation(command);
|
|
101
|
+
return exports.GATE_DEFINITIONS.filter(def => def.commandPatterns.some(p => p.test(invocation))).map(def => def.key);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Patterns that classify a CI check-run / commit-status NAME (not a shell
|
|
105
|
+
* command) as an execution gate. LUM-616: the independent harness reads CI
|
|
106
|
+
* signals straight from GitHub (check-runs + commit-statuses) across ANY
|
|
107
|
+
* provider, and the only semantics GitHub exposes is each check's name plus its
|
|
108
|
+
* pass/fail — so mapping a named check to a gate is unavoidably name-based.
|
|
109
|
+
* Platform-agnostic on purpose (GitHub Actions, Vercel, CircleCI, Travis, …).
|
|
110
|
+
* First match wins; order = most specific (typecheck/test) before the build
|
|
111
|
+
* catch-all so a "build and test" job lands on the narrower gate.
|
|
112
|
+
*/
|
|
113
|
+
const CHECK_NAME_PATTERNS = [
|
|
114
|
+
['typecheck', [/\btypecheck\b/i, /\btsc\b/i, /type[\s-]?check/i]],
|
|
115
|
+
['test', [/\btests?\b/i, /\bjest\b/i, /\bvitest\b/i, /\bspec\b/i]],
|
|
116
|
+
['build', [/\bbuild\b/i, /\bvercel\b/i, /\bdeploy/i]],
|
|
117
|
+
];
|
|
118
|
+
/**
|
|
119
|
+
* Classify a CI check / commit-status name as an execution gate, or null when
|
|
120
|
+
* it matches none — an unrecognized check is dropped, never coerced into a
|
|
121
|
+
* fabricated gate. A name carrying "comment" (e.g. "Vercel Preview Comments")
|
|
122
|
+
* is a bot annotation, not a CI check, and is rejected before pattern matching.
|
|
123
|
+
*/
|
|
124
|
+
function classifyGateName(name) {
|
|
125
|
+
const n = name.trim();
|
|
126
|
+
if (!n || /comment/i.test(n))
|
|
127
|
+
return null;
|
|
128
|
+
for (const [key, patterns] of CHECK_NAME_PATTERNS) {
|
|
129
|
+
if (patterns.some(p => p.test(n)))
|
|
130
|
+
return key;
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|