@tea-agent/loop-agent 0.33.7-beta.0 → 0.34.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 (101) hide show
  1. package/CHANGELOG.md +67 -16
  2. package/dist/application/task-lifecycle/advance.js +309 -9
  3. package/dist/application/task-lifecycle/gates.js +50 -0
  4. package/dist/application/task-lifecycle/observe.js +11 -2
  5. package/dist/application/task-lifecycle/plan-transitions.js +13 -21
  6. package/dist/commands/init-upgrade.js +32 -1
  7. package/dist/commands/init.js +94 -3
  8. package/dist/executors/dag-pi-executor.js +18 -3
  9. package/dist/executors/shell-executor.js +4 -91
  10. package/dist/executors/shell-write-guard.js +26 -8
  11. package/dist/shared/operator/capabilities.js +98 -44
  12. package/dist/shared/resilient-git.js +133 -0
  13. package/dist/task/source-prepare/artifact-meta.js +137 -0
  14. package/dist/task/source-prepare/index.js +2 -0
  15. package/dist/task/source-prepare/parse-intent.js +58 -10
  16. package/dist/task/source-prepare/prepare.js +229 -18
  17. package/dist/task/source-prepare/reference-integrity.js +18 -2
  18. package/dist/task/source-prepare/semantic-intake.js +404 -0
  19. package/dist/worker/console/app-data.js +2 -0
  20. package/dist/worker/console/chat/chat-event-store.js +190 -25
  21. package/dist/worker/console/chat/model-resolver.js +17 -0
  22. package/dist/worker/console/chat/pi-console-config.js +250 -32
  23. package/dist/worker/console/chat/pi-runtime.js +1007 -188
  24. package/dist/worker/console/chat/resource-loader.js +5 -4
  25. package/dist/worker/console/chat/routes.js +495 -157
  26. package/dist/worker/console/chat/runtime-context.js +48 -12
  27. package/dist/worker/console/chat/runtime-selection.js +59 -0
  28. package/dist/worker/console/chat/session-store.js +39 -0
  29. package/dist/worker/console/chat/shortcuts.js +1 -0
  30. package/dist/worker/console/chat/tool-adapter.js +9 -3
  31. package/dist/worker/console/chat/tools.js +5 -1
  32. package/dist/worker/console/dag-execution-receipt.js +380 -0
  33. package/dist/worker/console/interview/grill-me.js +7 -6
  34. package/dist/worker/console/operator-actions.js +785 -85
  35. package/dist/worker/console/prd-intake-bridge.js +393 -0
  36. package/dist/worker/console/recovery-cta.js +35 -6
  37. package/dist/worker/console/server.js +8 -15
  38. package/dist/worker/console/static/assets/index-BQkhJpV8.css +1 -0
  39. package/dist/worker/console/static/assets/index-BpuHmlSP.js +29 -0
  40. package/dist/worker/console/static/index.html +2 -2
  41. package/dist/worker/console/static-src/app/console-types.js +1 -0
  42. package/dist/worker/console/static-src/app/usePrdImport.js +2 -1
  43. package/dist/worker/console/static-src/app/useRecoveryActions.js +24 -1
  44. package/dist/worker/console/static-src/app/useRecoveryConsole.js +19 -1
  45. package/dist/worker/console/static-src/app/useTaskWizard.js +57 -2
  46. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
  47. package/dist/worker/console/static-src/operator-chat/landing-density.js +23 -0
  48. package/dist/worker/console/static-src/operator-chat/refs.js +9 -0
  49. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
  50. package/dist/worker/console/static-src/operator-chat/session-title-watcher.js +128 -0
  51. package/dist/worker/console/static-src/operator-chat/sidebar-split.js +90 -0
  52. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +37 -0
  53. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +125 -22
  54. package/dist/worker/console/static-src/operator-chat/useChatStream.js +215 -184
  55. package/dist/worker/console/static-src/operator-chat/useChatThread.js +49 -5
  56. package/dist/worker/console/static-src/operator-chat/useComposer.js +17 -0
  57. package/dist/worker/console/static-src/operator-chat/useOverlayFocus.js +84 -0
  58. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +225 -74
  59. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
  60. package/dist/worker/console/static-src/operator-chat/useWorkspaceLayout.js +58 -0
  61. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +31 -0
  62. package/dist/worker/delivery/final-verification.js +13 -5
  63. package/dist/worker/delivery/package.js +31 -19
  64. package/dist/worker/delivery/verification-bundle.js +6 -4
  65. package/dist/worker/observability/read-model.js +3 -0
  66. package/dist/worker/observe/static/operator-chrome.css +5 -2
  67. package/dist/worker/observe/static/operator-chrome.js +6 -1
  68. package/dist/worker/observe/static/styles.css +39 -9
  69. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +33 -462
  70. package/dist/workflows/dag/backend-test-case-manifest.js +0 -4
  71. package/dist/workflows/dag/backend-test-markdown-workflow.js +1 -25
  72. package/dist/workflows/dag/backend-test-module-stem.js +0 -5
  73. package/dist/workflows/dag/backend-test-pytest-collection.js +24 -345
  74. package/dist/workflows/dag/backend-test-scenario-param.js +82 -269
  75. package/dist/workflows/dag/backend-test-writer-completeness.js +16 -47
  76. package/dist/workflows/dag/dynamic-runtime/map.js +8 -24
  77. package/dist/workflows/dag/failure-category.js +3 -0
  78. package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
  79. package/dist/workflows/dag/init-hybrid.js +49 -55
  80. package/dist/workflows/dag/node-execution.js +3 -2
  81. package/dist/workflows/dag/retry-policy.js +44 -11
  82. package/dist/workflows/dag/runner.js +6 -0
  83. package/dist/workflows/dag/scheduler.js +49 -1
  84. package/dist/workflows/dag/types.js +0 -7
  85. package/dist/workflows/dag/validate.js +16 -2
  86. package/dist/workflows/dag/workspace-checkpoint.js +8 -27
  87. package/docs/templates/agent-dag.schema.json +4 -4
  88. package/docs/templates/backend-test-dag.json +29 -32
  89. package/harness.json +1 -1
  90. package/package.json +1 -1
  91. package/skills/local-jacoco-coverage/SKILL.md +281 -0
  92. package/skills/local-jacoco-coverage/references/requirement-to-source-mapping.md +85 -0
  93. package/skills/local-jacoco-coverage/references/runtime-alignment.md +106 -0
  94. package/skills/local-jacoco-coverage/scripts/run-coverage-analysis.sh +148 -0
  95. package/skills/local-jacoco-coverage/scripts/start-jacoco-agent.sh +110 -0
  96. package/skills/loop-agent/references/command-reference.md +3 -1
  97. package/skills/loop-agent/references/source-and-plan-practice.md +13 -0
  98. package/skills/loop-agent/references/task-workflow.md +4 -0
  99. package/dist/worker/console/chat/instruction-skills.js +0 -217
  100. package/dist/worker/console/static/assets/index-CnUXAqxG.css +0 -1
  101. package/dist/worker/console/static/assets/index-CteJFFL2.js +0 -29
@@ -0,0 +1,58 @@
1
+ import { useEffect, useState } from "react";
2
+ import { resolveWorkspaceLayoutMode, } from "./workspace-layout-mode.js";
3
+ import { resolveLandingDensity, } from "./landing-density.js";
4
+ const INITIAL = {
5
+ mode: "comfortable",
6
+ width: 1600,
7
+ landingDensity: "comfortable",
8
+ emptyHeight: 720,
9
+ };
10
+ /**
11
+ * Single ResizeObserver adapter for `.oc-workspace` width and optional empty
12
+ * messages height. Mode is never derived from UA or window.innerWidth alone.
13
+ */
14
+ export function useWorkspaceLayout(workspaceRef, emptyMessagesRef) {
15
+ const [snapshot, setSnapshot] = useState(INITIAL);
16
+ useEffect(() => {
17
+ const workspace = workspaceRef.current;
18
+ if (!workspace || typeof ResizeObserver === "undefined") {
19
+ return;
20
+ }
21
+ const measure = () => {
22
+ const width = workspace.getBoundingClientRect().width;
23
+ const emptyEl = emptyMessagesRef?.current ?? null;
24
+ const emptyHeight = emptyEl
25
+ ? emptyEl.getBoundingClientRect().height
26
+ : 720;
27
+ setSnapshot({
28
+ mode: resolveWorkspaceLayoutMode(width),
29
+ width,
30
+ landingDensity: resolveLandingDensity(emptyHeight),
31
+ emptyHeight,
32
+ });
33
+ };
34
+ measure();
35
+ const ro = new ResizeObserver(() => measure());
36
+ ro.observe(workspace);
37
+ // Poll briefly for empty-messages node mount without depending on its
38
+ // identity as a React effect dependency.
39
+ let emptyObserved = null;
40
+ const tryObserveEmpty = () => {
41
+ const emptyEl = emptyMessagesRef?.current ?? null;
42
+ if (emptyEl && emptyEl !== emptyObserved) {
43
+ if (emptyObserved)
44
+ ro.unobserve(emptyObserved);
45
+ ro.observe(emptyEl);
46
+ emptyObserved = emptyEl;
47
+ measure();
48
+ }
49
+ };
50
+ tryObserveEmpty();
51
+ const interval = window.setInterval(tryObserveEmpty, 250);
52
+ return () => {
53
+ window.clearInterval(interval);
54
+ ro.disconnect();
55
+ };
56
+ }, [workspaceRef, emptyMessagesRef]);
57
+ return snapshot;
58
+ }
@@ -0,0 +1,31 @@
1
+ /** Workspace layout modes resolved from measured Chat container width. */
2
+ /** Width thresholds in CSS px against `.oc-workspace` (not window/UA). */
3
+ export const WORKSPACE_LAYOUT_THRESHOLDS = {
4
+ comfortableMin: 1450,
5
+ compactMin: 1100,
6
+ };
7
+ /**
8
+ * Resolve the three-band workspace mode from a measured container width.
9
+ * Boundaries: comfortable >=1450, compact 1100-1449, narrow <1100.
10
+ */
11
+ export function resolveWorkspaceLayoutMode(widthPx) {
12
+ if (!Number.isFinite(widthPx) || widthPx < 0)
13
+ return "narrow";
14
+ if (widthPx >= WORKSPACE_LAYOUT_THRESHOLDS.comfortableMin)
15
+ return "comfortable";
16
+ if (widthPx >= WORKSPACE_LAYOUT_THRESHOLDS.compactMin)
17
+ return "compact";
18
+ return "narrow";
19
+ }
20
+ /** Whether process should render as a right overlay (no main reflow). */
21
+ export function processUsesOverlay(mode) {
22
+ return mode === "compact" || mode === "narrow";
23
+ }
24
+ /** Whether the permanent full sidebar is allowed. */
25
+ export function fullSidebarAllowed(mode) {
26
+ return mode === "comfortable";
27
+ }
28
+ /** Whether compact icon rail is shown. */
29
+ export function iconRailVisible(mode) {
30
+ return mode === "compact";
31
+ }
@@ -1,8 +1,7 @@
1
- import { execFile } from "node:child_process";
2
1
  import { createHash, randomUUID } from "node:crypto";
3
2
  import { mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
4
3
  import path from "node:path";
5
- import { promisify } from "node:util";
4
+ import { runResilientGitCommand } from "../../shared/resilient-git.js";
6
5
  import YAML from "yaml";
7
6
  import { controllerIdentitiesMatch, controllerIdentityExpectationFailure, resolveControllerIdentity, } from "../loop-agent/loop-agent-client.js";
8
7
  import { acceptanceSpecSchema } from "../task-graph/acceptance-schema.js";
@@ -15,7 +14,6 @@ import { taskSpecSchema } from "../task-spec/schema.js";
15
14
  import { preflightTargetRepo } from "../preflight.js";
16
15
  import { gitTransactionRecordSchema, transactionRecordPath } from "./git-transaction.js";
17
16
  import { buildFeatureVerificationBundle, writeFeatureVerificationBundle, } from "./verification-bundle.js";
18
- const execFileAsync = promisify(execFile);
19
17
  /**
20
18
  * Canonical Delivery evidence locations written by `feature verify-final`
21
19
  * (ADR 0007). Paths are repo-relative with forward slashes.
@@ -289,6 +287,16 @@ export async function writeEvidencePairAtomic(evidenceDir, value, fs = { rm }) {
289
287
  if (backedUp)
290
288
  await fs.rm(backup, { recursive: true, force: true }).catch(() => { });
291
289
  }
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; }
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
+ }
294
302
  function isNotFound(error) { return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT"); }
@@ -1,8 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { execFile } from "node:child_process";
3
2
  import { mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
4
3
  import path from "node:path";
5
- import { promisify } from "node:util";
4
+ import { isResilientGitCommandError, runResilientGitCommand } from "../../shared/resilient-git.js";
6
5
  import YAML from "yaml";
7
6
  import { z } from "zod";
8
7
  import { controllerIdentitiesMatch, LOOP_AGENT_PACKAGE_NAME, } from "../loop-agent/loop-agent-client.js";
@@ -13,7 +12,6 @@ import { validateFeatureTaskGraph } from "../task-graph/validate.js";
13
12
  import { taskSpecSchema } from "../task-spec/schema.js";
14
13
  import { gitTransactionRecordSchema, transactionRecordPath } from "./git-transaction.js";
15
14
  import { readVerifiedBundle, verifyBundleCurrentStateBindings, verifyBundleOutcomes, verifyBundleTaskSpecBindings } from "./verification-bundle.js";
16
- const execFileAsync = promisify(execFile);
17
15
  const hashedRefSchema = z.object({ path: z.string().min(1), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict();
18
16
  export const acceptanceCoverageArtifactSchema = z.object({
19
17
  schemaVersion: z.literal(1),
@@ -91,9 +89,9 @@ export async function prepareFeatureDelivery(input) {
91
89
  const now = input.now ?? new Date();
92
90
  const record = await readTransaction(repoRoot, featureId, blockers);
93
91
  if (record) {
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");
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"));
97
95
  if (branch !== record.branch)
98
96
  blockers.push(`current branch must be ${record.branch}`);
99
97
  if (head !== record.lastCheckpoint)
@@ -202,11 +200,11 @@ export async function validateDeliveryPackage(input) {
202
200
  blockers.push("Delivery Feature ownership mismatch");
203
201
  if (manifest.branch !== record.branch || manifest.baseBranch !== record.baseBranch || manifest.baseSha !== record.baseCommit || manifest.headSha !== record.lastCheckpoint)
204
202
  blockers.push("Delivery manifest does not match the Git transaction");
205
- const branch = await git(repoRoot, ["branch", "--show-current"]).catch(() => "");
206
- const head = await git(repoRoot, ["rev-parse", "HEAD"]).catch(() => "");
203
+ const branch = await git(repoRoot, ["branch", "--show-current"]).catch(rethrowInfrastructureOr(""));
204
+ const head = await git(repoRoot, ["rev-parse", "HEAD"]).catch(rethrowInfrastructureOr(""));
207
205
  if (branch !== record.branch || head !== record.lastCheckpoint)
208
206
  blockers.push("current Git position does not match Delivery");
209
- const rawCloseoutStatus = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]).catch(() => "git-error");
207
+ const rawCloseoutStatus = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]).catch(rethrowInfrastructureOr("git-error"));
210
208
  const allowedDirtyPaths = new Set((input.allowedDirtyPaths ?? []).map((item) => item.replace(/\\/g, "/")));
211
209
  const closeoutStatus = rawCloseoutStatus.split("\n").filter(Boolean).filter((line) => !allowedDirtyPaths.has(line.slice(3).replace(/\\/g, "/"))).join("\n").trim();
212
210
  if (closeoutStatus)
@@ -495,23 +493,37 @@ async function writeDeliveryArtifacts(artifacts, coverage, manifest) {
495
493
  }
496
494
  }
497
495
  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`; }
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;
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
+ };
503
518
  }
504
- catch {
505
- return false;
506
- } }
507
519
  async function checkpointMatches(repoRoot, head, checkpoint, spec) {
508
520
  if (!await gitOk(repoRoot, ["merge-base", "--is-ancestor", checkpoint.commit, head]))
509
521
  return false;
510
- const messageText = await gitRaw(repoRoot, ["show", "-s", "--format=%B", checkpoint.commit]).catch(() => "");
522
+ const messageText = await gitRaw(repoRoot, ["show", "-s", "--format=%B", checkpoint.commit]).catch(rethrowInfrastructureOr(""));
511
523
  for (const trailer of [`Feature: ${spec.feature_id}`, `Task: ${spec.id}`, `Acceptance: ${spec.acceptance_refs.join(", ")}`, `Worker-Run: ${checkpoint.workerRunId}`])
512
524
  if (!messageText.split(/\r?\n/).includes(trailer))
513
525
  return false;
514
- const actual = (await gitRaw(repoRoot, ["diff-tree", "--no-commit-id", "--name-only", "-r", checkpoint.commit]).catch(() => "")).split(/\r?\n/).filter(Boolean).sort();
526
+ const actual = (await gitRaw(repoRoot, ["diff-tree", "--no-commit-id", "--name-only", "-r", checkpoint.commit]).catch(rethrowInfrastructureOr(""))).split(/\r?\n/).filter(Boolean).sort();
515
527
  return JSON.stringify(actual) === JSON.stringify([...checkpoint.changedFiles].sort());
516
528
  }
517
529
  async function hashedRefValid(repoRoot, ref) { try {
@@ -7,6 +7,7 @@ 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";
10
11
  import { gitTransactionRecordSchema, transactionRecordPath } from "./git-transaction.js";
11
12
  /**
12
13
  * Feature Verification Bundle v1 — a hash-bound aggregate of completed typed
@@ -533,10 +534,11 @@ async function readGitRecord(repoRoot, featureId) {
533
534
  }
534
535
  }
535
536
  async function defaultGit(repoRoot, args) {
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();
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();
540
542
  }
541
543
  // Re-export the workflow schema so consumers can validate typed workflows.
542
544
  export { workflowSchema };
@@ -1488,6 +1488,7 @@ function mergeDagRun(existing, incoming) {
1488
1488
  executionMode: incoming.executionMode ?? existing.executionMode,
1489
1489
  liveness: incoming.liveness ?? existing.liveness,
1490
1490
  effectiveStatus: incoming.effectiveStatus ?? existing.effectiveStatus,
1491
+ failureCategory: incoming.failureCategory ?? existing.failureCategory,
1491
1492
  stateConsistent: incoming.stateConsistent ?? existing.stateConsistent,
1492
1493
  recoveryEligibility: incoming.recoveryEligibility ?? existing.recoveryEligibility,
1493
1494
  title: incoming.title ?? existing.title,
@@ -1893,6 +1894,7 @@ async function parseDagStateFile(statePath, now) {
1893
1894
  : {}),
1894
1895
  }
1895
1896
  : undefined;
1897
+ const failureCategory = readString(parsed, "failureCategory");
1896
1898
  return {
1897
1899
  dagRunId,
1898
1900
  status,
@@ -1900,6 +1902,7 @@ async function parseDagStateFile(statePath, now) {
1900
1902
  ...(executionMode ? { executionMode } : {}),
1901
1903
  liveness: liveness.status,
1902
1904
  effectiveStatus,
1905
+ ...(failureCategory ? { failureCategory } : {}),
1903
1906
  stateConsistent,
1904
1907
  ...(recoveryEligibility ? { recoveryEligibility } : {}),
1905
1908
  ...(title ? { title } : {}),
@@ -71,8 +71,11 @@
71
71
  text-decoration: none;
72
72
  white-space: nowrap;
73
73
  }
74
- .och-brand svg {
75
- color: var(--och-orange);
74
+ .och-brand-mark {
75
+ display: block;
76
+ width: 32px;
77
+ height: 32px;
78
+ object-fit: contain;
76
79
  flex-shrink: 0;
77
80
  }
78
81
  .och-brand:hover {
@@ -353,7 +353,12 @@ export function mountOperatorChrome(root, config = {}) {
353
353
 
354
354
  const brand = el("a", "och-brand");
355
355
  brand.href = model.homeHref;
356
- icon(brand, "radar");
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);
357
362
  brand.appendChild(el("span", null, model.brand));
358
363
  global.appendChild(brand);
359
364
 
@@ -1617,7 +1617,11 @@ body.is-resizing-dag-graph {
1617
1617
  /* 历史失败已恢复/已覆盖:独立中性样式(不占用当前终态失败红色,不洗白事实)。 */
1618
1618
  .dag-trajectory-occurrence.is-recovered {
1619
1619
  border-color: color-mix(in srgb, var(--amber, #b58900) 55%, var(--hairline));
1620
- background: color-mix(in srgb, var(--amber-soft, #fff7d6) 55%, var(--surface));
1620
+ background: color-mix(
1621
+ in srgb,
1622
+ var(--amber-soft, #fff7d6) 55%,
1623
+ var(--surface)
1624
+ );
1621
1625
  }
1622
1626
  .dag-trajectory-recovered {
1623
1627
  font-size: 11px;
@@ -1647,9 +1651,14 @@ body.is-resizing-dag-graph {
1647
1651
  }
1648
1652
  .dag-trajectory-banner {
1649
1653
  padding: 8px 12px;
1650
- border: 1px solid color-mix(in srgb, var(--amber, #b58900) 50%, var(--hairline));
1654
+ border: 1px solid
1655
+ color-mix(in srgb, var(--amber, #b58900) 50%, var(--hairline));
1651
1656
  border-radius: var(--radius-lg);
1652
- background: color-mix(in srgb, var(--amber-soft, #fff7d6) 60%, var(--surface));
1657
+ background: color-mix(
1658
+ in srgb,
1659
+ var(--amber-soft, #fff7d6) 60%,
1660
+ var(--surface)
1661
+ );
1653
1662
  color: var(--body);
1654
1663
  font-size: 12px;
1655
1664
  }
@@ -3050,7 +3059,11 @@ button.copy-command:focus-visible {
3050
3059
  }
3051
3060
  .dags-copy-btn:active {
3052
3061
  border-color: var(--hairline-strong, #cfcdc4);
3053
- background: color-mix(in srgb, var(--surface-muted, #f0efe9) 80%, var(--hairline, #e6e5e0));
3062
+ background: color-mix(
3063
+ in srgb,
3064
+ var(--surface-muted, #f0efe9) 80%,
3065
+ var(--hairline, #e6e5e0)
3066
+ );
3054
3067
  }
3055
3068
  /* Mouse focus must not leave residual orange chrome; orange is :focus-visible only. */
3056
3069
  .dags-copy-btn:focus:not(:focus-visible) {
@@ -3201,12 +3214,20 @@ button.copy-command:focus-visible {
3201
3214
  }
3202
3215
  .dags-page-num:hover:not(:disabled),
3203
3216
  .dags-page-nav:hover:not(:disabled) {
3204
- background: color-mix(in srgb, var(--orange-soft, #fff1ea) 70%, var(--surface-muted, #f0efe9));
3217
+ background: color-mix(
3218
+ in srgb,
3219
+ var(--orange-soft, #fff1ea) 70%,
3220
+ var(--surface-muted, #f0efe9)
3221
+ );
3205
3222
  color: var(--ink, #26251e);
3206
3223
  }
3207
3224
  .dags-page-num:active:not(:disabled),
3208
3225
  .dags-page-nav:active:not(:disabled) {
3209
- background: color-mix(in srgb, var(--orange, #f54e00) 14%, var(--surface-muted, #f0efe9));
3226
+ background: color-mix(
3227
+ in srgb,
3228
+ var(--orange, #f54e00) 14%,
3229
+ var(--surface-muted, #f0efe9)
3230
+ );
3210
3231
  }
3211
3232
  .dags-page-num:focus-visible,
3212
3233
  .dags-page-nav:focus-visible,
@@ -3224,7 +3245,8 @@ button.copy-command:focus-visible {
3224
3245
  border-color: var(--orange, #f54e00);
3225
3246
  color: #fff;
3226
3247
  opacity: 1;
3227
- box-shadow: 0 2px 8px color-mix(in srgb, var(--orange, #f54e00) 35%, transparent);
3248
+ box-shadow: 0 2px 8px
3249
+ color-mix(in srgb, var(--orange, #f54e00) 35%, transparent);
3228
3250
  cursor: default;
3229
3251
  }
3230
3252
  .dags-page-nav:disabled,
@@ -3320,7 +3342,11 @@ button.copy-command:focus-visible {
3320
3342
  background: var(--surface-muted, #f0efe9);
3321
3343
  }
3322
3344
  .dags-refresh-btn:active:not(:disabled) {
3323
- background: color-mix(in srgb, var(--surface-muted, #f0efe9) 80%, var(--hairline, #e6e5e0));
3345
+ background: color-mix(
3346
+ in srgb,
3347
+ var(--surface-muted, #f0efe9) 80%,
3348
+ var(--hairline, #e6e5e0)
3349
+ );
3324
3350
  }
3325
3351
  .dags-pagination.is-busy {
3326
3352
  opacity: 0.92;
@@ -3516,7 +3542,11 @@ button.copy-command:focus-visible {
3516
3542
  }
3517
3543
  .custom-select-option.is-selected.is-active,
3518
3544
  .custom-select-option.is-selected:hover {
3519
- background: color-mix(in srgb, var(--orange-soft, #fff1ea) 70%, var(--surface-muted, #f0efe9));
3545
+ background: color-mix(
3546
+ in srgb,
3547
+ var(--orange-soft, #fff1ea) 70%,
3548
+ var(--surface-muted, #f0efe9)
3549
+ );
3520
3550
  }
3521
3551
  .custom-select-option-label {
3522
3552
  flex: 1 1 auto;