@tea-agent/loop-agent 0.43.0-next.4 → 0.43.0-next.6

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 (96) hide show
  1. package/CHANGELOG.md +82 -9
  2. package/dist/application/task-lifecycle/advance.js +17 -4
  3. package/dist/application/task-lifecycle/observe.js +43 -29
  4. package/dist/application/task-lifecycle/plan-transitions.js +5 -4
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/executors/dag-pi-executor.js +233 -202
  7. package/dist/executors/pi-executor.js +2 -0
  8. package/dist/executors/pi-sdk-executor.js +77 -39
  9. package/dist/task/source-prepare/source-fidelity-pi.js +2 -2
  10. package/dist/task/source-references.js +48 -23
  11. package/dist/worker/console/chat/pi-runtime.js +113 -95
  12. package/dist/worker/console/chat/routes.js +164 -54
  13. package/dist/worker/console/chat/scm-routes.js +193 -0
  14. package/dist/worker/console/chat/scm-service.js +253 -0
  15. package/dist/worker/console/chat/scm-tools.js +111 -0
  16. package/dist/worker/console/chat/session-catalog.js +1 -0
  17. package/dist/worker/console/chat/session-mode-view.js +48 -0
  18. package/dist/worker/console/chat/session-mode.js +218 -0
  19. package/dist/worker/console/chat/session-store.js +22 -6
  20. package/dist/worker/console/chat/shortcuts.js +6 -0
  21. package/dist/worker/console/chat/tools.js +1 -0
  22. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CCj36aaC.js → abnfDiagram-N423BO3Z-BuyrlONK.js} +1 -1
  23. package/dist/worker/console/static/assets/{arc-Cl3MIVFh.js → arc-BvCl_wpi.js} +1 -1
  24. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C_n4kMJS.js → architectureDiagram-T3A2C74G-BkBpBaRq.js} +1 -1
  25. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DlWYJ7cK.js → blockDiagram-VBNYF7ZC-yve4Mcb_.js} +1 -1
  26. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ud5pS7vS.js → c4Diagram-5PPSVZJV-9yLGboBx.js} +1 -1
  27. package/dist/worker/console/static/assets/channel-BRahIhjw.js +1 -0
  28. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CQPwOv_j.js → chunk-2GRJ4B5K-BKP69b8r.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CMzjxrWI.js → chunk-2Q5K7J3B-NWFRnXju.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-pcnXnD6t.js → chunk-5RXB4S5H-BiweqLEc.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CEB3AvU2.js → chunk-5VM5RSS4-CP9GHmRY.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DSKnbXbp.js → chunk-6Q2QTUOP-BiGDkBtj.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CS6-PjCZ.js → chunk-GF5L2VYU-DkJ4ZjQS.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-f6q8Tnzz.js → chunk-JWPE2WC7-B5jjn7zq.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DkB0QX3q.js → chunk-KBJHAD2P-C6KoK9O0.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BdB4jAv6.js → chunk-RYQCIY6F-BvNIfyn-.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BVm1bnX6.js → chunk-XXDRQBXY-BZ33iX4W.js} +1 -1
  38. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-GOC80mhq.js +1 -0
  39. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-GOC80mhq.js +1 -0
  40. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-FV_P-IEp.js → cose-bilkent-JH36ORCC-CP8eIuZd.js} +1 -1
  41. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BM7RtkA2.js → cynefin-VYW2F7L2-DZECjpOe.js} +1 -1
  42. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C1BZwTG5.js → cynefinDiagram-MW4NZA55-CvVJoFqf.js} +1 -1
  43. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C6McNKmY.js → dagre-VZM6K2ZE-D_JTqdAn.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-C0aWRUaE.js → diagram-7IWD3JNH-Db7AQ0z0.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bfk_SKE8.js → diagram-B4RE2ZJO-AZPVo5CZ.js} +1 -1
  46. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-prx1T4qf.js → diagram-LBJQPF4R-BgXqI3E6.js} +1 -1
  47. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DBvty0eU.js → diagram-Q27KOJAE-BYjFn3Ba.js} +1 -1
  48. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CF0em8e9.js → diagram-UB23O5K3-B4rJgGGO.js} +1 -1
  49. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CF90b4Ws.js → ebnfDiagram-BXEA7PRR-C6XWZhpw.js} +1 -1
  50. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-hfunxzof.js → erDiagram-JOGREHBK-1YdQRIQW.js} +1 -1
  51. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-D-QSy4Nl.js → flowDiagram-UKHOOZJN-l3a0AwN0.js} +1 -1
  52. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DaBGNcvT.js → ganttDiagram-PKOTCBZU-CQL96F2Z.js} +1 -1
  53. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BChcubGL.js → gitGraphDiagram-DS77QQ5N-BxCvafRb.js} +1 -1
  54. package/dist/worker/console/static/assets/index-4wCGsXuU.js +465 -0
  55. package/dist/worker/console/static/assets/index-CiF9WBoR.css +1 -0
  56. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BkC67fIa.js → infoDiagram-6WML65LV-BUipIeDO.js} +1 -1
  57. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-ibhLczCD.js → ishikawaDiagram-WSZJBQD7-CRfomVMX.js} +1 -1
  58. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DCRMitay.js → journeyDiagram-NVQOT4AX-_UFvyviX.js} +1 -1
  59. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DJ_hKaYI.js → kanban-definition-27J2QSJJ-CKIRoqKv.js} +1 -1
  60. package/dist/worker/console/static/assets/{linear-CIlLsYsA.js → linear-CjttJDCM.js} +1 -1
  61. package/dist/worker/console/static/assets/{mermaid.core-C-5IQMP_.js → mermaid.core-DPvHX155.js} +5 -5
  62. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-TCzV5TQT.js → mindmap-definition-FAOFIHXS-Qmj-qwBe.js} +1 -1
  63. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-bfhd2BQH.js → pegDiagram-VL7TDLO6-Auw6DRgq.js} +1 -1
  64. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BxFRAm5N.js → pieDiagram-7S7Q4E2Y-CqMrfD4c.js} +1 -1
  65. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C-m12lxv.js → quadrantDiagram-CIZ2JOQS-BAddeggH.js} +1 -1
  66. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CwmLiGgx.js → railroadDiagram-AXF67PYL-DG1JuOu2.js} +1 -1
  67. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-oc0Q1jiD.js → requirementDiagram-LRYGKXZP-DLYGMKlC.js} +1 -1
  68. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-9Ght6Yur.js → sankeyDiagram-W5VNT64P-g68SbLM9.js} +1 -1
  69. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-ndm3lyz9.js → sequenceDiagram-SI44F4Z6-CmszLKv8.js} +1 -1
  70. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7sPlKVcw.js → sizeCapture-X5ZJPWSS-D2tEKUCj.js} +1 -1
  71. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Izl21-oS.js → stateDiagram-OKZ733FA-Syc5E_Jf.js} +1 -1
  72. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CwWwuPI-.js +1 -0
  73. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CtO3D65o.js → swimlanes-SLNWSIFB-Ibn0IRCa.js} +2 -2
  74. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-A7fqemr6.js +8 -0
  75. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cyg5cY7z.js → timeline-definition-Z64GVDOM-2ZW9W3iS.js} +1 -1
  76. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CwtE_TFK.js → vennDiagram-T6HMQDX7-v50HsKUh.js} +1 -1
  77. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-Dtt9v8OO.js → wardleyDiagram-T6FBY63Y-DqCXEoDH.js} +1 -1
  78. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B2Y3ryb6.js → xychartDiagram-ELKLHX3M-vIP8Vo9d.js} +1 -1
  79. package/dist/worker/console/static/index.html +2 -2
  80. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +20 -4
  81. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
  82. package/dist/worker/console/workspace-context.js +6 -0
  83. package/dist/workflows/dag/frontend-capacity.js +9 -0
  84. package/dist/workflows/dag/frontend-design-policy.js +2 -1
  85. package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
  86. package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
  87. package/dist/workflows/dag/frontend-shape-facts.js +12 -2
  88. package/package.json +1 -1
  89. package/skills/frontend-plan/SKILL.md +7 -0
  90. package/dist/worker/console/static/assets/channel-CEMRpNm-.js +0 -1
  91. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C9lhaZkR.js +0 -1
  92. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C9lhaZkR.js +0 -1
  93. package/dist/worker/console/static/assets/index-CJkCY1XR.js +0 -464
  94. package/dist/worker/console/static/assets/index-DUBCbiqv.css +0 -1
  95. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CmElLFpP.js +0 -1
  96. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DKRbGG8V.js +0 -8
@@ -16,6 +16,7 @@ import { randomUUID } from "node:crypto";
16
16
  import { access } from "node:fs/promises";
17
17
  import { parseAssistantContent } from "./assistant-content.js";
18
18
  import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, } from "./tools.js";
19
+ import { InvalidChatSessionModeError, SESSION_MODE_ACTION_DENIED_CODE, SESSION_MODE_ACTION_DENIED_MESSAGE, SESSION_MODE_IMMUTABLE_CODE, UnknownChatSessionModeError, isWorkflowSessionMode, parseCreateSessionMode, readStoredSessionMode, shouldAcceptOperatorSnapshot, } from "./session-mode.js";
19
20
  import { buildModelCallableToolSchemas } from "./tool-adapter.js";
20
21
  import { cleanChatTitle, acceptedOperationIdFromResult, isBlankChatSession } from "./session-store.js";
21
22
  import { hashChatTurnPayload, projectChatToolEventPayload, projectCompactSnapshot, } from "./chat-event-store.js";
@@ -46,11 +47,77 @@ import { isTerminalOperationState } from "../../../infrastructure/console/operat
46
47
  import { SddDataAlignmentError, } from "./sdd-data-alignment.js";
47
48
  import { TerminalSessionRegistry } from "./terminal-sessions.js";
48
49
  import { handleTerminalRequest } from "./terminal-routes.js";
50
+ import { handleScmRequest } from "./scm-routes.js";
49
51
  function projectBrowserChatSession(record) {
50
52
  const { telemetrySessionId, ...projected } = record;
51
53
  void telemetrySessionId;
52
54
  return projected;
53
55
  }
56
+ function reopenInitFromRecord(record) {
57
+ return {
58
+ sessionId: record.sessionId,
59
+ sessionFile: record.sessionFile,
60
+ ...(record.modelProvider && record.modelId
61
+ ? { model: { provider: record.modelProvider, modelId: record.modelId } }
62
+ : {}),
63
+ sessionMode: record.sessionMode,
64
+ };
65
+ }
66
+ async function readChatSessionMode(deps, sessionId) {
67
+ let record;
68
+ try {
69
+ record = await deps.store.get(sessionId);
70
+ }
71
+ catch (error) {
72
+ if (error instanceof UnknownChatSessionModeError) {
73
+ return {
74
+ ok: false,
75
+ status: 409,
76
+ code: error.code,
77
+ message: error.message,
78
+ };
79
+ }
80
+ throw error;
81
+ }
82
+ if (!record) {
83
+ return {
84
+ ok: false,
85
+ status: 404,
86
+ code: "NOT_FOUND",
87
+ message: `chat session not found: ${sessionId}`,
88
+ };
89
+ }
90
+ // Normalize: the store contract always fills sessionMode, but handler-level
91
+ // fakes / partial records may omit it — treat missing as workflow (the
92
+ // stock-record read rule), while a present-but-unknown value still fails
93
+ // closed in the store layer.
94
+ return {
95
+ ok: true,
96
+ record,
97
+ sessionMode: readStoredSessionMode(record.sessionMode),
98
+ };
99
+ }
100
+ async function requireWorkflowSession(res, deps, sessionId) {
101
+ const resolved = await readChatSessionMode(deps, sessionId);
102
+ if (!resolved.ok) {
103
+ sendJson(res, resolved.status, {
104
+ ok: false,
105
+ error: { code: resolved.code, message: resolved.message },
106
+ });
107
+ return undefined;
108
+ }
109
+ if (!isWorkflowSessionMode(resolved.sessionMode)) {
110
+ sendJson(res, 409, {
111
+ ok: false,
112
+ error: {
113
+ code: SESSION_MODE_ACTION_DENIED_CODE,
114
+ message: SESSION_MODE_ACTION_DENIED_MESSAGE,
115
+ },
116
+ });
117
+ return undefined;
118
+ }
119
+ return resolved.record;
120
+ }
54
121
  /**
55
122
  * Reattach the durable event-ring order to session-store rows. The session
56
123
  * store remains the content authority; the ring only supplies chronology.
@@ -567,6 +634,22 @@ workspaceId) {
567
634
  });
568
635
  return;
569
636
  }
637
+ let sessionMode;
638
+ try {
639
+ sessionMode = parseCreateSessionMode(body.sessionMode);
640
+ }
641
+ catch (error) {
642
+ sendJson(res, 400, {
643
+ ok: false,
644
+ error: {
645
+ code: error instanceof InvalidChatSessionModeError
646
+ ? error.code
647
+ : "INVALID_SESSION_MODE",
648
+ message: error instanceof Error ? error.message : String(error),
649
+ },
650
+ });
651
+ return;
652
+ }
570
653
  const provider = typeof body.modelProvider === "string" ? body.modelProvider : undefined;
571
654
  const modelId = typeof body.modelId === "string" ? body.modelId : undefined;
572
655
  const snapshotInput = body.snapshot;
@@ -588,6 +671,16 @@ workspaceId) {
588
671
  let createdSessionId;
589
672
  let persisted = false;
590
673
  try {
674
+ if (snapshotInput !== undefined && !shouldAcceptOperatorSnapshot(sessionMode)) {
675
+ sendJson(res, 409, {
676
+ ok: false,
677
+ error: {
678
+ code: SESSION_MODE_ACTION_DENIED_CODE,
679
+ message: "Operator snapshots cannot be imported into Pi sessions",
680
+ },
681
+ });
682
+ return;
683
+ }
591
684
  const snapshot = snapshotInput === undefined ? undefined : projectCompactSnapshot(snapshotInput);
592
685
  // A Console record promises that its Pi JSONL can be reopened after a
593
686
  // Console restart. Do not make that promise until Pi has finished
@@ -595,6 +688,7 @@ workspaceId) {
595
688
  // future JSONL path, leaving an unrecoverable orphan when init failed or
596
689
  // the process restarted in that window.
597
690
  const handle = await deps.runtime.createSession({
691
+ sessionMode,
598
692
  ...(provider && modelId ? { model: { provider, modelId } } : {}),
599
693
  ...(snapshot
600
694
  ? {
@@ -621,6 +715,7 @@ workspaceId) {
621
715
  sessionFile: handle.sessionFile,
622
716
  modelProvider: selection.model?.provider ?? handle.model?.provider,
623
717
  modelId: selection.model?.modelId ?? handle.model?.modelId,
718
+ sessionMode,
624
719
  // AC-R7-002: explicit parameter from the dispatcher — the runtime
625
720
  // store.create wrapper is gone; body workspaceId (if any) only acts as
626
721
  // a fallback for direct (non-dispatcher) handler tests.
@@ -691,6 +786,7 @@ export async function handleForkChatSession(req, res, deps, sourceSessionId) {
691
786
  try {
692
787
  const handle = await deps.runtime.forkSession({
693
788
  sourceSessionFile: source.sessionFile ?? "",
789
+ sessionMode: source.sessionMode,
694
790
  ...(source.modelProvider && source.modelId
695
791
  ? { model: { provider: source.modelProvider, modelId: source.modelId } }
696
792
  : {}),
@@ -757,10 +853,7 @@ export async function handleBranchChatSession(req, res, deps, sessionId) {
757
853
  }
758
854
  try {
759
855
  if (!deps.runtime.hasSession(sessionId)) {
760
- const reopened = await deps.runtime.reopenSession({
761
- sessionId,
762
- sessionFile: record.sessionFile,
763
- });
856
+ const reopened = await deps.runtime.reopenSession(reopenInitFromRecord(record));
764
857
  if (!reopened.ok) {
765
858
  const mapped = await mapFailedReopen(deps, sessionId, reopened);
766
859
  sendJson(res, mapped.status, {
@@ -1233,13 +1326,8 @@ export async function handleChatGoal(req, res, deps, sessionId) {
1233
1326
  });
1234
1327
  return;
1235
1328
  }
1236
- if (!(await deps.store.get(sessionId))) {
1237
- sendJson(res, 404, {
1238
- ok: false,
1239
- error: { code: "NOT_FOUND", message: `chat session not found: ${sessionId}` },
1240
- });
1329
+ if (!(await requireWorkflowSession(res, deps, sessionId)))
1241
1330
  return;
1242
- }
1243
1331
  if (req.method === "GET") {
1244
1332
  sendJson(res, 200, { ok: true, goal: registry.get(sessionId) });
1245
1333
  return;
@@ -1453,6 +1541,7 @@ export async function listChatSessionSummaries(deps, ownerWorkspaceId) {
1453
1541
  ...(session.thinkingLevel !== undefined
1454
1542
  ? { thinkingLevel: session.thinkingLevel }
1455
1543
  : {}),
1544
+ sessionMode: session.sessionMode,
1456
1545
  activeTurnId: active?.turnId,
1457
1546
  turnPhase: active?.abortRequestedAt
1458
1547
  ? "stopping"
@@ -1476,6 +1565,16 @@ export async function handlePatchChatSession(req, res, deps, sessionId) {
1476
1565
  }
1477
1566
  try {
1478
1567
  const body = await readJsonBody(req);
1568
+ if ("sessionMode" in body) {
1569
+ sendJson(res, 409, {
1570
+ ok: false,
1571
+ error: {
1572
+ code: SESSION_MODE_IMMUTABLE_CODE,
1573
+ message: "sessionMode cannot be changed after create",
1574
+ },
1575
+ });
1576
+ return;
1577
+ }
1479
1578
  let record;
1480
1579
  if (typeof body.title === "string")
1481
1580
  record = await deps.store.rename(sessionId, body.title);
@@ -1534,13 +1633,7 @@ export async function handleReopenChatSession(req, res, deps, sessionId) {
1534
1633
  });
1535
1634
  return;
1536
1635
  }
1537
- const result = await deps.runtime.reopenSession({
1538
- sessionId,
1539
- sessionFile: record.sessionFile,
1540
- ...(record.modelProvider && record.modelId
1541
- ? { model: { provider: record.modelProvider, modelId: record.modelId } }
1542
- : {}),
1543
- });
1636
+ const result = await deps.runtime.reopenSession(reopenInitFromRecord(record));
1544
1637
  if (!result.ok) {
1545
1638
  const mapped = await mapFailedReopen(deps, sessionId, result);
1546
1639
  sendJson(res, mapped.status, {
@@ -1620,13 +1713,7 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
1620
1713
  });
1621
1714
  return;
1622
1715
  }
1623
- const reopen = await deps.runtime.reopenSession({
1624
- sessionId,
1625
- sessionFile: record.sessionFile,
1626
- ...(record.modelProvider && record.modelId
1627
- ? { model: { provider: record.modelProvider, modelId: record.modelId } }
1628
- : {}),
1629
- });
1716
+ const reopen = await deps.runtime.reopenSession(reopenInitFromRecord(record));
1630
1717
  if (!reopen.ok) {
1631
1718
  const mapped = await mapFailedReopen(deps, sessionId, reopen);
1632
1719
  sendJson(res, mapped.status, {
@@ -1890,13 +1977,7 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
1890
1977
  });
1891
1978
  return;
1892
1979
  }
1893
- const reopen = await deps.runtime.reopenSession({
1894
- sessionId,
1895
- sessionFile: record.sessionFile,
1896
- ...(record.modelProvider && record.modelId
1897
- ? { model: { provider: record.modelProvider, modelId: record.modelId } }
1898
- : {}),
1899
- });
1980
+ const reopen = await deps.runtime.reopenSession(reopenInitFromRecord(record));
1900
1981
  if (!reopen.ok) {
1901
1982
  const mapped = await mapFailedReopen(deps, sessionId, reopen);
1902
1983
  sendJson(res, mapped.status, {
@@ -2174,6 +2255,10 @@ export async function driveGoalRound(deps, sessionId) {
2174
2255
  registry.disarm(sessionId);
2175
2256
  return "idle";
2176
2257
  }
2258
+ if (!isWorkflowSessionMode(readStoredSessionMode(record.sessionMode))) {
2259
+ registry.disarm(sessionId);
2260
+ return "idle";
2261
+ }
2177
2262
  const runtimeFacts = deps.runtime.getSessionBusyFacts(sessionId);
2178
2263
  const operationStore = deps.runtime.actionContext?.operations;
2179
2264
  const linkedOperationIds = linkedOperationIdsForGoal(deps.events.snapshot(sessionId));
@@ -2822,6 +2907,7 @@ export async function handleChatCommands(res, deps, sessionId) {
2822
2907
  const warning = typeof listed.warning === "string" && listed.warning.trim()
2823
2908
  ? listed.warning.trim().slice(0, 160)
2824
2909
  : undefined;
2910
+ const resolvedMode = await readChatSessionMode(deps, sessionId);
2825
2911
  res.statusCode = 200;
2826
2912
  res.setHeader("content-type", "application/json; charset=utf-8");
2827
2913
  res.end(JSON.stringify({
@@ -2829,18 +2915,25 @@ export async function handleChatCommands(res, deps, sessionId) {
2829
2915
  commands,
2830
2916
  capabilities: {
2831
2917
  sddBingo: Boolean(await deps.sddDataAlignment?.isEligible()),
2918
+ ...(resolvedMode.ok ? { sessionMode: resolvedMode.sessionMode } : {}),
2832
2919
  },
2833
2920
  ...(warning ? { warning } : {}),
2834
2921
  }));
2835
2922
  }
2836
2923
  catch (error) {
2837
2924
  // Whole-list / readiness failures stay fail-soft with empty commands + warning.
2925
+ // Keep the two capability shapes symmetric: the fallback mode mirrors the
2926
+ // client-side prop fallback in useComposer.
2927
+ const fallbackMode = await readChatSessionMode(deps, sessionId).catch(() => undefined);
2838
2928
  res.statusCode = 200;
2839
2929
  res.setHeader("content-type", "application/json; charset=utf-8");
2840
2930
  res.end(JSON.stringify({
2841
2931
  ok: true,
2842
2932
  commands: [],
2843
- capabilities: { sddBingo: false },
2933
+ capabilities: {
2934
+ sddBingo: false,
2935
+ ...(fallbackMode?.ok ? { sessionMode: fallbackMode.sessionMode } : {}),
2936
+ },
2844
2937
  warning: error instanceof Error ? error.message : "commands unavailable",
2845
2938
  }));
2846
2939
  }
@@ -3085,13 +3178,7 @@ export async function ensureActiveSession(deps, sessionId) {
3085
3178
  message: `chat session not found: ${sessionId}`,
3086
3179
  };
3087
3180
  }
3088
- const reopened = await deps.runtime.reopenSession({
3089
- sessionId,
3090
- sessionFile: record.sessionFile,
3091
- ...(record.modelProvider && record.modelId
3092
- ? { model: { provider: record.modelProvider, modelId: record.modelId } }
3093
- : {}),
3094
- });
3181
+ const reopened = await deps.runtime.reopenSession(reopenInitFromRecord(record));
3095
3182
  if (!reopened.ok) {
3096
3183
  return { ok: false, ...(await mapFailedReopen(deps, sessionId, reopened)) };
3097
3184
  }
@@ -3420,7 +3507,7 @@ async function handleStartInterview(req, res, deps, sessionId) {
3420
3507
  });
3421
3508
  return;
3422
3509
  }
3423
- if (!(await requireChatSession(res, deps, sessionId)))
3510
+ if (!(await requireWorkflowSession(res, deps, sessionId)))
3424
3511
  return;
3425
3512
  try {
3426
3513
  const body = await readJsonBody(req);
@@ -3462,7 +3549,7 @@ async function handleAnswerInterview(req, res, deps, sessionId, interviewSession
3462
3549
  });
3463
3550
  return;
3464
3551
  }
3465
- if (!(await requireChatSession(res, deps, sessionId)))
3552
+ if (!(await requireWorkflowSession(res, deps, sessionId)))
3466
3553
  return;
3467
3554
  try {
3468
3555
  const body = await readJsonBody(req);
@@ -3498,7 +3585,7 @@ async function handleAnswerInterview(req, res, deps, sessionId, interviewSession
3498
3585
  }
3499
3586
  }
3500
3587
  async function handleGetInterviewFact(res, deps, sessionId, interviewSessionId, fact) {
3501
- if (!(await requireChatSession(res, deps, sessionId)))
3588
+ if (!(await requireWorkflowSession(res, deps, sessionId)))
3502
3589
  return;
3503
3590
  const state = await new ChatInterviewAdapter(deps.appData, deps.events).get(interviewSessionId);
3504
3591
  if (!state || state.interview.operatorSessionId !== sessionId) {
@@ -3596,7 +3683,7 @@ async function handleContractApplyHumanGate(req, res, deps, sessionId, step) {
3596
3683
  });
3597
3684
  return;
3598
3685
  }
3599
- if (!(await requireChatSession(res, deps, sessionId)))
3686
+ if (!(await requireWorkflowSession(res, deps, sessionId)))
3600
3687
  return;
3601
3688
  const ctx = deps.runtime.actionContext;
3602
3689
  if (!ctx) {
@@ -3778,7 +3865,7 @@ async function handleDagHumanGate(req, res, deps, sessionId, step) {
3778
3865
  });
3779
3866
  return;
3780
3867
  }
3781
- if (!(await requireChatSession(res, deps, sessionId)))
3868
+ if (!(await requireWorkflowSession(res, deps, sessionId)))
3782
3869
  return;
3783
3870
  const ctx = deps.runtime.actionContext;
3784
3871
  if (!ctx) {
@@ -3887,7 +3974,7 @@ async function handleMutationHumanGate(req, res, deps, sessionId) {
3887
3974
  });
3888
3975
  return;
3889
3976
  }
3890
- if (!(await requireChatSession(res, deps, sessionId)))
3977
+ if (!(await requireWorkflowSession(res, deps, sessionId)))
3891
3978
  return;
3892
3979
  const ctx = deps.runtime.actionContext;
3893
3980
  if (!ctx) {
@@ -3976,7 +4063,7 @@ async function handleConfirmTaskKind(req, res, deps, sessionId, interviewSession
3976
4063
  });
3977
4064
  return;
3978
4065
  }
3979
- if (!(await requireChatSession(res, deps, sessionId)))
4066
+ if (!(await requireWorkflowSession(res, deps, sessionId)))
3980
4067
  return;
3981
4068
  try {
3982
4069
  const body = await readJsonBody(req);
@@ -4032,13 +4119,7 @@ export async function handleChatCompact(req, res, deps, sessionId) {
4032
4119
  return;
4033
4120
  }
4034
4121
  if (!deps.runtime.hasSession(sessionId)) {
4035
- const reopened = await deps.runtime.reopenSession({
4036
- sessionId,
4037
- sessionFile: record.sessionFile,
4038
- ...(record.modelProvider && record.modelId
4039
- ? { model: { provider: record.modelProvider, modelId: record.modelId } }
4040
- : {}),
4041
- });
4122
+ const reopened = await deps.runtime.reopenSession(reopenInitFromRecord(record));
4042
4123
  if (!reopened.ok) {
4043
4124
  const mapped = await mapFailedReopen(deps, sessionId, reopened);
4044
4125
  sendJson(res, mapped.status, {
@@ -4062,7 +4143,9 @@ export async function handleChatCompact(req, res, deps, sessionId) {
4062
4143
  const rawInstructions = typeof body.instructions === "string" ? body.instructions : undefined;
4063
4144
  const currentTodos = projectCurrentTodos(deps.events.snapshot(sessionId));
4064
4145
  const todoInstructions = buildTodoCompactionInstructions(rawInstructions, currentTodos);
4065
- const customInstructions = buildGoalCompactionInstructions(todoInstructions, deps.goalRegistry?.get(sessionId) ?? null);
4146
+ const customInstructions = isWorkflowSessionMode(record.sessionMode)
4147
+ ? buildGoalCompactionInstructions(todoInstructions, deps.goalRegistry?.get(sessionId) ?? null)
4148
+ : todoInstructions;
4066
4149
  const turnId = deps.events.latestTurnId(sessionId) ?? `${sessionId}:compact`;
4067
4150
  const compactionId = `${sessionId}:compaction:${randomUUID()}`;
4068
4151
  deps.events.append(sessionId, turnId, {
@@ -4148,6 +4231,24 @@ export async function handleChatMessages(res, deps, sessionId, url) {
4148
4231
  export async function handleChatRequest(req, res, deps, pathname, workspaceScope) {
4149
4232
  if (!deps)
4150
4233
  return false;
4234
+ try {
4235
+ return await dispatchChatRequest(req, res, deps, pathname, workspaceScope);
4236
+ }
4237
+ catch (error) {
4238
+ // Store-level fail-closed for corrupt sessionMode: bounded 409 instead of
4239
+ // a generic 500, for any handler that reads the record without its own
4240
+ // UnknownChatSessionModeError mapping (readChatSessionMode covers the rest).
4241
+ if (error instanceof UnknownChatSessionModeError) {
4242
+ sendJson(res, 409, {
4243
+ ok: false,
4244
+ error: { code: error.code, message: error.message },
4245
+ });
4246
+ return true;
4247
+ }
4248
+ throw error;
4249
+ }
4250
+ }
4251
+ async function dispatchChatRequest(req, res, deps, pathname, workspaceScope) {
4151
4252
  // Terminal management API (model + Console UI shared PTY registry).
4152
4253
  if (await handleTerminalRequest(req, res, {
4153
4254
  terminals: deps.terminals ?? new TerminalSessionRegistry(),
@@ -4157,6 +4258,15 @@ export async function handleChatRequest(req, res, deps, pathname, workspaceScope
4157
4258
  }, pathname)) {
4158
4259
  return true;
4159
4260
  }
4261
+ // Source control API (model + Console UI shared workspace ScmService).
4262
+ if (deps.scm) {
4263
+ if (await handleScmRequest(req, res, {
4264
+ scm: deps.scm,
4265
+ gateMutation: (r) => gateMutation(r, deps),
4266
+ }, pathname)) {
4267
+ return true;
4268
+ }
4269
+ }
4160
4270
  if (!pathname.startsWith("/api/operator/v1/chat/") &&
4161
4271
  !pathname.startsWith("/api/operator/v1/pi/"))
4162
4272
  return false;
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Operator Chat — source control HTTP API (Console UI side).
3
+ *
4
+ * Endpoints (all under /api/operator/v1/chat):
5
+ * GET /scm/status branch + changes + counts
6
+ * GET /scm/diff?path=&staged= unified diff (one path or --stat)
7
+ * POST /scm/stage { paths: string[] } | { all: true }
8
+ * POST /scm/unstage { paths: string[] }
9
+ * POST /scm/discard { paths: string[] } [dangerous]
10
+ * POST /scm/commit { message }
11
+ * GET /scm/log?limit= recent commits
12
+ * GET /scm/branches branch list + current
13
+ * POST /scm/branches { name, create? } switch or create
14
+ * POST /scm/push push to upstream
15
+ * POST /scm/pull pull from upstream
16
+ *
17
+ * Reads are open (same-origin console); every mutation passes gateMutation.
18
+ * The ScmService is workspace-scoped (repoRoot fixed at construction).
19
+ */
20
+ function sendJson(res, status, body) {
21
+ res.writeHead(status, {
22
+ "content-type": "application/json; charset=utf-8",
23
+ "cache-control": "no-store",
24
+ });
25
+ res.end(JSON.stringify(body));
26
+ }
27
+ function mapScmError(res, error) {
28
+ const status = error.code === "NO_PATHS" || error.code === "EMPTY_MESSAGE" || error.code === "INVALID_NAME"
29
+ ? 400
30
+ : error.code === "GIT_TIMEOUT"
31
+ ? 504
32
+ : 500;
33
+ sendJson(res, status, {
34
+ ok: false,
35
+ error: { code: error.code, message: error.message, ...(error.stderr ? { stderr: error.stderr } : {}) },
36
+ });
37
+ }
38
+ async function readJsonBody(req, maxBytes = 256 * 1024) {
39
+ const chunks = [];
40
+ let total = 0;
41
+ for await (const chunk of req) {
42
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
43
+ total += buf.length;
44
+ if (total > maxBytes)
45
+ throw new Error("request body too large");
46
+ chunks.push(buf);
47
+ }
48
+ if (chunks.length === 0)
49
+ return {};
50
+ const raw = Buffer.concat(chunks).toString("utf8");
51
+ if (!raw.trim())
52
+ return {};
53
+ return JSON.parse(raw);
54
+ }
55
+ function parsePaths(body) {
56
+ if (!Array.isArray(body.paths))
57
+ return [];
58
+ return body.paths
59
+ .filter((p) => typeof p === "string" && p.length > 0 && !p.includes("\0"))
60
+ .slice(0, 500);
61
+ }
62
+ /** Route dispatcher; returns true when the request was handled. */
63
+ export async function handleScmRequest(req, res, deps, pathname) {
64
+ if (!pathname.startsWith("/api/operator/v1/chat/scm"))
65
+ return false;
66
+ const method = (req.method ?? "GET").toUpperCase();
67
+ const rest = pathname.slice("/api/operator/v1/chat/scm".length);
68
+ const parts = rest.split("/").filter(Boolean);
69
+ const sub = parts[0] ?? "";
70
+ const url = new URL(req.url ?? "/", "http://localhost");
71
+ const gateFailed = (gate) => {
72
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
73
+ return true;
74
+ };
75
+ try {
76
+ if (method === "GET" && sub === "status" && parts.length === 1) {
77
+ const r = await deps.scm.status();
78
+ if (!r.ok)
79
+ return mapScmError(res, r), true;
80
+ sendJson(res, 200, { ok: true, data: r });
81
+ return true;
82
+ }
83
+ if (method === "GET" && sub === "diff" && parts.length === 1) {
84
+ const path = url.searchParams.get("path") ?? undefined;
85
+ const staged = url.searchParams.get("staged") === "1";
86
+ const r = await deps.scm.diff({ path, staged });
87
+ if (!r.ok)
88
+ return mapScmError(res, r), true;
89
+ sendJson(res, 200, { ok: true, data: r });
90
+ return true;
91
+ }
92
+ if (method === "GET" && sub === "log" && parts.length === 1) {
93
+ const limit = Number(url.searchParams.get("limit") ?? 30);
94
+ const r = await deps.scm.log(Number.isFinite(limit) ? Math.min(Math.max(limit, 1), 200) : 30);
95
+ if (!r.ok)
96
+ return mapScmError(res, r), true;
97
+ sendJson(res, 200, { ok: true, data: r });
98
+ return true;
99
+ }
100
+ if (method === "GET" && sub === "branches" && parts.length === 1) {
101
+ const r = await deps.scm.branchList();
102
+ if (!r.ok)
103
+ return mapScmError(res, r), true;
104
+ sendJson(res, 200, { ok: true, data: r });
105
+ return true;
106
+ }
107
+ // Everything below mutates → gate first.
108
+ const gate = deps.gateMutation(req);
109
+ if (!gate.ok)
110
+ return gateFailed(gate);
111
+ if (method === "POST" && sub === "stage" && parts.length === 1) {
112
+ const body = await readJsonBody(req);
113
+ const r = body.all === true
114
+ ? await deps.scm.stage([], true)
115
+ : await deps.scm.stage(parsePaths(body));
116
+ if (!r.ok)
117
+ return mapScmError(res, r), true;
118
+ sendJson(res, 200, { ok: true });
119
+ return true;
120
+ }
121
+ if (method === "POST" && sub === "unstage" && parts.length === 1) {
122
+ const body = await readJsonBody(req);
123
+ const r = await deps.scm.unstage(parsePaths(body));
124
+ if (!r.ok)
125
+ return mapScmError(res, r), true;
126
+ sendJson(res, 200, { ok: true });
127
+ return true;
128
+ }
129
+ if (method === "POST" && sub === "discard" && parts.length === 1) {
130
+ const body = await readJsonBody(req);
131
+ const r = await deps.scm.discard(parsePaths(body));
132
+ if (!r.ok)
133
+ return mapScmError(res, r), true;
134
+ sendJson(res, 200, { ok: true });
135
+ return true;
136
+ }
137
+ if (method === "POST" && sub === "commit" && parts.length === 1) {
138
+ const body = await readJsonBody(req);
139
+ const message = typeof body.message === "string" ? body.message : "";
140
+ const r = await deps.scm.commit(message);
141
+ if (!r.ok)
142
+ return mapScmError(res, r), true;
143
+ sendJson(res, 200, { ok: true, data: r });
144
+ return true;
145
+ }
146
+ if (method === "POST" && sub === "branches" && parts.length === 1) {
147
+ const body = await readJsonBody(req);
148
+ const name = typeof body.name === "string" ? body.name : "";
149
+ if (!name) {
150
+ sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: "name is required" } });
151
+ return true;
152
+ }
153
+ const r = body.create === true
154
+ ? await deps.scm.branchCreate(name)
155
+ : await deps.scm.branchSwitch(name);
156
+ if (!r.ok)
157
+ return mapScmError(res, r), true;
158
+ sendJson(res, 200, { ok: true });
159
+ return true;
160
+ }
161
+ if (method === "POST" && sub === "push" && parts.length === 1) {
162
+ const r = await deps.scm.push();
163
+ if (!r.ok)
164
+ return mapScmError(res, r), true;
165
+ sendJson(res, 200, { ok: true, data: r });
166
+ return true;
167
+ }
168
+ if (method === "POST" && sub === "pull" && parts.length === 1) {
169
+ const r = await deps.scm.pull();
170
+ if (!r.ok)
171
+ return mapScmError(res, r), true;
172
+ sendJson(res, 200, { ok: true, data: r });
173
+ return true;
174
+ }
175
+ sendJson(res, 404, { ok: false, error: { code: "NOT_FOUND", message: `unknown scm route: ${method} ${pathname}` } });
176
+ return true;
177
+ }
178
+ catch (error) {
179
+ if (!res.headersSent) {
180
+ sendJson(res, 500, {
181
+ ok: false,
182
+ error: {
183
+ code: "SCM_INTERNAL",
184
+ message: error instanceof Error ? error.message : String(error),
185
+ },
186
+ });
187
+ }
188
+ else {
189
+ res.end();
190
+ }
191
+ return true;
192
+ }
193
+ }