pi-harness-runtime 1.1.49 → 1.1.71-3

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 (203) 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.test.ts +0 -1
  6. package/harness/langchain/agents.ts +196 -13
  7. package/harness/langchain/checkpointer.d.ts +48 -0
  8. package/harness/langchain/checkpointer.test.d.ts +8 -0
  9. package/harness/langchain/cron-cli.d.ts +12 -0
  10. package/harness/langchain/cron-cli.ts +169 -0
  11. package/harness/langchain/daemon.d.ts +161 -0
  12. package/harness/langchain/daemon.test.d.ts +11 -0
  13. package/harness/langchain/daemon.test.ts +62 -5
  14. package/harness/langchain/daemon.ts +231 -12
  15. package/harness/langchain/file-tools.d.ts +97 -0
  16. package/harness/langchain/file-tools.ts +29 -11
  17. package/harness/langchain/graph.d.ts +706 -0
  18. package/harness/langchain/graph.ts +184 -26
  19. package/harness/langchain/model-router.d.ts +63 -0
  20. package/harness/langchain/model-router.test.d.ts +2 -0
  21. package/harness/langchain/model-router.test.ts +191 -0
  22. package/harness/langchain/model-router.ts +188 -0
  23. package/harness/langchain/models.d.ts +72 -0
  24. package/harness/langchain/models.ts +111 -49
  25. package/harness/langchain/ping-pong-middleware.d.ts +28 -0
  26. package/harness/langchain/ping-pong-middleware.test.d.ts +2 -0
  27. package/harness/langchain/ping-pong-middleware.test.ts +69 -0
  28. package/harness/langchain/ping-pong-middleware.ts +204 -0
  29. package/harness/langchain/run.d.ts +15 -0
  30. package/harness/langchain/run.ts +15 -3
  31. package/harness/langchain/scoreboard.test.d.ts +13 -0
  32. package/harness/langchain/scoreboard.test.ts +28 -5
  33. package/harness/langchain/status-line.d.ts +91 -0
  34. package/harness/langchain/status-line.test.d.ts +8 -0
  35. package/harness/langchain/surge.d.ts +103 -0
  36. package/harness/langchain/surge.test.d.ts +10 -0
  37. package/harness/langchain/surge.test.ts +179 -0
  38. package/harness/langchain/surge.ts +188 -19
  39. package/harness/langchain/widget.d.ts +105 -0
  40. package/harness/langchain/widget.test.d.ts +8 -0
  41. package/harness/ssh-detach-interceptor.test.ts +263 -0
  42. package/harness/ssh-detach-interceptor.ts +438 -0
  43. package/index.ts +212 -47
  44. package/package.json +4 -2
  45. package/packages/a2a-adapter/package.json +1 -1
  46. package/packages/architecture-generator/package.json +1 -1
  47. package/packages/auth/package.json +1 -1
  48. package/packages/autonomous-refactor/package.json +1 -1
  49. package/packages/autonomous-runtime/package.json +1 -1
  50. package/packages/cache-strategy/package.json +1 -1
  51. package/packages/capability-registry/package.json +1 -1
  52. package/packages/checkpoint/package.json +1 -1
  53. package/packages/cli-plugin-sdk/package.json +1 -1
  54. package/packages/clipboard/package.json +1 -1
  55. package/packages/clipboard-plugin/package.json +1 -1
  56. package/packages/code-generation/package.json +1 -1
  57. package/packages/code-review/package.json +1 -1
  58. package/packages/codex-adapter/package.json +1 -1
  59. package/packages/config-capture/package.json +1 -1
  60. package/packages/context-compiler/package.json +1 -1
  61. package/packages/context-discovery/package.json +1 -1
  62. package/packages/context-manager/package.json +1 -1
  63. package/packages/cookie-sanitizer/package.json +1 -1
  64. package/packages/cost-optimizer/package.json +1 -1
  65. package/packages/dependency-analyzer/package.json +1 -1
  66. package/packages/django-plugin/package.json +1 -1
  67. package/packages/doc-generator/package.json +1 -1
  68. package/packages/evaluation-engine/package.json +1 -1
  69. package/packages/evaluation-runner/package.json +1 -1
  70. package/packages/event-bus/package.json +1 -1
  71. package/packages/event-store/package.json +1 -1
  72. package/packages/experience-replay/package.json +1 -1
  73. package/packages/feedback-collector/package.json +1 -1
  74. package/packages/file-copy-helper/package.json +1 -1
  75. package/packages/framework-detector/package.json +1 -1
  76. package/packages/framework-plugin-sdk/package.json +1 -1
  77. package/packages/frappe-plugin/package.json +1 -1
  78. package/packages/generic-web-plugin/package.json +1 -1
  79. package/packages/health-monitor/package.json +1 -1
  80. package/packages/intent-analyzer/dist/src/index.d.ts +2 -0
  81. package/packages/intent-analyzer/dist/src/index.d.ts.map +1 -1
  82. package/packages/intent-analyzer/dist/src/index.js +1 -0
  83. package/packages/intent-analyzer/dist/src/index.js.map +1 -1
  84. package/packages/intent-analyzer/dist/src/ping-pong.d.ts +56 -0
  85. package/packages/intent-analyzer/dist/src/ping-pong.d.ts.map +1 -0
  86. package/packages/intent-analyzer/dist/src/ping-pong.js +286 -0
  87. package/packages/intent-analyzer/dist/src/ping-pong.js.map +1 -0
  88. package/packages/intent-analyzer/package.json +1 -1
  89. package/packages/intent-analyzer/src/index.ts +13 -0
  90. package/packages/intent-analyzer/src/ping-pong.ts +387 -0
  91. package/packages/intent-analyzer/test/ping-pong.test.ts +96 -0
  92. package/packages/jev-judge/README.md +234 -0
  93. package/packages/jev-judge/dist/decision-engine.d.ts +100 -0
  94. package/packages/jev-judge/dist/decision-engine.d.ts.map +1 -0
  95. package/packages/jev-judge/dist/decision-engine.js +151 -0
  96. package/packages/jev-judge/dist/decision-engine.js.map +1 -0
  97. package/packages/jev-judge/dist/e2e-judge.d.ts +61 -0
  98. package/packages/jev-judge/dist/e2e-judge.d.ts.map +1 -0
  99. package/packages/jev-judge/dist/e2e-judge.js +268 -0
  100. package/packages/jev-judge/dist/e2e-judge.js.map +1 -0
  101. package/packages/jev-judge/dist/flaky-detector.d.ts +34 -0
  102. package/packages/jev-judge/dist/flaky-detector.d.ts.map +1 -0
  103. package/packages/jev-judge/dist/flaky-detector.js +154 -0
  104. package/packages/jev-judge/dist/flaky-detector.js.map +1 -0
  105. package/packages/jev-judge/dist/index.d.ts +77 -0
  106. package/packages/jev-judge/dist/index.d.ts.map +1 -0
  107. package/packages/jev-judge/dist/index.js +271 -0
  108. package/packages/jev-judge/dist/index.js.map +1 -0
  109. package/packages/jev-judge/dist/types.d.ts +97 -0
  110. package/packages/jev-judge/dist/types.d.ts.map +1 -0
  111. package/packages/jev-judge/dist/types.js +8 -0
  112. package/packages/jev-judge/dist/types.js.map +1 -0
  113. package/packages/jev-judge/package.json +46 -0
  114. package/packages/jev-judge/src/decision-engine.ts +231 -0
  115. package/packages/jev-judge/src/e2e-judge.ts +364 -0
  116. package/packages/jev-judge/src/flaky-detector.ts +190 -0
  117. package/packages/jev-judge/src/index.ts +345 -0
  118. package/packages/jev-judge/src/types.ts +131 -0
  119. package/packages/knowledge-graph/package.json +1 -1
  120. package/packages/knowledge-retrieval/package.json +1 -1
  121. package/packages/laravel-plugin/package.json +1 -1
  122. package/packages/learning-engine/package.json +1 -1
  123. package/packages/mcp-adapter/package.json +1 -1
  124. package/packages/memory-engine/package.json +1 -1
  125. package/packages/milestone-manager/package.json +1 -1
  126. package/packages/model-registry/package.json +1 -1
  127. package/packages/nextjs-plugin/package.json +1 -1
  128. package/packages/notification/package.json +1 -1
  129. package/packages/observability/package.json +1 -1
  130. package/packages/okf-indexer/package.json +1 -1
  131. package/packages/performance-optimizer/package.json +1 -1
  132. package/packages/privilege-broker/package.json +1 -1
  133. package/packages/project-analyzer/package.json +1 -1
  134. package/packages/project-bootstrap/package.json +1 -1
  135. package/packages/projection-engine/package.json +1 -1
  136. package/packages/prompt-compiler/package.json +1 -1
  137. package/packages/prompt-versioning/package.json +1 -1
  138. package/packages/provider-adapter-sdk/package.json +1 -1
  139. package/packages/provider-router/package.json +1 -1
  140. package/packages/provider-selector/package.json +1 -1
  141. package/packages/providers/package.json +1 -1
  142. package/packages/quota-manager/package.json +1 -1
  143. package/packages/rate-limiter/package.json +1 -1
  144. package/packages/react-vite-plugin/package.json +1 -1
  145. package/packages/release-manager/package.json +1 -1
  146. package/packages/requirement-compiler/package.json +1 -1
  147. package/packages/runtime/package.json +1 -1
  148. package/packages/scheduler/package.json +1 -1
  149. package/packages/scheduler-adapter/package.json +1 -1
  150. package/packages/session/package.json +1 -1
  151. package/packages/session-api/package.json +1 -1
  152. package/packages/session-export/package.json +1 -1
  153. package/packages/shared-context/package.json +1 -1
  154. package/packages/skill-mcp-client/package.json +1 -1
  155. package/packages/skill-registry/package.json +1 -1
  156. package/packages/sprint-planner/package.json +1 -1
  157. package/packages/subscription-engine/package.json +1 -1
  158. package/packages/task-compiler/package.json +1 -1
  159. package/packages/tencentdb-memory/package.json +1 -1
  160. package/packages/tencentdb-sync/package.json +1 -1
  161. package/packages/test-data-generator/package.json +1 -1
  162. package/packages/test-generator/package.json +1 -1
  163. package/packages/todo-bd-sync/README.md +71 -0
  164. package/packages/todo-bd-sync/package.json +1 -1
  165. package/packages/todo-bd-sync/src/extension.ts +16 -1
  166. package/packages/token-estimation/package.json +1 -1
  167. package/packages/token-optimizer/package.json +1 -1
  168. package/packages/trajectory/dist/src/approved-patterns.d.ts +51 -0
  169. package/packages/trajectory/dist/src/approved-patterns.d.ts.map +1 -0
  170. package/packages/trajectory/dist/src/approved-patterns.js +134 -0
  171. package/packages/trajectory/dist/src/approved-patterns.js.map +1 -0
  172. package/packages/trajectory/dist/src/index.d.ts +13 -0
  173. package/packages/trajectory/dist/src/index.d.ts.map +1 -0
  174. package/packages/trajectory/dist/src/index.js +12 -0
  175. package/packages/trajectory/dist/src/index.js.map +1 -0
  176. package/packages/trajectory/dist/src/store.d.ts +69 -0
  177. package/packages/trajectory/dist/src/store.d.ts.map +1 -0
  178. package/packages/trajectory/dist/src/store.js +253 -0
  179. package/packages/trajectory/dist/src/store.js.map +1 -0
  180. package/packages/trajectory/dist/src/types.d.ts +73 -0
  181. package/packages/trajectory/dist/src/types.d.ts.map +1 -0
  182. package/packages/trajectory/dist/src/types.js +9 -0
  183. package/packages/trajectory/dist/src/types.js.map +1 -0
  184. package/packages/trajectory/dist/test/store.test.d.ts +2 -0
  185. package/packages/trajectory/dist/test/store.test.d.ts.map +1 -0
  186. package/packages/trajectory/dist/test/store.test.js +228 -0
  187. package/packages/trajectory/dist/test/store.test.js.map +1 -0
  188. package/packages/trajectory/package.json +29 -0
  189. package/packages/trajectory/src/approved-patterns.ts +180 -0
  190. package/packages/trajectory/src/index.ts +26 -0
  191. package/packages/trajectory/src/store.ts +285 -0
  192. package/packages/trajectory/src/types.ts +84 -0
  193. package/packages/trajectory/test/store.test.d.ts +2 -0
  194. package/packages/trajectory/test/store.test.ts +250 -0
  195. package/packages/tui/package.json +1 -1
  196. package/packages/types/package.json +1 -1
  197. package/packages/workflow-events/package.json +1 -1
  198. package/packages/workspace-scanner/package.json +1 -1
  199. package/packages/worktree/package.json +1 -1
  200. package/packages/write-review/package.json +1 -1
  201. package/packages/write-review/src/blackboard.ts +38 -5
  202. package/packages/write-review/src/types.ts +13 -0
  203. 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,11 +837,12 @@ 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,
826
844
  widget: config.widget,
845
+ deps: config.deps,
827
846
  } as Required<DaemonConfig>;
828
847
 
829
848
  this.agentId = this.config.agentId;
@@ -973,6 +992,8 @@ export class LoopDaemon {
973
992
  console.error(
974
993
  `[daemon] Task ${task.taskId} failed: ${err instanceof Error ? err.message : String(err)}`,
975
994
  );
995
+ // Release lease so the task can be picked up again on next poll
996
+ this._releaseLease(task.taskId);
976
997
  this.running.delete(task.taskId);
977
998
  });
978
999
  }
@@ -1024,6 +1045,7 @@ export class LoopDaemon {
1024
1045
  : await buildRealLoopDeps({
1025
1046
  maxIterations: this.config.maxIterations,
1026
1047
  blackboardDir: this.bus.getWorkspace(),
1048
+ request: task.request,
1027
1049
  }));
1028
1050
 
1029
1051
  // Inject transition publishing into onStep (also drives the widget)
@@ -1034,6 +1056,11 @@ export class LoopDaemon {
1034
1056
  this._updateWidget(step, state);
1035
1057
  };
1036
1058
 
1059
+ // ── Trajectory capture (M7a) ──────────────────────────────────────────
1060
+ const store = getTrajectoryStore();
1061
+ const trajId = store.start(task.request);
1062
+ const trajStartMs = Date.now();
1063
+
1037
1064
  // ── Run the graph (surge-aware: 529/overloaded pauses auto-resume) ──
1038
1065
  log(
1039
1066
  `Starting write-review loop (maxIterations=${this.config.maxIterations})`,
@@ -1054,24 +1081,77 @@ export class LoopDaemon {
1054
1081
  })();
1055
1082
  const loop: WriteReviewLoop = buildWriteReviewLoop(deps, { checkpointer });
1056
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
+
1057
1089
  const invokeTask = (signal?: AbortSignal) =>
1058
1090
  invokeWithSurgeRetry(
1059
1091
  () =>
1060
- loop.invoke(
1061
- { request: task.request },
1062
- { 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
+ },
1063
1121
  ),
1064
1122
  {
1065
1123
  policy: this.config.surgePolicy,
1124
+ scheduler: surgeScheduler,
1066
1125
  onSurge: ({ attempt, delayMs }) =>
1067
1126
  log(
1068
1127
  `Provider surge (529) — resume in ${Math.round(delayMs / 1000)}s (attempt ${attempt})`,
1069
1128
  ),
1070
- onExhausted: () => {
1071
- this._notifyHumanReviewNeeded(
1072
- task,
1073
- "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,
1074
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
1075
1155
  },
1076
1156
  },
1077
1157
  );
@@ -1084,6 +1164,8 @@ export class LoopDaemon {
1084
1164
  ac.abort();
1085
1165
  }, this.config.taskTimeoutMs);
1086
1166
  try {
1167
+ // NOTE: If timeout wins, finalState will be undefined. We handle
1168
+ // this below so we don't crash on finalState.review.
1087
1169
  finalState = await Promise.race([
1088
1170
  invokeTask(ac.signal),
1089
1171
  timeoutPromise(this.config.taskTimeoutMs),
@@ -1091,6 +1173,18 @@ export class LoopDaemon {
1091
1173
  } finally {
1092
1174
  clearTimeout(timeout);
1093
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
+ }
1094
1188
  } else {
1095
1189
  finalState = await invokeTask();
1096
1190
  }
@@ -1100,6 +1194,60 @@ export class LoopDaemon {
1100
1194
 
1101
1195
  log(`Loop finished: ${verdict} after ${iterations} iteration(s)`);
1102
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
+
1103
1251
  // Update widget on loop completion
1104
1252
  if (this.widget) {
1105
1253
  this.widget.setComplete(
@@ -1160,6 +1308,14 @@ export class LoopDaemon {
1160
1308
  // approved
1161
1309
  this._notifyReadyForClient(task);
1162
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
+ }
1163
1319
  } finally {
1164
1320
  this._releaseLease(task.taskId);
1165
1321
  this.running.delete(task.taskId);
@@ -1221,9 +1377,17 @@ export class LoopDaemon {
1221
1377
  w.startIteration(state.iteration, this.config.maxIterations);
1222
1378
  break;
1223
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
+ }
1224
1389
  case "review": {
1225
- w.setPhase("reviewing", "GPT reviewing");
1226
- this.statusLine?.updateLoopStatus("[R] reviewing");
1390
+ w.setPhase("reviewing", "GLM reviewing");
1227
1391
  // Process review comments — one record per file
1228
1392
  const comments = state.review?.comments ?? [];
1229
1393
  if (comments.length === 0) break;
@@ -1283,6 +1447,27 @@ export class LoopDaemon {
1283
1447
  });
1284
1448
  }
1285
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
+
1286
1471
  private _notifyReadyForClient(task: TriggeredTask): void {
1287
1472
  if (!this.notificationCenter) return;
1288
1473
  console.log(`[daemon] Notifying ReadyForClient`);
@@ -1297,6 +1482,40 @@ export class LoopDaemon {
1297
1482
  });
1298
1483
  }
1299
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
+
1300
1519
  private _buildChannels(cfg: NonNullable<DaemonConfig["notificationConfig"]>) {
1301
1520
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1302
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
 
@@ -46,7 +52,9 @@ export const readFile = tool(
46
52
  }
47
53
  const stats = statSync(resolved);
48
54
  if (stats.isDirectory()) {
49
- return JSON.stringify({ error: `Path is a directory, not a file: ${resolved}` });
55
+ return JSON.stringify({
56
+ error: `Path is a directory, not a file: ${resolved}`,
57
+ });
50
58
  }
51
59
  const content = readFileSync(resolved, encoding as BufferEncoding);
52
60
  return JSON.stringify({
@@ -70,7 +78,9 @@ export const readFile = tool(
70
78
  "Use this before editing a file so you know its current contents. " +
71
79
  "Returns JSON with path, size, lines, and content fields.",
72
80
  schema: z.object({
73
- path: z.string().describe("Absolute path or path relative to the repository root"),
81
+ path: z
82
+ .string()
83
+ .describe("Absolute path or path relative to the repository root"),
74
84
  encoding: z
75
85
  .enum(["utf-8", "utf-16", "ascii", "base64"])
76
86
  .optional()
@@ -100,9 +110,11 @@ export const writeFile = tool(
100
110
  }) => {
101
111
  try {
102
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 });
103
115
  const mode = append ? "a" : "w";
104
- // Normalize content: convert literal \n sequences to actual newlines
105
- 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
106
118
  writeFileSync(resolved, normalized, { encoding: "utf-8", flag: mode });
107
119
  const stats = statSync(resolved);
108
120
  return JSON.stringify({
@@ -121,17 +133,21 @@ export const writeFile = tool(
121
133
  {
122
134
  name: "write_file",
123
135
  description:
124
- "Write or overwrite a file on disk. Creates parent directories if needed. " +
125
- "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. " +
126
138
  "Set append=true to add to the end of an existing file. " +
127
139
  "Returns JSON with ok, path, size, and action fields.",
128
140
  schema: z.object({
129
- path: z.string().describe("Absolute path or path relative to the repository root"),
141
+ path: z
142
+ .string()
143
+ .describe("Absolute path or path relative to the repository root"),
130
144
  content: z.string().describe("File content. Use \\n for newlines."),
131
145
  append: z
132
146
  .boolean()
133
147
  .optional()
134
- .describe("Append to existing file instead of overwriting (default: false)"),
148
+ .describe(
149
+ "Append to existing file instead of overwriting (default: false)",
150
+ ),
135
151
  }),
136
152
  },
137
153
  );
@@ -169,7 +185,9 @@ export const listDir = tool(
169
185
  "List the contents of a directory (files and subdirectories, non-recursive). " +
170
186
  "Returns JSON with path and entries array (each entry has name, type, path).",
171
187
  schema: z.object({
172
- path: z.string().describe("Absolute path or path relative to the repository root"),
188
+ path: z
189
+ .string()
190
+ .describe("Absolute path or path relative to the repository root"),
173
191
  }),
174
192
  },
175
193
  );