@tea-agent/loop-agent 0.24.9 → 0.24.11-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.
Files changed (31) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/commands/init.js +2 -16
  3. package/dist/executors/shell-executor.js +6 -122
  4. package/dist/worker/cli.js +14 -59
  5. package/dist/worker/delivery/git-transaction.js +7 -32
  6. package/dist/worker/delivery/verification-bundle.js +5 -26
  7. package/dist/worker/observe/routes.js +0 -17
  8. package/dist/worker/observe/static/api.js +0 -9
  9. package/dist/worker/observe/static/constants.js +0 -9
  10. package/dist/worker/observe/static/state.js +0 -14
  11. package/dist/worker/observe/static/styles.css +0 -74
  12. package/dist/worker/observe/static/views/dag-inspector.js +15 -371
  13. package/dist/worker/outcomes/projector.js +1 -5
  14. package/dist/worker/runner/run-ready.js +1 -41
  15. package/dist/workflows/dag/backend-test-markdown-workflow.js +9 -75
  16. package/dist/workflows/dag/backend-test-result-contract.js +0 -103
  17. package/dist/workflows/dag/frontend-test-result-contract.js +41 -106
  18. package/dist/workflows/dag/init-hybrid.js +14 -13
  19. package/dist/workflows/dag/node-execution.js +2 -3
  20. package/dist/workflows/dag/types.js +0 -2
  21. package/dist/workflows/dag/validate.js +2 -3
  22. package/docs/architecture/worker-and-feature.md +11 -0
  23. package/docs/init-surface.manifest.json +3 -0
  24. package/docs/templates/agent-dag.schema.json +9 -0
  25. package/docs/templates/frontend-implementation-contract.schema.json +2 -2
  26. package/package.json +1 -1
  27. package/skills/frontend-bounded-implement/SKILL.md +53 -0
  28. package/skills/frontend-implementation/SKILL.md +3 -7
  29. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  30. package/dist/worker/observe/dag-run-artifacts.js +0 -90
  31. package/dist/worker/observe/node-input.js +0 -444
@@ -2010,80 +2010,6 @@ body.is-resizing-dag-graph {
2010
2010
  .spec-evidence-section { padding-bottom: var(--space-4); border-bottom: 1px solid var(--hairline); }
2011
2011
  .spec-evidence-section + .spec-evidence-section { margin-top: var(--space-4); }
2012
2012
  .spec-evidence-section h4 { margin: 0 0 var(--space-2); color: var(--ink); font-size: 12px; font-weight: 700; }
2013
-
2014
- .node-input-root { display: flex; flex-direction: column; gap: var(--space-3); }
2015
- .node-input-semantics {
2016
- margin: 0;
2017
- font-size: 12px;
2018
- font-weight: 600;
2019
- color: var(--muted);
2020
- }
2021
- .node-input-section {
2022
- padding-bottom: var(--space-3);
2023
- border-bottom: 1px solid var(--hairline);
2024
- }
2025
- .node-input-section h4 {
2026
- margin: 0 0 var(--space-2);
2027
- color: var(--ink);
2028
- font-size: 12px;
2029
- font-weight: 700;
2030
- }
2031
- .node-input-section h5 {
2032
- margin: var(--space-2) 0;
2033
- font-size: 12px;
2034
- font-weight: 600;
2035
- }
2036
- .node-input-summary {
2037
- display: grid;
2038
- grid-template-columns: max-content 1fr;
2039
- gap: 4px 12px;
2040
- margin: 0;
2041
- font-size: 12px;
2042
- }
2043
- .node-input-summary dt {
2044
- margin: 0;
2045
- color: var(--muted);
2046
- font-weight: 600;
2047
- }
2048
- .node-input-summary dd {
2049
- margin: 0;
2050
- color: var(--ink);
2051
- word-break: break-word;
2052
- }
2053
- .node-input-inherited {
2054
- display: inline-block;
2055
- margin-left: 4px;
2056
- padding: 0 6px;
2057
- border-radius: 999px;
2058
- background: var(--surface-2, #f3f1ec);
2059
- color: var(--muted);
2060
- font-size: 11px;
2061
- font-weight: 600;
2062
- }
2063
- .node-input-code-list,
2064
- .node-input-command-list {
2065
- margin: var(--space-2) 0 0;
2066
- padding-left: 1.2rem;
2067
- }
2068
- .node-input-command-list pre {
2069
- margin: 4px 0;
2070
- padding: 8px;
2071
- overflow: auto;
2072
- background: var(--surface-2, #f3f1ec);
2073
- border-radius: 6px;
2074
- font-size: 12px;
2075
- }
2076
- .node-input-truncated,
2077
- .node-input-warning {
2078
- margin: var(--space-2) 0 0;
2079
- font-size: 12px;
2080
- color: var(--muted);
2081
- }
2082
- .node-input-error {
2083
- margin: 0;
2084
- color: var(--danger, #b42318);
2085
- font-size: 13px;
2086
- }
2087
2013
  .spec-evidence-summary { margin: var(--space-2) 0 0; color: var(--body); font-size: 12px; line-height: 1.6; }
2088
2014
  .spec-evidence-list { display: grid; gap: 6px; margin: var(--space-2) 0 0; padding: 0; list-style: none; }
2089
2015
  .spec-evidence-list li { display: flex; align-items: flex-start; gap: 6px; color: var(--body); font-size: 12px; line-height: 1.5; overflow-wrap: anywhere; }
@@ -10,13 +10,12 @@ import {
10
10
  UI_TEXT,
11
11
  } from "../constants.js";
12
12
  import { clearNode, el } from "../dom.js";
13
- import { fetchJson, fetchSpecEvidenceFile, fetchDagNodeInput } from "../api.js";
13
+ import { fetchJson, fetchSpecEvidenceFile } from "../api.js";
14
14
  import { badge } from "../format.js";
15
15
  import { renderMarkdown } from "../markdown-render.js";
16
16
  import {
17
17
  uiState,
18
18
  makeSessionEventIdentity,
19
- makeDagNodeInputIdentity,
20
19
  openSpecEvidenceDetail,
21
20
  closeSpecEvidenceDetail,
22
21
  updateDagTimelineViewportState,
@@ -51,13 +50,9 @@ export function selectDagNode(dagRunId, nodeId) {
51
50
  // into node B's spec-evidence panel (AC-007 regression guard).
52
51
  uiState.specEvidenceDetail = null;
53
52
  uiState.specEvidenceListEvidence = null;
54
- uiState.dagNodeInputData = null;
55
- uiState.dagNodeInputError = null;
56
- uiState.dagNodeInputLoading = false;
57
53
  // Bump the session-event identity token so any in-flight response for the
58
54
  // previous node is rejected before it can mutate the current node cache.
59
55
  uiState.sessionEventIdentity = makeSessionEventIdentity(dagRunId, nodeId);
60
- uiState.dagNodeInputIdentity = makeDagNodeInputIdentity(dagRunId, nodeId);
61
56
  }
62
57
  void import("./dag.js").then((m) => m.renderDagDetail(dagRunId, false));
63
58
  }
@@ -535,337 +530,16 @@ function renderSpecEvidenceDetail(content, dagRunId, nodeId) {
535
530
  content.appendChild(wrapper);
536
531
  }
537
532
 
538
- function sourceLabel(source) {
539
- if (source === "defaults") return "继承默认值";
540
- if (source === "schema-default") return "schema 默认";
541
- if (source === "merged") return "合并";
542
- return null;
543
- }
544
-
545
- function appendMetaRow(dl, label, value, source) {
546
- if (value == null || value === "") return;
547
- const dt = el("dt", null, label);
548
- const dd = el("dd", null, String(value));
549
- const inherited = sourceLabel(source);
550
- if (inherited) {
551
- dd.appendChild(document.createTextNode(" "));
552
- dd.appendChild(el("span", "node-input-inherited", inherited));
553
- }
554
- dl.appendChild(dt);
555
- dl.appendChild(dd);
556
- }
557
-
558
- function appendCodeList(parent, title, values) {
559
- if (!values || values.length === 0) return;
560
- const section = el("section", "node-input-section");
561
- section.appendChild(el("h4", null, title));
562
- const list = el("ul", "node-input-code-list");
563
- for (const value of values) {
564
- const item = document.createElement("li");
565
- const code = document.createElement("code");
566
- code.textContent = String(value);
567
- item.appendChild(code);
568
- list.appendChild(item);
569
- }
570
- section.appendChild(list);
571
- parent.appendChild(section);
572
- }
573
-
574
- function renderDagNodeInputDetail(content, data) {
575
- const wrap = el("div", "node-input-root");
576
- wrap.appendChild(
577
- el("p", "node-input-semantics", UI_TEXT.inputFrozenLabel),
578
- );
579
-
580
- if (data.summary) {
581
- const section = el("section", "node-input-section");
582
- section.appendChild(el("h4", null, "摘要"));
583
- const dl = el("dl", "node-input-summary");
584
- appendMetaRow(
585
- dl,
586
- "executor",
587
- data.summary.executor,
588
- data.summary.executorSource,
589
- );
590
- appendMetaRow(dl, "role", data.summary.role, null);
591
- appendMetaRow(dl, "complexity", data.summary.complexity, null);
592
- appendMetaRow(
593
- dl,
594
- "writePolicy",
595
- data.summary.writePolicy,
596
- data.summary.writePolicySource,
597
- );
598
- appendMetaRow(dl, "toolProfile", data.summary.toolProfile, null);
599
- section.appendChild(dl);
600
- wrap.appendChild(section);
601
- }
602
-
603
- if (data.dependency) {
604
- const section = el("section", "node-input-section");
605
- section.appendChild(el("h4", null, "依赖"));
606
- const dl = el("dl", "node-input-summary");
607
- appendMetaRow(
608
- dl,
609
- "depends_on",
610
- (data.dependency.dependsOn ?? []).join(", ") || "(none)",
611
- null,
612
- );
613
- if (data.dependency.dependsPolicy) {
614
- appendMetaRow(dl, "dependsPolicy", data.dependency.dependsPolicy, null);
615
- }
616
- if ((data.dependency.failureAwareDependsOn ?? []).length > 0) {
617
- appendMetaRow(
618
- dl,
619
- "failureAwareDependsOn",
620
- data.dependency.failureAwareDependsOn.join(", "),
621
- null,
622
- );
623
- }
624
- if (data.dependency.runIf) {
625
- appendMetaRow(dl, "runIf", data.dependency.runIf, null);
626
- }
627
- section.appendChild(dl);
628
- wrap.appendChild(section);
629
- }
630
-
631
- if (data.prompt) {
632
- const section = el("section", "node-input-section");
633
- section.appendChild(el("h4", null, "任务正文"));
634
- if (data.prompt.source?.path) {
635
- const meta = el("p", "muted");
636
- meta.textContent = `source: ${data.prompt.source.path}${
637
- data.prompt.source.sha256Prefix
638
- ? ` (sha256 ${data.prompt.source.sha256Prefix}…)`
639
- : ""
640
- }`;
641
- section.appendChild(meta);
642
- }
643
- section.appendChild(renderMarkdown(data.prompt.text ?? ""));
644
- if (data.prompt.truncated) {
645
- section.appendChild(
646
- el(
647
- "p",
648
- "node-input-truncated",
649
- `已截断,仅显示前 ${data.prompt.maxBytes ?? 0} 字节(UTF-8)。`,
650
- ),
651
- );
652
- }
653
- wrap.appendChild(section);
654
- }
655
-
656
- if (data.executorInput) {
657
- const section = el("section", "node-input-section");
658
- section.appendChild(el("h4", null, "执行器输入摘要"));
659
- const dl = el("dl", "node-input-summary");
660
- appendMetaRow(dl, "kind", data.executorInput.kind, null);
661
- if (data.executorInput.modelHint) {
662
- appendMetaRow(dl, "modelHint", data.executorInput.modelHint, null);
663
- }
664
- section.appendChild(dl);
665
- appendCodeList(section, "skills", data.executorInput.skills);
666
- if (data.executorInput.shell) {
667
- const shell = data.executorInput.shell;
668
- const shellDl = el("dl", "node-input-summary");
669
- appendMetaRow(shellDl, "preset", shell.preset, null);
670
- appendMetaRow(shellDl, "cwd", shell.cwd, null);
671
- appendMetaRow(shellDl, "timeoutMs", shell.timeoutMs, null);
672
- appendMetaRow(
673
- shellDl,
674
- "nonZeroExitPolicy",
675
- shell.nonZeroExitPolicy,
676
- null,
677
- );
678
- appendMetaRow(
679
- shellDl,
680
- "gates",
681
- (shell.gates ?? []).join(", ") || "(none)",
682
- null,
683
- );
684
- section.appendChild(shellDl);
685
- if ((shell.commands ?? []).length > 0) {
686
- const list = el("ol", "node-input-command-list");
687
- for (const command of shell.commands) {
688
- const item = document.createElement("li");
689
- const pre = document.createElement("pre");
690
- const code = document.createElement("code");
691
- code.textContent = String(command);
692
- pre.appendChild(code);
693
- item.appendChild(pre);
694
- list.appendChild(item);
695
- }
696
- section.appendChild(list);
697
- }
698
- }
699
- if (data.executorInput.static) {
700
- const staticBlock = data.executorInput.static;
701
- const staticDl = el("dl", "node-input-summary");
702
- appendMetaRow(staticDl, "status", staticBlock.status, null);
703
- section.appendChild(staticDl);
704
- section.appendChild(renderMarkdown(staticBlock.resultPreview ?? ""));
705
- if (staticBlock.truncated) {
706
- section.appendChild(
707
- el(
708
- "p",
709
- "node-input-truncated",
710
- `已截断,仅显示前 ${staticBlock.maxBytes ?? 0} 字节(UTF-8)。`,
711
- ),
712
- );
713
- }
714
- }
715
- wrap.appendChild(section);
716
- }
717
-
718
- if (data.boundaries) {
719
- const section = el("section", "node-input-section");
720
- section.appendChild(el("h4", null, "边界与输出契约"));
721
- const dl = el("dl", "node-input-summary");
722
- appendMetaRow(dl, "outputMode", data.boundaries.outputMode, null);
723
- appendMetaRow(
724
- dl,
725
- "firstProtocolLine",
726
- data.boundaries.firstProtocolLine,
727
- null,
728
- );
729
- section.appendChild(dl);
730
- if (data.boundaries.outputContract) {
731
- section.appendChild(el("h5", null, "outputContract"));
732
- section.appendChild(renderMarkdown(data.boundaries.outputContract));
733
- }
734
- appendCodeList(section, "allowedPaths", data.boundaries.allowedPaths);
735
- appendCodeList(section, "forbiddenPaths", data.boundaries.forbiddenPaths);
736
- appendCodeList(section, "writeSet", data.boundaries.writeSet);
737
- wrap.appendChild(section);
738
- }
739
-
740
- if (data.promptFingerprint?.exists) {
741
- const section = el("section", "node-input-section");
742
- section.appendChild(el("h4", null, "Assembled prompt 指纹"));
743
- const dl = el("dl", "node-input-summary");
744
- appendMetaRow(
745
- dl,
746
- "sha256",
747
- data.promptFingerprint.sha256Prefix ?? "(parse failed)",
748
- null,
749
- );
750
- appendMetaRow(
751
- dl,
752
- "lengthChars",
753
- data.promptFingerprint.lengthChars,
754
- null,
755
- );
756
- appendMetaRow(dl, "artifact", data.promptFingerprint.artifact, null);
757
- section.appendChild(dl);
758
- section.appendChild(
759
- el("p", "muted", UI_TEXT.inputFingerprintNote),
760
- );
761
- wrap.appendChild(section);
762
- }
763
-
764
- if ((data.warnings ?? []).length > 0) {
765
- const section = el("section", "node-input-section");
766
- section.appendChild(el("h4", null, "警告"));
767
- for (const warning of data.warnings) {
768
- section.appendChild(el("p", "node-input-warning", String(warning)));
769
- }
770
- wrap.appendChild(section);
771
- }
772
-
773
- content.appendChild(wrap);
774
- }
775
-
776
- function unavailableMessage(reason) {
777
- if (reason === "unsupported-dynamic-node") {
778
- return UI_TEXT.inputDynamicUnsupported;
779
- }
780
- if (reason === "run-spec-unavailable") {
781
- return UI_TEXT.inputRunSpecUnavailable;
782
- }
783
- if (reason === "task-not-found") {
784
- return UI_TEXT.inputUnavailable;
785
- }
786
- return UI_TEXT.noInput;
787
- }
788
-
789
- async function renderDagNodeInput(content, dagRunId, node) {
790
- const nodeId = node.nodeId;
791
- const identity = makeDagNodeInputIdentity(dagRunId, nodeId);
792
- uiState.dagNodeInputIdentity = identity;
793
- uiState.dagNodeInputLoading = true;
794
- uiState.dagNodeInputError = null;
795
-
796
- clearNode(content);
797
- const loading = el("p", "muted", UI_TEXT.inputLoading);
798
- loading.setAttribute("role", "status");
799
- loading.setAttribute("aria-live", "polite");
800
- content.appendChild(loading);
801
-
802
- const result = await fetchDagNodeInput(dagRunId, nodeId);
803
- if (
804
- uiState.dagNodeInputIdentity !== identity ||
805
- uiState.selectedDagNodeId !== nodeId ||
806
- uiState.currentDagRunId !== dagRunId ||
807
- uiState.dagInspectorTab !== "input" ||
808
- !content.isConnected ||
809
- content.dataset.dagNodeId !== String(nodeId) ||
810
- content.dataset.dagRunId !== String(dagRunId)
811
- ) {
812
- return;
813
- }
814
-
815
- uiState.dagNodeInputLoading = false;
816
- clearNode(content);
817
-
818
- if (!result.ok) {
819
- const message =
820
- result.body?.error ||
821
- (result.status === 0
822
- ? UI_TEXT.inputError
823
- : `${UI_TEXT.inputError}(HTTP ${result.status})`);
824
- uiState.dagNodeInputError = message;
825
- uiState.dagNodeInputData = null;
826
- const error = el("p", "node-input-error", message);
827
- error.setAttribute("role", "alert");
828
- content.appendChild(error);
829
- return;
830
- }
831
-
832
- const data = result.body;
833
- uiState.dagNodeInputData = data;
834
- uiState.dagNodeInputError = null;
835
- if (!data?.available) {
836
- const empty = el(
837
- "p",
838
- "empty",
839
- unavailableMessage(data?.availabilityReason),
840
- );
841
- empty.setAttribute("role", "status");
842
- content.appendChild(empty);
843
- if ((data?.warnings ?? []).length > 0) {
844
- for (const warning of data.warnings) {
845
- content.appendChild(el("p", "node-input-warning", String(warning)));
846
- }
847
- }
848
- return;
849
- }
850
-
851
- renderDagNodeInputDetail(content, data);
852
- }
853
-
854
533
  function fillDagInspectorContent(content, dagRunId, node) {
855
534
  const previousRenderedNode = content.dataset.dagNodeId ?? "";
856
- const previousRenderedRun = content.dataset.dagRunId ?? "";
857
- const nodeChanged =
858
- previousRenderedNode !== String(node.nodeId ?? "") ||
859
- previousRenderedRun !== String(dagRunId ?? "");
535
+ const nodeChanged = previousRenderedNode !== String(node.nodeId ?? "");
860
536
  if (nodeChanged) {
861
537
  // Drop any open spec-evidence preview + cached list so a stale A detail
862
538
  // cannot survive into node B (AC-007 regression guard).
863
539
  uiState.specEvidenceDetail = null;
864
540
  uiState.specEvidenceListEvidence = null;
865
- uiState.dagNodeInputData = null;
866
- uiState.dagNodeInputError = null;
867
541
  }
868
- const activeTab = ["input", "timeline", "spec-evidence"].includes(
542
+ const activeTab = ["timeline", "spec-evidence"].includes(
869
543
  uiState.dagInspectorTab,
870
544
  )
871
545
  ? uiState.dagInspectorTab
@@ -874,15 +548,12 @@ async function renderDagNodeInput(content, dagRunId, node) {
874
548
  content.setAttribute("role", "tabpanel");
875
549
  content.setAttribute("aria-labelledby", `dag-inspector-tab-${activeTab}`);
876
550
  content.dataset.dagNodeId = String(node.nodeId ?? "");
877
- content.dataset.dagRunId = String(dagRunId ?? "");
878
551
  // Drop timeline-only classes when switching tabs so output layout stays clean.
879
552
  content.className = "dag-inspector-content";
880
553
  if (activeTab === "spec-evidence") {
881
554
  void renderSpecEvidence(content, dagRunId, node.nodeId);
882
555
  } else if (activeTab === "timeline") {
883
556
  renderSessionTimeline(content, node.nodeId);
884
- } else if (activeTab === "input") {
885
- void renderDagNodeInput(content, dagRunId, node);
886
557
  } else if (!node.outputPreview && !node.errorPreview) {
887
558
  content.appendChild(el("p", "empty", UI_TEXT.noOutput));
888
559
  } else {
@@ -921,7 +592,6 @@ function buildDagInspectorHeader(dagRunId, node) {
921
592
  header.appendChild(controls);
922
593
 
923
594
  const tabDefs = [
924
- ["input", "节点输入", "ri-login-box-line"],
925
595
  ["output", "节点输出", "ri-file-text-line"],
926
596
  ["timeline", "执行过程", "ri-route-line"],
927
597
  ["spec-evidence", "规范证据", "ri-book-open-line"],
@@ -990,17 +660,6 @@ function syncDagInspectorHeader(panel, dagRunId, node) {
990
660
  }
991
661
 
992
662
  function dagInspectorSignature(dagRunId, node) {
993
- const inputRevision =
994
- uiState.dagInspectorTab === "input"
995
- ? [
996
- uiState.dagNodeInputIdentity,
997
- uiState.dagNodeInputLoading,
998
- uiState.dagNodeInputError ?? "",
999
- uiState.dagNodeInputData?.availabilityReason ?? "",
1000
- uiState.dagNodeInputData?.promptFingerprint?.sha256Prefix ?? "",
1001
- node.status ?? "",
1002
- ]
1003
- : null;
1004
663
  return JSON.stringify([
1005
664
  dagRunId,
1006
665
  node.nodeId,
@@ -1008,7 +667,6 @@ function dagInspectorSignature(dagRunId, node) {
1008
667
  node.outputPreview ?? "",
1009
668
  node.errorPreview ?? "",
1010
669
  uiState.dagInspectorTab === "timeline" ? uiState.sessionEventOffset : null,
1011
- inputRevision,
1012
670
  ]);
1013
671
  }
1014
672
 
@@ -1044,30 +702,17 @@ export function renderDagInspector(dagRunId, node, initial = true) {
1044
702
  syncDagInspectorHeader(panel, dagRunId, node);
1045
703
  if (panel.dataset.contentSignature === nextContentSignature) return;
1046
704
  const previousNode = existingContent.dataset.dagNodeId ?? "";
1047
- const previousRun = existingContent.dataset.dagRunId ?? "";
1048
- const nodeChanged =
1049
- previousNode !== String(node.nodeId ?? "") ||
1050
- previousRun !== String(dagRunId ?? "");
1051
- const activeTab = ["input", "timeline", "spec-evidence"].includes(
1052
- uiState.dagInspectorTab,
1053
- )
1054
- ? uiState.dagInspectorTab
1055
- : "output";
1056
- const isInputTab = activeTab === "input";
1057
- const followLatest = isInputTab
1058
- ? false
1059
- : nodeChanged ||
1060
- computeFollowLatest(
1061
- existingContent.scrollTop,
1062
- existingContent.scrollHeight,
1063
- existingContent.clientHeight,
1064
- );
1065
- const regionKey = `dag:${dagRunId}:${node.nodeId}:${isInputTab ? "input" : "output"}`;
1066
- const viewportUpdater = isInputTab
1067
- ? "dagNodeInputViewportState"
1068
- : "dagNodeOutputViewportState";
1069
- uiState[viewportUpdater] = updateDagOutputViewportState(
1070
- uiState[viewportUpdater],
705
+ const nodeChanged = previousNode !== String(node.nodeId ?? "");
706
+ const followLatest =
707
+ nodeChanged ||
708
+ computeFollowLatest(
709
+ existingContent.scrollTop,
710
+ existingContent.scrollHeight,
711
+ existingContent.clientHeight,
712
+ );
713
+ const regionKey = `dag:${dagRunId}:${node.nodeId}:output`;
714
+ uiState.dagNodeOutputViewportState = updateDagOutputViewportState(
715
+ uiState.dagNodeOutputViewportState,
1071
716
  regionKey,
1072
717
  existingContent.scrollLeft,
1073
718
  existingContent.scrollTop,
@@ -1084,8 +729,7 @@ export function renderDagInspector(dagRunId, node, initial = true) {
1084
729
  if (followLatest) {
1085
730
  existingContent.scrollTop = existingContent.scrollHeight;
1086
731
  } else {
1087
- const saved = uiState[viewportUpdater];
1088
- existingContent.scrollTop = nodeChanged ? 0 : (saved?.scrollTop ?? 0);
732
+ existingContent.scrollTop = uiState.dagNodeOutputViewportState.scrollTop;
1089
733
  }
1090
734
  for (const [index, pre] of [
1091
735
  ...existingContent.querySelectorAll("pre"),
@@ -111,12 +111,8 @@ export async function projectOutcome(input) {
111
111
  if (artifact.sha256 && artifact.sha256 !== recomputed) {
112
112
  return contractError(`artifact sha256 mismatch for ${artifact.path}: declared=${artifact.sha256} actual=${recomputed}`);
113
113
  }
114
- // Persist portable repo-relative paths only (Delivery verifyArtifactRefs rejects abs paths).
115
- const relativePath = path
116
- .relative(path.resolve(input.repoRoot), resolved)
117
- .replace(/\\/g, "/");
118
114
  validatedArtifacts.push({
119
- path: relativePath,
115
+ path: artifact.path.replace(/\\/g, "/"),
120
116
  sha256: recomputed,
121
117
  ...(artifact.kind ? { kind: artifact.kind } : {}),
122
118
  ...(artifact.schemaId ? { schemaId: artifact.schemaId } : {}),
@@ -304,51 +304,11 @@ export async function runReadyTasks(options) {
304
304
  if (result.status !== "succeeded") {
305
305
  await options.onTaskFinalized?.({ status: result.status, taskSpec, workerRunId: result.workerRunId, runRecordPath: result.runRecordPath });
306
306
  }
307
- const recordErrorMessage = errorMessage(error);
308
- // Always project a Pool run fact for record-error so operators can
309
- // mark-failed / retry without hand-editing Running state.
310
- try {
311
- await recordTaskPoolRun({
312
- repoRoot: options.repoRoot,
313
- run: {
314
- schemaVersion: 1,
315
- batchRunId,
316
- workerRunId: result.workerRunId,
317
- taskId: result.businessId,
318
- featureId: taskSpec.feature_id,
319
- status: "run-error",
320
- recordedAt: new Date().toISOString(),
321
- error: recordErrorMessage,
322
- runRecordPath: result.runRecordPath,
323
- ...(result.workflow ? { workflow: result.workflow } : {}),
324
- ...(controllerIdentity ? { controllerIdentity } : {}),
325
- ...(retryOfWorkerRunId ? { retryOfWorkerRunId } : {}),
326
- },
327
- });
328
- await writeTaskPoolState(options.repoRoot, {
329
- schemaVersion: 2,
330
- featureId: taskSpec.feature_id,
331
- taskId,
332
- status: "Failed",
333
- updatedAt: new Date().toISOString(),
334
- workerRunId: result.workerRunId,
335
- lastRunRecordPath: result.runRecordPath,
336
- failure: {
337
- category: "EnvFailure",
338
- recommendedFollowUpKind: "manual-review",
339
- derivedFollowUpTaskId: `${taskId}-record-error-review`,
340
- source: "report-decision",
341
- },
342
- });
343
- }
344
- catch {
345
- // Best-effort; batch still reports record-error.
346
- }
347
307
  tasks.push({
348
308
  taskId,
349
309
  workerRunId: result.workerRunId,
350
310
  status: "record-error",
351
- error: recordErrorMessage,
311
+ error: errorMessage(error),
352
312
  runRecordPath: result.runRecordPath,
353
313
  });
354
314
  break;