@lumoai/cli 1.50.0 → 1.51.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 +1 -0
- package/assets/skill/references/milestones.md +75 -8
- package/assets/skill/references/tasks.md +10 -6
- package/dist/cli/src/commands/milestone-criteria-list.js +80 -0
- package/dist/cli/src/commands/milestone-criteria-set.js +145 -0
- package/dist/cli/src/commands/milestone-criteria-verdict.js +129 -0
- package/dist/cli/src/commands/milestone-update.js +34 -1
- package/dist/cli/src/index.js +29 -1
- package/dist/cli/src/lib/rank-tasks.js +30 -2
- package/package.json +1 -1
package/assets/skill/SKILL.md
CHANGED
|
@@ -126,6 +126,7 @@ lumo fidelity show LUM-42 --json
|
|
|
126
126
|
- `lumo milestone add/remove <id> <task...>` — batch bind/unbind tasks
|
|
127
127
|
- `lumo milestone summary [--retry]` — AI retro
|
|
128
128
|
- `lumo milestone reorder/move` — manual ordering
|
|
129
|
+
- `lumo milestone criteria set/list/verdict` — milestone-level **exit criteria** (LUM-643): the close contract, drafted & adjudicated (裁定) by the steward (LUM-640) or a human. Unresolved criteria (pending or NOT_MET) block `--status completed` **and** the LUM-624 auto-rollup with 409/fail-closed; human override is web-only (`?force=1`, stamped as a persistent trail). `verdict --met` on `EXTERNAL_FACT` requires `--evidence` (422); `PENDING_OUTCOME` can never be MET
|
|
129
130
|
|
|
130
131
|
**Documents** — CRUD/sharing → [docs.md](references/docs.md); **editing live docs** (raw/section/patch/append/diff/rebuild) → [doc-editing.md](references/doc-editing.md)
|
|
131
132
|
|
|
@@ -98,25 +98,92 @@ lumo milestone show 11111111-2222-3333-4444-555555555555
|
|
|
98
98
|
|
|
99
99
|
### `lumo milestone update <identifier>` — patch a milestone
|
|
100
100
|
|
|
101
|
-
| Flag | Type | Notes
|
|
102
|
-
| ---------------------- | ------ |
|
|
103
|
-
| `--project <ref>` | string | Required when identifier is a name and workspace has >1 project.
|
|
104
|
-
| `-n, --name <text>` | string | Cannot be empty.
|
|
105
|
-
| `-d, --description <>` | string | `--description ""` clears.
|
|
106
|
-
| `-s, --status <value>` | enum | `planned \| active \| completed \| cancelled`.
|
|
107
|
-
| `--start <date>` | string | `--start ""` clears.
|
|
108
|
-
| `--target <date>` | string | `--target ""` clears.
|
|
101
|
+
| Flag | Type | Notes |
|
|
102
|
+
| ---------------------- | ------ | ------------------------------------------------------------------------------ |
|
|
103
|
+
| `--project <ref>` | string | Required when identifier is a name and workspace has >1 project. |
|
|
104
|
+
| `-n, --name <text>` | string | Cannot be empty. |
|
|
105
|
+
| `-d, --description <>` | string | `--description ""` clears. |
|
|
106
|
+
| `-s, --status <value>` | enum | `planned \| active \| completed \| cancelled`. |
|
|
107
|
+
| `--start <date>` | string | `--start ""` clears. |
|
|
108
|
+
| `--target <date>` | string | `--target ""` clears. |
|
|
109
|
+
| `--token-budget <n>` | int | Advisory token budget (LUM-644), positive integer; `--token-budget ""` clears. |
|
|
109
110
|
|
|
110
111
|
At least one field required.
|
|
111
112
|
|
|
113
|
+
**Token budget is advisory only (LUM-644):** the web Overview's tokens KPI card shows burn vs budget, and when the MEASURED burn exceeds the budget an advisory alert appears in the Risk & pending queue. Nothing is ever blocked by it — no hard gate. Unmeasured burn reads as unknown, never as over/under budget.
|
|
114
|
+
|
|
115
|
+
**When to suggest**: when the user wants to cap/track token spend on a milestone ("给这个 milestone 设 token 预算 / budget"), or asks why the Overview shows an over-budget alert.
|
|
116
|
+
|
|
112
117
|
**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
118
|
|
|
119
|
+
**The close is gated by exit criteria (LUM-643):** when the milestone has exit criteria (see `milestone criteria` below), any row not adjudicated MET — unadjudicated (PENDING) **or** NOT_MET — refuses `--status completed` with **409**, and holds the LUM-624 auto-rollup fail-closed (all tasks DONE keeps the milestone ACTIVE, no retro fires). There is **no CLI/agent override**: the only escape hatch is a human closing via the web with an explicit override (`?force=1` + optional note), stamped on the milestone as a persistent trail (`completedOverride*` — who/when/note, never auto-cleared). To open the gate legitimately, adjudicate the contract with `lumo milestone criteria verdict`.
|
|
120
|
+
|
|
114
121
|
```bash
|
|
115
122
|
lumo milestone update "Q3 Launch" --status active
|
|
116
123
|
lumo milestone update "Q3 Launch" --target 2026-09-15
|
|
117
124
|
lumo milestone update "Q3 Launch" --description ""
|
|
125
|
+
lumo milestone update "Q3 Launch" --token-budget 50000000
|
|
126
|
+
lumo milestone update "Q3 Launch" --token-budget ""
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### `lumo milestone criteria set <identifier> --file <criteria.json> [--human]` — submit the exit contract
|
|
130
|
+
|
|
131
|
+
Milestone-level **exit criteria** (LUM-643) — the close contract, mirroring the task-level acceptance contract where it transfers. Key differences from `task criteria`: there is **no `verifierType` / no `checkpointer`** — exit criteria are **adjudicated (裁定)**, not machine-verified — and each item requires only `statement` + `referentKind`. The **steward session (LUM-640) is the intended drafting & adjudicating subject** (drafting is a governance action; session provenance is recorded on every row); `--human` records a transcribed HUMAN_EDIT revision instead.
|
|
132
|
+
|
|
133
|
+
The file is a JSON array:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
[
|
|
137
|
+
{
|
|
138
|
+
"statement": "Every LUM-64x migration is applied on prod (migrate status clean)",
|
|
139
|
+
"referentKind": "EXTERNAL_FACT"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"statement": "All milestone read models render the new fields",
|
|
143
|
+
"referentKind": "AGENT_CONSTRUCTED_STATE"
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Semantics: **full-group replace** — existing rows with a verdict are tombstoned (their adjudication trail survives for audit), verdict-free rows are hard-deleted. The contract **locks with 409 once the milestone is COMPLETED** (reopen to edit). Archived milestones also refuse with 409.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
lumo milestone criteria set "Q3 Launch" --file exit-criteria.json
|
|
152
|
+
lumo milestone criteria set "Q3 Launch" --file exit-criteria.json --human
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### `lumo milestone criteria list <identifier>` — print the exit contract
|
|
156
|
+
|
|
157
|
+
One line per criterion — `<id> [MET|NOT_MET|PENDING] SOURCE ⟨REFERENT⟩ statement` — plus an indented `↳` detail line (date · evidence · note) for adjudicated rows, and a trailing gate-state line: either `All exit criteria MET — the close gate is open.` or `N unresolved (pending or NOT_MET) — the close gate refuses status→COMPLETED until adjudicated (human override only via web).`
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
lumo milestone criteria list "Q3 Launch"
|
|
118
161
|
```
|
|
119
162
|
|
|
163
|
+
### `lumo milestone criteria verdict <identifier> <criterion> --met|--unmet` — adjudicate one criterion
|
|
164
|
+
|
|
165
|
+
Records the adjudication (裁定) on one exit criterion — verdict, who, when, and the adjudicating session (LUM-640 steward provenance) are stamped server-side; re-adjudication overwrites in place (latest verdict wins). `<criterion>` accepts the full id or a **≥6-char unique id prefix** from `criteria list` (ambiguous/unknown selectors list candidates and exit 1).
|
|
166
|
+
|
|
167
|
+
| Flag | Type | Notes |
|
|
168
|
+
| ---------------------- | ------- | ----------------------------------------------------------------------------------------------------- |
|
|
169
|
+
| `--met` / `--unmet` | boolean | Exactly one required. |
|
|
170
|
+
| `--note <text>` | string | Why this verdict (recommended). |
|
|
171
|
+
| `--evidence <pointer>` | string | **Required for `--met` on an `EXTERNAL_FACT` criterion** (422 otherwise) — the fact it was read from. |
|
|
172
|
+
| `--project <ref>` | string | Required when identifier is a name and workspace has >1 project. |
|
|
173
|
+
|
|
174
|
+
**Grounding gate (LUM-602/605 reuse):** `--met` on `EXTERNAL_FACT` without `--evidence` → **422** (re-ground or honestly re-declare the criterion `AGENT_CONSTRUCTED_STATE`); a `PENDING_OUTCOME` criterion can **never** be adjudicated MET (its truth lands post-delivery — use the outcome well). `--unmet` never needs evidence.
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
lumo milestone criteria verdict "Q3 Launch" cmexit0001 --met --evidence "prisma migrate status @ prod, 2026-07-02" --note "all LUM-64x applied"
|
|
178
|
+
lumo milestone criteria verdict "Q3 Launch" cmexit0002 --unmet --note "read model not wired yet"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### When to suggest `milestone criteria`
|
|
182
|
+
|
|
183
|
+
- A steward session planning a milestone ("what does done mean for this milestone", "draft exit criteria") → `criteria set`.
|
|
184
|
+
- Before recommending `milestone update --status completed`, or when it just bounced with 409 → `criteria list` to see what holds the gate, then `verdict` per criterion.
|
|
185
|
+
- User asks "can we close this milestone / what's blocking the close" → `criteria list` (the gate-state line answers it).
|
|
186
|
+
|
|
120
187
|
### `lumo milestone delete <identifier>` — delete a milestone
|
|
121
188
|
|
|
122
189
|
Requires `--yes`. No interactive prompt — CLI is agent-friendly. Tasks under the milestone keep their data; their `milestoneId` is cleared.
|
|
@@ -177,10 +177,14 @@ one-line reason. Read-only — it does **not** bind or load context. Pick one fr
|
|
|
177
177
|
the list, then run `lumo session attach <LUM-N>` + `lumo task context <LUM-N>`.
|
|
178
178
|
|
|
179
179
|
Ranking is lexicographic: **priority** (URGENT→LOW) first, then **active-sprint
|
|
180
|
-
membership**, then **
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
180
|
+
membership**, then **dependency topology** (LUM-645: tasks with an open
|
|
181
|
+
CONFIRMED blocker sort after workable ones; among workable peers the task that
|
|
182
|
+
directly unblocks the most open tasks ranks first), then **due date** (earlier
|
|
183
|
+
first), then in-flight status (IN_PROGRESS / IN_REVIEW ahead of TODO). DONE
|
|
184
|
+
tasks are excluded. The active sprint lookup is best-effort — if it fails the
|
|
185
|
+
command still recommends, just without the sprint boost. The topology counts
|
|
186
|
+
come from the server (`/api/tasks/me`); against an older server without them
|
|
187
|
+
the order is simply the pre-topology ranking.
|
|
184
188
|
|
|
185
189
|
| Flag | Type | Notes |
|
|
186
190
|
| ----------------- | ------- | ----------------------------------------------------------------------- |
|
|
@@ -199,9 +203,9 @@ Top 3 recommended tasks (of 12 open):
|
|
|
199
203
|
1. LUM-42 IN_PROGRESS URGENT Fix Slack OAuth redirect
|
|
200
204
|
↳ URGENT · active sprint · due 2026-06-03 (overdue) · in progress
|
|
201
205
|
2. LUM-48 TODO HIGH Investigate slow query
|
|
202
|
-
↳ HIGH · active sprint
|
|
206
|
+
↳ HIGH · active sprint · unblocks 2 tasks
|
|
203
207
|
3. LUM-12 TODO MEDIUM Add rate limiting
|
|
204
|
-
↳ MEDIUM · due 2026-06-10
|
|
208
|
+
↳ MEDIUM · blocked by 1 open dep · due 2026-06-10
|
|
205
209
|
|
|
206
210
|
Next: lumo session attach LUM-42 && lumo task context LUM-42
|
|
207
211
|
```
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatExitCriteriaRows = formatExitCriteriaRows;
|
|
4
|
+
exports.milestoneCriteriaList = milestoneCriteriaList;
|
|
5
|
+
const config_1 = require("../lib/config");
|
|
6
|
+
const api_1 = require("../lib/api");
|
|
7
|
+
const resolve_1 = require("../lib/resolve");
|
|
8
|
+
const sanitize_1 = require("../lib/sanitize");
|
|
9
|
+
/**
|
|
10
|
+
* Render exit-criterion rows for stdout. One line per criterion —
|
|
11
|
+
* `<id> [MET|NOT_MET|PENDING] SOURCE ⟨REFERENT⟩ statement` — plus an
|
|
12
|
+
* indented verdict detail line when adjudicated. Shared by `criteria list`,
|
|
13
|
+
* the `criteria set` echo and `criteria verdict`.
|
|
14
|
+
*/
|
|
15
|
+
function formatExitCriteriaRows(criteria) {
|
|
16
|
+
const lines = [];
|
|
17
|
+
for (const c of criteria) {
|
|
18
|
+
const verdict = c.verdict ?? 'PENDING';
|
|
19
|
+
lines.push(`${c.id} [${verdict}] ${c.source} ⟨${c.referentKind}⟩ ${(0, sanitize_1.sanitizeField)(c.statement)}`);
|
|
20
|
+
if (c.verdict) {
|
|
21
|
+
const parts = [];
|
|
22
|
+
if (c.verdictAt)
|
|
23
|
+
parts.push(`at ${c.verdictAt.slice(0, 10)}`);
|
|
24
|
+
if (c.verdictEvidence)
|
|
25
|
+
parts.push(`evidence: ${(0, sanitize_1.sanitizeField)(c.verdictEvidence)}`);
|
|
26
|
+
if (c.verdictNote)
|
|
27
|
+
parts.push(`note: ${(0, sanitize_1.sanitizeField)(c.verdictNote)}`);
|
|
28
|
+
if (parts.length > 0)
|
|
29
|
+
lines.push(` ↳ ${parts.join(' · ')}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return lines.length > 0 ? lines.join('\n') + '\n' : '';
|
|
33
|
+
}
|
|
34
|
+
/** `lumo milestone criteria list <milestone>` — print the exit contract. */
|
|
35
|
+
async function milestoneCriteriaList(identifier, opts) {
|
|
36
|
+
const creds = (0, config_1.readCredentials)();
|
|
37
|
+
if (!creds) {
|
|
38
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
42
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
43
|
+
let resolved;
|
|
44
|
+
try {
|
|
45
|
+
resolved = await (0, resolve_1.resolveMilestoneId)(base, creds.token, identifier, opts.project);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
let res;
|
|
52
|
+
try {
|
|
53
|
+
res = await fetch(`${base}/api/milestones/${resolved.id}/criteria`, {
|
|
54
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
59
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
if (res.status === 401) {
|
|
63
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
const body = (await res.json().catch(() => null));
|
|
68
|
+
console.error(`Error: exit-criteria list failed (HTTP ${res.status})${body?.error ? `: ${(0, sanitize_1.sanitizeField)(body.error)}` : ''}`);
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
const data = (await res.json());
|
|
72
|
+
if (data.criteria.length === 0) {
|
|
73
|
+
process.stdout.write(`No exit criteria on this milestone — draft them and submit with lumo milestone criteria set ${identifier} --file <criteria.json>\n`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
process.stdout.write(formatExitCriteriaRows(data.criteria));
|
|
77
|
+
process.stdout.write(data.unresolvedCount === 0
|
|
78
|
+
? 'All exit criteria MET — the close gate is open.\n'
|
|
79
|
+
: `${data.unresolvedCount} unresolved (pending or NOT_MET) — the close gate refuses status→COMPLETED until adjudicated (human override only via web).\n`);
|
|
80
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseExitCriteriaJson = parseExitCriteriaJson;
|
|
4
|
+
exports.milestoneCriteriaSet = milestoneCriteriaSet;
|
|
5
|
+
const config_1 = require("../lib/config");
|
|
6
|
+
const api_1 = require("../lib/api");
|
|
7
|
+
const resolve_1 = require("../lib/resolve");
|
|
8
|
+
const doc_input_1 = require("../lib/doc-input");
|
|
9
|
+
const path_guard_1 = require("../lib/path-guard");
|
|
10
|
+
const sanitize_1 = require("../lib/sanitize");
|
|
11
|
+
const milestone_criteria_list_1 = require("./milestone-criteria-list");
|
|
12
|
+
const REFERENT_KINDS = [
|
|
13
|
+
'EXTERNAL_FACT',
|
|
14
|
+
'AGENT_CONSTRUCTED_STATE',
|
|
15
|
+
'PENDING_OUTCOME',
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Client-side shape gate: fail fast on obviously malformed JSON before the
|
|
19
|
+
* round-trip. Full validation stays server-side. The file is a JSON array;
|
|
20
|
+
* one object per criterion: { statement, referentKind } — no verifierType /
|
|
21
|
+
* checkpointer (milestone exit criteria are adjudicated, not machine-run).
|
|
22
|
+
*/
|
|
23
|
+
function parseExitCriteriaJson(raw) {
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(raw);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
error: `file is not valid JSON (${err instanceof Error ? err.message : String(err)})`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (!Array.isArray(parsed)) {
|
|
35
|
+
return { ok: false, error: 'file must be a JSON array of criteria' };
|
|
36
|
+
}
|
|
37
|
+
if (parsed.length === 0) {
|
|
38
|
+
return { ok: false, error: 'file contains an empty array' };
|
|
39
|
+
}
|
|
40
|
+
for (const [i, item] of parsed.entries()) {
|
|
41
|
+
if (item === null || typeof item !== 'object' || Array.isArray(item)) {
|
|
42
|
+
return { ok: false, error: `item ${i} is not an object` };
|
|
43
|
+
}
|
|
44
|
+
const o = item;
|
|
45
|
+
if (typeof o.statement !== 'string' || o.statement.trim() === '') {
|
|
46
|
+
return { ok: false, error: `item ${i} is missing "statement"` };
|
|
47
|
+
}
|
|
48
|
+
if (typeof o.referentKind !== 'string' ||
|
|
49
|
+
!REFERENT_KINDS.includes(o.referentKind)) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
error: `item ${i} needs "referentKind" — one of ${REFERENT_KINDS.join(' | ')}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, items: parsed };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* `lumo milestone criteria set <milestone> --file <criteria.json> [--human]`
|
|
60
|
+
* — submit the whole exit contract (LUM-643, full-group replace; adjudicated
|
|
61
|
+
* rows are tombstoned server-side so their verdict trail survives). Default
|
|
62
|
+
* source is AGENT_DRAFT (the steward draft); `--human` records a transcribed
|
|
63
|
+
* HUMAN_EDIT revision. Locked with 409 once the milestone is COMPLETED.
|
|
64
|
+
*/
|
|
65
|
+
async function milestoneCriteriaSet(identifier, opts) {
|
|
66
|
+
if (!opts.file) {
|
|
67
|
+
console.error('Error: --file <criteria.json> is required. The file is a JSON array of { statement, referentKind }.');
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
const pathVerdict = (0, path_guard_1.checkArtifactFilePath)(opts.file);
|
|
71
|
+
if (!pathVerdict.ok) {
|
|
72
|
+
if (pathVerdict.reason === 'unreadable') {
|
|
73
|
+
console.error(`Error: ${(0, doc_input_1.unreadableFileMessage)(opts.file)}`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
console.error(`Error: refusing to read ${opts.file} — ${pathVerdict.detail}. ` +
|
|
77
|
+
`criteria --file must be a non-sensitive path inside the project directory.`);
|
|
78
|
+
}
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
let raw;
|
|
82
|
+
try {
|
|
83
|
+
raw = await (0, doc_input_1.readFileUtf8)(pathVerdict.resolved);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
console.error(`Error: ${(0, doc_input_1.unreadableFileMessage)(opts.file)}`);
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
const parsed = parseExitCriteriaJson(raw);
|
|
90
|
+
if (!parsed.ok) {
|
|
91
|
+
console.error(`Error: ${parsed.error}`);
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
const creds = (0, config_1.readCredentials)();
|
|
95
|
+
if (!creds) {
|
|
96
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
100
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
101
|
+
let resolved;
|
|
102
|
+
try {
|
|
103
|
+
resolved = await (0, resolve_1.resolveMilestoneId)(base, creds.token, identifier, opts.project);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
const headers = {
|
|
110
|
+
Authorization: `Bearer ${creds.token}`,
|
|
111
|
+
'Content-Type': 'application/json',
|
|
112
|
+
};
|
|
113
|
+
// Steward/session provenance (LUM-640) — recorded on each drafted row.
|
|
114
|
+
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
115
|
+
if (sessionId)
|
|
116
|
+
headers['X-Lumo-Session-Id'] = sessionId;
|
|
117
|
+
let res;
|
|
118
|
+
try {
|
|
119
|
+
res = await fetch(`${base}/api/milestones/${resolved.id}/criteria`, {
|
|
120
|
+
method: 'PUT',
|
|
121
|
+
headers,
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
source: opts.human ? 'HUMAN_EDIT' : 'AGENT_DRAFT',
|
|
124
|
+
criteria: parsed.items,
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
130
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
if (res.status === 401) {
|
|
134
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
if (!res.ok) {
|
|
138
|
+
const body = (await res.json().catch(() => null));
|
|
139
|
+
console.error(`Error: exit-criteria set failed (HTTP ${res.status})${body?.error ? `: ${(0, sanitize_1.sanitizeField)(body.error)}` : ''}`);
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
const data = (await res.json());
|
|
143
|
+
process.stdout.write(`Exit contract set (${data.criteria.length} criteria) — adjudicate with lumo milestone criteria verdict\n`);
|
|
144
|
+
process.stdout.write((0, milestone_criteria_list_1.formatExitCriteriaRows)(data.criteria));
|
|
145
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveCriterionSelector = resolveCriterionSelector;
|
|
4
|
+
exports.milestoneCriteriaVerdict = milestoneCriteriaVerdict;
|
|
5
|
+
const config_1 = require("../lib/config");
|
|
6
|
+
const api_1 = require("../lib/api");
|
|
7
|
+
const resolve_1 = require("../lib/resolve");
|
|
8
|
+
const sanitize_1 = require("../lib/sanitize");
|
|
9
|
+
const milestone_criteria_list_1 = require("./milestone-criteria-list");
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a criterion selector (full id or a ≥6-char unique id prefix)
|
|
12
|
+
* against the milestone's active contract. Ambiguous / unknown selectors
|
|
13
|
+
* list candidates and fail — mirrors `task deps` edge selection.
|
|
14
|
+
*/
|
|
15
|
+
function resolveCriterionSelector(selector, criteria) {
|
|
16
|
+
const exact = criteria.find(c => c.id === selector);
|
|
17
|
+
if (exact)
|
|
18
|
+
return { ok: true, id: exact.id };
|
|
19
|
+
if (selector.length < 6) {
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
error: `criterion selector "${selector}" is too short — pass the id (or a prefix of at least 6 chars) from \`lumo milestone criteria list\``,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const hits = criteria.filter(c => c.id.startsWith(selector));
|
|
26
|
+
if (hits.length === 1)
|
|
27
|
+
return { ok: true, id: hits[0].id };
|
|
28
|
+
if (hits.length === 0) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
error: `no exit criterion matches "${selector}" — see \`lumo milestone criteria list\``,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const candidates = hits
|
|
35
|
+
.map(h => ` ${h.id} ${h.statement.slice(0, 60)}`)
|
|
36
|
+
.join('\n');
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
error: `ambiguous selector "${selector}" matches ${hits.length} criteria:\n${candidates}`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `lumo milestone criteria verdict <milestone> <criterion> --met|--unmet`
|
|
44
|
+
* — adjudicate (裁定) one exit criterion (LUM-643). The steward session
|
|
45
|
+
* (LUM-640) is the intended adjudicating subject; who/when/session are
|
|
46
|
+
* stamped server-side. Grounding gate: `--met` on an EXTERNAL_FACT criterion
|
|
47
|
+
* requires `--evidence`, and a PENDING_OUTCOME criterion can never be MET —
|
|
48
|
+
* both rejected with 422 server-side.
|
|
49
|
+
*/
|
|
50
|
+
async function milestoneCriteriaVerdict(identifier, criterionSelector, opts) {
|
|
51
|
+
if (opts.met === opts.unmet) {
|
|
52
|
+
console.error('Error: pass exactly one of --met or --unmet');
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
const creds = (0, config_1.readCredentials)();
|
|
56
|
+
if (!creds) {
|
|
57
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
61
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
62
|
+
let resolved;
|
|
63
|
+
try {
|
|
64
|
+
resolved = await (0, resolve_1.resolveMilestoneId)(base, creds.token, identifier, opts.project);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
// Fetch the active contract to resolve the criterion selector client-side.
|
|
71
|
+
let listRes;
|
|
72
|
+
try {
|
|
73
|
+
listRes = await fetch(`${base}/api/milestones/${resolved.id}/criteria`, {
|
|
74
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
79
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
if (!listRes.ok) {
|
|
83
|
+
console.error(`Error: exit-criteria list failed (HTTP ${listRes.status})`);
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
const { criteria } = (await listRes.json());
|
|
87
|
+
const selector = resolveCriterionSelector(criterionSelector, criteria);
|
|
88
|
+
if (!selector.ok) {
|
|
89
|
+
console.error(`Error: ${selector.error}`);
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
const headers = {
|
|
93
|
+
Authorization: `Bearer ${creds.token}`,
|
|
94
|
+
'Content-Type': 'application/json',
|
|
95
|
+
};
|
|
96
|
+
// Adjudicating-session provenance (LUM-640) — stamped on the verdict.
|
|
97
|
+
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
98
|
+
if (sessionId)
|
|
99
|
+
headers['X-Lumo-Session-Id'] = sessionId;
|
|
100
|
+
let res;
|
|
101
|
+
try {
|
|
102
|
+
res = await fetch(`${base}/api/milestones/${resolved.id}/criteria/${selector.id}/verdict`, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers,
|
|
105
|
+
body: JSON.stringify({
|
|
106
|
+
verdict: opts.met ? 'MET' : 'NOT_MET',
|
|
107
|
+
...(opts.note ? { note: opts.note } : {}),
|
|
108
|
+
...(opts.evidence ? { evidence: opts.evidence } : {}),
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
114
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
if (res.status === 401) {
|
|
118
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
if (!res.ok) {
|
|
122
|
+
const body = (await res.json().catch(() => null));
|
|
123
|
+
console.error(`Error: verdict failed (HTTP ${res.status})${body?.error ? `: ${(0, sanitize_1.sanitizeField)(body.error)}` : ''}`);
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
const data = (await res.json());
|
|
127
|
+
process.stdout.write(`Adjudicated ${data.criterion.verdict} on exit criterion:\n`);
|
|
128
|
+
process.stdout.write((0, milestone_criteria_list_1.formatExitCriteriaRows)([data.criterion]));
|
|
129
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.normalizeMilestoneStatus = normalizeMilestoneStatus;
|
|
4
|
+
exports.normalizeTokenBudget = normalizeTokenBudget;
|
|
4
5
|
exports.buildMilestoneUpdatePayload = buildMilestoneUpdatePayload;
|
|
5
6
|
exports.formatMilestoneUpdateSummary = formatMilestoneUpdateSummary;
|
|
6
7
|
exports.milestoneUpdate = milestoneUpdate;
|
|
@@ -22,6 +23,21 @@ function normalizeMilestoneStatus(value) {
|
|
|
22
23
|
? upper
|
|
23
24
|
: null;
|
|
24
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* LUM-644: '' clears the budget (→ null, the nullable-flag convention);
|
|
28
|
+
* otherwise the value must be a positive integer within Postgres INT range.
|
|
29
|
+
* Invalid input → undefined (caller prints the error and exits 1).
|
|
30
|
+
*/
|
|
31
|
+
function normalizeTokenBudget(value) {
|
|
32
|
+
if (value === '')
|
|
33
|
+
return null;
|
|
34
|
+
if (!/^\d+$/.test(value))
|
|
35
|
+
return undefined;
|
|
36
|
+
const n = Number(value);
|
|
37
|
+
if (!Number.isSafeInteger(n) || n < 1 || n > 2_147_483_647)
|
|
38
|
+
return undefined;
|
|
39
|
+
return n;
|
|
40
|
+
}
|
|
25
41
|
function buildMilestoneUpdatePayload(opts) {
|
|
26
42
|
const payload = {};
|
|
27
43
|
const flagsGiven = [];
|
|
@@ -45,6 +61,12 @@ function buildMilestoneUpdatePayload(opts) {
|
|
|
45
61
|
payload.targetDate = opts.target === '' ? null : opts.target;
|
|
46
62
|
flagsGiven.push('--target');
|
|
47
63
|
}
|
|
64
|
+
if (opts.tokenBudget !== undefined) {
|
|
65
|
+
// The caller validated eagerly (normalizeTokenBudget); '' means clear.
|
|
66
|
+
payload.tokenBudget =
|
|
67
|
+
opts.tokenBudget === '' ? null : Number(opts.tokenBudget);
|
|
68
|
+
flagsGiven.push('--token-budget');
|
|
69
|
+
}
|
|
48
70
|
return { payload, flagsGiven };
|
|
49
71
|
}
|
|
50
72
|
function fmtDate(v) {
|
|
@@ -72,6 +94,11 @@ function formatMilestoneUpdateSummary(before, after) {
|
|
|
72
94
|
if (after.targetDate !== undefined) {
|
|
73
95
|
changes.push(`target → ${fmtDate(after.targetDate)}`);
|
|
74
96
|
}
|
|
97
|
+
if (after.tokenBudget !== undefined) {
|
|
98
|
+
changes.push(after.tokenBudget === null
|
|
99
|
+
? 'token budget → ∅'
|
|
100
|
+
: `token budget → ${after.tokenBudget.toLocaleString('en-US')}`);
|
|
101
|
+
}
|
|
75
102
|
return `Updated milestone "${beforeName}": ${changes.join(', ')}`;
|
|
76
103
|
}
|
|
77
104
|
async function milestoneUpdate(identifier, opts) {
|
|
@@ -85,12 +112,18 @@ async function milestoneUpdate(identifier, opts) {
|
|
|
85
112
|
}
|
|
86
113
|
normalizedStatus = n;
|
|
87
114
|
}
|
|
115
|
+
// Validate token budget eagerly (LUM-644): positive integer, or '' to clear.
|
|
116
|
+
if (opts.tokenBudget !== undefined &&
|
|
117
|
+
normalizeTokenBudget(opts.tokenBudget) === undefined) {
|
|
118
|
+
console.error(`Error: invalid token budget "${opts.tokenBudget}". Provide a positive integer (e.g. 5000000), or "" to clear.`);
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
88
121
|
const { payload, flagsGiven } = buildMilestoneUpdatePayload({
|
|
89
122
|
...opts,
|
|
90
123
|
...(normalizedStatus !== undefined && { status: normalizedStatus }),
|
|
91
124
|
});
|
|
92
125
|
if (flagsGiven.length === 0) {
|
|
93
|
-
console.error('Error: provide at least one field to update (--name, --description, --status, --start, --target)');
|
|
126
|
+
console.error('Error: provide at least one field to update (--name, --description, --status, --start, --target, --token-budget)');
|
|
94
127
|
return 1;
|
|
95
128
|
}
|
|
96
129
|
const creds = (0, config_1.readCredentials)();
|
package/dist/cli/src/index.js
CHANGED
|
@@ -101,6 +101,9 @@ const milestone_remove_1 = require("./commands/milestone-remove");
|
|
|
101
101
|
const milestone_summary_1 = require("./commands/milestone-summary");
|
|
102
102
|
const milestone_reorder_1 = require("./commands/milestone-reorder");
|
|
103
103
|
const milestone_move_1 = require("./commands/milestone-move");
|
|
104
|
+
const milestone_criteria_set_1 = require("./commands/milestone-criteria-set");
|
|
105
|
+
const milestone_criteria_list_1 = require("./commands/milestone-criteria-list");
|
|
106
|
+
const milestone_criteria_verdict_1 = require("./commands/milestone-criteria-verdict");
|
|
104
107
|
const sprint_create_1 = require("./commands/sprint-create");
|
|
105
108
|
const sprint_list_1 = require("./commands/sprint-list");
|
|
106
109
|
const sprint_show_1 = require("./commands/sprint-show");
|
|
@@ -605,13 +608,14 @@ milestoneCmd
|
|
|
605
608
|
.action(wrap((identifier, options) => (0, milestone_show_1.milestoneShow)(identifier, options)));
|
|
606
609
|
milestoneCmd
|
|
607
610
|
.command('update <identifier>')
|
|
608
|
-
.description('Update a milestone. Provide at least one of --name, --description, --status, --start, --target. Use "" to clear nullable fields.')
|
|
611
|
+
.description('Update a milestone. Provide at least one of --name, --description, --status, --start, --target, --token-budget. Use "" to clear nullable fields.')
|
|
609
612
|
.option('--project <ref>', 'Project name or slug (when identifier is a name)')
|
|
610
613
|
.option('-n, --name <text>', 'New name')
|
|
611
614
|
.option('-d, --description <text>', 'New description (empty string to clear)')
|
|
612
615
|
.option('-s, --status <value>', 'New status: planned | active | completed | cancelled (case-insensitive)')
|
|
613
616
|
.option('--start <date>', 'Start date YYYY-MM-DD (empty string to clear)')
|
|
614
617
|
.option('--target <date>', 'Target date YYYY-MM-DD (empty string to clear)')
|
|
618
|
+
.option('--token-budget <tokens>', 'Advisory token budget, positive integer (empty string to clear). Overview shows burn vs budget; over-budget raises a risk-queue alert — never a hard gate.')
|
|
615
619
|
.action(wrap((identifier, options) => (0, milestone_update_1.milestoneUpdate)(identifier, options)));
|
|
616
620
|
milestoneCmd
|
|
617
621
|
.command('delete <identifier>')
|
|
@@ -657,6 +661,30 @@ milestoneCmd
|
|
|
657
661
|
.option('--before <ref>', 'Place <ref> immediately before this milestone')
|
|
658
662
|
.option('--after <ref>', 'Place <ref> immediately after this milestone')
|
|
659
663
|
.action(wrap((ref, options) => (0, milestone_move_1.milestoneMove)(ref, options)));
|
|
664
|
+
const milestoneCriteria = milestoneCmd
|
|
665
|
+
.command('criteria')
|
|
666
|
+
.description('Milestone-level exit criteria — the close contract (LUM-643). Drafted and adjudicated by the milestone steward (LUM-640) or a human; unresolved criteria refuse status→COMPLETED (human override via web only).');
|
|
667
|
+
milestoneCriteria
|
|
668
|
+
.command('set <identifier>')
|
|
669
|
+
.description('Submit the whole exit contract from a JSON file (full replace; adjudicated rows are tombstoned, their verdict trail survives). Default = steward/agent draft; --human records a transcribed HUMAN_EDIT revision. Locked with 409 once the milestone is COMPLETED.')
|
|
670
|
+
.requiredOption('--file <path>', 'JSON array of criteria: [{"statement","referentKind":"EXTERNAL_FACT"|"AGENT_CONSTRUCTED_STATE"|"PENDING_OUTCOME"}] — no verifierType/checkpointer (exit criteria are adjudicated, not machine-run)')
|
|
671
|
+
.option('--human', 'Record a human contract revision (HUMAN_EDIT) transcribed from the conversation')
|
|
672
|
+
.option('--project <ref>', 'Project name or slug (when identifier is a name)')
|
|
673
|
+
.action(wrap((identifier, options) => (0, milestone_criteria_set_1.milestoneCriteriaSet)(identifier, options)));
|
|
674
|
+
milestoneCriteria
|
|
675
|
+
.command('list <identifier>')
|
|
676
|
+
.description('List a milestone’s exit criteria: id, verdict (MET | NOT_MET | PENDING), provenance, referent kind, statement — plus whether the close gate is open.')
|
|
677
|
+
.option('--project <ref>', 'Project name or slug (when identifier is a name)')
|
|
678
|
+
.action(wrap((identifier, options) => (0, milestone_criteria_list_1.milestoneCriteriaList)(identifier, options)));
|
|
679
|
+
milestoneCriteria
|
|
680
|
+
.command('verdict <identifier> <criterion>')
|
|
681
|
+
.description('Adjudicate (裁定) one exit criterion: --met or --unmet, with optional --note. Grounding gate: --met on an EXTERNAL_FACT criterion requires --evidence (422 otherwise), and a PENDING_OUTCOME criterion can never be MET. <criterion> = id or ≥6-char id prefix from `criteria list`.')
|
|
682
|
+
.option('--met', 'Adjudicate the criterion MET')
|
|
683
|
+
.option('--unmet', 'Adjudicate the criterion NOT_MET')
|
|
684
|
+
.option('--note <text>', 'Adjudication note (why this verdict)')
|
|
685
|
+
.option('--evidence <pointer>', 'Evidence pointer backing a MET on EXTERNAL_FACT (URL, commit SHA, CI run, DB probe …)')
|
|
686
|
+
.option('--project <ref>', 'Project name or slug (when identifier is a name)')
|
|
687
|
+
.action(wrap((identifier, criterion, options) => (0, milestone_criteria_verdict_1.milestoneCriteriaVerdict)(identifier, criterion, options)));
|
|
660
688
|
const sprintCmd = program
|
|
661
689
|
.command('sprint')
|
|
662
690
|
.description('Inspect sprints from the terminal');
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* of human-readable reason factors.
|
|
6
6
|
*
|
|
7
7
|
* Ordering is lexicographic (explainable over a tunable magic score), honoring
|
|
8
|
-
* "priority first": priority → active-sprint membership →
|
|
9
|
-
*
|
|
8
|
+
* "priority first": priority → active-sprint membership → dependency topology
|
|
9
|
+
* (unblocked before blocked, then most open tasks released) → dueDate →
|
|
10
|
+
* in-flight status → updatedAt desc tiebreak.
|
|
10
11
|
*/
|
|
11
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
13
|
exports.rankTasks = rankTasks;
|
|
@@ -38,11 +39,28 @@ function statusRank(status) {
|
|
|
38
39
|
function inActiveSprint(task, activeSprintIds) {
|
|
39
40
|
return task.sprintId !== null && activeSprintIds.has(task.sprintId);
|
|
40
41
|
}
|
|
42
|
+
/** Missing counts (older server) read as 0 — no topology signal, no reorder. */
|
|
43
|
+
function blockedByOpen(task) {
|
|
44
|
+
return task.blockedByOpenCount ?? 0;
|
|
45
|
+
}
|
|
46
|
+
function unblocksOpen(task) {
|
|
47
|
+
return task.unblocksOpenCount ?? 0;
|
|
48
|
+
}
|
|
41
49
|
function deriveReasons(task, activeSprintIds, now) {
|
|
42
50
|
const reasons = [task.priority];
|
|
43
51
|
if (inActiveSprint(task, activeSprintIds)) {
|
|
44
52
|
reasons.push('active sprint');
|
|
45
53
|
}
|
|
54
|
+
const blocked = blockedByOpen(task);
|
|
55
|
+
const unblocks = unblocksOpen(task);
|
|
56
|
+
if (blocked > 0) {
|
|
57
|
+
reasons.push(blocked === 1
|
|
58
|
+
? 'blocked by 1 open dep'
|
|
59
|
+
: `blocked by ${blocked} open deps`);
|
|
60
|
+
}
|
|
61
|
+
else if (unblocks > 0) {
|
|
62
|
+
reasons.push(unblocks === 1 ? 'unblocks 1 task' : `unblocks ${unblocks} tasks`);
|
|
63
|
+
}
|
|
46
64
|
if (task.dueDate) {
|
|
47
65
|
const day = task.dueDate.slice(0, 10);
|
|
48
66
|
const due = new Date(task.dueDate).getTime();
|
|
@@ -64,6 +82,16 @@ function rankTasks(tasks, activeSprintIds, now) {
|
|
|
64
82
|
const sb = inActiveSprint(b, activeSprintIds) ? 0 : 1;
|
|
65
83
|
if (sa !== sb)
|
|
66
84
|
return sa - sb;
|
|
85
|
+
// Dependency topology (LUM-645): workable-now before blocked, then the
|
|
86
|
+
// task that releases the most open work. Both 0 when the server doesn't
|
|
87
|
+
// send counts — the comparator falls through unchanged.
|
|
88
|
+
const ba = blockedByOpen(a) > 0 ? 1 : 0;
|
|
89
|
+
const bb = blockedByOpen(b) > 0 ? 1 : 0;
|
|
90
|
+
if (ba !== bb)
|
|
91
|
+
return ba - bb;
|
|
92
|
+
const ub = unblocksOpen(b) - unblocksOpen(a);
|
|
93
|
+
if (ub !== 0)
|
|
94
|
+
return ub;
|
|
67
95
|
const dv = dueValue(a.dueDate) - dueValue(b.dueDate);
|
|
68
96
|
if (dv !== 0 && !Number.isNaN(dv))
|
|
69
97
|
return dv;
|