@xdevops/issue-auto-finish 1.0.92 → 1.0.94
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/{PtyRunner-NYASBTRP.js → PtyRunner-6RSDKUMM.js} +4 -2
- package/dist/ai-runner/DialogClassifier.d.ts +44 -0
- package/dist/ai-runner/DialogClassifier.d.ts.map +1 -0
- package/dist/ai-runner/PlanFileResolver.d.ts +1 -0
- package/dist/ai-runner/PlanFileResolver.d.ts.map +1 -1
- package/dist/ai-runner/PtyRunner.d.ts +17 -0
- package/dist/ai-runner/PtyRunner.d.ts.map +1 -1
- package/dist/{ai-runner-TOHVJJ76.js → ai-runner-45IRCBIR.js} +2 -2
- package/dist/{analyze-DBH4K3J7.js → analyze-7TY5DYBT.js} +2 -2
- package/dist/{braindump-RYI4BGMG.js → braindump-FLX6HEVB.js} +2 -2
- package/dist/{chunk-4XMYOXGZ.js → chunk-36G3DPO3.js} +944 -93
- package/dist/chunk-36G3DPO3.js.map +1 -0
- package/dist/{chunk-6T7ZHAV2.js → chunk-4JI5AJEA.js} +9 -9
- package/dist/{chunk-WZGEYHCC.js → chunk-MTXTSSBH.js} +271 -716
- package/dist/chunk-MTXTSSBH.js.map +1 -0
- package/dist/{chunk-ENF24C44.js → chunk-RR65A7J4.js} +2 -2
- package/dist/{chunk-2WDVTLVF.js → chunk-ZDY5NCP3.js} +1 -1
- package/dist/cli.js +5 -5
- package/dist/hooks/HookInjector.d.ts +20 -0
- package/dist/hooks/HookInjector.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/{init-UKTP7LXS.js → init-O7XJLCP3.js} +2 -2
- package/dist/lib.js +2 -2
- package/dist/{restart-5D3ZDD5L.js → restart-4LNDGOOU.js} +2 -2
- package/dist/run.js +4 -4
- package/dist/{start-IQBNXLEI.js → start-Z4ODDTJ5.js} +2 -2
- package/package.json +1 -1
- package/src/web/frontend/dist/assets/index-BrvoaFSK.css +1 -0
- package/src/web/frontend/dist/assets/{index-BR0UoQER.js → index-CmyxgdS_.js} +54 -54
- package/src/web/frontend/dist/index.html +2 -2
- package/dist/chunk-4XMYOXGZ.js.map +0 -1
- package/dist/chunk-WZGEYHCC.js.map +0 -1
- package/src/web/frontend/dist/assets/index-DWOHf3bd.css +0 -1
- /package/dist/{PtyRunner-NYASBTRP.js.map → PtyRunner-6RSDKUMM.js.map} +0 -0
- /package/dist/{ai-runner-TOHVJJ76.js.map → ai-runner-45IRCBIR.js.map} +0 -0
- /package/dist/{analyze-DBH4K3J7.js.map → analyze-7TY5DYBT.js.map} +0 -0
- /package/dist/{braindump-RYI4BGMG.js.map → braindump-FLX6HEVB.js.map} +0 -0
- /package/dist/{chunk-6T7ZHAV2.js.map → chunk-4JI5AJEA.js.map} +0 -0
- /package/dist/{chunk-ENF24C44.js.map → chunk-RR65A7J4.js.map} +0 -0
- /package/dist/{chunk-2WDVTLVF.js.map → chunk-ZDY5NCP3.js.map} +0 -0
- /package/dist/{init-UKTP7LXS.js.map → init-O7XJLCP3.js.map} +0 -0
- /package/dist/{restart-5D3ZDD5L.js.map → restart-4LNDGOOU.js.map} +0 -0
- /package/dist/{start-IQBNXLEI.js.map → start-Z4ODDTJ5.js.map} +0 -0
|
@@ -12,8 +12,8 @@ import {
|
|
|
12
12
|
} from "./chunk-GF2RRYHB.js";
|
|
13
13
|
|
|
14
14
|
// src/ai-runner/PtyRunner.ts
|
|
15
|
-
import
|
|
16
|
-
import
|
|
15
|
+
import fs4 from "fs";
|
|
16
|
+
import path3 from "path";
|
|
17
17
|
|
|
18
18
|
// src/ai-runner/PlanFileResolver.ts
|
|
19
19
|
import fs from "fs";
|
|
@@ -24,6 +24,9 @@ var PLAN_DIRS = {
|
|
|
24
24
|
"claude-internal": path.join(os.homedir(), ".claude-internal", "plans"),
|
|
25
25
|
"codebuddy": path.join(os.homedir(), ".codebuddy", "plans")
|
|
26
26
|
};
|
|
27
|
+
var PLAN_METADATA_PATTERNS = [
|
|
28
|
+
/^00-plan-status\.md$/
|
|
29
|
+
];
|
|
27
30
|
var PlanFileResolver = class _PlanFileResolver {
|
|
28
31
|
plansDir;
|
|
29
32
|
beforeFiles = /* @__PURE__ */ new Map();
|
|
@@ -82,6 +85,7 @@ var PlanFileResolver = class _PlanFileResolver {
|
|
|
82
85
|
});
|
|
83
86
|
return this.fallbackByMtime(afterFiles, contentHint);
|
|
84
87
|
}
|
|
88
|
+
candidates.sort((a, b) => b.mtime - a.mtime);
|
|
85
89
|
if (candidates.length === 1) {
|
|
86
90
|
return this.readCandidate(candidates[0].path, candidates[0].mtime);
|
|
87
91
|
}
|
|
@@ -89,7 +93,6 @@ var PlanFileResolver = class _PlanFileResolver {
|
|
|
89
93
|
const matched = this.matchByContent(candidates, contentHint);
|
|
90
94
|
if (matched) return matched;
|
|
91
95
|
}
|
|
92
|
-
candidates.sort((a, b) => b.mtime - a.mtime);
|
|
93
96
|
logger2.info("Multiple new plan files found, using most recent", {
|
|
94
97
|
count: candidates.length,
|
|
95
98
|
selected: path.basename(candidates[0].path)
|
|
@@ -128,12 +131,16 @@ var PlanFileResolver = class _PlanFileResolver {
|
|
|
128
131
|
findNewFiles(afterFiles) {
|
|
129
132
|
const candidates = [];
|
|
130
133
|
for (const [filePath, mtime] of afterFiles) {
|
|
131
|
-
if (!this.beforeFiles.has(filePath)) {
|
|
134
|
+
if (!this.beforeFiles.has(filePath) && !this.isMetadataFile(filePath)) {
|
|
132
135
|
candidates.push({ path: filePath, mtime });
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
138
|
return candidates;
|
|
136
139
|
}
|
|
140
|
+
isMetadataFile(filePath) {
|
|
141
|
+
const basename = path.basename(filePath);
|
|
142
|
+
return PLAN_METADATA_PATTERNS.some((pattern) => pattern.test(basename));
|
|
143
|
+
}
|
|
137
144
|
/**
|
|
138
145
|
* Fallback: if no new files found (rare case — plan might have overwritten
|
|
139
146
|
* an existing file), find the most recently modified file.
|
|
@@ -164,21 +171,27 @@ var PlanFileResolver = class _PlanFileResolver {
|
|
|
164
171
|
return this.readCandidate(newestPath, newestMtime);
|
|
165
172
|
}
|
|
166
173
|
matchByContent(candidates, contentHint) {
|
|
174
|
+
const matches = [];
|
|
167
175
|
for (const candidate of candidates) {
|
|
168
176
|
try {
|
|
169
177
|
const content = fs.readFileSync(candidate.path, "utf-8");
|
|
170
178
|
if (this.contentMatches(content, contentHint)) {
|
|
171
|
-
|
|
172
|
-
file: path.basename(candidate.path),
|
|
173
|
-
hint: contentHint.slice(0, 50)
|
|
174
|
-
});
|
|
175
|
-
return { sourcePath: candidate.path, content };
|
|
179
|
+
matches.push({ sourcePath: candidate.path, content });
|
|
176
180
|
}
|
|
177
181
|
} catch {
|
|
178
182
|
continue;
|
|
179
183
|
}
|
|
180
184
|
}
|
|
181
|
-
return null;
|
|
185
|
+
if (matches.length === 0) return null;
|
|
186
|
+
matches.sort((a, b) => b.content.length - a.content.length);
|
|
187
|
+
const best = matches[0];
|
|
188
|
+
logger2.info("Plan file matched by content", {
|
|
189
|
+
file: path.basename(best.sourcePath),
|
|
190
|
+
hint: contentHint.slice(0, 50),
|
|
191
|
+
matchCount: matches.length,
|
|
192
|
+
size: best.content.length
|
|
193
|
+
});
|
|
194
|
+
return best;
|
|
182
195
|
}
|
|
183
196
|
contentMatches(content, hint) {
|
|
184
197
|
const parts = hint.split("|");
|
|
@@ -200,8 +213,711 @@ var PlanFileResolver = class _PlanFileResolver {
|
|
|
200
213
|
}
|
|
201
214
|
};
|
|
202
215
|
|
|
216
|
+
// src/ai-runner/DialogClassifier.ts
|
|
217
|
+
var logger3 = logger.child("DialogClassifier");
|
|
218
|
+
var HOOK_EVENT_TTL_MS = 5e3;
|
|
219
|
+
var PERMISSION_SUPPRESS_MS = 2e3;
|
|
220
|
+
var ASK_USER_SUPPRESS_MS = 3e3;
|
|
221
|
+
var DialogClassifier = class {
|
|
222
|
+
recentEvents = [];
|
|
223
|
+
handledIds = /* @__PURE__ */ new Set();
|
|
224
|
+
ingestHookEvent(event) {
|
|
225
|
+
this.recentEvents.push({ event, receivedAt: Date.now() });
|
|
226
|
+
this.pruneExpired();
|
|
227
|
+
if (event.event === "ask_user_question") {
|
|
228
|
+
logger3.info("Received ask_user_question hook event", {
|
|
229
|
+
question: event.question.slice(0, 80),
|
|
230
|
+
optionCount: event.options.length
|
|
231
|
+
});
|
|
232
|
+
} else if (event.event === "notification") {
|
|
233
|
+
logger3.debug("Received notification hook event", {
|
|
234
|
+
type: event.notification_type
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Check if there's a pending AskUserQuestion from hooks that hasn't been
|
|
240
|
+
* handled yet. Returns structured data or null.
|
|
241
|
+
*/
|
|
242
|
+
consumePendingAskUser() {
|
|
243
|
+
const now = Date.now();
|
|
244
|
+
for (let i = this.recentEvents.length - 1; i >= 0; i--) {
|
|
245
|
+
const { event, receivedAt } = this.recentEvents[i];
|
|
246
|
+
if (event.event !== "ask_user_question") continue;
|
|
247
|
+
if (now - receivedAt > HOOK_EVENT_TTL_MS) continue;
|
|
248
|
+
const eventId = `ask_user_${event.ts}`;
|
|
249
|
+
if (this.handledIds.has(eventId)) continue;
|
|
250
|
+
this.handledIds.add(eventId);
|
|
251
|
+
return {
|
|
252
|
+
question: event.question,
|
|
253
|
+
options: mapHookOptions(event.options)
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Whether a recent permission_prompt notification exists, indicating
|
|
260
|
+
* that regex-detected dialogs are likely false positives.
|
|
261
|
+
*/
|
|
262
|
+
hasRecentPermissionPrompt() {
|
|
263
|
+
const now = Date.now();
|
|
264
|
+
return this.recentEvents.some(
|
|
265
|
+
({ event, receivedAt }) => event.event === "notification" && event.notification_type === "permission_prompt" && now - receivedAt < PERMISSION_SUPPRESS_MS
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Whether a recent ask_user_question hook event exists, indicating
|
|
270
|
+
* that regex detection should be suppressed to avoid duplicate forwarding.
|
|
271
|
+
*/
|
|
272
|
+
hasRecentAskUser() {
|
|
273
|
+
const now = Date.now();
|
|
274
|
+
return this.recentEvents.some(
|
|
275
|
+
({ event, receivedAt }) => event.event === "ask_user_question" && now - receivedAt < ASK_USER_SUPPRESS_MS
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Whether regex-based dialog detection should be suppressed
|
|
280
|
+
* (either due to recent hook-based AskUser or permission_prompt).
|
|
281
|
+
*/
|
|
282
|
+
shouldSuppressRegex() {
|
|
283
|
+
return this.hasRecentAskUser() || this.hasRecentPermissionPrompt();
|
|
284
|
+
}
|
|
285
|
+
reset() {
|
|
286
|
+
this.recentEvents.length = 0;
|
|
287
|
+
this.handledIds.clear();
|
|
288
|
+
}
|
|
289
|
+
pruneExpired() {
|
|
290
|
+
const cutoff = Date.now() - HOOK_EVENT_TTL_MS;
|
|
291
|
+
this.recentEvents = this.recentEvents.filter((e) => e.receivedAt > cutoff);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
function mapHookOptions(hookOptions) {
|
|
295
|
+
return hookOptions.map((o, i) => ({
|
|
296
|
+
index: o.index ?? i + 1,
|
|
297
|
+
label: o.label
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/hooks/HookEventWatcher.ts
|
|
302
|
+
import fs2 from "fs";
|
|
303
|
+
var logger4 = logger.child("HookEventWatcher");
|
|
304
|
+
var HookEventWatcher = class {
|
|
305
|
+
eventsFile;
|
|
306
|
+
watcher = null;
|
|
307
|
+
pollTimer = null;
|
|
308
|
+
offset = 0;
|
|
309
|
+
listeners = [];
|
|
310
|
+
started = false;
|
|
311
|
+
constructor(eventsFile) {
|
|
312
|
+
this.eventsFile = eventsFile;
|
|
313
|
+
}
|
|
314
|
+
start() {
|
|
315
|
+
if (this.started) return;
|
|
316
|
+
this.started = true;
|
|
317
|
+
this.offset = this.getCurrentSize();
|
|
318
|
+
try {
|
|
319
|
+
this.watcher = fs2.watch(this.eventsFile, () => this.readNewEvents());
|
|
320
|
+
} catch {
|
|
321
|
+
logger4.debug("fs.watch unavailable, using poll-only mode");
|
|
322
|
+
}
|
|
323
|
+
this.pollTimer = setInterval(() => this.readNewEvents(), 1e3);
|
|
324
|
+
}
|
|
325
|
+
stop() {
|
|
326
|
+
if (!this.started) return;
|
|
327
|
+
this.started = false;
|
|
328
|
+
this.watcher?.close();
|
|
329
|
+
this.watcher = null;
|
|
330
|
+
if (this.pollTimer) {
|
|
331
|
+
clearInterval(this.pollTimer);
|
|
332
|
+
this.pollTimer = null;
|
|
333
|
+
}
|
|
334
|
+
this.listeners = [];
|
|
335
|
+
}
|
|
336
|
+
onEvent(callback) {
|
|
337
|
+
this.listeners.push(callback);
|
|
338
|
+
return {
|
|
339
|
+
dispose: () => {
|
|
340
|
+
this.listeners = this.listeners.filter((l) => l !== callback);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* 等待指定类型的事件,带超时。
|
|
346
|
+
* 对于 'stop' 事件,只有 blocked=false 时才 resolve。
|
|
347
|
+
*/
|
|
348
|
+
waitForEvent(eventType, timeoutMs) {
|
|
349
|
+
return new Promise((resolve, reject) => {
|
|
350
|
+
const timer = setTimeout(() => {
|
|
351
|
+
sub.dispose();
|
|
352
|
+
reject(new Error(`Timeout waiting for hook event "${eventType}" after ${timeoutMs}ms`));
|
|
353
|
+
}, timeoutMs);
|
|
354
|
+
const sub = this.onEvent((ev) => {
|
|
355
|
+
if (ev.event !== eventType) return;
|
|
356
|
+
if (ev.event === "stop" && ev.blocked) return;
|
|
357
|
+
clearTimeout(timer);
|
|
358
|
+
sub.dispose();
|
|
359
|
+
resolve(ev);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
/** 获取已记录的所有产物写入事件摘要 */
|
|
364
|
+
getArtifactSummary() {
|
|
365
|
+
const events = this.readAll().filter(
|
|
366
|
+
(e) => e.event === "artifact_write"
|
|
367
|
+
);
|
|
368
|
+
if (events.length === 0) return "";
|
|
369
|
+
return events.map((e) => `${e.file} (${e.bytes} bytes)`).join(", ");
|
|
370
|
+
}
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
// Private
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
readNewEvents() {
|
|
375
|
+
if (!this.started) return;
|
|
376
|
+
const size = this.getCurrentSize();
|
|
377
|
+
if (size <= this.offset) return;
|
|
378
|
+
try {
|
|
379
|
+
const fd = fs2.openSync(this.eventsFile, "r");
|
|
380
|
+
try {
|
|
381
|
+
const buf = Buffer.alloc(size - this.offset);
|
|
382
|
+
fs2.readSync(fd, buf, 0, buf.length, this.offset);
|
|
383
|
+
this.offset = size;
|
|
384
|
+
const chunk = buf.toString("utf-8");
|
|
385
|
+
for (const line of chunk.split("\n")) {
|
|
386
|
+
const trimmed = line.trim();
|
|
387
|
+
if (!trimmed) continue;
|
|
388
|
+
try {
|
|
389
|
+
const event = JSON.parse(trimmed);
|
|
390
|
+
this.emit(event);
|
|
391
|
+
} catch {
|
|
392
|
+
logger4.debug("Skipping malformed event line", { line: trimmed });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
} finally {
|
|
396
|
+
fs2.closeSync(fd);
|
|
397
|
+
}
|
|
398
|
+
} catch (err) {
|
|
399
|
+
logger4.debug("Error reading events file", { error: err.message });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
readAll() {
|
|
403
|
+
if (!fs2.existsSync(this.eventsFile)) return [];
|
|
404
|
+
try {
|
|
405
|
+
const content = fs2.readFileSync(this.eventsFile, "utf-8").trim();
|
|
406
|
+
if (!content) return [];
|
|
407
|
+
return content.split("\n").reduce((acc, line) => {
|
|
408
|
+
const trimmed = line.trim();
|
|
409
|
+
if (!trimmed) return acc;
|
|
410
|
+
try {
|
|
411
|
+
acc.push(JSON.parse(trimmed));
|
|
412
|
+
} catch {
|
|
413
|
+
}
|
|
414
|
+
return acc;
|
|
415
|
+
}, []);
|
|
416
|
+
} catch {
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
emit(event) {
|
|
421
|
+
for (const listener of this.listeners) {
|
|
422
|
+
try {
|
|
423
|
+
listener(event);
|
|
424
|
+
} catch (err) {
|
|
425
|
+
logger4.warn("Event listener error", { error: err.message });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
getCurrentSize() {
|
|
430
|
+
try {
|
|
431
|
+
return fs2.statSync(this.eventsFile).size;
|
|
432
|
+
} catch {
|
|
433
|
+
return 0;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// src/hooks/HookInjector.ts
|
|
439
|
+
import fs3 from "fs";
|
|
440
|
+
import path2 from "path";
|
|
441
|
+
var logger5 = logger.child("HookInjector");
|
|
442
|
+
var HOOKS_DIR = ".claude-plan/.hooks";
|
|
443
|
+
var EVENTS_FILE_NAME = ".hook-events.jsonl";
|
|
444
|
+
var MANIFEST_FILE_NAME = ".artifact-manifest.jsonl";
|
|
445
|
+
var CONTEXT_FILE_NAME = ".hook-context.json";
|
|
446
|
+
var HookInjector = class {
|
|
447
|
+
inject(ctx) {
|
|
448
|
+
this.writeHookScripts(ctx);
|
|
449
|
+
this.writeContextFile(ctx);
|
|
450
|
+
this.writeSettingsLocal(ctx);
|
|
451
|
+
this.initEventsFile(ctx);
|
|
452
|
+
logger5.info("Hooks injected", {
|
|
453
|
+
workDir: ctx.workDir,
|
|
454
|
+
issueIid: ctx.issueIid,
|
|
455
|
+
phase: ctx.phaseName,
|
|
456
|
+
artifacts: ctx.expectedArtifacts
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* 阶段切换时更新 hooks 配置(重写脚本 + settings.local.json)。
|
|
461
|
+
* 保留 events/manifest 文件(不截断),仅更新脚本和配置。
|
|
462
|
+
*/
|
|
463
|
+
updateForPhase(ctx) {
|
|
464
|
+
this.writeHookScripts(ctx);
|
|
465
|
+
this.writeContextFile(ctx);
|
|
466
|
+
this.writeSettingsLocal(ctx);
|
|
467
|
+
logger5.info("Hooks updated for phase", {
|
|
468
|
+
workDir: ctx.workDir,
|
|
469
|
+
issueIid: ctx.issueIid,
|
|
470
|
+
phase: ctx.phaseName
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
readManifest(workDir) {
|
|
474
|
+
const manifestPath = path2.join(workDir, ".claude-plan", MANIFEST_FILE_NAME);
|
|
475
|
+
return readJsonl(manifestPath);
|
|
476
|
+
}
|
|
477
|
+
readEvents(workDir) {
|
|
478
|
+
const eventsPath = path2.join(workDir, ".claude-plan", EVENTS_FILE_NAME);
|
|
479
|
+
return readJsonl(eventsPath);
|
|
480
|
+
}
|
|
481
|
+
getEventsFilePath(workDir) {
|
|
482
|
+
return path2.join(workDir, ".claude-plan", EVENTS_FILE_NAME);
|
|
483
|
+
}
|
|
484
|
+
getManifestFilePath(workDir) {
|
|
485
|
+
return path2.join(workDir, ".claude-plan", MANIFEST_FILE_NAME);
|
|
486
|
+
}
|
|
487
|
+
cleanup(workDir) {
|
|
488
|
+
const hooksDir = path2.join(workDir, HOOKS_DIR);
|
|
489
|
+
try {
|
|
490
|
+
if (fs3.existsSync(hooksDir)) {
|
|
491
|
+
fs3.rmSync(hooksDir, { recursive: true });
|
|
492
|
+
}
|
|
493
|
+
} catch (err) {
|
|
494
|
+
logger5.warn("Failed to cleanup hooks", { error: err.message });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
// Private
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
writeHookScripts(ctx) {
|
|
501
|
+
const hooksDir = path2.join(ctx.workDir, HOOKS_DIR);
|
|
502
|
+
fs3.mkdirSync(hooksDir, { recursive: true });
|
|
503
|
+
const eventsFile = path2.join(ctx.workDir, ".claude-plan", EVENTS_FILE_NAME);
|
|
504
|
+
const manifestFile = path2.join(ctx.workDir, ".claude-plan", MANIFEST_FILE_NAME);
|
|
505
|
+
const contextFile = path2.join(ctx.workDir, ".claude-plan", CONTEXT_FILE_NAME);
|
|
506
|
+
const expected = ctx.expectedArtifacts.join(",");
|
|
507
|
+
const phaseExpected = (ctx.phaseExpectedArtifacts ?? ctx.expectedArtifacts).join(",");
|
|
508
|
+
const scripts = [
|
|
509
|
+
{ name: "session-start.sh", content: buildSessionStartScript(eventsFile) },
|
|
510
|
+
{ name: "compact-restore.sh", content: buildCompactRestoreScript(eventsFile, contextFile) },
|
|
511
|
+
{ name: "post-tool-use.sh", content: buildPostToolUseScript(eventsFile, manifestFile, expected) },
|
|
512
|
+
{ name: "post-artifact.sh", content: buildPostArtifactScript(manifestFile, expected) },
|
|
513
|
+
{ name: "exit-plan-mode.sh", content: buildExitPlanModeScript(eventsFile, ctx.planDir, ctx.phaseExpectedArtifacts?.[0] ?? ctx.expectedArtifacts[0]) },
|
|
514
|
+
{ name: "permission.sh", content: buildPermissionScript(eventsFile) },
|
|
515
|
+
{ name: "protect-files.sh", content: buildProtectFilesScript(eventsFile, ctx.phaseName, ctx.planDir) },
|
|
516
|
+
{ name: "stop.sh", content: buildStopScript(eventsFile, ctx.planDir, phaseExpected) },
|
|
517
|
+
{ name: "ask-user-hook.sh", content: buildAskUserHookScript(eventsFile) },
|
|
518
|
+
{ name: "notification.sh", content: buildNotificationScript(eventsFile) }
|
|
519
|
+
];
|
|
520
|
+
for (const { name, content } of scripts) {
|
|
521
|
+
const scriptPath = path2.join(hooksDir, name);
|
|
522
|
+
fs3.writeFileSync(scriptPath, content, { mode: 493 });
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
writeContextFile(ctx) {
|
|
526
|
+
const contextPath = path2.join(ctx.workDir, ".claude-plan", CONTEXT_FILE_NAME);
|
|
527
|
+
const context = {
|
|
528
|
+
issueIid: ctx.issueIid,
|
|
529
|
+
issueTitle: ctx.issueTitle ?? "",
|
|
530
|
+
issueDescription: ctx.issueDescription ?? "",
|
|
531
|
+
phaseName: ctx.phaseName ?? "",
|
|
532
|
+
expectedArtifacts: ctx.expectedArtifacts,
|
|
533
|
+
planDir: ctx.planDir
|
|
534
|
+
};
|
|
535
|
+
fs3.writeFileSync(contextPath, JSON.stringify(context, null, 2), "utf-8");
|
|
536
|
+
}
|
|
537
|
+
writeSettingsLocal(ctx) {
|
|
538
|
+
const claudeDir = path2.join(ctx.workDir, ".claude");
|
|
539
|
+
fs3.mkdirSync(claudeDir, { recursive: true });
|
|
540
|
+
const settingsPath = path2.join(claudeDir, "settings.local.json");
|
|
541
|
+
let existing = {};
|
|
542
|
+
if (fs3.existsSync(settingsPath)) {
|
|
543
|
+
try {
|
|
544
|
+
existing = JSON.parse(fs3.readFileSync(settingsPath, "utf-8"));
|
|
545
|
+
} catch {
|
|
546
|
+
logger5.warn("Failed to parse existing settings.local.json, overwriting");
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const hooksDir = path2.join(ctx.workDir, HOOKS_DIR);
|
|
550
|
+
const hooks = buildHooksConfig(hooksDir, ctx);
|
|
551
|
+
const merged = { ...existing, hooks };
|
|
552
|
+
fs3.writeFileSync(settingsPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
553
|
+
}
|
|
554
|
+
initEventsFile(ctx) {
|
|
555
|
+
const eventsPath = path2.join(ctx.workDir, ".claude-plan", EVENTS_FILE_NAME);
|
|
556
|
+
const manifestPath = path2.join(ctx.workDir, ".claude-plan", MANIFEST_FILE_NAME);
|
|
557
|
+
fs3.writeFileSync(eventsPath, "", "utf-8");
|
|
558
|
+
fs3.writeFileSync(manifestPath, "", "utf-8");
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
function buildHooksConfig(hooksDir, ctx) {
|
|
562
|
+
const isPlanPhase = ctx.phaseName === "plan";
|
|
563
|
+
const artifactIfPatterns = buildArtifactIfPatterns(ctx.expectedArtifacts);
|
|
564
|
+
const config = {
|
|
565
|
+
SessionStart: [
|
|
566
|
+
{
|
|
567
|
+
hooks: [{
|
|
568
|
+
type: "command",
|
|
569
|
+
command: path2.join(hooksDir, "session-start.sh"),
|
|
570
|
+
timeout: 5
|
|
571
|
+
}]
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
matcher: "compact",
|
|
575
|
+
hooks: [{
|
|
576
|
+
type: "command",
|
|
577
|
+
command: path2.join(hooksDir, "compact-restore.sh"),
|
|
578
|
+
timeout: 5
|
|
579
|
+
}]
|
|
580
|
+
}
|
|
581
|
+
],
|
|
582
|
+
PreToolUse: [
|
|
583
|
+
{
|
|
584
|
+
matcher: "AskUserQuestion",
|
|
585
|
+
hooks: [{
|
|
586
|
+
type: "command",
|
|
587
|
+
command: path2.join(hooksDir, "ask-user-hook.sh"),
|
|
588
|
+
timeout: 5
|
|
589
|
+
}]
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
matcher: "Edit|Write",
|
|
593
|
+
hooks: [{
|
|
594
|
+
type: "command",
|
|
595
|
+
command: path2.join(hooksDir, "protect-files.sh"),
|
|
596
|
+
timeout: 5,
|
|
597
|
+
...buildProtectIfClause(ctx.phaseName)
|
|
598
|
+
}]
|
|
599
|
+
}
|
|
600
|
+
],
|
|
601
|
+
PostToolUse: buildPostToolUseConfig(hooksDir, artifactIfPatterns),
|
|
602
|
+
PermissionRequest: buildPermissionRequestConfig(hooksDir, isPlanPhase),
|
|
603
|
+
Notification: [{
|
|
604
|
+
hooks: [{
|
|
605
|
+
type: "command",
|
|
606
|
+
command: path2.join(hooksDir, "notification.sh"),
|
|
607
|
+
timeout: 5
|
|
608
|
+
}]
|
|
609
|
+
}],
|
|
610
|
+
Stop: [{
|
|
611
|
+
hooks: [{
|
|
612
|
+
type: "command",
|
|
613
|
+
command: path2.join(hooksDir, "stop.sh"),
|
|
614
|
+
timeout: 15
|
|
615
|
+
}]
|
|
616
|
+
}]
|
|
617
|
+
};
|
|
618
|
+
return config;
|
|
619
|
+
}
|
|
620
|
+
function buildPermissionRequestConfig(hooksDir, isPlanPhase) {
|
|
621
|
+
const groups = [];
|
|
622
|
+
if (isPlanPhase) {
|
|
623
|
+
groups.push({
|
|
624
|
+
matcher: "ExitPlanMode",
|
|
625
|
+
hooks: [{
|
|
626
|
+
type: "command",
|
|
627
|
+
command: path2.join(hooksDir, "exit-plan-mode.sh"),
|
|
628
|
+
timeout: 5
|
|
629
|
+
}]
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
groups.push({
|
|
633
|
+
matcher: "Bash|Edit|Write|Read|Glob|Grep|WebFetch|WebSearch|mcp__.*",
|
|
634
|
+
hooks: [{
|
|
635
|
+
type: "command",
|
|
636
|
+
command: path2.join(hooksDir, "permission.sh"),
|
|
637
|
+
timeout: 5
|
|
638
|
+
}]
|
|
639
|
+
});
|
|
640
|
+
return groups;
|
|
641
|
+
}
|
|
642
|
+
function buildPostToolUseConfig(hooksDir, artifactIfPatterns) {
|
|
643
|
+
const groups = [];
|
|
644
|
+
if (artifactIfPatterns) {
|
|
645
|
+
groups.push({
|
|
646
|
+
matcher: "Write|Edit",
|
|
647
|
+
hooks: [{
|
|
648
|
+
type: "command",
|
|
649
|
+
command: path2.join(hooksDir, "post-artifact.sh"),
|
|
650
|
+
timeout: 10,
|
|
651
|
+
if: artifactIfPatterns
|
|
652
|
+
}]
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
groups.push({
|
|
656
|
+
matcher: "Write|Edit",
|
|
657
|
+
hooks: [{
|
|
658
|
+
type: "command",
|
|
659
|
+
command: path2.join(hooksDir, "post-tool-use.sh"),
|
|
660
|
+
timeout: 10
|
|
661
|
+
}]
|
|
662
|
+
});
|
|
663
|
+
return groups;
|
|
664
|
+
}
|
|
665
|
+
function buildArtifactIfPatterns(artifacts) {
|
|
666
|
+
if (artifacts.length === 0) return void 0;
|
|
667
|
+
return artifacts.flatMap((f) => [`Write(*${f})`, `Edit(*${f})`]).join("|");
|
|
668
|
+
}
|
|
669
|
+
function buildProtectIfClause(_phaseName) {
|
|
670
|
+
const alwaysProtected = [".env", ".env.*", "package-lock.json", "pnpm-lock.yaml"];
|
|
671
|
+
const ifValue = alwaysProtected.flatMap((f) => [`Edit(*${f})`, `Write(*${f})`]).join("|");
|
|
672
|
+
return { if: ifValue };
|
|
673
|
+
}
|
|
674
|
+
function buildSessionStartScript(eventsFile) {
|
|
675
|
+
return `#!/bin/bash
|
|
676
|
+
set -euo pipefail
|
|
677
|
+
INPUT=$(cat)
|
|
678
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
|
|
679
|
+
printf '{"ts":"%s","event":"session_start","session_id":"%s"}\\n' \\
|
|
680
|
+
"$(date -u +%FT%TZ)" "$SESSION_ID" >> ${quote(eventsFile)}
|
|
681
|
+
exit 0
|
|
682
|
+
`;
|
|
683
|
+
}
|
|
684
|
+
function buildCompactRestoreScript(eventsFile, contextFile) {
|
|
685
|
+
return `#!/bin/bash
|
|
686
|
+
set -euo pipefail
|
|
687
|
+
|
|
688
|
+
CONTEXT_FILE=${quote(contextFile)}
|
|
689
|
+
if [ ! -f "$CONTEXT_FILE" ]; then
|
|
690
|
+
exit 0
|
|
691
|
+
fi
|
|
692
|
+
|
|
693
|
+
ISSUE_IID=$(jq -r '.issueIid // empty' < "$CONTEXT_FILE")
|
|
694
|
+
ISSUE_TITLE=$(jq -r '.issueTitle // empty' < "$CONTEXT_FILE")
|
|
695
|
+
ISSUE_DESC=$(jq -r '.issueDescription // empty' < "$CONTEXT_FILE")
|
|
696
|
+
PHASE=$(jq -r '.phaseName // empty' < "$CONTEXT_FILE")
|
|
697
|
+
PLAN_DIR=$(jq -r '.planDir // empty' < "$CONTEXT_FILE")
|
|
698
|
+
ARTIFACTS=$(jq -r '.expectedArtifacts | join(", ") // empty' < "$CONTEXT_FILE")
|
|
699
|
+
|
|
700
|
+
READY=""
|
|
701
|
+
MISSING=""
|
|
702
|
+
for f in $(jq -r '.expectedArtifacts[]' < "$CONTEXT_FILE" 2>/dev/null); do
|
|
703
|
+
FPATH="$PLAN_DIR/$f"
|
|
704
|
+
if [ -f "$FPATH" ] && [ "$(wc -c < "$FPATH")" -ge 50 ]; then
|
|
705
|
+
READY="$READY $f"
|
|
706
|
+
else
|
|
707
|
+
MISSING="$MISSING $f"
|
|
708
|
+
fi
|
|
709
|
+
done
|
|
710
|
+
READY=$(echo "$READY" | xargs)
|
|
711
|
+
MISSING=$(echo "$MISSING" | xargs)
|
|
712
|
+
|
|
713
|
+
printf '{"ts":"%s","event":"compact_restore"}\\n' "$(date -u +%FT%TZ)" >> ${quote(eventsFile)}
|
|
714
|
+
|
|
715
|
+
cat <<CONTEXT
|
|
716
|
+
[\u4E0A\u4E0B\u6587\u6062\u590D \u2014 compaction \u540E\u81EA\u52A8\u6CE8\u5165]
|
|
717
|
+
Issue #$ISSUE_IID: $ISSUE_TITLE
|
|
718
|
+
\u5F53\u524D\u9636\u6BB5: $PHASE
|
|
719
|
+
\u9884\u671F\u4EA7\u7269: $ARTIFACTS
|
|
720
|
+
\u5DF2\u5C31\u7EEA: \${READY:-\u65E0}
|
|
721
|
+
\u672A\u5B8C\u6210: \${MISSING:-\u65E0}
|
|
722
|
+
|
|
723
|
+
\u9700\u6C42\u63CF\u8FF0:
|
|
724
|
+
$ISSUE_DESC
|
|
725
|
+
CONTEXT
|
|
726
|
+
exit 0
|
|
727
|
+
`;
|
|
728
|
+
}
|
|
729
|
+
function buildPostToolUseScript(eventsFile, manifestFile, expected) {
|
|
730
|
+
return `#!/bin/bash
|
|
731
|
+
set -euo pipefail
|
|
732
|
+
INPUT=$(cat)
|
|
733
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
734
|
+
[ -z "$FILE_PATH" ] && exit 0
|
|
735
|
+
|
|
736
|
+
EXPECTED=${quote(expected)}
|
|
737
|
+
BASENAME=$(basename "$FILE_PATH")
|
|
738
|
+
|
|
739
|
+
if echo "$EXPECTED" | tr ',' '\\n' | grep -qx "$BASENAME"; then
|
|
740
|
+
BYTES=$(wc -c < "$FILE_PATH" 2>/dev/null || echo 0)
|
|
741
|
+
printf '{"ts":"%s","event":"artifact_write","file":"%s","path":"%s","bytes":%s}\\n' \\
|
|
742
|
+
"$(date -u +%FT%TZ)" "$BASENAME" "$FILE_PATH" "$BYTES" >> ${quote(manifestFile)}
|
|
743
|
+
fi
|
|
744
|
+
|
|
745
|
+
printf '{"ts":"%s","event":"artifact_write","file":"%s","path":"%s","bytes":0}\\n' \\
|
|
746
|
+
"$(date -u +%FT%TZ)" "$BASENAME" "$FILE_PATH" >> ${quote(eventsFile)}
|
|
747
|
+
exit 0
|
|
748
|
+
`;
|
|
749
|
+
}
|
|
750
|
+
function buildPostArtifactScript(manifestFile, expected) {
|
|
751
|
+
return `#!/bin/bash
|
|
752
|
+
set -euo pipefail
|
|
753
|
+
INPUT=$(cat)
|
|
754
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
755
|
+
[ -z "$FILE_PATH" ] && exit 0
|
|
756
|
+
|
|
757
|
+
EXPECTED=${quote(expected)}
|
|
758
|
+
BASENAME=$(basename "$FILE_PATH")
|
|
759
|
+
|
|
760
|
+
if echo "$EXPECTED" | tr ',' '\\n' | grep -qx "$BASENAME"; then
|
|
761
|
+
BYTES=$(wc -c < "$FILE_PATH" 2>/dev/null || echo 0)
|
|
762
|
+
printf '{"ts":"%s","event":"write","file":"%s","path":"%s","bytes":%s}\\n' \\
|
|
763
|
+
"$(date -u +%FT%TZ)" "$BASENAME" "$FILE_PATH" "$BYTES" >> ${quote(manifestFile)}
|
|
764
|
+
fi
|
|
765
|
+
exit 0
|
|
766
|
+
`;
|
|
767
|
+
}
|
|
768
|
+
function buildExitPlanModeScript(eventsFile, planDir, planArtifact) {
|
|
769
|
+
return `#!/bin/bash
|
|
770
|
+
set -euo pipefail
|
|
771
|
+
INPUT=$(cat)
|
|
772
|
+
|
|
773
|
+
PLAN_DIR=${quote(planDir)}
|
|
774
|
+
PLAN_ARTIFACT=${quote(planArtifact ?? "01-plan.md")}
|
|
775
|
+
|
|
776
|
+
PLAN_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.plan // empty')
|
|
777
|
+
if [ -n "$PLAN_CONTENT" ]; then
|
|
778
|
+
mkdir -p "$PLAN_DIR"
|
|
779
|
+
printf '%s' "$PLAN_CONTENT" > "$PLAN_DIR/$PLAN_ARTIFACT"
|
|
780
|
+
BYTES=$(wc -c < "$PLAN_DIR/$PLAN_ARTIFACT")
|
|
781
|
+
printf '{"ts":"%s","event":"plan_captured","file":"%s","path":"%s/%s","bytes":%s}\\n' \\
|
|
782
|
+
"$(date -u +%FT%TZ)" "$PLAN_ARTIFACT" "$PLAN_DIR" "$PLAN_ARTIFACT" "$BYTES" >> ${quote(eventsFile)}
|
|
783
|
+
fi
|
|
784
|
+
|
|
785
|
+
printf '{"ts":"%s","event":"exit_plan_mode"}\\n' "$(date -u +%FT%TZ)" >> ${quote(eventsFile)}
|
|
786
|
+
|
|
787
|
+
echo '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
|
|
788
|
+
exit 0
|
|
789
|
+
`;
|
|
790
|
+
}
|
|
791
|
+
function buildPermissionScript(eventsFile) {
|
|
792
|
+
return `#!/bin/bash
|
|
793
|
+
set -euo pipefail
|
|
794
|
+
INPUT=$(cat)
|
|
795
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
|
|
796
|
+
printf '{"ts":"%s","event":"permission_request","tool":"%s"}\\n' \\
|
|
797
|
+
"$(date -u +%FT%TZ)" "$TOOL" >> ${quote(eventsFile)}
|
|
798
|
+
echo '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
|
|
799
|
+
exit 0
|
|
800
|
+
`;
|
|
801
|
+
}
|
|
802
|
+
function buildProtectFilesScript(eventsFile, _phaseName, _planDir) {
|
|
803
|
+
return `#!/bin/bash
|
|
804
|
+
set -euo pipefail
|
|
805
|
+
INPUT=$(cat)
|
|
806
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
|
|
807
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
808
|
+
[ -z "$FILE_PATH" ] && exit 0
|
|
809
|
+
|
|
810
|
+
BASENAME=$(basename "$FILE_PATH")
|
|
811
|
+
|
|
812
|
+
blocked_reason() {
|
|
813
|
+
printf '{"ts":"%s","event":"protect_blocked","tool":"%s","file":"%s"}\\n' \\
|
|
814
|
+
"$(date -u +%FT%TZ)" "$TOOL" "$BASENAME" >> ${quote(eventsFile)}
|
|
815
|
+
echo "$1" >&2
|
|
816
|
+
exit 2
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
case "$BASENAME" in
|
|
820
|
+
.env|.env.*)
|
|
821
|
+
blocked_reason "\u7981\u6B62\u4FEE\u6539\u73AF\u5883\u914D\u7F6E\u6587\u4EF6 $BASENAME\uFF0C\u8BF7\u901A\u8FC7 .env.example \u6216\u6587\u6863\u8BF4\u660E\u914D\u7F6E\u53D8\u66F4\u3002"
|
|
822
|
+
;;
|
|
823
|
+
package-lock.json|pnpm-lock.yaml)
|
|
824
|
+
blocked_reason "\u7981\u6B62\u76F4\u63A5\u4FEE\u6539\u9501\u6587\u4EF6 $BASENAME\uFF0C\u8BF7\u901A\u8FC7 npm install / pnpm install \u66F4\u65B0\u4F9D\u8D56\u3002"
|
|
825
|
+
;;
|
|
826
|
+
esac
|
|
827
|
+
|
|
828
|
+
exit 0
|
|
829
|
+
`;
|
|
830
|
+
}
|
|
831
|
+
function buildStopScript(eventsFile, planDir, phaseExpected) {
|
|
832
|
+
return `#!/bin/bash
|
|
833
|
+
set -euo pipefail
|
|
834
|
+
INPUT=$(cat)
|
|
835
|
+
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
|
|
836
|
+
|
|
837
|
+
PLAN_DIR=${quote(planDir)}
|
|
838
|
+
MIN_BYTES=50
|
|
839
|
+
PHASE_EXPECTED=${quote(phaseExpected)}
|
|
840
|
+
|
|
841
|
+
MISSING=""
|
|
842
|
+
READY=""
|
|
843
|
+
for f in $(echo "$PHASE_EXPECTED" | tr ',' ' '); do
|
|
844
|
+
[ -z "$f" ] && continue
|
|
845
|
+
FPATH="$PLAN_DIR/$f"
|
|
846
|
+
if [ -f "$FPATH" ] && [ "$(wc -c < "$FPATH")" -ge "$MIN_BYTES" ]; then
|
|
847
|
+
BYTES=$(wc -c < "$FPATH")
|
|
848
|
+
READY="$READY $f(\${BYTES} bytes)"
|
|
849
|
+
else
|
|
850
|
+
MISSING="$MISSING $f"
|
|
851
|
+
fi
|
|
852
|
+
done
|
|
853
|
+
|
|
854
|
+
MISSING=$(echo "$MISSING" | xargs)
|
|
855
|
+
READY=$(echo "$READY" | xargs)
|
|
856
|
+
|
|
857
|
+
if [ -n "$MISSING" ] && [ "$STOP_ACTIVE" != "true" ]; then
|
|
858
|
+
printf '{"ts":"%s","event":"stop","blocked":true,"missing":"%s"}\\n' \\
|
|
859
|
+
"$(date -u +%FT%TZ)" "$MISSING" >> ${quote(eventsFile)}
|
|
860
|
+
|
|
861
|
+
REASON="\u4EA7\u7269\u672A\u5C31\u7EEA: $MISSING\u3002\u8BF7\u5199\u5165 $PLAN_DIR/ \u4E0B\u7684\u5BF9\u5E94\u6587\u4EF6\u3002\u5DF2\u5C31\u7EEA: \${READY:-\u65E0}"
|
|
862
|
+
|
|
863
|
+
printf '{"decision":"block","reason":"%s"}' "$REASON"
|
|
864
|
+
exit 0
|
|
865
|
+
fi
|
|
866
|
+
|
|
867
|
+
printf '{"ts":"%s","event":"stop","blocked":false,"missing":"%s"}\\n' \\
|
|
868
|
+
"$(date -u +%FT%TZ)" "\${MISSING:-none}" >> ${quote(eventsFile)}
|
|
869
|
+
exit 0
|
|
870
|
+
`;
|
|
871
|
+
}
|
|
872
|
+
function buildAskUserHookScript(eventsFile) {
|
|
873
|
+
return `#!/bin/bash
|
|
874
|
+
set -euo pipefail
|
|
875
|
+
INPUT=$(cat)
|
|
876
|
+
QUESTION=$(echo "$INPUT" | jq -r '.tool_input.question // empty')
|
|
877
|
+
OPTIONS=$(echo "$INPUT" | jq -c '[.tool_input.options[]? | {index: .index, label: .label}] // []' 2>/dev/null || echo '[]')
|
|
878
|
+
[ -z "$QUESTION" ] && exit 0
|
|
879
|
+
printf '{"ts":"%s","event":"ask_user_question","question":"%s","options":%s}\\n' \\
|
|
880
|
+
"$(date -u +%FT%TZ)" "$(echo "$QUESTION" | head -c 500 | tr '"' "'")" "$OPTIONS" >> ${quote(eventsFile)}
|
|
881
|
+
exit 0
|
|
882
|
+
`;
|
|
883
|
+
}
|
|
884
|
+
function buildNotificationScript(eventsFile) {
|
|
885
|
+
return `#!/bin/bash
|
|
886
|
+
set -euo pipefail
|
|
887
|
+
INPUT=$(cat)
|
|
888
|
+
NTYPE=$(echo "$INPUT" | jq -r '.notification_type // empty')
|
|
889
|
+
MSG=$(echo "$INPUT" | jq -r '.message // empty' | head -c 200 | tr '"' "'")
|
|
890
|
+
[ -z "$NTYPE" ] && exit 0
|
|
891
|
+
printf '{"ts":"%s","event":"notification","notification_type":"%s","message":"%s"}\\n' \\
|
|
892
|
+
"$(date -u +%FT%TZ)" "$NTYPE" "$MSG" >> ${quote(eventsFile)}
|
|
893
|
+
exit 0
|
|
894
|
+
`;
|
|
895
|
+
}
|
|
896
|
+
function quote(s) {
|
|
897
|
+
return `"${s.replace(/"/g, '\\"')}"`;
|
|
898
|
+
}
|
|
899
|
+
function readJsonl(filePath) {
|
|
900
|
+
if (!fs3.existsSync(filePath)) return [];
|
|
901
|
+
try {
|
|
902
|
+
const content = fs3.readFileSync(filePath, "utf-8").trim();
|
|
903
|
+
if (!content) return [];
|
|
904
|
+
return content.split("\n").reduce((acc, line) => {
|
|
905
|
+
const trimmed = line.trim();
|
|
906
|
+
if (!trimmed) return acc;
|
|
907
|
+
try {
|
|
908
|
+
acc.push(JSON.parse(trimmed));
|
|
909
|
+
} catch {
|
|
910
|
+
logger5.debug("Skipping malformed JSONL line", { line: trimmed });
|
|
911
|
+
}
|
|
912
|
+
return acc;
|
|
913
|
+
}, []);
|
|
914
|
+
} catch {
|
|
915
|
+
return [];
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
203
919
|
// src/ai-runner/PtyRunner.ts
|
|
204
|
-
var
|
|
920
|
+
var logger6 = logger.child("PtyRunner");
|
|
205
921
|
var ANSI_RE = /\x1b\[[?><=]*[0-9;]*[a-zA-Z~]|\x1b\][^\x07]*\x07|\x1b\(B/g;
|
|
206
922
|
function stripAnsi(str) {
|
|
207
923
|
return str.replace(ANSI_RE, "");
|
|
@@ -340,6 +1056,34 @@ function isTuiNoise(line) {
|
|
|
340
1056
|
if (CLAUDE_BANNER_INFO_RE.test(t)) return true;
|
|
341
1057
|
return false;
|
|
342
1058
|
}
|
|
1059
|
+
var InputWaitController = class {
|
|
1060
|
+
constructor(totalBudgetMs) {
|
|
1061
|
+
this.totalBudgetMs = totalBudgetMs;
|
|
1062
|
+
this.wallClockSegmentStart = Date.now();
|
|
1063
|
+
}
|
|
1064
|
+
_waiting = false;
|
|
1065
|
+
wallClockUsedMs = 0;
|
|
1066
|
+
wallClockSegmentStart;
|
|
1067
|
+
get waiting() {
|
|
1068
|
+
return this._waiting;
|
|
1069
|
+
}
|
|
1070
|
+
pause() {
|
|
1071
|
+
if (this._waiting) return;
|
|
1072
|
+
this._waiting = true;
|
|
1073
|
+
this.wallClockUsedMs += Date.now() - this.wallClockSegmentStart;
|
|
1074
|
+
}
|
|
1075
|
+
resume() {
|
|
1076
|
+
if (!this._waiting) return;
|
|
1077
|
+
this._waiting = false;
|
|
1078
|
+
this.wallClockSegmentStart = Date.now();
|
|
1079
|
+
}
|
|
1080
|
+
get remainingMs() {
|
|
1081
|
+
return Math.max(this.totalBudgetMs - this.wallClockUsedMs, 6e4);
|
|
1082
|
+
}
|
|
1083
|
+
get usedMs() {
|
|
1084
|
+
return this.wallClockUsedMs;
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
343
1087
|
var PtyRunner = class {
|
|
344
1088
|
constructor(nvmNodeVersion, terminalManager, defaultAgentMode, phaseAgentMap, globalModel, idleDetectMs = 3e4) {
|
|
345
1089
|
this.nvmNodeVersion = nvmNodeVersion;
|
|
@@ -357,32 +1101,32 @@ var PtyRunner = class {
|
|
|
357
1101
|
// ---- AIRunner interface ---------------------------------------------------
|
|
358
1102
|
async run(options) {
|
|
359
1103
|
if (isShuttingDown()) {
|
|
360
|
-
|
|
1104
|
+
logger6.warn("PtyRunner skipped \u2014 service is shutting down");
|
|
361
1105
|
return { success: false, output: "Service shutting down", exitCode: null };
|
|
362
1106
|
}
|
|
363
1107
|
const { prompt, workDir, timeoutMs, onStreamEvent, phaseName } = options;
|
|
364
1108
|
const agentMode = this.resolveAgentForPhase(phaseName);
|
|
365
1109
|
const startMode = options.mode;
|
|
366
1110
|
const continueSession = options.continueSession ?? false;
|
|
367
|
-
|
|
1111
|
+
logger6.info("PtyRunner.run()", { workDir, timeoutMs, phaseName, agentMode, continueSession });
|
|
368
1112
|
const { sessionId, isNew } = this.ensureSession(workDir, agentMode, startMode);
|
|
369
1113
|
if (isNew) {
|
|
370
|
-
|
|
1114
|
+
logger6.info("Waiting for AI agent prompt (new session)", { sessionId, phaseName });
|
|
371
1115
|
await this.waitForPrompt(sessionId, 3e5);
|
|
372
1116
|
} else if (continueSession) {
|
|
373
|
-
|
|
1117
|
+
logger6.info("Waiting for AI agent prompt (continue-session)", { sessionId, phaseName });
|
|
374
1118
|
await this.waitForPrompt(sessionId, 3e4);
|
|
375
1119
|
} else {
|
|
376
|
-
|
|
1120
|
+
logger6.info("Waiting for AI agent prompt (reused session)", { sessionId, phaseName });
|
|
377
1121
|
await this.waitForPrompt(sessionId, 1e4);
|
|
378
1122
|
}
|
|
379
1123
|
if (startMode === "plan" && this.shouldUseNativePlan(agentMode)) {
|
|
380
1124
|
return this.runNativePlanMode(sessionId, isNew, options, agentMode, workDir);
|
|
381
1125
|
}
|
|
382
1126
|
if (continueSession && !isNew) {
|
|
383
|
-
|
|
1127
|
+
logger6.info("Continue-session mode: attaching detectCompletion (no /clear)", { sessionId });
|
|
384
1128
|
const result2 = await this.detectCompletion(sessionId, options, onStreamEvent);
|
|
385
|
-
|
|
1129
|
+
logger6.info("PtyRunner continue-session completed", {
|
|
386
1130
|
workDir,
|
|
387
1131
|
agentMode,
|
|
388
1132
|
phaseName,
|
|
@@ -398,7 +1142,7 @@ var PtyRunner = class {
|
|
|
398
1142
|
const instruction = `Please read and follow all instructions in ${promptFile}`;
|
|
399
1143
|
await this.writeCommand(sessionId, instruction, agentMode);
|
|
400
1144
|
const result = await this.detectCompletion(sessionId, options, onStreamEvent);
|
|
401
|
-
|
|
1145
|
+
logger6.info("PtyRunner phase completed", {
|
|
402
1146
|
workDir,
|
|
403
1147
|
agentMode,
|
|
404
1148
|
phaseName,
|
|
@@ -413,7 +1157,7 @@ var PtyRunner = class {
|
|
|
413
1157
|
this.terminalManager.destroy(info.sessionId);
|
|
414
1158
|
}
|
|
415
1159
|
this.sessions.clear();
|
|
416
|
-
|
|
1160
|
+
logger6.info("PtyRunner: all managed sessions destroyed");
|
|
417
1161
|
}
|
|
418
1162
|
killByWorkDir(targetWorkDir) {
|
|
419
1163
|
const info = this.sessions.get(targetWorkDir);
|
|
@@ -432,7 +1176,7 @@ var PtyRunner = class {
|
|
|
432
1176
|
return false;
|
|
433
1177
|
}
|
|
434
1178
|
this.terminalManager.write(info.sessionId, "");
|
|
435
|
-
|
|
1179
|
+
logger6.info("Interrupted PTY session for retry", {
|
|
436
1180
|
workDir: targetWorkDir,
|
|
437
1181
|
sessionId: info.sessionId
|
|
438
1182
|
});
|
|
@@ -489,7 +1233,7 @@ var PtyRunner = class {
|
|
|
489
1233
|
ensureSession(workDir, agentMode, startMode) {
|
|
490
1234
|
const existing = this.sessions.get(workDir);
|
|
491
1235
|
if (existing && existing.agentMode !== agentMode) {
|
|
492
|
-
|
|
1236
|
+
logger6.info("Agent switched, destroying old PTY session", {
|
|
493
1237
|
workDir,
|
|
494
1238
|
oldAgent: existing.agentMode,
|
|
495
1239
|
newAgent: agentMode,
|
|
@@ -500,7 +1244,7 @@ var PtyRunner = class {
|
|
|
500
1244
|
}
|
|
501
1245
|
if (existing && existing.agentMode === agentMode) {
|
|
502
1246
|
if (this.terminalManager.get(existing.sessionId)) {
|
|
503
|
-
|
|
1247
|
+
logger6.info("Reusing existing PTY session (same agent)", {
|
|
504
1248
|
workDir,
|
|
505
1249
|
agentMode,
|
|
506
1250
|
sessionId: existing.sessionId
|
|
@@ -511,7 +1255,7 @@ var PtyRunner = class {
|
|
|
511
1255
|
}
|
|
512
1256
|
const orphan = this.terminalManager.findByWorkDir(workDir);
|
|
513
1257
|
if (orphan) {
|
|
514
|
-
|
|
1258
|
+
logger6.info("Destroying orphaned PTY session on workDir", {
|
|
515
1259
|
workDir,
|
|
516
1260
|
sessionId: orphan.id
|
|
517
1261
|
});
|
|
@@ -535,7 +1279,7 @@ var PtyRunner = class {
|
|
|
535
1279
|
currentMode: profile.defaultModeName ?? "bypass",
|
|
536
1280
|
startedWithMode: startMode
|
|
537
1281
|
});
|
|
538
|
-
|
|
1282
|
+
logger6.info("Created new PTY session", {
|
|
539
1283
|
workDir,
|
|
540
1284
|
agentMode,
|
|
541
1285
|
binary,
|
|
@@ -567,7 +1311,7 @@ var PtyRunner = class {
|
|
|
567
1311
|
if (stabilityTimer) clearTimeout(stabilityTimer);
|
|
568
1312
|
if (silenceTimer) clearTimeout(silenceTimer);
|
|
569
1313
|
subscription.dispose();
|
|
570
|
-
|
|
1314
|
+
logger6.warn("Timed out waiting for AI agent prompt", { sessionId, timeoutMs });
|
|
571
1315
|
resolve();
|
|
572
1316
|
}, timeoutMs);
|
|
573
1317
|
const done = (reason) => {
|
|
@@ -575,7 +1319,7 @@ var PtyRunner = class {
|
|
|
575
1319
|
if (stabilityTimer) clearTimeout(stabilityTimer);
|
|
576
1320
|
if (silenceTimer) clearTimeout(silenceTimer);
|
|
577
1321
|
subscription.dispose();
|
|
578
|
-
|
|
1322
|
+
logger6.info("AI agent prompt detected", { sessionId, reason });
|
|
579
1323
|
resolve();
|
|
580
1324
|
};
|
|
581
1325
|
const resetSilenceTimer = () => {
|
|
@@ -583,7 +1327,7 @@ var PtyRunner = class {
|
|
|
583
1327
|
if (silenceTimer) clearTimeout(silenceTimer);
|
|
584
1328
|
silenceTimer = setTimeout(() => {
|
|
585
1329
|
if (promptSeen) return;
|
|
586
|
-
|
|
1330
|
+
logger6.info("Banner shown and PTY silent \u2014 treating agent as ready", { sessionId });
|
|
587
1331
|
done("silence-after-banner");
|
|
588
1332
|
}, SILENCE_READY_MS);
|
|
589
1333
|
};
|
|
@@ -597,11 +1341,11 @@ var PtyRunner = class {
|
|
|
597
1341
|
resetSilenceTimer();
|
|
598
1342
|
if (!trustDialogHandled && TRUST_DIALOG_RE.test(stripped)) {
|
|
599
1343
|
trustDialogHandled = true;
|
|
600
|
-
|
|
1344
|
+
logger6.info("Trust dialog detected, auto-confirming", { sessionId });
|
|
601
1345
|
setTimeout(() => this.terminalManager.write(sessionId, "\r"), 500);
|
|
602
1346
|
}
|
|
603
1347
|
if (PERMISSION_DIALOG_RE.test(stripped)) {
|
|
604
|
-
|
|
1348
|
+
logger6.info("Permission dialog detected in waitForPrompt, auto-confirming", { sessionId });
|
|
605
1349
|
setTimeout(() => this.terminalManager.write(sessionId, "\r"), 500);
|
|
606
1350
|
}
|
|
607
1351
|
if (isIdlePrompt(stripped)) {
|
|
@@ -636,7 +1380,7 @@ var PtyRunner = class {
|
|
|
636
1380
|
if (!session) return;
|
|
637
1381
|
const targetMode = wantPlan ? profile.planModeName : profile.defaultModeName ?? "bypass";
|
|
638
1382
|
if (session.currentMode === targetMode) {
|
|
639
|
-
|
|
1383
|
+
logger6.info("PTY already in target mode", { sessionId, targetMode });
|
|
640
1384
|
return;
|
|
641
1385
|
}
|
|
642
1386
|
const MAX_ATTEMPTS = 5;
|
|
@@ -648,7 +1392,7 @@ var PtyRunner = class {
|
|
|
648
1392
|
if (detected) {
|
|
649
1393
|
session.currentMode = detected;
|
|
650
1394
|
if (detected === targetMode) {
|
|
651
|
-
|
|
1395
|
+
logger6.info("PTY mode switched", {
|
|
652
1396
|
sessionId,
|
|
653
1397
|
agentMode,
|
|
654
1398
|
targetMode,
|
|
@@ -658,7 +1402,7 @@ var PtyRunner = class {
|
|
|
658
1402
|
}
|
|
659
1403
|
}
|
|
660
1404
|
}
|
|
661
|
-
|
|
1405
|
+
logger6.warn("Failed to switch PTY mode after max attempts", {
|
|
662
1406
|
sessionId,
|
|
663
1407
|
agentMode,
|
|
664
1408
|
targetMode,
|
|
@@ -712,9 +1456,9 @@ var PtyRunner = class {
|
|
|
712
1456
|
const issueIid = extractIidFromPath(workDir);
|
|
713
1457
|
const contentHint = issueIid ? PlanFileResolver.buildContentHint(issueIid) : void 0;
|
|
714
1458
|
if (continueSession) {
|
|
715
|
-
|
|
1459
|
+
logger6.info("Native plan mode: continue-session (no /clear, no prompt)", { sessionId });
|
|
716
1460
|
} else {
|
|
717
|
-
|
|
1461
|
+
logger6.info("Native plan mode: switching to plan", { sessionId, agentMode });
|
|
718
1462
|
await this.ensurePlanMode(sessionId, agentMode, true, workDir);
|
|
719
1463
|
if (!isNew) {
|
|
720
1464
|
await this.writeCommand(sessionId, "/clear", agentMode);
|
|
@@ -734,47 +1478,62 @@ var PtyRunner = class {
|
|
|
734
1478
|
artifactCheck: planArtifactCheck
|
|
735
1479
|
}, options.onStreamEvent, continueSession);
|
|
736
1480
|
if (planResult.timedOut) {
|
|
737
|
-
|
|
1481
|
+
logger6.warn("Native plan mode: plan phase timed out", {
|
|
738
1482
|
sessionId,
|
|
739
1483
|
wasActive: planResult.wasActiveAtTimeout
|
|
740
1484
|
});
|
|
741
1485
|
return this.buildRunResult(planResult, sessionId);
|
|
742
1486
|
}
|
|
743
|
-
logger3.info("Native plan mode: resolving plan file from CLI storage", { sessionId });
|
|
744
|
-
const resolved = resolver.resolve(contentHint);
|
|
745
|
-
if (!resolved) {
|
|
746
|
-
logger3.error("Native plan mode: no plan file found in CLI storage", { sessionId, workDir });
|
|
747
|
-
return {
|
|
748
|
-
success: false,
|
|
749
|
-
output: planResult.output,
|
|
750
|
-
errorMessage: "Plan \u9636\u6BB5\u5B8C\u6210\u4F46\u672A\u5728 CLI \u8BA1\u5212\u76EE\u5F55\u4E2D\u627E\u5230\u8BA1\u5212\u6587\u4EF6",
|
|
751
|
-
sessionId,
|
|
752
|
-
exitCode: null
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
1487
|
const artifactPaths = options.artifactPaths ?? [];
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
fs2.writeFileSync(targetPath, resolved.content, "utf-8");
|
|
763
|
-
logger3.info("Plan file copied to artifact path", {
|
|
764
|
-
source: path2.basename(resolved.sourcePath),
|
|
765
|
-
target: targetPath,
|
|
766
|
-
size: resolved.content.length
|
|
767
|
-
});
|
|
1488
|
+
const MIN_PLAN_BYTES = 50;
|
|
1489
|
+
const hookHandled = artifactPaths.length > 0 && artifactPaths.every((p) => {
|
|
1490
|
+
try {
|
|
1491
|
+
return fs4.existsSync(p) && fs4.statSync(p).size >= MIN_PLAN_BYTES;
|
|
1492
|
+
} catch {
|
|
1493
|
+
return false;
|
|
768
1494
|
}
|
|
769
|
-
}
|
|
770
|
-
|
|
1495
|
+
});
|
|
1496
|
+
if (hookHandled) {
|
|
1497
|
+
logger6.info("Native plan mode: plan captured by ExitPlanMode hook", {
|
|
771
1498
|
sessionId,
|
|
772
|
-
|
|
1499
|
+
artifactPaths
|
|
773
1500
|
});
|
|
1501
|
+
} else {
|
|
1502
|
+
logger6.info("Native plan mode: resolving plan file from CLI storage (fallback)", { sessionId });
|
|
1503
|
+
const resolved = resolver.resolve(contentHint);
|
|
1504
|
+
if (!resolved) {
|
|
1505
|
+
logger6.error("Native plan mode: no plan file found in CLI storage", { sessionId, workDir });
|
|
1506
|
+
return {
|
|
1507
|
+
success: false,
|
|
1508
|
+
output: planResult.output,
|
|
1509
|
+
errorMessage: "Plan \u9636\u6BB5\u5B8C\u6210\u4F46\u672A\u5728 CLI \u8BA1\u5212\u76EE\u5F55\u4E2D\u627E\u5230\u8BA1\u5212\u6587\u4EF6",
|
|
1510
|
+
sessionId,
|
|
1511
|
+
exitCode: null
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
if (artifactPaths.length > 0) {
|
|
1515
|
+
for (const targetPath of artifactPaths) {
|
|
1516
|
+
const targetDir = path3.dirname(targetPath);
|
|
1517
|
+
if (!fs4.existsSync(targetDir)) {
|
|
1518
|
+
fs4.mkdirSync(targetDir, { recursive: true });
|
|
1519
|
+
}
|
|
1520
|
+
fs4.writeFileSync(targetPath, resolved.content, "utf-8");
|
|
1521
|
+
logger6.info("Plan file copied to artifact path", {
|
|
1522
|
+
source: path3.basename(resolved.sourcePath),
|
|
1523
|
+
target: targetPath,
|
|
1524
|
+
size: resolved.content.length
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
} else {
|
|
1528
|
+
logger6.warn("Native plan mode: no artifactPaths specified, plan file not copied", {
|
|
1529
|
+
sessionId,
|
|
1530
|
+
resolvedFile: resolved.sourcePath
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
774
1533
|
}
|
|
775
|
-
|
|
1534
|
+
logger6.info("Native plan mode completed", {
|
|
776
1535
|
sessionId,
|
|
777
|
-
|
|
1536
|
+
hookHandled,
|
|
778
1537
|
artifactsCopied: artifactPaths.length
|
|
779
1538
|
});
|
|
780
1539
|
return this.buildRunResult(planResult, sessionId);
|
|
@@ -792,13 +1551,19 @@ var PtyRunner = class {
|
|
|
792
1551
|
};
|
|
793
1552
|
}
|
|
794
1553
|
writePromptFile(workDir, prompt) {
|
|
795
|
-
const dir =
|
|
796
|
-
if (!
|
|
797
|
-
|
|
1554
|
+
const dir = path3.join(workDir, ".claude-plan");
|
|
1555
|
+
if (!fs4.existsSync(dir)) {
|
|
1556
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
1557
|
+
}
|
|
1558
|
+
const gitignorePath = path3.join(dir, ".gitignore");
|
|
1559
|
+
const entry = ".phase-prompt.md";
|
|
1560
|
+
if (!fs4.existsSync(gitignorePath) || !fs4.readFileSync(gitignorePath, "utf-8").includes(entry)) {
|
|
1561
|
+
fs4.appendFileSync(gitignorePath, `${entry}
|
|
1562
|
+
`, "utf-8");
|
|
798
1563
|
}
|
|
799
1564
|
const relPath = ".claude-plan/.phase-prompt.md";
|
|
800
|
-
const absPath =
|
|
801
|
-
|
|
1565
|
+
const absPath = path3.join(workDir, relPath);
|
|
1566
|
+
fs4.writeFileSync(absPath, prompt, "utf-8");
|
|
802
1567
|
return relPath;
|
|
803
1568
|
}
|
|
804
1569
|
// ---- Completion detection -------------------------------------------------
|
|
@@ -818,6 +1583,27 @@ var PtyRunner = class {
|
|
|
818
1583
|
let pendingDialogParsed = null;
|
|
819
1584
|
const idleTimeoutMs = options.idleTimeoutMs ?? 6e5;
|
|
820
1585
|
const timeoutMs = options.timeoutMs;
|
|
1586
|
+
const inputWait = new InputWaitController(timeoutMs);
|
|
1587
|
+
const pauseTimersForInput = () => {
|
|
1588
|
+
if (inputWait.waiting) return;
|
|
1589
|
+
inputWait.pause();
|
|
1590
|
+
clearTimeout(wallTimer);
|
|
1591
|
+
logger6.info("Timers paused \u2014 waiting for user input", {
|
|
1592
|
+
sessionId,
|
|
1593
|
+
wallClockUsedMs: inputWait.usedMs
|
|
1594
|
+
});
|
|
1595
|
+
};
|
|
1596
|
+
const resumeTimersAfterInput = () => {
|
|
1597
|
+
if (!inputWait.waiting) return;
|
|
1598
|
+
inputWait.resume();
|
|
1599
|
+
lastOutputTime = Date.now();
|
|
1600
|
+
wallTimer = scheduleWallTimer(inputWait.remainingMs);
|
|
1601
|
+
logger6.info("Timers resumed after user input", {
|
|
1602
|
+
sessionId,
|
|
1603
|
+
remainingMs: inputWait.remainingMs,
|
|
1604
|
+
wallClockUsedMs: inputWait.usedMs
|
|
1605
|
+
});
|
|
1606
|
+
};
|
|
821
1607
|
const GRACE_WINDOW_MS = options.timeoutGraceMs ?? 6e4;
|
|
822
1608
|
const EXTENSION_MS = options.timeoutExtensionMs ?? 6e5;
|
|
823
1609
|
const MAX_EXTENSIONS = options.timeoutMaxExtensions ?? 3;
|
|
@@ -830,11 +1616,12 @@ var PtyRunner = class {
|
|
|
830
1616
|
};
|
|
831
1617
|
const scheduleWallTimer = (delayMs) => {
|
|
832
1618
|
return setTimeout(() => {
|
|
1619
|
+
if (inputWait.waiting) return;
|
|
833
1620
|
const recentMs = Date.now() - lastOutputTime;
|
|
834
1621
|
const isActive = hasSubstantiveOutput && recentMs < GRACE_WINDOW_MS;
|
|
835
1622
|
if (isActive && extensions < MAX_EXTENSIONS) {
|
|
836
1623
|
extensions++;
|
|
837
|
-
|
|
1624
|
+
logger6.info("Wall-clock timeout extended (agent still active)", {
|
|
838
1625
|
sessionId,
|
|
839
1626
|
extensions,
|
|
840
1627
|
maxExtensions: MAX_EXTENSIONS,
|
|
@@ -853,6 +1640,7 @@ var PtyRunner = class {
|
|
|
853
1640
|
};
|
|
854
1641
|
let wallTimer = scheduleWallTimer(timeoutMs);
|
|
855
1642
|
const idleCheck = setInterval(() => {
|
|
1643
|
+
if (inputWait.waiting) return;
|
|
856
1644
|
if (!hasSubstantiveOutput) return;
|
|
857
1645
|
if (Date.now() - lastOutputTime >= idleTimeoutMs) {
|
|
858
1646
|
finish({
|
|
@@ -863,6 +1651,52 @@ var PtyRunner = class {
|
|
|
863
1651
|
});
|
|
864
1652
|
}
|
|
865
1653
|
}, 5e3);
|
|
1654
|
+
const dialogClassifier = new DialogClassifier();
|
|
1655
|
+
let hookWatcher;
|
|
1656
|
+
let hookSub;
|
|
1657
|
+
const hookInjector = new HookInjector();
|
|
1658
|
+
const eventsFilePath = hookInjector.getEventsFilePath(options.workDir);
|
|
1659
|
+
if (fs4.existsSync(eventsFilePath)) {
|
|
1660
|
+
hookWatcher = new HookEventWatcher(eventsFilePath);
|
|
1661
|
+
hookSub = hookWatcher.onEvent((event) => {
|
|
1662
|
+
dialogClassifier.ingestHookEvent(event);
|
|
1663
|
+
if (event.event === "ask_user_question" && options.onInputRequired && !dialogHandled && !resolved) {
|
|
1664
|
+
const askData = dialogClassifier.consumePendingAskUser();
|
|
1665
|
+
if (askData) {
|
|
1666
|
+
dialogHandled = true;
|
|
1667
|
+
dialogBuffer.length = 0;
|
|
1668
|
+
if (dialogQuiesceTimer) {
|
|
1669
|
+
clearTimeout(dialogQuiesceTimer);
|
|
1670
|
+
dialogQuiesceTimer = void 0;
|
|
1671
|
+
}
|
|
1672
|
+
pauseTimersForInput();
|
|
1673
|
+
logger6.info("AskUserQuestion detected via hook (high confidence, zero delay)", {
|
|
1674
|
+
sessionId,
|
|
1675
|
+
question: askData.question.slice(0, 80),
|
|
1676
|
+
optionCount: askData.options.length
|
|
1677
|
+
});
|
|
1678
|
+
options.onInputRequired({
|
|
1679
|
+
type: "interactive-dialog",
|
|
1680
|
+
content: askData.question,
|
|
1681
|
+
options: askData.options
|
|
1682
|
+
}).then((response) => {
|
|
1683
|
+
resumeTimersAfterInput();
|
|
1684
|
+
dialogHandled = false;
|
|
1685
|
+
if (!resolved && response) {
|
|
1686
|
+
this.terminalManager.write(sessionId, response + "\r");
|
|
1687
|
+
}
|
|
1688
|
+
}).catch((err) => {
|
|
1689
|
+
logger6.warn("onInputRequired callback failed (hook-based)", {
|
|
1690
|
+
error: err.message
|
|
1691
|
+
});
|
|
1692
|
+
resumeTimersAfterInput();
|
|
1693
|
+
dialogHandled = false;
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
});
|
|
1698
|
+
hookWatcher.start();
|
|
1699
|
+
}
|
|
866
1700
|
const subscription = this.terminalManager.onData(sessionId, (data) => {
|
|
867
1701
|
if (resolved) return;
|
|
868
1702
|
const stripped = stripAnsi(data);
|
|
@@ -877,7 +1711,7 @@ var PtyRunner = class {
|
|
|
877
1711
|
clearTimeout(dialogQuiesceTimer);
|
|
878
1712
|
dialogQuiesceTimer = void 0;
|
|
879
1713
|
pendingDialogParsed = null;
|
|
880
|
-
|
|
1714
|
+
logger6.info("Dialog quiesce cancelled \u2014 new substantive output arrived", { sessionId });
|
|
881
1715
|
}
|
|
882
1716
|
}
|
|
883
1717
|
if (!echoConsumed && stripped.includes(".phase-prompt.md")) {
|
|
@@ -888,27 +1722,27 @@ var PtyRunner = class {
|
|
|
888
1722
|
return;
|
|
889
1723
|
}
|
|
890
1724
|
if (options.completionSignal && hasSubstantiveOutput && options.completionSignal.test(stripped)) {
|
|
891
|
-
|
|
1725
|
+
logger6.info("Completion signal detected", { sessionId });
|
|
892
1726
|
finish({ output: outputLines.join(""), timedOut: false });
|
|
893
1727
|
return;
|
|
894
1728
|
}
|
|
895
1729
|
if (hasSubstantiveOutput && WORKED_SUMMARY_RE.test(stripped)) {
|
|
896
1730
|
const artifactReady = options.artifactCheck ? options.artifactCheck() : true;
|
|
897
1731
|
if (artifactReady) {
|
|
898
|
-
|
|
1732
|
+
logger6.info("Session-end summary detected, finishing", { sessionId });
|
|
899
1733
|
finish({ output: outputLines.join(""), timedOut: false });
|
|
900
1734
|
return;
|
|
901
1735
|
}
|
|
902
|
-
|
|
1736
|
+
logger6.info("Session summary detected but artifacts not ready, continuing", { sessionId });
|
|
903
1737
|
}
|
|
904
1738
|
if (PERMISSION_DIALOG_RE.test(stripped)) {
|
|
905
|
-
|
|
1739
|
+
logger6.info("Permission dialog detected, auto-confirming", { sessionId });
|
|
906
1740
|
setTimeout(() => {
|
|
907
1741
|
if (!resolved) this.terminalManager.write(sessionId, "\r");
|
|
908
1742
|
}, 500);
|
|
909
1743
|
return;
|
|
910
1744
|
}
|
|
911
|
-
if (options.onInputRequired && !dialogHandled && isInteractiveDialog(stripped)) {
|
|
1745
|
+
if (options.onInputRequired && !dialogHandled && !dialogClassifier.shouldSuppressRegex() && isInteractiveDialog(stripped)) {
|
|
912
1746
|
const parsed = parseInteractiveDialog(stripped);
|
|
913
1747
|
if (parsed) {
|
|
914
1748
|
const confidence = getDialogConfidence(stripped);
|
|
@@ -919,7 +1753,8 @@ var PtyRunner = class {
|
|
|
919
1753
|
clearTimeout(dialogQuiesceTimer);
|
|
920
1754
|
dialogQuiesceTimer = void 0;
|
|
921
1755
|
}
|
|
922
|
-
|
|
1756
|
+
pauseTimersForInput();
|
|
1757
|
+
logger6.info("Interactive dialog detected (high confidence), forwarding to handler", {
|
|
923
1758
|
sessionId,
|
|
924
1759
|
question: parsed.question.slice(0, 80),
|
|
925
1760
|
optionCount: parsed.options.length
|
|
@@ -929,21 +1764,23 @@ var PtyRunner = class {
|
|
|
929
1764
|
content: parsed.question,
|
|
930
1765
|
options: parsed.options
|
|
931
1766
|
}).then((response) => {
|
|
1767
|
+
resumeTimersAfterInput();
|
|
932
1768
|
dialogHandled = false;
|
|
933
1769
|
if (!resolved && response) {
|
|
934
1770
|
this.terminalManager.write(sessionId, response + "\r");
|
|
935
1771
|
}
|
|
936
1772
|
}).catch((err) => {
|
|
937
|
-
|
|
1773
|
+
logger6.warn("onInputRequired callback failed for interactive dialog", {
|
|
938
1774
|
error: err.message
|
|
939
1775
|
});
|
|
1776
|
+
resumeTimersAfterInput();
|
|
940
1777
|
dialogHandled = false;
|
|
941
1778
|
});
|
|
942
1779
|
return;
|
|
943
1780
|
}
|
|
944
1781
|
if (!dialogQuiesceTimer) {
|
|
945
1782
|
pendingDialogParsed = parsed;
|
|
946
|
-
|
|
1783
|
+
logger6.info("Interactive dialog detected (low confidence), starting quiesce", {
|
|
947
1784
|
sessionId,
|
|
948
1785
|
question: parsed.question.slice(0, 80),
|
|
949
1786
|
optionCount: parsed.options.length
|
|
@@ -955,7 +1792,8 @@ var PtyRunner = class {
|
|
|
955
1792
|
dialogBuffer.length = 0;
|
|
956
1793
|
const dp = pendingDialogParsed;
|
|
957
1794
|
pendingDialogParsed = null;
|
|
958
|
-
|
|
1795
|
+
pauseTimersForInput();
|
|
1796
|
+
logger6.info("Dialog quiesce elapsed \u2014 forwarding low-confidence dialog", {
|
|
959
1797
|
sessionId,
|
|
960
1798
|
question: dp.question.slice(0, 80)
|
|
961
1799
|
});
|
|
@@ -964,14 +1802,16 @@ var PtyRunner = class {
|
|
|
964
1802
|
content: dp.question,
|
|
965
1803
|
options: dp.options
|
|
966
1804
|
}).then((response) => {
|
|
1805
|
+
resumeTimersAfterInput();
|
|
967
1806
|
dialogHandled = false;
|
|
968
1807
|
if (!resolved && response) {
|
|
969
1808
|
this.terminalManager.write(sessionId, response + "\r");
|
|
970
1809
|
}
|
|
971
1810
|
}).catch((err) => {
|
|
972
|
-
|
|
1811
|
+
logger6.warn("onInputRequired callback failed (quiesced dialog)", {
|
|
973
1812
|
error: err.message
|
|
974
1813
|
});
|
|
1814
|
+
resumeTimersAfterInput();
|
|
975
1815
|
dialogHandled = false;
|
|
976
1816
|
});
|
|
977
1817
|
}, DIALOG_QUIESCE_MS);
|
|
@@ -1005,10 +1845,10 @@ var PtyRunner = class {
|
|
|
1005
1845
|
if (hasSubstantiveOutput && isIdlePrompt(stripped)) {
|
|
1006
1846
|
if (isMixedFrame) {
|
|
1007
1847
|
} else if (options.completionSignal) {
|
|
1008
|
-
|
|
1848
|
+
logger6.debug("Idle prompt ignored (waiting for completionSignal)", { sessionId });
|
|
1009
1849
|
} else if (debounceTimer && isNoise) {
|
|
1010
1850
|
} else {
|
|
1011
|
-
if (options.onInputRequired && !dialogHandled && dialogBuffer.length >= 2) {
|
|
1851
|
+
if (options.onInputRequired && !dialogHandled && !dialogClassifier.shouldSuppressRegex() && dialogBuffer.length >= 2) {
|
|
1012
1852
|
const combined = dialogBuffer.join("\n");
|
|
1013
1853
|
if (isInteractiveDialog(combined)) {
|
|
1014
1854
|
const parsed = parseInteractiveDialog(combined);
|
|
@@ -1021,7 +1861,8 @@ var PtyRunner = class {
|
|
|
1021
1861
|
clearTimeout(dialogQuiesceTimer);
|
|
1022
1862
|
dialogQuiesceTimer = void 0;
|
|
1023
1863
|
}
|
|
1024
|
-
|
|
1864
|
+
pauseTimersForInput();
|
|
1865
|
+
logger6.info("Interactive dialog detected via accumulated buffer (high confidence)", {
|
|
1025
1866
|
sessionId,
|
|
1026
1867
|
question: parsed.question.slice(0, 80),
|
|
1027
1868
|
optionCount: parsed.options.length
|
|
@@ -1031,21 +1872,23 @@ var PtyRunner = class {
|
|
|
1031
1872
|
content: parsed.question,
|
|
1032
1873
|
options: parsed.options
|
|
1033
1874
|
}).then((response) => {
|
|
1875
|
+
resumeTimersAfterInput();
|
|
1034
1876
|
dialogHandled = false;
|
|
1035
1877
|
if (!resolved && response) {
|
|
1036
1878
|
this.terminalManager.write(sessionId, response + "\r");
|
|
1037
1879
|
}
|
|
1038
1880
|
}).catch((err) => {
|
|
1039
|
-
|
|
1881
|
+
logger6.warn("onInputRequired callback failed (accumulated)", {
|
|
1040
1882
|
error: err.message
|
|
1041
1883
|
});
|
|
1884
|
+
resumeTimersAfterInput();
|
|
1042
1885
|
dialogHandled = false;
|
|
1043
1886
|
});
|
|
1044
1887
|
return;
|
|
1045
1888
|
}
|
|
1046
1889
|
if (!dialogQuiesceTimer) {
|
|
1047
1890
|
pendingDialogParsed = parsed;
|
|
1048
|
-
|
|
1891
|
+
logger6.info("Dialog detected via buffer (low confidence), starting quiesce", {
|
|
1049
1892
|
sessionId,
|
|
1050
1893
|
question: parsed.question.slice(0, 80)
|
|
1051
1894
|
});
|
|
@@ -1056,20 +1899,23 @@ var PtyRunner = class {
|
|
|
1056
1899
|
dialogBuffer.length = 0;
|
|
1057
1900
|
const dp = pendingDialogParsed;
|
|
1058
1901
|
pendingDialogParsed = null;
|
|
1059
|
-
|
|
1902
|
+
pauseTimersForInput();
|
|
1903
|
+
logger6.info("Buffer dialog quiesce elapsed \u2014 forwarding", { sessionId });
|
|
1060
1904
|
options.onInputRequired({
|
|
1061
1905
|
type: "interactive-dialog",
|
|
1062
1906
|
content: dp.question,
|
|
1063
1907
|
options: dp.options
|
|
1064
1908
|
}).then((response) => {
|
|
1909
|
+
resumeTimersAfterInput();
|
|
1065
1910
|
dialogHandled = false;
|
|
1066
1911
|
if (!resolved && response) {
|
|
1067
1912
|
this.terminalManager.write(sessionId, response + "\r");
|
|
1068
1913
|
}
|
|
1069
1914
|
}).catch((err) => {
|
|
1070
|
-
|
|
1915
|
+
logger6.warn("onInputRequired callback failed (buffer quiesced)", {
|
|
1071
1916
|
error: err.message
|
|
1072
1917
|
});
|
|
1918
|
+
resumeTimersAfterInput();
|
|
1073
1919
|
dialogHandled = false;
|
|
1074
1920
|
});
|
|
1075
1921
|
}, DIALOG_QUIESCE_MS);
|
|
@@ -1088,7 +1934,7 @@ var PtyRunner = class {
|
|
|
1088
1934
|
}
|
|
1089
1935
|
const artifactReady = options.artifactCheck ? options.artifactCheck() : true;
|
|
1090
1936
|
if (!artifactReady) {
|
|
1091
|
-
|
|
1937
|
+
logger6.info("Idle prompt detected but artifacts not ready, continuing to wait", {
|
|
1092
1938
|
sessionId
|
|
1093
1939
|
});
|
|
1094
1940
|
scheduleDebounce();
|
|
@@ -1114,14 +1960,14 @@ var PtyRunner = class {
|
|
|
1114
1960
|
}
|
|
1115
1961
|
}
|
|
1116
1962
|
if (!resolved) {
|
|
1117
|
-
|
|
1963
|
+
logger6.warn("PTY process exited during phase", { sessionId, exitCode, wasKilled });
|
|
1118
1964
|
finish({
|
|
1119
1965
|
output: outputLines.join(""),
|
|
1120
1966
|
timedOut: wasKilled,
|
|
1121
1967
|
timeoutType: wasKilled ? "wall-clock" : void 0
|
|
1122
1968
|
});
|
|
1123
1969
|
} else {
|
|
1124
|
-
|
|
1970
|
+
logger6.info("PTY exited after phase completion (post stop-hook)", { sessionId, exitCode });
|
|
1125
1971
|
}
|
|
1126
1972
|
});
|
|
1127
1973
|
const cleanup = () => {
|
|
@@ -1130,6 +1976,9 @@ var PtyRunner = class {
|
|
|
1130
1976
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1131
1977
|
if (dialogQuiesceTimer) clearTimeout(dialogQuiesceTimer);
|
|
1132
1978
|
subscription.dispose();
|
|
1979
|
+
hookSub?.dispose();
|
|
1980
|
+
hookWatcher?.stop();
|
|
1981
|
+
dialogClassifier.reset();
|
|
1133
1982
|
};
|
|
1134
1983
|
});
|
|
1135
1984
|
}
|
|
@@ -1137,6 +1986,7 @@ var PtyRunner = class {
|
|
|
1137
1986
|
|
|
1138
1987
|
export {
|
|
1139
1988
|
PlanFileResolver,
|
|
1989
|
+
HookInjector,
|
|
1140
1990
|
stripAnsi,
|
|
1141
1991
|
isIdlePrompt,
|
|
1142
1992
|
TRUST_DIALOG_RE,
|
|
@@ -1148,6 +1998,7 @@ export {
|
|
|
1148
1998
|
isInteractiveDialog,
|
|
1149
1999
|
containsActiveWork,
|
|
1150
2000
|
isTuiNoise,
|
|
2001
|
+
InputWaitController,
|
|
1151
2002
|
PtyRunner
|
|
1152
2003
|
};
|
|
1153
|
-
//# sourceMappingURL=chunk-
|
|
2004
|
+
//# sourceMappingURL=chunk-36G3DPO3.js.map
|