@detergent-software/conductor 1.0.0-dev.10 → 1.0.0-dev.12
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/CHANGELOG.md +12 -0
- package/README.md +61 -7
- package/build/cli.js +102 -13
- package/build/cli.js.map +1 -1
- package/build/daemon.js +68 -10
- package/build/daemon.js.map +1 -1
- package/build/inbox.js +1 -1
- package/build/inbox.js.map +1 -1
- package/build/manager.js +54 -21
- package/build/manager.js.map +1 -1
- package/build/notes.js +17 -34
- package/build/notes.js.map +1 -1
- package/build/notify.js +32 -3
- package/build/notify.js.map +1 -1
- package/build/overrides.js +83 -0
- package/build/overrides.js.map +1 -0
- package/build/queue.js +178 -0
- package/build/queue.js.map +1 -0
- package/build/rule-actions.js +444 -0
- package/build/rule-actions.js.map +1 -0
- package/build/rules-service.js +11 -0
- package/build/rules-service.js.map +1 -1
- package/build/rules.js +5 -3
- package/build/rules.js.map +1 -1
- package/build/scheduler.js +274 -0
- package/build/scheduler.js.map +1 -0
- package/build/server.js +65 -1
- package/build/server.js.map +1 -1
- package/build/store.js +128 -6
- package/build/store.js.map +1 -1
- package/build/watchers/index.js +1 -0
- package/build/watchers/index.js.map +1 -1
- package/build/watchers/linear-api.js +55 -0
- package/build/watchers/linear-api.js.map +1 -0
- package/build/watchers/linear-labels.js +43 -0
- package/build/watchers/linear-labels.js.map +1 -0
- package/build/watchers/linear-queue.js +93 -0
- package/build/watchers/linear-queue.js.map +1 -0
- package/build/why.js +78 -0
- package/build/why.js.map +1 -0
- package/mod/README.md +1 -1
- package/mod/hooks/model.ts +64 -5
- package/mod/hooks/register.tsx +41 -3
- package/package.json +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { linearApi } from "./linear-api.js";
|
|
3
|
+
import { spend } from "./source.js";
|
|
4
|
+
const PAGE = 250;
|
|
5
|
+
const OPEN_TYPES = ['backlog', 'unstarted', 'started'];
|
|
6
|
+
const CLOSED_TYPES = new Set(['canceled', 'completed', 'duplicate']);
|
|
7
|
+
const ISSUE_FIELDS = `identifier title priority createdAt state { type } project { name }
|
|
8
|
+
labels(first: 20) { nodes { name } }
|
|
9
|
+
inverseRelations(first: 20) { nodes { type issue { identifier } } }`;
|
|
10
|
+
const OPEN_ISSUES = `open: issues(first: ${PAGE}, after: $after, filter: { team: { key: { eq: $team } }, state: { type: { in: ${JSON.stringify(OPEN_TYPES)} } } }) {
|
|
11
|
+
pageInfo { hasNextPage endCursor }
|
|
12
|
+
nodes { ${ISSUE_FIELDS} }
|
|
13
|
+
}`;
|
|
14
|
+
export const QUEUE_QUERY = `query Queue($team: String!, $area: String!, $numbers: [Float!]!, $after: String) {
|
|
15
|
+
projects(first: ${PAGE}, filter: { accessibleTeams: { some: { key: { eq: $team } } } }) { nodes { name priority sortOrder } }
|
|
16
|
+
issueLabels(first: ${PAGE}, filter: { parent: { name: { eq: $area } } }) { nodes { name } }
|
|
17
|
+
${OPEN_ISSUES}
|
|
18
|
+
inflight: issues(first: ${PAGE}, includeArchived: true, filter: { team: { key: { eq: $team } }, number: { in: $numbers } }) {
|
|
19
|
+
nodes { identifier state { type } }
|
|
20
|
+
}
|
|
21
|
+
}`;
|
|
22
|
+
export const OPEN_PAGE_QUERY = `query OpenPage($team: String!, $after: String) {
|
|
23
|
+
${OPEN_ISSUES}
|
|
24
|
+
}`;
|
|
25
|
+
const issueNode = z.object({
|
|
26
|
+
createdAt: z.string().nullable(),
|
|
27
|
+
identifier: z.string(),
|
|
28
|
+
inverseRelations: z.object({ nodes: z.array(z.object({ issue: z.object({ identifier: z.string() }).nullable(), type: z.string() })) }),
|
|
29
|
+
labels: z.object({ nodes: z.array(z.object({ name: z.string() })) }),
|
|
30
|
+
priority: z.number().nullable(),
|
|
31
|
+
project: z.object({ name: z.string() }).nullable(),
|
|
32
|
+
state: z.object({ type: z.string() }),
|
|
33
|
+
title: z.string().nullable(),
|
|
34
|
+
});
|
|
35
|
+
const openPage = z.object({
|
|
36
|
+
nodes: z.array(issueNode),
|
|
37
|
+
pageInfo: z.object({ endCursor: z.string().nullable(), hasNextPage: z.boolean() }),
|
|
38
|
+
});
|
|
39
|
+
const queueAnswer = z.object({
|
|
40
|
+
inflight: z.object({ nodes: z.array(z.object({ identifier: z.string(), state: z.object({ type: z.string() }) })) }),
|
|
41
|
+
issueLabels: z.object({ nodes: z.array(z.object({ name: z.string() })) }),
|
|
42
|
+
open: openPage,
|
|
43
|
+
projects: z.object({ nodes: z.array(z.object({ name: z.string(), priority: z.number().nullable(), sortOrder: z.number().nullable() })) }),
|
|
44
|
+
});
|
|
45
|
+
export function linearQueue(options = {}) {
|
|
46
|
+
const request = linearApi(options);
|
|
47
|
+
const post = async (query, variables, budget) => {
|
|
48
|
+
spend(budget);
|
|
49
|
+
return request(query, variables);
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
read: async (profile, inFlight, budget) => {
|
|
53
|
+
const { areaLabelGroup, team } = profile.tracker;
|
|
54
|
+
const numbers = inFlight.map((id) => Number(/^[A-Za-z]+-(\d+)$/.exec(id)?.[1])).filter(Number.isInteger);
|
|
55
|
+
const first = queueAnswer.parse(await post(QUEUE_QUERY, { after: null, area: areaLabelGroup, numbers, team }, budget));
|
|
56
|
+
const pages = [];
|
|
57
|
+
for (let page = first.open; page.pageInfo.hasNextPage;) {
|
|
58
|
+
page = z.object({ open: openPage }).parse(await post(OPEN_PAGE_QUERY, { after: page.pageInfo.endCursor, team }, budget)).open;
|
|
59
|
+
pages.push(page);
|
|
60
|
+
}
|
|
61
|
+
return snapshotFrom(first, pages);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// The snapshot from the Queue query's answer and any later pages of open items. An item's blockers
|
|
66
|
+
// are the ones still open; an in-flight item the read finds done, canceled or a duplicate is closed.
|
|
67
|
+
export function snapshotFrom(data, pages = []) {
|
|
68
|
+
const nodes = [data.open, ...pages].flatMap((page) => page.nodes);
|
|
69
|
+
const open = new Set(nodes.map((n) => n.identifier));
|
|
70
|
+
const areaLabels = data.issueLabels.nodes.map((l) => l.name);
|
|
71
|
+
const areas = new Set(areaLabels);
|
|
72
|
+
return {
|
|
73
|
+
areaLabels,
|
|
74
|
+
closed: data.inflight.nodes.filter((n) => CLOSED_TYPES.has(n.state.type)).map((n) => n.identifier),
|
|
75
|
+
issues: nodes.map((n) => {
|
|
76
|
+
const labels = n.labels.nodes.map((l) => l.name);
|
|
77
|
+
return {
|
|
78
|
+
area: labels.find((l) => areas.has(l)) ?? null,
|
|
79
|
+
// Linear stores "A blocks B" on A; B sees it as an inverse relation.
|
|
80
|
+
blockedBy: n.inverseRelations.nodes.flatMap((r) => (r.type === 'blocks' && r.issue && open.has(r.issue.identifier) ? [r.issue.identifier] : [])),
|
|
81
|
+
createdAt: n.createdAt,
|
|
82
|
+
id: n.identifier,
|
|
83
|
+
labels,
|
|
84
|
+
priority: n.priority,
|
|
85
|
+
stateType: n.state.type,
|
|
86
|
+
title: n.title,
|
|
87
|
+
trackerProject: n.project?.name ?? null,
|
|
88
|
+
};
|
|
89
|
+
}),
|
|
90
|
+
projects: data.projects.nodes,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=linear-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linear-queue.js","sourceRoot":"","sources":["../../src/watchers/linear-queue.ts"],"names":[],"mappings":"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"}
|
package/build/why.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {} from "./store.js";
|
|
3
|
+
// `conduct why` (design note, section 9): every action is an event that names the rule behind it,
|
|
4
|
+
// directly (`rule`) or through the event that caused it (`data.cause`). This follows that chain
|
|
5
|
+
// back to the rule, and gives its text as it was when it fired.
|
|
6
|
+
const eventSchema = z.object({
|
|
7
|
+
at: z.string(),
|
|
8
|
+
data: z.record(z.string(), z.unknown()),
|
|
9
|
+
id: z.number(),
|
|
10
|
+
kind: z.string(),
|
|
11
|
+
project: z.string().nullable(),
|
|
12
|
+
rule: z.string().nullable(),
|
|
13
|
+
subject: z.string().nullable(),
|
|
14
|
+
});
|
|
15
|
+
export const whySchema = z.object({
|
|
16
|
+
// The event asked about, then each cause before it.
|
|
17
|
+
chain: z.array(eventSchema),
|
|
18
|
+
// The rule behind it: its ref, and its text as it fired (or as it is now, when the event didn't
|
|
19
|
+
// record it). Null when no rule caused it: a worker's own note, Jason's command.
|
|
20
|
+
rule: z
|
|
21
|
+
.object({
|
|
22
|
+
do: z.array(z.string()).nullable(),
|
|
23
|
+
from: z.enum(['a command', 'the event', 'the rules in force', 'not in force any more']),
|
|
24
|
+
ref: z.string(),
|
|
25
|
+
when: z.string().nullable(),
|
|
26
|
+
})
|
|
27
|
+
.nullable(),
|
|
28
|
+
});
|
|
29
|
+
const MAX_CHAIN = 10;
|
|
30
|
+
const recorded = z.looseObject({ do: z.array(z.string()).optional(), when: z.string().nullable().optional() });
|
|
31
|
+
export function explain(store, id, rules) {
|
|
32
|
+
const first = store.getEvent(id);
|
|
33
|
+
if (!first)
|
|
34
|
+
return null;
|
|
35
|
+
const chain = [first];
|
|
36
|
+
for (let at = first; chain.length < MAX_CHAIN;) {
|
|
37
|
+
const cause = at.data.cause;
|
|
38
|
+
const next = typeof cause === 'number' ? store.getEvent(cause) : null;
|
|
39
|
+
if (!next || chain.some((e) => e.id === next.id))
|
|
40
|
+
break;
|
|
41
|
+
chain.push(next);
|
|
42
|
+
at = next;
|
|
43
|
+
}
|
|
44
|
+
const source = chain.find((e) => e.rule !== null);
|
|
45
|
+
if (!source?.rule)
|
|
46
|
+
return { chain, rule: null };
|
|
47
|
+
// An override set by a command names it (`conduct pause`); a rule's ref has a source and a colon.
|
|
48
|
+
if (!source.rule.includes(':'))
|
|
49
|
+
return { chain, rule: { do: null, from: 'a command', ref: source.rule, when: null } };
|
|
50
|
+
// The text as it fired is on the `rule.fired` event, which a `rule.done` names as its cause.
|
|
51
|
+
for (const e of chain) {
|
|
52
|
+
const text = e.rule === source.rule ? recorded.safeParse(e.data).data : undefined;
|
|
53
|
+
if (text?.do !== undefined)
|
|
54
|
+
return { chain, rule: { do: text.do, from: 'the event', ref: source.rule, when: text.when ?? null } };
|
|
55
|
+
}
|
|
56
|
+
const now = rules.find((r) => r.ref === source.rule);
|
|
57
|
+
return {
|
|
58
|
+
chain,
|
|
59
|
+
rule: now
|
|
60
|
+
? { do: now.text.do, from: 'the rules in force', ref: now.ref, when: now.text.when }
|
|
61
|
+
: { do: null, from: 'not in force any more', ref: source.rule, when: null },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// `conduct why`: the rule first, then the chain of events from the one asked about back to its cause.
|
|
65
|
+
export function whyLines(why) {
|
|
66
|
+
const { rule } = why;
|
|
67
|
+
const lines = rule === null ? ['no rule: nothing in its chain was caused by a rule (a worker, a command, an outside event)']
|
|
68
|
+
: rule.from === 'a command' ? [`set by \`${rule.ref}\``]
|
|
69
|
+
: [`${rule.ref}: ${rule.when === null ? '' : `when ${rule.when} → `}${rule.do?.join('; ') ?? '(its text is gone)'} (from ${rule.from})`];
|
|
70
|
+
for (const [i, e] of why.chain.entries()) {
|
|
71
|
+
const detail = ['outcome', 'action', 'error', 'reason', 'by']
|
|
72
|
+
.flatMap((k) => (typeof e.data[k] === 'string' ? [`${k}: ${String(e.data[k])}`] : []))
|
|
73
|
+
.join('; ');
|
|
74
|
+
lines.push(`${i === 0 ? ' ' : ' ← '}#${e.id} ${new Date(e.at).toLocaleString()} ${e.kind}${e.subject ? ` ${e.subject}` : ''}${e.project ? ` (${e.project})` : ''}${e.rule ? ` [${e.rule}]` : ''}${detail ? `: ${detail}` : ''}`);
|
|
75
|
+
}
|
|
76
|
+
return lines;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=why.js.map
|
package/build/why.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"why.js","sourceRoot":"","sources":["../src/why.ts"],"names":[],"mappings":"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"}
|
package/mod/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Conductor mod
|
|
2
2
|
|
|
3
|
-
A Claude Code plugin (a hooks module) that does the conductor console's plumbing in code, so it costs no tokens: a status line in every session and, in the console (`CONDUCTOR_CONSOLE=1`), the inbox band, the fleet pane, `/conductor` `/inbox` `/approve` `/revise` `/attach` `/notify`, toasts, Remote Control pushes (and the one warning when Remote Control is off), and the one model wake-up for an escalation. What it shows and does is in the conductor README's *Mod* section (`apps/conductor/README.md`).
|
|
3
|
+
A Claude Code plugin (a hooks module) that does the conductor console's plumbing in code, so it costs no tokens: a status line in every session and, in the console (`CONDUCTOR_CONSOLE=1`), the inbox band, the fleet pane, `/conductor` `/inbox` `/approve` `/revise` `/attach` `/notify` `/pause` `/unpause` `/focus-project`, toasts, Remote Control pushes (and the one warning when Remote Control is off), and the one model wake-up for an escalation. What it shows and does is in the conductor README's *Mod* section (`apps/conductor/README.md`).
|
|
4
4
|
|
|
5
5
|
Install it with the daemon's CLI, which ships this folder:
|
|
6
6
|
|
package/mod/hooks/model.ts
CHANGED
|
@@ -355,17 +355,76 @@ export function notifyArgs(args: string, now: number): { error: string } | { nam
|
|
|
355
355
|
if (!typed?.[1]) return { error: 'usage: /notify [<profile> [--until <when>]]' };
|
|
356
356
|
const when = typed[2];
|
|
357
357
|
if (when === undefined) return { name: typed[1], until: null };
|
|
358
|
+
const until = untilOf(when, now);
|
|
359
|
+
return typeof until === 'string' ? { name: typed[1], until } : until;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const DAYS = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
|
|
363
|
+
|
|
364
|
+
// `--until <when>` as an ISO time: 17:00 (the next one), tomorrow or a day (local midnight), a date
|
|
365
|
+
// (local midnight), or a date and time, as `conduct` reads them.
|
|
366
|
+
export function untilOf(when: string, now: number): string | { error: string } {
|
|
358
367
|
const clock = /^([01]?\d|2[0-3]):([0-5]\d)$/.exec(when);
|
|
359
|
-
|
|
368
|
+
const lower = when.toLowerCase();
|
|
369
|
+
const day = DAYS.findIndex((d) => d === lower || d.slice(0, 3) === lower);
|
|
370
|
+
const at = new Date(now);
|
|
360
371
|
if (clock) {
|
|
361
|
-
at = new Date(now);
|
|
362
372
|
at.setHours(Number(clock[1]), Number(clock[2]), 0, 0);
|
|
363
373
|
if (at.getTime() <= now) at.setDate(at.getDate() + 1);
|
|
374
|
+
} else if (lower === 'tomorrow' || day !== -1) {
|
|
375
|
+
at.setHours(0, 0, 0, 0);
|
|
376
|
+
at.setDate(at.getDate() + (lower === 'tomorrow' ? 1 : (day - at.getDay() + 7) % 7 || 7));
|
|
364
377
|
} else {
|
|
365
|
-
at
|
|
378
|
+
at.setTime(new Date(/^\d{4}-\d{2}-\d{2}$/.test(when) ? `${when}T00:00` : when).getTime());
|
|
366
379
|
}
|
|
367
|
-
if (Number.isNaN(at.getTime()))
|
|
368
|
-
|
|
380
|
+
if (Number.isNaN(at.getTime())) {
|
|
381
|
+
return { error: `--until "${when}" is not a time: 17:00, tomorrow, a day such as monday, or a date and time such as 2026-10-12T09:00` };
|
|
382
|
+
}
|
|
383
|
+
return at.toISOString();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// `/pause [<project>] [--until <when>]`, `/unpause [<project>]` and `/focus-project <project> [--until
|
|
387
|
+
// <when>]` as POST /overrides' body. A focus with no end lasts until midnight, and puts the
|
|
388
|
+
// project's items first (weight 100).
|
|
389
|
+
export function overrideArgs(command: 'focus' | 'pause' | 'unpause', args: string, now: number): { body: Record<string, unknown> } | { error: string } {
|
|
390
|
+
const typed = /^(?:([a-z][a-z0-9-]*))?(?:\s*--until\s+(\S+))?$/.exec(args.trim());
|
|
391
|
+
const usage = { error: command === 'focus' ? 'usage: /focus-project <project> [--until <when>]' : `usage: /${command} [<project>]${command === 'pause' ? ' [--until <when>]' : ''}` };
|
|
392
|
+
if (!typed) return usage;
|
|
393
|
+
const [, project, when] = typed;
|
|
394
|
+
if (command === 'unpause') return when === undefined ? { body: { action: 'unpause', by: 'conductor mod /unpause', ...(project ? { project } : {}) } } : usage;
|
|
395
|
+
const until = when === undefined ? (command === 'focus' ? untilOf('tomorrow', now) : null) : untilOf(when, now);
|
|
396
|
+
if (until !== null && typeof until !== 'string') return until;
|
|
397
|
+
if (command === 'focus') {
|
|
398
|
+
if (!project) return usage;
|
|
399
|
+
return { body: { action: 'focus', by: 'conductor mod /focus-project', project, until, weight: 100 } };
|
|
400
|
+
}
|
|
401
|
+
return { body: { action: 'pause', by: 'conductor mod /pause', ...(project ? { project } : {}), ...(until === null ? {} : { until }) } };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// POST /overrides' answer as the line to show.
|
|
405
|
+
export function overrideText(command: 'focus' | 'pause' | 'unpause', project: null | string, answer: ApiAnswer, now: number): string {
|
|
406
|
+
if (answer.status === 404) return 'the running conductor has no overrides: restart it on this build (conduct upgrade)';
|
|
407
|
+
if (answer.status !== 201) return `not ${command === 'focus' ? 'focused' : `${command}d`}: ${errorOf(answer)}`;
|
|
408
|
+
const set = isRecord(answer.body) && Array.isArray(answer.body.set) ? answer.body.set.filter(isRecord) : [];
|
|
409
|
+
const until = typeof set[0]?.until === 'string' ? ` for ${remaining(set[0].until, now)}` : '';
|
|
410
|
+
const who = project ?? 'every project';
|
|
411
|
+
if (command === 'pause') return `paused launches in ${who}${until || ' until /unpause'}`;
|
|
412
|
+
if (command === 'unpause') return `launches in ${who} are no longer paused`;
|
|
413
|
+
return `${who} goes first${until}`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// GET /why?item=…'s answer: the rule behind the item's newest action, in one line.
|
|
417
|
+
export function whyText(item: string, answer: ApiAnswer): string {
|
|
418
|
+
if (answer.status === 404) return 'the running conductor has no conduct why: restart it on this build (conduct upgrade)';
|
|
419
|
+
if (answer.status !== 200) return `why ${item}: ${errorOf(answer)}`;
|
|
420
|
+
const first = isRecord(answer.body) && Array.isArray(answer.body.items) ? answer.body.items.find(isRecord) : undefined;
|
|
421
|
+
if (!first) return `no action on ${item} was caused by a rule`;
|
|
422
|
+
const rule = isRecord(first.rule) ? first.rule : null;
|
|
423
|
+
const latest = Array.isArray(first.chain) && isRecord(first.chain[0]) ? first.chain[0] : null;
|
|
424
|
+
const what = latest && typeof latest.kind === 'string' ? `${latest.kind}: ` : '';
|
|
425
|
+
if (!rule || typeof rule.ref !== 'string') return `${item}: ${what}no rule`;
|
|
426
|
+
const text = typeof rule.when === 'string' && Array.isArray(rule.do) ? ` (when ${rule.when} → ${strings(rule.do).join('; ')})` : '';
|
|
427
|
+
return `${item}: ${what}${rule.ref}${text}`;
|
|
369
428
|
}
|
|
370
429
|
|
|
371
430
|
// `/notify` with no profile: the profile, quiet hours, Remote Control.
|
package/mod/hooks/register.tsx
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
inboxText,
|
|
18
18
|
notifyArgs,
|
|
19
19
|
notifyText,
|
|
20
|
+
overrideArgs,
|
|
21
|
+
overrideText,
|
|
20
22
|
parseLine,
|
|
21
23
|
profileText,
|
|
22
24
|
projectGroups,
|
|
@@ -29,6 +31,7 @@ import {
|
|
|
29
31
|
spendToday,
|
|
30
32
|
statusText,
|
|
31
33
|
tokens,
|
|
34
|
+
whyText,
|
|
32
35
|
workerDoing,
|
|
33
36
|
} from './model.ts';
|
|
34
37
|
|
|
@@ -238,6 +241,29 @@ async function answer($: EngineInterface, id: number, body: { action: 'approve'
|
|
|
238
241
|
}
|
|
239
242
|
}
|
|
240
243
|
|
|
244
|
+
// `/pause`, `/unpause`, `/focus-project` and the pane's buttons: an override that ends on its own, as
|
|
245
|
+
// `conduct pause|unpause|focus` sets it; the line to show.
|
|
246
|
+
async function override($: EngineInterface, command: 'focus' | 'pause' | 'unpause', args: string): Promise<string> {
|
|
247
|
+
const now = await $.clock.now();
|
|
248
|
+
const typed = overrideArgs(command, args, now);
|
|
249
|
+
if ('error' in typed) return typed.error;
|
|
250
|
+
try {
|
|
251
|
+
const project = typeof typed.body.project === 'string' ? typed.body.project : null;
|
|
252
|
+
return overrideText(command, project, await callApi($, 'POST', '/overrides', typed.body), now);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
return error instanceof Error ? error.message : String(error);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// The rule behind an item's newest action (`conduct why <item>`), in one line.
|
|
259
|
+
async function why($: EngineInterface, item: string): Promise<string> {
|
|
260
|
+
try {
|
|
261
|
+
return whyText(item, await callApi($, 'GET', `/why?item=${encodeURIComponent(item)}`));
|
|
262
|
+
} catch (error) {
|
|
263
|
+
return error instanceof Error ? error.message : String(error);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
241
267
|
async function attach($: EngineInterface, id: number, name: string): Promise<string> {
|
|
242
268
|
try {
|
|
243
269
|
const result = await callApi($, 'POST', `/workers/${id}/attach`);
|
|
@@ -270,6 +296,9 @@ async function registerCommands($: EngineInterface): Promise<void> {
|
|
|
270
296
|
await $.command.register({ argumentHint: '<item | #id> <what to change>', description: 'Ask for a revised plan (posts revise: … as you)', name: 'revise' });
|
|
271
297
|
await $.command.register({ argumentHint: '<item | worker>', description: 'Wake a parked worker and open it in a tab', name: 'attach' });
|
|
272
298
|
await $.command.register({ argumentHint: '[<profile> [--until <when>]]', description: 'Show or switch the notification profile (default, away, focus …)', name: 'notify' });
|
|
299
|
+
await $.command.register({ argumentHint: '[<project>] [--until <when>]', description: 'No new launches in a project (every project without one)', name: 'pause' });
|
|
300
|
+
await $.command.register({ argumentHint: '[<project>]', description: 'Launch again after /pause', name: 'unpause' });
|
|
301
|
+
await $.command.register({ argumentHint: '<project> [--until <when>]', description: "Put a project's items first (until midnight without --until)", name: 'focus-project' });
|
|
273
302
|
}
|
|
274
303
|
|
|
275
304
|
export const register: Register = (on) => {
|
|
@@ -336,6 +365,10 @@ export const register: Register = (on) => {
|
|
|
336
365
|
return { text: snap.error ?? notifyText(snap.notify, await $.clock.now()) };
|
|
337
366
|
});
|
|
338
367
|
|
|
368
|
+
on('command.run', { command: 'pause' }, async ($, e) => ({ text: await override($, 'pause', e.args) }));
|
|
369
|
+
on('command.run', { command: 'unpause' }, async ($, e) => ({ text: await override($, 'unpause', e.args) }));
|
|
370
|
+
on('command.run', { command: 'focus-project' }, async ($, e) => ({ text: await override($, 'focus', e.args) }));
|
|
371
|
+
|
|
339
372
|
// The band above the prompt, in the console, while the inbox has items. A bare digit typed into
|
|
340
373
|
// an empty prompt presses a band Button, so a plan's approve takes a second press (`y`).
|
|
341
374
|
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
|
|
@@ -472,9 +505,13 @@ export const register: Register = (on) => {
|
|
|
472
505
|
))}
|
|
473
506
|
{projectGroups(snap.workers).map((group) => (
|
|
474
507
|
<Box flexDirection="column" key={`project-${group.project}`}>
|
|
475
|
-
<
|
|
476
|
-
|
|
477
|
-
|
|
508
|
+
<Box columnGap={1}>
|
|
509
|
+
<Text dimColor wrap="truncate">
|
|
510
|
+
{rule(`${group.project} · ${group.running}/${group.rows.length} running`).slice(0, Math.max(10, e.props.bodyColumns - 18))}
|
|
511
|
+
</Text>
|
|
512
|
+
<Button key={`pause-${group.project}`} label="pause" onPress={async () => $.ui.toast(await override($, 'pause', group.project))} />
|
|
513
|
+
<Button key={`focus-${group.project}`} label="focus" onPress={async () => $.ui.toast(await override($, 'focus', group.project))} />
|
|
514
|
+
</Box>
|
|
478
515
|
{group.rows.map((w) => (
|
|
479
516
|
<Box flexDirection="column" key={`worker-${w.id}`}>
|
|
480
517
|
<Button key={`row-${w.id}`} onPress={() => update($, expanded, (name) => (name === w.name ? null : w.name))} plain>
|
|
@@ -486,6 +523,7 @@ export const register: Register = (on) => {
|
|
|
486
523
|
{open === w.name && (
|
|
487
524
|
<Box columnGap={2} flexWrap="wrap" marginLeft={2}>
|
|
488
525
|
<Button key={`attach-${w.id}`} label="attach" onPress={async () => $.ui.toast(await attach($, w.id, w.name))} />
|
|
526
|
+
<Button key={`why-${w.id}`} label="why" onPress={async () => $.ui.toast(await why($, w.item))} />
|
|
489
527
|
<Text dimColor>
|
|
490
528
|
{w.status}
|
|
491
529
|
{w.waits.length > 0 ? ` · waits on ${w.waits.join(', ')}` : ''}
|
package/package.json
CHANGED