@wrongstack/tools 0.5.5 → 0.5.7
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/dist/bash.d.ts +2 -1
- package/dist/bash.js +361 -3
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +435 -12
- package/dist/builtin.js.map +1 -1
- package/dist/circuit-breaker.d.ts +111 -0
- package/dist/circuit-breaker.js +150 -0
- package/dist/circuit-breaker.js.map +1 -0
- package/dist/exec.js +346 -2
- package/dist/exec.js.map +1 -1
- package/dist/index.d.ts +19 -5
- package/dist/index.js +439 -13
- package/dist/index.js.map +1 -1
- package/dist/pack.js +435 -12
- package/dist/pack.js.map +1 -1
- package/dist/process-registry.d.ts +112 -0
- package/dist/process-registry.js +327 -0
- package/dist/process-registry.js.map +1 -0
- package/package.json +10 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs4 from 'fs/promises';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { dirname } from 'path';
|
|
4
|
-
import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, buildChildEnv, stripAnsi, loadPlan, emptyPlan, clearPlan, savePlan, removePlanItem, setPlanItemStatus,
|
|
4
|
+
import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, buildChildEnv, stripAnsi, loadPlan, emptyPlan, clearPlan, savePlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, formatPlan } from '@wrongstack/core';
|
|
5
5
|
import { spawn } from 'child_process';
|
|
6
6
|
import * as os from 'os';
|
|
7
7
|
import * as dns from 'dns/promises';
|
|
@@ -854,6 +854,326 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
854
854
|
};
|
|
855
855
|
}
|
|
856
856
|
|
|
857
|
+
// src/circuit-breaker.ts
|
|
858
|
+
var DEFAULT_MAX_CONSECUTIVE_FAILURES = 5;
|
|
859
|
+
var DEFAULT_SLOW_CALL_THRESHOLD_MS = 6e4;
|
|
860
|
+
var DEFAULT_MAX_SLOW_CALLS = 3;
|
|
861
|
+
var DEFAULT_WINDOW_MS = 6e4;
|
|
862
|
+
var DEFAULT_MAX_CALLS_PER_WINDOW = 30;
|
|
863
|
+
var DEFAULT_COOLDOWN_MS = 3e4;
|
|
864
|
+
var CircuitBreaker = class {
|
|
865
|
+
maxConsecutiveFailures;
|
|
866
|
+
slowCallThresholdMs;
|
|
867
|
+
maxSlowCalls;
|
|
868
|
+
windowMs;
|
|
869
|
+
maxCallsPerWindow;
|
|
870
|
+
cooldownMs;
|
|
871
|
+
state = "closed";
|
|
872
|
+
consecutiveFailures = 0;
|
|
873
|
+
window = [];
|
|
874
|
+
lastFailureAt = null;
|
|
875
|
+
lastSlowAt = null;
|
|
876
|
+
/** Timestamp when the breaker was opened (for cooldown calculation). */
|
|
877
|
+
openedAt = null;
|
|
878
|
+
/** Timestamp when the last call ran (for half-open gate). */
|
|
879
|
+
lastCallAt = null;
|
|
880
|
+
constructor(config = {}) {
|
|
881
|
+
this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;
|
|
882
|
+
this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;
|
|
883
|
+
this.maxSlowCalls = config.maxSlowCalls ?? DEFAULT_MAX_SLOW_CALLS;
|
|
884
|
+
this.windowMs = config.windowMs ?? DEFAULT_WINDOW_MS;
|
|
885
|
+
this.maxCallsPerWindow = config.maxCallsPerWindow ?? DEFAULT_MAX_CALLS_PER_WINDOW;
|
|
886
|
+
this.cooldownMs = config.cooldownMs ?? DEFAULT_COOLDOWN_MS;
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Returns true if the circuit allows a new call to proceed.
|
|
890
|
+
* When false, callers should abort the tool call and return a
|
|
891
|
+
* circuit-breaker error instead of spawning a process.
|
|
892
|
+
*/
|
|
893
|
+
get canProceed() {
|
|
894
|
+
this._checkStateTransition();
|
|
895
|
+
return this.state !== "open";
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Snapshot of the current breaker state for observability (`/kill`).
|
|
899
|
+
*/
|
|
900
|
+
snapshot() {
|
|
901
|
+
this._checkStateTransition();
|
|
902
|
+
const now = Date.now();
|
|
903
|
+
let cooldownRemaining = null;
|
|
904
|
+
if (this.openedAt !== null && this.state === "open") {
|
|
905
|
+
const elapsed = now - this.openedAt;
|
|
906
|
+
cooldownRemaining = Math.max(0, this.cooldownMs - elapsed);
|
|
907
|
+
}
|
|
908
|
+
return {
|
|
909
|
+
state: this.state,
|
|
910
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
911
|
+
slowCallsInWindow: this.window.filter((c) => c.slow).length,
|
|
912
|
+
callsInWindow: this.window.length,
|
|
913
|
+
windowMs: this.windowMs,
|
|
914
|
+
cooldownRemainingMs: cooldownRemaining,
|
|
915
|
+
lastFailureAt: this.lastFailureAt,
|
|
916
|
+
lastSlowAt: this.lastSlowAt
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Call this BEFORE spawning a bash/exec process.
|
|
921
|
+
* Returns true if the call is allowed; false if the breaker is open.
|
|
922
|
+
* When false, callers MUST NOT spawn a process.
|
|
923
|
+
*/
|
|
924
|
+
beforeCall() {
|
|
925
|
+
this._checkStateTransition();
|
|
926
|
+
if (this.state === "open") return false;
|
|
927
|
+
return true;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Call this AFTER a bash/exec process finishes (success or failure).
|
|
931
|
+
* `durationMs` is the wall-clock time the process ran.
|
|
932
|
+
* `failed` is true when the process returned a non-zero exit code or
|
|
933
|
+
* threw an exception before spawning.
|
|
934
|
+
*/
|
|
935
|
+
afterCall(durationMs, failed) {
|
|
936
|
+
const now = Date.now();
|
|
937
|
+
this.lastCallAt = now;
|
|
938
|
+
if (this.state === "half-open") {
|
|
939
|
+
if (failed) {
|
|
940
|
+
this._trip();
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
this._reset();
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
this._pruneWindow(now);
|
|
947
|
+
const slow = durationMs >= this.slowCallThresholdMs;
|
|
948
|
+
this.window.push({ at: now, failed, slow });
|
|
949
|
+
if (failed) {
|
|
950
|
+
this.consecutiveFailures++;
|
|
951
|
+
this.lastFailureAt = now;
|
|
952
|
+
if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
|
|
953
|
+
this._trip();
|
|
954
|
+
}
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
this.consecutiveFailures = 0;
|
|
958
|
+
if (slow) {
|
|
959
|
+
this.lastSlowAt = now;
|
|
960
|
+
const slowCount = this.window.filter((c) => c.slow).length;
|
|
961
|
+
if (slowCount >= this.maxSlowCalls) {
|
|
962
|
+
this._trip();
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
const callCount = this.window.length;
|
|
966
|
+
if (callCount >= this.maxCallsPerWindow) {
|
|
967
|
+
this._trip();
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
/** Force the breaker open. Used by /kill force and Ctrl+C. */
|
|
971
|
+
forceOpen() {
|
|
972
|
+
this._trip();
|
|
973
|
+
}
|
|
974
|
+
/** Force a reset to closed. Used by tests and /kill reset. */
|
|
975
|
+
forceReset() {
|
|
976
|
+
this._reset();
|
|
977
|
+
}
|
|
978
|
+
_trip() {
|
|
979
|
+
if (this.state === "open") return;
|
|
980
|
+
this.state = "open";
|
|
981
|
+
this.openedAt = Date.now();
|
|
982
|
+
}
|
|
983
|
+
_reset() {
|
|
984
|
+
this.state = "closed";
|
|
985
|
+
this.consecutiveFailures = 0;
|
|
986
|
+
this.window = [];
|
|
987
|
+
this.openedAt = null;
|
|
988
|
+
}
|
|
989
|
+
/** Transition from open → half-open when cooldown elapses. */
|
|
990
|
+
_checkStateTransition() {
|
|
991
|
+
if (this.state !== "open" || this.openedAt === null) return;
|
|
992
|
+
const elapsed = Date.now() - this.openedAt;
|
|
993
|
+
if (elapsed >= this.cooldownMs) {
|
|
994
|
+
this.state = "half-open";
|
|
995
|
+
this.openedAt = null;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
_pruneWindow(now) {
|
|
999
|
+
const cutoff = now - this.windowMs;
|
|
1000
|
+
this.window = this.window.filter((c) => c.at >= cutoff);
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
// src/process-registry.ts
|
|
1005
|
+
var DEFAULT_GRACE_MS = 2e3;
|
|
1006
|
+
var ProcessRegistryImpl = class {
|
|
1007
|
+
processes = /* @__PURE__ */ new Map();
|
|
1008
|
+
breaker;
|
|
1009
|
+
constructor(breakerConfig) {
|
|
1010
|
+
this.breaker = new CircuitBreaker(breakerConfig);
|
|
1011
|
+
}
|
|
1012
|
+
register(info) {
|
|
1013
|
+
this.processes.set(info.pid, { ...info, killed: false });
|
|
1014
|
+
}
|
|
1015
|
+
/** Unregister a process by PID. Called on 'close' / 'exit' events. */
|
|
1016
|
+
unregister(pid) {
|
|
1017
|
+
this.processes.delete(pid);
|
|
1018
|
+
}
|
|
1019
|
+
/** Get a single process by PID. */
|
|
1020
|
+
get(pid) {
|
|
1021
|
+
return this.processes.get(pid);
|
|
1022
|
+
}
|
|
1023
|
+
/** Get all tracked processes. */
|
|
1024
|
+
list() {
|
|
1025
|
+
return Array.from(this.processes.values());
|
|
1026
|
+
}
|
|
1027
|
+
/** Get processes filtered by name (e.g. 'bash', 'exec'). */
|
|
1028
|
+
byName(name) {
|
|
1029
|
+
return this.list().filter((p) => p.name === name);
|
|
1030
|
+
}
|
|
1031
|
+
/** Get processes filtered by session. */
|
|
1032
|
+
bySession(sessionId) {
|
|
1033
|
+
return this.list().filter((p) => p.sessionId === sessionId);
|
|
1034
|
+
}
|
|
1035
|
+
/** Count of active (non-killed) processes. */
|
|
1036
|
+
get activeCount() {
|
|
1037
|
+
let n = 0;
|
|
1038
|
+
for (const p of this.processes.values()) {
|
|
1039
|
+
if (!p.killed) n++;
|
|
1040
|
+
}
|
|
1041
|
+
return n;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Combined stats for observability — used by /ps and the TUI status bar.
|
|
1045
|
+
*/
|
|
1046
|
+
stats() {
|
|
1047
|
+
return {
|
|
1048
|
+
activeCount: this.activeCount,
|
|
1049
|
+
totalCount: this.processes.size,
|
|
1050
|
+
breaker: this.breaker.snapshot()
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Returns true if the circuit allows a new bash/exec call to proceed.
|
|
1055
|
+
* When false, callers MUST NOT spawn a process.
|
|
1056
|
+
*/
|
|
1057
|
+
get canProceed() {
|
|
1058
|
+
return this.breaker.canProceed;
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Called before spawning a process. Returns true if allowed; false if
|
|
1062
|
+
* the circuit breaker is open.
|
|
1063
|
+
*/
|
|
1064
|
+
beforeCall() {
|
|
1065
|
+
return this.breaker.beforeCall();
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Called after a process finishes. `durationMs` is wall-clock time;
|
|
1069
|
+
* `failed` is true for non-zero exit codes.
|
|
1070
|
+
*/
|
|
1071
|
+
afterCall(durationMs, failed) {
|
|
1072
|
+
this.breaker.afterCall(durationMs, failed);
|
|
1073
|
+
}
|
|
1074
|
+
/** Force-open the circuit breaker (Ctrl+C, /kill force). */
|
|
1075
|
+
forceBreakerOpen() {
|
|
1076
|
+
this.breaker.forceOpen();
|
|
1077
|
+
}
|
|
1078
|
+
/** Force-reset the circuit breaker to closed (/kill reset). */
|
|
1079
|
+
forceBreakerReset() {
|
|
1080
|
+
this.breaker.forceReset();
|
|
1081
|
+
}
|
|
1082
|
+
/** Kill a single process by PID.
|
|
1083
|
+
*
|
|
1084
|
+
* On POSIX: sends SIGTERM to the *process group* (-pid) so that
|
|
1085
|
+
* runaway grandchild processes (`sleep 9999 & disown`) are also killed.
|
|
1086
|
+
* After `graceMs` a SIGKILL is sent if the process hasn't exited.
|
|
1087
|
+
*
|
|
1088
|
+
* On Windows: `child.kill()` maps to TerminateProcess — process groups
|
|
1089
|
+
* are not meaningfully supported. A second `force=true` call sends
|
|
1090
|
+
* SIGKILL (which maps to TerminateProcess again — the distinction is
|
|
1091
|
+
* in the exit code, not the signal).
|
|
1092
|
+
*
|
|
1093
|
+
* Returns true if the process was found and kill was attempted.
|
|
1094
|
+
*/
|
|
1095
|
+
kill(pid, opts = {}) {
|
|
1096
|
+
const p = this.processes.get(pid);
|
|
1097
|
+
if (!p) return false;
|
|
1098
|
+
if (p.killed) return true;
|
|
1099
|
+
const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
|
|
1100
|
+
const isWin = os.platform() === "win32";
|
|
1101
|
+
if (isWin) {
|
|
1102
|
+
try {
|
|
1103
|
+
p.child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
1104
|
+
} catch {
|
|
1105
|
+
}
|
|
1106
|
+
p.killed = true;
|
|
1107
|
+
return true;
|
|
1108
|
+
}
|
|
1109
|
+
try {
|
|
1110
|
+
if (force) {
|
|
1111
|
+
try {
|
|
1112
|
+
process.kill(-pid, "SIGKILL");
|
|
1113
|
+
} catch {
|
|
1114
|
+
p.child.kill("SIGKILL");
|
|
1115
|
+
}
|
|
1116
|
+
} else {
|
|
1117
|
+
try {
|
|
1118
|
+
process.kill(-pid, "SIGTERM");
|
|
1119
|
+
} catch {
|
|
1120
|
+
p.child.kill("SIGTERM");
|
|
1121
|
+
}
|
|
1122
|
+
const timer = setTimeout(() => {
|
|
1123
|
+
if (this.processes.has(pid) && !p.child.killed) {
|
|
1124
|
+
try {
|
|
1125
|
+
process.kill(-pid, "SIGKILL");
|
|
1126
|
+
} catch {
|
|
1127
|
+
try {
|
|
1128
|
+
p.child.kill("SIGKILL");
|
|
1129
|
+
} catch {
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}, graceMs);
|
|
1134
|
+
timer.unref?.();
|
|
1135
|
+
}
|
|
1136
|
+
} catch {
|
|
1137
|
+
}
|
|
1138
|
+
p.killed = true;
|
|
1139
|
+
return true;
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Kill all tracked processes.
|
|
1143
|
+
* Returns the PIDs that were kill()ed.
|
|
1144
|
+
*/
|
|
1145
|
+
killAll(opts = {}) {
|
|
1146
|
+
const pids = Array.from(this.processes.keys());
|
|
1147
|
+
const killed = [];
|
|
1148
|
+
for (const pid of pids) {
|
|
1149
|
+
if (this.kill(pid, opts)) killed.push(pid);
|
|
1150
|
+
}
|
|
1151
|
+
return killed;
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Kill all processes for a specific session.
|
|
1155
|
+
* Returns the PIDs that were kill()ed.
|
|
1156
|
+
*/
|
|
1157
|
+
killSession(sessionId, opts = {}) {
|
|
1158
|
+
const pids = this.bySession(sessionId).map((p) => p.pid);
|
|
1159
|
+
const killed = [];
|
|
1160
|
+
for (const pid of pids) {
|
|
1161
|
+
if (this.kill(pid, opts)) killed.push(pid);
|
|
1162
|
+
}
|
|
1163
|
+
return killed;
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
var _registry;
|
|
1167
|
+
function getProcessRegistry() {
|
|
1168
|
+
if (!_registry) {
|
|
1169
|
+
_registry = new ProcessRegistryImpl();
|
|
1170
|
+
}
|
|
1171
|
+
return _registry;
|
|
1172
|
+
}
|
|
1173
|
+
function _resetProcessRegistry() {
|
|
1174
|
+
_registry = void 0;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
857
1177
|
// src/bash.ts
|
|
858
1178
|
var MAX_OUTPUT = 32768;
|
|
859
1179
|
var DEFAULT_TIMEOUT = 3e4;
|
|
@@ -892,12 +1212,27 @@ var bashTool = {
|
|
|
892
1212
|
},
|
|
893
1213
|
async *executeStream(input, ctx, opts) {
|
|
894
1214
|
if (!input?.command) throw new Error("bash: command is required");
|
|
1215
|
+
const registry = getProcessRegistry();
|
|
1216
|
+
if (!registry.beforeCall()) {
|
|
1217
|
+
yield {
|
|
1218
|
+
type: "final",
|
|
1219
|
+
output: {
|
|
1220
|
+
output: "",
|
|
1221
|
+
exit_code: 1,
|
|
1222
|
+
timed_out: false,
|
|
1223
|
+
pid: null,
|
|
1224
|
+
error: "bash: circuit breaker open \u2014 too many consecutive failures or slow calls. Use /kill to inspect or /kill reset to recover."
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
895
1229
|
const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT, 6e5));
|
|
896
1230
|
const isWin = os.platform() === "win32";
|
|
897
1231
|
const shell = isWin ? process.env["COMSPEC"] ?? "cmd.exe" : process.env["SHELL"] ?? "/bin/bash";
|
|
898
1232
|
const args = isWin ? ["/c", input.command] : ["-c", input.command];
|
|
899
1233
|
const env = buildChildEnv(ctx.session?.id);
|
|
900
1234
|
const detached = isWin ? !!input.background : true;
|
|
1235
|
+
const startedAt = Date.now();
|
|
901
1236
|
if (input.background) {
|
|
902
1237
|
let buf2 = "";
|
|
903
1238
|
let truncated = false;
|
|
@@ -908,7 +1243,18 @@ var bashTool = {
|
|
|
908
1243
|
detached: true,
|
|
909
1244
|
signal: opts.signal
|
|
910
1245
|
});
|
|
911
|
-
const
|
|
1246
|
+
const pid2 = child2.pid;
|
|
1247
|
+
if (typeof pid2 === "number") {
|
|
1248
|
+
registry.register({
|
|
1249
|
+
pid: pid2,
|
|
1250
|
+
name: "bash",
|
|
1251
|
+
command: input.command,
|
|
1252
|
+
startedAt: Date.now(),
|
|
1253
|
+
sessionId: ctx.session?.id,
|
|
1254
|
+
child: child2
|
|
1255
|
+
});
|
|
1256
|
+
child2.on("close", () => registry.unregister(pid2));
|
|
1257
|
+
}
|
|
912
1258
|
child2.stdout?.on("data", (chunk) => {
|
|
913
1259
|
if (!truncated) {
|
|
914
1260
|
const remain = MAX_OUTPUT - buf2.length;
|
|
@@ -928,15 +1274,16 @@ var bashTool = {
|
|
|
928
1274
|
}
|
|
929
1275
|
});
|
|
930
1276
|
child2.on("close", () => {
|
|
1277
|
+
registry.afterCall(Date.now() - startedAt, false);
|
|
931
1278
|
});
|
|
932
|
-
if (typeof
|
|
1279
|
+
if (typeof pid2 === "number") child2.unref();
|
|
933
1280
|
yield {
|
|
934
1281
|
type: "final",
|
|
935
1282
|
output: {
|
|
936
1283
|
output: truncated ? buf2.slice(0, MAX_OUTPUT) + "\u2026[truncated]" : buf2,
|
|
937
1284
|
exit_code: null,
|
|
938
1285
|
timed_out: false,
|
|
939
|
-
pid
|
|
1286
|
+
pid: pid2
|
|
940
1287
|
}
|
|
941
1288
|
};
|
|
942
1289
|
return;
|
|
@@ -948,6 +1295,17 @@ var bashTool = {
|
|
|
948
1295
|
detached,
|
|
949
1296
|
signal: opts.signal
|
|
950
1297
|
});
|
|
1298
|
+
const pid = child.pid;
|
|
1299
|
+
if (typeof pid === "number") {
|
|
1300
|
+
registry.register({
|
|
1301
|
+
pid,
|
|
1302
|
+
name: "bash",
|
|
1303
|
+
command: input.command,
|
|
1304
|
+
startedAt: Date.now(),
|
|
1305
|
+
sessionId: ctx.session?.id,
|
|
1306
|
+
child
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
951
1309
|
let buf = "";
|
|
952
1310
|
let pending = "";
|
|
953
1311
|
let timedOut = false;
|
|
@@ -1030,10 +1388,13 @@ var bashTool = {
|
|
|
1030
1388
|
});
|
|
1031
1389
|
child.on("error", (err) => {
|
|
1032
1390
|
for (const t of timers) clearTimeout(t);
|
|
1391
|
+
registry.afterCall(Date.now() - startedAt, true);
|
|
1033
1392
|
push({ kind: "error", err });
|
|
1034
1393
|
});
|
|
1035
1394
|
child.on("close", (code) => {
|
|
1036
1395
|
for (const t of timers) clearTimeout(t);
|
|
1396
|
+
if (typeof pid === "number") registry.unregister(pid);
|
|
1397
|
+
registry.afterCall(Date.now() - startedAt, code !== 0 && code !== null);
|
|
1037
1398
|
push({ kind: "end", code });
|
|
1038
1399
|
});
|
|
1039
1400
|
try {
|
|
@@ -1164,6 +1525,18 @@ var execTool = {
|
|
|
1164
1525
|
required: ["command"]
|
|
1165
1526
|
},
|
|
1166
1527
|
async execute(input, ctx, opts) {
|
|
1528
|
+
const registry = getProcessRegistry();
|
|
1529
|
+
if (!registry.canProceed) {
|
|
1530
|
+
return {
|
|
1531
|
+
command: input.command,
|
|
1532
|
+
args: input.args ?? [],
|
|
1533
|
+
stdout: "",
|
|
1534
|
+
stderr: "Circuit breaker is open \u2014 too many consecutive failures. Use /kill reset to recover.",
|
|
1535
|
+
exitCode: 1,
|
|
1536
|
+
truncated: false,
|
|
1537
|
+
allowed: false
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1167
1540
|
const cmd = input.command.trim();
|
|
1168
1541
|
if (!cmd)
|
|
1169
1542
|
return {
|
|
@@ -1223,15 +1596,23 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1223
1596
|
let stdout = "";
|
|
1224
1597
|
let stderr = "";
|
|
1225
1598
|
let killed = false;
|
|
1599
|
+
const startedAt = Date.now();
|
|
1226
1600
|
const child = spawn(cmd, args, {
|
|
1227
1601
|
cwd,
|
|
1228
1602
|
signal,
|
|
1229
1603
|
env: buildChildEnv(sessionId),
|
|
1230
1604
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1231
1605
|
});
|
|
1606
|
+
const registry = getProcessRegistry();
|
|
1607
|
+
const pid = child.pid;
|
|
1608
|
+
if (typeof pid === "number") {
|
|
1609
|
+
const fullCommand = `${cmd} ${args.join(" ")}`;
|
|
1610
|
+
registry.register({ pid, name: "exec", command: fullCommand, startedAt: Date.now(), sessionId, child });
|
|
1611
|
+
}
|
|
1232
1612
|
const timer = setTimeout(() => {
|
|
1233
1613
|
killed = true;
|
|
1234
|
-
|
|
1614
|
+
if (typeof pid === "number") registry.kill(pid);
|
|
1615
|
+
else child.kill("SIGTERM");
|
|
1235
1616
|
}, timeout);
|
|
1236
1617
|
child.stdout?.on("data", (chunk) => {
|
|
1237
1618
|
if (stdout.length < MAX_OUTPUT2) stdout += chunk.toString();
|
|
@@ -1241,18 +1622,24 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1241
1622
|
});
|
|
1242
1623
|
child.on("close", (code) => {
|
|
1243
1624
|
clearTimeout(timer);
|
|
1625
|
+
if (typeof pid === "number") registry.unregister(pid);
|
|
1626
|
+
const durationMs = Date.now() - startedAt;
|
|
1627
|
+
const exitCode = killed ? 124 : code ?? 1;
|
|
1628
|
+
registry.afterCall(durationMs, exitCode !== 0);
|
|
1244
1629
|
resolve5({
|
|
1245
1630
|
command: cmd,
|
|
1246
1631
|
args,
|
|
1247
1632
|
stdout: stdout.slice(0, MAX_OUTPUT2),
|
|
1248
1633
|
stderr: stderr.slice(0, MAX_OUTPUT2),
|
|
1249
|
-
exitCode
|
|
1634
|
+
exitCode,
|
|
1250
1635
|
truncated: stdout.length >= MAX_OUTPUT2 || stderr.length >= MAX_OUTPUT2,
|
|
1251
1636
|
allowed: true
|
|
1252
1637
|
});
|
|
1253
1638
|
});
|
|
1254
1639
|
child.on("error", (err) => {
|
|
1255
1640
|
clearTimeout(timer);
|
|
1641
|
+
if (typeof pid === "number") registry.unregister(pid);
|
|
1642
|
+
registry.afterCall(Date.now() - startedAt, true);
|
|
1256
1643
|
resolve5({
|
|
1257
1644
|
command: cmd,
|
|
1258
1645
|
args,
|
|
@@ -1821,8 +2208,8 @@ var todoTool = {
|
|
|
1821
2208
|
var planTool = {
|
|
1822
2209
|
name: "plan",
|
|
1823
2210
|
category: "Session",
|
|
1824
|
-
description: "Inspect or edit the strategic plan board for this session. Plans persist across resume (unlike todos). Use this to lay out the multi-step approach before diving in, then mark steps in_progress/done as the work proceeds.",
|
|
1825
|
-
usageHint:
|
|
2211
|
+
description: "Inspect or edit the strategic plan board for this session. Plans persist across resume (unlike todos). Use this to lay out the multi-step approach before diving in, then mark steps in_progress/done as the work proceeds. Promote a plan item to todos to start working on it. Apply templates for common workflows.",
|
|
2212
|
+
usageHint: 'Set action to one of: show | add | start | done | remove | promote | derive | template_use | clear. Pass `title` for add. Pass `target` (item id, 1-based index, or title substring) for start/done/remove/promote/derive. Pass `subtasks` for promote/derive to break the plan item into multiple todos. Pass `template` (e.g. "new-feature", "bug-fix", "refactor", "release") for template_use. Always returns the formatted plan plus open/total counts.',
|
|
1826
2213
|
permission: "auto",
|
|
1827
2214
|
mutating: false,
|
|
1828
2215
|
timeoutMs: 2e3,
|
|
@@ -1831,13 +2218,22 @@ var planTool = {
|
|
|
1831
2218
|
properties: {
|
|
1832
2219
|
action: {
|
|
1833
2220
|
type: "string",
|
|
1834
|
-
enum: ["show", "add", "start", "done", "remove", "clear"]
|
|
2221
|
+
enum: ["show", "add", "start", "done", "remove", "promote", "derive", "template_use", "clear"]
|
|
1835
2222
|
},
|
|
1836
2223
|
title: { type: "string", description: "Required when action = add." },
|
|
1837
2224
|
details: { type: "string", description: "Optional extra context for add." },
|
|
1838
2225
|
target: {
|
|
1839
2226
|
type: "string",
|
|
1840
|
-
description: "Plan item id, 1-based index, or title substring. Required for start/done/remove."
|
|
2227
|
+
description: "Plan item id, 1-based index, or title substring. Required for start/done/remove/promote/derive."
|
|
2228
|
+
},
|
|
2229
|
+
subtasks: {
|
|
2230
|
+
type: "array",
|
|
2231
|
+
items: { type: "string" },
|
|
2232
|
+
description: "Optional subtasks for promote/derive. If omitted, a single todo is created from the plan item title."
|
|
2233
|
+
},
|
|
2234
|
+
template: {
|
|
2235
|
+
type: "string",
|
|
2236
|
+
description: "Template name for template_use action. Available: new-feature, bug-fix, refactor, release, security-audit, onboarding."
|
|
1841
2237
|
}
|
|
1842
2238
|
},
|
|
1843
2239
|
required: ["action"]
|
|
@@ -1896,6 +2292,35 @@ var planTool = {
|
|
|
1896
2292
|
await savePlan(planPath, plan);
|
|
1897
2293
|
break;
|
|
1898
2294
|
}
|
|
2295
|
+
case "promote":
|
|
2296
|
+
case "derive": {
|
|
2297
|
+
if (!input.target) {
|
|
2298
|
+
return mkResult(plan, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
2299
|
+
}
|
|
2300
|
+
const derived = deriveTodosFromPlanItem(plan, input.target, input.subtasks);
|
|
2301
|
+
if (!derived) {
|
|
2302
|
+
return mkResult(plan, false, `No plan item matched "${input.target}".`);
|
|
2303
|
+
}
|
|
2304
|
+
plan = derived.plan;
|
|
2305
|
+
await savePlan(planPath, plan);
|
|
2306
|
+
ctx.state.replaceTodos(derived.todos);
|
|
2307
|
+
return mkResult(plan, true, `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`, derived.todos);
|
|
2308
|
+
}
|
|
2309
|
+
case "template_use": {
|
|
2310
|
+
const templateName = input.template?.trim();
|
|
2311
|
+
if (!templateName) {
|
|
2312
|
+
return mkResult(plan, false, "template_use requires `template` name.");
|
|
2313
|
+
}
|
|
2314
|
+
const template = getPlanTemplate(templateName);
|
|
2315
|
+
if (!template) {
|
|
2316
|
+
return mkResult(plan, false, `Unknown template "${templateName}".`);
|
|
2317
|
+
}
|
|
2318
|
+
for (const item of template.items) {
|
|
2319
|
+
({ plan } = addPlanItem(plan, item.title, item.details));
|
|
2320
|
+
}
|
|
2321
|
+
await savePlan(planPath, plan);
|
|
2322
|
+
return mkResult(plan, true, `Applied template "${template.name}" \u2014 ${template.items.length} items added.`);
|
|
2323
|
+
}
|
|
1899
2324
|
case "clear":
|
|
1900
2325
|
plan = clearPlan(plan);
|
|
1901
2326
|
await savePlan(planPath, plan);
|
|
@@ -1906,14 +2331,15 @@ var planTool = {
|
|
|
1906
2331
|
return mkResult(plan, true, `Plan ${input.action} ok.`);
|
|
1907
2332
|
}
|
|
1908
2333
|
};
|
|
1909
|
-
function mkResult(plan, ok, message) {
|
|
2334
|
+
function mkResult(plan, ok, message, todos) {
|
|
1910
2335
|
const open = plan.items.filter((i) => i.status !== "done").length;
|
|
1911
2336
|
return {
|
|
1912
2337
|
ok,
|
|
1913
2338
|
message,
|
|
1914
2339
|
plan: formatPlan(plan),
|
|
1915
2340
|
count: plan.items.length,
|
|
1916
|
-
open
|
|
2341
|
+
open,
|
|
2342
|
+
todos
|
|
1917
2343
|
};
|
|
1918
2344
|
}
|
|
1919
2345
|
var TIMEOUT_MS4 = 3e4;
|
|
@@ -4486,6 +4912,6 @@ var builtinToolsPack = {
|
|
|
4486
4912
|
tools: builtinTools
|
|
4487
4913
|
};
|
|
4488
4914
|
|
|
4489
|
-
export { auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, createModeTool, diffTool, documentTool, editTool, execTool, fetchTool, forgetTool, formatTool, gitTool, globTool, grepTool, installTool, jsonTool, lintTool, logsTool, outdatedTool, patchTool, planTool, readTool, rememberTool, replaceTool, scaffoldTool, searchTool, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
|
|
4915
|
+
export { CircuitBreaker, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, createModeTool, diffTool, documentTool, editTool, execTool, fetchTool, forgetTool, formatTool, getProcessRegistry, gitTool, globTool, grepTool, installTool, jsonTool, lintTool, logsTool, outdatedTool, patchTool, planTool, readTool, rememberTool, replaceTool, scaffoldTool, searchTool, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
|
|
4490
4916
|
//# sourceMappingURL=index.js.map
|
|
4491
4917
|
//# sourceMappingURL=index.js.map
|