@tea-agent/loop-agent 0.33.6 → 0.33.7-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -23
- package/dist/application/task-lifecycle/advance.js +4 -254
- package/dist/application/task-lifecycle/gates.js +0 -50
- package/dist/application/task-lifecycle/observe.js +2 -11
- package/dist/commands/init-upgrade.js +1 -32
- package/dist/commands/init.js +3 -94
- package/dist/executors/shell-executor.js +91 -4
- package/dist/executors/shell-write-guard.js +8 -26
- package/dist/shared/operator/capabilities.js +42 -72
- package/dist/task/source-prepare/index.js +0 -2
- package/dist/task/source-prepare/parse-intent.js +10 -58
- package/dist/task/source-prepare/prepare.js +16 -180
- package/dist/task/source-prepare/reference-integrity.js +2 -18
- package/dist/worker/console/app-data.js +0 -2
- package/dist/worker/console/chat/chat-event-store.js +25 -190
- package/dist/worker/console/chat/instruction-skills.js +217 -0
- package/dist/worker/console/chat/pi-console-config.js +32 -250
- package/dist/worker/console/chat/pi-runtime.js +71 -625
- package/dist/worker/console/chat/resource-loader.js +4 -5
- package/dist/worker/console/chat/routes.js +146 -324
- package/dist/worker/console/chat/runtime-context.js +12 -48
- package/dist/worker/console/chat/runtime-selection.js +0 -59
- package/dist/worker/console/chat/shortcuts.js +0 -1
- package/dist/worker/console/chat/tool-adapter.js +3 -9
- package/dist/worker/console/chat/tools.js +1 -5
- package/dist/worker/console/operator-actions.js +68 -559
- package/dist/worker/console/server.js +15 -8
- package/dist/worker/console/static/assets/index-CnUXAqxG.css +1 -0
- package/dist/worker/console/static/assets/index-CteJFFL2.js +29 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +8 -45
- package/dist/worker/console/static-src/operator-chat/refs.js +0 -9
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +0 -16
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +184 -210
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +5 -49
- package/dist/worker/console/static-src/operator-chat/useComposer.js +0 -17
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +74 -225
- package/dist/worker/delivery/final-verification.js +5 -13
- package/dist/worker/delivery/package.js +19 -31
- package/dist/worker/delivery/verification-bundle.js +4 -6
- package/dist/worker/observe/static/operator-chrome.css +2 -5
- package/dist/worker/observe/static/operator-chrome.js +1 -6
- package/dist/worker/observe/static/styles.css +9 -39
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +462 -33
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +25 -1
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
- package/dist/workflows/dag/backend-test-scenario-param.js +269 -82
- package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/frontend-worktree-diff.js +27 -12
- package/dist/workflows/dag/init-hybrid.js +46 -34
- package/dist/workflows/dag/types.js +7 -0
- package/dist/workflows/dag/workspace-checkpoint.js +27 -8
- package/docs/templates/backend-test-dag.json +32 -29
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -3
- package/skills/loop-agent/references/source-and-plan-practice.md +0 -13
- package/skills/loop-agent/references/task-workflow.md +0 -4
- package/dist/shared/resilient-git.js +0 -133
- package/dist/task/source-prepare/artifact-meta.js +0 -137
- package/dist/task/source-prepare/semantic-intake.js +0 -404
- package/dist/worker/console/dag-execution-receipt.js +0 -380
- package/dist/worker/console/static/assets/index-BUOLppPr.js +0 -28
- package/dist/worker/console/static/assets/index-C1KzazY5.css +0 -1
- package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +0 -257
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +0 -196
|
@@ -1,44 +1,19 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { formatModelRef, mergeSessionRuntime, sessionRuntimeMatches, } from "../../chat/runtime-selection.js";
|
|
2
|
+
import { formatModelRef, mergeSessionRuntime, runtimeSelectionFromRecord, sessionRuntimeMatches, } from "../../chat/runtime-selection.js";
|
|
3
3
|
import { confirmationToken } from "./format.js";
|
|
4
|
-
import { mergeRuntimeContextWithSnapshot, runtimeSelectionFromSnapshot, } from "./runtime-snapshot-store.js";
|
|
5
4
|
import { buildToolCatalog } from "./tools-catalog.js";
|
|
6
|
-
import { adoptMutationSnapshot, dispatchRuntimeSnapshot, getRuntimeSnapshotState, refreshRuntimeSnapshot, settleMutationLifecycle, useRuntimeSnapshot, } from "./useRuntimeSnapshot.js";
|
|
7
|
-
/**
|
|
8
|
-
* Monotonic mutation sequence shared across switchRuntime calls (RF-04). Each
|
|
9
|
-
* mutation captures its own token; only the LATEST started token may settle
|
|
10
|
-
* mutationBusy/switching, so a slow earlier response can never clear busy
|
|
11
|
-
* while a newer mutation is still in flight.
|
|
12
|
-
*/
|
|
13
|
-
let mutationSeq = 0;
|
|
14
5
|
/** Capabilities catalog, model / thinking-level switcher, runtime-context
|
|
15
6
|
* popover and the outside-click dismissal for both transient menus. */
|
|
16
7
|
export function useRuntimeControls(params) {
|
|
17
8
|
const { origin, session, setSession, refs, setError } = params;
|
|
18
9
|
const [capabilities, setCapabilities] = useState(null);
|
|
19
10
|
const [models, setModels] = useState([]);
|
|
20
|
-
const [modelsStatus, setModelsStatus] = useState("idle");
|
|
21
|
-
const [modelsError, setModelsError] = useState(null);
|
|
22
11
|
const [thinkingLevels, setThinkingLevels] = useState([]);
|
|
23
12
|
const [runtimeContext, setRuntimeContext] = useState(null);
|
|
24
|
-
const [runtimeContextError, setRuntimeContextError] = useState(null);
|
|
25
|
-
const [switching, setSwitching] = useState(false);
|
|
26
13
|
const [toolsOpen, setToolsOpen] = useState(false);
|
|
27
14
|
const [runtimeContextOpen, setRuntimeContextOpen] = useState(false);
|
|
28
15
|
const toolsMenuRef = useRef(null);
|
|
29
16
|
const runtimeContextMenuRef = useRef(null);
|
|
30
|
-
const switchTokenRef = useRef(0);
|
|
31
|
-
// RF3-02: a session switch immediately abandons the in-flight mutation
|
|
32
|
-
// lifecycle — the local token is invalidated and switching cleared, so a
|
|
33
|
-
// late response from the previous session can never settle the new
|
|
34
|
-
// session's busy/switching. (The store side is reset synchronously by
|
|
35
|
-
// attachRuntimeSnapshotController's mutation-reset.)
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
switchTokenRef.current = -1;
|
|
38
|
-
setSwitching(false);
|
|
39
|
-
}, [session?.sessionId, setSwitching]);
|
|
40
|
-
const snapshotState = useRuntimeSnapshot();
|
|
41
|
-
const snapshot = snapshotState.snapshot;
|
|
42
17
|
const loadCapabilities = useCallback(async () => {
|
|
43
18
|
try {
|
|
44
19
|
const res = await fetch(`${origin}/api/operator/v1/chat/capabilities`);
|
|
@@ -54,202 +29,106 @@ export function useRuntimeControls(params) {
|
|
|
54
29
|
useEffect(() => {
|
|
55
30
|
void loadCapabilities();
|
|
56
31
|
}, [loadCapabilities]);
|
|
57
|
-
|
|
58
|
-
// session server-side; a failure is a STRUCTURED error state (with retry),
|
|
59
|
-
// never silently folded into an empty list.
|
|
60
|
-
const loadModels = useCallback(async () => {
|
|
32
|
+
useEffect(() => {
|
|
61
33
|
if (!session) {
|
|
62
34
|
setModels([]);
|
|
63
|
-
|
|
35
|
+
setRuntimeContext(null);
|
|
64
36
|
return;
|
|
65
37
|
}
|
|
66
38
|
const sessionId = session.sessionId;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const body = (await res.json().catch(() => null));
|
|
39
|
+
const base = `${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}`;
|
|
40
|
+
void fetch(`${base}/models`, { credentials: "include" })
|
|
41
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
42
|
+
.then((body) => {
|
|
72
43
|
// Guard against a stale response landing after the user switched/
|
|
73
44
|
// created/forked a session: only adopt models for the session we
|
|
74
45
|
// actually requested them for.
|
|
75
|
-
if (refs.sessionRef.current?.sessionId !== sessionId)
|
|
76
|
-
return;
|
|
77
|
-
if (!res.ok || !body) {
|
|
78
|
-
setModelsStatus("error");
|
|
79
|
-
setModelsError(body
|
|
80
|
-
?.error?.message ?? `模型列表读取失败(HTTP ${res.status})`);
|
|
46
|
+
if (!body || refs.sessionRef.current?.sessionId !== sessionId)
|
|
81
47
|
return;
|
|
82
|
-
}
|
|
83
48
|
setModels(body.models ?? []);
|
|
84
49
|
setThinkingLevels(body.thinkingLevels ?? []);
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
catch (reason) {
|
|
88
|
-
if (refs.sessionRef.current?.sessionId !== sessionId)
|
|
89
|
-
return;
|
|
90
|
-
setModelsStatus("error");
|
|
91
|
-
setModelsError(reason instanceof Error ? reason.message : String(reason));
|
|
92
|
-
}
|
|
93
|
-
}, [origin, session, refs]);
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
void loadModels();
|
|
96
|
-
}, [loadModels]);
|
|
97
|
-
useEffect(() => {
|
|
98
|
-
if (!session) {
|
|
99
|
-
setRuntimeContext(null);
|
|
100
|
-
setRuntimeContextError(null);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
const sessionId = session.sessionId;
|
|
104
|
-
const base = `${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}`;
|
|
105
|
-
setRuntimeContextError(null);
|
|
50
|
+
});
|
|
106
51
|
void fetch(`${base}/runtime-context`, { credentials: "include" })
|
|
107
|
-
.then(
|
|
108
|
-
|
|
52
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
53
|
+
.then((body) => {
|
|
109
54
|
// Stale-response guard: a slow runtime-context reply for session A
|
|
110
55
|
// must not overwrite the context/session state of the now-current
|
|
111
56
|
// session B.
|
|
112
57
|
if (!body?.context || refs.sessionRef.current?.sessionId !== sessionId)
|
|
113
58
|
return;
|
|
114
|
-
|
|
115
|
-
// carries no revision semantics, so a late reply for the same
|
|
116
|
-
// session could hold a stale selection. Selection is derived only
|
|
117
|
-
// from the LATEST adopted snapshot, read HERE at .then time (right
|
|
118
|
-
// before the setRuntimeContext functional updater runs) via the
|
|
119
|
-
// production read-only accessor getRuntimeSnapshotState() — the
|
|
120
|
-
// only immediate authority when the store has already adopted a
|
|
121
|
-
// new revision but this component has not rerendered yet. A
|
|
122
|
-
// render-synchronized value (snapshotRef) could still be the old
|
|
123
|
-
// revision in that window, so it is removed entirely.
|
|
124
|
-
// systemPrompt/resources/suffix/readOnly still come from the
|
|
125
|
-
// response. Zero session writes and zero store writes: the
|
|
126
|
-
// snapshot hydration effect never re-runs and the menu/popover
|
|
127
|
-
// selection never jumps back.
|
|
59
|
+
setRuntimeContext(body.context);
|
|
128
60
|
const ctx = body.context;
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
61
|
+
const patch = {
|
|
62
|
+
model: ctx.model,
|
|
63
|
+
thinkingLevel: ctx.thinkingLevel,
|
|
64
|
+
};
|
|
65
|
+
setSession((current) => {
|
|
66
|
+
if (!current || current.sessionId !== sessionId)
|
|
67
|
+
return current;
|
|
68
|
+
if (sessionRuntimeMatches(current, patch))
|
|
69
|
+
return current;
|
|
70
|
+
const merged = mergeSessionRuntime(current, patch);
|
|
71
|
+
refs.sessionRef.current = merged;
|
|
72
|
+
return merged;
|
|
73
|
+
});
|
|
136
74
|
});
|
|
137
|
-
}, [origin, session?.sessionId, refs]);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
75
|
+
}, [origin, session?.sessionId, refs, setSession]);
|
|
76
|
+
const switchRuntime = useCallback(async (body) => {
|
|
77
|
+
if (!session)
|
|
78
|
+
return;
|
|
79
|
+
// Capture the target session so a slow model/thinking response cannot
|
|
80
|
+
// write its result into a different (newly created/forked/switched)
|
|
81
|
+
// session the user has since moved to.
|
|
82
|
+
const targetSessionId = session.sessionId;
|
|
83
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(session.sessionId)}/model`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
credentials: "include",
|
|
86
|
+
headers: {
|
|
87
|
+
"content-type": "application/json",
|
|
88
|
+
"x-loop-console-confirmation": confirmationToken(),
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify(body),
|
|
91
|
+
});
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
// Only surface the failure for the session that still owns it.
|
|
94
|
+
if (refs.sessionRef.current?.sessionId === targetSessionId)
|
|
95
|
+
setError("模型或思考等级切换失败");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const payload = (await res.json().catch(() => ({})));
|
|
99
|
+
if (!payload.session)
|
|
100
|
+
return;
|
|
101
|
+
// Drop the result entirely if the active session changed mid-flight.
|
|
102
|
+
if (refs.sessionRef.current?.sessionId !== targetSessionId)
|
|
146
103
|
return;
|
|
147
|
-
const patch = runtimeSelectionFromSnapshot(snapshot);
|
|
148
104
|
setSession((current) => {
|
|
149
|
-
if (!current || current.sessionId !==
|
|
105
|
+
if (!current || current.sessionId !== targetSessionId)
|
|
150
106
|
return current;
|
|
107
|
+
const patch = runtimeSelectionFromRecord(payload.session);
|
|
151
108
|
if (sessionRuntimeMatches(current, patch))
|
|
152
109
|
return current;
|
|
153
110
|
const merged = mergeSessionRuntime(current, patch);
|
|
154
111
|
refs.sessionRef.current = merged;
|
|
155
112
|
return merged;
|
|
156
113
|
});
|
|
157
|
-
setRuntimeContext((current) =>
|
|
158
|
-
|
|
159
|
-
return current;
|
|
160
|
-
const next = {
|
|
114
|
+
setRuntimeContext((current) => current
|
|
115
|
+
? {
|
|
161
116
|
...current,
|
|
162
|
-
...(
|
|
163
|
-
|
|
164
|
-
|
|
117
|
+
...(payload.session.modelProvider && payload.session.modelId
|
|
118
|
+
? {
|
|
119
|
+
model: {
|
|
120
|
+
provider: payload.session.modelProvider,
|
|
121
|
+
modelId: payload.session.modelId,
|
|
122
|
+
},
|
|
123
|
+
}
|
|
165
124
|
: {}),
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
return next;
|
|
173
|
-
});
|
|
174
|
-
}, [snapshot, session?.sessionId, refs, setSession, setRuntimeContext]);
|
|
175
|
-
const switchRuntime = useCallback(async (body) => {
|
|
176
|
-
if (!session)
|
|
177
|
-
return false;
|
|
178
|
-
// Capture the target session so a slow model/thinking response cannot
|
|
179
|
-
// write its result into a different (newly created/forked/switched)
|
|
180
|
-
// session the user has since moved to.
|
|
181
|
-
const targetSessionId = session.sessionId;
|
|
182
|
-
// RF-04: token the mutation start; only the latest token's settle
|
|
183
|
-
// clears mutationBusy/switching (stale earlier settles can't win).
|
|
184
|
-
const token = ++mutationSeq;
|
|
185
|
-
switchTokenRef.current = token;
|
|
186
|
-
dispatchRuntimeSnapshot({ type: "mutation-start", token });
|
|
187
|
-
setSwitching(true);
|
|
188
|
-
try {
|
|
189
|
-
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(session.sessionId)}/model`, {
|
|
190
|
-
method: "POST",
|
|
191
|
-
credentials: "include",
|
|
192
|
-
headers: {
|
|
193
|
-
"content-type": "application/json",
|
|
194
|
-
"x-loop-console-confirmation": confirmationToken(),
|
|
195
|
-
},
|
|
196
|
-
body: JSON.stringify(body),
|
|
197
|
-
});
|
|
198
|
-
const payload = (await res.json().catch(() => ({})));
|
|
199
|
-
// RF2-02: an out-of-order response (its token is no longer the
|
|
200
|
-
// latest started mutation) is dropped ENTIRELY on BOTH the success
|
|
201
|
-
// and the failure branch: no setSession/setRuntimeContext, no
|
|
202
|
-
// mutation-adopt, no busy settle, no refresh — the newest mutation
|
|
203
|
-
// owns the UI and a slow earlier reply can never overwrite it.
|
|
204
|
-
if (switchTokenRef.current !== token)
|
|
205
|
-
return false;
|
|
206
|
-
if (!res.ok) {
|
|
207
|
-
// RF2-01: a non-2xx response (e.g. 503 PI_RUNTIME_UNAVAILABLE —
|
|
208
|
-
// the mutation already applied server-side but the snapshot
|
|
209
|
-
// projection failed) for the still-current session shows the
|
|
210
|
-
// structured error, then triggers a REAL GET
|
|
211
|
-
// /runtime-snapshot so the store converges on the server
|
|
212
|
-
// selection via the load-success revision guard. When the
|
|
213
|
-
// session switched or the response is stale: no error, no
|
|
214
|
-
// refresh, no state writes.
|
|
215
|
-
if (refs.sessionRef.current?.sessionId === targetSessionId) {
|
|
216
|
-
setError(payload.error?.message ??
|
|
217
|
-
`模型或思考等级切换失败(HTTP ${res.status})`);
|
|
218
|
-
void refreshRuntimeSnapshot();
|
|
219
|
-
}
|
|
220
|
-
return false;
|
|
125
|
+
...(payload.session.thinkingLevel
|
|
126
|
+
? { thinkingLevel: payload.session.thinkingLevel }
|
|
127
|
+
: body.thinkingLevel
|
|
128
|
+
? { thinkingLevel: body.thinkingLevel }
|
|
129
|
+
: {}),
|
|
221
130
|
}
|
|
222
|
-
|
|
223
|
-
return false;
|
|
224
|
-
// Drop the result entirely if the active session changed mid-flight.
|
|
225
|
-
if (refs.sessionRef.current?.sessionId !== targetSessionId)
|
|
226
|
-
return false;
|
|
227
|
-
// RF3-01: derived session/runtimeContext state is ONLY written from
|
|
228
|
-
// a snapshot the reducer's three guards (latest mutationToken,
|
|
229
|
-
// current sessionId, monotonic revision) accepted — no direct
|
|
230
|
-
// setSession/setRuntimeContext here. A missing snapshot returns
|
|
231
|
-
// false with zero derived writes; on adoption the hydrate effect
|
|
232
|
-
// derives session + runtimeContext from the adopted snapshot.
|
|
233
|
-
return adoptMutationSnapshot(token, targetSessionId, payload.snapshot);
|
|
234
|
-
}
|
|
235
|
-
catch (reason) {
|
|
236
|
-
// RF2-02: stale out-of-order network failures are dropped too —
|
|
237
|
-
// no error, no refresh, no state writes for an expired token.
|
|
238
|
-
if (switchTokenRef.current !== token)
|
|
239
|
-
return false;
|
|
240
|
-
if (refs.sessionRef.current?.sessionId === targetSessionId)
|
|
241
|
-
setError(reason instanceof Error ? reason.message : String(reason));
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
finally {
|
|
245
|
-
// RF3-02: settle only when the target session is still active and
|
|
246
|
-
// the token is still the latest — a late old-session response
|
|
247
|
-
// neither dispatches an effective settle nor clears the current
|
|
248
|
-
// busy/switching.
|
|
249
|
-
const settled = settleMutationLifecycle(token, targetSessionId, refs);
|
|
250
|
-
if (settled && switchTokenRef.current === token)
|
|
251
|
-
setSwitching(false);
|
|
252
|
-
}
|
|
131
|
+
: current);
|
|
253
132
|
}, [origin, session, refs, setSession, setError]);
|
|
254
133
|
// Close tools catalog on outside click / Escape.
|
|
255
134
|
useEffect(() => {
|
|
@@ -300,46 +179,18 @@ export function useRuntimeControls(params) {
|
|
|
300
179
|
const toolCatalog = useMemo(() => buildToolCatalog(capabilities), [
|
|
301
180
|
capabilities,
|
|
302
181
|
]);
|
|
303
|
-
// Snapshot is the single source of truth when ready; the /models fetch
|
|
304
|
-
// result is the fallback (both hit the same backend, never fake-empty).
|
|
305
|
-
const effectiveModels = snapshot
|
|
306
|
-
? (snapshot.models ?? []).map((model) => ({
|
|
307
|
-
provider: model.provider,
|
|
308
|
-
id: model.id,
|
|
309
|
-
...(model.name ? { name: model.name } : {}),
|
|
310
|
-
...(model.hasCredentials !== undefined
|
|
311
|
-
? { hasCredentials: model.hasCredentials }
|
|
312
|
-
: {}),
|
|
313
|
-
}))
|
|
314
|
-
: models;
|
|
315
|
-
const effectiveThinkingLevels = snapshot?.thinkingLevels?.length
|
|
316
|
-
? snapshot.thinkingLevels.map((option) => option.value)
|
|
317
|
-
: thinkingLevels;
|
|
318
182
|
const activeModel = session?.model ?? runtimeContext?.model;
|
|
319
183
|
const activeModelRef = activeModel
|
|
320
184
|
? formatModelRef(activeModel.provider, activeModel.modelId)
|
|
321
185
|
: "";
|
|
322
|
-
|
|
323
|
-
// with the Pi effective level available separately for the badge.
|
|
324
|
-
const activeThinkingLevel = snapshot?.selection?.thinkingSelection ?? session?.thinkingLevel ?? "";
|
|
325
|
-
const effectiveThinkingLevel = snapshot?.selection?.effectiveThinkingLevel ??
|
|
326
|
-
session?.thinkingLevel ??
|
|
327
|
-
undefined;
|
|
186
|
+
const activeThinkingLevel = session?.thinkingLevel ?? runtimeContext?.thinkingLevel ?? "";
|
|
328
187
|
const activeModelListed = !activeModelRef ||
|
|
329
|
-
|
|
188
|
+
models.some((model) => formatModelRef(model.provider, model.id) === activeModelRef);
|
|
330
189
|
return {
|
|
331
190
|
capabilities,
|
|
332
|
-
models
|
|
333
|
-
|
|
334
|
-
modelsError,
|
|
335
|
-
onRetryModels: () => void loadModels(),
|
|
336
|
-
thinkingLevels: effectiveThinkingLevels,
|
|
191
|
+
models,
|
|
192
|
+
thinkingLevels,
|
|
337
193
|
runtimeContext,
|
|
338
|
-
runtimeContextError,
|
|
339
|
-
onRetryRuntimeContext: () => {
|
|
340
|
-
setRuntimeContextError(null);
|
|
341
|
-
void refreshRuntimeSnapshot();
|
|
342
|
-
},
|
|
343
194
|
toolsOpen,
|
|
344
195
|
setToolsOpen,
|
|
345
196
|
runtimeContextOpen,
|
|
@@ -347,12 +198,10 @@ export function useRuntimeControls(params) {
|
|
|
347
198
|
toolsMenuRef,
|
|
348
199
|
runtimeContextMenuRef,
|
|
349
200
|
switchRuntime,
|
|
350
|
-
switching,
|
|
351
201
|
toolCatalog,
|
|
352
202
|
activeModel,
|
|
353
203
|
activeModelRef,
|
|
354
204
|
activeThinkingLevel,
|
|
355
|
-
effectiveThinkingLevel,
|
|
356
205
|
activeModelListed,
|
|
357
206
|
};
|
|
358
207
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import { mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
+
import { promisify } from "node:util";
|
|
5
6
|
import YAML from "yaml";
|
|
6
7
|
import { controllerIdentitiesMatch, controllerIdentityExpectationFailure, resolveControllerIdentity, } from "../loop-agent/loop-agent-client.js";
|
|
7
8
|
import { acceptanceSpecSchema } from "../task-graph/acceptance-schema.js";
|
|
@@ -14,6 +15,7 @@ import { taskSpecSchema } from "../task-spec/schema.js";
|
|
|
14
15
|
import { preflightTargetRepo } from "../preflight.js";
|
|
15
16
|
import { gitTransactionRecordSchema, transactionRecordPath } from "./git-transaction.js";
|
|
16
17
|
import { buildFeatureVerificationBundle, writeFeatureVerificationBundle, } from "./verification-bundle.js";
|
|
18
|
+
const execFileAsync = promisify(execFile);
|
|
17
19
|
/**
|
|
18
20
|
* Canonical Delivery evidence locations written by `feature verify-final`
|
|
19
21
|
* (ADR 0007). Paths are repo-relative with forward slashes.
|
|
@@ -287,16 +289,6 @@ export async function writeEvidencePairAtomic(evidenceDir, value, fs = { rm }) {
|
|
|
287
289
|
if (backedUp)
|
|
288
290
|
await fs.rm(backup, { recursive: true, force: true }).catch(() => { });
|
|
289
291
|
}
|
|
290
|
-
async function git(repoRoot, args) {
|
|
291
|
-
|
|
292
|
-
if (result.code !== 0)
|
|
293
|
-
throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`);
|
|
294
|
-
return result.stdout.trim();
|
|
295
|
-
}
|
|
296
|
-
async function gitRaw(repoRoot, args) {
|
|
297
|
-
const result = await runResilientGitCommand({ cwd: repoRoot, args, readOnly: true });
|
|
298
|
-
if (result.code !== 0)
|
|
299
|
-
throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`);
|
|
300
|
-
return result.stdout;
|
|
301
|
-
}
|
|
292
|
+
async function git(repoRoot, args) { return (await execFileAsync("git", args, { cwd: repoRoot, encoding: "utf8" })).stdout.trim(); }
|
|
293
|
+
async function gitRaw(repoRoot, args) { return (await execFileAsync("git", args, { cwd: repoRoot, encoding: "utf8" })).stdout; }
|
|
302
294
|
function isNotFound(error) { return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT"); }
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
2
3
|
import { mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
+
import { promisify } from "node:util";
|
|
5
6
|
import YAML from "yaml";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import { controllerIdentitiesMatch, LOOP_AGENT_PACKAGE_NAME, } from "../loop-agent/loop-agent-client.js";
|
|
@@ -12,6 +13,7 @@ import { validateFeatureTaskGraph } from "../task-graph/validate.js";
|
|
|
12
13
|
import { taskSpecSchema } from "../task-spec/schema.js";
|
|
13
14
|
import { gitTransactionRecordSchema, transactionRecordPath } from "./git-transaction.js";
|
|
14
15
|
import { readVerifiedBundle, verifyBundleCurrentStateBindings, verifyBundleOutcomes, verifyBundleTaskSpecBindings } from "./verification-bundle.js";
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
15
17
|
const hashedRefSchema = z.object({ path: z.string().min(1), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict();
|
|
16
18
|
export const acceptanceCoverageArtifactSchema = z.object({
|
|
17
19
|
schemaVersion: z.literal(1),
|
|
@@ -89,9 +91,9 @@ export async function prepareFeatureDelivery(input) {
|
|
|
89
91
|
const now = input.now ?? new Date();
|
|
90
92
|
const record = await readTransaction(repoRoot, featureId, blockers);
|
|
91
93
|
if (record) {
|
|
92
|
-
const branch = await git(repoRoot, ["branch", "--show-current"]).catch(
|
|
93
|
-
const head = await git(repoRoot, ["rev-parse", "HEAD"]).catch(
|
|
94
|
-
const status = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]).catch(
|
|
94
|
+
const branch = await git(repoRoot, ["branch", "--show-current"]).catch(() => "");
|
|
95
|
+
const head = await git(repoRoot, ["rev-parse", "HEAD"]).catch(() => "");
|
|
96
|
+
const status = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]).catch(() => "git-error");
|
|
95
97
|
if (branch !== record.branch)
|
|
96
98
|
blockers.push(`current branch must be ${record.branch}`);
|
|
97
99
|
if (head !== record.lastCheckpoint)
|
|
@@ -200,11 +202,11 @@ export async function validateDeliveryPackage(input) {
|
|
|
200
202
|
blockers.push("Delivery Feature ownership mismatch");
|
|
201
203
|
if (manifest.branch !== record.branch || manifest.baseBranch !== record.baseBranch || manifest.baseSha !== record.baseCommit || manifest.headSha !== record.lastCheckpoint)
|
|
202
204
|
blockers.push("Delivery manifest does not match the Git transaction");
|
|
203
|
-
const branch = await git(repoRoot, ["branch", "--show-current"]).catch(
|
|
204
|
-
const head = await git(repoRoot, ["rev-parse", "HEAD"]).catch(
|
|
205
|
+
const branch = await git(repoRoot, ["branch", "--show-current"]).catch(() => "");
|
|
206
|
+
const head = await git(repoRoot, ["rev-parse", "HEAD"]).catch(() => "");
|
|
205
207
|
if (branch !== record.branch || head !== record.lastCheckpoint)
|
|
206
208
|
blockers.push("current Git position does not match Delivery");
|
|
207
|
-
const rawCloseoutStatus = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]).catch(
|
|
209
|
+
const rawCloseoutStatus = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]).catch(() => "git-error");
|
|
208
210
|
const allowedDirtyPaths = new Set((input.allowedDirtyPaths ?? []).map((item) => item.replace(/\\/g, "/")));
|
|
209
211
|
const closeoutStatus = rawCloseoutStatus.split("\n").filter(Boolean).filter((line) => !allowedDirtyPaths.has(line.slice(3).replace(/\\/g, "/"))).join("\n").trim();
|
|
210
212
|
if (closeoutStatus)
|
|
@@ -493,37 +495,23 @@ async function writeDeliveryArtifacts(artifacts, coverage, manifest) {
|
|
|
493
495
|
}
|
|
494
496
|
}
|
|
495
497
|
function renderPr(manifest) { return `# Summary\n\nFeature ${manifest.featureId} local Delivery Package.\n\n## Feature/base/head\n\n- Branch: ${manifest.branch}\n- Base: ${manifest.baseBranch} @ ${manifest.baseSha}\n- Head: ${manifest.headSha}\n\n## Completed Tasks\n\n${manifest.tasks.map((task) => `- ${task.taskId}: ${task.commit} (${task.changedFiles.join(", ")})`).join("\n")}\n\n## Acceptance Coverage\n\n- Required: ${manifest.acceptanceCoverage.requiredTotal}\n- Covered: ${manifest.acceptanceCoverage.covered}\n- Waived: ${manifest.acceptanceCoverage.waived}\n- Blocked: ${manifest.acceptanceCoverage.blocked}\n\n## Verification\n\n- QA: ${manifest.qa.evidence.path}\n- Final: ${manifest.finalVerification.path}\n\n## Changed Files\n\n${[...new Set(manifest.tasks.flatMap((task) => task.changedFiles))].map((file) => `- ${file}`).join("\n")}\n\n## Risks\n\n- high=${manifest.riskSummary.high}, medium=${manifest.riskSummary.medium}, low=${manifest.riskSummary.low}\n\n## Human Review Checklist\n\n- [ ] Review diff and commits\n- [ ] Confirm required AC coverage and waivers\n- [ ] Confirm QA and final verification evidence\n- [ ] Review risks\n- [ ] Decide whether to push or create a remote PR manually\n`; }
|
|
496
|
-
async function git(repoRoot, args) {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
return
|
|
501
|
-
}
|
|
502
|
-
async function gitRaw(repoRoot, args) {
|
|
503
|
-
const result = await runResilientGitCommand({ cwd: repoRoot, args, readOnly: true });
|
|
504
|
-
if (result.code !== 0)
|
|
505
|
-
throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`);
|
|
506
|
-
return result.stdout;
|
|
507
|
-
}
|
|
508
|
-
async function gitOk(repoRoot, args) {
|
|
509
|
-
const result = await runResilientGitCommand({ cwd: repoRoot, args, readOnly: true });
|
|
510
|
-
return result.code === 0;
|
|
511
|
-
}
|
|
512
|
-
function rethrowInfrastructureOr(fallback) {
|
|
513
|
-
return (error) => {
|
|
514
|
-
if (isResilientGitCommandError(error))
|
|
515
|
-
throw error;
|
|
516
|
-
return fallback;
|
|
517
|
-
};
|
|
498
|
+
async function git(repoRoot, args) { const result = await execFileAsync("git", args, { cwd: repoRoot, encoding: "utf8" }); return result.stdout.trim(); }
|
|
499
|
+
async function gitRaw(repoRoot, args) { const result = await execFileAsync("git", args, { cwd: repoRoot, encoding: "utf8" }); return result.stdout; }
|
|
500
|
+
async function gitOk(repoRoot, args) { try {
|
|
501
|
+
await git(repoRoot, args);
|
|
502
|
+
return true;
|
|
518
503
|
}
|
|
504
|
+
catch {
|
|
505
|
+
return false;
|
|
506
|
+
} }
|
|
519
507
|
async function checkpointMatches(repoRoot, head, checkpoint, spec) {
|
|
520
508
|
if (!await gitOk(repoRoot, ["merge-base", "--is-ancestor", checkpoint.commit, head]))
|
|
521
509
|
return false;
|
|
522
|
-
const messageText = await gitRaw(repoRoot, ["show", "-s", "--format=%B", checkpoint.commit]).catch(
|
|
510
|
+
const messageText = await gitRaw(repoRoot, ["show", "-s", "--format=%B", checkpoint.commit]).catch(() => "");
|
|
523
511
|
for (const trailer of [`Feature: ${spec.feature_id}`, `Task: ${spec.id}`, `Acceptance: ${spec.acceptance_refs.join(", ")}`, `Worker-Run: ${checkpoint.workerRunId}`])
|
|
524
512
|
if (!messageText.split(/\r?\n/).includes(trailer))
|
|
525
513
|
return false;
|
|
526
|
-
const actual = (await gitRaw(repoRoot, ["diff-tree", "--no-commit-id", "--name-only", "-r", checkpoint.commit]).catch(
|
|
514
|
+
const actual = (await gitRaw(repoRoot, ["diff-tree", "--no-commit-id", "--name-only", "-r", checkpoint.commit]).catch(() => "")).split(/\r?\n/).filter(Boolean).sort();
|
|
527
515
|
return JSON.stringify(actual) === JSON.stringify([...checkpoint.changedFiles].sort());
|
|
528
516
|
}
|
|
529
517
|
async function hashedRefValid(repoRoot, ref) { try {
|
|
@@ -7,7 +7,6 @@ import { artifactMatchesEvidenceToken, isShellVerificationToken, } from "../outc
|
|
|
7
7
|
import { readVerifiedOutcome } from "../outcomes/store.js";
|
|
8
8
|
import { getTaskPoolRoot, readFeatureTaskPoolStates } from "../pool/run-store.js";
|
|
9
9
|
import { workflowSchema } from "../task-spec/workflow-routing.js";
|
|
10
|
-
import { runResilientGitCommand } from "../../shared/resilient-git.js";
|
|
11
10
|
import { gitTransactionRecordSchema, transactionRecordPath } from "./git-transaction.js";
|
|
12
11
|
/**
|
|
13
12
|
* Feature Verification Bundle v1 — a hash-bound aggregate of completed typed
|
|
@@ -534,11 +533,10 @@ async function readGitRecord(repoRoot, featureId) {
|
|
|
534
533
|
}
|
|
535
534
|
}
|
|
536
535
|
async function defaultGit(repoRoot, args) {
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
return result.stdout.trim();
|
|
536
|
+
const { execFile } = await import("node:child_process");
|
|
537
|
+
const { promisify } = await import("node:util");
|
|
538
|
+
const execFileAsync = promisify(execFile);
|
|
539
|
+
return (await execFileAsync("git", args, { cwd: repoRoot, encoding: "utf8" })).stdout.trim();
|
|
542
540
|
}
|
|
543
541
|
// Re-export the workflow schema so consumers can validate typed workflows.
|
|
544
542
|
export { workflowSchema };
|
|
@@ -353,12 +353,7 @@ export function mountOperatorChrome(root, config = {}) {
|
|
|
353
353
|
|
|
354
354
|
const brand = el("a", "och-brand");
|
|
355
355
|
brand.href = model.homeHref;
|
|
356
|
-
brand
|
|
357
|
-
const brandMark = el("img", "och-brand-mark");
|
|
358
|
-
brandMark.src = "/inspect/favicon.svg";
|
|
359
|
-
brandMark.alt = "";
|
|
360
|
-
brandMark.setAttribute("aria-hidden", "true");
|
|
361
|
-
brand.appendChild(brandMark);
|
|
356
|
+
icon(brand, "radar");
|
|
362
357
|
brand.appendChild(el("span", null, model.brand));
|
|
363
358
|
global.appendChild(brand);
|
|
364
359
|
|