opencode-superagent 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +336 -2
- package/agent/superagent-router.md +7 -0
- package/agent/superagent-worker.md +10 -0
- package/dist/server.d.ts +237 -0
- package/dist/server.js +148 -0
- package/dist/src/abort.d.ts +31 -0
- package/dist/src/abort.js +171 -0
- package/dist/src/agents.d.ts +51 -0
- package/dist/src/agents.js +83 -0
- package/dist/src/context.d.ts +27 -0
- package/dist/src/context.js +114 -0
- package/dist/src/dirs.d.ts +45 -0
- package/dist/src/dirs.js +129 -0
- package/dist/src/dispatch.d.ts +38 -0
- package/dist/src/dispatch.js +296 -0
- package/dist/src/format.d.ts +3 -0
- package/dist/src/format.js +14 -0
- package/dist/src/guard.d.ts +4 -0
- package/dist/src/guard.js +9 -0
- package/dist/src/prompts.d.ts +1 -0
- package/dist/src/prompts.js +37 -0
- package/dist/src/protocol.d.ts +219 -0
- package/dist/src/protocol.js +95 -0
- package/dist/src/recall.d.ts +58 -0
- package/dist/src/recall.js +309 -0
- package/dist/src/report.d.ts +78 -0
- package/dist/src/report.js +176 -0
- package/dist/src/router.d.ts +34 -0
- package/dist/src/router.js +234 -0
- package/dist/src/session-meta.d.ts +42 -0
- package/dist/src/session-meta.js +91 -0
- package/dist/src/sleep.d.ts +35 -0
- package/dist/src/sleep.js +58 -0
- package/dist/src/state.d.ts +126 -0
- package/dist/src/state.js +480 -0
- package/dist/src/ticket-tools.d.ts +45 -0
- package/dist/src/ticket-tools.js +148 -0
- package/dist/src/ticket-view.d.ts +62 -0
- package/dist/src/ticket-view.js +135 -0
- package/dist/src/ticket.d.ts +253 -0
- package/dist/src/ticket.js +717 -0
- package/dist/src/tools.d.ts +85 -0
- package/dist/src/tools.js +392 -0
- package/dist/src/watchdog.d.ts +23 -0
- package/dist/src/watchdog.js +94 -0
- package/dist/src/watcher.d.ts +17 -0
- package/dist/src/watcher.js +165 -0
- package/dist/tui.d.ts +3 -0
- package/dist/tui.js +1057 -0
- package/package.json +80 -4
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar view model for the ticket list.
|
|
3
|
+
*
|
|
4
|
+
* The `tickets` RPC returns `renderTicketList` text rather than structured
|
|
5
|
+
* rows (the server side is deployed separately and needs a service restart to
|
|
6
|
+
* change), so the TUI parses that text back into rows and re-lays them out for
|
|
7
|
+
* the fixed-width sidebar. Everything here is pure and width-aware, which
|
|
8
|
+
* keeps the OpenTUI component in `tui.tsx` to rendering only.
|
|
9
|
+
*
|
|
10
|
+
* `src/ticket.ts` is only imported for types: valuing `TICKET_STATES` here
|
|
11
|
+
* would pull the Effect/KV store into the TUI bundle. `test/ticket-view.test.ts`
|
|
12
|
+
* asserts the local copy stays in step with the real state list.
|
|
13
|
+
*/
|
|
14
|
+
import type { TicketPriority, TicketState } from "./ticket.js";
|
|
15
|
+
export type { TicketState };
|
|
16
|
+
export declare const TICKET_STATES: readonly ["open", "in_progress", "blocked", "done", "failed", "cancelled", "superseded"];
|
|
17
|
+
/**
|
|
18
|
+
* `SESSION_SIDEBAR_WIDTH` is 42 columns: 4 of horizontal padding plus the
|
|
19
|
+
* scrollbar column the sidebar reserves on the right leave 37 cells. The panel
|
|
20
|
+
* uses 36 and keeps one cell of slack, so a row can never push the sidebar
|
|
21
|
+
* into a horizontal scroll.
|
|
22
|
+
*/
|
|
23
|
+
export declare const TICKET_PANEL_WIDTH = 36;
|
|
24
|
+
/** Titles are capped below the full remainder so a long one never pushes the id off-screen. */
|
|
25
|
+
export declare const TICKET_TITLE_MAX = 26;
|
|
26
|
+
/** Terminal tickets are collapsed; the panel is a glance, not an archive. */
|
|
27
|
+
export declare const TICKET_TERMINAL_LIMIT = 3;
|
|
28
|
+
export declare const TICKET_PANEL_LIMIT = 20;
|
|
29
|
+
export declare const TICKET_PANEL_REFRESH_MS = 4000;
|
|
30
|
+
export declare const TICKET_STATE_SYMBOL: Readonly<Record<TicketState, string>>;
|
|
31
|
+
export interface TicketRow {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly title: string;
|
|
34
|
+
readonly state: TicketState;
|
|
35
|
+
readonly priority: TicketPriority;
|
|
36
|
+
}
|
|
37
|
+
export interface TicketPanelItem {
|
|
38
|
+
readonly id: string;
|
|
39
|
+
readonly shortID: string;
|
|
40
|
+
readonly title: string;
|
|
41
|
+
readonly state: TicketState;
|
|
42
|
+
}
|
|
43
|
+
export interface TicketPanel {
|
|
44
|
+
readonly total: number;
|
|
45
|
+
/** Tickets that are not closed yet, i.e. their count in the header. */
|
|
46
|
+
readonly active: number;
|
|
47
|
+
readonly items: ReadonlyArray<TicketPanelItem>;
|
|
48
|
+
/** Terminal tickets that did not fit under `TICKET_TERMINAL_LIMIT`. */
|
|
49
|
+
readonly hidden: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Parse `renderTicketList` output. Rows arrive grouped by state and already
|
|
53
|
+
* sorted newest-first inside a group, so the input order is the time order.
|
|
54
|
+
* Unrecognised lines are ignored: a future format change degrades the panel to
|
|
55
|
+
* "no tickets" instead of breaking the sidebar.
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseTicketList(text: string): Array<TicketRow>;
|
|
58
|
+
/** Truncate to `max` terminal cells (CJK counts as 2) with a trailing ellipsis. */
|
|
59
|
+
export declare function truncateCells(text: string, max: number): string;
|
|
60
|
+
export declare function shortTicketID(ticketID: string): string;
|
|
61
|
+
/** Order, truncate and collapse parsed rows into what the sidebar renders. */
|
|
62
|
+
export declare function ticketPanel(rows: ReadonlyArray<TicketRow>, width?: number, terminalLimit?: number): TicketPanel;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar view model for the ticket list.
|
|
3
|
+
*
|
|
4
|
+
* The `tickets` RPC returns `renderTicketList` text rather than structured
|
|
5
|
+
* rows (the server side is deployed separately and needs a service restart to
|
|
6
|
+
* change), so the TUI parses that text back into rows and re-lays them out for
|
|
7
|
+
* the fixed-width sidebar. Everything here is pure and width-aware, which
|
|
8
|
+
* keeps the OpenTUI component in `tui.tsx` to rendering only.
|
|
9
|
+
*
|
|
10
|
+
* `src/ticket.ts` is only imported for types: valuing `TICKET_STATES` here
|
|
11
|
+
* would pull the Effect/KV store into the TUI bundle. `test/ticket-view.test.ts`
|
|
12
|
+
* asserts the local copy stays in step with the real state list.
|
|
13
|
+
*/
|
|
14
|
+
import { displayWidth } from "./tools.js";
|
|
15
|
+
export const TICKET_STATES = [
|
|
16
|
+
"open",
|
|
17
|
+
"in_progress",
|
|
18
|
+
"blocked",
|
|
19
|
+
"done",
|
|
20
|
+
"failed",
|
|
21
|
+
"cancelled",
|
|
22
|
+
"superseded",
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* `SESSION_SIDEBAR_WIDTH` is 42 columns: 4 of horizontal padding plus the
|
|
26
|
+
* scrollbar column the sidebar reserves on the right leave 37 cells. The panel
|
|
27
|
+
* uses 36 and keeps one cell of slack, so a row can never push the sidebar
|
|
28
|
+
* into a horizontal scroll.
|
|
29
|
+
*/
|
|
30
|
+
export const TICKET_PANEL_WIDTH = 36;
|
|
31
|
+
/** Titles are capped below the full remainder so a long one never pushes the id off-screen. */
|
|
32
|
+
export const TICKET_TITLE_MAX = 26;
|
|
33
|
+
/** Terminal tickets are collapsed; the panel is a glance, not an archive. */
|
|
34
|
+
export const TICKET_TERMINAL_LIMIT = 3;
|
|
35
|
+
export const TICKET_PANEL_LIMIT = 20;
|
|
36
|
+
export const TICKET_PANEL_REFRESH_MS = 4000;
|
|
37
|
+
const SHORT_ID_LENGTH = 4;
|
|
38
|
+
export const TICKET_STATE_SYMBOL = {
|
|
39
|
+
open: "○",
|
|
40
|
+
in_progress: "●",
|
|
41
|
+
blocked: "◆",
|
|
42
|
+
done: "✓",
|
|
43
|
+
failed: "✗",
|
|
44
|
+
cancelled: "⊘",
|
|
45
|
+
superseded: "·",
|
|
46
|
+
};
|
|
47
|
+
/** Unfinished states first (running work, then stuck work, then queued), terminal ones last. */
|
|
48
|
+
const STATE_RANK = {
|
|
49
|
+
in_progress: 0,
|
|
50
|
+
blocked: 1,
|
|
51
|
+
open: 2,
|
|
52
|
+
done: 3,
|
|
53
|
+
failed: 4,
|
|
54
|
+
cancelled: 5,
|
|
55
|
+
superseded: 6,
|
|
56
|
+
};
|
|
57
|
+
const TERMINAL = new Set(["done", "failed", "cancelled", "superseded"]);
|
|
58
|
+
const GROUP_LINE = /^(open|in_progress|blocked|done|failed|cancelled|superseded) \(\d+\)$/;
|
|
59
|
+
const ITEM_LINE = /^· (tk_\S+) (p[012])(?: (.*))?$/;
|
|
60
|
+
/** `renderTicketList` writes `MM-DD HH:MM` (plus an optional worker suffix) under every item. */
|
|
61
|
+
const TRAILER_LINE = /^\s{2}\d{2}-\d{2} \d{2}:\d{2}(?: w=\S+)?$/;
|
|
62
|
+
/**
|
|
63
|
+
* Parse `renderTicketList` output. Rows arrive grouped by state and already
|
|
64
|
+
* sorted newest-first inside a group, so the input order is the time order.
|
|
65
|
+
* Unrecognised lines are ignored: a future format change degrades the panel to
|
|
66
|
+
* "no tickets" instead of breaking the sidebar.
|
|
67
|
+
*/
|
|
68
|
+
export function parseTicketList(text) {
|
|
69
|
+
const rows = [];
|
|
70
|
+
let state;
|
|
71
|
+
for (const line of text.split("\n")) {
|
|
72
|
+
const group = GROUP_LINE.exec(line);
|
|
73
|
+
if (group?.[1] !== undefined) {
|
|
74
|
+
state = group[1];
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const item = ITEM_LINE.exec(line);
|
|
78
|
+
if (item !== null && state !== undefined) {
|
|
79
|
+
rows.push({
|
|
80
|
+
id: item[1],
|
|
81
|
+
priority: item[2],
|
|
82
|
+
state,
|
|
83
|
+
title: (item[3] ?? "").trim(),
|
|
84
|
+
});
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// A wrapped title continues on indented lines; trailers are not titles.
|
|
88
|
+
if (state !== undefined && rows.length > 0 && line.startsWith(" ") && !TRAILER_LINE.test(line)) {
|
|
89
|
+
const last = rows[rows.length - 1];
|
|
90
|
+
const continuation = line.trim();
|
|
91
|
+
if (continuation !== "")
|
|
92
|
+
last.title = last.title === "" ? continuation : `${last.title} ${continuation}`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return rows;
|
|
96
|
+
}
|
|
97
|
+
/** Truncate to `max` terminal cells (CJK counts as 2) with a trailing ellipsis. */
|
|
98
|
+
export function truncateCells(text, max) {
|
|
99
|
+
if (max <= 0)
|
|
100
|
+
return "";
|
|
101
|
+
if (displayWidth(text) <= max)
|
|
102
|
+
return text;
|
|
103
|
+
let result = "";
|
|
104
|
+
let cells = 0;
|
|
105
|
+
for (const char of text) {
|
|
106
|
+
const width = displayWidth(char);
|
|
107
|
+
if (cells + width > max - 1)
|
|
108
|
+
break;
|
|
109
|
+
result += char;
|
|
110
|
+
cells += width;
|
|
111
|
+
}
|
|
112
|
+
return `${result}…`;
|
|
113
|
+
}
|
|
114
|
+
export function shortTicketID(ticketID) {
|
|
115
|
+
return ticketID.length <= SHORT_ID_LENGTH ? ticketID : ticketID.slice(-SHORT_ID_LENGTH);
|
|
116
|
+
}
|
|
117
|
+
/** Order, truncate and collapse parsed rows into what the sidebar renders. */
|
|
118
|
+
export function ticketPanel(rows, width = TICKET_PANEL_WIDTH, terminalLimit = TICKET_TERMINAL_LIMIT) {
|
|
119
|
+
const titleMax = Math.min(TICKET_TITLE_MAX, width - (1 + 1 + 1 + SHORT_ID_LENGTH));
|
|
120
|
+
// `Array.prototype.sort` is stable, so newest-first survives inside a state.
|
|
121
|
+
const ordered = [...rows].sort((left, right) => STATE_RANK[left.state] - STATE_RANK[right.state]);
|
|
122
|
+
const active = ordered.filter((row) => !TERMINAL.has(row.state));
|
|
123
|
+
const terminal = ordered.filter((row) => TERMINAL.has(row.state));
|
|
124
|
+
return {
|
|
125
|
+
total: rows.length,
|
|
126
|
+
active: active.length,
|
|
127
|
+
hidden: Math.max(0, terminal.length - terminalLimit),
|
|
128
|
+
items: [...active, ...terminal.slice(0, terminalLimit)].map((row) => ({
|
|
129
|
+
id: row.id,
|
|
130
|
+
shortID: shortTicketID(row.id),
|
|
131
|
+
title: truncateCells(row.title, titleMax),
|
|
132
|
+
state: row.state,
|
|
133
|
+
})),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode/plugin/effect";
|
|
2
|
+
import { Tool } from "@opencode/schema/tool";
|
|
3
|
+
import { Effect, type Schema } from "effect";
|
|
4
|
+
import { type TriggerInput, type WorkerState } from "./state.js";
|
|
5
|
+
/**
|
|
6
|
+
* Ticket (work order) store for the Super-agent P0.
|
|
7
|
+
*
|
|
8
|
+
* A ticket is the durable statement of "what the user asked for and how we
|
|
9
|
+
* will know it is done". The Router creates one before dispatching work; the
|
|
10
|
+
* dispatch records the worker session on it and the worker's terminal state
|
|
11
|
+
* closes it. Records live in KV under `ticket:<id>`; the `ticketidx:*` keys
|
|
12
|
+
* are derived accelerators, so hydration always rebuilds them from the
|
|
13
|
+
* record prefix (a lost index never loses a ticket).
|
|
14
|
+
*
|
|
15
|
+
* State machine:
|
|
16
|
+
*
|
|
17
|
+
* open ─┬─▶ in_progress ─┬─▶ done
|
|
18
|
+
* │ ├─▶ failed
|
|
19
|
+
* │ ├─▶ blocked ──▶ in_progress
|
|
20
|
+
* ├─▶ blocked └─▶ cancelled
|
|
21
|
+
* ├─▶ cancelled
|
|
22
|
+
* └─▶ superseded
|
|
23
|
+
*
|
|
24
|
+
* `done`, `failed`, `cancelled` and `superseded` are terminal.
|
|
25
|
+
*/
|
|
26
|
+
export type TicketState = "open" | "in_progress" | "blocked" | "done" | "failed" | "cancelled" | "superseded";
|
|
27
|
+
export type TicketPriority = "p0" | "p1" | "p2";
|
|
28
|
+
export type TicketCloseOutcome = "done" | "failed" | "cancelled";
|
|
29
|
+
export interface TicketRequester {
|
|
30
|
+
readonly source: string;
|
|
31
|
+
readonly threadID: string;
|
|
32
|
+
readonly author?: string;
|
|
33
|
+
}
|
|
34
|
+
/** Which trigger authorized the ticket: the id plus a short quote of what the user said. */
|
|
35
|
+
export interface TicketAuthorization {
|
|
36
|
+
readonly triggerID: string;
|
|
37
|
+
readonly quote: string;
|
|
38
|
+
readonly at: number;
|
|
39
|
+
}
|
|
40
|
+
export interface TicketAttempt {
|
|
41
|
+
readonly at: number;
|
|
42
|
+
readonly sessionID?: string;
|
|
43
|
+
readonly note?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface TicketHistoryEntry {
|
|
46
|
+
readonly at: number;
|
|
47
|
+
readonly kind: "created" | "state" | "note" | "dispatch" | "closed";
|
|
48
|
+
readonly from?: TicketState;
|
|
49
|
+
readonly to?: TicketState;
|
|
50
|
+
readonly note?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface Ticket {
|
|
53
|
+
readonly id: string;
|
|
54
|
+
state: TicketState;
|
|
55
|
+
readonly title: string;
|
|
56
|
+
readonly objective: string;
|
|
57
|
+
readonly acceptance: ReadonlyArray<string>;
|
|
58
|
+
readonly requester: TicketRequester;
|
|
59
|
+
readonly authorization: TicketAuthorization;
|
|
60
|
+
readonly threadID: string;
|
|
61
|
+
workerSessionID?: string;
|
|
62
|
+
readonly attempts: Array<TicketAttempt>;
|
|
63
|
+
readonly priority: TicketPriority;
|
|
64
|
+
directory?: string;
|
|
65
|
+
readonly dedupeKey: string;
|
|
66
|
+
readonly parentTicketID?: string;
|
|
67
|
+
readonly tags: Array<string>;
|
|
68
|
+
readonly createdAt: number;
|
|
69
|
+
updatedAt: number;
|
|
70
|
+
closedAt?: number;
|
|
71
|
+
closeReason?: string;
|
|
72
|
+
readonly history: Array<TicketHistoryEntry>;
|
|
73
|
+
}
|
|
74
|
+
export declare const TICKET_PREFIX = "ticket:";
|
|
75
|
+
export declare const TICKET_STATUS_PREFIX = "ticketidx:status:";
|
|
76
|
+
export declare const TICKET_THREAD_PREFIX = "ticketidx:thread:";
|
|
77
|
+
export declare const TICKET_ALL_KEY = "ticketidx:all";
|
|
78
|
+
export declare const TITLE_MAX = 80;
|
|
79
|
+
export declare const OBJECTIVE_MAX = 2000;
|
|
80
|
+
export declare const ACCEPTANCE_MAX = 10;
|
|
81
|
+
export declare const ACCEPTANCE_ITEM_MAX = 300;
|
|
82
|
+
export declare const QUOTE_MAX = 300;
|
|
83
|
+
export declare const SUMMARY_MAX = 500;
|
|
84
|
+
/** Index buckets are capped so a long-lived gateway never writes an unbounded list. */
|
|
85
|
+
export declare const INDEX_LIMIT = 200;
|
|
86
|
+
/** Newest-N tickets kept in memory; older records stay in KV and reappear after hydrate. */
|
|
87
|
+
export declare const MEMORY_LIMIT = 500;
|
|
88
|
+
export declare const LIST_DEFAULT = 20;
|
|
89
|
+
export declare const LIST_MAX = 100;
|
|
90
|
+
export declare const TICKET_STATES: ReadonlyArray<TicketState>;
|
|
91
|
+
/** A ticket can be dispatched while it is one of these. */
|
|
92
|
+
export declare const ACTIVE_STATES: ReadonlyArray<TicketState>;
|
|
93
|
+
export declare function isTerminalState(state: TicketState): boolean;
|
|
94
|
+
export declare function allowedTransitions(state: TicketState): ReadonlyArray<TicketState>;
|
|
95
|
+
/** Same-state moves are no-ops, not transitions. */
|
|
96
|
+
export declare function canTransition(from: TicketState, to: TicketState): boolean;
|
|
97
|
+
/** Human-readable reason a move is illegal, or `undefined` when it is allowed. */
|
|
98
|
+
export declare function transitionError(from: TicketState, to: TicketState): string | undefined;
|
|
99
|
+
/** Lowercased, whitespace-collapsed objective, so wording drift does not defeat dedupe. */
|
|
100
|
+
export declare function normalizeObjective(text: string): string;
|
|
101
|
+
export declare function dedupeKeyFor(input: {
|
|
102
|
+
readonly source: string;
|
|
103
|
+
readonly threadID: string;
|
|
104
|
+
readonly objective: string;
|
|
105
|
+
}): string;
|
|
106
|
+
export declare const ticketKey: (id: string) => string;
|
|
107
|
+
export declare const ticketStatusKey: (state: TicketState) => string;
|
|
108
|
+
export declare const ticketThreadKey: (threadID: string) => string;
|
|
109
|
+
/** In-memory mirror of every hydrated ticket, rebuilt from KV at startup. */
|
|
110
|
+
export declare const tickets: Map<string, Ticket>;
|
|
111
|
+
/**
|
|
112
|
+
* The trigger most recently delivered to the Router. `ticket_create` copies
|
|
113
|
+
* it into `authorization`, so a ticket always points at what the user said.
|
|
114
|
+
* It is a heuristic: the newest flushed trigger wins, which matches how the
|
|
115
|
+
* Router batches queued events before answering.
|
|
116
|
+
*/
|
|
117
|
+
export interface RouterTriggerNote {
|
|
118
|
+
readonly id: string;
|
|
119
|
+
readonly source: string;
|
|
120
|
+
readonly threadID: string;
|
|
121
|
+
readonly author?: string;
|
|
122
|
+
readonly text: string;
|
|
123
|
+
readonly at: number;
|
|
124
|
+
}
|
|
125
|
+
export declare function noteRouterTrigger(trigger: TriggerInput): void;
|
|
126
|
+
export declare function currentTrigger(): RouterTriggerNote | undefined;
|
|
127
|
+
/** Reset memory (hydrate and tests): the map and its thread index are rebuilt together. */
|
|
128
|
+
export declare function resetTickets(): void;
|
|
129
|
+
export declare function getTicket(ticketID: string): Ticket | undefined;
|
|
130
|
+
/** Same-thread, same-dedupe-key ticket still open or in progress, if any. */
|
|
131
|
+
export declare function findDuplicate(threadID: string, dedupeKey: string): Ticket | undefined;
|
|
132
|
+
export declare function isActive(ticket: Ticket): boolean;
|
|
133
|
+
export interface TicketStats {
|
|
134
|
+
readonly total: number;
|
|
135
|
+
readonly open: number;
|
|
136
|
+
readonly inProgress: number;
|
|
137
|
+
readonly blocked: number;
|
|
138
|
+
readonly closed: number;
|
|
139
|
+
}
|
|
140
|
+
export declare function ticketStats(): TicketStats;
|
|
141
|
+
/** Compact "tk_x title; tk_y title" line for the system status, empty when there is nothing active. */
|
|
142
|
+
export declare function activeTicketSummary(threadID: string | undefined, limit?: number): string;
|
|
143
|
+
/** Active ticket for a worker session, used when a worker ends. */
|
|
144
|
+
export declare function activeTicketForWorker(sessionID: string): Ticket | undefined;
|
|
145
|
+
/**
|
|
146
|
+
* Worker states after which the ticket may be dispatched again. `idle` is not
|
|
147
|
+
* here on purpose: an idle session can still be resumed, so a re-dispatch
|
|
148
|
+
* would race a live worker.
|
|
149
|
+
*/
|
|
150
|
+
export declare const TERMINAL_WORKER_STATES: ReadonlyArray<WorkerState>;
|
|
151
|
+
export declare function isWorkerTerminal(state: WorkerState): boolean;
|
|
152
|
+
export declare function validateCreateInput(input: {
|
|
153
|
+
readonly threadID?: string;
|
|
154
|
+
readonly title?: string;
|
|
155
|
+
readonly objective?: string;
|
|
156
|
+
readonly acceptance?: ReadonlyArray<string>;
|
|
157
|
+
readonly directory?: string;
|
|
158
|
+
}): string | undefined;
|
|
159
|
+
export declare function decodeTicket(value: Schema.Json | undefined): Ticket | undefined;
|
|
160
|
+
export declare function encodeTicket(ticket: Ticket): Schema.Json;
|
|
161
|
+
export interface CreateTicketInput {
|
|
162
|
+
readonly threadID: string;
|
|
163
|
+
readonly title: string;
|
|
164
|
+
readonly objective: string;
|
|
165
|
+
readonly acceptance: ReadonlyArray<string>;
|
|
166
|
+
readonly directory: string;
|
|
167
|
+
readonly source: string;
|
|
168
|
+
readonly author?: string;
|
|
169
|
+
readonly priority?: TicketPriority;
|
|
170
|
+
readonly parentTicketID?: string;
|
|
171
|
+
readonly dedupeKey?: string;
|
|
172
|
+
readonly tags?: ReadonlyArray<string>;
|
|
173
|
+
/** Defaults to the trigger most recently delivered to the Router. */
|
|
174
|
+
readonly authorization?: TicketAuthorization;
|
|
175
|
+
readonly now?: number;
|
|
176
|
+
}
|
|
177
|
+
export interface CreateTicketResult {
|
|
178
|
+
readonly ticket: Ticket;
|
|
179
|
+
readonly reused: boolean;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Create a ticket, or return the still-open same-key ticket in the same
|
|
183
|
+
* thread (`reused: true`). Objective, acceptance and directory are required;
|
|
184
|
+
* an empty acceptance is rejected so no placeholder ticket can exist.
|
|
185
|
+
*/
|
|
186
|
+
export declare const createTicket: (ctx: Plugin.Context, input: CreateTicketInput) => Effect.Effect<{
|
|
187
|
+
ticket: Ticket;
|
|
188
|
+
reused: boolean;
|
|
189
|
+
}, Tool.Error, never>;
|
|
190
|
+
export interface UpdateTicketInput {
|
|
191
|
+
readonly ticketID: string;
|
|
192
|
+
readonly state?: TicketState;
|
|
193
|
+
readonly note?: string;
|
|
194
|
+
readonly workerSessionID?: string;
|
|
195
|
+
readonly now?: number;
|
|
196
|
+
}
|
|
197
|
+
/** Move a ticket through the state machine. Illegal moves fail with a `Tool.Error`. */
|
|
198
|
+
export declare const updateTicket: (ctx: Plugin.Context, input: UpdateTicketInput) => Effect.Effect<Ticket, Tool.Error, never>;
|
|
199
|
+
export interface CloseTicketInput {
|
|
200
|
+
readonly ticketID: string;
|
|
201
|
+
readonly outcome: TicketCloseOutcome;
|
|
202
|
+
readonly summary: string;
|
|
203
|
+
readonly now?: number;
|
|
204
|
+
}
|
|
205
|
+
/** Close a ticket with an outcome, recording summary/closedAt in the record and history. */
|
|
206
|
+
export declare const closeTicket: (ctx: Plugin.Context, input: CloseTicketInput) => Effect.Effect<Ticket, Tool.Error, never>;
|
|
207
|
+
export interface ListTicketsInput {
|
|
208
|
+
readonly threadID?: string;
|
|
209
|
+
readonly status?: TicketState;
|
|
210
|
+
readonly since?: number;
|
|
211
|
+
readonly limit?: number;
|
|
212
|
+
}
|
|
213
|
+
/** Newest-updated first; filters are combined with AND. */
|
|
214
|
+
export declare function listTickets(input?: ListTicketsInput): Array<Ticket>;
|
|
215
|
+
/**
|
|
216
|
+
* Gate every dispatch must pass: a dispatch always names a ticket, and that
|
|
217
|
+
* ticket must be workable right now. The rules are enforced here (not in the
|
|
218
|
+
* prompt) so a Router cannot dispatch without a ticket, onto a closed ticket,
|
|
219
|
+
* across threads, or onto a ticket that already has a live (non-terminal) worker.
|
|
220
|
+
*/
|
|
221
|
+
export declare const requireDispatchTicket: (ticketID: string | undefined, threadID: string | undefined) => Effect.Effect<Ticket, Tool.Error, never>;
|
|
222
|
+
export interface LinkTicketInput {
|
|
223
|
+
readonly ticketID: string;
|
|
224
|
+
readonly sessionID: string;
|
|
225
|
+
readonly note?: string;
|
|
226
|
+
readonly now?: number;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Attach a dispatched worker to its ticket and move `open` to `in_progress`.
|
|
230
|
+
* Only `open`/`in_progress` tickets accept a worker.
|
|
231
|
+
*/
|
|
232
|
+
export declare const linkTicketToWorker: (ctx: Plugin.Context, input: LinkTicketInput) => Effect.Effect<Ticket, Tool.Error, never>;
|
|
233
|
+
export interface CloseForWorkerInput {
|
|
234
|
+
readonly sessionID: string;
|
|
235
|
+
readonly outcome: "done" | "failed";
|
|
236
|
+
readonly summary?: string;
|
|
237
|
+
readonly note?: string;
|
|
238
|
+
readonly now?: number;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Close the in-progress ticket attached to a worker that just ended. Tickets
|
|
242
|
+
* in any other state are left alone: a closed ticket never reopens and an
|
|
243
|
+
* aborted worker is handled by its own path.
|
|
244
|
+
*/
|
|
245
|
+
export declare const closeTicketsForWorker: (ctx: Plugin.Context, input: CloseForWorkerInput) => Effect.Effect<Ticket | undefined, Tool.Error, never>;
|
|
246
|
+
/**
|
|
247
|
+
* Load every `ticket:<id>` record and rebuild the indexes. Older workers are
|
|
248
|
+
* never backfilled: a worker with no ticket stays without one, silently.
|
|
249
|
+
*/
|
|
250
|
+
export declare const hydrateTickets: (ctx: Plugin.Context) => Effect.Effect<void, never, never>;
|
|
251
|
+
export declare const DETAIL_WIDTH = 52;
|
|
252
|
+
export declare function renderTicketList(items: ReadonlyArray<Ticket>, width?: number): string;
|
|
253
|
+
export declare function renderTicketDetail(ticket: Ticket, width?: number): string;
|