topchester-ai 0.18.0 → 0.20.0

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/cli.mjs CHANGED
@@ -2212,7 +2212,7 @@ function killChild(pid, detached, signal) {
2212
2212
  const inspectCommandTool = defineTool({
2213
2213
  name: "inspect_command",
2214
2214
  description: "Run a narrowly validated read-only command for repository orientation.",
2215
- prompt: "inspect_command: run a safe read-only discovery command inside the workspace for quick orientation; prefer read_file, list_files, grep, and find_file for exact file tasks, and do not use it for builds, tests, installs, network, shell scripts, or edits. To use it, reply with only JSON: {\"tool\":\"inspect_command\",\"args\":{\"command\":\"pwd && rg --files docs/plans | head -20\",\"workdir\":\".\",\"timeout_ms\":10000}}",
2215
+ prompt: "inspect_command: run a safe read-only discovery command inside the workspace for quick repo orientation; prefer read_file, list_files, grep, and find_file for exact file tasks, and do not use it for builds, tests, installs, network, shell scripts, edits, or user-requested specific commands such as node --version, which node, or pnpm --version. To use it, reply with only JSON: {\"tool\":\"inspect_command\",\"args\":{\"command\":\"pwd && rg --files docs/plans | head -20\",\"workdir\":\".\",\"timeout_ms\":10000}}",
2216
2216
  argsSchema: inspectCommandArgsSchema,
2217
2217
  execute: (context, args) => inspectWorkspaceCommand(context.workspaceRoot, args, { pathEnv: context.pathEnv })
2218
2218
  });
@@ -3259,8 +3259,8 @@ const runCommandArgsSchema = z.object({
3259
3259
  });
3260
3260
  const runCommandTool = defineTool({
3261
3261
  name: "run_command",
3262
- description: "Run a strictly policy-approved project command inside the workspace.",
3263
- prompt: "run_command: run a project command only when strict policy allows it; prefer run_validator for tests, lint, typecheck, build, check, format-check, and smoke. To use it, reply with only JSON: {\"tool\":\"run_command\",\"args\":{\"command\":\"node scripts/check-fixtures.mjs\",\"workdir\":\".\",\"timeout_ms\":30000}}",
3262
+ description: "Run a policy-gated command inside the workspace.",
3263
+ prompt: "run_command: run a user-requested command when no more specific tool fits; it is the general policy-gated command runner, and runtime policy decides whether the command is allowed, rejected, or needs approval. Prefer run_validator for tests, lint, typecheck, build, check, format-check, and smoke. To use it, reply with only JSON: {\"tool\":\"run_command\",\"args\":{\"command\":\"node --version\",\"workdir\":\".\",\"timeout_ms\":30000}}",
3264
3264
  argsSchema: runCommandArgsSchema,
3265
3265
  requiresExclusiveWorkspace: true,
3266
3266
  execute: async (context, args) => runWorkspaceCommand(context.workspaceRoot, args, context.config?.tools?.commands, context.runCommandApprovals?.allowExactCommands, context.pathEnv, context.abortSignal)
@@ -4452,6 +4452,45 @@ const commandPolicySchema = z.object({
4452
4452
  allowExact: z.array(commandPatternSchema).optional().default([]),
4453
4453
  deny: z.array(commandPatternSchema).optional().default([])
4454
4454
  }).strict();
4455
+ const hookEventNames = [
4456
+ "SessionStart",
4457
+ "UserPromptSubmit",
4458
+ "PreToolUse",
4459
+ "PostToolUse",
4460
+ "PermissionRequest",
4461
+ "PreCompact",
4462
+ "Stop"
4463
+ ];
4464
+ const hookEventAliasMap = {
4465
+ TaskStart: "SessionStart",
4466
+ TaskAcknowledge: "UserPromptSubmit",
4467
+ UserActionRequired: "PermissionRequest",
4468
+ TaskComplete: "Stop"
4469
+ };
4470
+ const hookTimeoutMsSchema = z.number().int().positive().max(6e5);
4471
+ const hookMatcherSchema = z.union([z.string().min(1), z.array(z.string().min(1))]).optional();
4472
+ const hookHandlerSchema = z.object({
4473
+ type: z.literal("command").optional(),
4474
+ command: z.string().min(1),
4475
+ timeoutMs: hookTimeoutMsSchema.optional(),
4476
+ matcher: hookMatcherSchema
4477
+ }).strict();
4478
+ const canonicalHooksConfigSchema = z.object({
4479
+ enabled: z.boolean().optional(),
4480
+ SessionStart: z.array(hookHandlerSchema).optional(),
4481
+ UserPromptSubmit: z.array(hookHandlerSchema).optional(),
4482
+ PreToolUse: z.array(hookHandlerSchema).optional(),
4483
+ PostToolUse: z.array(hookHandlerSchema).optional(),
4484
+ PermissionRequest: z.array(hookHandlerSchema).optional(),
4485
+ PreCompact: z.array(hookHandlerSchema).optional(),
4486
+ Stop: z.array(hookHandlerSchema).optional()
4487
+ }).strict();
4488
+ const rawHooksConfigSchema = canonicalHooksConfigSchema.extend({
4489
+ TaskStart: z.array(hookHandlerSchema).optional(),
4490
+ TaskAcknowledge: z.array(hookHandlerSchema).optional(),
4491
+ UserActionRequired: z.array(hookHandlerSchema).optional(),
4492
+ TaskComplete: z.array(hookHandlerSchema).optional()
4493
+ }).strict();
4455
4494
  const topchesterConfigSchema = z.object({
4456
4495
  models: z.object({
4457
4496
  defaultPurpose: modelPurposeSchema.optional(),
@@ -4460,12 +4499,14 @@ const topchesterConfigSchema = z.object({
4460
4499
  providers: providersSchema.optional()
4461
4500
  }).strict().optional(),
4462
4501
  ignore: z.object({ paths: z.array(ignorePathSchema).optional() }).optional(),
4463
- tools: z.object({ commands: commandPolicySchema.optional() }).strict().optional()
4502
+ tools: z.object({ commands: commandPolicySchema.optional() }).strict().optional(),
4503
+ hooks: canonicalHooksConfigSchema.optional()
4464
4504
  });
4465
4505
  const rawTopchesterConfigSchema = z.object({
4466
4506
  models: rawModelsSchema.optional(),
4467
4507
  ignore: z.object({ paths: z.array(ignorePathSchema).optional() }).optional(),
4468
- tools: z.object({ commands: commandPolicySchema.optional() }).strict().optional()
4508
+ tools: z.object({ commands: commandPolicySchema.optional() }).strict().optional(),
4509
+ hooks: rawHooksConfigSchema.optional()
4469
4510
  });
4470
4511
  function getGlobalTopchesterConfigDir() {
4471
4512
  return join(homedir(), ".config", "topchester");
@@ -4644,6 +4685,9 @@ function parseConfigFile(path, value) {
4644
4685
  return parsed.data;
4645
4686
  }
4646
4687
  function normalizeConfigInput(value) {
4688
+ return normalizeHooksConfigInput(normalizeModelsConfigInput(value));
4689
+ }
4690
+ function normalizeModelsConfigInput(value) {
4647
4691
  if (!isPlainObject(value) || !isPlainObject(value.models)) return value;
4648
4692
  const models = { ...value.models };
4649
4693
  const providers = isPlainObject(models.providers) ? { ...models.providers } : {};
@@ -4683,6 +4727,22 @@ function normalizeConfigInput(value) {
4683
4727
  }
4684
4728
  };
4685
4729
  }
4730
+ function normalizeHooksConfigInput(value) {
4731
+ if (!isPlainObject(value) || !isPlainObject(value.hooks)) return value;
4732
+ const hooks = { ...value.hooks };
4733
+ for (const [alias, canonical] of Object.entries(hookEventAliasMap)) {
4734
+ const aliasHandlers = hooks[alias];
4735
+ if (aliasHandlers === void 0) continue;
4736
+ const canonicalHandlers = hooks[canonical];
4737
+ if (!Array.isArray(aliasHandlers) || canonicalHandlers !== void 0 && !Array.isArray(canonicalHandlers)) continue;
4738
+ hooks[canonical] = [...canonicalHandlers ?? [], ...aliasHandlers];
4739
+ delete hooks[alias];
4740
+ }
4741
+ return {
4742
+ ...value,
4743
+ hooks
4744
+ };
4745
+ }
4686
4746
  function normalizeModelRef(ref, defaultProvider) {
4687
4747
  if (typeof ref === "string") return parseModelRef(ref, defaultProvider);
4688
4748
  if (!isPlainObject(ref) || typeof ref.name !== "string") return;
@@ -4745,7 +4805,7 @@ function isOpenAIProvider(providerId, baseURL) {
4745
4805
  function deepMerge(base, override, path = []) {
4746
4806
  if (Array.isArray(base) && Array.isArray(override)) {
4747
4807
  const joinedPath = path.join(".");
4748
- return joinedPath === "ignore.paths" || joinedPath === "tools.commands.allow" || joinedPath === "tools.commands.allowExact" || joinedPath === "tools.commands.deny" ? [...base, ...override] : override;
4808
+ return joinedPath === "ignore.paths" || joinedPath === "tools.commands.allow" || joinedPath === "tools.commands.allowExact" || joinedPath === "tools.commands.deny" || path.length === 2 && path[0] === "hooks" && hookEventNames.includes(path[1]) ? [...base, ...override] : override;
4749
4809
  }
4750
4810
  if (!isPlainObject(base) || !isPlainObject(override)) return override;
4751
4811
  const result = { ...base };
@@ -8629,6 +8689,225 @@ function isAbortError(error) {
8629
8689
  return error.name === "AbortError" || error.message.toLowerCase().includes("aborted");
8630
8690
  }
8631
8691
  //#endregion
8692
+ //#region src/agent/hooks.ts
8693
+ const DEFAULT_HOOK_TIMEOUT_MS = 5e3;
8694
+ const MAX_CAPTURED_OUTPUT_CHARS = 64e3;
8695
+ const hookResponseSchema = z.object({
8696
+ action: z.enum([
8697
+ "continue",
8698
+ "block",
8699
+ "stop"
8700
+ ]).optional(),
8701
+ decision: z.string().optional(),
8702
+ cancel: z.boolean().optional(),
8703
+ context: z.union([z.string(), z.array(z.string())]).optional(),
8704
+ message: z.string().optional(),
8705
+ feedback: z.string().optional(),
8706
+ reason: z.string().optional()
8707
+ }).passthrough();
8708
+ async function runTopchesterHooks(context, event, payload, options = {}) {
8709
+ const handlers = getConfiguredHookHandlers(context, event, options.toolName);
8710
+ const result = {
8711
+ contexts: [],
8712
+ messages: [],
8713
+ handlerCount: 0
8714
+ };
8715
+ for (const handler of handlers) {
8716
+ result.handlerCount += 1;
8717
+ const handlerResult = await runCommandHandler(context, event, payload, handler, options);
8718
+ result.contexts.push(...handlerResult.contexts);
8719
+ result.messages.push(...handlerResult.messages);
8720
+ if (handlerResult.blocked) {
8721
+ result.blocked = handlerResult.blocked;
8722
+ break;
8723
+ }
8724
+ if (handlerResult.stopped) {
8725
+ result.stopped = handlerResult.stopped;
8726
+ break;
8727
+ }
8728
+ }
8729
+ return result;
8730
+ }
8731
+ function formatHookContextsForPrompt(event, contexts) {
8732
+ const normalized = contexts.map((context) => context.trim()).filter(Boolean);
8733
+ if (normalized.length === 0) return "";
8734
+ return [`Hook context from ${event}:`, ...normalized].join("\n\n");
8735
+ }
8736
+ function getConfiguredHookHandlers(context, event, toolName) {
8737
+ const hooks = context.config.hooks;
8738
+ if (!hooks || hooks.enabled === false) return [];
8739
+ return (hooks[event] ?? []).filter((handler) => hookMatches(handler, event, toolName));
8740
+ }
8741
+ function hookMatches(handler, event, toolName) {
8742
+ const matcher = handler.matcher;
8743
+ if (matcher === void 0) return true;
8744
+ const target = toolName ?? event;
8745
+ return (Array.isArray(matcher) ? matcher : [matcher]).some((entry) => entry === "*" || entry === target);
8746
+ }
8747
+ async function runCommandHandler(context, event, payload, handler, options) {
8748
+ const result = await runHookProcess(handler.command ?? "", payload, {
8749
+ cwd: context.workspaceRoot,
8750
+ timeoutMs: handler.timeoutMs ?? DEFAULT_HOOK_TIMEOUT_MS,
8751
+ abortSignal: options.abortSignal,
8752
+ env: buildHookEnv(event, options.toolName)
8753
+ });
8754
+ logHookProcessResult(context, event, handler, result);
8755
+ if (result.timedOut || result.aborted || result.spawnError || result.exitCode !== 0) return emptyHookRunResult();
8756
+ const stdout = result.stdout.trim();
8757
+ if (!stdout) return emptyHookRunResult();
8758
+ let parsed;
8759
+ try {
8760
+ parsed = JSON.parse(stdout);
8761
+ } catch (error) {
8762
+ logHookWarning(context, {
8763
+ event: "hook_response_parse_failed",
8764
+ hookEventName: event,
8765
+ error: error instanceof Error ? error.message : String(error),
8766
+ stdoutLength: result.stdout.length
8767
+ });
8768
+ return emptyHookRunResult();
8769
+ }
8770
+ const response = hookResponseSchema.safeParse(parsed);
8771
+ if (!response.success) {
8772
+ logHookWarning(context, {
8773
+ event: "hook_response_invalid",
8774
+ hookEventName: event,
8775
+ issues: response.error.issues.map((issue) => issue.message)
8776
+ });
8777
+ return emptyHookRunResult();
8778
+ }
8779
+ return normalizeHookResponse(event, response.data);
8780
+ }
8781
+ function normalizeHookResponse(event, response) {
8782
+ const result = emptyHookRunResult();
8783
+ const action = normalizeHookAction(event, response);
8784
+ const message = firstNonEmpty(response.message, response.feedback, response.reason);
8785
+ const contexts = Array.isArray(response.context) ? response.context : response.context ? [response.context] : [];
8786
+ result.contexts.push(...contexts.filter((context) => context.trim().length > 0));
8787
+ if (message) result.messages.push(message);
8788
+ if (action === "block") result.blocked = { message: message || `Hook ${event} blocked the request.` };
8789
+ else if (action === "stop") result.stopped = { message: message || `Hook ${event} stopped the turn.` };
8790
+ return result;
8791
+ }
8792
+ function normalizeHookAction(event, response) {
8793
+ if (response.cancel) return event === "Stop" ? "stop" : "block";
8794
+ if (response.action) return response.action;
8795
+ const decision = response.decision?.trim().toLowerCase();
8796
+ if (decision === "block" || decision === "deny" || decision === "denied") return "block";
8797
+ if (decision === "stop" || decision === "halt") return "stop";
8798
+ return "continue";
8799
+ }
8800
+ function buildHookEnv(event, toolName) {
8801
+ return {
8802
+ ...process.env,
8803
+ TOPCHESTER_HOOK_EVENT: event,
8804
+ TOPCHESTER_HOOK_TOOL: toolName ?? ""
8805
+ };
8806
+ }
8807
+ async function runHookProcess(command, payload, options) {
8808
+ const startedAt = Date.now();
8809
+ const shell = process.env.SHELL || "/bin/sh";
8810
+ return new Promise((resolve) => {
8811
+ let stdout = "";
8812
+ let stderr = "";
8813
+ let settled = false;
8814
+ let timedOut = false;
8815
+ let aborted = false;
8816
+ const child = spawn(shell, ["-lc", command], {
8817
+ cwd: options.cwd,
8818
+ env: options.env,
8819
+ stdio: [
8820
+ "pipe",
8821
+ "pipe",
8822
+ "pipe"
8823
+ ]
8824
+ });
8825
+ const finish = (partial) => {
8826
+ if (settled) return;
8827
+ settled = true;
8828
+ clearTimeout(timeout);
8829
+ options.abortSignal?.removeEventListener("abort", abort);
8830
+ resolve({
8831
+ stdout,
8832
+ stderr,
8833
+ exitCode: null,
8834
+ signal: null,
8835
+ timedOut,
8836
+ aborted,
8837
+ durationMs: Date.now() - startedAt,
8838
+ ...partial
8839
+ });
8840
+ };
8841
+ const timeout = setTimeout(() => {
8842
+ timedOut = true;
8843
+ child.kill("SIGTERM");
8844
+ }, options.timeoutMs);
8845
+ const abort = () => {
8846
+ aborted = true;
8847
+ child.kill("SIGTERM");
8848
+ };
8849
+ if (options.abortSignal?.aborted) abort();
8850
+ else options.abortSignal?.addEventListener("abort", abort, { once: true });
8851
+ child.stdout?.setEncoding("utf8");
8852
+ child.stderr?.setEncoding("utf8");
8853
+ child.stdout?.on("data", (chunk) => {
8854
+ stdout = appendCapped(stdout, chunk);
8855
+ });
8856
+ child.stderr?.on("data", (chunk) => {
8857
+ stderr = appendCapped(stderr, chunk);
8858
+ });
8859
+ child.on("error", (error) => {
8860
+ finish({ spawnError: error.message });
8861
+ });
8862
+ child.on("close", (exitCode, signal) => {
8863
+ finish({
8864
+ exitCode,
8865
+ signal
8866
+ });
8867
+ });
8868
+ child.stdin?.on("error", () => {});
8869
+ child.stdin?.end(`${JSON.stringify(payload)}\n`);
8870
+ });
8871
+ }
8872
+ function logHookProcessResult(context, event, handler, result) {
8873
+ context.logger.debug({
8874
+ event: "hook_run",
8875
+ hookEventName: event,
8876
+ handlerType: handler.type ?? "command",
8877
+ matcher: handler.matcher,
8878
+ exitCode: result.exitCode,
8879
+ signal: result.signal,
8880
+ timedOut: result.timedOut,
8881
+ aborted: result.aborted,
8882
+ spawnError: result.spawnError,
8883
+ durationMs: result.durationMs,
8884
+ stdoutLength: result.stdout.length,
8885
+ stderrLength: result.stderr.length
8886
+ }, "hook run");
8887
+ }
8888
+ function logHookWarning(context, payload) {
8889
+ const logger = context.logger;
8890
+ if (typeof logger.warn === "function") {
8891
+ logger.warn(payload, "hook warning");
8892
+ return;
8893
+ }
8894
+ context.logger.debug(payload, "hook warning");
8895
+ }
8896
+ function emptyHookRunResult() {
8897
+ return {
8898
+ contexts: [],
8899
+ messages: [],
8900
+ handlerCount: 0
8901
+ };
8902
+ }
8903
+ function appendCapped(current, chunk) {
8904
+ if (current.length >= MAX_CAPTURED_OUTPUT_CHARS) return current;
8905
+ return `${current}${chunk}`.slice(0, MAX_CAPTURED_OUTPUT_CHARS);
8906
+ }
8907
+ function firstNonEmpty(...values) {
8908
+ return values.find((value) => value !== void 0 && value.trim().length > 0);
8909
+ }
8910
+ //#endregion
8632
8911
  //#region src/agent/profiles.ts
8633
8912
  const READ_ONLY_TOOLS = [
8634
8913
  "read_file",
@@ -8946,14 +9225,24 @@ function getChatSystemPrompt(options = {}) {
8946
9225
  ...canUseTool("list_files") && canUseTool("grep") && canUseTool("find_file") && canUseTool("read_file") ? ["- Use list_files, grep, find_file, and read_file for exact file listing, search, lookup, and reading tasks."] : [],
8947
9226
  ...canUseTool("git_status") && canUseTool("git_diff") && canUseTool("git_log") ? ["- Use git_status, git_diff, and git_log for Git state, diffs, and history. Prefer these over inspect_command for Git workflow inspection."] : [],
8948
9227
  ...canUseTool("git_add") && canUseTool("git_commit") ? ["- Use git_add and git_commit only when the user explicitly asks to stage or commit. Never stage unrelated files, never stage '.', and never commit unless staged paths exactly match the user's request."] : [],
8949
- ...canUseTool("inspect_command") ? ["- Use inspect_command only for quick read-only repo orientation when a short familiar command chain is clearer than several dedicated tool calls.", "- inspect_command is not a shell. Unsafe commands, shell expansion, scripts, installs, builds, tests, network access, and file mutation are not available through it."] : [],
9228
+ ...canUseTool("inspect_command") ? [
9229
+ "- Use inspect_command only for quick read-only repo orientation when the user did not ask to run a specific command and a short familiar command chain is clearer than several dedicated tool calls.",
9230
+ "- inspect_command is not a shell. Unsafe commands, shell expansion, scripts, installs, builds, tests, network access, and file mutation are not available through it.",
9231
+ canUseTool("run_command") ? "- Do not use inspect_command when the user asks to run a specific command such as node --version, which node, or pnpm --version; use run_command or run_validator instead." : ""
9232
+ ].filter(Boolean) : [],
8950
9233
  ...canUseTool("run_validator") ? [
8951
9234
  "- After code edits, use run_validator when there is a relevant test, lint, typecheck, build, check, format-check, or smoke command that can prove the change.",
8952
9235
  "- Failed run_validator exits are evidence. Read stdout and stderr, fix the issue when it is in scope, and rerun the narrowest useful validator.",
8953
9236
  canUseTool("run_command") ? "- If run_validator is rejected because the command is not a strict validator shape but the user still needs command output, retry with run_command when project policy allows it." : "",
8954
9237
  "- Do not use inspect_command for tests, builds, lint, typecheck, format checks, or smoke checks. Use run_validator for verification."
8955
9238
  ].filter(Boolean) : [],
8956
- ...canUseTool("run_command") ? ["- Use run_command only for commands allowed by project command policy. Prefer dedicated read, edit, Git, and validator tools when they fit.", "- Do not use run_command for installs, deploys, network commands, destructive commands, interactive commands, file reads, file writes, Git inspection, or validation when run_validator can do it."] : [],
9239
+ ...canUseTool("run_command") ? [
9240
+ "- When the user explicitly asks to run a command or asks for command output, use run_command unless a more specific tool is clearly the right fit.",
9241
+ "- run_command is the general policy-gated command runner. Do not avoid it because a command might be disallowed; call run_command and let command policy return the allowed, rejected, or approval result.",
9242
+ "- Prefer dedicated tools for file reads, file writes, edits, Git inspection, and searches.",
9243
+ "- Use run_command only for commands allowed by project command policy. Prefer dedicated read, edit, Git, and validator tools when they fit.",
9244
+ "- Do not use run_command for installs, deploys, network commands, destructive commands, interactive commands, file reads, file writes, Git inspection, or validation when run_validator can do it."
9245
+ ] : [],
8957
9246
  ...canUseTool("edit_file") && canUseTool("read_file") ? ["- Use read_file before editing a file so your edit is based on current file content and hash metadata."] : [],
8958
9247
  ...canUseTool("read_file") && (canUseTool("edit_file") || canUseTool("write_file")) ? ["- When passing expected_current_hash to edit_file or write_file, use the current pre-edit/pre-write hash from the latest read_file result for that exact file. Never invent it and never use a predicted after-edit or after-write hash."] : [],
8959
9248
  ...canUseTool("edit_file") ? ["- Use edit_file for targeted edits to existing files. Make multiple disjoint edits for the same file in one call when possible."] : [],
@@ -9578,6 +9867,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
9578
9867
  options;
9579
9868
  taskPlan = createTaskPlanController();
9580
9869
  approvedRunCommands = /* @__PURE__ */ new Set();
9870
+ startedHookSessionKeys = /* @__PURE__ */ new Set();
9581
9871
  /**
9582
9872
  * Holds the shared application context for one runtime instance.
9583
9873
  * The runtime does not own those dependencies; it coordinates the
@@ -9610,6 +9900,20 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
9610
9900
  async checkKnowledgeBase() {
9611
9901
  return getKnowledgeStatusEvents(await this.getKnowledgeStatusWithNonCleanFileCount());
9612
9902
  }
9903
+ async runSessionStartHooks(session, options = {}) {
9904
+ const sessionKey = session?.sessionId ?? `workspace:${this.context.workspaceRoot}`;
9905
+ if (this.startedHookSessionKeys.has(sessionKey)) return [];
9906
+ this.startedHookSessionKeys.add(sessionKey);
9907
+ const result = await this.runHookEvent("SessionStart", this.createBaseHookPayload("SessionStart", session, {
9908
+ isResumed: Boolean(options.isResumed),
9909
+ taskStartAlias: "TaskStart"
9910
+ }), { abortSignal: options.abortSignal });
9911
+ return this.hookResultToEvents(result);
9912
+ }
9913
+ async runPreCompactHooks(session, options = {}) {
9914
+ const result = await this.runHookEvent("PreCompact", this.createBaseHookPayload("PreCompact", session, { reason: options.reason ?? "Compaction is about to start." }), { abortSignal: options.abortSignal });
9915
+ return this.hookResultToEvents(result);
9916
+ }
9613
9917
  /**
9614
9918
  * Streams one user chat turn through the agent loop. It builds the model
9615
9919
  * prompt with relevant KB context, calls the model, executes any requested
@@ -9622,13 +9926,26 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
9622
9926
  * ordered events.
9623
9927
  */
9624
9928
  async *submitMessageStream(conversation, message, abortSignal, options = {}) {
9625
- let nextPrompt = await this.buildPromptWithKnowledgeContext(buildConversationPrompt(conversation, message), message);
9929
+ const session = options.session ?? this.options.session;
9930
+ for (const event of await this.runSessionStartHooks(session, { abortSignal })) yield event;
9931
+ const userPromptHook = await this.runHookEvent("UserPromptSubmit", this.createBaseHookPayload("UserPromptSubmit", session, {
9932
+ prompt: { text: message },
9933
+ prompt_text: message,
9934
+ user_prompt: message
9935
+ }), { abortSignal });
9936
+ for (const event of this.hookResultToEvents(userPromptHook)) yield event;
9937
+ if (userPromptHook.blocked || userPromptHook.stopped) {
9938
+ const interruption = userPromptHook.blocked ?? userPromptHook.stopped;
9939
+ if (userPromptHook.messages.length === 0) yield agentEvent.systemMessage(interruption.message);
9940
+ yield agentEvent.status("ready");
9941
+ return;
9942
+ }
9943
+ let nextPrompt = this.appendHookContextsToPrompt(await this.buildPromptWithKnowledgeContext(buildConversationPrompt(conversation, message), message), "UserPromptSubmit", userPromptHook.contexts);
9626
9944
  let totalDurationMs = 0;
9627
9945
  const tokenUsageTotals = {};
9628
9946
  const profile = this.options.profile ?? PRIMARY_AGENT_PROFILE;
9629
9947
  const permissions = createToolPermissionView(profile, { deniedTools: this.options.parentPermissions?.deniedTools });
9630
9948
  const tools = getProfileToolDefinitions(permissions);
9631
- const session = options.session ?? this.options.session;
9632
9949
  const subagents = new SubagentManager({
9633
9950
  context: this.context,
9634
9951
  parentSession: session,
@@ -9733,7 +10050,9 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
9733
10050
  }
9734
10051
  yield agentEvent.taskPlan(this.taskPlan.update({ items: [] }));
9735
10052
  }
9736
- yield agentEvent.assistantMessage(finalText.trim() || "I got an empty response from the model.", formatAgentMessageMeta(result.modelId, totalDurationMs, tokenUsageTotals));
10053
+ const finalMessage = finalText.trim() || "I got an empty response from the model.";
10054
+ yield agentEvent.assistantMessage(finalMessage, formatAgentMessageMeta(result.modelId, totalDurationMs, tokenUsageTotals));
10055
+ for (const event of await this.runStopHookEvents(session, finalMessage, "completed", abortSignal)) yield event;
9737
10056
  yield agentEvent.status("ready");
9738
10057
  return;
9739
10058
  }
@@ -9750,10 +10069,33 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
9750
10069
  if (modelToolCalls.length > 1 && modelToolCalls.every((call) => call.tool === "task")) {
9751
10070
  const taskCalls = modelToolCalls.map((call) => call);
9752
10071
  const taskResults = [];
10072
+ const postHookContexts = [];
9753
10073
  for (let index = 0; index < taskCalls.length; index += DEFAULT_TASK_CONCURRENCY) {
9754
10074
  const batch = taskCalls.slice(index, index + DEFAULT_TASK_CONCURRENCY);
10075
+ const executableBatch = [];
10076
+ for (let batchIndex = 0; batchIndex < batch.length; batchIndex += 1) {
10077
+ const call = batch[batchIndex];
10078
+ const resultIndex = index + batchIndex;
10079
+ const preHook = await this.runPreToolUseHook(call, modelToolCalls[resultIndex]?.id, session, abortSignal);
10080
+ for (const event of this.hookResultToEvents(preHook)) yield event;
10081
+ if (preHook.stopped) {
10082
+ if (preHook.messages.length === 0) yield agentEvent.systemMessage(preHook.stopped.message);
10083
+ yield agentEvent.status("ready");
10084
+ return;
10085
+ }
10086
+ if (preHook.blocked) {
10087
+ taskResults[resultIndex] = createToolErrorResult(call.tool, preHook.blocked.message);
10088
+ continue;
10089
+ }
10090
+ executableBatch.push({
10091
+ call,
10092
+ resultIndex,
10093
+ toolCallId: modelToolCalls[resultIndex]?.id
10094
+ });
10095
+ }
10096
+ if (executableBatch.length === 0) continue;
9755
10097
  const taskEventQueue = createRuntimeEventQueue();
9756
- const batchResultPromise = Promise.all(batch.map((call, batchIndex) => executeToolCall(this.context.workspaceRoot, call, {
10098
+ const batchResultPromise = Promise.all(executableBatch.map((entry) => executeToolCall(this.context.workspaceRoot, entry.call, {
9757
10099
  logger: this.context.logger,
9758
10100
  config: this.context.config,
9759
10101
  taskPlan: this.taskPlan,
@@ -9761,22 +10103,60 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
9761
10103
  permissions,
9762
10104
  subagents,
9763
10105
  abortSignal,
9764
- toolCallId: modelToolCalls[index + batchIndex]?.id,
10106
+ toolCallId: entry.toolCallId,
9765
10107
  eventSink: (event) => taskEventQueue.push(event)
9766
10108
  }))).finally(() => {
9767
10109
  taskEventQueue.close();
9768
10110
  });
9769
10111
  for await (const event of taskEventQueue) yield event;
9770
- taskResults.push(...await batchResultPromise);
10112
+ const batchResults = await batchResultPromise;
10113
+ for (let batchIndex = 0; batchIndex < executableBatch.length; batchIndex += 1) {
10114
+ const entry = executableBatch[batchIndex];
10115
+ taskResults[entry.resultIndex] = batchResults[batchIndex];
10116
+ }
10117
+ }
10118
+ for (let index = 0; index < taskCalls.length; index += 1) {
10119
+ const call = taskCalls[index];
10120
+ const toolResult = taskResults[index];
10121
+ yield agentEvent.toolCall(call, formatToolCallMessage(call, toolResult));
10122
+ const postHook = await this.runPostToolUseHook(call, modelToolCalls[index]?.id, toolResult, session, abortSignal);
10123
+ for (const event of this.hookResultToEvents(postHook)) yield event;
10124
+ postHookContexts.push(...postHook.contexts);
10125
+ if (postHook.stopped) {
10126
+ if (postHook.messages.length === 0) yield agentEvent.systemMessage(postHook.stopped.message);
10127
+ yield agentEvent.status("ready");
10128
+ return;
10129
+ }
9771
10130
  }
9772
- for (let index = 0; index < taskCalls.length; index += 1) yield agentEvent.toolCall(taskCalls[index], formatToolCallMessage(taskCalls[index], taskResults[index]));
9773
10131
  afterTool = "task";
9774
- nextPrompt = `${nextPrompt}\n\n${taskResults.map((toolResult) => formatToolResultForPrompt(toolResult)).join("\n\n")}\n\n${formatContinuationInstruction(result.toolProtocol, taskResults.at(-1), isToolAllowed(permissions, "plan_todo"))}`;
10132
+ nextPrompt = this.appendHookContextsToPrompt(`${nextPrompt}\n\n${taskResults.map((toolResult) => formatToolResultForPrompt(toolResult)).join("\n\n")}\n\n${formatContinuationInstruction(result.toolProtocol, taskResults.at(-1), isToolAllowed(permissions, "plan_todo"))}`, "PostToolUse", postHookContexts);
9775
10133
  continue;
9776
10134
  }
9777
10135
  if (modelToolCalls.length > 1 && modelToolCalls.every((call) => isParallelSafeToolName(call.tool))) {
9778
10136
  const parallelCalls = modelToolCalls.map((call) => call);
9779
- const parallelResults = await Promise.all(parallelCalls.map((call, index) => executeToolCall(this.context.workspaceRoot, call, {
10137
+ const parallelResults = [];
10138
+ const executableCalls = [];
10139
+ const postHookContexts = [];
10140
+ for (let index = 0; index < parallelCalls.length; index += 1) {
10141
+ const call = parallelCalls[index];
10142
+ const preHook = await this.runPreToolUseHook(call, modelToolCalls[index]?.id, session, abortSignal);
10143
+ for (const event of this.hookResultToEvents(preHook)) yield event;
10144
+ if (preHook.stopped) {
10145
+ if (preHook.messages.length === 0) yield agentEvent.systemMessage(preHook.stopped.message);
10146
+ yield agentEvent.status("ready");
10147
+ return;
10148
+ }
10149
+ if (preHook.blocked) {
10150
+ parallelResults[index] = createToolErrorResult(call.tool, preHook.blocked.message);
10151
+ continue;
10152
+ }
10153
+ executableCalls.push({
10154
+ call,
10155
+ resultIndex: index,
10156
+ toolCallId: modelToolCalls[index]?.id
10157
+ });
10158
+ }
10159
+ const executedResults = await Promise.all(executableCalls.map((entry) => executeToolCall(this.context.workspaceRoot, entry.call, {
9780
10160
  logger: this.context.logger,
9781
10161
  config: this.context.config,
9782
10162
  taskPlan: this.taskPlan,
@@ -9784,48 +10164,86 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
9784
10164
  permissions,
9785
10165
  subagents,
9786
10166
  abortSignal,
9787
- toolCallId: modelToolCalls[index]?.id
10167
+ toolCallId: entry.toolCallId
9788
10168
  })));
9789
- for (let index = 0; index < parallelCalls.length; index += 1) yield agentEvent.toolCall(parallelCalls[index], formatToolCallMessage(parallelCalls[index], parallelResults[index]));
10169
+ for (let index = 0; index < executableCalls.length; index += 1) {
10170
+ const entry = executableCalls[index];
10171
+ parallelResults[entry.resultIndex] = executedResults[index];
10172
+ }
10173
+ for (let index = 0; index < parallelCalls.length; index += 1) {
10174
+ const call = parallelCalls[index];
10175
+ const toolResult = parallelResults[index];
10176
+ yield agentEvent.toolCall(call, formatToolCallMessage(call, toolResult));
10177
+ const postHook = await this.runPostToolUseHook(call, modelToolCalls[index]?.id, toolResult, session, abortSignal);
10178
+ for (const event of this.hookResultToEvents(postHook)) yield event;
10179
+ postHookContexts.push(...postHook.contexts);
10180
+ if (postHook.stopped) {
10181
+ if (postHook.messages.length === 0) yield agentEvent.systemMessage(postHook.stopped.message);
10182
+ yield agentEvent.status("ready");
10183
+ return;
10184
+ }
10185
+ }
9790
10186
  afterTool = parallelCalls.at(-1)?.tool;
9791
- nextPrompt = `${nextPrompt}\n\n${parallelResults.map((toolResult) => formatToolResultForPrompt(toolResult)).join("\n\n")}\n\n${formatContinuationInstruction(result.toolProtocol, parallelResults.at(-1), isToolAllowed(permissions, "plan_todo"))}`;
10187
+ nextPrompt = this.appendHookContextsToPrompt(`${nextPrompt}\n\n${parallelResults.map((toolResult) => formatToolResultForPrompt(toolResult)).join("\n\n")}\n\n${formatContinuationInstruction(result.toolProtocol, parallelResults.at(-1), isToolAllowed(permissions, "plan_todo"))}`, "PostToolUse", postHookContexts);
9792
10188
  continue;
9793
10189
  }
9794
10190
  const executableToolCall = toolCall;
9795
10191
  const suppressiblePlanTodoAnswer = getSuppressiblePlanTodoAnswer(executableToolCall, result.text, this.taskPlan.get());
9796
10192
  if (suppressiblePlanTodoAnswer !== void 0) {
9797
- yield agentEvent.assistantMessage(suppressiblePlanTodoAnswer || "I got an empty response from the model.", formatAgentMessageMeta(result.modelId, totalDurationMs, tokenUsageTotals));
10193
+ const finalMessage = suppressiblePlanTodoAnswer || "I got an empty response from the model.";
10194
+ yield agentEvent.assistantMessage(finalMessage, formatAgentMessageMeta(result.modelId, totalDurationMs, tokenUsageTotals));
10195
+ for (const event of await this.runStopHookEvents(session, finalMessage, "completed", abortSignal)) yield event;
9798
10196
  yield agentEvent.status("ready");
9799
10197
  return;
9800
10198
  }
9801
- const approval = await this.resolveRunCommandApproval(executableToolCall, options);
10199
+ const preHook = await this.runPreToolUseHook(executableToolCall, toolCall.id, session, abortSignal);
10200
+ for (const event of this.hookResultToEvents(preHook)) yield event;
9802
10201
  let toolResult;
9803
- if (approval.cancelled) toolResult = createToolErrorResult(executableToolCall.tool, approval.reason);
10202
+ if (preHook.stopped) {
10203
+ if (preHook.messages.length === 0) yield agentEvent.systemMessage(preHook.stopped.message);
10204
+ yield agentEvent.status("ready");
10205
+ return;
10206
+ }
10207
+ if (preHook.blocked) toolResult = createToolErrorResult(executableToolCall.tool, preHook.blocked.message);
9804
10208
  else {
9805
- const toolEventQueue = createRuntimeEventQueue();
9806
- const toolResultPromise = executeToolCall(this.context.workspaceRoot, executableToolCall, {
9807
- logger: this.context.logger,
9808
- config: this.context.config,
9809
- runCommandApprovals: { allowExactCommands: approval.approvedCommands },
9810
- taskPlan: this.taskPlan,
9811
- profile,
9812
- permissions,
9813
- subagents,
9814
- abortSignal,
9815
- toolCallId: toolCall.id,
9816
- eventSink: (event) => toolEventQueue.push(event)
9817
- }).finally(() => {
9818
- toolEventQueue.close();
9819
- });
9820
- for await (const event of toolEventQueue) yield event;
9821
- toolResult = await toolResultPromise;
10209
+ const approval = await this.resolveRunCommandApproval(executableToolCall, toolCall.id, options, session, abortSignal);
10210
+ for (const event of approval.events) yield event;
10211
+ if (approval.cancelled) toolResult = createToolErrorResult(executableToolCall.tool, approval.reason);
10212
+ else {
10213
+ const toolEventQueue = createRuntimeEventQueue();
10214
+ const toolResultPromise = executeToolCall(this.context.workspaceRoot, executableToolCall, {
10215
+ logger: this.context.logger,
10216
+ config: this.context.config,
10217
+ runCommandApprovals: { allowExactCommands: approval.approvedCommands },
10218
+ taskPlan: this.taskPlan,
10219
+ profile,
10220
+ permissions,
10221
+ subagents,
10222
+ abortSignal,
10223
+ toolCallId: toolCall.id,
10224
+ eventSink: (event) => toolEventQueue.push(event)
10225
+ }).finally(() => {
10226
+ toolEventQueue.close();
10227
+ });
10228
+ for await (const event of toolEventQueue) yield event;
10229
+ toolResult = await toolResultPromise;
10230
+ }
9822
10231
  }
9823
10232
  yield agentEvent.toolCall(executableToolCall, formatToolCallMessage(executableToolCall, toolResult));
9824
10233
  if (!isToolErrorResult(toolResult) && toolResult.tool === "plan_todo") yield agentEvent.taskPlan(toolResult.plan);
10234
+ const postHook = await this.runPostToolUseHook(executableToolCall, toolCall.id, toolResult, session, abortSignal);
10235
+ for (const event of this.hookResultToEvents(postHook)) yield event;
10236
+ if (postHook.stopped) {
10237
+ if (postHook.messages.length === 0) yield agentEvent.systemMessage(postHook.stopped.message);
10238
+ yield agentEvent.status("ready");
10239
+ return;
10240
+ }
9825
10241
  afterTool = executableToolCall.tool;
9826
- nextPrompt = `${nextPrompt}\n\n${formatToolResultForPrompt(toolResult)}\n\n${formatContinuationInstruction(result.toolProtocol, toolResult, isToolAllowed(permissions, "plan_todo"))}`;
10242
+ nextPrompt = this.appendHookContextsToPrompt(`${nextPrompt}\n\n${formatToolResultForPrompt(toolResult)}\n\n${formatContinuationInstruction(result.toolProtocol, toolResult, isToolAllowed(permissions, "plan_todo"))}`, "PostToolUse", postHook.contexts);
9827
10243
  }
9828
- yield agentEvent.assistantMessage("I stopped because the tool loop ended unexpectedly.", formatAgentMessageMeta(lastModelId, totalDurationMs, tokenUsageTotals));
10244
+ const finalMessage = "I stopped because the tool loop ended unexpectedly.";
10245
+ yield agentEvent.assistantMessage(finalMessage, formatAgentMessageMeta(lastModelId, totalDurationMs, tokenUsageTotals));
10246
+ for (const event of await this.runStopHookEvents(session, finalMessage, "failed", abortSignal)) yield event;
9829
10247
  yield agentEvent.status("ready");
9830
10248
  }
9831
10249
  /**
@@ -9840,16 +10258,80 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
9840
10258
  }
9841
10259
  return events;
9842
10260
  }
9843
- async resolveRunCommandApproval(call, options) {
10261
+ async runPreToolUseHook(call, toolCallId, session, abortSignal) {
10262
+ return this.runHookEvent("PreToolUse", this.createToolHookPayload("PreToolUse", call, toolCallId, session), {
10263
+ toolName: call.tool,
10264
+ abortSignal
10265
+ });
10266
+ }
10267
+ async runPostToolUseHook(call, toolCallId, result, session, abortSignal) {
10268
+ return this.runHookEvent("PostToolUse", this.createToolHookPayload("PostToolUse", call, toolCallId, session, { result }), {
10269
+ toolName: call.tool,
10270
+ abortSignal
10271
+ });
10272
+ }
10273
+ async runStopHookEvents(session, finalMessage, status, abortSignal) {
10274
+ const result = await this.runHookEvent("Stop", this.createBaseHookPayload("Stop", session, {
10275
+ taskCompleteAlias: "TaskComplete",
10276
+ finalMessage,
10277
+ status
10278
+ }), { abortSignal });
10279
+ return this.hookResultToEvents(result);
10280
+ }
10281
+ async runHookEvent(event, payload, options = {}) {
10282
+ return runTopchesterHooks(this.context, event, payload, options);
10283
+ }
10284
+ createBaseHookPayload(event, session, extra = {}) {
10285
+ return {
10286
+ hook_event_name: event,
10287
+ event,
10288
+ cwd: this.context.workspaceRoot,
10289
+ workspaceRoot: this.context.workspaceRoot,
10290
+ source: "topchester",
10291
+ ...session ? {
10292
+ session_id: session.sessionId,
10293
+ sessionId: session.sessionId,
10294
+ session: {
10295
+ sessionId: session.sessionId,
10296
+ rootSessionId: session.metadata.rootSessionId,
10297
+ parentSessionId: session.metadata.parentSessionId,
10298
+ source: session.metadata.source
10299
+ }
10300
+ } : {},
10301
+ ...extra
10302
+ };
10303
+ }
10304
+ createToolHookPayload(event, call, toolCallId, session, extra = {}) {
10305
+ return this.createBaseHookPayload(event, session, {
10306
+ tool_name: call.tool,
10307
+ tool_input: call.args,
10308
+ tool: {
10309
+ name: call.tool,
10310
+ input: call.args,
10311
+ ...toolCallId ? { callId: toolCallId } : {}
10312
+ },
10313
+ ...extra
10314
+ });
10315
+ }
10316
+ hookResultToEvents(result) {
10317
+ return result.messages.map((message) => agentEvent.systemMessage(message));
10318
+ }
10319
+ appendHookContextsToPrompt(prompt, event, contexts) {
10320
+ const hookContext = formatHookContextsForPrompt(event, contexts);
10321
+ return hookContext ? `${prompt}\n\n${hookContext}` : prompt;
10322
+ }
10323
+ async resolveRunCommandApproval(call, toolCallId, options, session, abortSignal) {
9844
10324
  const approvedCommands = [...this.approvedRunCommands];
9845
10325
  if (call.tool !== "run_command") return {
9846
10326
  cancelled: false,
9847
- approvedCommands
10327
+ approvedCommands,
10328
+ events: []
9848
10329
  };
9849
10330
  const parsed = runCommandArgsSchema.safeParse(call.args);
9850
10331
  if (!parsed.success) return {
9851
10332
  cancelled: false,
9852
- approvedCommands
10333
+ approvedCommands,
10334
+ events: []
9853
10335
  };
9854
10336
  const decision = await validateRunCommandPolicy(parsed.data, {
9855
10337
  workspaceRoot: this.context.workspaceRoot,
@@ -9858,13 +10340,31 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
9858
10340
  });
9859
10341
  if (decision.allowed) return {
9860
10342
  cancelled: false,
9861
- approvedCommands
10343
+ approvedCommands,
10344
+ events: []
9862
10345
  };
9863
10346
  if (!isRunCommandApprovalEligible(decision.reason) || !options.requestRunCommandApproval) return {
9864
10347
  cancelled: false,
9865
- approvedCommands
10348
+ approvedCommands,
10349
+ events: []
9866
10350
  };
9867
10351
  const command = decision.commands[0] ?? parsed.data.command.trim();
10352
+ const actionRequiredHook = await this.runHookEvent("PermissionRequest", this.createToolHookPayload("PermissionRequest", call, toolCallId, session, {
10353
+ notification_type: "permission_prompt",
10354
+ permission_mode: "",
10355
+ command,
10356
+ workdir: parsed.data.workdir,
10357
+ reason: decision.reason
10358
+ }), {
10359
+ toolName: call.tool,
10360
+ abortSignal
10361
+ });
10362
+ const events = this.hookResultToEvents(actionRequiredHook);
10363
+ if (actionRequiredHook.blocked || actionRequiredHook.stopped) return {
10364
+ cancelled: true,
10365
+ reason: (actionRequiredHook.blocked ?? actionRequiredHook.stopped).message,
10366
+ events
10367
+ };
9868
10368
  const approval = await options.requestRunCommandApproval({
9869
10369
  command,
9870
10370
  workdir: parsed.data.workdir,
@@ -9872,18 +10372,21 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
9872
10372
  });
9873
10373
  if (approval === "cancel") return {
9874
10374
  cancelled: true,
9875
- reason: `run_command cancelled by user for '${command}'.`
10375
+ reason: `run_command cancelled by user for '${command}'.`,
10376
+ events
9876
10377
  };
9877
10378
  if (approval === "allow_session" || approval === "allow_repo") {
9878
10379
  this.approvedRunCommands.add(command);
9879
10380
  return {
9880
10381
  cancelled: false,
9881
- approvedCommands: [...this.approvedRunCommands]
10382
+ approvedCommands: [...this.approvedRunCommands],
10383
+ events
9882
10384
  };
9883
10385
  }
9884
10386
  return {
9885
10387
  cancelled: false,
9886
- approvedCommands: [...approvedCommands, command]
10388
+ approvedCommands: [...approvedCommands, command],
10389
+ events
9887
10390
  };
9888
10391
  }
9889
10392
  /**
@@ -10368,6 +10871,7 @@ var TopchesterTuiShell = class {
10368
10871
  const isResumed = this.options.session !== void 0;
10369
10872
  const messages = this.options.initialMessages ?? getStartupThreadMessages(this.context);
10370
10873
  if (!isResumed) await persistMessagesWithWarning(session, messages, messages);
10874
+ await this.appendStartupRuntimeEvents(session, messages, await this.runtime.runSessionStartHooks?.(session, { isResumed }) ?? []);
10371
10875
  const folderName = getFolderName(this.context.workspaceRoot);
10372
10876
  const modelLabel = getModelLabel(this.context);
10373
10877
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
@@ -10837,10 +11341,24 @@ var TopchesterTuiShell = class {
10837
11341
  this.session = session;
10838
11342
  this.sessionStartedAt = Date.now();
10839
11343
  await persistMessagesWithWarning(session, messages, messages);
11344
+ await this.appendStartupRuntimeEvents(session, messages, await this.runtime.runSessionStartHooks?.(session, { isResumed: false }) ?? []);
10840
11345
  app.resetForNewSession(messages);
10841
11346
  tui.requestRender();
10842
11347
  await this.checkAgent(app, tui);
10843
11348
  }
11349
+ async appendStartupRuntimeEvents(session, messages, events) {
11350
+ for (const event of events) {
11351
+ messages.push(...renderRuntimeEvent(event));
11352
+ const payload = runtimeEventToSessionPayload(event);
11353
+ if (!payload) continue;
11354
+ try {
11355
+ await session.append(payload);
11356
+ } catch (error) {
11357
+ messages.push(systemMessage(`Session save failed: ${formatPlainError(error)}`));
11358
+ return;
11359
+ }
11360
+ }
11361
+ }
10844
11362
  async applyRuntimeEvents(app, events, renderRequester) {
10845
11363
  for (const event of events) {
10846
11364
  if (event.type === "status") app.setStatus(event.status);
@@ -10929,6 +11447,16 @@ async function executeRunCommand(context, options) {
10929
11447
  });
10930
11448
  try {
10931
11449
  if (!options.resume) await persistStartupMessages(session, runContext);
11450
+ await applyRuntimeEvents({
11451
+ events: await runtime.runSessionStartHooks(session, {
11452
+ isResumed: Boolean(options.resume),
11453
+ abortSignal: abortController.signal
11454
+ }),
11455
+ session,
11456
+ jsonEvents,
11457
+ runId,
11458
+ plain: !options.json
11459
+ });
10932
11460
  await applyRuntimeEvents({
10933
11461
  events: await runtime.checkKnowledgeBase(),
10934
11462
  session,
@@ -11169,15 +11697,6 @@ program.command("dev").description("start local development mode").action(() =>
11169
11697
  console.log("Topchester local dev mode");
11170
11698
  printStartupSummary(context);
11171
11699
  });
11172
- program.command("update").alias("upgrade").description("update Topchester with the package manager that installed it").argument("[target]", "version or npm dist tag to install", "latest").action(async (target) => {
11173
- try {
11174
- const command = await runSelfUpdate({ target });
11175
- console.log(formatSelfUpdateSuccess(command).join("\n"));
11176
- } catch (error) {
11177
- console.error(formatStartupError(error));
11178
- process.exitCode = 1;
11179
- }
11180
- });
11181
11700
  program.command("run").description("run one prompt or slash command without opening the TUI").argument("<prompt...>", "prompt text or slash command").option("--model <model>", "override the agent.primary model for this run").option("--timeout <ms>", "timeout for the run in milliseconds", parsePositiveInteger).option("--json", "write JSONL run events to stdout").option("--output-json <path>", "write JSONL run events to a file").action(async (promptParts, options) => {
11182
11701
  const context = createContextFromOptions();
11183
11702
  const globalOptions = program.opts();
@@ -11237,6 +11756,15 @@ kbCommand.command("status").description("show project files that are not current
11237
11756
  const result = await ui.spinner("Checking KB file status...", async () => filterNonCleanKnowledgeCompileResult(await dryRunKnowledgeCompile(context.workspaceRoot, { config: context.config })));
11238
11757
  console.log(formatKnowledgeCompileStatusResult(result, { formatSyncStatus: formatDryRunSyncStatus }).join("\n"));
11239
11758
  });
11759
+ program.command("update").alias("upgrade").description("update Topchester with the package manager that installed it").argument("[target]", "version or npm dist tag to install", "latest").action(async (target) => {
11760
+ try {
11761
+ const command = await runSelfUpdate({ target });
11762
+ console.log(formatSelfUpdateSuccess(command).join("\n"));
11763
+ } catch (error) {
11764
+ console.error(formatStartupError(error));
11765
+ process.exitCode = 1;
11766
+ }
11767
+ });
11240
11768
  await program.parseAsync();
11241
11769
  function printStartupSummary(context) {
11242
11770
  const assignments = context.config.models?.assignments ?? {};