agent-dealer 1.2.6 → 1.2.7
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/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/index-DyAJNyfV.css +1 -0
- package/bundle/server/static-ui/assets/index-yLyxRd-7.js +64 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/action-presentation.d.ts +51 -0
- package/bundle/shared/dist/action-presentation.js +280 -0
- package/bundle/shared/dist/action-presentation.test.d.ts +1 -0
- package/bundle/shared/dist/action-presentation.test.js +180 -0
- package/bundle/shared/dist/attempt-waste.d.ts +2 -2
- package/bundle/shared/dist/execution-report.d.ts +2 -2
- package/bundle/shared/dist/human-actions.d.ts +12 -12
- package/bundle/shared/dist/index.d.ts +41 -40
- package/bundle/shared/dist/index.js +1 -0
- package/bundle/shared/dist/issues.d.ts +12 -12
- package/bundle/shared/dist/outbound-draft.d.ts +6 -6
- package/bundle/shared/dist/profile-snapshot.d.ts +2 -2
- package/bundle/shared/dist/usage-events.d.ts +2 -2
- package/bundle/shared/dist/worker-sessions.d.ts +6 -6
- package/bundle/shared/dist/workflow.d.ts +2 -2
- package/bundle/shared/package.json +1 -1
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-BII-LgB8.css +0 -1
- package/bundle/server/static-ui/assets/index-CoDzZcsz.js +0 -60
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
9
9
|
<meta name="theme-color" content="#0a0e14" />
|
|
10
10
|
<link rel="preload" href="/fonts/Monaco.ttf" as="font" type="font/ttf" crossorigin />
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-yLyxRd-7.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DyAJNyfV.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface ActionPresentationInput {
|
|
2
|
+
actionType: string;
|
|
3
|
+
question: string;
|
|
4
|
+
reason: string;
|
|
5
|
+
evidenceJson: string | null;
|
|
6
|
+
responseOptionsJson: string | null;
|
|
7
|
+
continuationPreviewJson: string | null;
|
|
8
|
+
}
|
|
9
|
+
export type ActionSummaryKind = "diverged-push" | "unpushed" | "worktree-conflict" | "dirty-worktree" | "merge-failure" | "reviewer-retry" | "ordinary-escalation" | "other";
|
|
10
|
+
export interface ActionResponseOption {
|
|
11
|
+
choice: string;
|
|
12
|
+
label: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ActionSummaryDetails {
|
|
15
|
+
/** Full stored question, verbatim — never truncated. */
|
|
16
|
+
question: string;
|
|
17
|
+
/** Full stored reason, verbatim — never truncated. */
|
|
18
|
+
reason: string;
|
|
19
|
+
/** Exact recovery commands parsed out of the reason. */
|
|
20
|
+
commands: string[];
|
|
21
|
+
/** Worktree / log paths parsed out of the reason, question, and evidence. */
|
|
22
|
+
paths: string[];
|
|
23
|
+
/** Structured recovery facts (push pins, continuation, review verdict). */
|
|
24
|
+
factLines: string[];
|
|
25
|
+
/** Full structured evidence, pretty-printed — null when the action has none. */
|
|
26
|
+
evidencePretty: string | null;
|
|
27
|
+
}
|
|
28
|
+
export interface ActionSummary {
|
|
29
|
+
kind: ActionSummaryKind;
|
|
30
|
+
/** Short human question for the default (collapsed) view. */
|
|
31
|
+
title: string;
|
|
32
|
+
/** One-sentence blocker / next-step context, or null when there is none. */
|
|
33
|
+
context: string | null;
|
|
34
|
+
/**
|
|
35
|
+
* True only where coordinator semantics prove Resume continues without
|
|
36
|
+
* discarding preserved work or re-colliding with the same blocker: a
|
|
37
|
+
* reviewer-origin retry (fresh reviewer at a pinned head, no worktree
|
|
38
|
+
* touched) or an ordinary escalation with no preserved-work markers.
|
|
39
|
+
* Worktree conflicts, dirty worktrees, and unpushed/diverged pushes keep
|
|
40
|
+
* their preserved work — never safe to promise.
|
|
41
|
+
*/
|
|
42
|
+
safeResume: boolean;
|
|
43
|
+
/** Server-declared choices, with `resume` shown as "Resume safely" only when safeResume. */
|
|
44
|
+
displayOptions: ActionResponseOption[];
|
|
45
|
+
/** The safest server-declared next action: push_with_lease for a diverged push,
|
|
46
|
+
* otherwise the first non-destructive choice. Null when no options exist. */
|
|
47
|
+
primaryChoice: string | null;
|
|
48
|
+
details: ActionSummaryDetails;
|
|
49
|
+
}
|
|
50
|
+
export declare function parseActionResponseOptions(action: ActionPresentationInput): ActionResponseOption[];
|
|
51
|
+
export declare function summarizeHumanAction(action: ActionPresentationInput): ActionSummary;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// NOT-288: one shared presentation read of a human action for every friend-path
|
|
2
|
+
// surface (Issues home NeedsAttentionPanel, Issue Detail). The stored
|
|
3
|
+
// `question`/`reason` still carry the full folded recovery text (paths, git
|
|
4
|
+
// commands, SHAs) so the timeline and evidence stay auditable and pre-change
|
|
5
|
+
// open actions keep working — this layer only derives a short question, a
|
|
6
|
+
// one-sentence next step, and the expandable Details from those stored fields.
|
|
7
|
+
function parseJsonSafe(json) {
|
|
8
|
+
if (!json)
|
|
9
|
+
return null;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(json);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function parseActionResponseOptions(action) {
|
|
18
|
+
const parsed = parseJsonSafe(action.responseOptionsJson);
|
|
19
|
+
if (!Array.isArray(parsed))
|
|
20
|
+
return [];
|
|
21
|
+
return parsed.filter((o) => typeof o?.choice === "string" && typeof o?.label === "string");
|
|
22
|
+
}
|
|
23
|
+
function parseEvidence(action) {
|
|
24
|
+
return parseJsonSafe(action.evidenceJson) ?? {};
|
|
25
|
+
}
|
|
26
|
+
function pushFacts(action) {
|
|
27
|
+
const evidence = parseEvidence(action);
|
|
28
|
+
const facts = evidence["pushDivergence"];
|
|
29
|
+
if (!facts || typeof facts !== "object")
|
|
30
|
+
return null;
|
|
31
|
+
return facts;
|
|
32
|
+
}
|
|
33
|
+
function hasWorktreeBlockerEvidence(action) {
|
|
34
|
+
const evidence = parseEvidence(action);
|
|
35
|
+
const blocker = evidence["worktreeBlocker"];
|
|
36
|
+
return (!!blocker &&
|
|
37
|
+
typeof blocker === "object" &&
|
|
38
|
+
typeof blocker.fingerprint === "string");
|
|
39
|
+
}
|
|
40
|
+
function isMergeFailureEvidence(action) {
|
|
41
|
+
return parseEvidence(action)["mergeFailure"] === true;
|
|
42
|
+
}
|
|
43
|
+
function resumeContinuationRole(action) {
|
|
44
|
+
const parsed = parseJsonSafe(action.continuationPreviewJson);
|
|
45
|
+
return typeof parsed?.resumeRole === "string" ? parsed.resumeRole : null;
|
|
46
|
+
}
|
|
47
|
+
/** Signals that local work is preserved and Resume cannot be promised safe. */
|
|
48
|
+
const PRESERVED_WORK_PATTERN = /worktree|unpushed|uncommitted|dirty|preserved|non-fast-forward|diverged|already used by|recovery:|force-with-lease/i;
|
|
49
|
+
const UNPUSHED_TEXT_PATTERN = /could not be pushed|non-fast-forward|rejected.*push|push.*rejected/i;
|
|
50
|
+
const DIRTY_TEXT_PATTERN = /uncommitted|dirty worktree|has uncommitted changes/i;
|
|
51
|
+
/** Splits the folded "Recovery:\n<commands>" tail the coordinator appends to reasons. */
|
|
52
|
+
function splitRecovery(reason) {
|
|
53
|
+
const marker = /\n?Recovery:\s*\n?/;
|
|
54
|
+
const match = reason.match(marker);
|
|
55
|
+
if (!match || match.index === undefined)
|
|
56
|
+
return { head: reason.trim(), commands: [] };
|
|
57
|
+
const head = reason.slice(0, match.index).trim();
|
|
58
|
+
const tail = reason.slice(match.index + match[0].length);
|
|
59
|
+
const commands = tail
|
|
60
|
+
.split("\n")
|
|
61
|
+
.map((line) => line.trim())
|
|
62
|
+
.filter((line) => line.length > 0);
|
|
63
|
+
return { head, commands };
|
|
64
|
+
}
|
|
65
|
+
/** First sentence of a blocker statement — the one-sentence context source. */
|
|
66
|
+
function firstSentence(text) {
|
|
67
|
+
const head = splitRecovery(text).head;
|
|
68
|
+
const singleLine = head.replace(/\s+/g, " ").trim();
|
|
69
|
+
const match = singleLine.match(/^(.+?[.!?])(\s|$)/);
|
|
70
|
+
return (match ? match[1] : singleLine).trim();
|
|
71
|
+
}
|
|
72
|
+
const PATH_PATTERN = /(?:~|\/(?:[^/\s\\:"'*?<>|][^/\s\\:"'*?<>|]*))+(?:\/[^/\s\\:"'*?<>|]+)*/g;
|
|
73
|
+
/** Absolute worktree/log paths from free text plus the structured evidence path. */
|
|
74
|
+
function extractPaths(action) {
|
|
75
|
+
const found = [];
|
|
76
|
+
const seen = new Set();
|
|
77
|
+
const push = (p) => {
|
|
78
|
+
const trimmed = p.trim().replace(/[.,;:!?)"'\]]+$/, "");
|
|
79
|
+
if (trimmed.length < 2 || seen.has(trimmed))
|
|
80
|
+
return;
|
|
81
|
+
seen.add(trimmed);
|
|
82
|
+
found.push(trimmed);
|
|
83
|
+
};
|
|
84
|
+
for (const text of [action.reason, action.question]) {
|
|
85
|
+
for (const match of text.matchAll(PATH_PATTERN)) {
|
|
86
|
+
const candidate = match[0];
|
|
87
|
+
// Guard against version numbers and SHAs matching the loose pattern.
|
|
88
|
+
if (/^\/\d+(\.\d+)*$/.test(candidate))
|
|
89
|
+
continue;
|
|
90
|
+
if (!candidate.includes("/") && !candidate.startsWith("~"))
|
|
91
|
+
continue;
|
|
92
|
+
push(candidate);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const facts = pushFacts(action);
|
|
96
|
+
if (typeof facts?.worktreePath === "string" && facts.worktreePath)
|
|
97
|
+
push(facts.worktreePath);
|
|
98
|
+
return found;
|
|
99
|
+
}
|
|
100
|
+
function shortSha(sha) {
|
|
101
|
+
return sha.length > 12 ? sha.slice(0, 12) : sha;
|
|
102
|
+
}
|
|
103
|
+
/** Recovery facts both panels show the same way: pins, tips, continuation. */
|
|
104
|
+
function factLines(action) {
|
|
105
|
+
const lines = [];
|
|
106
|
+
const facts = pushFacts(action);
|
|
107
|
+
if (facts && typeof facts.localSha === "string" && typeof facts.remoteSha === "string") {
|
|
108
|
+
const branch = typeof facts.branch === "string" ? facts.branch : "the branch";
|
|
109
|
+
const ahead = typeof facts.ahead === "number" ? ` (+${facts.ahead})` : "";
|
|
110
|
+
const behind = typeof facts.behind === "number" ? ` (+${facts.behind})` : "";
|
|
111
|
+
lines.push(`Push: local ${facts.localSha}${ahead} · origin/${branch} ${facts.remoteSha}${behind}`);
|
|
112
|
+
if (typeof facts.observedRemoteSha === "string" && facts.observedRemoteSha !== facts.remoteSha) {
|
|
113
|
+
lines.push(`Remote moved: origin/${branch} is now at ${facts.observedRemoteSha}`);
|
|
114
|
+
}
|
|
115
|
+
if (typeof facts.lastLeaseError === "string" && facts.lastLeaseError) {
|
|
116
|
+
lines.push(`Last lease attempt: ${facts.lastLeaseError}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const role = resumeContinuationRole(action);
|
|
120
|
+
if (role)
|
|
121
|
+
lines.push(`Continuation: resumes as ${role}`);
|
|
122
|
+
return lines;
|
|
123
|
+
}
|
|
124
|
+
function isDestructiveChoice(choice) {
|
|
125
|
+
return choice === "close" || choice === "reject" || choice === "abort";
|
|
126
|
+
}
|
|
127
|
+
function primaryChoiceFor(options) {
|
|
128
|
+
if (options.length === 0)
|
|
129
|
+
return null;
|
|
130
|
+
if (options.some((o) => o.choice === "push_with_lease"))
|
|
131
|
+
return "push_with_lease";
|
|
132
|
+
return (options.find((o) => !isDestructiveChoice(o.choice)) ?? options[0]).choice;
|
|
133
|
+
}
|
|
134
|
+
function classify(action) {
|
|
135
|
+
if (action.actionType !== "policy_escalation")
|
|
136
|
+
return "other";
|
|
137
|
+
if (isMergeFailureEvidence(action))
|
|
138
|
+
return "merge-failure";
|
|
139
|
+
const facts = pushFacts(action);
|
|
140
|
+
if (facts?.relationship === "diverged")
|
|
141
|
+
return "diverged-push";
|
|
142
|
+
if (facts && (facts.relationship === "behind" || typeof facts.localSha === "string"))
|
|
143
|
+
return "unpushed";
|
|
144
|
+
const text = `${action.reason}\n${action.question}`;
|
|
145
|
+
if (UNPUSHED_TEXT_PATTERN.test(text))
|
|
146
|
+
return "unpushed";
|
|
147
|
+
// A reviewer-origin retry re-queues a fresh reviewer at a pinned head without
|
|
148
|
+
// touching a developer worktree — classify before the generic worktree text
|
|
149
|
+
// check so a "reviewer worktree checkout failed" reason still reads as safe.
|
|
150
|
+
if (resumeContinuationRole(action) === "reviewer")
|
|
151
|
+
return "reviewer-retry";
|
|
152
|
+
// Dirty before worktree: a preserved-dirt escalation names its worktree path in
|
|
153
|
+
// the recovery commands but stays a dirty worktree, not a branch collision.
|
|
154
|
+
if (DIRTY_TEXT_PATTERN.test(text))
|
|
155
|
+
return "dirty-worktree";
|
|
156
|
+
if (hasWorktreeBlockerEvidence(action) ||
|
|
157
|
+
/already used by worktree/i.test(text) ||
|
|
158
|
+
(/worktree/i.test(text) && /Recovery:/.test(text))) {
|
|
159
|
+
return "worktree-conflict";
|
|
160
|
+
}
|
|
161
|
+
return "ordinary-escalation";
|
|
162
|
+
}
|
|
163
|
+
const SAFE_RESUME_LABEL = "Resume safely";
|
|
164
|
+
export function summarizeHumanAction(action) {
|
|
165
|
+
const kind = classify(action);
|
|
166
|
+
const options = parseActionResponseOptions(action);
|
|
167
|
+
const { commands } = splitRecovery(action.reason);
|
|
168
|
+
const paths = extractPaths(action);
|
|
169
|
+
const facts = factLines(action);
|
|
170
|
+
const evidencePretty = (() => {
|
|
171
|
+
const evidence = parseJsonSafe(action.evidenceJson);
|
|
172
|
+
if (evidence === null || evidence === undefined)
|
|
173
|
+
return null;
|
|
174
|
+
return JSON.stringify(evidence, null, 2);
|
|
175
|
+
})();
|
|
176
|
+
const details = {
|
|
177
|
+
question: action.question,
|
|
178
|
+
reason: action.reason,
|
|
179
|
+
commands,
|
|
180
|
+
paths,
|
|
181
|
+
factLines: facts,
|
|
182
|
+
evidencePretty,
|
|
183
|
+
};
|
|
184
|
+
switch (kind) {
|
|
185
|
+
case "diverged-push": {
|
|
186
|
+
const f = pushFacts(action);
|
|
187
|
+
const pin = f && typeof f.branch === "string" && typeof f.localSha === "string" && typeof f.remoteSha === "string"
|
|
188
|
+
? ` (origin/${f.branch}: local ${shortSha(f.localSha)} against remote ${shortSha(f.remoteSha)})`
|
|
189
|
+
: "";
|
|
190
|
+
return {
|
|
191
|
+
kind,
|
|
192
|
+
title: "Publish these finished commits with a pinned lease?",
|
|
193
|
+
context: `Your branch diverged from the remote — confirm the pinned commits${pin} under Details, then choose Push with lease.`,
|
|
194
|
+
safeResume: false,
|
|
195
|
+
displayOptions: options,
|
|
196
|
+
primaryChoice: primaryChoiceFor(options),
|
|
197
|
+
details,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
case "unpushed":
|
|
201
|
+
return {
|
|
202
|
+
kind,
|
|
203
|
+
title: "The push was rejected — how should we proceed?",
|
|
204
|
+
context: "Your commits are saved; resuming continues development without discarding them.",
|
|
205
|
+
safeResume: false,
|
|
206
|
+
displayOptions: options,
|
|
207
|
+
primaryChoice: primaryChoiceFor(options),
|
|
208
|
+
details,
|
|
209
|
+
};
|
|
210
|
+
case "worktree-conflict":
|
|
211
|
+
return {
|
|
212
|
+
kind,
|
|
213
|
+
title: "A saved worktree still holds this branch — how should we proceed?",
|
|
214
|
+
context: "Your work is preserved; check the worktree path and recovery steps under Details before choosing.",
|
|
215
|
+
safeResume: false,
|
|
216
|
+
displayOptions: options,
|
|
217
|
+
primaryChoice: primaryChoiceFor(options),
|
|
218
|
+
details,
|
|
219
|
+
};
|
|
220
|
+
case "dirty-worktree":
|
|
221
|
+
return {
|
|
222
|
+
kind,
|
|
223
|
+
title: "Uncommitted changes were preserved — how should we proceed?",
|
|
224
|
+
context: "Nothing was discarded; review the saved changes under Details before choosing.",
|
|
225
|
+
safeResume: false,
|
|
226
|
+
displayOptions: options,
|
|
227
|
+
primaryChoice: primaryChoiceFor(options),
|
|
228
|
+
details,
|
|
229
|
+
};
|
|
230
|
+
case "merge-failure":
|
|
231
|
+
return {
|
|
232
|
+
kind,
|
|
233
|
+
title: "The merge failed after approval — retry it?",
|
|
234
|
+
context: "The work is already approved; retrying the merge publishes the same commits without a new developer round.",
|
|
235
|
+
safeResume: false,
|
|
236
|
+
displayOptions: options,
|
|
237
|
+
primaryChoice: primaryChoiceFor(options),
|
|
238
|
+
details,
|
|
239
|
+
};
|
|
240
|
+
case "reviewer-retry":
|
|
241
|
+
return {
|
|
242
|
+
kind,
|
|
243
|
+
title: "The review attempt failed — retry it?",
|
|
244
|
+
context: "Nothing is wrong with the code itself; retrying re-queues the reviewer at the same pinned commit.",
|
|
245
|
+
safeResume: true,
|
|
246
|
+
displayOptions: options,
|
|
247
|
+
primaryChoice: primaryChoiceFor(options),
|
|
248
|
+
details,
|
|
249
|
+
};
|
|
250
|
+
case "ordinary-escalation": {
|
|
251
|
+
const blocker = firstSentence(action.reason);
|
|
252
|
+
const safe = !PRESERVED_WORK_PATTERN.test(`${action.reason}\n${action.question}`);
|
|
253
|
+
const displayOptions = safe
|
|
254
|
+
? options.map((o) => o.choice === "resume" && o.label === "Resume development"
|
|
255
|
+
? { ...o, label: SAFE_RESUME_LABEL }
|
|
256
|
+
: o)
|
|
257
|
+
: options;
|
|
258
|
+
return {
|
|
259
|
+
kind,
|
|
260
|
+
title: "Development is parked — resume or close?",
|
|
261
|
+
context: blocker ? `${blocker} Choose ${safe ? "Resume safely" : "Resume"} to continue, or Close to stop.` : null,
|
|
262
|
+
safeResume: safe,
|
|
263
|
+
displayOptions,
|
|
264
|
+
primaryChoice: primaryChoiceFor(displayOptions),
|
|
265
|
+
details,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
case "other":
|
|
269
|
+
default:
|
|
270
|
+
return {
|
|
271
|
+
kind: "other",
|
|
272
|
+
title: action.question,
|
|
273
|
+
context: null,
|
|
274
|
+
safeResume: false,
|
|
275
|
+
displayOptions: options,
|
|
276
|
+
primaryChoice: primaryChoiceFor(options),
|
|
277
|
+
details,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// NOT-288: the shared presentation layer keeps worktree/policy escalations
|
|
2
|
+
// concise by default while preserving every technical fact under Details.
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { summarizeHumanAction, } from "./action-presentation.js";
|
|
6
|
+
const LOCAL_SHA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
7
|
+
const REMOTE_SHA = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
|
8
|
+
const WORKTREE_PATH = "/Users/friend/.agent-dealer-dev/worktrees/310f25af-developer";
|
|
9
|
+
function baseAction(extra = {}) {
|
|
10
|
+
return {
|
|
11
|
+
actionType: "policy_escalation",
|
|
12
|
+
question: "reason Resume development, or close the issue?",
|
|
13
|
+
reason: "reason",
|
|
14
|
+
evidenceJson: null,
|
|
15
|
+
responseOptionsJson: JSON.stringify([
|
|
16
|
+
{ choice: "resume", label: "Resume development" },
|
|
17
|
+
{ choice: "close", label: "Close" },
|
|
18
|
+
]),
|
|
19
|
+
continuationPreviewJson: null,
|
|
20
|
+
...extra,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function worktreeConflictAction() {
|
|
24
|
+
const reason = `issue branch is already used by worktree at ${WORKTREE_PATH}. ` +
|
|
25
|
+
`Recovery:\n` +
|
|
26
|
+
`git -C ${WORKTREE_PATH} status\n` +
|
|
27
|
+
`git worktree remove --force ${WORKTREE_PATH}`;
|
|
28
|
+
return baseAction({
|
|
29
|
+
reason,
|
|
30
|
+
question: `${reason} Resume development, or close the issue?`,
|
|
31
|
+
evidenceJson: JSON.stringify({ worktreeBlocker: { fingerprint: "abc123" } }),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
test("worktree conflict shows a short question with paths and commands under Details", () => {
|
|
35
|
+
const summary = summarizeHumanAction(worktreeConflictAction());
|
|
36
|
+
assert.equal(summary.kind, "worktree-conflict");
|
|
37
|
+
assert.ok(!summary.title.includes(WORKTREE_PATH), "default question hides the path");
|
|
38
|
+
assert.ok(!summary.title.includes("git "), "default question hides git commands");
|
|
39
|
+
assert.ok(summary.context && summary.context.length > 0, "one-sentence next step present");
|
|
40
|
+
assert.equal(summary.safeResume, false);
|
|
41
|
+
// Labels stay server-accurate: no promise of safety on preserved work.
|
|
42
|
+
assert.deepEqual(summary.displayOptions.map((o) => o.label), ["Resume development", "Close"]);
|
|
43
|
+
assert.ok(!summary.displayOptions.some((o) => /safely/i.test(o.label)));
|
|
44
|
+
// Details preserve everything without truncation.
|
|
45
|
+
assert.equal(summary.details.reason, worktreeConflictAction().reason);
|
|
46
|
+
assert.equal(summary.details.question, worktreeConflictAction().question);
|
|
47
|
+
assert.deepEqual(summary.details.commands, [
|
|
48
|
+
`git -C ${WORKTREE_PATH} status`,
|
|
49
|
+
`git worktree remove --force ${WORKTREE_PATH}`,
|
|
50
|
+
]);
|
|
51
|
+
assert.ok(summary.details.paths.includes(WORKTREE_PATH));
|
|
52
|
+
});
|
|
53
|
+
test("dirty worktree shows a short question and never promises a safe resume", () => {
|
|
54
|
+
const reason = "Developer worktree has uncommitted changes after the session ended. " +
|
|
55
|
+
`Recovery:\ngit -C ${WORKTREE_PATH} status\ngit -C ${WORKTREE_PATH} stash list`;
|
|
56
|
+
const summary = summarizeHumanAction(baseAction({ reason, question: `${reason} Resume development, or close the issue?` }));
|
|
57
|
+
assert.equal(summary.kind, "dirty-worktree");
|
|
58
|
+
assert.ok(!summary.title.includes(WORKTREE_PATH));
|
|
59
|
+
assert.ok(!summary.title.includes("git "));
|
|
60
|
+
assert.ok(summary.context);
|
|
61
|
+
assert.equal(summary.safeResume, false);
|
|
62
|
+
assert.ok(!summary.displayOptions.some((o) => /safely/i.test(o.label)));
|
|
63
|
+
assert.deepEqual(summary.details.commands, [
|
|
64
|
+
`git -C ${WORKTREE_PATH} status`,
|
|
65
|
+
`git -C ${WORKTREE_PATH} stash list`,
|
|
66
|
+
]);
|
|
67
|
+
assert.ok(summary.details.paths.includes(WORKTREE_PATH));
|
|
68
|
+
});
|
|
69
|
+
test("ordinary escalation shows a short question plus one-sentence context, Details keep the reason", () => {
|
|
70
|
+
const reason = "Git/GitHub verification failed: gh pr checks timed out. (infra-attempt limit reached).";
|
|
71
|
+
const summary = summarizeHumanAction(baseAction({ reason, question: `${reason} Resume development, or close the issue?` }));
|
|
72
|
+
assert.equal(summary.kind, "ordinary-escalation");
|
|
73
|
+
assert.equal(summary.title, "Development is parked — resume or close?");
|
|
74
|
+
assert.ok(summary.context && summary.context.includes("Git/GitHub verification failed"));
|
|
75
|
+
assert.ok(!summary.title.includes("gh "), "no command block as the first question");
|
|
76
|
+
assert.equal(summary.safeResume, true);
|
|
77
|
+
assert.deepEqual(summary.displayOptions.map((o) => o.label), ["Resume safely", "Close"]);
|
|
78
|
+
assert.equal(summary.primaryChoice, "resume");
|
|
79
|
+
assert.equal(summary.details.reason, reason);
|
|
80
|
+
assert.deepEqual(summary.details.commands, []);
|
|
81
|
+
});
|
|
82
|
+
test("reviewer retry keeps its specific label and reads as safe", () => {
|
|
83
|
+
const summary = summarizeHumanAction(baseAction({
|
|
84
|
+
reason: "Reviewer session failed: timeout.",
|
|
85
|
+
question: "Reviewer session failed: timeout. Retry the review, or close the issue?",
|
|
86
|
+
continuationPreviewJson: JSON.stringify({ resumeRole: "reviewer", resumeHeadSha: "deadbeef" }),
|
|
87
|
+
responseOptionsJson: JSON.stringify([
|
|
88
|
+
{ choice: "resume", label: "Retry review" },
|
|
89
|
+
{ choice: "close", label: "Close" },
|
|
90
|
+
]),
|
|
91
|
+
}));
|
|
92
|
+
assert.equal(summary.kind, "reviewer-retry");
|
|
93
|
+
assert.equal(summary.safeResume, true);
|
|
94
|
+
assert.deepEqual(summary.displayOptions.map((o) => o.label), ["Retry review", "Close"]);
|
|
95
|
+
});
|
|
96
|
+
test("diverged push keeps push_with_lease with full SHAs visible in Details", () => {
|
|
97
|
+
const summary = summarizeHumanAction(baseAction({
|
|
98
|
+
reason: "Developer's commits could not be pushed: rejected: non-fast-forward",
|
|
99
|
+
question: "Developer's commits could not be pushed: rejected. Push local, resume, or close?",
|
|
100
|
+
evidenceJson: JSON.stringify({
|
|
101
|
+
pushDivergence: {
|
|
102
|
+
branch: "issue-1",
|
|
103
|
+
localSha: LOCAL_SHA,
|
|
104
|
+
remoteSha: REMOTE_SHA,
|
|
105
|
+
ahead: 2,
|
|
106
|
+
behind: 1,
|
|
107
|
+
relationship: "diverged",
|
|
108
|
+
worktreePath: WORKTREE_PATH,
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
responseOptionsJson: JSON.stringify([
|
|
112
|
+
{ choice: "push_with_lease", label: "Push with lease" },
|
|
113
|
+
{ choice: "resume", label: "Resume development" },
|
|
114
|
+
{ choice: "close", label: "Close" },
|
|
115
|
+
]),
|
|
116
|
+
}));
|
|
117
|
+
assert.equal(summary.kind, "diverged-push");
|
|
118
|
+
assert.equal(summary.safeResume, false);
|
|
119
|
+
assert.deepEqual(summary.displayOptions.map((o) => o.choice), ["push_with_lease", "resume", "close"]);
|
|
120
|
+
assert.equal(summary.primaryChoice, "push_with_lease");
|
|
121
|
+
assert.ok(!summary.displayOptions.some((o) => /safely/i.test(o.label)));
|
|
122
|
+
const facts = summary.details.factLines.join("\n");
|
|
123
|
+
assert.ok(facts.includes(LOCAL_SHA), "full local SHA, not truncated");
|
|
124
|
+
assert.ok(facts.includes(REMOTE_SHA), "full remote SHA, not truncated");
|
|
125
|
+
assert.ok(facts.includes("origin/issue-1"));
|
|
126
|
+
assert.ok(summary.details.paths.includes(WORKTREE_PATH));
|
|
127
|
+
assert.ok(summary.details.evidencePretty?.includes(LOCAL_SHA));
|
|
128
|
+
});
|
|
129
|
+
test("behind-only push evidence never offers push_with_lease", () => {
|
|
130
|
+
const summary = summarizeHumanAction(baseAction({
|
|
131
|
+
reason: "Developer's commits could not be pushed: rejected: non-fast-forward",
|
|
132
|
+
question: "Developer's commits could not be pushed. Resume development, or close the issue?",
|
|
133
|
+
evidenceJson: JSON.stringify({
|
|
134
|
+
pushDivergence: {
|
|
135
|
+
branch: "issue-1",
|
|
136
|
+
localSha: LOCAL_SHA,
|
|
137
|
+
remoteSha: REMOTE_SHA,
|
|
138
|
+
ahead: 0,
|
|
139
|
+
behind: 2,
|
|
140
|
+
relationship: "behind",
|
|
141
|
+
},
|
|
142
|
+
}),
|
|
143
|
+
}));
|
|
144
|
+
assert.equal(summary.kind, "unpushed");
|
|
145
|
+
assert.deepEqual(summary.displayOptions.map((o) => o.choice), ["resume", "close"]);
|
|
146
|
+
assert.equal(summary.safeResume, false);
|
|
147
|
+
});
|
|
148
|
+
test("legacy actions without evidence render sensibly and lose nothing", () => {
|
|
149
|
+
const reason = `issue branch is already used by worktree at ${WORKTREE_PATH}. ` +
|
|
150
|
+
`Recovery:\ngit worktree list\ngit worktree remove --force ${WORKTREE_PATH}`;
|
|
151
|
+
const question = `${reason} Resume development, or close the issue?`;
|
|
152
|
+
const summary = summarizeHumanAction(baseAction({ reason, question }));
|
|
153
|
+
assert.equal(summary.kind, "worktree-conflict");
|
|
154
|
+
assert.ok(!summary.title.includes(WORKTREE_PATH));
|
|
155
|
+
assert.equal(summary.safeResume, false);
|
|
156
|
+
assert.equal(summary.details.reason, reason);
|
|
157
|
+
assert.equal(summary.details.question, question);
|
|
158
|
+
assert.deepEqual(summary.details.commands, [
|
|
159
|
+
"git worktree list",
|
|
160
|
+
`git worktree remove --force ${WORKTREE_PATH}`,
|
|
161
|
+
]);
|
|
162
|
+
assert.deepEqual(summary.displayOptions.map((o) => o.choice), ["resume", "close"]);
|
|
163
|
+
});
|
|
164
|
+
test("non-policy actions pass through with their own question and options", () => {
|
|
165
|
+
const summary = summarizeHumanAction(baseAction({
|
|
166
|
+
actionType: "final_review",
|
|
167
|
+
reason: "Reviewer approved the PR",
|
|
168
|
+
question: "Merge this work, send it back for another repair round, or close it?",
|
|
169
|
+
responseOptionsJson: JSON.stringify([
|
|
170
|
+
{ choice: "merge", label: "Merge" },
|
|
171
|
+
{ choice: "repair", label: "Another repair round" },
|
|
172
|
+
{ choice: "close", label: "Close" },
|
|
173
|
+
]),
|
|
174
|
+
}));
|
|
175
|
+
assert.equal(summary.kind, "other");
|
|
176
|
+
assert.equal(summary.title, "Merge this work, send it back for another repair round, or close it?");
|
|
177
|
+
assert.equal(summary.safeResume, false);
|
|
178
|
+
assert.deepEqual(summary.displayOptions.map((o) => o.label), ["Merge", "Another repair round", "Close"]);
|
|
179
|
+
assert.equal(summary.primaryChoice, "merge");
|
|
180
|
+
});
|
|
@@ -27,21 +27,21 @@ export declare const CheckpointObservedPayload: z.ZodObject<{
|
|
|
27
27
|
samplingPrecisionMs: z.ZodNullable<z.ZodNumber>;
|
|
28
28
|
branch: z.ZodNullable<z.ZodString>;
|
|
29
29
|
}, "strip", z.ZodTypeAny, {
|
|
30
|
+
branch: string | null;
|
|
30
31
|
inputSha: string | null;
|
|
31
32
|
kind: "commit" | "verification_receipt" | "branch_pushed";
|
|
32
33
|
observedSha: string | null;
|
|
33
34
|
observedAt: string;
|
|
34
35
|
origin: "sampler" | "salvage" | "session_end" | null;
|
|
35
36
|
samplingPrecisionMs: number | null;
|
|
36
|
-
branch: string | null;
|
|
37
37
|
}, {
|
|
38
|
+
branch: string | null;
|
|
38
39
|
inputSha: string | null;
|
|
39
40
|
kind: "commit" | "verification_receipt" | "branch_pushed";
|
|
40
41
|
observedSha: string | null;
|
|
41
42
|
observedAt: string;
|
|
42
43
|
origin: "sampler" | "salvage" | "session_end" | null;
|
|
43
44
|
samplingPrecisionMs: number | null;
|
|
44
|
-
branch: string | null;
|
|
45
45
|
}>;
|
|
46
46
|
export type CheckpointObservedPayload = z.infer<typeof CheckpointObservedPayload>;
|
|
47
47
|
export declare const RetryReusedPayload: z.ZodObject<{
|
|
@@ -43,7 +43,7 @@ export declare const ExecutionReportQuery: z.ZodObject<{
|
|
|
43
43
|
limit: number;
|
|
44
44
|
page: number;
|
|
45
45
|
status?: string | undefined;
|
|
46
|
-
role?: "
|
|
46
|
+
role?: "reviewer" | "developer" | "legacy" | undefined;
|
|
47
47
|
runtime?: string | undefined;
|
|
48
48
|
model?: string | undefined;
|
|
49
49
|
from?: string | undefined;
|
|
@@ -51,7 +51,7 @@ export declare const ExecutionReportQuery: z.ZodObject<{
|
|
|
51
51
|
repo?: string | undefined;
|
|
52
52
|
}, {
|
|
53
53
|
status?: string | undefined;
|
|
54
|
-
role?: "
|
|
54
|
+
role?: "reviewer" | "developer" | "legacy" | undefined;
|
|
55
55
|
runtime?: string | undefined;
|
|
56
56
|
model?: string | undefined;
|
|
57
57
|
from?: string | undefined;
|
|
@@ -29,34 +29,34 @@ export declare const HumanAction: z.ZodObject<{
|
|
|
29
29
|
requestedAt: z.ZodString;
|
|
30
30
|
resolvedAt: z.ZodNullable<z.ZodString>;
|
|
31
31
|
}, "strip", z.ZodTypeAny, {
|
|
32
|
+
actionType: "policy_escalation" | "final_review" | "product_scope_decision" | "attempts_exhausted" | "deck_interaction_required" | "reflection_interaction_required" | "outbound_delivery_interaction_required";
|
|
33
|
+
question: string;
|
|
34
|
+
reason: string;
|
|
35
|
+
evidenceJson: string | null;
|
|
36
|
+
responseOptionsJson: string | null;
|
|
37
|
+
continuationPreviewJson: string | null;
|
|
32
38
|
status: "open" | "resolved";
|
|
33
39
|
id: string;
|
|
34
40
|
issueId: string | null;
|
|
35
41
|
runId: string | null;
|
|
36
42
|
workflowInstanceId: string | null;
|
|
37
|
-
actionType: "product_scope_decision" | "policy_escalation" | "attempts_exhausted" | "final_review" | "deck_interaction_required" | "reflection_interaction_required" | "outbound_delivery_interaction_required";
|
|
38
|
-
reason: string;
|
|
39
|
-
question: string;
|
|
40
|
-
evidenceJson: string | null;
|
|
41
|
-
responseOptionsJson: string | null;
|
|
42
|
-
continuationPreviewJson: string | null;
|
|
43
43
|
requestId: string | null;
|
|
44
44
|
resolutionJson: string | null;
|
|
45
45
|
resolvedBy: string | null;
|
|
46
46
|
requestedAt: string;
|
|
47
47
|
resolvedAt: string | null;
|
|
48
48
|
}, {
|
|
49
|
+
actionType: "policy_escalation" | "final_review" | "product_scope_decision" | "attempts_exhausted" | "deck_interaction_required" | "reflection_interaction_required" | "outbound_delivery_interaction_required";
|
|
50
|
+
question: string;
|
|
51
|
+
reason: string;
|
|
52
|
+
evidenceJson: string | null;
|
|
53
|
+
responseOptionsJson: string | null;
|
|
54
|
+
continuationPreviewJson: string | null;
|
|
49
55
|
status: "open" | "resolved";
|
|
50
56
|
id: string;
|
|
51
57
|
issueId: string | null;
|
|
52
58
|
runId: string | null;
|
|
53
59
|
workflowInstanceId: string | null;
|
|
54
|
-
actionType: "product_scope_decision" | "policy_escalation" | "attempts_exhausted" | "final_review" | "deck_interaction_required" | "reflection_interaction_required" | "outbound_delivery_interaction_required";
|
|
55
|
-
reason: string;
|
|
56
|
-
question: string;
|
|
57
|
-
evidenceJson: string | null;
|
|
58
|
-
responseOptionsJson: string | null;
|
|
59
|
-
continuationPreviewJson: string | null;
|
|
60
60
|
requestId: string | null;
|
|
61
61
|
resolutionJson: string | null;
|
|
62
62
|
resolvedBy: string | null;
|