@yannelli/paseo-linear-plugin 1.0.0 → 1.1.0-beta.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/OVERVIEW.md +19 -1
- package/README.md +77 -6
- package/client/access.tsx +66 -0
- package/client/agent-options.ts +12 -2
- package/client/agent-preferences.ts +112 -0
- package/client/browser.tsx +12 -9
- package/client/graph-labels.ts +137 -0
- package/client/launch-choices.ts +18 -5
- package/client/launch-plan.ts +88 -39
- package/client/launch.tsx +9 -4
- package/client/live-agents.tsx +407 -0
- package/client/live-cursor.tsx +291 -0
- package/client/live-feed.tsx +151 -0
- package/client/live-graph.tsx +434 -0
- package/client/live-header.tsx +312 -0
- package/client/live-issues.tsx +345 -0
- package/client/live-map.tsx +345 -0
- package/client/live-panel.tsx +444 -0
- package/client/live-timeline.ts +341 -0
- package/client/pill.tsx +393 -0
- package/client/project-init.tsx +168 -0
- package/client/settings-access.tsx +56 -0
- package/client/settings-live.tsx +121 -0
- package/client/settings-projects.tsx +9 -0
- package/index.client.tsx +39 -1
- package/index.server.ts +24 -4
- package/package.json +1 -1
- package/server/agent-runs.ts +299 -0
- package/server/handlers.ts +12 -1
- package/server/init.ts +64 -0
- package/server/links.ts +153 -0
- package/server/live.ts +334 -0
- package/server/stores.ts +119 -0
- package/server/sync.ts +127 -0
- package/shared/activity.ts +308 -0
- package/shared/graph-geometry.ts +122 -0
- package/shared/graph-model.ts +319 -0
- package/shared/links.ts +252 -0
- package/shared/live.ts +200 -0
- package/shared/map-model.ts +192 -0
- package/shared/project-setup.ts +55 -0
- package/shared/prompts.ts +16 -1
- package/shared/settings.ts +57 -1
- package/shared/shell-activity.ts +309 -0
- package/shared/shell-lexer.ts +180 -0
- package/shared/subagents.ts +106 -0
- package/shared/todo-sync.ts +158 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { WorkflowState } from "./linear";
|
|
2
|
+
|
|
3
|
+
// Claude and Codex todos share one timeline shape. Codex ids are list positions, so todos
|
|
4
|
+
// map to issues by a key prefix in their text, such as "ENG-124: add the queue".
|
|
5
|
+
|
|
6
|
+
export type TodoStatus = "pending" | "in_progress" | "completed";
|
|
7
|
+
|
|
8
|
+
export interface TodoLike {
|
|
9
|
+
text: string;
|
|
10
|
+
completed: boolean;
|
|
11
|
+
status?: TodoStatus;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface KeyProgress {
|
|
15
|
+
key: string;
|
|
16
|
+
total: number;
|
|
17
|
+
completed: number;
|
|
18
|
+
inProgress: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const KEY_PREFIX =
|
|
22
|
+
/^\s*(?:[-*]\s+)?(?:\[([A-Za-z][A-Za-z0-9]*-\d+)\]\s*:?|([A-Za-z][A-Za-z0-9]*-\d+)\s*(?::|-\s))/;
|
|
23
|
+
|
|
24
|
+
export function todoStatus(item: TodoLike): TodoStatus {
|
|
25
|
+
return item.status ?? (item.completed ? "completed" : "pending");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function todoKey(text: string): string | null {
|
|
29
|
+
const match = KEY_PREFIX.exec(text);
|
|
30
|
+
const key = match?.[1] ?? match?.[2];
|
|
31
|
+
return key ? key.toUpperCase() : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Strips the key prefix so a todo reads as plain text under its issue. */
|
|
35
|
+
export function todoLabel(text: string): string {
|
|
36
|
+
const match = KEY_PREFIX.exec(text);
|
|
37
|
+
return match ? text.slice(match[0].length).trim() || text : text;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function progressByKey(items: readonly TodoLike[]): Map<string, KeyProgress> {
|
|
41
|
+
const progress = new Map<string, KeyProgress>();
|
|
42
|
+
for (const item of items) {
|
|
43
|
+
const key = todoKey(item.text);
|
|
44
|
+
if (!key) continue;
|
|
45
|
+
const entry = progress.get(key) ?? { key, total: 0, completed: 0, inProgress: 0 };
|
|
46
|
+
const status = todoStatus(item);
|
|
47
|
+
entry.total += 1;
|
|
48
|
+
if (status === "completed") entry.completed += 1;
|
|
49
|
+
if (status === "in_progress") entry.inProgress += 1;
|
|
50
|
+
progress.set(key, entry);
|
|
51
|
+
}
|
|
52
|
+
return progress;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SyncIssue {
|
|
56
|
+
id: string;
|
|
57
|
+
identifier: string;
|
|
58
|
+
state: WorkflowState;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SyncInput {
|
|
62
|
+
parent: SyncIssue & { children: readonly SyncIssue[] };
|
|
63
|
+
progress: ReadonlyMap<string, KeyProgress>;
|
|
64
|
+
/** Workflow states of the parent's team. */
|
|
65
|
+
states: readonly WorkflowState[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface StatusMove {
|
|
69
|
+
issueId: string;
|
|
70
|
+
identifier: string;
|
|
71
|
+
from: WorkflowState;
|
|
72
|
+
to: WorkflowState;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const RANK: Record<string, number> = {
|
|
76
|
+
triage: 0,
|
|
77
|
+
backlog: 0,
|
|
78
|
+
unstarted: 1,
|
|
79
|
+
started: 2,
|
|
80
|
+
completed: 3,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function rank(state: WorkflowState): number | null {
|
|
84
|
+
return RANK[state.type] ?? null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const byPosition = (a: WorkflowState, b: WorkflowState) => a.position - b.position;
|
|
88
|
+
|
|
89
|
+
function firstOfType(states: readonly WorkflowState[], type: string, exclude?: RegExp) {
|
|
90
|
+
return (
|
|
91
|
+
states
|
|
92
|
+
.filter((state) => state.type === type && !(exclude && exclude.test(state.name)))
|
|
93
|
+
.sort(byPosition)[0] ?? null
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const REVIEW = /review/i;
|
|
98
|
+
|
|
99
|
+
export function reviewState(states: readonly WorkflowState[]): WorkflowState | null {
|
|
100
|
+
return states.filter((s) => s.type === "started" && REVIEW.test(s.name)).sort(byPosition)[0] ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function targetFor(progress: KeyProgress | undefined): "started" | "completed" | null {
|
|
104
|
+
if (!progress || progress.total === 0) return null;
|
|
105
|
+
if (progress.completed === progress.total) return "completed";
|
|
106
|
+
return progress.completed > 0 || progress.inProgress > 0 ? "started" : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Forward-only moves for the parent and its direct children; a todo naming any other key is
|
|
110
|
+
// ignored. Canceled issues never move, and the parent stops at a review state, never Done.
|
|
111
|
+
export function planStatusMoves(input: SyncInput): StatusMove[] {
|
|
112
|
+
const { parent, progress, states } = input;
|
|
113
|
+
const known = new Set(states.map((state) => state.id));
|
|
114
|
+
const started = firstOfType(states, "started", REVIEW) ?? firstOfType(states, "started");
|
|
115
|
+
const completed = firstOfType(states, "completed");
|
|
116
|
+
const moves: StatusMove[] = [];
|
|
117
|
+
const effective = new Map<string, WorkflowState>();
|
|
118
|
+
|
|
119
|
+
for (const child of parent.children) {
|
|
120
|
+
effective.set(child.id, child.state);
|
|
121
|
+
const target = targetFor(progress.get(child.identifier.toUpperCase()));
|
|
122
|
+
const to = target === "completed" ? completed : target === "started" ? started : null;
|
|
123
|
+
const current = rank(child.state);
|
|
124
|
+
if (!to || current === null || !known.has(child.state.id)) continue;
|
|
125
|
+
if ((rank(to) ?? 0) <= current) continue;
|
|
126
|
+
moves.push({ issueId: child.id, identifier: child.identifier, from: child.state, to });
|
|
127
|
+
effective.set(child.id, to);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const parentRank = rank(parent.state);
|
|
131
|
+
if (parentRank === null || !known.has(parent.state.id)) return moves;
|
|
132
|
+
const anyWork =
|
|
133
|
+
moves.length > 0 ||
|
|
134
|
+
targetFor(progress.get(parent.identifier.toUpperCase())) !== null ||
|
|
135
|
+
parent.children.some((child) => targetFor(progress.get(child.identifier.toUpperCase())));
|
|
136
|
+
const allDone =
|
|
137
|
+
parent.children.length > 0 &&
|
|
138
|
+
parent.children.every((child) => effective.get(child.id)?.type === "completed");
|
|
139
|
+
const review = reviewState(states);
|
|
140
|
+
if (allDone && review && parentRank <= RANK.started && parent.state.id !== review.id) {
|
|
141
|
+
if (!(parent.state.type === "started" && REVIEW.test(parent.state.name))) {
|
|
142
|
+
moves.push({ issueId: parent.id, identifier: parent.identifier, from: parent.state, to: review });
|
|
143
|
+
}
|
|
144
|
+
} else if (anyWork && started && parentRank < RANK.started) {
|
|
145
|
+
moves.push({ issueId: parent.id, identifier: parent.identifier, from: parent.state, to: started });
|
|
146
|
+
}
|
|
147
|
+
return moves;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The prompt line that makes todos map to sub-issues. */
|
|
151
|
+
export function todoConventionLine(children: readonly { identifier: string }[]): string | null {
|
|
152
|
+
const first = children[0];
|
|
153
|
+
if (!first) return null;
|
|
154
|
+
return [
|
|
155
|
+
"Track your work with one or more todos per sub-issue.",
|
|
156
|
+
`Start each todo with its sub-issue key, for example "${first.identifier}: <task>".`,
|
|
157
|
+
].join(" ");
|
|
158
|
+
}
|