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.
- package/CHANGELOG.md +30 -9
- package/README.md +119 -0
- package/harness/langchain/agents.d.ts +143 -0
- package/harness/langchain/agents.test.d.ts +2 -0
- package/harness/langchain/agents.ts +196 -13
- package/harness/langchain/checkpointer.d.ts +48 -0
- package/harness/langchain/checkpointer.test.d.ts +8 -0
- package/harness/langchain/cron-cli.d.ts +12 -0
- package/harness/langchain/cron-cli.ts +169 -0
- package/harness/langchain/daemon.d.ts +161 -0
- package/harness/langchain/daemon.test.d.ts +11 -0
- package/harness/langchain/daemon.test.ts +3 -1
- package/harness/langchain/daemon.ts +230 -12
- package/harness/langchain/file-tools.d.ts +97 -0
- package/harness/langchain/file-tools.ts +14 -6
- package/harness/langchain/graph.d.ts +706 -0
- package/harness/langchain/graph.ts +158 -25
- package/harness/langchain/model-router.d.ts +63 -0
- package/harness/langchain/model-router.test.d.ts +2 -0
- package/harness/langchain/model-router.test.ts +191 -0
- package/harness/langchain/model-router.ts +188 -0
- package/harness/langchain/models.d.ts +72 -0
- package/harness/langchain/models.ts +111 -49
- package/harness/langchain/ping-pong-middleware.d.ts +28 -0
- package/harness/langchain/ping-pong-middleware.test.d.ts +2 -0
- package/harness/langchain/ping-pong-middleware.test.ts +69 -0
- package/harness/langchain/ping-pong-middleware.ts +204 -0
- package/harness/langchain/run.d.ts +15 -0
- package/harness/langchain/run.ts +15 -3
- package/harness/langchain/scoreboard.test.d.ts +13 -0
- package/harness/langchain/scoreboard.test.ts +23 -1
- package/harness/langchain/status-line.d.ts +91 -0
- package/harness/langchain/status-line.test.d.ts +8 -0
- package/harness/langchain/surge.d.ts +103 -0
- package/harness/langchain/surge.test.d.ts +10 -0
- package/harness/langchain/surge.test.ts +179 -0
- package/harness/langchain/surge.ts +188 -19
- package/harness/langchain/widget.d.ts +105 -0
- package/harness/langchain/widget.test.d.ts +8 -0
- package/harness/ssh-detach-interceptor.test.ts +263 -0
- package/harness/ssh-detach-interceptor.ts +438 -0
- package/index.ts +212 -47
- package/package.json +4 -2
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/dist/src/index.d.ts +2 -0
- package/packages/intent-analyzer/dist/src/index.d.ts.map +1 -1
- package/packages/intent-analyzer/dist/src/index.js +1 -0
- package/packages/intent-analyzer/dist/src/index.js.map +1 -1
- package/packages/intent-analyzer/dist/src/ping-pong.d.ts +56 -0
- package/packages/intent-analyzer/dist/src/ping-pong.d.ts.map +1 -0
- package/packages/intent-analyzer/dist/src/ping-pong.js +286 -0
- package/packages/intent-analyzer/dist/src/ping-pong.js.map +1 -0
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/intent-analyzer/src/index.ts +13 -0
- package/packages/intent-analyzer/src/ping-pong.ts +387 -0
- package/packages/intent-analyzer/test/ping-pong.test.ts +96 -0
- package/packages/jev-judge/README.md +234 -0
- package/packages/jev-judge/dist/decision-engine.d.ts +100 -0
- package/packages/jev-judge/dist/decision-engine.d.ts.map +1 -0
- package/packages/jev-judge/dist/decision-engine.js +151 -0
- package/packages/jev-judge/dist/decision-engine.js.map +1 -0
- package/packages/jev-judge/dist/e2e-judge.d.ts +61 -0
- package/packages/jev-judge/dist/e2e-judge.d.ts.map +1 -0
- package/packages/jev-judge/dist/e2e-judge.js +268 -0
- package/packages/jev-judge/dist/e2e-judge.js.map +1 -0
- package/packages/jev-judge/dist/flaky-detector.d.ts +34 -0
- package/packages/jev-judge/dist/flaky-detector.d.ts.map +1 -0
- package/packages/jev-judge/dist/flaky-detector.js +154 -0
- package/packages/jev-judge/dist/flaky-detector.js.map +1 -0
- package/packages/jev-judge/dist/index.d.ts +77 -0
- package/packages/jev-judge/dist/index.d.ts.map +1 -0
- package/packages/jev-judge/dist/index.js +271 -0
- package/packages/jev-judge/dist/index.js.map +1 -0
- package/packages/jev-judge/dist/types.d.ts +97 -0
- package/packages/jev-judge/dist/types.d.ts.map +1 -0
- package/packages/jev-judge/dist/types.js +8 -0
- package/packages/jev-judge/dist/types.js.map +1 -0
- package/packages/jev-judge/package.json +46 -0
- package/packages/jev-judge/src/decision-engine.ts +231 -0
- package/packages/jev-judge/src/e2e-judge.ts +364 -0
- package/packages/jev-judge/src/flaky-detector.ts +190 -0
- package/packages/jev-judge/src/index.ts +345 -0
- package/packages/jev-judge/src/types.ts +131 -0
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/README.md +71 -0
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/todo-bd-sync/src/extension.ts +16 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/dist/src/approved-patterns.d.ts +51 -0
- package/packages/trajectory/dist/src/approved-patterns.d.ts.map +1 -0
- package/packages/trajectory/dist/src/approved-patterns.js +134 -0
- package/packages/trajectory/dist/src/approved-patterns.js.map +1 -0
- package/packages/trajectory/dist/src/index.d.ts +13 -0
- package/packages/trajectory/dist/src/index.d.ts.map +1 -0
- package/packages/trajectory/dist/src/index.js +12 -0
- package/packages/trajectory/dist/src/index.js.map +1 -0
- package/packages/trajectory/dist/src/store.d.ts +69 -0
- package/packages/trajectory/dist/src/store.d.ts.map +1 -0
- package/packages/trajectory/dist/src/store.js +253 -0
- package/packages/trajectory/dist/src/store.js.map +1 -0
- package/packages/trajectory/dist/src/types.d.ts +73 -0
- package/packages/trajectory/dist/src/types.d.ts.map +1 -0
- package/packages/trajectory/dist/src/types.js +9 -0
- package/packages/trajectory/dist/src/types.js.map +1 -0
- package/packages/trajectory/dist/test/store.test.d.ts +2 -0
- package/packages/trajectory/dist/test/store.test.d.ts.map +1 -0
- package/packages/trajectory/dist/test/store.test.js +228 -0
- package/packages/trajectory/dist/test/store.test.js.map +1 -0
- package/packages/trajectory/package.json +29 -0
- package/packages/trajectory/src/approved-patterns.ts +180 -0
- package/packages/trajectory/src/index.ts +26 -0
- package/packages/trajectory/src/store.ts +285 -0
- package/packages/trajectory/src/types.ts +84 -0
- package/packages/trajectory/test/store.test.d.ts +2 -0
- package/packages/trajectory/test/store.test.ts +250 -0
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/packages/write-review/src/blackboard.ts +24 -1
- package/packages/write-review/src/types.ts +13 -0
- 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 {
|
|
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 {
|
|
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
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
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", "
|
|
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 {
|
|
14
|
-
|
|
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
|
-
//
|
|
109
|
-
const normalized = content
|
|
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
|
|
129
|
-
"Use
|
|
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({
|