mixdog 0.9.41 → 0.9.44
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 +3 -2
- package/scripts/agent-tag-reuse-smoke.mjs +124 -10
- package/scripts/ansi-color-capability-test.mjs +90 -0
- package/scripts/anthropic-oauth-refresh-race-test.mjs +59 -0
- package/scripts/arg-guard-test.mjs +16 -0
- package/scripts/compact-pressure-test.mjs +256 -1
- package/scripts/generate-runtime-manifest.mjs +39 -22
- package/scripts/grok-oauth-refresh-race-test.mjs +198 -0
- package/scripts/internal-comms-bench.mjs +0 -1
- package/scripts/internal-comms-smoke.mjs +38 -39
- package/scripts/internal-tools-normalization-test.mjs +52 -0
- package/scripts/maintenance-default-routes-test.mjs +164 -0
- package/scripts/max-output-recovery-test.mjs +49 -0
- package/scripts/mcp-client-normalization-test.mjs +45 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +123 -0
- package/scripts/provider-toolcall-test.mjs +23 -0
- package/scripts/result-classification-test.mjs +75 -0
- package/scripts/routing-corpus.mjs +1 -1
- package/scripts/shell-jobs-windows-hide-test.mjs +164 -0
- package/scripts/smoke.mjs +1 -1
- package/scripts/submit-commandbusy-race-test.mjs +99 -7
- package/scripts/tui-transcript-jitter-harness-entry.jsx +302 -0
- package/scripts/tui-transcript-jitter-harness.mjs +58 -0
- package/scripts/tui-transcript-perf-test.mjs +56 -1
- package/src/agents/reviewer/AGENT.md +5 -3
- package/src/rules/lead/lead-brief.md +5 -4
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +40 -3
- package/src/runtime/agent/orchestrator/config.mjs +29 -14
- package/src/runtime/agent/orchestrator/internal-tools.mjs +16 -3
- package/src/runtime/agent/orchestrator/mcp/client.mjs +17 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +49 -6
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +14 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +46 -21
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +59 -2
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +140 -81
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +23 -5
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +4 -2
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +8 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -5
- package/src/runtime/memory/data/runtime-manifest.json +11 -11
- package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -2
- package/src/runtime/memory/tool-defs.mjs +4 -3
- package/src/runtime/shared/tool-surface.mjs +1 -2
- package/src/session-runtime/context-status.mjs +8 -2
- package/src/standalone/agent-tool/render.mjs +2 -0
- package/src/standalone/agent-tool.mjs +267 -72
- package/src/standalone/explore-tool.mjs +17 -16
- package/src/tui/app/provider-setup-picker.mjs +1 -0
- package/src/tui/app/use-transcript-window.mjs +5 -1
- package/src/tui/components/StatusLine.jsx +11 -32
- package/src/tui/dist/index.mjs +257 -106
- package/src/tui/engine/session-api.mjs +3 -0
- package/src/tui/engine/session-flow.mjs +19 -8
- package/src/tui/engine/turn.mjs +24 -2
- package/src/tui/engine.mjs +0 -1
- package/src/tui/index.jsx +3 -2
- package/src/tui/markdown/streaming-markdown.mjs +35 -9
- package/src/tui/statusline-ansi-bridge.mjs +17 -7
- package/src/ui/ansi.mjs +85 -5
- package/src/ui/statusline-agents.mjs +0 -8
- package/src/ui/statusline-format.mjs +7 -7
- package/src/ui/statusline.mjs +2 -4
- package/src/workflows/default/WORKFLOW.md +29 -20
- package/src/workflows/solo-review/WORKFLOW.md +47 -0
- package/src/workflows/bench/WORKFLOW.md +0 -60
|
@@ -516,6 +516,9 @@ export function createEngineApiA(bag) {
|
|
|
516
516
|
syncContextStats({ allowEstimated: true });
|
|
517
517
|
set({ ...routeState(), stats: { ...getState().stats } });
|
|
518
518
|
if (result) {
|
|
519
|
+
if (!result.error && result.changed !== false) {
|
|
520
|
+
set({ items: replaceItems([]) });
|
|
521
|
+
}
|
|
519
522
|
pushItem({
|
|
520
523
|
kind: 'statusdone',
|
|
521
524
|
id: nextId(),
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* src/tui/engine/session-flow.mjs - prompt queue drain + session clear/reset. Extracted from engine.mjs.
|
|
3
3
|
*/
|
|
4
4
|
import { presentErrorText } from '../../runtime/shared/err-text.mjs';
|
|
5
|
+
import { resetAllStreamingMarkdownStablePrefixes } from '../markdown/streaming-markdown.mjs';
|
|
5
6
|
import { createSessionStats } from './session-stats.mjs';
|
|
6
7
|
import { queuePriorityValue, defaultQueuePriority, isQueuedEntryEditable, isQueuedEntryVisible, isSlashQueuedEntry, notificationDisplayText, sessionActivityTimestamp, promptDisplayText, mergePromptContents, mergePastedImages, mergePastedTexts, callCommitCallbacks, STEERING_SUPPRESSED_DISPLAY } from './queue-helpers.mjs';
|
|
7
8
|
import { appendTuiSteeringPersist, dropTuiSteeringPersist, drainTuiSteeringPersist } from './tui-steering-persist.mjs';
|
|
8
9
|
|
|
9
10
|
export function createSessionFlow(bag) {
|
|
10
11
|
const {
|
|
11
|
-
runtime, nextId, tuiDebug, flags, pending, pendingNotificationKeys, displayedExecutionNotificationKeys, clearExecutionDedupState, getState, set, pushItem, replaceItems, pushNotice, pushUserOrSyntheticItem, autoClearState, agentStatusState, routeState, syncContextStats, flushDeferredExecutionPendingResumeKick,
|
|
12
|
+
runtime, nextId, tuiDebug, flags, pending, pendingNotificationKeys, displayedExecutionNotificationKeys, clearExecutionDedupState, clearToastTimers, getState, set, pushItem, replaceItems, pushNotice, pushUserOrSyntheticItem, autoClearState, agentStatusState, routeState, syncContextStats, flushDeferredExecutionPendingResumeKick,
|
|
12
13
|
} = bag;
|
|
13
14
|
|
|
14
15
|
// Upper bound on the awaited compacting clear. requireCompactSuccess makes
|
|
@@ -382,7 +383,10 @@ export function createSessionFlow(bag) {
|
|
|
382
383
|
// Shared clear body for idle auto-clear and the session_manage tool.
|
|
383
384
|
// useCompaction=true mirrors auto-clear (summarize via configured
|
|
384
385
|
// compactType, context carries forward); false is a plain /clear wipe.
|
|
385
|
-
async function performSessionClear({
|
|
386
|
+
async function performSessionClear({
|
|
387
|
+
verb, doneLabel, skipLabel, surface, useCompaction,
|
|
388
|
+
compactTimeoutMs = AUTO_CLEAR_COMPACT_TIMEOUT_MS,
|
|
389
|
+
}) {
|
|
386
390
|
flags.autoClearRunning = true;
|
|
387
391
|
const startedAt = Date.now();
|
|
388
392
|
// commandBusy blocks concurrent session commands (resume/newSession/
|
|
@@ -401,6 +405,7 @@ export function createSessionFlow(bag) {
|
|
|
401
405
|
const compaction = runtime.getCompactionSettings();
|
|
402
406
|
compactType = compaction.compactType || compaction.type || null;
|
|
403
407
|
}
|
|
408
|
+
let clearResult;
|
|
404
409
|
if (compactType) {
|
|
405
410
|
// Bounded watchdog around the compacting clear. On timeout we throw so
|
|
406
411
|
// the catch below keeps the conversation, surfaces a user-visible
|
|
@@ -410,21 +415,22 @@ export function createSessionFlow(bag) {
|
|
|
410
415
|
// new auto-clear attempts until the abandoned promise settles, and on
|
|
411
416
|
// late fulfillment we run the same post-success UI sync as the normal
|
|
412
417
|
// path so the UI cannot diverge from a runtime session that actually
|
|
413
|
-
// got cleared. Late rejection is a no-op.
|
|
418
|
+
// got cleared. Late rejection or a false result is a no-op.
|
|
414
419
|
const clearPromise = runtime.clear({ compactType, requireCompactSuccess: true });
|
|
415
420
|
let timer = null;
|
|
416
421
|
const timeout = new Promise((_, reject) => {
|
|
417
422
|
timer = setTimeout(
|
|
418
|
-
() => reject(new Error(`compaction timed out after ${
|
|
419
|
-
|
|
423
|
+
() => reject(new Error(`compaction timed out after ${compactTimeoutMs}ms; auto-clear deferred to next idle`)),
|
|
424
|
+
compactTimeoutMs,
|
|
420
425
|
);
|
|
421
426
|
});
|
|
422
427
|
try {
|
|
423
|
-
await Promise.race([clearPromise, timeout]);
|
|
428
|
+
clearResult = await Promise.race([clearPromise, timeout]);
|
|
424
429
|
} catch (raceError) {
|
|
425
430
|
flags.autoClearInFlight = true;
|
|
426
431
|
clearPromise.then(
|
|
427
|
-
() => {
|
|
432
|
+
(lateResult) => {
|
|
433
|
+
if (lateResult === false) return;
|
|
428
434
|
if (getState().busy) {
|
|
429
435
|
// A turn started after commandBusy released; applying the
|
|
430
436
|
// cleared-session UI now would wipe items/queued and force
|
|
@@ -446,7 +452,10 @@ export function createSessionFlow(bag) {
|
|
|
446
452
|
if (timer) clearTimeout(timer);
|
|
447
453
|
}
|
|
448
454
|
} else {
|
|
449
|
-
await runtime.clear({});
|
|
455
|
+
clearResult = await runtime.clear({});
|
|
456
|
+
}
|
|
457
|
+
if (clearResult === false) {
|
|
458
|
+
throw new Error('runtime clear returned false');
|
|
450
459
|
}
|
|
451
460
|
applyClearedSessionUi(doneLabel);
|
|
452
461
|
return true;
|
|
@@ -490,6 +499,8 @@ export function createSessionFlow(bag) {
|
|
|
490
499
|
return getState().stats;
|
|
491
500
|
};
|
|
492
501
|
const clearUiActivityBeforeContextSync = () => {
|
|
502
|
+
clearToastTimers();
|
|
503
|
+
resetAllStreamingMarkdownStablePrefixes();
|
|
493
504
|
getState().items = replaceItems([]);
|
|
494
505
|
getState().toasts = [];
|
|
495
506
|
getState().queued = [];
|
package/src/tui/engine/turn.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { aggregateRawResult, aggregateBucketForCategory, aggregateSummaries, ass
|
|
|
12
12
|
|
|
13
13
|
export function createRunTurn(bag) {
|
|
14
14
|
const {
|
|
15
|
-
runtime, nextId, tuiDebug, LEAD_TURN_TIMEOUT_MS, flags, pending, itemIndexById, getState, set, pushItem, patchItem, updateStreamingTail: updateStreamingTailFromStore, settleStreamingTail: settleStreamingTailFromStore, clearStreamingTail: clearStreamingTailFromStore, pushNotice, pushUserOrSyntheticItem, markToolCallActive, markToolCallDone, clearActiveToolSummary, agentStatusState, routeState, syncContextStats, denyAllToolApprovals, requestToolApproval, patchToolCardResult, flushToolResults, flushDeferredExecutionPendingResumeKick, drain, drainPendingSteering,
|
|
15
|
+
runtime, nextId, tuiDebug, LEAD_TURN_TIMEOUT_MS, flags, pending, itemIndexById, getState, set, pushItem, patchItem, replaceItems, updateStreamingTail: updateStreamingTailFromStore, settleStreamingTail: settleStreamingTailFromStore, clearStreamingTail: clearStreamingTailFromStore, pushNotice, pushUserOrSyntheticItem, markToolCallActive, markToolCallDone, clearActiveToolSummary, agentStatusState, routeState, syncContextStats, denyAllToolApprovals, requestToolApproval, patchToolCardResult, flushToolResults, flushDeferredExecutionPendingResumeKick, drain, drainPendingSteering,
|
|
16
16
|
} = bag;
|
|
17
17
|
// Small fallbacks keep isolated createRunTurn harnesses source-compatible;
|
|
18
18
|
// the real engine supplies atomic implementations that also maintain revision.
|
|
@@ -209,7 +209,14 @@ export function createRunTurn(bag) {
|
|
|
209
209
|
let cancelled = false;
|
|
210
210
|
let askResult = null;
|
|
211
211
|
let turnFinishedNormally = false;
|
|
212
|
+
let transcriptCompactedThisTurn = false;
|
|
212
213
|
const itemsAtTurnStart = getState().items.length;
|
|
214
|
+
const submittedIdSet = new Set(submittedIds.filter((id) => id != null));
|
|
215
|
+
const firstSubmittedIndex = getState().items.findIndex((item) => submittedIdSet.has(item?.id));
|
|
216
|
+
// The submitted user row is pushed immediately before runTurn. Keep it and
|
|
217
|
+
// everything produced after it if compaction succeeds mid-turn; dropping all
|
|
218
|
+
// items would invalidate live tool-card ids and the streaming assistant tail.
|
|
219
|
+
let currentTurnItemsStart = firstSubmittedIndex >= 0 ? firstSubmittedIndex : itemsAtTurnStart;
|
|
213
220
|
const cardByCallId = new Map();
|
|
214
221
|
const toolCards = [];
|
|
215
222
|
const toolGroups = new Map();
|
|
@@ -987,6 +994,13 @@ export function createRunTurn(bag) {
|
|
|
987
994
|
// later same-category tool call doesn't reuse a card whose count
|
|
988
995
|
// would then change above this statusdone item.
|
|
989
996
|
clearAggregateContinuation();
|
|
997
|
+
const compactStatus = String(event?.status || '').toLowerCase();
|
|
998
|
+
if (!['failed', 'skipped', 'no_change'].includes(compactStatus)) {
|
|
999
|
+
const currentTurnItems = getState().items.slice(currentTurnItemsStart);
|
|
1000
|
+
set({ items: replaceItems(currentTurnItems, { preserveStreamingTail: true }) });
|
|
1001
|
+
currentTurnItemsStart = 0;
|
|
1002
|
+
transcriptCompactedThisTurn = true;
|
|
1003
|
+
}
|
|
990
1004
|
pushItem({
|
|
991
1005
|
kind: 'statusdone',
|
|
992
1006
|
id: nextId(),
|
|
@@ -1103,6 +1117,13 @@ export function createRunTurn(bag) {
|
|
|
1103
1117
|
} else {
|
|
1104
1118
|
askResult = result;
|
|
1105
1119
|
markPromptCommitted();
|
|
1120
|
+
if (result?.terminationReason === 'refusal') {
|
|
1121
|
+
pushNotice(
|
|
1122
|
+
'The model refused to respond (safety refusal) — retry or rephrase your prompt.',
|
|
1123
|
+
'warn',
|
|
1124
|
+
{ transcript: true },
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1106
1127
|
|
|
1107
1128
|
flushToolResults(session?.messages || [], toolCards, cardByCallId, toolGroups, resultsDone, { finalize: true });
|
|
1108
1129
|
finalizeToolHeaders();
|
|
@@ -1208,7 +1229,8 @@ export function createRunTurn(bag) {
|
|
|
1208
1229
|
}
|
|
1209
1230
|
}
|
|
1210
1231
|
const producedTranscriptItem =
|
|
1211
|
-
|
|
1232
|
+
transcriptCompactedThisTurn
|
|
1233
|
+
|| getState().items.length + closingItems.length > itemsAtTurnStart;
|
|
1212
1234
|
const reclaimed = cancelled && flags.activePromptRestore?.reclaimed === true;
|
|
1213
1235
|
flags.activePromptRestore = null;
|
|
1214
1236
|
const elapsedMs = Date.now() - startedAt;
|
package/src/tui/engine.mjs
CHANGED
|
@@ -738,7 +738,6 @@ export async function createEngineSession({
|
|
|
738
738
|
autoClearState, agentStatusState, baseRouteState, routeState, syncContextStats,
|
|
739
739
|
presentNextToolApproval, finishToolApproval, denyAllToolApprovals, requestToolApproval,
|
|
740
740
|
patchToolCardResult, flushToolResults,
|
|
741
|
-
clearExecutionDedupState,
|
|
742
741
|
kickExecutionPendingResume, flushDeferredExecutionPendingResumeKick, scheduleExecutionPendingResumeKick, discardExecutionPendingResume, updateAgentJobCard, subscribeRuntimeNotifications,
|
|
743
742
|
});
|
|
744
743
|
Object.assign(bag, createSessionFlow(bag));
|
package/src/tui/index.jsx
CHANGED
|
@@ -15,6 +15,7 @@ import { App } from './App.jsx';
|
|
|
15
15
|
import { createEngineSession } from './engine.mjs';
|
|
16
16
|
import { scheduleRenderFrameAck } from './engine/render-timing.mjs';
|
|
17
17
|
import { installProcessSignalCleanup } from '../runtime/shared/process-shutdown.mjs';
|
|
18
|
+
import { rgbSgr } from '../ui/ansi.mjs';
|
|
18
19
|
import { emitTerminalBackground, loadThemeSettingFromConfig, theme } from './theme.mjs';
|
|
19
20
|
import { POP_KITTY, DISABLE_MODIFY_OTHER_KEYS } from './keyboard-protocol.mjs';
|
|
20
21
|
import { displayWidth } from './display-width.mjs';
|
|
@@ -259,10 +260,10 @@ function resolveTuiStderrLogPath() {
|
|
|
259
260
|
|| join(process.env.MIXDOG_RUNTIME_ROOT || join(tmpdir(), 'mixdog'), 'mixdog-tui.stderr.log');
|
|
260
261
|
}
|
|
261
262
|
|
|
262
|
-
/** `rgb(r,g,b)` →
|
|
263
|
+
/** `rgb(r,g,b)` → supported RGB SGR prefix ('' when unparsable). */
|
|
263
264
|
function ansiFg(rgb) {
|
|
264
265
|
const m = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/.exec(String(rgb || ''));
|
|
265
|
-
return m ?
|
|
266
|
+
return m ? rgbSgr(m[1], m[2], m[3]) : '';
|
|
266
267
|
}
|
|
267
268
|
|
|
268
269
|
/**
|
|
@@ -7,6 +7,8 @@ import { configureMarked, hasMarkdownSyntax } from './render-ansi.mjs';
|
|
|
7
7
|
import { trimPartialClosingFences, findOpenFenceStart } from './stream-fence.mjs';
|
|
8
8
|
|
|
9
9
|
const stablePrefixByStreamKey = new Map();
|
|
10
|
+
// Reuse the current normalized-text split across measure → render → harvest.
|
|
11
|
+
const resolvedPartsByStreamKey = new Map();
|
|
10
12
|
const STABLE_PREFIX_LRU_MAX = 32;
|
|
11
13
|
|
|
12
14
|
/** Lockstep with App streaming row estimate (leading/trailing newline trim). */
|
|
@@ -37,6 +39,25 @@ function getStablePrefixKey(key) {
|
|
|
37
39
|
return value;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
function getResolvedPartsKey(key, text) {
|
|
43
|
+
if (!key) return null;
|
|
44
|
+
const entry = resolvedPartsByStreamKey.get(key);
|
|
45
|
+
if (!entry || entry.text !== text) return null;
|
|
46
|
+
return entry.parts;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function cacheResolvedPartsKey(key, text, parts) {
|
|
50
|
+
if (!key) return parts;
|
|
51
|
+
if (resolvedPartsByStreamKey.has(key)) resolvedPartsByStreamKey.delete(key);
|
|
52
|
+
resolvedPartsByStreamKey.set(key, { text, parts });
|
|
53
|
+
while (resolvedPartsByStreamKey.size > STABLE_PREFIX_LRU_MAX) {
|
|
54
|
+
const oldest = resolvedPartsByStreamKey.keys().next().value;
|
|
55
|
+
if (oldest === undefined) break;
|
|
56
|
+
resolvedPartsByStreamKey.delete(oldest);
|
|
57
|
+
}
|
|
58
|
+
return parts;
|
|
59
|
+
}
|
|
60
|
+
|
|
40
61
|
function hasOpenFence(text) {
|
|
41
62
|
let ticks = 0;
|
|
42
63
|
let tildes = 0;
|
|
@@ -93,35 +114,40 @@ export function balanceStreamingMarkdown(text) {
|
|
|
93
114
|
|
|
94
115
|
export function resetStreamingMarkdownStablePrefix(streamKey) {
|
|
95
116
|
if (streamKey == null || streamKey === '') return;
|
|
96
|
-
|
|
117
|
+
const key = String(streamKey);
|
|
118
|
+
stablePrefixByStreamKey.delete(key);
|
|
119
|
+
resolvedPartsByStreamKey.delete(key);
|
|
97
120
|
}
|
|
98
121
|
|
|
99
122
|
export function resetAllStreamingMarkdownStablePrefixes() {
|
|
100
123
|
stablePrefixByStreamKey.clear();
|
|
124
|
+
resolvedPartsByStreamKey.clear();
|
|
101
125
|
}
|
|
102
126
|
|
|
103
127
|
export function resolveStreamingMarkdownParts(text, streamKey) {
|
|
104
128
|
const t = streamingLayoutText(text);
|
|
105
129
|
const key = streamKey == null || streamKey === '' ? null : String(streamKey);
|
|
130
|
+
const cachedParts = getResolvedPartsKey(key, t);
|
|
131
|
+
if (cachedParts) return cachedParts;
|
|
106
132
|
|
|
107
133
|
if (!t) {
|
|
108
134
|
if (key) stablePrefixByStreamKey.delete(key);
|
|
109
|
-
return {
|
|
135
|
+
return cacheResolvedPartsKey(key, t, {
|
|
110
136
|
plain: true,
|
|
111
137
|
stablePrefix: '',
|
|
112
138
|
unstableSuffix: '',
|
|
113
139
|
unstableForRender: '',
|
|
114
|
-
};
|
|
140
|
+
});
|
|
115
141
|
}
|
|
116
142
|
|
|
117
143
|
if (!hasMarkdownSyntax(t)) {
|
|
118
144
|
if (key) stablePrefixByStreamKey.delete(key);
|
|
119
|
-
return {
|
|
145
|
+
return cacheResolvedPartsKey(key, t, {
|
|
120
146
|
plain: true,
|
|
121
147
|
stablePrefix: '',
|
|
122
148
|
unstableSuffix: t,
|
|
123
149
|
unstableForRender: t,
|
|
124
|
-
};
|
|
150
|
+
});
|
|
125
151
|
}
|
|
126
152
|
|
|
127
153
|
let stablePrefix = key ? getStablePrefixKey(key) : '';
|
|
@@ -141,13 +167,13 @@ export function resolveStreamingMarkdownParts(text, streamKey) {
|
|
|
141
167
|
if (key && openPrefix) touchStablePrefixKey(key, openPrefix);
|
|
142
168
|
else if (key) stablePrefixByStreamKey.delete(key);
|
|
143
169
|
const unstableSuffix = t.substring(openPrefix.length);
|
|
144
|
-
return {
|
|
170
|
+
return cacheResolvedPartsKey(key, t, {
|
|
145
171
|
plain: false,
|
|
146
172
|
openFence: true,
|
|
147
173
|
stablePrefix: openPrefix,
|
|
148
174
|
unstableSuffix,
|
|
149
175
|
unstableForRender: unstableSuffix,
|
|
150
|
-
};
|
|
176
|
+
});
|
|
151
177
|
}
|
|
152
178
|
|
|
153
179
|
try {
|
|
@@ -179,10 +205,10 @@ export function resolveStreamingMarkdownParts(text, streamKey) {
|
|
|
179
205
|
if (isWhitespaceOnlyText(stablePrefix)) stablePrefix = '';
|
|
180
206
|
|
|
181
207
|
const unstableSuffix = t.substring(stablePrefix.length);
|
|
182
|
-
return {
|
|
208
|
+
return cacheResolvedPartsKey(key, t, {
|
|
183
209
|
plain: false,
|
|
184
210
|
stablePrefix,
|
|
185
211
|
unstableSuffix,
|
|
186
212
|
unstableForRender: balanceStreamingMarkdown(unstableSuffix),
|
|
187
|
-
};
|
|
213
|
+
});
|
|
188
214
|
}
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* normalizes that at display time so `/theme` can re-tone without touching ui/.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { rgbToAnsi256 } from '../ui/ansi.mjs';
|
|
10
|
+
|
|
9
11
|
const RESET = '\x1b[0m';
|
|
10
12
|
|
|
11
13
|
/** Truecolor SGR sequences emitted by the shared statusline stack (not theme). */
|
|
@@ -23,6 +25,14 @@ function truecolorSgr(r, g, b) {
|
|
|
23
25
|
return `\x1b[38;2;${r};${g};${b}m`;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
function ansi256Sgr(r, g, b) {
|
|
29
|
+
return `\x1b[38;5;${rgbToAnsi256(r, g, b)}m`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function canonicalSgrVariants(rgb) {
|
|
33
|
+
return [truecolorSgr(...rgb), ansi256Sgr(...rgb)];
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
function footerLocalNum(value) {
|
|
27
37
|
const n = Number(value);
|
|
28
38
|
return Number.isFinite(n) ? n : 0;
|
|
@@ -139,13 +149,13 @@ export function applyDefaultStatusForegroundAfterReset(text, STATUS, reset = RES
|
|
|
139
149
|
export function remapCanonicalStatuslineTruecolor(text, colors) {
|
|
140
150
|
const c = STATUSLINE_CANONICAL_TRUECOLOR;
|
|
141
151
|
const pairs = [
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
152
|
+
...canonicalSgrVariants(c.statusText).map((from) => [from, colors.STATUS]),
|
|
153
|
+
...canonicalSgrVariants(c.subtle).map((from) => [from, colors.SUBTLE]),
|
|
154
|
+
...canonicalSgrVariants(c.success).map((from) => [from, colors.SUCCESS]),
|
|
155
|
+
...canonicalSgrVariants(c.successBright).map((from) => [from, colors.SUCCESS]),
|
|
156
|
+
...canonicalSgrVariants(c.warning).map((from) => [from, colors.WARNING]),
|
|
157
|
+
...canonicalSgrVariants(c.warningBright).map((from) => [from, colors.WARNING]),
|
|
158
|
+
...canonicalSgrVariants(c.error).map((from) => [from, colors.ERROR]),
|
|
149
159
|
];
|
|
150
160
|
let out = String(text || '');
|
|
151
161
|
for (const [from, to] of pairs) {
|
package/src/ui/ansi.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* The decision is computed once at import time but can be recomputed via
|
|
13
13
|
* `refreshColorSupport()` (used by tests / when output is rebound).
|
|
14
14
|
*/
|
|
15
|
-
import { stdout, env } from 'node:process';
|
|
15
|
+
import { stdout, env, platform } from 'node:process';
|
|
16
16
|
|
|
17
17
|
let COLOR_ENABLED = computeColorEnabled();
|
|
18
18
|
|
|
@@ -39,12 +39,92 @@ export function colorEnabled() {
|
|
|
39
39
|
const ESC = '\x1b[';
|
|
40
40
|
const RESET = `${ESC}0m`;
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Whether the terminal can render 24-bit SGR colors.
|
|
44
|
+
*
|
|
45
|
+
* Apple Terminal is the one explicit negative because it advertises a normal
|
|
46
|
+
* xterm TERM while not implementing truecolor. Unknown terminals retain the
|
|
47
|
+
* historical truecolor default.
|
|
48
|
+
*/
|
|
49
|
+
export function supportsTruecolor(environment = env, platformName = platform) {
|
|
50
|
+
const termProgram = String(environment?.TERM_PROGRAM || '').trim().toLowerCase();
|
|
51
|
+
if (termProgram === 'apple_terminal') return false;
|
|
52
|
+
|
|
53
|
+
const colorTerm = String(environment?.COLORTERM || '').trim().toLowerCase();
|
|
54
|
+
if (colorTerm === 'truecolor' || colorTerm === '24bit') return true;
|
|
55
|
+
if (environment?.WT_SESSION !== undefined && environment.WT_SESSION !== '') return true;
|
|
56
|
+
if (['iterm.app', 'wezterm', 'ghostty', 'vscode'].includes(termProgram)) return true;
|
|
57
|
+
if (/(?:direct|truecolor)/i.test(String(environment?.TERM || ''))) return true;
|
|
58
|
+
if (platformName === 'win32') return true;
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const ANSI_256_CUBE_LEVELS = Object.freeze([0, 95, 135, 175, 215, 255]);
|
|
63
|
+
|
|
64
|
+
function colorByte(value) {
|
|
65
|
+
const n = Number(value);
|
|
66
|
+
return Number.isFinite(n) ? Math.max(0, Math.min(255, Math.round(n))) : 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function nearestCubeLevel(value) {
|
|
70
|
+
let best = 0;
|
|
71
|
+
let bestDistance = Infinity;
|
|
72
|
+
for (let i = 0; i < ANSI_256_CUBE_LEVELS.length; i++) {
|
|
73
|
+
const distance = Math.abs(value - ANSI_256_CUBE_LEVELS[i]);
|
|
74
|
+
if (distance < bestDistance) {
|
|
75
|
+
best = i;
|
|
76
|
+
bestDistance = distance;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return best;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Convert an RGB triplet to the nearest stable xterm 256-color palette index. */
|
|
83
|
+
export function rgbToAnsi256(r, g, b) {
|
|
84
|
+
const red = colorByte(r);
|
|
85
|
+
const green = colorByte(g);
|
|
86
|
+
const blue = colorByte(b);
|
|
87
|
+
|
|
88
|
+
const redLevel = nearestCubeLevel(red);
|
|
89
|
+
const greenLevel = nearestCubeLevel(green);
|
|
90
|
+
const blueLevel = nearestCubeLevel(blue);
|
|
91
|
+
const cubeRed = ANSI_256_CUBE_LEVELS[redLevel];
|
|
92
|
+
const cubeGreen = ANSI_256_CUBE_LEVELS[greenLevel];
|
|
93
|
+
const cubeBlue = ANSI_256_CUBE_LEVELS[blueLevel];
|
|
94
|
+
const cubeDistance = ((red - cubeRed) ** 2)
|
|
95
|
+
+ ((green - cubeGreen) ** 2)
|
|
96
|
+
+ ((blue - cubeBlue) ** 2);
|
|
97
|
+
|
|
98
|
+
const average = (red + green + blue) / 3;
|
|
99
|
+
const grayLevel = Math.max(0, Math.min(23, Math.round((average - 8) / 10)));
|
|
100
|
+
const grayValue = 8 + (grayLevel * 10);
|
|
101
|
+
const grayDistance = ((red - grayValue) ** 2)
|
|
102
|
+
+ ((green - grayValue) ** 2)
|
|
103
|
+
+ ((blue - grayValue) ** 2);
|
|
104
|
+
|
|
105
|
+
if (grayDistance < cubeDistance) return 232 + grayLevel;
|
|
106
|
+
return 16 + (36 * redLevel) + (6 * greenLevel) + blueLevel;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Raw RGB SGR prefix, downsampled to 256 colors when truecolor is unavailable. */
|
|
110
|
+
export function rgbSgr(r, g, b, background = false) {
|
|
111
|
+
const channel = background ? 48 : 38;
|
|
112
|
+
if (supportsTruecolor()) return `${ESC}${channel};2;${r};${g};${b}m`;
|
|
113
|
+
return `${ESC}${channel};5;${rgbToAnsi256(r, g, b)}m`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function supportedSgrOpen(open) {
|
|
117
|
+
const match = /^(38|48);2;([^;]+);([^;]+);([^;]+)$/.exec(String(open));
|
|
118
|
+
if (!match || supportsTruecolor()) return open;
|
|
119
|
+
return `${match[1]};5;${rgbToAnsi256(match[2], match[3], match[4])}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
42
122
|
/** Build a `(text) => string` wrapper for the given SGR open code(s). */
|
|
43
123
|
function sgr(open) {
|
|
44
|
-
const openSeq = `${ESC}${open}m`;
|
|
45
124
|
return (text) => {
|
|
46
125
|
if (!COLOR_ENABLED) return String(text ?? '');
|
|
47
126
|
const s = String(text ?? '');
|
|
127
|
+
const openSeq = `${ESC}${supportedSgrOpen(open)}m`;
|
|
48
128
|
// Re-open after any nested reset so composed styles survive concatenation.
|
|
49
129
|
return openSeq + s.replace(/\x1b\[0m/g, RESET + openSeq) + RESET;
|
|
50
130
|
};
|
|
@@ -84,9 +164,9 @@ export const bgGray = sgr('100');
|
|
|
84
164
|
export const bgBlack = sgr('40');
|
|
85
165
|
export const bgBlue = sgr('44');
|
|
86
166
|
|
|
87
|
-
// ---
|
|
88
|
-
//
|
|
89
|
-
//
|
|
167
|
+
// --- RGB colors --------------------------------------------------------------
|
|
168
|
+
// Emit 24-bit SGR where supported, otherwise use the nearest 256-color entry.
|
|
169
|
+
// Honors NO_COLOR / TTY exactly like the named helpers above.
|
|
90
170
|
|
|
91
171
|
/** Foreground truecolor wrapper: `rgb(215,119,87)('x')`. */
|
|
92
172
|
export function rgb(r, g, b) {
|
|
@@ -14,14 +14,6 @@ import { num, GRN, R, B } from './statusline-format.mjs';
|
|
|
14
14
|
const DEFAULT_HIDDEN_STATUSLINE_AGENTS = Object.freeze(['explorer', 'cycle1-agent', 'cycle2-agent', 'cycle3-agent', 'scheduler-task', 'webhook-handler']);
|
|
15
15
|
let _hiddenStatuslineAgents = null;
|
|
16
16
|
|
|
17
|
-
export function summarizeWorkerTags(workers, limit = 3) {
|
|
18
|
-
const cleanLabels = [...new Set((Array.isArray(workers) ? workers : [])
|
|
19
|
-
.map((worker) => String(worker?.tag || '').trim())
|
|
20
|
-
.filter(Boolean))];
|
|
21
|
-
if (cleanLabels.length <= limit) return cleanLabels.join(', ');
|
|
22
|
-
return `${cleanLabels.slice(0, limit).join(', ')}, +${cleanLabels.length - limit}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
17
|
function normalizeAgentWorkerForStatusline(worker = {}) {
|
|
26
18
|
const tag = String(worker.tag || worker.agent || worker.name || '').trim();
|
|
27
19
|
if (!tag) return null;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* bar/segment formatters, small numeric helpers, and the TUI-independent
|
|
6
6
|
* formatElapsed() replica. No behavior change — statusline.mjs re-imports these.
|
|
7
7
|
*/
|
|
8
|
-
import { colorEnabled, rgb } from './ansi.mjs';
|
|
8
|
+
import { colorEnabled, rgb, rgbSgr } from './ansi.mjs';
|
|
9
9
|
import { displayModelName, shortenModelName } from './model-display.mjs';
|
|
10
10
|
import { getModelMetadataSync } from '../runtime/agent/orchestrator/providers/model-catalog.mjs';
|
|
11
11
|
|
|
@@ -23,12 +23,12 @@ function sgr(code) {
|
|
|
23
23
|
|
|
24
24
|
export const R = sgr('0');
|
|
25
25
|
export const B = sgr('1');
|
|
26
|
-
export const D =
|
|
27
|
-
export const GRN =
|
|
28
|
-
export const YLW =
|
|
29
|
-
export const RED =
|
|
30
|
-
export const CYN =
|
|
31
|
-
export const GREY =
|
|
26
|
+
export const D = colorEnabled() ? rgbSgr(136, 136, 136) : '';
|
|
27
|
+
export const GRN = colorEnabled() ? rgbSgr(0, 170, 75) : '';
|
|
28
|
+
export const YLW = colorEnabled() ? rgbSgr(255, 193, 7) : '';
|
|
29
|
+
export const RED = colorEnabled() ? rgbSgr(220, 70, 88) : '';
|
|
30
|
+
export const CYN = colorEnabled() ? rgbSgr(136, 136, 136) : '';
|
|
31
|
+
export const GREY = colorEnabled() ? rgbSgr(136, 136, 136) : '';
|
|
32
32
|
|
|
33
33
|
export function terminalColumns() {
|
|
34
34
|
const cols = Number(process.stdout?.columns);
|
package/src/ui/statusline.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from './statusline-format.mjs';
|
|
24
24
|
import { shellJobsStatus, memoryCycleStatus } from './statusline-segments.mjs';
|
|
25
25
|
import {
|
|
26
|
-
|
|
26
|
+
agentStatuslinePayload, classifyAgentWorkers, activeHiddenAgentWorkers, agentWebSearchStatus,
|
|
27
27
|
} from './statusline-agents.mjs';
|
|
28
28
|
export { createSessionStats, applyUsageDelta } from './session-stats.mjs';
|
|
29
29
|
// Facade re-exports: keep these public symbols resolving from statusline.mjs.
|
|
@@ -362,14 +362,12 @@ function renderNativeStatusline({
|
|
|
362
362
|
if (runningWorkers.length) {
|
|
363
363
|
const n = runningWorkers.length;
|
|
364
364
|
const label = `Running ${n} Agent${n === 1 ? '' : 's'}`;
|
|
365
|
-
const tagSummary = summarizeWorkerTags(runningWorkers);
|
|
366
|
-
const tags = tagSummary ? ` ${D}(${R}${B}${tagSummary}${R}${D})${R}` : '';
|
|
367
365
|
const oldestStart = runningWorkers.reduce((min, w) => {
|
|
368
366
|
const t = num(w?.startedAtMs);
|
|
369
367
|
return t > 0 && t < min ? t : min;
|
|
370
368
|
}, Infinity);
|
|
371
369
|
const elapsed = Number.isFinite(oldestStart) ? formatElapsed(Date.now() - oldestStart) : '';
|
|
372
|
-
addL2(`${spin} ${B}${label}${R}${
|
|
370
|
+
addL2(`${spin} ${B}${label}${R}${elapsedSuffix(elapsed)}`);
|
|
373
371
|
}
|
|
374
372
|
const tools = activeTools && typeof activeTools === 'object' ? activeTools : {};
|
|
375
373
|
const exploreInfo = tools.explore || null;
|
|
@@ -13,15 +13,20 @@ Approval is a later explicit user message after the latest plan ("do it",
|
|
|
13
13
|
approval with a scope change needs a revised plan and fresh approval. Before
|
|
14
14
|
approval: no edits, state mutation, or delegation.
|
|
15
15
|
|
|
16
|
-
Lead
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
Lead owns three duties, in order: plan — set the direction of the work from its own
|
|
17
|
+
understanding of the task; delegate — hand execution to the matching agent; verify —
|
|
18
|
+
judge what comes back. Understanding is never delegated: without its own plan a Lead
|
|
19
|
+
can neither brief nor judge. Lead-direct work is allowed only for pure read/analysis,
|
|
20
|
+
git/configuration, or when the user explicitly supplies both the exact target and exact
|
|
21
|
+
replacement/output. Never infer an exemption from a task name, file count, or
|
|
22
|
+
perceived difficulty. Every other implementation, reverse engineering, debugging
|
|
23
|
+
application, or artifact generation delegates to the matching agent. Debugger is an
|
|
24
|
+
escalation role reserved for explicitly requested debugging or a bug surviving 2+ fix
|
|
25
|
+
cycles — not a default owner of analysis or reverse engineering, which route to
|
|
26
|
+
Worker/Heavy Worker like any implementation. Worker applies an established bounded
|
|
27
|
+
change or fully specified artifact; Heavy Worker owns implementation that must establish
|
|
28
|
+
the change through investigation or staged delivery. Applying a Debugger result is
|
|
29
|
+
implementation, not diagnosis.
|
|
25
30
|
|
|
26
31
|
1. Plan: draft before any implementation; settle scope/plan, ask if ambiguous,
|
|
27
32
|
then await the gate.
|
|
@@ -29,22 +34,26 @@ delivery. Applying a Debugger result is implementation, not diagnosis.
|
|
|
29
34
|
one turn, with no count cap. Serialize only a real dependency, overlapping
|
|
30
35
|
write, or inseparable coupling. Briefs follow the Lead brief contract.
|
|
31
36
|
After async spawn, end the turn.
|
|
32
|
-
3. Review:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
3. Review: triage by the work performed, never by the size, form, or destination
|
|
38
|
+
of its result — a one-line answer can carry non-trivial work, and a
|
|
39
|
+
conversational reply is not automatically exempt. Trivial work — a routine
|
|
40
|
+
lookup answered directly, routine git/configuration confirmed by its own
|
|
41
|
+
mechanical check, a change whose correctness is test/build/diff-obvious, or
|
|
42
|
+
applying an exact target and replacement the user supplied — ships on shell
|
|
43
|
+
self-verification alone, and only after that check actually ran and passed.
|
|
44
|
+
Non-trivial work — multi-step reasoning, interpretation, investigation, or
|
|
45
|
+
anything whose correctness is not verifiable at a glance — gets a Reviewer
|
|
46
|
+
cross-check (one reviewer per scope, kept across the fix loop; all ready
|
|
47
|
+
reviewers spawn in one turn) and Lead integration/cross-scope verification
|
|
48
|
+
in parallel, whoever performed it, Lead solo included. Debugger analysis
|
|
49
|
+
cannot substitute for implementation review: applying a Debugger result triggers
|
|
40
50
|
the same Reviewer + Lead verification. Reviewer independently judges risk,
|
|
41
51
|
intent, boundaries; Lead checks acceptance/interactions, not duplicate
|
|
42
52
|
same-scope work. High-risk scopes add distinct lenses. Synthesize one
|
|
43
53
|
verdict; send merged fixes to the original live session; loop fix ->
|
|
44
54
|
re-verify (same Reviewer + Lead re-check) until clean. Debugger first for
|
|
45
|
-
requested debugging or a bug surviving 2+ fix cycles.
|
|
46
|
-
|
|
47
|
-
verdict, next work as in-progress, never conclusions.
|
|
55
|
+
requested debugging or a bug surviving 2+ fix cycles. Agent reports relay
|
|
56
|
+
scope, verdict, next work as in-progress, never conclusions.
|
|
48
57
|
4. Report: final (not interim) report compares work to approved plan and gives
|
|
49
58
|
verified result; never forward raw agent output. Ask about ship/deploy when
|
|
50
59
|
relevant. Build/deploy/commit/push require an explicit user request after
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: solo-review
|
|
3
|
+
name: Solo Review
|
|
4
|
+
description: "Lead implements directly; eligible low-risk single scopes receive one independent Reviewer."
|
|
5
|
+
agents: reviewer
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Solo Review
|
|
9
|
+
|
|
10
|
+
GATE: Before approval, only read-only investigation/planning while consulting.
|
|
11
|
+
Approval is a later explicit user message after the latest plan ("do it",
|
|
12
|
+
"proceed", "go ahead"). Initial/additional/changed requests reset planning;
|
|
13
|
+
approval with a scope change needs a revised plan and fresh approval. Before
|
|
14
|
+
approval: no edits, state mutation, or delegation.
|
|
15
|
+
|
|
16
|
+
1. Plan: before implementation, Lead classifies the scope as eligible
|
|
17
|
+
low-risk/single-scope or ineligible high-risk/multi-scope, drafts the plan,
|
|
18
|
+
settles scope, asks if ambiguous, then awaits the gate. Ineligible work must
|
|
19
|
+
switch to Default workflow with a revised Default plan and fresh approval
|
|
20
|
+
before implementation. After that switch, Solo Review constraints are
|
|
21
|
+
suspended: Default reviewer-per-scope fan-out and high-risk lenses override
|
|
22
|
+
Solo Review's single-Reviewer and concurrency limits.
|
|
23
|
+
2. Execute: after approval, Lead performs all implementation and verification
|
|
24
|
+
directly. Never delegate implementation, investigation, debugging, or
|
|
25
|
+
maintenance to implementation helper roles (Worker, Heavy Worker, Explorer,
|
|
26
|
+
Debugger, or Maintainer). Complete in-scope fixes without reapproval; interim
|
|
27
|
+
updates are in-progress, never conclusions.
|
|
28
|
+
3. Review: only an eligible low-risk, single-scope final deliverable, once
|
|
29
|
+
implemented and Lead-verified, receives exactly one Reviewer to critically
|
|
30
|
+
and independently evaluate the approved intent, complete deliverable,
|
|
31
|
+
affected boundaries, and verification. Lead evaluates the findings, applies
|
|
32
|
+
every necessary fix directly, and re-verifies. Send the revised deliverable
|
|
33
|
+
back to that same live Reviewer for every re-check. If that session is
|
|
34
|
+
unavailable, one cold replacement Reviewer may re-review the original intent,
|
|
35
|
+
current deliverable, and verification evidence; never run replacement or
|
|
36
|
+
additional Reviewers concurrently. Repeat fix -> Lead verification -> Reviewer
|
|
37
|
+
re-check until issue-free or blocked.
|
|
38
|
+
4. Report: only after the review loop is clean, the final (not interim) report
|
|
39
|
+
compares the result to the approved plan and gives verification and material
|
|
40
|
+
remaining risk/next step; never forward raw Reviewer output. Ask about
|
|
41
|
+
ship/deploy when relevant. Verification builds/tests needed to evaluate the
|
|
42
|
+
deliverable are permitted during execution and review. Release builds,
|
|
43
|
+
deploy/commit/push require an explicit user request after issue-free feedback;
|
|
44
|
+
implementation approval alone is insufficient.
|
|
45
|
+
|
|
46
|
+
On outcome/direction change, pause and re-consult; otherwise continue approved
|
|
47
|
+
work without reapproval.
|