pi-harness-runtime 1.1.458 → 1.1.460
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 +73 -0
- package/README.md +10 -2
- package/harness/honcho-init.ts +907 -192
- package/harness/honcho-memory.ts +31 -69
- package/harness/key-file-audit.ts +79 -1
- package/harness/pending-countdown.ts +348 -0
- package/harness/service-commands.ts +48 -16
- package/index.ts +460 -76
- package/package.json +3 -8
- package/packages/agent-policy/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/evaluation-engine/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/file-copy-helper/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/tooling-preference/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/two-phase-review/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/write-review/package.json +1 -1
package/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
formatKeyFileFixes,
|
|
27
27
|
formatKeyPermissionFixes,
|
|
28
28
|
selectKeyFileFixes,
|
|
29
|
+
secureKeyTree,
|
|
29
30
|
type KeyFileAuditResult,
|
|
30
31
|
} from "./harness/key-file-audit.ts";
|
|
31
32
|
import { renderDetailLines } from "./harness/startup-lines.ts";
|
|
@@ -41,7 +42,17 @@ import {
|
|
|
41
42
|
relayShouldBeReported,
|
|
42
43
|
} from "./harness/startup-report.ts";
|
|
43
44
|
import { localSkillsReport, skillsReport } from "./harness/skills-startup.ts";
|
|
45
|
+
import {
|
|
46
|
+
PendingQuestionCountdown,
|
|
47
|
+
type CountdownView,
|
|
48
|
+
} from "./harness/pending-countdown.ts";
|
|
44
49
|
import { isChannelDisabled, setChannelDisabled } from "./harness/notification-settings.ts";
|
|
50
|
+
// The Honcho contract lives with the Honcho integration. A type-only import
|
|
51
|
+
// is erased at runtime, so the startup keeps loading the module lazily.
|
|
52
|
+
import type {
|
|
53
|
+
HonchoInitResult,
|
|
54
|
+
HonchoMcpRegistrar,
|
|
55
|
+
} from "./harness/honcho-init.ts";
|
|
45
56
|
import { UsageTracker } from "./tracker.ts";
|
|
46
57
|
import { MirrorStore, type MirrorRecord } from "./mirror.ts";
|
|
47
58
|
import { MiniMaxQuotaScraper } from "./harness/e2e/minimax-quota-scraper.js";
|
|
@@ -211,17 +222,44 @@ function getKeyFileAudit(): KeyFileAuditResult | null {
|
|
|
211
222
|
* is a list nobody runs. The command is never executed here.
|
|
212
223
|
*/
|
|
213
224
|
function reportExposedKeyFiles(): void {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
225
|
+
let audit = getKeyFileAudit();
|
|
226
|
+
let exposed = audit?.exposed ?? [];
|
|
227
|
+
let exposedDirectories = audit?.exposedDirectories ?? [];
|
|
217
228
|
if (exposed.length === 0 && exposedDirectories.length === 0) return;
|
|
229
|
+
|
|
230
|
+
// Key files belong to this app and are private by design. Repair unsafe
|
|
231
|
+
// Unix permissions automatically; keep the actionable warning only when
|
|
232
|
+
// the platform cannot express the repair or a path cannot be changed.
|
|
233
|
+
const repair = secureKeyTree(harnessKeysDir());
|
|
234
|
+
if (repair.supported) {
|
|
235
|
+
try {
|
|
236
|
+
audit = auditKeyFiles({ keysDir: harnessKeysDir() });
|
|
237
|
+
keyFileAudit = audit;
|
|
238
|
+
} catch {
|
|
239
|
+
// Preserve the original findings if the verification scan fails.
|
|
240
|
+
}
|
|
241
|
+
exposed = audit?.exposed ?? exposed;
|
|
242
|
+
exposedDirectories = audit?.exposedDirectories ?? exposedDirectories;
|
|
243
|
+
if (exposed.length === 0 && exposedDirectories.length === 0) {
|
|
244
|
+
addHarnessMessage(
|
|
245
|
+
"Keys",
|
|
246
|
+
`permissions secured automatically (${repair.securedFiles} files, ${repair.securedDirectories} directories)`,
|
|
247
|
+
"ok",
|
|
248
|
+
);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
218
253
|
const report = exposedKeyFilesReport({
|
|
219
254
|
exposed,
|
|
220
255
|
exposedDirectories,
|
|
221
256
|
fixes: formatKeyPermissionFixes({ keysDir: harnessKeysDir() }),
|
|
222
257
|
});
|
|
223
258
|
if (!report) return;
|
|
224
|
-
|
|
259
|
+
const failedPaths = repair.failedPaths.length > 0
|
|
260
|
+
? [`Could not change permissions for: ${repair.failedPaths.join(", ")}`]
|
|
261
|
+
: [];
|
|
262
|
+
addHarnessMessage(report.label, report.text, report.status, [...failedPaths, ...report.details]);
|
|
225
263
|
}
|
|
226
264
|
|
|
227
265
|
/**
|
|
@@ -258,6 +296,9 @@ const sharedState = {
|
|
|
258
296
|
validationLedger: null as unknown,
|
|
259
297
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
260
298
|
updateAutoContinueStatus: null as any,
|
|
299
|
+
// The live countdown of the pending question. The channels stop it when
|
|
300
|
+
// a human answer arrives.
|
|
301
|
+
pendingCountdown: null as PendingQuestionCountdown | null,
|
|
261
302
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
262
303
|
pqController: null as any,
|
|
263
304
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -301,6 +342,8 @@ function applyChannelApproval(
|
|
|
301
342
|
// cleared after a successful resolution.
|
|
302
343
|
sharedState.waitingForUserSince = null;
|
|
303
344
|
sharedState.updateAutoContinueStatus?.("IDLE");
|
|
345
|
+
// The countdown of the answered question is over with it.
|
|
346
|
+
sharedState.pendingCountdown?.stop();
|
|
304
347
|
|
|
305
348
|
if (result.resolution === "yes") {
|
|
306
349
|
(sharedState.todoContinuationController as any)?.resume?.();
|
|
@@ -898,6 +941,12 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
898
941
|
let lastUserMessage: Date | null = null;
|
|
899
942
|
let autoContinueTimer: NodeJS.Timeout | null = null;
|
|
900
943
|
let _inferenceInProgress = false; // Track for overlap prevention
|
|
944
|
+
// The timing of the wait. The countdown in the status row and the
|
|
945
|
+
// decision of the judge use the same constants, so the displayed
|
|
946
|
+
// deadline and the decision can never drift apart.
|
|
947
|
+
const AUTO_CONTINUE_INTERVAL_MS = 60 * 1000; // Fallback poll: every minute
|
|
948
|
+
const MIN_WAIT_MINUTES = 5; // Minimum wait before auto-continue
|
|
949
|
+
const MAX_WAIT_MINUTES = 30; // After this the work pauses instead of waiting
|
|
901
950
|
// A continuation was dispatched and its turn has not settled yet.
|
|
902
951
|
// The gate owns that state: the status stays "RUN" until the turn
|
|
903
952
|
// settles, and no timer may clear it in the meantime.
|
|
@@ -989,6 +1038,15 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
989
1038
|
}
|
|
990
1039
|
});
|
|
991
1040
|
let _sessionId = Date.now().toString(36); // Track session for cancellation
|
|
1041
|
+
// The session of the Pi runtime. A countdown belongs to the session
|
|
1042
|
+
// that asked the question.
|
|
1043
|
+
let _piSessionId = "";
|
|
1044
|
+
// The session the running countdown belongs to. A countdown of a
|
|
1045
|
+
// session that is gone must not draw its status any more.
|
|
1046
|
+
let _countdownSessionId = "";
|
|
1047
|
+
// The request the row of the footer belongs to. A countdown that
|
|
1048
|
+
// stops must not clear the row of a newer question.
|
|
1049
|
+
let _countdownRowRequestId: string | null = null;
|
|
992
1050
|
let _lastNotifiedQuestion = ""; // Track last question to prevent repeat notifications
|
|
993
1051
|
let _completedNotifiedFor: string | null = null; // Track completion notification deduplication
|
|
994
1052
|
|
|
@@ -1014,36 +1072,164 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1014
1072
|
ERROR: "Auto-continue unavailable",
|
|
1015
1073
|
} as const;
|
|
1016
1074
|
|
|
1075
|
+
// The live countdown of the pending question.
|
|
1076
|
+
//
|
|
1077
|
+
// It owns the deadline of the wait: it shows the remaining time in
|
|
1078
|
+
// the status row once per second and starts the decision of the
|
|
1079
|
+
// judge exactly at the deadline. The fallback poll only takes over
|
|
1080
|
+
// when there is no countdown, so a decision is never started twice.
|
|
1081
|
+
const pendingCountdown = new PendingQuestionCountdown({
|
|
1082
|
+
// The two policies of the wait belong to the countdown, so the
|
|
1083
|
+
// displayed deadline and the deadline of the judge cannot drift
|
|
1084
|
+
// apart.
|
|
1085
|
+
waitMinutes: MIN_WAIT_MINUTES,
|
|
1086
|
+
recheckIntervalMs: AUTO_CONTINUE_INTERVAL_MS,
|
|
1087
|
+
render: (view: CountdownView) => {
|
|
1088
|
+
_countdownRowRequestId = view.requestId;
|
|
1089
|
+
updateAutoContinueStatus(
|
|
1090
|
+
"WAITING",
|
|
1091
|
+
view.requestId.slice(0, 8),
|
|
1092
|
+
view.mode === "recheck"
|
|
1093
|
+
? `Jev will recheck in ${view.remaining}`
|
|
1094
|
+
: `Jev decision in ${view.remaining}`,
|
|
1095
|
+
);
|
|
1096
|
+
},
|
|
1097
|
+
onDeadline: (requestId: string) => {
|
|
1098
|
+
logToFile("[auto-continue]", "The decision deadline was reached:", requestId);
|
|
1099
|
+
checkAndAutoContinue("countdown")
|
|
1100
|
+
.then(() => {
|
|
1101
|
+
// The decision did not start - another one was still in
|
|
1102
|
+
// flight, or the wait was gone before it ran. The
|
|
1103
|
+
// deadline is not lost: the countdown is armed again
|
|
1104
|
+
// and the decision follows.
|
|
1105
|
+
if (pendingCountdown.fired && pendingCountdown.requestId === requestId) {
|
|
1106
|
+
pendingCountdown.startRecheck(requestId);
|
|
1107
|
+
}
|
|
1108
|
+
})
|
|
1109
|
+
.catch((error) => {
|
|
1110
|
+
logToFile("[auto-continue]", "The decision failed:", oneLineError(error));
|
|
1111
|
+
// The decision did not run: the countdown releases the
|
|
1112
|
+
// wait, so the fallback poll can take over again.
|
|
1113
|
+
pendingCountdown.stop();
|
|
1114
|
+
});
|
|
1115
|
+
},
|
|
1116
|
+
isPending: (requestId: string) => {
|
|
1117
|
+
// The wait must still belong to the request: a late tick of
|
|
1118
|
+
// an answered or replaced question must not draw its status
|
|
1119
|
+
// and must not trigger a decision.
|
|
1120
|
+
if (!waitingForUserSince) return false;
|
|
1121
|
+
if (_countdownSessionId !== _piSessionId) return false;
|
|
1122
|
+
const current = sharedState.pqController?.getCurrentQuestion?.();
|
|
1123
|
+
if (!current) return false;
|
|
1124
|
+
return (
|
|
1125
|
+
current.requestId === requestId &&
|
|
1126
|
+
(current.state === "pending" || current.state === "evaluating")
|
|
1127
|
+
);
|
|
1128
|
+
},
|
|
1129
|
+
onStop: (requestId: string | null) => {
|
|
1130
|
+
// The countdown is over: the row is cleared with it, so no
|
|
1131
|
+
// row claims a decision that is not due any more. Only the row
|
|
1132
|
+
// of that countdown is cleared: a row of another state - the
|
|
1133
|
+
// judge continues, for example - belongs to that state, and
|
|
1134
|
+
// the row of a newer question belongs to that question.
|
|
1135
|
+
if (_countdownRowRequestId && requestId && _countdownRowRequestId !== requestId) {
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
_countdownRowRequestId = null;
|
|
1139
|
+
if (_currentAutoContinueState.startsWith("[WAIT]")) {
|
|
1140
|
+
_currentAutoContinueState = "";
|
|
1141
|
+
_setAutoContinueStatus?.(undefined);
|
|
1142
|
+
}
|
|
1143
|
+
},
|
|
1144
|
+
onError: (error: unknown) => {
|
|
1145
|
+
logToFile("[auto-continue]", "The countdown failed:", oneLineError(error));
|
|
1146
|
+
},
|
|
1147
|
+
});
|
|
1148
|
+
sharedState.pendingCountdown = pendingCountdown;
|
|
1149
|
+
|
|
1017
1150
|
// Wire shared state for callback handlers
|
|
1018
1151
|
sharedState.waitingForUserSince = waitingForUserSince;
|
|
1019
1152
|
sharedState.updateAutoContinueStatus = updateAutoContinueStatus;
|
|
1020
1153
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1154
|
+
/**
|
|
1155
|
+
* Start the countdown of a pending question.
|
|
1156
|
+
*
|
|
1157
|
+
* The deadline of the decision is derived from the wait that is
|
|
1158
|
+
* already running, so the displayed deadline and the decision of
|
|
1159
|
+
* the judge can never drift apart.
|
|
1160
|
+
*/
|
|
1161
|
+
function startPendingCountdown(requestId: string): void {
|
|
1162
|
+
_countdownSessionId = _piSessionId;
|
|
1163
|
+
const startedAt = waitingForUserSince?.getTime() ?? Date.now();
|
|
1164
|
+
pendingCountdown.start(requestId, startedAt);
|
|
1165
|
+
}
|
|
1024
1166
|
|
|
1167
|
+
/**
|
|
1168
|
+
* Start the countdown of the next attempt.
|
|
1169
|
+
*
|
|
1170
|
+
* The judge asked to wait longer: the countdown counts down to the
|
|
1171
|
+
* next attempt instead of to the deadline of the decision.
|
|
1172
|
+
*/
|
|
1173
|
+
function startRecheckCountdown(requestId: string): void {
|
|
1174
|
+
_countdownSessionId = _piSessionId;
|
|
1175
|
+
pendingCountdown.startRecheck(requestId);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Update auto-continue status in TUI footer
|
|
1179
|
+
function updateAutoContinueStatus(
|
|
1180
|
+
state: keyof typeof AC_STATE_MESSAGES,
|
|
1181
|
+
requestId?: string,
|
|
1182
|
+
detail?: string,
|
|
1183
|
+
): void {
|
|
1025
1184
|
const message = AC_STATE_MESSAGES[state];
|
|
1026
1185
|
if (state === "IDLE") {
|
|
1027
|
-
//
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
// Only update if state changed (prevent flicker)
|
|
1036
|
-
if (statusText !== _currentAutoContinueState) {
|
|
1037
|
-
_setAutoContinueStatus(statusText);
|
|
1038
|
-
_currentAutoContinueState = statusText;
|
|
1186
|
+
// Only the row is cleared here: the countdown belongs to the
|
|
1187
|
+
// wait, not to the row. A settled continuation must not stop
|
|
1188
|
+
// the countdown of a question that the agent asked in the
|
|
1189
|
+
// same turn, and it must not blank its row either.
|
|
1190
|
+
if (!pendingCountdown.active) {
|
|
1191
|
+
_currentAutoContinueState = "";
|
|
1192
|
+
_setAutoContinueStatus?.(undefined);
|
|
1039
1193
|
}
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const parts = [`[${AC_STATE[state]}] ${message}`];
|
|
1198
|
+
if (requestId) parts.push(requestId);
|
|
1199
|
+
if (detail) parts.push(detail);
|
|
1200
|
+
const statusText = parts.join(" | ");
|
|
1201
|
+
|
|
1202
|
+
// Only update if state changed (prevent flicker). The state is
|
|
1203
|
+
// kept even without a UI binding: a session that starts without
|
|
1204
|
+
// a TUI shows the current state as soon as the UI is there.
|
|
1205
|
+
if (statusText !== _currentAutoContinueState) {
|
|
1206
|
+
_currentAutoContinueState = statusText;
|
|
1207
|
+
_setAutoContinueStatus?.(statusText);
|
|
1040
1208
|
}
|
|
1041
1209
|
}
|
|
1042
1210
|
|
|
1043
1211
|
// Capture UI context from session_start
|
|
1044
1212
|
pi.on("session_start", (_event, ctx) => {
|
|
1213
|
+
// The session of the runtime: the countdown of a question belongs
|
|
1214
|
+
// to the session that asked it.
|
|
1215
|
+
const sessionId = (
|
|
1216
|
+
ctx as { sessionManager?: { getSessionId?: () => string | undefined } }
|
|
1217
|
+
).sessionManager?.getSessionId?.();
|
|
1218
|
+
if (typeof sessionId === "string" && sessionId) {
|
|
1219
|
+
if (_piSessionId && _piSessionId !== sessionId) {
|
|
1220
|
+
// The session changed: the countdown of the old session
|
|
1221
|
+
// is over with it.
|
|
1222
|
+
pendingCountdown.stop();
|
|
1223
|
+
}
|
|
1224
|
+
_piSessionId = sessionId;
|
|
1225
|
+
}
|
|
1045
1226
|
if (ctx.hasUI && ctx.ui) {
|
|
1046
1227
|
_setAutoContinueStatus = ctx.ui.setStatus.bind(ctx.ui, "harness-auto-continue");
|
|
1228
|
+
// A session without a TUI collected the state already: it is
|
|
1229
|
+
// shown as soon as the UI is there.
|
|
1230
|
+
if (_currentAutoContinueState) {
|
|
1231
|
+
_setAutoContinueStatus(_currentAutoContinueState);
|
|
1232
|
+
}
|
|
1047
1233
|
}
|
|
1048
1234
|
|
|
1049
1235
|
});
|
|
@@ -1056,6 +1242,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1056
1242
|
autoContinueTimer = null;
|
|
1057
1243
|
}
|
|
1058
1244
|
|
|
1245
|
+
// The countdown of the pending question is over with the session.
|
|
1246
|
+
pendingCountdown.stop();
|
|
1247
|
+
|
|
1059
1248
|
// Clear UI binding and status
|
|
1060
1249
|
if (_setAutoContinueStatus) {
|
|
1061
1250
|
_setAutoContinueStatus(undefined);
|
|
@@ -1114,6 +1303,8 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1114
1303
|
waitingForUserSince = null;
|
|
1115
1304
|
sharedState.waitingForUserSince = null;
|
|
1116
1305
|
_lastNotifiedQuestion = "";
|
|
1306
|
+
// The question is answered: the countdown of the wait is over.
|
|
1307
|
+
pendingCountdown.stop();
|
|
1117
1308
|
updateAutoContinueStatus("IDLE");
|
|
1118
1309
|
// Dispatch answer to Pi
|
|
1119
1310
|
const message = answer === "yes" ? "yes" : "not yet";
|
|
@@ -1190,8 +1381,10 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1190
1381
|
// Use question.requestId as the jobId for callback matching
|
|
1191
1382
|
const questionJobId = question.requestId;
|
|
1192
1383
|
|
|
1193
|
-
//
|
|
1194
|
-
|
|
1384
|
+
// The status row counts down to the decision of the judge.
|
|
1385
|
+
// The countdown is bound to this request, so a newer
|
|
1386
|
+
// question replaces it.
|
|
1387
|
+
startPendingCountdown(questionJobId);
|
|
1195
1388
|
|
|
1196
1389
|
// Get task context for additional info
|
|
1197
1390
|
const taskContext = await getTaskContext().catch(() => ({ total: 0, completed: 0, remaining: [] as string[] }));
|
|
@@ -1354,6 +1547,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1354
1547
|
if (waitingForUserSince) {
|
|
1355
1548
|
waitingForUserSince = null;
|
|
1356
1549
|
_lastNotifiedQuestion = "";
|
|
1550
|
+
// The user answered in Pi: the countdown of the wait is
|
|
1551
|
+
// over with it.
|
|
1552
|
+
pendingCountdown.stop();
|
|
1357
1553
|
updateAutoContinueStatus("IDLE");
|
|
1358
1554
|
// Cancel pending question on user response
|
|
1359
1555
|
// (User answered via Pi instead of button)
|
|
@@ -1363,18 +1559,22 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1363
1559
|
});
|
|
1364
1560
|
|
|
1365
1561
|
// Periodic check for auto-continue decision
|
|
1366
|
-
const AUTO_CONTINUE_INTERVAL_MS = 60 * 1000; // Check every 1 minute
|
|
1367
|
-
const MIN_WAIT_MINUTES = 5; // Minimum wait before auto-continue
|
|
1368
|
-
const MAX_WAIT_MINUTES = 30; // After this the work pauses instead of waiting
|
|
1369
1562
|
|
|
1370
|
-
const checkAndAutoContinue = async () => {
|
|
1563
|
+
const checkAndAutoContinue = async (source: "countdown" | "poll" = "poll") => {
|
|
1371
1564
|
// Prevent overlap if inference is already in progress
|
|
1372
1565
|
if (_inferenceInProgress) {
|
|
1566
|
+
// A deadline that was blocked is armed again by the caller of
|
|
1567
|
+
// the countdown, so the decision is not lost.
|
|
1373
1568
|
return;
|
|
1374
1569
|
}
|
|
1375
1570
|
|
|
1376
1571
|
if (!waitingForUserSince) return;
|
|
1377
1572
|
|
|
1573
|
+
// The countdown owns the wait while it runs: the fallback poll
|
|
1574
|
+
// only takes over when there is no countdown, so the decision is
|
|
1575
|
+
// never started twice.
|
|
1576
|
+
if (source === "poll" && pendingCountdown.active) return;
|
|
1577
|
+
|
|
1378
1578
|
// Capture session ID at check time for post-inference validation
|
|
1379
1579
|
const checkSessionId = _sessionId;
|
|
1380
1580
|
// Use same hash algorithm as detection
|
|
@@ -1436,6 +1636,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1436
1636
|
if (taskContext.total === 0 && !hasPendingQuestion) {
|
|
1437
1637
|
logToFile("[auto-continue]", "No tasks and no pending questions, skipping auto-continue check");
|
|
1438
1638
|
_inferenceInProgress = false;
|
|
1639
|
+
// The countdown is over: the fallback poll takes over the
|
|
1640
|
+
// next attempt.
|
|
1641
|
+
pendingCountdown.stop();
|
|
1439
1642
|
return;
|
|
1440
1643
|
}
|
|
1441
1644
|
|
|
@@ -1590,15 +1793,20 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1590
1793
|
if (waitBelongsToRequest(questionContext?.requestId)) {
|
|
1591
1794
|
waitingForUserSince = null;
|
|
1592
1795
|
sharedState.waitingForUserSince = null;
|
|
1796
|
+
// The request expired: the countdown of the wait
|
|
1797
|
+
// is over with it.
|
|
1798
|
+
pendingCountdown.stop();
|
|
1593
1799
|
updateAutoContinueStatus("IDLE");
|
|
1594
1800
|
}
|
|
1595
1801
|
} else {
|
|
1596
|
-
// The request stays open: the
|
|
1597
|
-
//
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
questionContext
|
|
1601
|
-
|
|
1802
|
+
// The request stays open: the countdown counts down to
|
|
1803
|
+
// the next check, so the retry does not wait for the
|
|
1804
|
+
// fallback poll.
|
|
1805
|
+
if (questionContext?.requestId) {
|
|
1806
|
+
startRecheckCountdown(questionContext.requestId);
|
|
1807
|
+
} else {
|
|
1808
|
+
updateAutoContinueStatus("WAITING");
|
|
1809
|
+
}
|
|
1602
1810
|
}
|
|
1603
1811
|
} else {
|
|
1604
1812
|
// Close the question with the Jev decision. The buttons of
|
|
@@ -1635,6 +1843,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1635
1843
|
waitingForUserSince = null;
|
|
1636
1844
|
sharedState.waitingForUserSince = null;
|
|
1637
1845
|
_lastNotifiedQuestion = "";
|
|
1846
|
+
// The stale decision is discarded: the countdown
|
|
1847
|
+
// of the wait is over with it.
|
|
1848
|
+
pendingCountdown.stop();
|
|
1638
1849
|
updateAutoContinueStatus("IDLE");
|
|
1639
1850
|
} else {
|
|
1640
1851
|
logToFile(
|
|
@@ -1658,6 +1869,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1658
1869
|
waitingForUserSince = null;
|
|
1659
1870
|
sharedState.waitingForUserSince = null;
|
|
1660
1871
|
_lastNotifiedQuestion = "";
|
|
1872
|
+
// The work pauses for a human: the countdown of
|
|
1873
|
+
// the wait is over with it.
|
|
1874
|
+
pendingCountdown.stop();
|
|
1661
1875
|
updateAutoContinueStatus("IDLE");
|
|
1662
1876
|
}
|
|
1663
1877
|
return;
|
|
@@ -1691,6 +1905,8 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1691
1905
|
waitingForUserSince = null;
|
|
1692
1906
|
sharedState.waitingForUserSince = null;
|
|
1693
1907
|
_lastNotifiedQuestion = "";
|
|
1908
|
+
// The question is closed: the countdown of the wait is over.
|
|
1909
|
+
pendingCountdown.stop();
|
|
1694
1910
|
|
|
1695
1911
|
// The status stays "RUN" until the turn that the steer
|
|
1696
1912
|
// started has settled. A timer would clear it while the
|
|
@@ -1701,6 +1917,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
1701
1917
|
_inferenceInProgress = false;
|
|
1702
1918
|
} catch {
|
|
1703
1919
|
_inferenceInProgress = false;
|
|
1920
|
+
// The countdown is over: the fallback poll takes over the next
|
|
1921
|
+
// attempt.
|
|
1922
|
+
pendingCountdown.stop();
|
|
1704
1923
|
updateAutoContinueStatus("ERROR");
|
|
1705
1924
|
addHarnessMessage("Jev", "auto-continue evaluation failed", "err", [
|
|
1706
1925
|
"How to fix: Check logs at /tmp/pi-harness.log for API errors",
|
|
@@ -2074,7 +2293,10 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
2074
2293
|
});
|
|
2075
2294
|
|
|
2076
2295
|
// Start periodic check
|
|
2077
|
-
autoContinueTimer = setInterval(
|
|
2296
|
+
autoContinueTimer = setInterval(
|
|
2297
|
+
() => checkAndAutoContinue("poll"),
|
|
2298
|
+
AUTO_CONTINUE_INTERVAL_MS,
|
|
2299
|
+
);
|
|
2078
2300
|
if (jevKeyMissing) {
|
|
2079
2301
|
// Without a provider the health check cannot run. The missing-key
|
|
2080
2302
|
// message (with its fix commands) stays the visible one.
|
|
@@ -3220,39 +3442,65 @@ function initConsoleGuard(pi: ExtensionAPI): void {
|
|
|
3220
3442
|
}
|
|
3221
3443
|
|
|
3222
3444
|
// --- honcho-memory: Honcho MCP for peer/user memory via https://mcp.honcho.dev ------
|
|
3223
|
-
// Registers Honcho MCP server
|
|
3445
|
+
// Registers the Honcho MCP server with Pi's NATIVE MCP support and implements
|
|
3446
|
+
// the automatic memory lifecycle:
|
|
3224
3447
|
// - Ingest sanitized messages on completed turns
|
|
3225
3448
|
// - Retrieve relevant context before agent execution
|
|
3226
3449
|
// - Deduplication by message ID
|
|
3227
3450
|
// - Bounded context injection with provenance
|
|
3228
3451
|
|
|
3452
|
+
/**
|
|
3453
|
+
* The contract of the Honcho integration module.
|
|
3454
|
+
*
|
|
3455
|
+
* The module is loaded at runtime, so the shape of its exports is declared
|
|
3456
|
+
* here: this is the only part TypeScript cannot check for us.
|
|
3457
|
+
*/
|
|
3458
|
+
interface HonchoModule {
|
|
3459
|
+
initHoncho: (options: {
|
|
3460
|
+
apiKey?: string;
|
|
3461
|
+
pi?: HonchoMcpRegistrar;
|
|
3462
|
+
}) => Promise<HonchoInitResult>;
|
|
3463
|
+
registerHonchoLifecycle: (pi: unknown) => void;
|
|
3464
|
+
/** The credential: the environment wins over the key file. */
|
|
3465
|
+
loadHonchoApiKey: () => string | undefined;
|
|
3466
|
+
/** Removes the credential from a text. */
|
|
3467
|
+
redactHonchoKey: (text: string, apiKey: string) => string;
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3229
3470
|
async function initHoncho(pi: ExtensionAPI): Promise<void> {
|
|
3230
3471
|
addHarnessMessage("Honcho", "initializing", "wait");
|
|
3472
|
+
let apiKey = "";
|
|
3473
|
+
let honcho: HonchoModule | null = null;
|
|
3231
3474
|
try {
|
|
3232
|
-
// Import
|
|
3233
|
-
|
|
3475
|
+
// Import the Honcho integration module.
|
|
3476
|
+
honcho = (await import("./harness/honcho-init.js")) as unknown as HonchoModule;
|
|
3234
3477
|
|
|
3235
|
-
//
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3478
|
+
// The credential comes from the loader of the module: the environment
|
|
3479
|
+
// variable wins over the key file, and the file is the per-user default.
|
|
3480
|
+
apiKey = honcho.loadHonchoApiKey() ?? "";
|
|
3481
|
+
if (!apiKey) {
|
|
3482
|
+
// A key file that exists but is empty is a different advice than a
|
|
3483
|
+
// missing file.
|
|
3484
|
+
const { readFileSync, existsSync } = await import("node:fs");
|
|
3485
|
+
const home = process.env.HOME || process.env.USERPROFILE || homedir();
|
|
3486
|
+
const keyPath = `${home}/.pi-harness-runtime/keys/honcho-api-key.txt`;
|
|
3487
|
+
const emptyFile = existsSync(keyPath) && readFileSync(keyPath, "utf8").trim() === "";
|
|
3488
|
+
|
|
3489
|
+
if (emptyFile) {
|
|
3490
|
+
logStartup("[pi-harness] Honcho: API key is empty — skipping");
|
|
3491
|
+
addHarnessMessage("Honcho", "API key empty", "warn", [
|
|
3492
|
+
...keyFileFixesOrFallback("Honcho", ["API key"]),
|
|
3493
|
+
"Then reload Pi.",
|
|
3494
|
+
]);
|
|
3495
|
+
return;
|
|
3496
|
+
}
|
|
3239
3497
|
|
|
3240
|
-
if (!existsSync(keyPath)) {
|
|
3241
3498
|
logStartup("[pi-harness] Honcho not configured:");
|
|
3242
3499
|
logStartup("[pi-harness] Get key: https://app.honcho.dev/api-keys");
|
|
3243
3500
|
logStartup(`[pi-harness] Then: echo "{api-key}" > ~/.pi-harness-runtime/keys/honcho-api-key.txt`);
|
|
3244
3501
|
addHarnessMessage("Honcho", "API key missing", "warn", [
|
|
3245
3502
|
...keyFileFixesOrFallback("Honcho", ["API key"]),
|
|
3246
|
-
"
|
|
3247
|
-
]);
|
|
3248
|
-
return;
|
|
3249
|
-
}
|
|
3250
|
-
|
|
3251
|
-
const apiKey = readFileSync(keyPath, "utf8").trim();
|
|
3252
|
-
if (!apiKey) {
|
|
3253
|
-
logStartup("[pi-harness] Honcho: API key is empty — skipping");
|
|
3254
|
-
addHarnessMessage("Honcho", "API key empty", "warn", [
|
|
3255
|
-
...keyFileFixesOrFallback("Honcho", ["API key"]),
|
|
3503
|
+
"Get a key: https://app.honcho.dev/api-keys",
|
|
3256
3504
|
"Then reload Pi.",
|
|
3257
3505
|
]);
|
|
3258
3506
|
return;
|
|
@@ -3260,27 +3508,130 @@ async function initHoncho(pi: ExtensionAPI): Promise<void> {
|
|
|
3260
3508
|
|
|
3261
3509
|
// Register lifecycle hooks using the new integration
|
|
3262
3510
|
// SAFETY: pi.on accepts these event names at runtime
|
|
3263
|
-
registerHonchoLifecycle(pi as unknown as { on: (event: string, handler: (...args: unknown[]) => void | Promise<void>) => void });
|
|
3264
|
-
|
|
3265
|
-
// Initialize Honcho with the API key
|
|
3266
|
-
//
|
|
3267
|
-
|
|
3511
|
+
honcho.registerHonchoLifecycle(pi as unknown as { on: (event: string, handler: (...args: unknown[]) => void | Promise<void>) => void });
|
|
3512
|
+
|
|
3513
|
+
// Initialize Honcho with the API key. The integration uses Pi's own MCP
|
|
3514
|
+
// client, so no event adapter is passed any more. It also registers
|
|
3515
|
+
// the MCP server with the extension API of Pi, so the model and
|
|
3516
|
+
// `/mcp` see it - and a refused registration is not fatal: the
|
|
3517
|
+
// harness talks to the server through its own MCP client, so only
|
|
3518
|
+
// the native tools are missing and the rest of the startup continues.
|
|
3519
|
+
// SAFETY: the runtime that runs this extension exposes
|
|
3520
|
+
// registerMcpServer (verified with Pi 1.4.0); the local type
|
|
3521
|
+
// definitions are older, so the call is cast.
|
|
3522
|
+
const result = await honcho.initHoncho({
|
|
3523
|
+
apiKey,
|
|
3524
|
+
pi: pi as unknown as HonchoMcpRegistrar,
|
|
3525
|
+
});
|
|
3268
3526
|
|
|
3269
|
-
if (result) {
|
|
3270
|
-
logStartup("[pi-harness] Honcho
|
|
3271
|
-
addHarnessMessage("Honcho", "
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
"How to fix: Check HONCHO_API_KEY in ~/.pi-harness-runtime/keys/honcho-api-key.txt",
|
|
3276
|
-
"Verify Honcho account is active at honcho.dev.",
|
|
3527
|
+
if (!result || !result.status) {
|
|
3528
|
+
logStartup("[pi-harness] Honcho: initialization produced no status");
|
|
3529
|
+
addHarnessMessage("Honcho", "init failed", "err", [
|
|
3530
|
+
"Check /tmp/pi-harness.log for the Honcho reason.",
|
|
3531
|
+
"The Honcho integration returned no status.",
|
|
3532
|
+
"Honcho memory stays disabled for this session.",
|
|
3277
3533
|
]);
|
|
3534
|
+
return;
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
switch (result.status) {
|
|
3538
|
+
case "ready": {
|
|
3539
|
+
logStartup("[pi-harness] Honcho integration ready");
|
|
3540
|
+
if (result.mcpRegistered) {
|
|
3541
|
+
logStartup("[pi-harness] Honcho: native MCP server registered");
|
|
3542
|
+
addHarnessMessage("Honcho", "initialized", "ok");
|
|
3543
|
+
} else {
|
|
3544
|
+
// The memory works, but the model has no Honcho tools: the
|
|
3545
|
+
// user has to know that, not only the log. The reason of
|
|
3546
|
+
// the registration names the cause, so the advice is not
|
|
3547
|
+
// always "check the Pi version".
|
|
3548
|
+
const cause = honcho.redactHonchoKey(
|
|
3549
|
+
result.mcpRegistrationReason ?? "the reason was not reported",
|
|
3550
|
+
apiKey,
|
|
3551
|
+
);
|
|
3552
|
+
logStartup(`[pi-harness] Honcho: native MCP server not registered (the model sees no Honcho tools): ${cause}`);
|
|
3553
|
+
// The advice follows the cause: a runtime without
|
|
3554
|
+
// registerMcpServer is a different problem than a name
|
|
3555
|
+
// that another extension owns.
|
|
3556
|
+
const capability = cause.includes("registerMcpServer")
|
|
3557
|
+
? "This runtime does not provide registerMcpServer: the Honcho memory works, but the model sees no Honcho tools."
|
|
3558
|
+
: "The Honcho memory works, but the model sees no Honcho tools.";
|
|
3559
|
+
addHarnessMessage("Honcho", "native MCP tools not registered", "warn", [
|
|
3560
|
+
`The registration failed: ${cause}`,
|
|
3561
|
+
"Check /tmp/pi-harness.log for the Honcho reason.",
|
|
3562
|
+
capability,
|
|
3563
|
+
]);
|
|
3564
|
+
}
|
|
3565
|
+
break;
|
|
3566
|
+
}
|
|
3567
|
+
case "missing_key": {
|
|
3568
|
+
// The only state that may point at the key file: the credential
|
|
3569
|
+
// is what is missing.
|
|
3570
|
+
logStartup("[pi-harness] Honcho not configured:");
|
|
3571
|
+
logStartup("[pi-harness] Get key: https://app.honcho.dev/api-keys");
|
|
3572
|
+
logStartup(`[pi-harness] Then: echo "{api-key}" > ~/.pi-harness-runtime/keys/honcho-api-key.txt`);
|
|
3573
|
+
addHarnessMessage("Honcho", "API key missing", "warn", [
|
|
3574
|
+
...keyFileFixesOrFallback("Honcho", ["API key"]),
|
|
3575
|
+
"Get a key: https://app.honcho.dev/api-keys",
|
|
3576
|
+
"Then reload Pi.",
|
|
3577
|
+
]);
|
|
3578
|
+
break;
|
|
3579
|
+
}
|
|
3580
|
+
case "mcp_unavailable": {
|
|
3581
|
+
// The key was read successfully - the problem is the MCP runtime.
|
|
3582
|
+
const reason = honcho.redactHonchoKey(result.reason ?? "no reason given", apiKey);
|
|
3583
|
+
const hint = honcho.redactHonchoKey(
|
|
3584
|
+
result.hint ?? "The native MCP client library @earendil-works/pi-mcp is required.",
|
|
3585
|
+
apiKey,
|
|
3586
|
+
);
|
|
3587
|
+
logStartup(`[pi-harness] Honcho: native MCP unavailable (${reason})`);
|
|
3588
|
+
addHarnessMessage("Honcho", "native MCP unavailable", "err", [
|
|
3589
|
+
"Check /tmp/pi-harness.log for the Honcho reason.",
|
|
3590
|
+
reason,
|
|
3591
|
+
hint,
|
|
3592
|
+
]);
|
|
3593
|
+
break;
|
|
3594
|
+
}
|
|
3595
|
+
case "registration_rejected": {
|
|
3596
|
+
const reason = honcho.redactHonchoKey(
|
|
3597
|
+
result.reason ?? "the server did not accept the request",
|
|
3598
|
+
apiKey,
|
|
3599
|
+
);
|
|
3600
|
+
const httpStatus = typeof result.httpStatus === "number" ? ` (HTTP ${result.httpStatus})` : "";
|
|
3601
|
+
const hint = honcho.redactHonchoKey(
|
|
3602
|
+
result.hint ?? "Check the Honcho account at https://honcho.dev.",
|
|
3603
|
+
apiKey,
|
|
3604
|
+
);
|
|
3605
|
+
logStartup(`[pi-harness] Honcho: the server refused the request${httpStatus} (the MCP server is not registered): ${reason}`);
|
|
3606
|
+
addHarnessMessage("Honcho", "server refused the request", "err", [
|
|
3607
|
+
"Check the Honcho account and the endpoint: https://app.honcho.dev/api-keys",
|
|
3608
|
+
reason,
|
|
3609
|
+
hint,
|
|
3610
|
+
]);
|
|
3611
|
+
break;
|
|
3612
|
+
}
|
|
3613
|
+
default: {
|
|
3614
|
+
const reason = honcho.redactHonchoKey(String(result.status), apiKey);
|
|
3615
|
+
logStartup("[pi-harness] Honcho: initialization failed:", reason);
|
|
3616
|
+
addHarnessMessage("Honcho", "init failed", "err", [
|
|
3617
|
+
"Check /tmp/pi-harness.log for the Honcho reason.",
|
|
3618
|
+
reason,
|
|
3619
|
+
"Honcho memory stays disabled for this session.",
|
|
3620
|
+
]);
|
|
3621
|
+
break;
|
|
3622
|
+
}
|
|
3278
3623
|
}
|
|
3279
3624
|
} catch (err) {
|
|
3280
|
-
|
|
3625
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3626
|
+
const reason =
|
|
3627
|
+
honcho && typeof honcho.redactHonchoKey === "function"
|
|
3628
|
+
? honcho.redactHonchoKey(message, apiKey)
|
|
3629
|
+
: message;
|
|
3630
|
+
logStartup("[pi-harness] Honcho: init failed:", reason);
|
|
3281
3631
|
addHarnessMessage("Honcho", "init failed", "err", [
|
|
3282
|
-
|
|
3283
|
-
|
|
3632
|
+
"Check /tmp/pi-harness.log for the Honcho reason.",
|
|
3633
|
+
reason,
|
|
3634
|
+
"Honcho memory stays disabled for this session.",
|
|
3284
3635
|
]);
|
|
3285
3636
|
}
|
|
3286
3637
|
}
|
|
@@ -3820,8 +4171,31 @@ async function initHermesSkills(pi: ExtensionAPI): Promise<void> {
|
|
|
3820
4171
|
addHarnessMessage("Skills", "initializing", "wait");
|
|
3821
4172
|
try {
|
|
3822
4173
|
// Dynamically import to avoid circular deps
|
|
3823
|
-
const { initSkills, sanitizationStatus } =
|
|
4174
|
+
const { initSkills, sanitizationStatus, sanitizationFilePath } =
|
|
3824
4175
|
await import("./packages/skills/dist/index.js");
|
|
4176
|
+
let sanitizationFileCreated = false;
|
|
4177
|
+
let sanitizationFileCreateError: string | undefined;
|
|
4178
|
+
try {
|
|
4179
|
+
const path = sanitizationFilePath();
|
|
4180
|
+
const { existsSync, mkdirSync, writeFileSync } = await import("node:fs");
|
|
4181
|
+
const { dirname } = await import("node:path");
|
|
4182
|
+
if (!existsSync(path)) {
|
|
4183
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
4184
|
+
try {
|
|
4185
|
+
writeFileSync(
|
|
4186
|
+
path,
|
|
4187
|
+
`${JSON.stringify({ rules: [], nameMap: {} }, null, 2)}\n`,
|
|
4188
|
+
{ encoding: "utf8", flag: "wx", mode: 0o600 },
|
|
4189
|
+
);
|
|
4190
|
+
sanitizationFileCreated = true;
|
|
4191
|
+
} catch (error) {
|
|
4192
|
+
// Another Pi process may have created the file after existsSync.
|
|
4193
|
+
if ((error as NodeJS.ErrnoException)?.code !== "EEXIST") throw error;
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
} catch (error) {
|
|
4197
|
+
sanitizationFileCreateError = error instanceof Error ? error.message : String(error);
|
|
4198
|
+
}
|
|
3825
4199
|
|
|
3826
4200
|
// Scan skills from default locations
|
|
3827
4201
|
const result = initSkills();
|
|
@@ -3833,21 +4207,30 @@ async function initHermesSkills(pi: ExtensionAPI): Promise<void> {
|
|
|
3833
4207
|
const local = localSkillsReport(localCount);
|
|
3834
4208
|
addHarnessMessage("Skills", local.text, local.status, local.details);
|
|
3835
4209
|
// The rules that keep a client name out of the shared collection
|
|
3836
|
-
// live in a local file
|
|
3837
|
-
//
|
|
3838
|
-
// name travels into the collection. That is a visible state, not
|
|
3839
|
-
// a silent one.
|
|
4210
|
+
// live in a local file. Create a private empty template on first
|
|
4211
|
+
// use; client-specific rules can be added locally.
|
|
3840
4212
|
try {
|
|
3841
4213
|
const rules = sanitizationStatus();
|
|
3842
|
-
if (
|
|
4214
|
+
if (sanitizationFileCreated && rules.configured) {
|
|
4215
|
+
addHarnessMessage(
|
|
4216
|
+
"Skill privacy",
|
|
4217
|
+
"created a private sanitization rules file",
|
|
4218
|
+
"info",
|
|
4219
|
+
[
|
|
4220
|
+
`File: ${rules.path}`,
|
|
4221
|
+
"The file starts with no client-specific rules. Add private rules before publishing skills that contain client names.",
|
|
4222
|
+
],
|
|
4223
|
+
);
|
|
4224
|
+
} else if (!rules.configured) {
|
|
3843
4225
|
addHarnessMessage(
|
|
3844
4226
|
"Skill privacy",
|
|
3845
|
-
"sanitization rules not
|
|
4227
|
+
"sanitization rules file could not be created",
|
|
3846
4228
|
"warn",
|
|
3847
4229
|
[
|
|
3848
|
-
`
|
|
3849
|
-
"
|
|
3850
|
-
"
|
|
4230
|
+
`Create ${rules.path} with { "rules": [], "nameMap": {} } and add private client-specific rules if needed.`,
|
|
4231
|
+
"Check the path in PI_SKILL_SANITIZATION_FILE.",
|
|
4232
|
+
"PI_SKILL_SANITIZATION_FILE must name a file path, not a JSON array.",
|
|
4233
|
+
...(sanitizationFileCreateError ? [sanitizationFileCreateError] : []),
|
|
3851
4234
|
...(rules.error ? [rules.error] : []),
|
|
3852
4235
|
],
|
|
3853
4236
|
);
|
|
@@ -6020,6 +6403,7 @@ Run \`bd ready\` to see current bd issues.
|
|
|
6020
6403
|
// --- /usage today — focused view -------------------------------------
|
|
6021
6404
|
pi.registerCommand("usage-today", {
|
|
6022
6405
|
description: "Show today's usage + 5h window",
|
|
6406
|
+
// The handler returns a Promise: the command API of Pi requires one.
|
|
6023
6407
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
6024
6408
|
const local = aggregateWindows(tracker.all());
|
|
6025
6409
|
const lines = [
|