@tea-agent/loop-agent 0.33.6 → 0.33.7-beta.1

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +39 -23
  2. package/dist/application/task-lifecycle/advance.js +4 -254
  3. package/dist/application/task-lifecycle/gates.js +0 -50
  4. package/dist/application/task-lifecycle/observe.js +2 -11
  5. package/dist/commands/init-upgrade.js +1 -32
  6. package/dist/commands/init.js +3 -94
  7. package/dist/executors/shell-executor.js +91 -4
  8. package/dist/executors/shell-write-guard.js +8 -26
  9. package/dist/shared/operator/capabilities.js +42 -72
  10. package/dist/task/source-prepare/index.js +0 -2
  11. package/dist/task/source-prepare/parse-intent.js +10 -58
  12. package/dist/task/source-prepare/prepare.js +16 -180
  13. package/dist/task/source-prepare/reference-integrity.js +2 -18
  14. package/dist/worker/console/app-data.js +0 -2
  15. package/dist/worker/console/chat/chat-event-store.js +25 -190
  16. package/dist/worker/console/chat/instruction-skills.js +217 -0
  17. package/dist/worker/console/chat/pi-console-config.js +32 -250
  18. package/dist/worker/console/chat/pi-runtime.js +71 -625
  19. package/dist/worker/console/chat/resource-loader.js +4 -5
  20. package/dist/worker/console/chat/routes.js +146 -324
  21. package/dist/worker/console/chat/runtime-context.js +12 -48
  22. package/dist/worker/console/chat/runtime-selection.js +0 -59
  23. package/dist/worker/console/chat/shortcuts.js +0 -1
  24. package/dist/worker/console/chat/tool-adapter.js +3 -9
  25. package/dist/worker/console/chat/tools.js +1 -5
  26. package/dist/worker/console/operator-actions.js +68 -559
  27. package/dist/worker/console/server.js +15 -8
  28. package/dist/worker/console/static/assets/index-CnUXAqxG.css +1 -0
  29. package/dist/worker/console/static/assets/index-CteJFFL2.js +29 -0
  30. package/dist/worker/console/static/index.html +2 -2
  31. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +8 -45
  32. package/dist/worker/console/static-src/operator-chat/refs.js +0 -9
  33. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +0 -16
  34. package/dist/worker/console/static-src/operator-chat/useChatStream.js +184 -210
  35. package/dist/worker/console/static-src/operator-chat/useChatThread.js +5 -49
  36. package/dist/worker/console/static-src/operator-chat/useComposer.js +0 -17
  37. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +74 -225
  38. package/dist/worker/delivery/final-verification.js +5 -13
  39. package/dist/worker/delivery/package.js +19 -31
  40. package/dist/worker/delivery/verification-bundle.js +4 -6
  41. package/dist/worker/observe/static/operator-chrome.css +2 -5
  42. package/dist/worker/observe/static/operator-chrome.js +1 -6
  43. package/dist/worker/observe/static/styles.css +9 -39
  44. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +462 -33
  45. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  46. package/dist/workflows/dag/backend-test-markdown-workflow.js +25 -1
  47. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  48. package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
  49. package/dist/workflows/dag/backend-test-scenario-param.js +552 -124
  50. package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
  51. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  52. package/dist/workflows/dag/frontend-worktree-diff.js +27 -12
  53. package/dist/workflows/dag/init-hybrid.js +49 -37
  54. package/dist/workflows/dag/types.js +7 -0
  55. package/dist/workflows/dag/workspace-checkpoint.js +27 -8
  56. package/docs/templates/backend-test-dag.json +33 -30
  57. package/harness.json +1 -1
  58. package/package.json +1 -1
  59. package/skills/loop-agent/references/command-reference.md +1 -3
  60. package/skills/loop-agent/references/source-and-plan-practice.md +0 -13
  61. package/skills/loop-agent/references/task-workflow.md +0 -4
  62. package/dist/shared/resilient-git.js +0 -133
  63. package/dist/task/source-prepare/artifact-meta.js +0 -137
  64. package/dist/task/source-prepare/semantic-intake.js +0 -404
  65. package/dist/worker/console/dag-execution-receipt.js +0 -380
  66. package/dist/worker/console/static/assets/index-BUOLppPr.js +0 -28
  67. package/dist/worker/console/static/assets/index-C1KzazY5.css +0 -1
  68. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +0 -257
  69. 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
- // Models + thinking levels: AC-01/AC-06 — the GET may reopen a historical
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
- setModelsStatus("idle");
35
+ setRuntimeContext(null);
64
36
  return;
65
37
  }
66
38
  const sessionId = session.sessionId;
67
- setModelsStatus("loading");
68
- setModelsError(null);
69
- try {
70
- const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}/models`, { credentials: "include" });
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
- setModelsStatus("ready");
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(async (r) => {
108
- const body = await r.json().catch(() => null);
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
- // RF5: the response body NEVER writes model/thinkingLevel — it
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 adoptedSnapshot = getRuntimeSnapshotState().snapshot;
130
- setRuntimeContext((current) => mergeRuntimeContextWithSnapshot(current, ctx, adoptedSnapshot, sessionId));
131
- })
132
- .catch(() => {
133
- if (refs.sessionRef.current?.sessionId !== sessionId)
134
- return;
135
- setRuntimeContextError("注入上下文读取失败,请重试");
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
- // AC-01/RF3-01: hydrate the Composer from the unified snapshot — real
139
- // model and thinking selection; `auto` displays the Pi actual effective
140
- // level. Derived session/runtimeContext state ONLY comes from a snapshot
141
- // the reducer guards accepted: the mutation success branch never writes
142
- // session/runtimeContext directly, so this effect re-runs on adoption and
143
- // is a no-op for rejected/missing snapshots (zero derived writes).
144
- useEffect(() => {
145
- if (!snapshot || !session || snapshot.sessionId !== session.sessionId)
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 !== snapshot.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
- if (!current)
159
- return current;
160
- const next = {
114
+ setRuntimeContext((current) => current
115
+ ? {
161
116
  ...current,
162
- ...(patch.model ? { model: patch.model } : {}),
163
- ...(patch.thinkingLevel
164
- ? { thinkingLevel: patch.thinkingLevel }
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
- if (next.model?.provider === current.model?.provider &&
168
- next.model?.modelId === current.model?.modelId &&
169
- next.thinkingLevel === current.thinkingLevel) {
170
- return current;
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
- if (!payload.session)
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
- // AC-01/AC-02: thinking selection shows "auto" when no explicit override,
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
- effectiveModels.some((model) => formatModelRef(model.provider, model.id) === activeModelRef);
188
+ models.some((model) => formatModelRef(model.provider, model.id) === activeModelRef);
330
189
  return {
331
190
  capabilities,
332
- models: effectiveModels,
333
- modelsStatus,
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 { runResilientGitCommand } from "../../shared/resilient-git.js";
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
- const result = await runResilientGitCommand({ cwd: repoRoot, args, readOnly: true });
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 { isResilientGitCommandError, runResilientGitCommand } from "../../shared/resilient-git.js";
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(rethrowInfrastructureOr(""));
93
- const head = await git(repoRoot, ["rev-parse", "HEAD"]).catch(rethrowInfrastructureOr(""));
94
- const status = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]).catch(rethrowInfrastructureOr("git-error"));
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(rethrowInfrastructureOr(""));
204
- const head = await git(repoRoot, ["rev-parse", "HEAD"]).catch(rethrowInfrastructureOr(""));
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(rethrowInfrastructureOr("git-error"));
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
- const result = await runResilientGitCommand({ cwd: repoRoot, args, readOnly: true });
498
- if (result.code !== 0)
499
- throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`);
500
- return result.stdout.trim();
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(rethrowInfrastructureOr(""));
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(rethrowInfrastructureOr(""))).split(/\r?\n/).filter(Boolean).sort();
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 result = await runResilientGitCommand({ cwd: repoRoot, args, readOnly: true });
538
- if (result.code !== 0) {
539
- throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`);
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 };
@@ -71,11 +71,8 @@
71
71
  text-decoration: none;
72
72
  white-space: nowrap;
73
73
  }
74
- .och-brand-mark {
75
- display: block;
76
- width: 32px;
77
- height: 32px;
78
- object-fit: contain;
74
+ .och-brand svg {
75
+ color: var(--och-orange);
79
76
  flex-shrink: 0;
80
77
  }
81
78
  .och-brand:hover {
@@ -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.setAttribute("aria-label", `${model.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