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.
- 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.test.ts +0 -1
- 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 +62 -5
- package/harness/langchain/daemon.ts +231 -12
- package/harness/langchain/file-tools.d.ts +97 -0
- package/harness/langchain/file-tools.ts +29 -11
- package/harness/langchain/graph.d.ts +706 -0
- package/harness/langchain/graph.ts +184 -26
- 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 +28 -5
- 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 +38 -5
- 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,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
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
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
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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", "
|
|
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 {
|
|
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
|
|
|
@@ -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({
|
|
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
|
|
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
|
-
//
|
|
105
|
-
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
|
|
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
|
|
125
|
-
"Use
|
|
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
|
|
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(
|
|
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
|
|
188
|
+
path: z
|
|
189
|
+
.string()
|
|
190
|
+
.describe("Absolute path or path relative to the repository root"),
|
|
173
191
|
}),
|
|
174
192
|
},
|
|
175
193
|
);
|