@cueloop/schema 0.1.0-alpha.102 → 0.1.0-alpha.104
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/package.json +1 -1
- package/src/agent-comment.ts +2 -0
- package/src/agent-harness.ts +19 -0
- package/src/anchor.ts +13 -0
- package/src/diff.test.ts +1 -0
- package/src/diff.ts +4 -0
- package/src/feedback.ts +7 -0
- package/src/fuzzy.ts +5 -0
- package/src/herdr.ts +3 -0
- package/src/history.ts +16 -0
- package/src/inline.ts +9 -0
- package/src/markdown.ts +15 -0
- package/src/participants.ts +2 -0
- package/src/path-view.ts +4 -0
- package/src/share-access.ts +2 -0
- package/src/thread-agent.ts +9 -0
- package/src/types.ts +2 -0
- package/src/working-copy.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/schema",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.104",
|
|
4
4
|
"description": "The Thread primitive: types, markdown block model, quote-primary anchors, feedback serialization",
|
|
5
5
|
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
6
6
|
"bugs": {
|
package/src/agent-comment.ts
CHANGED
|
@@ -7,9 +7,11 @@ export function agentCommentRoot(state: ThreadAgentState, id: string): AgentComm
|
|
|
7
7
|
const comment = state.comments.find((entry) => entry.id === id);
|
|
8
8
|
|
|
9
9
|
if (comment) return comment;
|
|
10
|
+
|
|
10
11
|
const submission = state.submissions?.find((entry) => entry.id === id && entry.commentId === id);
|
|
11
12
|
|
|
12
13
|
if (!submission) return undefined;
|
|
14
|
+
|
|
13
15
|
const blocks = parseBlocks(submission.prompt);
|
|
14
16
|
const anchor = makeAnchor(blocks, 0, 0, Math.min(blocks[0]?.text.length ?? 0, 32768));
|
|
15
17
|
|
package/src/agent-harness.ts
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import type { AgentPermission, AgentTool, AgentConfigOption } from "./thread-agent";
|
|
2
2
|
|
|
3
|
+
/** Reasoning effort choices shared by Thread defaults and their settings controls. */
|
|
4
|
+
export const AGENT_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"] as const;
|
|
5
|
+
|
|
6
|
+
/** Reasoning effort stored in the new-conversation defaults. */
|
|
7
|
+
export type AgentReasoningEffort = (typeof AGENT_REASONING_EFFORTS)[number];
|
|
8
|
+
|
|
9
|
+
/** Model identity and effort supplied by the owner for new Thread conversations. */
|
|
10
|
+
export interface ThreadModelDefaults {
|
|
11
|
+
model: string;
|
|
12
|
+
effort: AgentReasoningEffort;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Provider-qualified model defaults apply only when a Thread creates a conversation. */
|
|
16
|
+
export const THREAD_MODEL_DEFAULTS: Record<"pi" | "fx", ThreadModelDefaults> = {
|
|
17
|
+
pi: { model: "openai-codex/gpt-6.1-sol", effort: "medium" },
|
|
18
|
+
fx: { model: "grok/grok-4.7", effort: "medium" },
|
|
19
|
+
};
|
|
20
|
+
|
|
3
21
|
/** Harness events carry transcript data without exposing a provider's wire protocol. */
|
|
4
22
|
export type AgentHarnessEvent =
|
|
5
23
|
| { kind: "message"; id?: string; text: string; replace?: boolean }
|
|
@@ -44,6 +62,7 @@ export interface AgentHarnessOptions {
|
|
|
44
62
|
export interface AgentHarnessAdapter {
|
|
45
63
|
id: string;
|
|
46
64
|
label: string;
|
|
65
|
+
recovery?: "durable";
|
|
47
66
|
connect(options: AgentHarnessOptions): AgentHarnessConnection;
|
|
48
67
|
remove?(sessionId: string): void | Promise<void>;
|
|
49
68
|
}
|
package/src/anchor.ts
CHANGED
|
@@ -94,8 +94,11 @@ function contextScore(
|
|
|
94
94
|
let score = 0;
|
|
95
95
|
|
|
96
96
|
if (prefix === anchor.prefix) score += 2;
|
|
97
|
+
|
|
97
98
|
if (suffix === anchor.suffix) score += 2;
|
|
99
|
+
|
|
98
100
|
if (anchor.blockIndex === blockIndex) score += 1;
|
|
101
|
+
|
|
99
102
|
if (anchor.blockIndex === blockIndex && anchor.start === start) score += 1;
|
|
100
103
|
|
|
101
104
|
return score;
|
|
@@ -138,11 +141,13 @@ const FUZZY_CONTEXT_TIE_BREAK = 0.01;
|
|
|
138
141
|
/** Block indices to scan, spiralling out from the anchor's hinted block: hint, hint+1, hint-1, ... */
|
|
139
142
|
function blockScanOrder(hint: number | undefined, count: number): number[] {
|
|
140
143
|
if (count <= 0) return [];
|
|
144
|
+
|
|
141
145
|
const first = Number.isInteger(hint) ? Math.min(Math.max(hint!, 0), count - 1) : 0;
|
|
142
146
|
const order = [first];
|
|
143
147
|
|
|
144
148
|
for (const step of Array.from({ length: count - 1 }, (_unused, index) => index + 1)) {
|
|
145
149
|
if (first + step < count) order.push(first + step);
|
|
150
|
+
|
|
146
151
|
if (first - step >= 0) order.push(first - step);
|
|
147
152
|
}
|
|
148
153
|
|
|
@@ -175,6 +180,7 @@ function findFuzzyCandidates(
|
|
|
175
180
|
|
|
176
181
|
candidates.push({ blockIndex, start: match.start, end: match.end, score });
|
|
177
182
|
}
|
|
183
|
+
|
|
178
184
|
if (budget.remaining <= 0) break;
|
|
179
185
|
}
|
|
180
186
|
|
|
@@ -184,6 +190,7 @@ function findFuzzyCandidates(
|
|
|
184
190
|
/** Highest-scoring candidate, or null when there are none. */
|
|
185
191
|
function pickBest(candidates: Candidate[]): Candidate | null {
|
|
186
192
|
if (candidates.length === 0) return null;
|
|
193
|
+
|
|
187
194
|
candidates.sort((left, right) => right.score - left.score);
|
|
188
195
|
|
|
189
196
|
return candidates[0]!;
|
|
@@ -214,6 +221,7 @@ function joinBlocks(blocks: Block[]): JoinedDocument {
|
|
|
214
221
|
|
|
215
222
|
blocks.forEach((block, blockIndex) => {
|
|
216
223
|
if (blockIndex > 0) text += BLOCK_SEPARATOR;
|
|
224
|
+
|
|
217
225
|
starts.push(text.length);
|
|
218
226
|
text += block.text;
|
|
219
227
|
});
|
|
@@ -241,11 +249,13 @@ function locateInBlocks(
|
|
|
241
249
|
let blockIndex = starts.findLastIndex((start) => start <= offset);
|
|
242
250
|
|
|
243
251
|
if (blockIndex < 0) blockIndex = 0;
|
|
252
|
+
|
|
244
253
|
const blockEnd = starts[blockIndex]! + blocks[blockIndex]!.text.length;
|
|
245
254
|
|
|
246
255
|
if (edge === "start" && offset >= blockEnd && blockIndex < blocks.length - 1) {
|
|
247
256
|
return { blockIndex: blockIndex + 1, offset: 0 };
|
|
248
257
|
}
|
|
258
|
+
|
|
249
259
|
if (edge === "end" && offset === starts[blockIndex] && blockIndex > 0) {
|
|
250
260
|
return {
|
|
251
261
|
blockIndex: blockIndex - 1,
|
|
@@ -272,7 +282,9 @@ function documentScore(
|
|
|
272
282
|
let score = 0;
|
|
273
283
|
|
|
274
284
|
if (prefix === anchor.prefix) score += 2;
|
|
285
|
+
|
|
275
286
|
if (suffix === anchor.suffix) score += 2;
|
|
287
|
+
|
|
276
288
|
if (anchor.blockIndex === startBlockIndex) score += 1;
|
|
277
289
|
|
|
278
290
|
return score;
|
|
@@ -369,6 +381,7 @@ function resolveSpanningAnchor(anchor: Anchor, blocks: Block[]): ResolvedAnchor
|
|
|
369
381
|
/** Resolve an anchor against the current blocks, or null when orphaned. */
|
|
370
382
|
export function resolveAnchor(anchor: Anchor, blocks: Block[]): ResolvedAnchor | null {
|
|
371
383
|
if (anchor.quote === "") return null;
|
|
384
|
+
|
|
372
385
|
if (anchor.quote.includes(BLOCK_SEPARATOR)) return resolveSpanningAnchor(anchor, blocks);
|
|
373
386
|
|
|
374
387
|
const exact = pickBest(findExactCandidates(anchor, blocks, anchor.quote));
|
package/src/diff.test.ts
CHANGED
package/src/diff.ts
CHANGED
|
@@ -70,6 +70,7 @@ export function unifiedDiff(oldText: string, newText: string, context = 3): Unif
|
|
|
70
70
|
const ops = lcsDiff(oldText.split("\n"), newText.split("\n"));
|
|
71
71
|
|
|
72
72
|
if (!ops.some((op) => op.kind !== "ctx")) return null;
|
|
73
|
+
|
|
73
74
|
let oldLineNumber = 1;
|
|
74
75
|
let newLineNumber = 1;
|
|
75
76
|
const rows = ops.map((op) => {
|
|
@@ -81,6 +82,7 @@ export function unifiedDiff(oldText: string, newText: string, context = 3): Unif
|
|
|
81
82
|
};
|
|
82
83
|
|
|
83
84
|
if (op.kind !== "add") oldLineNumber++;
|
|
85
|
+
|
|
84
86
|
if (op.kind !== "del") newLineNumber++;
|
|
85
87
|
|
|
86
88
|
return row;
|
|
@@ -107,10 +109,12 @@ export function unifiedDiff(oldText: string, newText: string, context = 3): Unif
|
|
|
107
109
|
|
|
108
110
|
return;
|
|
109
111
|
}
|
|
112
|
+
|
|
110
113
|
if (!openHunk) {
|
|
111
114
|
openHunk = { rows: [] };
|
|
112
115
|
hunks.push(openHunk);
|
|
113
116
|
}
|
|
117
|
+
|
|
114
118
|
openHunk.rows.push(row);
|
|
115
119
|
});
|
|
116
120
|
const lines: UnifiedLine[] = [];
|
package/src/feedback.ts
CHANGED
|
@@ -75,6 +75,7 @@ export function renderFeedback(input: FeedbackInput): string {
|
|
|
75
75
|
|
|
76
76
|
lines.push("# Review: " + input.outcome.replaceAll("_", " "));
|
|
77
77
|
lines.push("");
|
|
78
|
+
|
|
78
79
|
if (input.summary.trim()) {
|
|
79
80
|
lines.push(input.summary.trim());
|
|
80
81
|
lines.push("");
|
|
@@ -88,6 +89,7 @@ export function renderFeedback(input: FeedbackInput): string {
|
|
|
88
89
|
hasEdits = true;
|
|
89
90
|
lines.push("## Curated changes");
|
|
90
91
|
lines.push("");
|
|
92
|
+
|
|
91
93
|
if (input.workingCopy.trim()) {
|
|
92
94
|
lines.push("The reviewer accepted a subset of your proposed changes. Apply this");
|
|
93
95
|
lines.push("exact unified diff; it replaces what you submitted.");
|
|
@@ -98,6 +100,7 @@ export function renderFeedback(input: FeedbackInput): string {
|
|
|
98
100
|
} else {
|
|
99
101
|
lines.push("The reviewer rejected all of your proposed changes.");
|
|
100
102
|
}
|
|
103
|
+
|
|
101
104
|
lines.push("");
|
|
102
105
|
} else {
|
|
103
106
|
const diff =
|
|
@@ -132,6 +135,7 @@ export function renderFeedback(input: FeedbackInput): string {
|
|
|
132
135
|
input.actionBodies,
|
|
133
136
|
),
|
|
134
137
|
);
|
|
138
|
+
|
|
135
139
|
if (input.sessionId) {
|
|
136
140
|
lines.push("## Reporting what you addressed");
|
|
137
141
|
lines.push("");
|
|
@@ -178,6 +182,7 @@ function annotationSectionLines(
|
|
|
178
182
|
const target = annotationTarget(annotation);
|
|
179
183
|
|
|
180
184
|
if (target.kind !== "file") continue;
|
|
185
|
+
|
|
181
186
|
const group = fileGroups.get(target.path) ?? [];
|
|
182
187
|
|
|
183
188
|
group.push(annotation);
|
|
@@ -200,6 +205,7 @@ function annotationSectionLines(
|
|
|
200
205
|
lines.push(`- ${expandActionRefs(reply.body, actionBodies).replace(/\n/g, "\n ")}`);
|
|
201
206
|
lines.push("");
|
|
202
207
|
}
|
|
208
|
+
|
|
203
209
|
lines.push(`annotation id: \`${annotation.id}\``);
|
|
204
210
|
lines.push("");
|
|
205
211
|
};
|
|
@@ -227,6 +233,7 @@ function annotationSectionLines(
|
|
|
227
233
|
pushNote(annotation, index, location, orphan);
|
|
228
234
|
});
|
|
229
235
|
}
|
|
236
|
+
|
|
230
237
|
for (const [filePath, notes] of fileGroups) {
|
|
231
238
|
lines.push(`## ${filePath} (${notes.length})`);
|
|
232
239
|
lines.push("");
|
package/src/fuzzy.ts
CHANGED
|
@@ -20,7 +20,9 @@ export interface FuzzyMatch {
|
|
|
20
20
|
*/
|
|
21
21
|
export function levenshteinDistance(left: string, right: string): number {
|
|
22
22
|
if (left === right) return 0;
|
|
23
|
+
|
|
23
24
|
if (left.length === 0) return right.length;
|
|
25
|
+
|
|
24
26
|
if (right.length === 0) return left.length;
|
|
25
27
|
|
|
26
28
|
let previousRow: number[] = Array.from({ length: right.length + 1 }, (_unused, column) => column);
|
|
@@ -114,11 +116,14 @@ export function fuzzyFindBestMatch(
|
|
|
114
116
|
) {
|
|
115
117
|
// each window costs a needle x window edit-distance pass; spend that from the shared budget
|
|
116
118
|
budget.remaining -= needle.length * windowLength;
|
|
119
|
+
|
|
117
120
|
if (budget.remaining <= 0) return bestMatch;
|
|
121
|
+
|
|
118
122
|
const window = haystack.slice(start, start + windowLength);
|
|
119
123
|
const similarity = similarityRatio(needle, window);
|
|
120
124
|
|
|
121
125
|
if (similarity < minimumSimilarity) continue;
|
|
126
|
+
|
|
122
127
|
if (bestMatch === null || similarity > bestMatch.similarity) {
|
|
123
128
|
bestMatch = { start, end: start + windowLength, similarity };
|
|
124
129
|
}
|
package/src/herdr.ts
CHANGED
|
@@ -22,9 +22,11 @@ export type HerdrEnv = Record<string, string | undefined>;
|
|
|
22
22
|
*/
|
|
23
23
|
export function detectHerdr(env: HerdrEnv = process.env): HerdrContext | null {
|
|
24
24
|
if (env.HERDR_ENV !== "1") return null;
|
|
25
|
+
|
|
25
26
|
const paneId = env.HERDR_PANE_ID;
|
|
26
27
|
|
|
27
28
|
if (!paneId) return null;
|
|
29
|
+
|
|
28
30
|
// herdr 0.8.0 exposes no HERDR_BIN_PATH; the CLI is `herdr` on PATH and finds
|
|
29
31
|
// the app over HERDR_SOCKET_PATH from the inherited env. Tests still override
|
|
30
32
|
// HERDR_BIN_PATH to point the spawn at a stub script.
|
|
@@ -48,6 +50,7 @@ export function returnPaneFor(
|
|
|
48
50
|
env: HerdrEnv = process.env,
|
|
49
51
|
): string | undefined {
|
|
50
52
|
if (!insideHerdr(env)) return undefined;
|
|
53
|
+
|
|
51
54
|
const pane = env.CUELOOP_RETURN_PANE ?? sessionHerdrPane;
|
|
52
55
|
|
|
53
56
|
return pane && pane !== env.HERDR_PANE_ID ? pane : undefined;
|
package/src/history.ts
CHANGED
|
@@ -105,6 +105,7 @@ export function pathOf(history: SessionHistory, fromId: string = tipOf(history))
|
|
|
105
105
|
) {
|
|
106
106
|
if (seen.has(entry.id))
|
|
107
107
|
throw new HistoryError("cycle", `entry "${entry.id}" is its own ancestor`);
|
|
108
|
+
|
|
108
109
|
seen.add(entry.id);
|
|
109
110
|
path.unshift(entry);
|
|
110
111
|
}
|
|
@@ -123,6 +124,7 @@ export function validateHistory(history: SessionHistory): string | null {
|
|
|
123
124
|
|
|
124
125
|
for (const entry of history.entries) {
|
|
125
126
|
if (ids.has(entry.id)) return `duplicate entry id "${entry.id}"`;
|
|
127
|
+
|
|
126
128
|
ids.add(entry.id);
|
|
127
129
|
}
|
|
128
130
|
for (const entry of history.entries) {
|
|
@@ -130,11 +132,15 @@ export function validateHistory(history: SessionHistory): string | null {
|
|
|
130
132
|
return `entry "${entry.id}" points at missing parent "${entry.parentId}"`;
|
|
131
133
|
}
|
|
132
134
|
}
|
|
135
|
+
|
|
133
136
|
if (history.tips[MAIN_BRANCH] === undefined) return `no "${MAIN_BRANCH}" branch`;
|
|
137
|
+
|
|
134
138
|
if (history.tips[history.branch] === undefined)
|
|
135
139
|
return `current branch "${history.branch}" has no tip`;
|
|
140
|
+
|
|
136
141
|
for (const [branch, tip] of Object.entries(history.tips)) {
|
|
137
142
|
if (!ids.has(tip)) return `branch "${branch}" points at missing entry "${tip}"`;
|
|
143
|
+
|
|
138
144
|
try {
|
|
139
145
|
if (!pathOf(history, tip).some((entry) => entry.type === "revision")) {
|
|
140
146
|
return `branch "${branch}" has no revision on its path`;
|
|
@@ -170,14 +176,18 @@ export function derivePath(history: SessionHistory, fromId?: string): DerivedPat
|
|
|
170
176
|
const head = revisions.at(-1);
|
|
171
177
|
|
|
172
178
|
if (!head) throw new HistoryError("not-a-revision", "a path without a revision has no head");
|
|
179
|
+
|
|
173
180
|
const open = new Set<string>();
|
|
174
181
|
const messages: Message[] = [];
|
|
175
182
|
const summaries: Array<SessionEntry & { type: "branch-summary" }> = [];
|
|
176
183
|
|
|
177
184
|
for (const entry of path) {
|
|
178
185
|
if (entry.type === "comment") open.add(entry.annotationId);
|
|
186
|
+
|
|
179
187
|
if (entry.type === "comment-removed") open.delete(entry.annotationId);
|
|
188
|
+
|
|
180
189
|
if (entry.type === "message") messages.push(entry.message);
|
|
190
|
+
|
|
181
191
|
if (entry.type === "branch-summary") summaries.push(entry);
|
|
182
192
|
}
|
|
183
193
|
|
|
@@ -265,9 +275,11 @@ export function navigateTo(
|
|
|
265
275
|
if (!entryOf(history, targetId)) {
|
|
266
276
|
throw new HistoryError("unknown-entry", `no entry "${targetId}"`);
|
|
267
277
|
}
|
|
278
|
+
|
|
268
279
|
const tip = tipOf(history);
|
|
269
280
|
|
|
270
281
|
if (targetId === tip) throw new HistoryError("already-there", "already at that entry");
|
|
282
|
+
|
|
271
283
|
const path = pathOf(history);
|
|
272
284
|
const index = path.findIndex((entry) => entry.id === targetId);
|
|
273
285
|
|
|
@@ -277,6 +289,7 @@ export function navigateTo(
|
|
|
277
289
|
`"${targetId}" is not on branch "${history.branch}" - switch branch first`,
|
|
278
290
|
);
|
|
279
291
|
}
|
|
292
|
+
|
|
280
293
|
const abandoned = path.slice(index + 1).map((entry) => entry.id);
|
|
281
294
|
const moved: SessionHistory = {
|
|
282
295
|
...history,
|
|
@@ -306,6 +319,7 @@ export function forkHistory(history: SessionHistory): SessionHistory {
|
|
|
306
319
|
for (const entry of pathOf(history)) {
|
|
307
320
|
if (entry.type === "message" || (entry.type === "revision" && entry.by === "reviewer"))
|
|
308
321
|
continue;
|
|
322
|
+
|
|
309
323
|
kept.push({ ...entry, parentId: kept.at(-1)?.id ?? null });
|
|
310
324
|
const label = history.labels[entry.id];
|
|
311
325
|
|
|
@@ -397,6 +411,7 @@ export function historyFromLinear(
|
|
|
397
411
|
nextComment++;
|
|
398
412
|
}
|
|
399
413
|
});
|
|
414
|
+
|
|
400
415
|
if (session.message) {
|
|
401
416
|
push({
|
|
402
417
|
id: `${session.id}_message`,
|
|
@@ -405,6 +420,7 @@ export function historyFromLinear(
|
|
|
405
420
|
createdAt: session.message.sentAt,
|
|
406
421
|
});
|
|
407
422
|
}
|
|
423
|
+
|
|
408
424
|
const tip = entries.at(-1);
|
|
409
425
|
|
|
410
426
|
if (!tip) throw new HistoryError("not-a-revision", `session ${session.id} has no revision`);
|
package/src/inline.ts
CHANGED
|
@@ -143,11 +143,14 @@ function matchLink(
|
|
|
143
143
|
end: number;
|
|
144
144
|
} | null {
|
|
145
145
|
if (text[index] !== "[") return null;
|
|
146
|
+
|
|
146
147
|
const labelStart = index + 1;
|
|
147
148
|
const labelEnd = text.indexOf("]", labelStart);
|
|
148
149
|
|
|
149
150
|
if (labelEnd < 0 || labelEnd === labelStart) return null;
|
|
151
|
+
|
|
150
152
|
if (text[labelEnd + 1] !== "(") return null;
|
|
153
|
+
|
|
151
154
|
const hrefStart = labelEnd + 2;
|
|
152
155
|
// the destination may carry balanced parentheses (wiki URLs); the span ends
|
|
153
156
|
// at the first unmatched closer
|
|
@@ -162,6 +165,7 @@ function matchLink(
|
|
|
162
165
|
depth--;
|
|
163
166
|
continue;
|
|
164
167
|
}
|
|
168
|
+
|
|
165
169
|
if (scan === hrefStart) return null;
|
|
166
170
|
|
|
167
171
|
return { labelStart, labelEnd, hrefStart, hrefEnd: scan, end: scan + 1 };
|
|
@@ -177,6 +181,7 @@ function matchCode(
|
|
|
177
181
|
index: number,
|
|
178
182
|
): { contentStart: number; contentEnd: number; end: number } | null {
|
|
179
183
|
if (text[index] !== "`") return null;
|
|
184
|
+
|
|
180
185
|
let fence = 0;
|
|
181
186
|
|
|
182
187
|
while (text[index + fence] === "`") fence++;
|
|
@@ -194,7 +199,9 @@ function matchEmphasis(text: string, index: number): Span | null {
|
|
|
194
199
|
const two = text.slice(index, index + 2);
|
|
195
200
|
|
|
196
201
|
if (two === "**") return closingSpan(text, index, 2, "strong");
|
|
202
|
+
|
|
197
203
|
if (two === "~~") return closingSpan(text, index, 2, "strike");
|
|
204
|
+
|
|
198
205
|
if (text[index] === "*") return closingSpan(text, index, 1, "em");
|
|
199
206
|
|
|
200
207
|
return null;
|
|
@@ -224,9 +231,11 @@ function findLoneStar(text: string, from: number): number {
|
|
|
224
231
|
while (index < text.length) {
|
|
225
232
|
if (text[index] === "*") {
|
|
226
233
|
if (text[index + 1] !== "*") return index;
|
|
234
|
+
|
|
227
235
|
while (text[index] === "*") index++; // skip the whole `**` run
|
|
228
236
|
continue;
|
|
229
237
|
}
|
|
238
|
+
|
|
230
239
|
index++;
|
|
231
240
|
}
|
|
232
241
|
|
package/src/markdown.ts
CHANGED
|
@@ -47,6 +47,7 @@ export function stripLeadingBlockMarker(line: string): string {
|
|
|
47
47
|
/** A GFM table delimiter row: one or more dash cells split by pipes, each optionally `:`-aligned. */
|
|
48
48
|
function isTableDelimiterRow(line: string): boolean {
|
|
49
49
|
if (!line.includes("|")) return false;
|
|
50
|
+
|
|
50
51
|
const cells = line
|
|
51
52
|
.trim()
|
|
52
53
|
.replace(/^\||\|$/g, "")
|
|
@@ -81,9 +82,11 @@ function isMarkerLine(line: string): boolean {
|
|
|
81
82
|
*/
|
|
82
83
|
function frontmatterBlock(lines: string[]): Block | null {
|
|
83
84
|
if (lines[0]?.trim() !== "---") return null;
|
|
85
|
+
|
|
84
86
|
let close = 1;
|
|
85
87
|
|
|
86
88
|
while (close < lines.length && lines[close]!.trim() !== "---") close++;
|
|
89
|
+
|
|
87
90
|
if (close >= lines.length) return null;
|
|
88
91
|
|
|
89
92
|
return {
|
|
@@ -110,12 +113,17 @@ function singleLineBlock(line: string, lineIndex: number): Block | null {
|
|
|
110
113
|
});
|
|
111
114
|
|
|
112
115
|
if (line.startsWith("### ")) return marked("h3");
|
|
116
|
+
|
|
113
117
|
if (line.startsWith("## ")) return marked("h2");
|
|
118
|
+
|
|
114
119
|
if (line.startsWith("# ")) return marked("h1");
|
|
120
|
+
|
|
115
121
|
if (/^(---|\*\*\*|___)\s*$/.test(line)) {
|
|
116
122
|
return { kind: "hr", text: "", lineStart: lineIndex, lineEnd: lineIndex };
|
|
117
123
|
}
|
|
124
|
+
|
|
118
125
|
if (line.startsWith("- ")) return marked("li");
|
|
126
|
+
|
|
119
127
|
if (/^\d+\. /.test(line)) return marked("oli");
|
|
120
128
|
|
|
121
129
|
return null;
|
|
@@ -203,10 +211,13 @@ function nextBlock(lines: string[], lineIndex: number): BlockScan {
|
|
|
203
211
|
const line = lines[lineIndex]!;
|
|
204
212
|
|
|
205
213
|
if (line.startsWith("```")) return codeBlock(lines, lineIndex);
|
|
214
|
+
|
|
206
215
|
const single = singleLineBlock(line, lineIndex);
|
|
207
216
|
|
|
208
217
|
if (single) return { block: single, next: lineIndex + 1 };
|
|
218
|
+
|
|
209
219
|
if (line.startsWith("> ")) return quoteBlock(lines, lineIndex);
|
|
220
|
+
|
|
210
221
|
if (isTableStart(lines, lineIndex)) return tableBlock(lines, lineIndex);
|
|
211
222
|
|
|
212
223
|
return paragraphBlock(lines, lineIndex);
|
|
@@ -243,11 +254,13 @@ function blockSourceSegments(
|
|
|
243
254
|
} else {
|
|
244
255
|
contentLines = rawLines.map((_, rawLine) => ({ rawLine, prefix: 0 }));
|
|
245
256
|
}
|
|
257
|
+
|
|
246
258
|
const content = contentLines
|
|
247
259
|
.map(({ rawLine, prefix }) => rawLines[rawLine]!.slice(prefix))
|
|
248
260
|
.join("\n");
|
|
249
261
|
|
|
250
262
|
if (content !== block.text) return undefined;
|
|
263
|
+
|
|
251
264
|
const rawLineStarts: number[] = [];
|
|
252
265
|
let rawOffset = 0;
|
|
253
266
|
|
|
@@ -317,11 +330,13 @@ export function parseBlocks(markdown: string): Block[] {
|
|
|
317
330
|
blocks.push(frontmatter);
|
|
318
331
|
lineIndex = frontmatter.lineEnd + 1;
|
|
319
332
|
}
|
|
333
|
+
|
|
320
334
|
while (lineIndex < lines.length) {
|
|
321
335
|
if (lines[lineIndex]!.trim() === "") {
|
|
322
336
|
lineIndex++;
|
|
323
337
|
continue;
|
|
324
338
|
}
|
|
339
|
+
|
|
325
340
|
const scan = nextBlock(lines, lineIndex);
|
|
326
341
|
|
|
327
342
|
blocks.push(scan.block);
|
package/src/participants.ts
CHANGED
|
@@ -35,11 +35,13 @@ export function registerParticipant(
|
|
|
35
35
|
const trimmed = name?.trim();
|
|
36
36
|
|
|
37
37
|
if (existing && !trimmed && !source) return session;
|
|
38
|
+
|
|
38
39
|
const resolvedName = trimmed ?? existing?.name;
|
|
39
40
|
const resolvedHandle = source?.handle ?? existing?.handle;
|
|
40
41
|
const next: Identity = { id: author, provider: source?.provider ?? existing?.provider ?? "ssh" };
|
|
41
42
|
|
|
42
43
|
if (resolvedName) next.name = resolvedName;
|
|
44
|
+
|
|
43
45
|
if (resolvedHandle) next.handle = resolvedHandle;
|
|
44
46
|
|
|
45
47
|
return {
|
package/src/path-view.ts
CHANGED
|
@@ -52,10 +52,13 @@ export function viewOfPath(history: SessionHistory, known: Annotation[]): PathVi
|
|
|
52
52
|
export function applyPathView(session: Thread, view: PathView): void {
|
|
53
53
|
session.artifact = { ...session.artifact, content: view.content };
|
|
54
54
|
session.annotations = view.annotations;
|
|
55
|
+
|
|
55
56
|
if (view.textCuts?.length) session.textCuts = view.textCuts;
|
|
56
57
|
else delete session.textCuts;
|
|
58
|
+
|
|
57
59
|
if (view.workingCopy === undefined) delete session.workingCopy;
|
|
58
60
|
else session.workingCopy = view.workingCopy;
|
|
61
|
+
|
|
59
62
|
if (view.shelvedAnnotations.length === 0) delete session.shelvedAnnotations;
|
|
60
63
|
else session.shelvedAnnotations = view.shelvedAnnotations;
|
|
61
64
|
}
|
|
@@ -74,6 +77,7 @@ export function viewFollowing(session: Thread, branch?: string): Thread {
|
|
|
74
77
|
const shared: Thread = { ...session };
|
|
75
78
|
|
|
76
79
|
if (!session.history) return shared;
|
|
80
|
+
|
|
77
81
|
const history = followBranch(session.history, followed);
|
|
78
82
|
const known = [...session.annotations, ...(session.shelvedAnnotations ?? [])];
|
|
79
83
|
|
package/src/share-access.ts
CHANGED
|
@@ -7,7 +7,9 @@ export function isShareViewerAllowed(
|
|
|
7
7
|
githubLogin: string | undefined,
|
|
8
8
|
): boolean {
|
|
9
9
|
if (!policy.requireAuth) return true;
|
|
10
|
+
|
|
10
11
|
if (githubLogin === undefined) return false;
|
|
12
|
+
|
|
11
13
|
const login = githubLogin.toLowerCase();
|
|
12
14
|
|
|
13
15
|
return policy.allowlist.some((allowed) => allowed.toLowerCase() === login);
|
package/src/thread-agent.ts
CHANGED
|
@@ -60,6 +60,14 @@ export interface ThreadAgentState {
|
|
|
60
60
|
outcome?: "completed" | "failed" | "cancelled";
|
|
61
61
|
})[];
|
|
62
62
|
configOptions?: AgentConfigOption[];
|
|
63
|
+
handoff?: {
|
|
64
|
+
target: string;
|
|
65
|
+
requestedTarget?: string;
|
|
66
|
+
operationId: string;
|
|
67
|
+
prompt?: string;
|
|
68
|
+
source?: { id: string; label: string; sessionId?: string };
|
|
69
|
+
} & ({ status: "queued" } | { status: "summarizing" } | { status: "prepared"; text: string });
|
|
70
|
+
continuation?: string;
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
/** A submission freezes its input; retry keeps the same mirror and discussion origin. */
|
|
@@ -69,6 +77,7 @@ export interface AgentSubmission {
|
|
|
69
77
|
messageId?: string;
|
|
70
78
|
prompt: string;
|
|
71
79
|
harnessPrompt?: string;
|
|
80
|
+
attemptId?: string;
|
|
72
81
|
quote?: string;
|
|
73
82
|
context?: string;
|
|
74
83
|
status: "queued" | "running" | "completed" | "failed";
|
package/src/types.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* dependencies beyond the history shapes.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type { ThreadAgentState } from "./thread-agent";
|
|
7
8
|
import type { SessionHistory } from "./history";
|
|
8
9
|
|
|
9
10
|
export const SCHEMA_VERSION = "1";
|
|
@@ -320,6 +321,7 @@ export interface Thread {
|
|
|
320
321
|
parentSessionId?: string;
|
|
321
322
|
shares?: ShareLink[];
|
|
322
323
|
participants?: Identity[];
|
|
324
|
+
agentSnapshot?: ThreadAgentState;
|
|
323
325
|
}
|
|
324
326
|
|
|
325
327
|
/** Whether a Message releases a harness gate. */
|
package/src/working-copy.ts
CHANGED
|
@@ -17,6 +17,7 @@ export function mergeTextCut(
|
|
|
17
17
|
// SAFETY: a no-op returns the caller's array unchanged; callers never mutate the result.
|
|
18
18
|
return cuts as TextCut[];
|
|
19
19
|
}
|
|
20
|
+
|
|
20
21
|
const ranges = [
|
|
21
22
|
...cuts.map((cut) => ({ start: cut.start, end: cut.end })),
|
|
22
23
|
{ start: boundedStart, end: boundedEnd },
|
|
@@ -52,12 +53,14 @@ export function restoreTextCut(
|
|
|
52
53
|
const cutIndex = cuts.findIndex((cut) => cut.start <= start && end <= cut.end);
|
|
53
54
|
|
|
54
55
|
if (cutIndex === -1 || end <= start) return null;
|
|
56
|
+
|
|
55
57
|
const cut = cuts[cutIndex]!;
|
|
56
58
|
const replacements: TextCut[] = [];
|
|
57
59
|
|
|
58
60
|
if (cut.start < start) {
|
|
59
61
|
replacements.push({ start: cut.start, end: start, quote: source.slice(cut.start, start) });
|
|
60
62
|
}
|
|
63
|
+
|
|
61
64
|
if (end < cut.end) {
|
|
62
65
|
replacements.push({ start: end, end: cut.end, quote: source.slice(end, cut.end) });
|
|
63
66
|
}
|
|
@@ -68,6 +71,7 @@ export function restoreTextCut(
|
|
|
68
71
|
/** Apply valid source ranges in source order, ignoring stale or overlapping records. */
|
|
69
72
|
export function applyTextCuts(source: string, cuts: readonly TextCut[]): string {
|
|
70
73
|
if (cuts.length === 0) return source;
|
|
74
|
+
|
|
71
75
|
let cursor = 0;
|
|
72
76
|
const pieces: string[] = [];
|
|
73
77
|
|
|
@@ -76,6 +80,7 @@ export function applyTextCuts(source: string, cuts: readonly TextCut[]): string
|
|
|
76
80
|
)) {
|
|
77
81
|
if (cut.start < cursor || cut.start < 0 || cut.end > source.length || cut.end <= cut.start)
|
|
78
82
|
continue;
|
|
83
|
+
|
|
79
84
|
if (source.slice(cut.start, cut.end) !== cut.quote) continue;
|
|
80
85
|
|
|
81
86
|
pieces.push(source.slice(cursor, cut.start));
|
|
@@ -190,7 +195,9 @@ export function restoreBlock(
|
|
|
190
195
|
const insertedLines = sourceChunk(base, block).split("\n");
|
|
191
196
|
|
|
192
197
|
if (before.length && before[before.length - 1]!.trim() !== "") insertedLines.unshift("");
|
|
198
|
+
|
|
193
199
|
if (after.length && after[0]!.trim() !== "") insertedLines.push("");
|
|
200
|
+
|
|
194
201
|
const restored = [...before, ...insertedLines, ...after].join("\n");
|
|
195
202
|
|
|
196
203
|
return signature(restored) === signature(base) ? undefined : restored;
|