pi-harness-runtime 1.1.50 → 1.1.77

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 (202) hide show
  1. package/CHANGELOG.md +30 -9
  2. package/README.md +119 -0
  3. package/harness/langchain/agents.d.ts +143 -0
  4. package/harness/langchain/agents.test.d.ts +2 -0
  5. package/harness/langchain/agents.ts +196 -13
  6. package/harness/langchain/checkpointer.d.ts +48 -0
  7. package/harness/langchain/checkpointer.test.d.ts +8 -0
  8. package/harness/langchain/cron-cli.d.ts +12 -0
  9. package/harness/langchain/cron-cli.ts +169 -0
  10. package/harness/langchain/daemon.d.ts +161 -0
  11. package/harness/langchain/daemon.test.d.ts +11 -0
  12. package/harness/langchain/daemon.test.ts +3 -1
  13. package/harness/langchain/daemon.ts +230 -12
  14. package/harness/langchain/file-tools.d.ts +97 -0
  15. package/harness/langchain/file-tools.ts +14 -6
  16. package/harness/langchain/graph.d.ts +706 -0
  17. package/harness/langchain/graph.ts +158 -25
  18. package/harness/langchain/model-router.d.ts +63 -0
  19. package/harness/langchain/model-router.test.d.ts +2 -0
  20. package/harness/langchain/model-router.test.ts +191 -0
  21. package/harness/langchain/model-router.ts +188 -0
  22. package/harness/langchain/models.d.ts +72 -0
  23. package/harness/langchain/models.ts +111 -49
  24. package/harness/langchain/ping-pong-middleware.d.ts +28 -0
  25. package/harness/langchain/ping-pong-middleware.test.d.ts +2 -0
  26. package/harness/langchain/ping-pong-middleware.test.ts +69 -0
  27. package/harness/langchain/ping-pong-middleware.ts +204 -0
  28. package/harness/langchain/run.d.ts +15 -0
  29. package/harness/langchain/run.ts +15 -3
  30. package/harness/langchain/scoreboard.test.d.ts +13 -0
  31. package/harness/langchain/scoreboard.test.ts +23 -1
  32. package/harness/langchain/status-line.d.ts +91 -0
  33. package/harness/langchain/status-line.test.d.ts +8 -0
  34. package/harness/langchain/surge.d.ts +103 -0
  35. package/harness/langchain/surge.test.d.ts +10 -0
  36. package/harness/langchain/surge.test.ts +179 -0
  37. package/harness/langchain/surge.ts +188 -19
  38. package/harness/langchain/widget.d.ts +105 -0
  39. package/harness/langchain/widget.test.d.ts +8 -0
  40. package/harness/ssh-detach-interceptor.test.ts +263 -0
  41. package/harness/ssh-detach-interceptor.ts +438 -0
  42. package/index.ts +212 -47
  43. package/package.json +4 -2
  44. package/packages/a2a-adapter/package.json +1 -1
  45. package/packages/architecture-generator/package.json +1 -1
  46. package/packages/auth/package.json +1 -1
  47. package/packages/autonomous-refactor/package.json +1 -1
  48. package/packages/autonomous-runtime/package.json +1 -1
  49. package/packages/cache-strategy/package.json +1 -1
  50. package/packages/capability-registry/package.json +1 -1
  51. package/packages/checkpoint/package.json +1 -1
  52. package/packages/cli-plugin-sdk/package.json +1 -1
  53. package/packages/clipboard/package.json +1 -1
  54. package/packages/clipboard-plugin/package.json +1 -1
  55. package/packages/code-generation/package.json +1 -1
  56. package/packages/code-review/package.json +1 -1
  57. package/packages/codex-adapter/package.json +1 -1
  58. package/packages/config-capture/package.json +1 -1
  59. package/packages/context-compiler/package.json +1 -1
  60. package/packages/context-discovery/package.json +1 -1
  61. package/packages/context-manager/package.json +1 -1
  62. package/packages/cookie-sanitizer/package.json +1 -1
  63. package/packages/cost-optimizer/package.json +1 -1
  64. package/packages/dependency-analyzer/package.json +1 -1
  65. package/packages/django-plugin/package.json +1 -1
  66. package/packages/doc-generator/package.json +1 -1
  67. package/packages/evaluation-engine/package.json +1 -1
  68. package/packages/evaluation-runner/package.json +1 -1
  69. package/packages/event-bus/package.json +1 -1
  70. package/packages/event-store/package.json +1 -1
  71. package/packages/experience-replay/package.json +1 -1
  72. package/packages/feedback-collector/package.json +1 -1
  73. package/packages/file-copy-helper/package.json +1 -1
  74. package/packages/framework-detector/package.json +1 -1
  75. package/packages/framework-plugin-sdk/package.json +1 -1
  76. package/packages/frappe-plugin/package.json +1 -1
  77. package/packages/generic-web-plugin/package.json +1 -1
  78. package/packages/health-monitor/package.json +1 -1
  79. package/packages/intent-analyzer/dist/src/index.d.ts +2 -0
  80. package/packages/intent-analyzer/dist/src/index.d.ts.map +1 -1
  81. package/packages/intent-analyzer/dist/src/index.js +1 -0
  82. package/packages/intent-analyzer/dist/src/index.js.map +1 -1
  83. package/packages/intent-analyzer/dist/src/ping-pong.d.ts +56 -0
  84. package/packages/intent-analyzer/dist/src/ping-pong.d.ts.map +1 -0
  85. package/packages/intent-analyzer/dist/src/ping-pong.js +286 -0
  86. package/packages/intent-analyzer/dist/src/ping-pong.js.map +1 -0
  87. package/packages/intent-analyzer/package.json +1 -1
  88. package/packages/intent-analyzer/src/index.ts +13 -0
  89. package/packages/intent-analyzer/src/ping-pong.ts +387 -0
  90. package/packages/intent-analyzer/test/ping-pong.test.ts +96 -0
  91. package/packages/jev-judge/README.md +234 -0
  92. package/packages/jev-judge/dist/decision-engine.d.ts +100 -0
  93. package/packages/jev-judge/dist/decision-engine.d.ts.map +1 -0
  94. package/packages/jev-judge/dist/decision-engine.js +151 -0
  95. package/packages/jev-judge/dist/decision-engine.js.map +1 -0
  96. package/packages/jev-judge/dist/e2e-judge.d.ts +61 -0
  97. package/packages/jev-judge/dist/e2e-judge.d.ts.map +1 -0
  98. package/packages/jev-judge/dist/e2e-judge.js +268 -0
  99. package/packages/jev-judge/dist/e2e-judge.js.map +1 -0
  100. package/packages/jev-judge/dist/flaky-detector.d.ts +34 -0
  101. package/packages/jev-judge/dist/flaky-detector.d.ts.map +1 -0
  102. package/packages/jev-judge/dist/flaky-detector.js +154 -0
  103. package/packages/jev-judge/dist/flaky-detector.js.map +1 -0
  104. package/packages/jev-judge/dist/index.d.ts +77 -0
  105. package/packages/jev-judge/dist/index.d.ts.map +1 -0
  106. package/packages/jev-judge/dist/index.js +271 -0
  107. package/packages/jev-judge/dist/index.js.map +1 -0
  108. package/packages/jev-judge/dist/types.d.ts +97 -0
  109. package/packages/jev-judge/dist/types.d.ts.map +1 -0
  110. package/packages/jev-judge/dist/types.js +8 -0
  111. package/packages/jev-judge/dist/types.js.map +1 -0
  112. package/packages/jev-judge/package.json +46 -0
  113. package/packages/jev-judge/src/decision-engine.ts +231 -0
  114. package/packages/jev-judge/src/e2e-judge.ts +364 -0
  115. package/packages/jev-judge/src/flaky-detector.ts +190 -0
  116. package/packages/jev-judge/src/index.ts +345 -0
  117. package/packages/jev-judge/src/types.ts +131 -0
  118. package/packages/knowledge-graph/package.json +1 -1
  119. package/packages/knowledge-retrieval/package.json +1 -1
  120. package/packages/laravel-plugin/package.json +1 -1
  121. package/packages/learning-engine/package.json +1 -1
  122. package/packages/mcp-adapter/package.json +1 -1
  123. package/packages/memory-engine/package.json +1 -1
  124. package/packages/milestone-manager/package.json +1 -1
  125. package/packages/model-registry/package.json +1 -1
  126. package/packages/nextjs-plugin/package.json +1 -1
  127. package/packages/notification/package.json +1 -1
  128. package/packages/observability/package.json +1 -1
  129. package/packages/okf-indexer/package.json +1 -1
  130. package/packages/performance-optimizer/package.json +1 -1
  131. package/packages/privilege-broker/package.json +1 -1
  132. package/packages/project-analyzer/package.json +1 -1
  133. package/packages/project-bootstrap/package.json +1 -1
  134. package/packages/projection-engine/package.json +1 -1
  135. package/packages/prompt-compiler/package.json +1 -1
  136. package/packages/prompt-versioning/package.json +1 -1
  137. package/packages/provider-adapter-sdk/package.json +1 -1
  138. package/packages/provider-router/package.json +1 -1
  139. package/packages/provider-selector/package.json +1 -1
  140. package/packages/providers/package.json +1 -1
  141. package/packages/quota-manager/package.json +1 -1
  142. package/packages/rate-limiter/package.json +1 -1
  143. package/packages/react-vite-plugin/package.json +1 -1
  144. package/packages/release-manager/package.json +1 -1
  145. package/packages/requirement-compiler/package.json +1 -1
  146. package/packages/runtime/package.json +1 -1
  147. package/packages/scheduler/package.json +1 -1
  148. package/packages/scheduler-adapter/package.json +1 -1
  149. package/packages/session/package.json +1 -1
  150. package/packages/session-api/package.json +1 -1
  151. package/packages/session-export/package.json +1 -1
  152. package/packages/shared-context/package.json +1 -1
  153. package/packages/skill-mcp-client/package.json +1 -1
  154. package/packages/skill-registry/package.json +1 -1
  155. package/packages/sprint-planner/package.json +1 -1
  156. package/packages/subscription-engine/package.json +1 -1
  157. package/packages/task-compiler/package.json +1 -1
  158. package/packages/tencentdb-memory/package.json +1 -1
  159. package/packages/tencentdb-sync/package.json +1 -1
  160. package/packages/test-data-generator/package.json +1 -1
  161. package/packages/test-generator/package.json +1 -1
  162. package/packages/todo-bd-sync/README.md +71 -0
  163. package/packages/todo-bd-sync/package.json +1 -1
  164. package/packages/todo-bd-sync/src/extension.ts +16 -1
  165. package/packages/token-estimation/package.json +1 -1
  166. package/packages/token-optimizer/package.json +1 -1
  167. package/packages/trajectory/dist/src/approved-patterns.d.ts +51 -0
  168. package/packages/trajectory/dist/src/approved-patterns.d.ts.map +1 -0
  169. package/packages/trajectory/dist/src/approved-patterns.js +134 -0
  170. package/packages/trajectory/dist/src/approved-patterns.js.map +1 -0
  171. package/packages/trajectory/dist/src/index.d.ts +13 -0
  172. package/packages/trajectory/dist/src/index.d.ts.map +1 -0
  173. package/packages/trajectory/dist/src/index.js +12 -0
  174. package/packages/trajectory/dist/src/index.js.map +1 -0
  175. package/packages/trajectory/dist/src/store.d.ts +69 -0
  176. package/packages/trajectory/dist/src/store.d.ts.map +1 -0
  177. package/packages/trajectory/dist/src/store.js +253 -0
  178. package/packages/trajectory/dist/src/store.js.map +1 -0
  179. package/packages/trajectory/dist/src/types.d.ts +73 -0
  180. package/packages/trajectory/dist/src/types.d.ts.map +1 -0
  181. package/packages/trajectory/dist/src/types.js +9 -0
  182. package/packages/trajectory/dist/src/types.js.map +1 -0
  183. package/packages/trajectory/dist/test/store.test.d.ts +2 -0
  184. package/packages/trajectory/dist/test/store.test.d.ts.map +1 -0
  185. package/packages/trajectory/dist/test/store.test.js +228 -0
  186. package/packages/trajectory/dist/test/store.test.js.map +1 -0
  187. package/packages/trajectory/package.json +29 -0
  188. package/packages/trajectory/src/approved-patterns.ts +180 -0
  189. package/packages/trajectory/src/index.ts +26 -0
  190. package/packages/trajectory/src/store.ts +285 -0
  191. package/packages/trajectory/src/types.ts +84 -0
  192. package/packages/trajectory/test/store.test.d.ts +2 -0
  193. package/packages/trajectory/test/store.test.ts +250 -0
  194. package/packages/tui/package.json +1 -1
  195. package/packages/types/package.json +1 -1
  196. package/packages/workflow-events/package.json +1 -1
  197. package/packages/workspace-scanner/package.json +1 -1
  198. package/packages/worktree/package.json +1 -1
  199. package/packages/write-review/package.json +1 -1
  200. package/packages/write-review/src/blackboard.ts +24 -1
  201. package/packages/write-review/src/types.ts +13 -0
  202. package/skills/harness-runtime/SKILL.md +67 -4
@@ -27,8 +27,16 @@
27
27
  */
28
28
 
29
29
  import { randomUUID } from "node:crypto";
30
- import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
30
+ import {
31
+ existsSync,
32
+ mkdirSync,
33
+ readdirSync,
34
+ readFileSync,
35
+ writeFileSync,
36
+ } from "node:fs";
37
+ import { execSync } from "node:child_process";
31
38
  import { join } from "node:path";
39
+ import { homedir } from "node:os";
32
40
 
33
41
  import { LeaseManager } from "../../packages/autonomous-runtime/src/lease.js";
34
42
  import type { ApprovalClass } from "../../packages/autonomous-runtime/src/types.js";
@@ -47,11 +55,21 @@ import {
47
55
  } from "../../packages/event-bus/src/herdr-bus.js";
48
56
  import { NotificationCenter } from "../../packages/notification/dist/notification-center.js";
49
57
 
50
- import { invokeWithSurgeRetry, type SurgePolicy } from "./surge.js";
58
+ import {
59
+ invokeWithSurgeRetry,
60
+ invokeWithGLMRetry,
61
+ SurgeScheduler,
62
+ type SurgePolicy,
63
+ } from "./surge.js";
51
64
  import type { LoopWidget } from "./widget.js";
52
65
  import { StatusLineManager, isPiLensAvailable } from "./status-line.js";
53
66
  import { createLoopCheckpointer } from "./checkpointer.js";
54
67
 
68
+ import {
69
+ getTrajectoryStore,
70
+ getApprovedPatternStore,
71
+ } from "../../packages/trajectory/src/index.js";
72
+
55
73
  import {
56
74
  buildDryRunDeps,
57
75
  buildRealLoopDeps,
@@ -819,7 +837,7 @@ export class LoopDaemon {
819
837
  surgePolicy: config.surgePolicy,
820
838
  taskTimeoutMs: config.taskTimeoutMs,
821
839
  checkpointer: config.checkpointer,
822
- sources: config.sources ?? ["inbox", "bus"],
840
+ sources: config.sources ?? ["inbox", "bus"], // bd-tasks is opt-in: add it only for harness-managed bd issues
823
841
  workspace: config.workspace ?? getHerdrWorkspace(),
824
842
  notificationConfig: config.notificationConfig ?? undefined,
825
843
  tunnelCommand: config.tunnelCommand,
@@ -974,6 +992,8 @@ export class LoopDaemon {
974
992
  console.error(
975
993
  `[daemon] Task ${task.taskId} failed: ${err instanceof Error ? err.message : String(err)}`,
976
994
  );
995
+ // Release lease so the task can be picked up again on next poll
996
+ this._releaseLease(task.taskId);
977
997
  this.running.delete(task.taskId);
978
998
  });
979
999
  }
@@ -1025,6 +1045,7 @@ export class LoopDaemon {
1025
1045
  : await buildRealLoopDeps({
1026
1046
  maxIterations: this.config.maxIterations,
1027
1047
  blackboardDir: this.bus.getWorkspace(),
1048
+ request: task.request,
1028
1049
  }));
1029
1050
 
1030
1051
  // Inject transition publishing into onStep (also drives the widget)
@@ -1035,6 +1056,11 @@ export class LoopDaemon {
1035
1056
  this._updateWidget(step, state);
1036
1057
  };
1037
1058
 
1059
+ // ── Trajectory capture (M7a) ──────────────────────────────────────────
1060
+ const store = getTrajectoryStore();
1061
+ const trajId = store.start(task.request);
1062
+ const trajStartMs = Date.now();
1063
+
1038
1064
  // ── Run the graph (surge-aware: 529/overloaded pauses auto-resume) ──
1039
1065
  log(
1040
1066
  `Starting write-review loop (maxIterations=${this.config.maxIterations})`,
@@ -1055,24 +1081,77 @@ export class LoopDaemon {
1055
1081
  })();
1056
1082
  const loop: WriteReviewLoop = buildWriteReviewLoop(deps, { checkpointer });
1057
1083
 
1084
+ // Persistent scheduler: attempt counter survives across multiple loop.invoke()
1085
+ // calls within the same task. This enables true exponential escalation
1086
+ // for the 2nd, 3rd, ... surge — attempt 1 waits ~3min, attempt 2 waits ~6min, etc.
1087
+ const surgeScheduler = new SurgeScheduler(this.config.surgePolicy);
1088
+
1058
1089
  const invokeTask = (signal?: AbortSignal) =>
1059
1090
  invokeWithSurgeRetry(
1060
1091
  () =>
1061
- loop.invoke(
1062
- { request: task.request },
1063
- { configurable: { thread_id: loopId }, signal },
1092
+ invokeWithGLMRetry(
1093
+ () =>
1094
+ loop.invoke(
1095
+ { request: task.request },
1096
+ { configurable: { thread_id: loopId }, signal },
1097
+ ),
1098
+ {
1099
+ tickMs: 30_000,
1100
+ onGLMQuota: (signal) => {
1101
+ const secs = Math.max(
1102
+ 0,
1103
+ Math.round((signal.resetAtEpoch - Date.now()) / 1000),
1104
+ );
1105
+ const mins = Math.floor(secs / 60);
1106
+ const remSecs = secs % 60;
1107
+ const countdown =
1108
+ mins > 0
1109
+ ? ` (resets in ${mins}m ${remSecs}s)`
1110
+ : ` (resets in ${remSecs}s)`;
1111
+ log(`GLM quota exhausted${countdown}`);
1112
+ this.widget?.setSurgePause(new Date(signal.resetAt));
1113
+ this.statusLine?.updateLoopStatus(
1114
+ `[Q] GLM quota — resets${countdown}`,
1115
+ );
1116
+ },
1117
+ onGLMRetry: () => {
1118
+ log("GLM quota window reached — retrying loop");
1119
+ },
1120
+ },
1064
1121
  ),
1065
1122
  {
1066
1123
  policy: this.config.surgePolicy,
1124
+ scheduler: surgeScheduler,
1067
1125
  onSurge: ({ attempt, delayMs }) =>
1068
1126
  log(
1069
1127
  `Provider surge (529) — resume in ${Math.round(delayMs / 1000)}s (attempt ${attempt})`,
1070
1128
  ),
1071
- onExhausted: () => {
1072
- this._notifyHumanReviewNeeded(
1073
- task,
1074
- "Provider surged past max surge attempts — task failed",
1129
+ onExhausted: async () => {
1130
+ // Escalating backoff: when all internal surge retries are exhausted,
1131
+ // sleep for N × 3min (N = number of surges seen so far), then retry.
1132
+ // Surge 1: sleep 3min → retry. Surge 2: sleep 6min → retry. etc.
1133
+ const p = {
1134
+ ...{
1135
+ multiplier: 2,
1136
+ minDelayMs: 30_000,
1137
+ maxDelayMs: 15 * 60_000,
1138
+ jitterRatio: 0.2,
1139
+ maxAttempts: 5,
1140
+ },
1141
+ ...this.config.surgePolicy,
1142
+ };
1143
+ const baseDelayMs = 3 * 60_000; // midpoint of "recovers within 1-5 minutes"
1144
+ const surgeCount = surgeScheduler.attempts; // how many surges already handled
1145
+ const waitMs = Math.min(
1146
+ baseDelayMs * surgeCount,
1147
+ p.maxDelayMs ?? 15 * 60_000,
1075
1148
  );
1149
+ const waitMins = Math.round(waitMs / 60_000);
1150
+ log(
1151
+ `All ${p.maxAttempts} surge retries exhausted (${surgeCount} surges seen) — escalating wait to ${waitMins}m before retrying.`,
1152
+ );
1153
+ await new Promise<void>((resolve) => setTimeout(resolve, waitMs));
1154
+ return true; // signal: handled, please retry
1076
1155
  },
1077
1156
  },
1078
1157
  );
@@ -1085,6 +1164,8 @@ export class LoopDaemon {
1085
1164
  ac.abort();
1086
1165
  }, this.config.taskTimeoutMs);
1087
1166
  try {
1167
+ // NOTE: If timeout wins, finalState will be undefined. We handle
1168
+ // this below so we don't crash on finalState.review.
1088
1169
  finalState = await Promise.race([
1089
1170
  invokeTask(ac.signal),
1090
1171
  timeoutPromise(this.config.taskTimeoutMs),
@@ -1092,6 +1173,18 @@ export class LoopDaemon {
1092
1173
  } finally {
1093
1174
  clearTimeout(timeout);
1094
1175
  }
1176
+ // Handle timeout case: timeoutPromise resolved first, finalState is undefined.
1177
+ // Must not fall through to finalState.review access (would throw).
1178
+ if (finalState === undefined) {
1179
+ log("Task timed out — releasing lease and cleaning up");
1180
+ this._releaseLease(task.taskId);
1181
+ this.running.delete(task.taskId);
1182
+ this._notifyHumanReviewNeeded(
1183
+ task,
1184
+ `Task timed out after ${this.config.taskTimeoutMs}ms — manual review needed`,
1185
+ );
1186
+ return;
1187
+ }
1095
1188
  } else {
1096
1189
  finalState = await invokeTask();
1097
1190
  }
@@ -1101,6 +1194,60 @@ export class LoopDaemon {
1101
1194
 
1102
1195
  log(`Loop finished: ${verdict} after ${iterations} iteration(s)`);
1103
1196
 
1197
+ // ── Trajectory append (M7a) ───────────────────────────────────────────
1198
+ const rawComments = finalState.review?.comments ?? [];
1199
+ const comments = rawComments.map((c) => ({
1200
+ file: c.file ?? undefined,
1201
+ comment: c.comment,
1202
+ severity: (c.severity ?? "minor") as "minor" | "major" | "critical",
1203
+ }));
1204
+ const files = [
1205
+ ...new Set(rawComments.map((c) => c.file).filter(Boolean) as string[]),
1206
+ ];
1207
+ const reason =
1208
+ verdict === "approved"
1209
+ ? "reviewer approved"
1210
+ : verdict === "blocked"
1211
+ ? "reviewer blocked the task"
1212
+ : iterations >= this.config.maxIterations
1213
+ ? `max iterations (${iterations}) reached with changes still requested`
1214
+ : comments.length > 0 && comments.every((c) => c.severity === "minor")
1215
+ ? `converged: only minor comments (${comments.length})`
1216
+ : `stuck: changes still requested after ${iterations} iteration(s)`;
1217
+
1218
+ // M7c: Build the trajectory record and classify convergence
1219
+ const trajRecord = {
1220
+ id: trajId,
1221
+ taskRequest: task.request,
1222
+ createdAt: new Date(trajStartMs).toISOString(),
1223
+ durationMs: Date.now() - trajStartMs,
1224
+ iterations,
1225
+ verdict: verdict as "approved" | "blocked" | "changes_requested",
1226
+ reason,
1227
+ plan: finalState.plan ?? "",
1228
+ code: finalState.code ?? "",
1229
+ files,
1230
+ comments,
1231
+ summary: finalState.review?.summary ?? "",
1232
+ classified: false as boolean,
1233
+ };
1234
+ const classification = store.classify(trajRecord);
1235
+ log(
1236
+ `[M7c] Trajectory "${classification.label}" ` +
1237
+ `(confidence ${Math.round(classification.confidence * 100)}%) — ` +
1238
+ classification.recommendation,
1239
+ );
1240
+
1241
+ // Persist the trajectory record
1242
+ store.append(trajRecord);
1243
+
1244
+ // M7b: Record approved patterns so the reviewer learns from success
1245
+ if (verdict === "approved") {
1246
+ const approvedPatterns = getApprovedPatternStore();
1247
+ approvedPatterns.approve(comments);
1248
+ log(`[M7b] Recorded ${comments.length} approved pattern(s)`);
1249
+ }
1250
+
1104
1251
  // Update widget on loop completion
1105
1252
  if (this.widget) {
1106
1253
  this.widget.setComplete(
@@ -1161,6 +1308,14 @@ export class LoopDaemon {
1161
1308
  // approved
1162
1309
  this._notifyReadyForClient(task);
1163
1310
  }
1311
+
1312
+ // ── Notify agent of loop completion (TUI todo count) ───────────────
1313
+ this._writeLoopCompletion(task, verdict, iterations);
1314
+
1315
+ // ── Close bd issue on loop completion ───────────────────────────────
1316
+ if (task.source === "bd-tasks") {
1317
+ this._closeBdIssue(task.taskId, verdict);
1318
+ }
1164
1319
  } finally {
1165
1320
  this._releaseLease(task.taskId);
1166
1321
  this.running.delete(task.taskId);
@@ -1222,9 +1377,17 @@ export class LoopDaemon {
1222
1377
  w.startIteration(state.iteration, this.config.maxIterations);
1223
1378
  break;
1224
1379
  }
1380
+ case "glmq": {
1381
+ // GLM quota hit — show countdown to reset
1382
+ const resetAt =
1383
+ state.iteration > 0
1384
+ ? new Date(state.iteration) // passthrough from onGLMQuota
1385
+ : undefined;
1386
+ w.setSurgePause(resetAt);
1387
+ break;
1388
+ }
1225
1389
  case "review": {
1226
- w.setPhase("reviewing", "GPT reviewing");
1227
- this.statusLine?.updateLoopStatus("[R] reviewing");
1390
+ w.setPhase("reviewing", "GLM reviewing");
1228
1391
  // Process review comments — one record per file
1229
1392
  const comments = state.review?.comments ?? [];
1230
1393
  if (comments.length === 0) break;
@@ -1284,6 +1447,27 @@ export class LoopDaemon {
1284
1447
  });
1285
1448
  }
1286
1449
 
1450
+ private _closeBdIssue(taskId: string, verdict: string): void {
1451
+ // Close the bd issue when the loop finishes.
1452
+ // approved → close as done. changes_requested (exhausted) → close as done.
1453
+ // blocked → leave open for human review.
1454
+ const reason =
1455
+ verdict === "approved"
1456
+ ? "approved"
1457
+ : verdict === "blocked"
1458
+ ? undefined // leave blocked issues open
1459
+ : "completed";
1460
+ if (!reason) return;
1461
+ try {
1462
+ execSync(`bd close ${taskId} --reason "${reason}" 2>/dev/null`, {
1463
+ encoding: "utf8",
1464
+ timeout: 5_000,
1465
+ });
1466
+ } catch {
1467
+ // best-effort — don't fail the task over a missing bd
1468
+ }
1469
+ }
1470
+
1287
1471
  private _notifyReadyForClient(task: TriggeredTask): void {
1288
1472
  if (!this.notificationCenter) return;
1289
1473
  console.log(`[daemon] Notifying ReadyForClient`);
@@ -1298,6 +1482,40 @@ export class LoopDaemon {
1298
1482
  });
1299
1483
  }
1300
1484
 
1485
+ /**
1486
+ * Write a loop-completion event to ~/.pi-harness-runtime/loop-completions/.
1487
+ * The harness extension (pi-harness-runtime package in the pi-coding-agent process)
1488
+ * watches this directory and calls pi.sendUserMessage() to update the TUI todo count.
1489
+ *
1490
+ * File format:
1491
+ * <taskId>.json {"taskId","request","verdict","iterations","finishedAt"}
1492
+ */
1493
+ private _writeLoopCompletion(
1494
+ task: TriggeredTask,
1495
+ verdict: string,
1496
+ iterations: number,
1497
+ ): void {
1498
+ const dir = join(homedir(), ".pi-harness-runtime", "loop-completions");
1499
+ try {
1500
+ if (!existsSync(dir)) {
1501
+ mkdirSync(dir, { recursive: true });
1502
+ }
1503
+ const payload = JSON.stringify({
1504
+ taskId: task.taskId,
1505
+ request: task.request,
1506
+ verdict,
1507
+ iterations,
1508
+ finishedAt: new Date().toISOString(),
1509
+ });
1510
+ // Use taskId as filename; .json extension so it reads as JSON
1511
+ const safeId = task.taskId.replace(/[^a-zA-Z0-9_-]/g, "_");
1512
+ writeFileSync(join(dir, `${safeId}.json`), payload, "utf8");
1513
+ } catch (err) {
1514
+ // Best-effort: never fail the loop over a notification write
1515
+ console.warn(`[daemon] Failed to write loop completion: ${err}`);
1516
+ }
1517
+ }
1518
+
1301
1519
  private _buildChannels(cfg: NonNullable<DaemonConfig["notificationConfig"]>) {
1302
1520
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1303
1521
  const channels: any[] = [];
@@ -0,0 +1,97 @@
1
+ /**
2
+ * File system tools for the coder agent — read and write files on disk.
3
+ *
4
+ * These tools let the MiniMax coder edit the actual repository instead of
5
+ * just outputting code blocks. Tools are plain Node.js fs wrappers; the
6
+ * agent receives structured JSON results it can act on.
7
+ *
8
+ * Wiki: wiki/multi-agent-langchain.md §file-tools
9
+ */
10
+ import { z } from "zod";
11
+ /**
12
+ * Read the contents of a file.
13
+ *
14
+ * Use this before editing to see the current code.
15
+ * Returns the full file contents as a string.
16
+ */
17
+ export declare const readFile: import("langchain").DynamicStructuredTool<z.ZodObject<{
18
+ path: z.ZodString;
19
+ encoding: z.ZodOptional<z.ZodEnum<{
20
+ ascii: "ascii";
21
+ base64: "base64";
22
+ "utf-16": "utf-16";
23
+ "utf-8": "utf-8";
24
+ }>>;
25
+ }, z.core.$strip>, {
26
+ path: string;
27
+ encoding?: "ascii" | "base64" | "utf-16" | "utf-8" | undefined;
28
+ }, {
29
+ path: string;
30
+ encoding?: "ascii" | "base64" | "utf-16" | "utf-8" | undefined;
31
+ }, string, unknown, "read_file">;
32
+ /**
33
+ * Write or overwrite a file on disk.
34
+ *
35
+ * Creates parent directories if they don't exist.
36
+ * The caller (an agent) is responsible for ensuring the content is correct.
37
+ */
38
+ export declare const writeFile: import("langchain").DynamicStructuredTool<z.ZodObject<{
39
+ path: z.ZodString;
40
+ content: z.ZodString;
41
+ append: z.ZodOptional<z.ZodBoolean>;
42
+ }, z.core.$strip>, {
43
+ path: string;
44
+ content: string;
45
+ append?: boolean | undefined;
46
+ }, {
47
+ path: string;
48
+ content: string;
49
+ append?: boolean | undefined;
50
+ }, string, unknown, "write_file">;
51
+ /**
52
+ * List files in a directory (non-recursive).
53
+ *
54
+ * Useful for discovering the project structure.
55
+ */
56
+ export declare const listDir: import("langchain").DynamicStructuredTool<z.ZodObject<{
57
+ path: z.ZodString;
58
+ }, z.core.$strip>, {
59
+ path: string;
60
+ }, {
61
+ path: string;
62
+ }, string, unknown, "list_directory">;
63
+ /** All file system tools, ready to pass to `createCoderAgent`. */
64
+ export declare const fileTools: (import("langchain").DynamicStructuredTool<z.ZodObject<{
65
+ path: z.ZodString;
66
+ encoding: z.ZodOptional<z.ZodEnum<{
67
+ ascii: "ascii";
68
+ base64: "base64";
69
+ "utf-16": "utf-16";
70
+ "utf-8": "utf-8";
71
+ }>>;
72
+ }, z.core.$strip>, {
73
+ path: string;
74
+ encoding?: "ascii" | "base64" | "utf-16" | "utf-8" | undefined;
75
+ }, {
76
+ path: string;
77
+ encoding?: "ascii" | "base64" | "utf-16" | "utf-8" | undefined;
78
+ }, string, unknown, "read_file"> | import("langchain").DynamicStructuredTool<z.ZodObject<{
79
+ path: z.ZodString;
80
+ content: z.ZodString;
81
+ append: z.ZodOptional<z.ZodBoolean>;
82
+ }, z.core.$strip>, {
83
+ path: string;
84
+ content: string;
85
+ append?: boolean | undefined;
86
+ }, {
87
+ path: string;
88
+ content: string;
89
+ append?: boolean | undefined;
90
+ }, string, unknown, "write_file"> | import("langchain").DynamicStructuredTool<z.ZodObject<{
91
+ path: z.ZodString;
92
+ }, z.core.$strip>, {
93
+ path: string;
94
+ }, {
95
+ path: string;
96
+ }, string, unknown, "list_directory">)[];
97
+ //# sourceMappingURL=file-tools.d.ts.map
@@ -10,8 +10,14 @@
10
10
 
11
11
  import { tool } from "langchain";
12
12
  import { z } from "zod";
13
- import { readFileSync, writeFileSync, existsSync, statSync } from "node:fs";
14
- import { join, isAbsolute, resolve } from "node:path";
13
+ import {
14
+ mkdirSync,
15
+ readFileSync,
16
+ writeFileSync,
17
+ existsSync,
18
+ statSync,
19
+ } from "node:fs";
20
+ import { dirname, join, isAbsolute, resolve } from "node:path";
15
21
 
16
22
  // ─── Helpers ─────────────────────────────────────────────────────────────────
17
23
 
@@ -104,9 +110,11 @@ export const writeFile = tool(
104
110
  }) => {
105
111
  try {
106
112
  const resolved = resolvePath(path, process.cwd());
113
+ // P1-4: create parent directories before writing (fixes ENOENT on nested paths)
114
+ mkdirSync(dirname(resolved), { recursive: true });
107
115
  const mode = append ? "a" : "w";
108
- // Normalize content: convert literal \n sequences to actual newlines
109
- const normalized = content.replace(/\\n/g, "\n");
116
+ // P1-4: stop corrupting literal \\n sequences in code files
117
+ const normalized = content; // drop the replace(); agents use actual \n in strings
110
118
  writeFileSync(resolved, normalized, { encoding: "utf-8", flag: mode });
111
119
  const stats = statSync(resolved);
112
120
  return JSON.stringify({
@@ -125,8 +133,8 @@ export const writeFile = tool(
125
133
  {
126
134
  name: "write_file",
127
135
  description:
128
- "Write or overwrite a file on disk. Creates parent directories if needed. " +
129
- "Use literal \\n in the content string for newlines. " +
136
+ "Write or overwrite a file on disk. Creates parent directories automatically. " +
137
+ "Use actual newline characters in the content string. " +
130
138
  "Set append=true to add to the end of an existing file. " +
131
139
  "Returns JSON with ok, path, size, and action fields.",
132
140
  schema: z.object({