mergestorm 0.3.25 → 0.3.27
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/README.md +19 -6
- package/dist/api.d.ts +59 -0
- package/dist/api.js +40 -0
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.js +3 -2
- package/dist/commands/dismiss.d.ts +15 -0
- package/dist/commands/dismiss.js +145 -0
- package/dist/commands/registry.js +17 -1
- package/dist/commands/stack.d.ts +9 -0
- package/dist/commands/stack.js +74 -7
- package/dist/shell.js +1 -1
- package/dist/stack-blockers.d.ts +49 -1
- package/dist/stack-blockers.js +124 -11
- package/dist/stack-dto.d.ts +14 -0
- package/dist/stack-watch-obligation.d.ts +43 -0
- package/dist/stack-watch-obligation.js +79 -0
- package/dist/stack-watch.d.ts +7 -1
- package/dist/stack-watch.js +39 -20
- package/dist/ui/ansi.d.ts +14 -1
- package/dist/ui/ansi.js +25 -5
- package/dist/ui/banner.d.ts +2 -1
- package/dist/ui/banner.js +26 -47
- package/dist/ui/logo.d.ts +13 -8
- package/dist/ui/logo.js +25 -13
- package/dist/ui/prompt.d.ts +11 -1
- package/dist/ui/prompt.js +92 -9
- package/package.json +1 -1
- package/skill/mergestorm-bounce-watch/SKILL.md +9 -5
- package/skill/mergestorm-pr-loop/SKILL.md +7 -1
package/dist/stack-blockers.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export type StackAgentBusy = {
|
|
|
12
12
|
agent: "vortex" | "cyclone";
|
|
13
13
|
blocker: string;
|
|
14
14
|
};
|
|
15
|
+
export type StackBusyAgent = StackAgentBusy["agent"];
|
|
16
|
+
export type StackHeldBlocker = StackBlocker & {
|
|
17
|
+
actAfter: "agents_idle";
|
|
18
|
+
waitingOn: StackBusyAgent[];
|
|
19
|
+
};
|
|
15
20
|
export type StackLayerAgents = {
|
|
16
21
|
prNumber: number;
|
|
17
22
|
headSha: string | null;
|
|
@@ -27,16 +32,57 @@ export type StackLayerAgents = {
|
|
|
27
32
|
cyclone: boolean;
|
|
28
33
|
};
|
|
29
34
|
};
|
|
35
|
+
export type StackRepairHint = {
|
|
36
|
+
kind: "restack_conflict" | "merge_conflict";
|
|
37
|
+
prNumber: number;
|
|
38
|
+
headSha: string | null;
|
|
39
|
+
branch: string;
|
|
40
|
+
liveParent: string | null;
|
|
41
|
+
files: string[];
|
|
42
|
+
steps: string;
|
|
43
|
+
} | {
|
|
44
|
+
kind: "ci_failure";
|
|
45
|
+
prNumber: number;
|
|
46
|
+
headSha: string | null;
|
|
47
|
+
branch: string;
|
|
48
|
+
failingCheck: string | null;
|
|
49
|
+
steps: string;
|
|
50
|
+
};
|
|
51
|
+
export type StackLandGatePending = {
|
|
52
|
+
prNumber: number;
|
|
53
|
+
headSha: string | null;
|
|
54
|
+
reason: string;
|
|
55
|
+
};
|
|
56
|
+
export declare function landGateIsPending(reason: string | null | undefined): boolean;
|
|
30
57
|
export declare function layerAgentsBusy(layer: StackLayerDto): StackAgentsBusy;
|
|
31
58
|
export declare function layerAgents(layer: StackLayerDto): StackLayerAgents;
|
|
32
59
|
export declare function isMgParkBase(branch: string | null | undefined): boolean;
|
|
33
60
|
/** GitHub DIRTY vs an mg-park-* freeze is not a merge conflict, verified or not. */
|
|
34
61
|
export declare function boundDirty(layer: StackLayerDto): boolean;
|
|
62
|
+
export declare function mergeabilityPending(layer: StackLayerDto): boolean;
|
|
35
63
|
export declare function sameHead(a: string | null | undefined, b: string | null | undefined): boolean;
|
|
36
64
|
export declare function currentLayer(stack: StackDto): StackLayerDto | undefined;
|
|
65
|
+
export declare function conflictLiveParent(stack: StackDto, layer: StackLayerDto): string | null;
|
|
66
|
+
export declare function conflictRepairSteps(branch: string, liveParent: string | null): string;
|
|
67
|
+
export declare function agentsCannotClear(blocker: string): boolean;
|
|
68
|
+
export declare function stackRepair(stack: StackDto, attention: StackBlocker | null, layer: StackLayerDto | undefined, bounce: MergeQueueEntryDto | undefined): StackRepairHint | null;
|
|
37
69
|
/** Pair-gate attention plus live hard blocks on every other open layer. */
|
|
38
70
|
export declare function stackBlockers(stack: StackDto, entries?: MergeQueueEntryDto[], cursor?: Pick<StackWatchCursor, "bounceId" | "afterFinishedAt">): {
|
|
71
|
+
attention: null;
|
|
72
|
+
held: StackHeldBlocker;
|
|
73
|
+
issues: StackBlocker[];
|
|
74
|
+
currentCandidate: {
|
|
75
|
+
prNumber: number;
|
|
76
|
+
headSha: string | null;
|
|
77
|
+
} | null;
|
|
78
|
+
bounce: undefined;
|
|
79
|
+
busy: StackAgentBusy[];
|
|
80
|
+
agents: StackLayerAgents | null;
|
|
81
|
+
repair: StackRepairHint | null;
|
|
82
|
+
landGatePending: StackLandGatePending | null;
|
|
83
|
+
} | {
|
|
39
84
|
attention: StackBlocker | null;
|
|
85
|
+
held: null;
|
|
40
86
|
issues: StackBlocker[];
|
|
41
87
|
currentCandidate: {
|
|
42
88
|
prNumber: number;
|
|
@@ -45,6 +91,8 @@ export declare function stackBlockers(stack: StackDto, entries?: MergeQueueEntry
|
|
|
45
91
|
bounce: MergeQueueEntryDto | undefined;
|
|
46
92
|
busy: StackAgentBusy[];
|
|
47
93
|
agents: StackLayerAgents | null;
|
|
94
|
+
repair: StackRepairHint | null;
|
|
95
|
+
landGatePending: StackLandGatePending | null;
|
|
48
96
|
};
|
|
49
97
|
/** Compact contract text shared by status and wait summaries. */
|
|
50
|
-
export declare function stackBlockersSummary(attention: Pick<StackBlocker, "prNumber" | "blocker"> | null, issues: StackBlocker[], busy?: readonly StackAgentBusy[]): string;
|
|
98
|
+
export declare function stackBlockersSummary(attention: Pick<StackBlocker, "prNumber" | "blocker"> | null, issues: StackBlocker[], busy?: readonly StackAgentBusy[], landGatePending?: Pick<StackLandGatePending, "prNumber" | "reason"> | null, actAfter?: StackHeldBlocker["actAfter"] | null): string;
|
package/dist/stack-blockers.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { deriveStackAgentsBusy, mergeQueueBounceLabel, restackRetryPending, } from "./stack-dto.js";
|
|
2
|
+
export function landGateIsPending(reason) {
|
|
3
|
+
const token = reason?.trim().replace(/^landing blocked:\s*/i, "").match(/^[a-z_]+/i)?.[0] ?? "";
|
|
4
|
+
return /_(?:pending|running)$/i.test(token);
|
|
5
|
+
}
|
|
2
6
|
export function layerAgentsBusy(layer) {
|
|
3
7
|
return layer.agentsBusy ?? deriveStackAgentsBusy(layer);
|
|
4
8
|
}
|
|
@@ -26,6 +30,12 @@ export function boundDirty(layer) {
|
|
|
26
30
|
return (!layer.headSha || layer.mergeableHeadSha === layer.headSha) &&
|
|
27
31
|
(layer.mergeable === false || layer.mergeableState?.toLowerCase() === "dirty");
|
|
28
32
|
}
|
|
33
|
+
export function mergeabilityPending(layer) {
|
|
34
|
+
if (isMgParkBase(layer.parentBranch))
|
|
35
|
+
return false;
|
|
36
|
+
return layer.mergeable == null && !!layer.headSha && layer.mergeableHeadSha === layer.headSha &&
|
|
37
|
+
layer.mergeableState?.toLowerCase() === "pending";
|
|
38
|
+
}
|
|
29
39
|
export function sameHead(a, b) {
|
|
30
40
|
const left = a?.trim().toLowerCase();
|
|
31
41
|
const right = b?.trim().toLowerCase();
|
|
@@ -88,6 +98,9 @@ function layerAttention(stack, layer, queue, cursor) {
|
|
|
88
98
|
const name = layer.checks?.failingName?.trim();
|
|
89
99
|
return hardBlock(`CI failed${name ? ` — ${name}` : ""}`);
|
|
90
100
|
}
|
|
101
|
+
if (layer.agentRuns?.some((run) => run.agent === "cyclone" && run.status === "failed" &&
|
|
102
|
+
sameHead(run.sha, layer.headSha)))
|
|
103
|
+
return hardBlock("Cyclone failed");
|
|
91
104
|
const tempest = layer.agentRuns?.find((run) => run.agent === "tempest" &&
|
|
92
105
|
["findings", "failed"].includes(run.status) && sameHead(run.sha, layer.headSha));
|
|
93
106
|
if (tempest)
|
|
@@ -98,8 +111,9 @@ function layerAttention(stack, layer, queue, cursor) {
|
|
|
98
111
|
if (seam)
|
|
99
112
|
return hardBlock(seam);
|
|
100
113
|
if (stack.unit?.landPrNumber === layer.prNumber) {
|
|
101
|
-
|
|
102
|
-
|
|
114
|
+
const landing = stack.unit.landingBlockReason?.trim();
|
|
115
|
+
if (landing && !landGateIsPending(landing))
|
|
116
|
+
return hardBlock(landing);
|
|
103
117
|
if (stack.unit.tempestLandStatus?.toLowerCase() === "failed")
|
|
104
118
|
return hardBlock("Tempest failed");
|
|
105
119
|
}
|
|
@@ -115,6 +129,81 @@ function layerAttention(stack, layer, queue, cursor) {
|
|
|
115
129
|
return none;
|
|
116
130
|
return { blocker: mergeQueueBounceLabel(bounce), bounceKind: kind, bounce };
|
|
117
131
|
}
|
|
132
|
+
function isTerminalLayer(layer) {
|
|
133
|
+
return layer.state === "merged" || layer.state === "closed";
|
|
134
|
+
}
|
|
135
|
+
export function conflictLiveParent(stack, layer) {
|
|
136
|
+
const promoted = (candidate) => !!stack.unit?.members?.some((member) => member.prNumber === candidate.prNumber && member.promotedHeadSha?.trim());
|
|
137
|
+
const sibling = stack.layers
|
|
138
|
+
.filter((candidate) => candidate.prNumber > 0 && candidate.position < layer.position &&
|
|
139
|
+
!isTerminalLayer(candidate) && !promoted(candidate))
|
|
140
|
+
.sort((a, b) => b.position - a.position)[0];
|
|
141
|
+
if (sibling)
|
|
142
|
+
return sibling.branch;
|
|
143
|
+
const retired = new Set([
|
|
144
|
+
...stack.layers.filter((candidate) => isTerminalLayer(candidate) || promoted(candidate)).map((candidate) => candidate.branch),
|
|
145
|
+
...(stack.unit?.members ?? []).filter((member) => member.promotedHeadSha?.trim()).map((member) => member.branch),
|
|
146
|
+
]);
|
|
147
|
+
const usable = (branch) => !!branch?.trim() && !isMgParkBase(branch) && !retired.has(branch) && branch !== layer.branch;
|
|
148
|
+
for (const branch of [layer.restackError?.from?.branch, layer.parentBranch, stack.unit?.branch, stack.trunkBranch]) {
|
|
149
|
+
if (usable(branch))
|
|
150
|
+
return branch;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
function conflictFiles(layer, bounce) {
|
|
155
|
+
const paths = bounce?.bounceDetail?.conflictPaths?.filter((path) => path.trim());
|
|
156
|
+
if (paths?.length)
|
|
157
|
+
return [...paths];
|
|
158
|
+
const detail = layer.restackError?.kind === "rebase_conflict" ? layer.restackError.detail : layer.conflictDetail;
|
|
159
|
+
const listed = detail?.match(/^conflict in (.+)$/)?.[1];
|
|
160
|
+
return listed ? listed.split(", ").map((path) => path.trim()).filter(Boolean) : [];
|
|
161
|
+
}
|
|
162
|
+
export function conflictRepairSteps(branch, liveParent) {
|
|
163
|
+
if (!liveParent) {
|
|
164
|
+
return `The live parent of ${branch} is unknown (it is never an mg-park-* freeze). Do not merge or rebase anything; stop and tell the human.`;
|
|
165
|
+
}
|
|
166
|
+
return `Merge ${liveParent} into ${branch}: git fetch origin, check out ${branch} at its live remote head, git merge origin/${liveParent}, resolve the conflicts, run the tests, then an ordinary git push (no force). Do not rebase onto trunk, main, or mg-park-*, and do not retarget the PR base.`;
|
|
167
|
+
}
|
|
168
|
+
function landPrLiveParent(stack, layer) {
|
|
169
|
+
const base = stack.unit?.landTarget?.trim() || layer.parentBranch?.trim();
|
|
170
|
+
return base && !isMgParkBase(base) && base !== layer.branch ? base : null;
|
|
171
|
+
}
|
|
172
|
+
export function agentsCannotClear(blocker) {
|
|
173
|
+
return blocker === "Conflict" || blocker === "Restack failed" || blocker === "Draft PR" ||
|
|
174
|
+
blocker.startsWith("Merge conflicts");
|
|
175
|
+
}
|
|
176
|
+
export function stackRepair(stack, attention, layer, bounce) {
|
|
177
|
+
if (!attention || !layer || attention.prNumber !== layer.prNumber)
|
|
178
|
+
return null;
|
|
179
|
+
const landPr = !!stack.unit && layer.prNumber === stack.unit.landPrNumber && layer.branch === stack.unit.branch;
|
|
180
|
+
const restackConflict = attention.blocker === "Conflict" || attention.bounceKind === "restack_conflict";
|
|
181
|
+
const mergeConflict = attention.blocker.startsWith("Merge conflicts");
|
|
182
|
+
if (restackConflict || mergeConflict) {
|
|
183
|
+
const liveParent = landPr ? landPrLiveParent(stack, layer) : conflictLiveParent(stack, layer);
|
|
184
|
+
return {
|
|
185
|
+
kind: restackConflict ? "restack_conflict" : "merge_conflict",
|
|
186
|
+
prNumber: layer.prNumber,
|
|
187
|
+
headSha: layer.headSha ?? null,
|
|
188
|
+
branch: layer.branch,
|
|
189
|
+
liveParent,
|
|
190
|
+
files: conflictFiles(layer, bounce),
|
|
191
|
+
steps: conflictRepairSteps(layer.branch, liveParent),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (attention.blocker.startsWith("CI failed") || attention.bounceKind === "ci_failure") {
|
|
195
|
+
const failingCheck = bounce?.bounceDetail?.failingCheck?.trim() || layer.checks?.failingName?.trim() || null;
|
|
196
|
+
return {
|
|
197
|
+
kind: "ci_failure",
|
|
198
|
+
prNumber: layer.prNumber,
|
|
199
|
+
headSha: layer.headSha ?? null,
|
|
200
|
+
branch: layer.branch,
|
|
201
|
+
failingCheck,
|
|
202
|
+
steps: `Read the failing ${failingCheck ? `check ${failingCheck}` : "checks"} on ${layer.branch} (gh run list --branch ${layer.branch}, then gh run view <run-id> --log-failed), fix it on ${layer.branch} with the smallest patch, run that check locally, confirm the remote head is still ${layer.headSha ?? "the head you started from"}, then an ordinary git push. Do not retarget the PR base.`,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
118
207
|
/** Pair-gate attention plus live hard blocks on every other open layer. */
|
|
119
208
|
export function stackBlockers(stack, entries = [], cursor = {}) {
|
|
120
209
|
const candidate = currentLayer(stack);
|
|
@@ -134,8 +223,13 @@ export function stackBlockers(stack, entries = [], cursor = {}) {
|
|
|
134
223
|
for (const layer of open) {
|
|
135
224
|
const selected = layer.prNumber === candidate?.prNumber;
|
|
136
225
|
const result = layerAttention(stack, layer, selected ? queue : [], cursor);
|
|
137
|
-
if (!result.blocker)
|
|
226
|
+
if (!result.blocker) {
|
|
227
|
+
if (mergeabilityPending(layer) && (selected || !candidate || layer.position > candidate.position)) {
|
|
228
|
+
issues.push({ prNumber: layer.prNumber, headSha: layer.headSha ?? null,
|
|
229
|
+
blocker: `Merge state unknown${layer.parentBranch ? ` vs ${layer.parentBranch}` : ""}`, bounceKind: null });
|
|
230
|
+
}
|
|
138
231
|
continue;
|
|
232
|
+
}
|
|
139
233
|
const issue = { prNumber: layer.prNumber, headSha: layer.headSha ?? null,
|
|
140
234
|
blocker: result.blocker, bounceKind: result.bounceKind };
|
|
141
235
|
if (selected || (!attention && layer === child)) {
|
|
@@ -155,16 +249,35 @@ export function stackBlockers(stack, entries = [], cursor = {}) {
|
|
|
155
249
|
busy.push({ prNumber: attention.prNumber, headSha: attention.headSha, agent, blocker: attention.blocker });
|
|
156
250
|
}
|
|
157
251
|
}
|
|
252
|
+
const landing = stack.unit?.landingBlockReason?.trim();
|
|
253
|
+
const landLayer = stack.unit?.landPrNumber != null && !stack.archivedAt
|
|
254
|
+
? open.find((layer) => layer.prNumber === stack.unit?.landPrNumber)
|
|
255
|
+
: undefined;
|
|
256
|
+
const landGatePending = landLayer && landing && landGateIsPending(landing)
|
|
257
|
+
? { prNumber: landLayer.prNumber, headSha: landLayer.headSha ?? null, reason: landing }
|
|
258
|
+
: null;
|
|
259
|
+
if (busy.length && attention && agentsCannotClear(attention.blocker)) {
|
|
260
|
+
const held = { ...attention, actAfter: "agents_idle", waitingOn: busy.map((entry) => entry.agent) };
|
|
261
|
+
return { attention: null, held, issues: [attention, ...issues], currentCandidate, bounce: undefined, busy, agents,
|
|
262
|
+
repair: stackRepair(stack, attention, attentionLayer, undefined), landGatePending };
|
|
263
|
+
}
|
|
158
264
|
if (busy.length)
|
|
159
|
-
return { attention: null, issues, currentCandidate, bounce: undefined, busy, agents };
|
|
160
|
-
return { attention, issues, currentCandidate, bounce, busy, agents };
|
|
265
|
+
return { attention: null, held: null, issues, currentCandidate, bounce: undefined, busy, agents, repair: null, landGatePending };
|
|
266
|
+
return { attention, held: null, issues, currentCandidate, bounce, busy, agents, repair: stackRepair(stack, attention, attentionLayer, bounce), landGatePending };
|
|
161
267
|
}
|
|
162
268
|
/** Compact contract text shared by status and wait summaries. */
|
|
163
|
-
export function stackBlockersSummary(attention, issues, busy = []) {
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
? ` ·
|
|
269
|
+
export function stackBlockersSummary(attention, issues, busy = [], landGatePending = null, actAfter = null) {
|
|
270
|
+
const agentWork = (entry) => entry.agent === "cyclone" ? "Cyclone patches" : "Vortex reviews";
|
|
271
|
+
const blocked = attention && actAfter && busy.length
|
|
272
|
+
? ` · blocked after the agents: #${attention.prNumber} ${attention.blocker} (plan the fix; act once ${busy.map(agentWork).join(" and ")} finish and the watch returns attention)`
|
|
273
|
+
: attention ? ` · blocked: #${attention.prNumber} ${attention.blocker}` : "";
|
|
274
|
+
const working = busy.length && !actAfter
|
|
275
|
+
? ` · held: #${busy[0].prNumber} ${busy[0].blocker} while ${busy.map(agentWork).join(" and ")}`
|
|
276
|
+
: "";
|
|
277
|
+
const listed = actAfter && attention ? issues.filter((issue) => issue.prNumber !== attention.prNumber || issue.blocker !== attention.blocker) : issues;
|
|
278
|
+
const labels = listed.slice(0, 3).map(issue => `#${issue.prNumber} ${issue.blocker}`).join("; ");
|
|
279
|
+
const gate = landGatePending && !attention
|
|
280
|
+
? ` · land gate: #${landGatePending.prNumber} ${landGatePending.reason} (nothing to fix; wait)`
|
|
167
281
|
: "";
|
|
168
|
-
|
|
169
|
-
return blocked + working + (issues.length ? ` · issues: ${labels}${issues.length > 3 ? `; +${issues.length - 3} more` : ""}` : "");
|
|
282
|
+
return blocked + working + gate + (listed.length ? ` · issues: ${labels}${listed.length > 3 ? `; +${listed.length - 3} more` : ""}` : "");
|
|
170
283
|
}
|
package/dist/stack-dto.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ export type RestackError = {
|
|
|
5
5
|
attemptedAt: string;
|
|
6
6
|
attempts: number;
|
|
7
7
|
backupRef: string | null;
|
|
8
|
+
from?: {
|
|
9
|
+
branch: string;
|
|
10
|
+
oldSha: string;
|
|
11
|
+
};
|
|
8
12
|
};
|
|
9
13
|
export declare function restackAttemptsExhausted(error: Pick<RestackError, "attempts">): boolean;
|
|
10
14
|
export declare function pushRejectionIsRetryable(message: string): boolean;
|
|
@@ -154,6 +158,14 @@ export type StackLayerConflictResolution = {
|
|
|
154
158
|
*/
|
|
155
159
|
dispatchId: string | null;
|
|
156
160
|
};
|
|
161
|
+
export type StackLayerCiFixStatus = "verified" | "unverified" | "no_changes" | "failed";
|
|
162
|
+
export type StackLayerCiFix = {
|
|
163
|
+
status: StackLayerCiFixStatus;
|
|
164
|
+
failingCheck: string | null;
|
|
165
|
+
resultSha: string | null;
|
|
166
|
+
reason: string | null;
|
|
167
|
+
at: string;
|
|
168
|
+
};
|
|
157
169
|
export type StackLayerDto = {
|
|
158
170
|
branch: string;
|
|
159
171
|
parentBranch: string | null;
|
|
@@ -228,6 +240,7 @@ export type StackLayerDto = {
|
|
|
228
240
|
* layers and when the specialist never ran.
|
|
229
241
|
*/
|
|
230
242
|
conflictResolution?: StackLayerConflictResolution | null;
|
|
243
|
+
ciFix?: StackLayerCiFix | null;
|
|
231
244
|
};
|
|
232
245
|
export type StackUnitMemberDto = {
|
|
233
246
|
prNumber: number;
|
|
@@ -370,6 +383,7 @@ export type MergeQueueBounceDetail = {
|
|
|
370
383
|
conflictDetail?: string;
|
|
371
384
|
conflictPaths?: string[];
|
|
372
385
|
message?: string;
|
|
386
|
+
warning?: string;
|
|
373
387
|
};
|
|
374
388
|
export type MergeQueueEntryDto = {
|
|
375
389
|
id: string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { StackDto } from "./stack-dto.js";
|
|
2
|
+
export declare const STACK_WATCH_NOT_DONE_SENTENCE = "This stack is not landed. Your task is not done. Call stack_wait again with this cursor.";
|
|
3
|
+
export type StackWatchDoneReason = "landed" | "closed" | "archived" | "not_found";
|
|
4
|
+
export type StackWatchReason = StackWatchDoneReason | "open" | "attention" | "unread" | "rate_limited" | "failed";
|
|
5
|
+
export type StackWatchNextArgs = {
|
|
6
|
+
stack_id: string;
|
|
7
|
+
enrolled_head_sha?: string | null;
|
|
8
|
+
after_finished_at?: string | null;
|
|
9
|
+
bounce_id?: string | null;
|
|
10
|
+
timeout_s: 45;
|
|
11
|
+
};
|
|
12
|
+
export type StackWatchNext = {
|
|
13
|
+
tool: "stack_wait";
|
|
14
|
+
args: StackWatchNextArgs;
|
|
15
|
+
command: string;
|
|
16
|
+
};
|
|
17
|
+
export type StackWatchObligation = {
|
|
18
|
+
done: boolean;
|
|
19
|
+
until: "landed";
|
|
20
|
+
reason: StackWatchReason;
|
|
21
|
+
next: StackWatchNext | null;
|
|
22
|
+
message: string;
|
|
23
|
+
};
|
|
24
|
+
export type StackWatchObligationInput = {
|
|
25
|
+
stackId: string;
|
|
26
|
+
terminal: StackWatchDoneReason | null;
|
|
27
|
+
cursor?: {
|
|
28
|
+
enrolledHeadSha?: string | null;
|
|
29
|
+
afterFinishedAt?: string | null;
|
|
30
|
+
bounceId?: string | null;
|
|
31
|
+
} | null;
|
|
32
|
+
status?: string;
|
|
33
|
+
attention?: {
|
|
34
|
+
prNumber: number;
|
|
35
|
+
blocker: string;
|
|
36
|
+
} | null;
|
|
37
|
+
retryAfterSeconds?: number;
|
|
38
|
+
unread?: boolean;
|
|
39
|
+
freshCursor?: boolean;
|
|
40
|
+
};
|
|
41
|
+
export declare function stackTerminalReason(stack: Pick<StackDto, "archivedAt" | "layers" | "unit"> | null | undefined): StackWatchDoneReason | null;
|
|
42
|
+
export declare function stackWatchNextArgs(stackId: string, cursor: StackWatchObligationInput["cursor"]): StackWatchNextArgs;
|
|
43
|
+
export declare function stackWatchObligation(input: StackWatchObligationInput): StackWatchObligation;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export const STACK_WATCH_NOT_DONE_SENTENCE = "This stack is not landed. Your task is not done. Call stack_wait again with this cursor.";
|
|
2
|
+
const DONE_MESSAGES = {
|
|
3
|
+
landed: "This stack is landed. The watch is done; stop calling stack_wait for it.",
|
|
4
|
+
closed: "Every PR in this stack is merged or closed and nothing is left to land, but not every PR landed. The watch is done; tell the human which PRs were closed without landing.",
|
|
5
|
+
archived: "This stack is archived: the human took it back. The watch is done; stop calling stack_wait for it and do not push to its PRs.",
|
|
6
|
+
not_found: "No stack with this id exists for this account. Mergestorm deletes a stack once none of its layers is open, so a stack you were watching has most likely landed; a mistyped id or another account's stack reads the same. The watch is done; confirm each PR is merged (gh pr view <n> --json state) before reporting it landed.",
|
|
7
|
+
};
|
|
8
|
+
export function stackTerminalReason(stack) {
|
|
9
|
+
if (!stack)
|
|
10
|
+
return "not_found";
|
|
11
|
+
if (stack.archivedAt)
|
|
12
|
+
return "archived";
|
|
13
|
+
const layers = stack.layers;
|
|
14
|
+
if (layers.some((layer) => layer.state !== "merged" && layer.state !== "closed"))
|
|
15
|
+
return null;
|
|
16
|
+
const unit = stack.unit;
|
|
17
|
+
if (layers.length === 0 && !unit)
|
|
18
|
+
return null;
|
|
19
|
+
const landPr = unit?.landPr;
|
|
20
|
+
if (landPr && landPr.prNumber > 0 && landPr.state !== "merged" && landPr.state !== "closed")
|
|
21
|
+
return null;
|
|
22
|
+
if (unit && unit.state !== "landed" && unit.state !== "abandoned")
|
|
23
|
+
return null;
|
|
24
|
+
const everyLayerMerged = layers.every((layer) => layer.state === "merged");
|
|
25
|
+
return everyLayerMerged && (!unit || unit.state === "landed") ? "landed" : "closed";
|
|
26
|
+
}
|
|
27
|
+
export function stackWatchNextArgs(stackId, cursor) {
|
|
28
|
+
return {
|
|
29
|
+
stack_id: stackId,
|
|
30
|
+
...(cursor && cursor.enrolledHeadSha !== undefined ? { enrolled_head_sha: cursor.enrolledHeadSha } : {}),
|
|
31
|
+
...(cursor && cursor.afterFinishedAt !== undefined ? { after_finished_at: cursor.afterFinishedAt } : {}),
|
|
32
|
+
...(cursor && cursor.bounceId !== undefined ? { bounce_id: cursor.bounceId } : {}),
|
|
33
|
+
timeout_s: 45,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function stackWatchObligation(input) {
|
|
37
|
+
if (input.terminal) {
|
|
38
|
+
return {
|
|
39
|
+
done: true,
|
|
40
|
+
until: "landed",
|
|
41
|
+
reason: input.terminal,
|
|
42
|
+
next: null,
|
|
43
|
+
message: DONE_MESSAGES[input.terminal],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const args = stackWatchNextArgs(input.stackId, input.cursor);
|
|
47
|
+
const reason = input.status === "rate_limited"
|
|
48
|
+
? "rate_limited"
|
|
49
|
+
: input.status === "failed"
|
|
50
|
+
? "failed"
|
|
51
|
+
: input.attention ? "attention" : input.unread ? "unread" : "open";
|
|
52
|
+
const parts = [
|
|
53
|
+
STACK_WATCH_NOT_DONE_SENTENCE,
|
|
54
|
+
`Watch stack ${input.stackId} as a whole until it lands or the human takes it back. A clean push, a submit, or one merged layer does not finish it: merging a lower layer can break a layer above it.`,
|
|
55
|
+
];
|
|
56
|
+
if (reason === "rate_limited") {
|
|
57
|
+
parts.push(`Wait ${input.retryAfterSeconds !== undefined ? `${input.retryAfterSeconds}s` : "retry_after_seconds"} first, then call stack_wait with the same cursor.`);
|
|
58
|
+
}
|
|
59
|
+
else if (reason === "failed") {
|
|
60
|
+
parts.push(`This read failed, which does not end the task. Call stack_wait once more with the same cursor; if that fails too, tell the human that stack ${input.stackId} is not landed and is no longer being watched.`);
|
|
61
|
+
}
|
|
62
|
+
else if (reason === "attention" && input.attention) {
|
|
63
|
+
parts.push(`First fix #${input.attention.prNumber} (${input.attention.blocker}); repair, when present, names the fix. Calling stack_wait again before fixing it returns the same attention. After your push, call stack_wait with this cursor and enrolled_head_sha set to the SHA you pushed.`);
|
|
64
|
+
}
|
|
65
|
+
else if (reason === "unread") {
|
|
66
|
+
parts.push("This tool did not read the stack's state; stack_wait will.");
|
|
67
|
+
}
|
|
68
|
+
parts.push(`Next call: stack_wait ${JSON.stringify(args)}.`);
|
|
69
|
+
if (input.freshCursor) {
|
|
70
|
+
parts.push("If you already hold a cursor from an earlier stack_wait on this stack, keep passing that cursor instead.");
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
done: false,
|
|
74
|
+
until: "landed",
|
|
75
|
+
reason,
|
|
76
|
+
next: { tool: "stack_wait", args, command: `mg stack wait ${input.stackId} --json` },
|
|
77
|
+
message: parts.join(" "),
|
|
78
|
+
};
|
|
79
|
+
}
|
package/dist/stack-watch.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type StackAgentBusy, type StackBlocker, type StackLayerAgents } from "./stack-blockers.js";
|
|
1
|
+
import { type StackAgentBusy, type StackBlocker, type StackBusyAgent, type StackHeldBlocker, type StackLayerAgents, type StackLandGatePending, type StackRepairHint } from "./stack-blockers.js";
|
|
2
|
+
import { type StackWatchObligation } from "./stack-watch-obligation.js";
|
|
2
3
|
import { apiFetch } from "./api.js";
|
|
3
4
|
import type { Config } from "./config.js";
|
|
4
5
|
export declare const STACK_WATCH_SCHEMA: "mergestorm.stack_watch/v1";
|
|
@@ -27,7 +28,12 @@ export type StackWatchEnvelope = {
|
|
|
27
28
|
} | null;
|
|
28
29
|
assessment: "available" | "unavailable";
|
|
29
30
|
busy: StackAgentBusy[];
|
|
31
|
+
actAfter: StackHeldBlocker["actAfter"] | null;
|
|
32
|
+
waitingOn: StackBusyAgent[];
|
|
30
33
|
agents: StackLayerAgents | null;
|
|
34
|
+
repair: StackRepairHint | null;
|
|
35
|
+
landGatePending: StackLandGatePending | null;
|
|
36
|
+
watch: StackWatchObligation;
|
|
31
37
|
};
|
|
32
38
|
export type PollStackWatchOptions = {
|
|
33
39
|
timeoutMs?: number;
|
package/dist/stack-watch.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { currentLayer, layerAgents, stackBlockers, } from "./stack-blockers.js";
|
|
2
|
+
import { stackTerminalReason, stackWatchObligation, } from "./stack-watch-obligation.js";
|
|
2
3
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
3
4
|
import { apiFetch } from "./api.js";
|
|
4
5
|
import { REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, isTransientReviewPollError, isTransientReviewPollStatus, transientRetryWaitMs, } from "./commands/review-client.js";
|
|
@@ -47,10 +48,20 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
47
48
|
let lastEnvelope = {
|
|
48
49
|
schema: STACK_WATCH_SCHEMA, status: "failed", stackId: id, blocker: null,
|
|
49
50
|
bounceKind: null, prNumber: null, headSha: null, cursor,
|
|
50
|
-
issues: [], currentCandidate: null, assessment: "unavailable", busy: [], agents: null,
|
|
51
|
+
issues: [], currentCandidate: null, assessment: "unavailable", busy: [], actAfter: null, waitingOn: [], agents: null, repair: null,
|
|
52
|
+
landGatePending: null,
|
|
51
53
|
};
|
|
54
|
+
let terminal = null;
|
|
55
|
+
const seal = (snapshot, retryAfterSeconds) => ({
|
|
56
|
+
...snapshot,
|
|
57
|
+
watch: stackWatchObligation({
|
|
58
|
+
stackId: id, terminal, cursor: snapshot.cursor, status: snapshot.status, retryAfterSeconds,
|
|
59
|
+
attention: snapshot.status === "attention" && snapshot.blocker && snapshot.prNumber != null
|
|
60
|
+
? { prNumber: snapshot.prNumber, blocker: snapshot.blocker } : null,
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
52
63
|
let transientFailures = 0;
|
|
53
|
-
const timeout = () => new StackWatchTimeoutError(lastEnvelope);
|
|
64
|
+
const timeout = () => new StackWatchTimeoutError(seal(lastEnvelope));
|
|
54
65
|
const pause = async (ms) => {
|
|
55
66
|
const remaining = Math.min(ms, Math.max(0, deadline - now()));
|
|
56
67
|
if (remaining > 0)
|
|
@@ -76,11 +87,11 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
76
87
|
throw timeout();
|
|
77
88
|
if (timeoutMs !== 0 && isTransientReviewPollError(err) && transientFailures < REVIEW_POLL_MAX_TRANSIENT_RETRIES) {
|
|
78
89
|
transientFailures += 1;
|
|
79
|
-
opts.onTick?.(lastEnvelope);
|
|
90
|
+
opts.onTick?.(seal(lastEnvelope));
|
|
80
91
|
await pause(transientRetryWaitMs({ failureCount: transientFailures, random: opts.random }));
|
|
81
92
|
continue;
|
|
82
93
|
}
|
|
83
|
-
throw new StackWatchError(err instanceof Error ? err.message : String(err), { ...lastEnvelope, status: "failed", assessment: "unavailable" }, { cause: err });
|
|
94
|
+
throw new StackWatchError(err instanceof Error ? err.message : String(err), seal({ ...lastEnvelope, status: "failed", assessment: "unavailable" }), { cause: err });
|
|
84
95
|
}
|
|
85
96
|
if (response.status === 200)
|
|
86
97
|
return response.body;
|
|
@@ -90,14 +101,14 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
90
101
|
if (timeoutMs !== 0 && isTransientReviewPollStatus(response.status) && transientFailures < REVIEW_POLL_MAX_TRANSIENT_RETRIES &&
|
|
91
102
|
(response.status !== 429 || retryWaitMs < deadline - now())) {
|
|
92
103
|
transientFailures += 1;
|
|
93
|
-
opts.onTick?.({ ...lastEnvelope, status: response.status === 429 ? "rate_limited" : lastEnvelope.status });
|
|
104
|
+
opts.onTick?.(seal({ ...lastEnvelope, status: response.status === 429 ? "rate_limited" : lastEnvelope.status }, response.status === 429 ? response.retryAfterSeconds : undefined));
|
|
94
105
|
await pause(retryWaitMs);
|
|
95
106
|
continue;
|
|
96
107
|
}
|
|
97
|
-
throw new StackWatchError(`Stack poll failed (HTTP ${response.status})`, {
|
|
108
|
+
throw new StackWatchError(`Stack poll failed (HTTP ${response.status})`, seal({
|
|
98
109
|
...lastEnvelope, status: response.status === 429 ? "rate_limited" : "failed",
|
|
99
110
|
assessment: "unavailable",
|
|
100
|
-
}, undefined, response.retryAfterSeconds);
|
|
111
|
+
}, response.retryAfterSeconds), undefined, response.retryAfterSeconds);
|
|
101
112
|
}
|
|
102
113
|
throw timeout();
|
|
103
114
|
};
|
|
@@ -106,16 +117,22 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
106
117
|
const stack = snapshot && Array.isArray(snapshot.stacks)
|
|
107
118
|
? snapshot.stacks.find((candidate) => candidate.id.trim().toLowerCase() === id)
|
|
108
119
|
: undefined;
|
|
120
|
+
if (snapshot && Array.isArray(snapshot.stacks) && !stack) {
|
|
121
|
+
terminal = "not_found";
|
|
122
|
+
throw new StackWatchError("Stack not found or not owned by the current user", seal({ ...lastEnvelope, status: "failed", assessment: "unavailable", blocker: null, issues: [], busy: [], actAfter: null, waitingOn: [],
|
|
123
|
+
prNumber: null, headSha: null, repair: null, agents: null, currentCandidate: null, landGatePending: null }));
|
|
124
|
+
}
|
|
109
125
|
if (!stack || !Array.isArray(stack.layers)) {
|
|
110
|
-
throw new StackWatchError("Stack snapshot missing or invalid", { ...lastEnvelope, status: "failed", assessment: "unavailable" });
|
|
126
|
+
throw new StackWatchError("Stack snapshot missing or invalid", seal({ ...lastEnvelope, status: "failed", assessment: "unavailable" }));
|
|
111
127
|
}
|
|
128
|
+
terminal = stackTerminalReason(stack);
|
|
112
129
|
return stack;
|
|
113
130
|
};
|
|
114
131
|
const queueRoute = `/api/v1/stacks/queue?stackId=${encodeURIComponent(id)}`;
|
|
115
132
|
const readQueue = async (route, finalSnapshot = false) => {
|
|
116
133
|
const queue = await request(route, finalSnapshot);
|
|
117
134
|
if (!queue || !Array.isArray(queue.entries)) {
|
|
118
|
-
throw new StackWatchError("Invalid merge queue response", { ...lastEnvelope, status: "failed", assessment: "unavailable" });
|
|
135
|
+
throw new StackWatchError("Invalid merge queue response", seal({ ...lastEnvelope, status: "failed", assessment: "unavailable" }));
|
|
119
136
|
}
|
|
120
137
|
return {
|
|
121
138
|
entries: queue.entries.filter((entry) => entry.stackId.trim().toLowerCase() === id),
|
|
@@ -123,16 +140,18 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
123
140
|
};
|
|
124
141
|
};
|
|
125
142
|
const evaluate = (stack, entries) => {
|
|
126
|
-
const { attention, issues, currentCandidate, bounce, busy, agents } = stackBlockers(stack, entries, cursor);
|
|
143
|
+
const { attention, held: named, issues, currentCandidate, bounce, busy, agents, repair, landGatePending } = stackBlockers(stack, entries, cursor);
|
|
127
144
|
if (bounce)
|
|
128
145
|
cursor = Object.freeze({ ...cursor, bounceId: bounce.id,
|
|
129
146
|
...(bounce.finishedAt !== undefined ? { afterFinishedAt: bounce.finishedAt } : {}) });
|
|
130
147
|
const held = busy[0];
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
148
|
+
const shown = attention ?? named;
|
|
149
|
+
lastEnvelope = { ...lastEnvelope, cursor, issues, currentCandidate, assessment: "available", busy, agents, repair, landGatePending,
|
|
150
|
+
actAfter: named?.actAfter ?? null, waitingOn: named?.waitingOn ?? [],
|
|
151
|
+
blocker: shown?.blocker ?? null, bounceKind: shown?.bounceKind ?? null,
|
|
152
|
+
prNumber: shown?.prNumber ?? held?.prNumber ?? currentCandidate?.prNumber ?? null,
|
|
153
|
+
headSha: shown ? shown.headSha : held ? held.headSha : currentCandidate?.headSha ?? null,
|
|
154
|
+
status: attention ? "attention" : busy.length || landGatePending || entries.some((entry) => ["queued", "running", "waiting"].includes(entry.state))
|
|
136
155
|
? "in_progress" : "waiting" };
|
|
137
156
|
};
|
|
138
157
|
let stack = await enrich(timeoutMs === 0);
|
|
@@ -145,8 +164,8 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
145
164
|
agents: layer ? layerAgents(layer) : null };
|
|
146
165
|
let queue = await readQueue(queueRoute, timeoutMs === 0);
|
|
147
166
|
evaluate(stack, queue.entries);
|
|
148
|
-
if (lastEnvelope.status === "attention" || timeoutMs === 0)
|
|
149
|
-
return lastEnvelope;
|
|
167
|
+
if (lastEnvelope.status === "attention" || timeoutMs === 0 || terminal)
|
|
168
|
+
return seal(lastEnvelope);
|
|
150
169
|
while (now() < deadline) {
|
|
151
170
|
const heldAt = now();
|
|
152
171
|
const seconds = Math.min(lastEnvelope.busy.length ? STACK_WATCH_BUSY_RECHECK_S : 45, Math.max(1, Math.ceil((deadline - heldAt) / 1000)));
|
|
@@ -162,9 +181,9 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
162
181
|
agents: refreshed ? layerAgents(refreshed) : null };
|
|
163
182
|
transientFailures = 0;
|
|
164
183
|
evaluate(stack, queue.entries);
|
|
165
|
-
if (lastEnvelope.status === "attention")
|
|
166
|
-
return lastEnvelope;
|
|
167
|
-
opts.onTick?.(lastEnvelope);
|
|
184
|
+
if (lastEnvelope.status === "attention" || terminal)
|
|
185
|
+
return seal(lastEnvelope);
|
|
186
|
+
opts.onTick?.(seal(lastEnvelope));
|
|
168
187
|
}
|
|
169
188
|
throw timeout();
|
|
170
189
|
}
|
package/dist/ui/ansi.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand ink. xterm-256 77 (#5FAF5F) � muted green that Terminal.app
|
|
3
|
+
* actually paints. 24-bit `38;2;61;�` was read as palette 61 (purple).
|
|
4
|
+
*/
|
|
5
|
+
export declare const BRAND_GREEN: {
|
|
6
|
+
readonly hex: "#5FAF5F";
|
|
7
|
+
readonly index: 77;
|
|
8
|
+
};
|
|
9
|
+
export declare const BRAND_GREEN_SGR: string;
|
|
10
|
+
/** Full-cell fill. Glyphs like ? leave a hairline; a space with this bg does not. */
|
|
11
|
+
export declare const BRAND_FILL_SGR: string;
|
|
1
12
|
export declare const ansi: {
|
|
2
13
|
readonly enabled: boolean;
|
|
3
14
|
green: (t: string) => string;
|
|
@@ -7,8 +18,10 @@ export declare const ansi: {
|
|
|
7
18
|
yellow: (t: string) => string;
|
|
8
19
|
gray: (t: string) => string;
|
|
9
20
|
brightGreen: (t: string) => string;
|
|
21
|
+
/** Solid cell (space + fg/bg 77). Stacks flush � no ? hairline. */
|
|
22
|
+
brandFill: (t: string) => string;
|
|
10
23
|
invert: (t: string) => string;
|
|
11
24
|
boldGreen: (t: string) => string;
|
|
12
|
-
/** Prompt / mark ink. Never 32
|
|
25
|
+
/** Prompt / mark ink. Never ANSI 32, 92, or 24-bit. */
|
|
13
26
|
boldBrightGreen: (t: string) => string;
|
|
14
27
|
};
|
package/dist/ui/ansi.js
CHANGED
|
@@ -9,7 +9,25 @@ function computeEnabled() {
|
|
|
9
9
|
function wrap(code, text) {
|
|
10
10
|
if (!computeEnabled())
|
|
11
11
|
return text;
|
|
12
|
-
return
|
|
12
|
+
return `\x1b[${code}m${text}\x1b[0m`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Brand ink. xterm-256 77 (#5FAF5F) � muted green that Terminal.app
|
|
16
|
+
* actually paints. 24-bit `38;2;61;�` was read as palette 61 (purple).
|
|
17
|
+
*/
|
|
18
|
+
export const BRAND_GREEN = { hex: "#5FAF5F", index: 77 };
|
|
19
|
+
export const BRAND_GREEN_SGR = `38;5;${BRAND_GREEN.index}`;
|
|
20
|
+
/** Full-cell fill. Glyphs like ? leave a hairline; a space with this bg does not. */
|
|
21
|
+
export const BRAND_FILL_SGR = `${BRAND_GREEN_SGR};48;5;${BRAND_GREEN.index}`;
|
|
22
|
+
function brandGreen(text, bold = false) {
|
|
23
|
+
if (!computeEnabled())
|
|
24
|
+
return text;
|
|
25
|
+
return `\x1b[${bold ? "1;" : ""}${BRAND_GREEN_SGR}m${text}\x1b[0m`;
|
|
26
|
+
}
|
|
27
|
+
function brandFill(text) {
|
|
28
|
+
if (!computeEnabled())
|
|
29
|
+
return text;
|
|
30
|
+
return `\x1b[${BRAND_FILL_SGR}m${text}\x1b[0m`;
|
|
13
31
|
}
|
|
14
32
|
export const ansi = {
|
|
15
33
|
get enabled() {
|
|
@@ -21,9 +39,11 @@ export const ansi = {
|
|
|
21
39
|
red: (t) => wrap("31", t),
|
|
22
40
|
yellow: (t) => wrap("33", t),
|
|
23
41
|
gray: (t) => wrap("90", t),
|
|
24
|
-
brightGreen: (t) =>
|
|
42
|
+
brightGreen: (t) => brandGreen(t),
|
|
43
|
+
/** Solid cell (space + fg/bg 77). Stacks flush � no ? hairline. */
|
|
44
|
+
brandFill: (t) => brandFill(t),
|
|
25
45
|
invert: (t) => wrap("7", t),
|
|
26
|
-
boldGreen: (t) => (computeEnabled() ?
|
|
27
|
-
/** Prompt / mark ink. Never 32
|
|
28
|
-
boldBrightGreen: (t) => (
|
|
46
|
+
boldGreen: (t) => (computeEnabled() ? `\x1b[1;32m${t}\x1b[0m` : t),
|
|
47
|
+
/** Prompt / mark ink. Never ANSI 32, 92, or 24-bit. */
|
|
48
|
+
boldBrightGreen: (t) => brandGreen(t, true),
|
|
29
49
|
};
|