@gianfrancopiana/openclaw-autoresearch 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,7 @@ Three tools drive the loop:
16
16
  | `run_experiment` | Executes a shell command, times it, captures stdout/stderr, parses `METRIC name=number` lines, and opens a pending experiment window that must be logged before another run can start. |
17
17
  | `log_experiment` | Records the pending run. The first logged run in a segment is tagged as the baseline automatically. `keep` auto-commits to git. `discard`/`crash` log without committing, and `discard` now requires an `idea` note that is appended to `autoresearch.ideas.md`. If the prior `run_experiment` captured the primary metric, `log_experiment` can infer `commit` and `metric` automatically. After 3+ runs in a segment, it also reports a confidence score for the best improvement versus noise. |
18
18
 
19
- Each tool also accepts an optional `cwd` so callers can target a nested repo explicitly instead of relying on the current session working directory.
19
+ In OpenClaw sessions, the plugin uses the host-provided `workspaceDir` as the normal repo root. Each tool also accepts an optional `cwd` so callers can explicitly target a nested or non-session repo when needed.
20
20
 
21
21
  All state lives in six repo-root files:
22
22
 
@@ -33,7 +33,8 @@ The design is file-first: any agent can pick up the repo-root files and continue
33
33
 
34
34
  ## Install
35
35
 
36
- Requires OpenClaw `2026.3.13` or newer.
36
+ Requires OpenClaw `2026.4.25` or newer.
37
+ Needs bash, git, and a git repo.
37
38
 
38
39
  Use OpenClaw's plugin installer:
39
40
 
@@ -63,12 +64,15 @@ npm pack
63
64
  openclaw plugins install ./gianfrancopiana-openclaw-autoresearch-<version>.tgz
64
65
  ```
65
66
 
66
- The install command records the plugin, enables it, and exposes the plugin surfaces on restart. The installer reads `package.json#openclaw.extensions`, loads the root [`index.ts`](index.ts), and discovers the manifest in [`openclaw.plugin.json`](openclaw.plugin.json).
67
+ The install command records the plugin, enables it, and makes it available
68
+ after restart. OpenClaw reads the package metadata, loads the root
69
+ [`index.ts`](index.ts), and finds the manifest in
70
+ [`openclaw.plugin.json`](openclaw.plugin.json).
67
71
 
68
72
  Verify:
69
73
 
70
74
  - skill: `autoresearch-create`
71
- - tools: `init_experiment`, `run_experiment`, `log_experiment`
75
+ - tools: `init_experiment`, `run_experiment`, `log_experiment`, `autoresearch_status`
72
76
  - command: `/autoresearch` (recommended)
73
77
  - direct skill fallback: `/skill autoresearch-create`
74
78
 
@@ -119,15 +123,17 @@ This port preserves upstream semantics, names, and file contracts while adapting
119
123
 
120
124
  ```bash
121
125
  npm install --include=dev
126
+ npm run check:release-metadata
122
127
  npm run typecheck
123
128
  npm test
124
129
  npm run validate
125
130
  npm run release:verify
131
+ npm run smoke:openclaw-host -- /absolute/path/to/openclaw
126
132
  ```
127
133
 
128
134
  Release instructions, including npm 2FA publishing, live in [`RELEASING.md`](RELEASING.md).
129
135
 
130
- The local test shim supports typechecking and tests without a full OpenClaw host checkout. Runtime behavior depends on a real OpenClaw host.
136
+ The local test shim supports typechecking and tests without a full OpenClaw host checkout. Runtime behavior depends on a real OpenClaw host, so run the host smoke against a current checkout before release.
131
137
 
132
138
  ## License
133
139
 
package/RELEASING.md CHANGED
@@ -7,7 +7,15 @@
7
7
 
8
8
  ## Release
9
9
 
10
- 1. Update `package.json` and `openclaw.plugin.json` if you are changing the version.
10
+ 1. Update the package version in `package.json`, then sync generated metadata:
11
+
12
+ ```bash
13
+ npm run sync:release-metadata
14
+ ```
15
+
16
+ If you change the minimum supported OpenClaw version, keep
17
+ `openclaw.install`, `openclaw.compat`, and `openclaw.build` aligned too.
18
+
11
19
  2. Run the release checks:
12
20
 
13
21
  ```bash
@@ -15,7 +23,16 @@
15
23
  npm run release:verify
16
24
  ```
17
25
 
18
- 3. Publish:
26
+ CI runs the same release verification, so metadata drift should fail before
27
+ publish.
28
+
29
+ 3. Smoke-test against a current local OpenClaw checkout:
30
+
31
+ ```bash
32
+ npm run smoke:openclaw-host -- /absolute/path/to/openclaw
33
+ ```
34
+
35
+ 4. Publish:
19
36
 
20
37
  ```bash
21
38
  npm publish --otp=123456
@@ -23,7 +40,7 @@
23
40
 
24
41
  Replace `123456` with the current code from your authenticator app.
25
42
 
26
- 4. Verify install:
43
+ 5. Verify install:
27
44
 
28
45
  ```bash
29
46
  openclaw plugins install @gianfrancopiana/openclaw-autoresearch
@@ -1,9 +1,12 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
1
+ import {
2
+ definePluginEntry,
3
+ type OpenClawPluginApi,
4
+ type OpenClawPluginToolContext,
5
+ } from "openclaw/plugin-sdk/core";
2
6
  import {
3
7
  AUTORESEARCH_PLUGIN_DESCRIPTION,
4
8
  AUTORESEARCH_PLUGIN_ID,
5
9
  AUTORESEARCH_PLUGIN_NAME,
6
- autoresearchPluginConfigSchema,
7
10
  } from "./src/config.js";
8
11
  import { createInitExperimentTool } from "./src/tools/init-experiment.js";
9
12
  import { createRunExperimentTool } from "./src/tools/run-experiment.js";
@@ -12,19 +15,43 @@ import { createAutoresearchStatusTool } from "./src/tools/autoresearch-status.js
12
15
  import { registerAutoresearchHooks } from "./src/hooks.js";
13
16
  import { registerAutoresearchCommand } from "./src/commands/autoresearch.js";
14
17
 
15
- const plugin = {
18
+ function createToolFactory<TTool>(
19
+ createTool: (
20
+ api: OpenClawPluginApi,
21
+ toolContext?: Pick<OpenClawPluginToolContext, "sessionKey" | "sessionId" | "workspaceDir">,
22
+ ) => TTool,
23
+ api: OpenClawPluginApi,
24
+ ) {
25
+ return (toolContext: OpenClawPluginToolContext) => createTool(api, toolContext);
26
+ }
27
+
28
+ function registerRequiredTool<TTool>(
29
+ api: OpenClawPluginApi,
30
+ name: string,
31
+ createTool: (
32
+ api: OpenClawPluginApi,
33
+ toolContext?: Pick<OpenClawPluginToolContext, "sessionKey" | "sessionId" | "workspaceDir">,
34
+ ) => TTool,
35
+ ) {
36
+ api.registerTool(
37
+ createToolFactory(createTool, api) as Parameters<OpenClawPluginApi["registerTool"]>[0],
38
+ {
39
+ name,
40
+ optional: false,
41
+ },
42
+ );
43
+ }
44
+
45
+ export default definePluginEntry({
16
46
  id: AUTORESEARCH_PLUGIN_ID,
17
47
  name: AUTORESEARCH_PLUGIN_NAME,
18
48
  description: AUTORESEARCH_PLUGIN_DESCRIPTION,
19
- configSchema: autoresearchPluginConfigSchema,
20
49
  register(api: OpenClawPluginApi) {
21
50
  registerAutoresearchHooks(api);
22
51
  registerAutoresearchCommand(api);
23
- api.registerTool(createInitExperimentTool(api));
24
- api.registerTool(createRunExperimentTool(api));
25
- api.registerTool(createLogExperimentTool(api));
26
- api.registerTool(createAutoresearchStatusTool(api));
52
+ registerRequiredTool(api, "init_experiment", createInitExperimentTool);
53
+ registerRequiredTool(api, "run_experiment", createRunExperimentTool);
54
+ registerRequiredTool(api, "log_experiment", createLogExperimentTool);
55
+ registerRequiredTool(api, "autoresearch_status", createAutoresearchStatusTool);
27
56
  },
28
- };
29
-
30
- export default plugin;
57
+ });
@@ -1,5 +1,5 @@
1
1
  import * as fs from "node:fs";
2
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
3
3
  import {
4
4
  AUTORESEARCH_ROOT_FILES,
5
5
  getAutoresearchRootFilePath,
@@ -8,10 +8,9 @@ import {
8
8
  import { reconstructStateFromJsonl } from "../state.js";
9
9
  import { formatAutoresearchStatusText } from "../tools/autoresearch-status.js";
10
10
  import {
11
- clearAutoresearchSteers,
11
+ clearAutoresearchRuntimeState,
12
12
  getAutoresearchRuntimeState,
13
13
  setAutoresearchPendingCommand,
14
- setAutoresearchRunInFlight,
15
14
  setAutoresearchRuntimeMode,
16
15
  } from "../runtime-state.js";
17
16
  import {
@@ -19,11 +18,16 @@ import {
19
18
  getAutoresearchSessionLockStatus,
20
19
  removeAutoresearchSessionLock,
21
20
  } from "../session-lock.js";
21
+ import { type AutoresearchScopeRef, resolveAutoresearchScope } from "../scope.js";
22
22
 
23
23
  type CommandContext = {
24
24
  args?: string;
25
25
  channel?: string;
26
26
  senderId?: string;
27
+ sessionKey?: string;
28
+ sessionId?: string;
29
+ workspaceDir?: string;
30
+ runId?: string;
27
31
  cwd?: string;
28
32
  };
29
33
 
@@ -45,7 +49,7 @@ export function registerAutoresearchCommand(api: OpenClawPluginApi): void {
45
49
  description: "Enable, disable, or inspect repo-root autoresearch mode.",
46
50
  acceptsArgs: true,
47
51
  handler: (ctx: CommandContext) => {
48
- const cwd = resolveCommandCwd(api, ctx);
52
+ const scope = resolveCommandScope(ctx);
49
53
  const rawArgs = (ctx.args ?? "").trim();
50
54
  const [verb, ...rest] = rawArgs.split(/\s+/).filter(Boolean);
51
55
  const action = (verb ?? "").toLowerCase();
@@ -53,18 +57,19 @@ export function registerAutoresearchCommand(api: OpenClawPluginApi): void {
53
57
 
54
58
  if (!rawArgs || action === "resume" || action === "on") {
55
59
  return {
56
- text: enableAutoresearchMode(cwd, rawArgs && action !== "resume" && action !== "on" ? rawArgs : remainder),
60
+ text: enableAutoresearchMode(
61
+ scope,
62
+ rawArgs && action !== "resume" && action !== "on" ? rawArgs : remainder,
63
+ ),
57
64
  };
58
65
  }
59
66
  if (action === "setup") {
60
- return { text: primeAutoresearchSetup(cwd, remainder) };
67
+ return { text: primeAutoresearchSetup(scope, remainder) };
61
68
  }
62
69
  if (action === "off") {
63
- setAutoresearchRuntimeMode(cwd, "off");
64
- setAutoresearchPendingCommand(cwd, null);
65
- clearAutoresearchSteers(cwd);
66
- setAutoresearchRunInFlight(cwd, false);
67
- removeAutoresearchSessionLock(cwd);
70
+ clearAutoresearchRuntimeState(scope);
71
+ setAutoresearchRuntimeMode(scope, "off");
72
+ removeAutoresearchSessionLock(scope);
68
73
  return {
69
74
  text: [
70
75
  "Autoresearch mode OFF.",
@@ -73,30 +78,35 @@ export function registerAutoresearchCommand(api: OpenClawPluginApi): void {
73
78
  };
74
79
  }
75
80
  if (action === "status") {
76
- return { text: buildAutoresearchCommandText(cwd, "status") };
81
+ return { text: buildAutoresearchCommandText(scope, "status") };
77
82
  }
78
83
  if (action === "help") {
79
- return { text: `${COMMAND_USAGE}\n\n${buildAutoresearchCommandText(cwd, "default")}` };
84
+ return { text: `${COMMAND_USAGE}\n\n${buildAutoresearchCommandText(scope, "default")}` };
80
85
  }
81
86
 
82
87
  return {
83
- text: enableAutoresearchMode(cwd, rawArgs),
88
+ text: enableAutoresearchMode(scope, rawArgs),
84
89
  };
85
90
  },
86
91
  });
87
92
  }
88
93
 
89
94
  export function buildAutoresearchCommandText(
90
- cwd: string,
95
+ scopeRef: AutoresearchScopeRef,
91
96
  mode: "default" | "status",
92
97
  ): string {
93
- const runtimeState = getAutoresearchRuntimeState(cwd);
94
- const presentFiles = getPresentCanonicalFiles(cwd);
98
+ const scope = resolveAutoresearchScope(scopeRef);
99
+ const runtimeState = getAutoresearchRuntimeState(scopeRef);
100
+ if (!scope.repoDir) {
101
+ return buildWorkspacePendingCommandText(runtimeState);
102
+ }
103
+
104
+ const presentFiles = getPresentCanonicalFiles(scope.repoDir);
95
105
  const presentSessionFiles = presentFiles.filter(
96
106
  (file) => file !== AUTORESEARCH_ROOT_FILES.sessionLock,
97
107
  );
98
108
  const hasSession = presentSessionFiles.length > 0;
99
- const lockStatus = getAutoresearchSessionLockStatus(cwd);
109
+ const lockStatus = getAutoresearchSessionLockStatus(scope);
100
110
 
101
111
  if (!hasSession) {
102
112
  return [
@@ -109,14 +119,18 @@ export function buildAutoresearchCommandText(
109
119
  ].join("\n");
110
120
  }
111
121
 
112
- const state = reconstructStateFromJsonl(cwd);
122
+ const state = reconstructStateFromJsonl(scope.repoDir);
113
123
  const lines = [
114
124
  `Autoresearch session detected at repo root: ${presentFiles.join(", ")}`,
115
125
  `Read \`${AUTORESEARCH_ROOT_FILES.sessionDoc}\` before resuming or changing the loop.`,
116
126
  ];
117
127
 
118
128
  if (mode === "status") {
119
- lines.push("", formatAutoresearchStatusText(state, runtimeState), `Session lock: ${formatLockStatus(lockStatus)}`);
129
+ lines.push(
130
+ "",
131
+ formatAutoresearchStatusText(state, runtimeState),
132
+ `Session lock: ${formatLockStatus(lockStatus)}`,
133
+ );
120
134
  } else if (state.mode === "active" || state.hasSessionDoc) {
121
135
  lines.push(
122
136
  "Use `/autoresearch` or `/autoresearch on` to enable mode for the next agent turn, then continue the upstream loop with `init_experiment`, `run_experiment`, and `log_experiment` as needed.",
@@ -130,25 +144,47 @@ export function buildAutoresearchCommandText(
130
144
  return lines.join("\n");
131
145
  }
132
146
 
133
- function enableAutoresearchMode(cwd: string, args: string | null): string {
134
- const lockStatus = acquireAutoresearchSessionLock(cwd);
135
- if (lockStatus.state === "active" && !lockStatus.ownedByCurrentProcess) {
136
- return [
137
- "Autoresearch mode NOT enabled.",
138
- `Another live autoresearch loop holds autoresearch.lock (PID ${lockStatus.pid}, started ${new Date(lockStatus.timestamp ?? 0).toISOString()}).`,
139
- "Resume that loop instead of creating a parallel session.",
140
- ].join("\n");
147
+ function buildWorkspacePendingCommandText(
148
+ runtimeState: ReturnType<typeof getAutoresearchRuntimeState>,
149
+ ): string {
150
+ return [
151
+ "OpenClaw has not exposed a workspace root for this session yet.",
152
+ "Send a normal message from the target workspace, or invoke an autoresearch tool there, so the plugin can bind `workspaceDir` and inspect the repo-root files.",
153
+ `Runtime mode: ${runtimeState.mode}`,
154
+ `Pending run: ${runtimeState.pendingRun ? "yes" : "no"}`,
155
+ ].join("\n");
156
+ }
157
+
158
+ function enableAutoresearchMode(scopeRef: AutoresearchScopeRef, args: string | null): string {
159
+ const scope = resolveAutoresearchScope(scopeRef);
160
+ if (scope.repoDir) {
161
+ const lockStatus = acquireAutoresearchSessionLock(scope);
162
+ if (lockStatus.state === "active" && !lockStatus.ownedByCurrentSession) {
163
+ return [
164
+ "Autoresearch mode NOT enabled.",
165
+ `Another live autoresearch loop holds autoresearch.lock (PID ${lockStatus.pid}, started ${new Date(lockStatus.timestamp ?? 0).toISOString()}).`,
166
+ "Resume that loop instead of creating a parallel session.",
167
+ ].join("\n");
168
+ }
141
169
  }
142
170
 
143
- setAutoresearchRuntimeMode(cwd, "on");
144
- const presentFiles = getPresentCanonicalFiles(cwd);
171
+ setAutoresearchRuntimeMode(scopeRef, "on");
172
+ const presentFiles = scope.repoDir ? getPresentCanonicalFiles(scope.repoDir) : [];
145
173
  const hasSession = presentFiles.some((file) => file !== AUTORESEARCH_ROOT_FILES.sessionLock);
174
+ setAutoresearchPendingCommand(scopeRef, {
175
+ kind: hasSession ? "resume" : "setup",
176
+ args,
177
+ });
178
+
179
+ if (!scope.repoDir) {
180
+ return [
181
+ "Autoresearch mode ON.",
182
+ "Workspace root is not bound yet, so the next agent or tool turn for this session will resolve it from OpenClaw `workspaceDir` before continuing.",
183
+ args ? `Captured instruction: ${args}` : "Send a normal message to continue once the workspace is available.",
184
+ ].join("\n");
185
+ }
146
186
 
147
187
  if (!hasSession) {
148
- setAutoresearchPendingCommand(cwd, {
149
- kind: "setup",
150
- args,
151
- });
152
188
  return [
153
189
  "Autoresearch mode ON.",
154
190
  "No repo-root session was detected, so the next agent turn will be primed for setup.",
@@ -158,10 +194,6 @@ function enableAutoresearchMode(cwd: string, args: string | null): string {
158
194
  ].join("\n");
159
195
  }
160
196
 
161
- setAutoresearchPendingCommand(cwd, {
162
- kind: "resume",
163
- args,
164
- });
165
197
  return [
166
198
  "Autoresearch mode ON.",
167
199
  `Next agent turn will be primed from \`${AUTORESEARCH_ROOT_FILES.sessionDoc}\` and the canonical repo-root files.`,
@@ -169,35 +201,43 @@ function enableAutoresearchMode(cwd: string, args: string | null): string {
169
201
  ].join("\n");
170
202
  }
171
203
 
172
- function primeAutoresearchSetup(cwd: string, args: string | null): string {
173
- const lockStatus = acquireAutoresearchSessionLock(cwd);
174
- if (lockStatus.state === "active" && !lockStatus.ownedByCurrentProcess) {
175
- return [
176
- "Autoresearch setup NOT primed.",
177
- `Another live autoresearch loop holds autoresearch.lock (PID ${lockStatus.pid}, started ${new Date(lockStatus.timestamp ?? 0).toISOString()}).`,
178
- "Resume that loop instead of starting a parallel setup flow.",
179
- ].join("\n");
204
+ function primeAutoresearchSetup(scopeRef: AutoresearchScopeRef, args: string | null): string {
205
+ const scope = resolveAutoresearchScope(scopeRef);
206
+ if (scope.repoDir) {
207
+ const lockStatus = acquireAutoresearchSessionLock(scope);
208
+ if (lockStatus.state === "active" && !lockStatus.ownedByCurrentSession) {
209
+ return [
210
+ "Autoresearch setup NOT primed.",
211
+ `Another live autoresearch loop holds autoresearch.lock (PID ${lockStatus.pid}, started ${new Date(lockStatus.timestamp ?? 0).toISOString()}).`,
212
+ "Resume that loop instead of starting a parallel setup flow.",
213
+ ].join("\n");
214
+ }
180
215
  }
181
216
 
182
- setAutoresearchRuntimeMode(cwd, "on");
183
- setAutoresearchPendingCommand(cwd, {
217
+ setAutoresearchRuntimeMode(scopeRef, "on");
218
+ setAutoresearchPendingCommand(scopeRef, {
184
219
  kind: "setup",
185
220
  args,
186
221
  });
187
222
 
188
223
  return [
189
224
  "Autoresearch setup primed.",
190
- "The next agent turn will be told to create the canonical repo-root files and start the loop.",
225
+ scope.repoDir
226
+ ? "The next agent turn will be told to create the canonical repo-root files and start the loop."
227
+ : "The next agent turn will wait for OpenClaw to provide a workspace root, then create the canonical repo-root files and start the loop.",
191
228
  "Continue with a normal message on the next turn, or invoke the skill directly with `/skill autoresearch-create`.",
192
229
  args ? `Captured setup instruction: ${args}` : "Add an argument to `/autoresearch setup` if you want a specific goal or constraint carried forward.",
193
230
  ].join("\n");
194
231
  }
195
232
 
196
- function resolveCommandCwd(api: OpenClawPluginApi, ctx: CommandContext): string {
197
- if (typeof ctx.cwd === "string" && ctx.cwd.trim().length > 0) {
198
- return ctx.cwd;
199
- }
200
- return api.resolvePath(".");
233
+ function resolveCommandScope(ctx: CommandContext): AutoresearchScopeRef {
234
+ return {
235
+ sessionKey: ctx.sessionKey,
236
+ sessionId: ctx.sessionId,
237
+ workspaceDir: ctx.workspaceDir,
238
+ runId: ctx.runId,
239
+ legacyCwd: ctx.cwd,
240
+ };
201
241
  }
202
242
 
203
243
  function getPresentCanonicalFiles(cwd: string): string[] {
@@ -1,4 +1,4 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
2
 
3
3
  const OUTPUT_TAIL_LINES = 80;
4
4
  const DEFAULT_TIMEOUT_SECONDS = 600;
@@ -1,4 +1,4 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
2
 
3
3
  const GIT_TIMEOUT_MS = 30_000;
4
4
 
@@ -1,4 +1,4 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
2
  import { AUTORESEARCH_ROOT_FILES } from "./files.js";
3
3
  import { reconstructStateFromJsonl } from "./state.js";
4
4
  import { readAutoresearchCheckpoint } from "./checkpoint.js";
@@ -12,6 +12,11 @@ import {
12
12
  setAutoresearchContinuationReminder,
13
13
  } from "./runtime-state.js";
14
14
  import { removeAutoresearchSessionLock } from "./session-lock.js";
15
+ import {
16
+ forgetAutoresearchScope,
17
+ type AutoresearchScopeRef,
18
+ resolveAutoresearchScope,
19
+ } from "./scope.js";
15
20
 
16
21
  type BeforeAgentStartEvent = {
17
22
  systemPrompt?: string;
@@ -19,9 +24,13 @@ type BeforeAgentStartEvent = {
19
24
 
20
25
  type HookContext = {
21
26
  cwd?: string;
27
+ workspaceDir?: string;
28
+ sessionKey?: string;
29
+ sessionId?: string;
30
+ runId?: string;
22
31
  };
23
32
 
24
- type HookCapablePluginApi = OpenClawPluginApi & {
33
+ type HookCapablePluginApi = {
25
34
  on?: (hookName: string, handler: (event: unknown, ctx: HookContext) => unknown) => void;
26
35
  registerHook?: (
27
36
  hookName: string,
@@ -30,15 +39,10 @@ type HookCapablePluginApi = OpenClawPluginApi & {
30
39
  };
31
40
 
32
41
  export function registerAutoresearchHooks(api: OpenClawPluginApi): void {
33
- const hookApi = api as HookCapablePluginApi;
42
+ const hookApi = api as unknown as HookCapablePluginApi;
34
43
  if (typeof hookApi.on === "function") {
35
44
  hookApi.on("before_prompt_build", (_event, ctx) => {
36
- const cwd = resolveHookCwd(api, ctx);
37
- if (cwd === null) {
38
- return;
39
- }
40
-
41
- const addition = buildBeforePromptBuildContext(cwd);
45
+ const addition = buildBeforePromptBuildContext(resolveHookScope(ctx));
42
46
  if (addition === null) {
43
47
  return;
44
48
  }
@@ -49,12 +53,8 @@ export function registerAutoresearchHooks(api: OpenClawPluginApi): void {
49
53
  });
50
54
 
51
55
  hookApi.on("message_received", (event, ctx) => {
52
- const cwd = resolveHookCwd(api, ctx);
53
- if (cwd === null) {
54
- return;
55
- }
56
-
57
- const state = getAutoresearchRuntimeState(cwd);
56
+ const scope = resolveHookScope(ctx);
57
+ const state = getAutoresearchRuntimeState(scope);
58
58
  if (!state.runInFlight) {
59
59
  return;
60
60
  }
@@ -64,12 +64,12 @@ export function registerAutoresearchHooks(api: OpenClawPluginApi): void {
64
64
  return;
65
65
  }
66
66
 
67
- queueAutoresearchSteer(cwd, messageText);
67
+ queueAutoresearchSteer(scope, messageText);
68
68
  });
69
69
 
70
70
  hookApi.on("before_tool_call", (event, ctx) => {
71
- const cwd = resolveHookCwd(api, ctx);
72
- if (cwd === null || !shouldEnforceAutoresearchMode(cwd)) {
71
+ const scope = resolveHookScope(ctx);
72
+ if (!shouldEnforceAutoresearchMode(scope)) {
73
73
  return;
74
74
  }
75
75
 
@@ -92,25 +92,22 @@ export function registerAutoresearchHooks(api: OpenClawPluginApi): void {
92
92
  });
93
93
 
94
94
  hookApi.on("agent_end", (_event, ctx) => {
95
- const cwd = resolveHookCwd(api, ctx);
96
- if (cwd === null) {
95
+ const scope = resolveAutoresearchScope(resolveHookScope(ctx));
96
+ if (!scope.repoDir) {
97
97
  return;
98
98
  }
99
99
 
100
- const state = reconstructStateFromJsonl(cwd);
100
+ const state = reconstructStateFromJsonl(scope.repoDir);
101
101
  if (state.mode === "active" && state.ideas.hasBacklog) {
102
- setAutoresearchContinuationReminder(cwd, true);
102
+ setAutoresearchContinuationReminder(resolveHookScope(ctx), true);
103
103
  }
104
104
  });
105
105
 
106
106
  hookApi.on("session_end", (_event, ctx) => {
107
- const cwd = resolveHookCwd(api, ctx);
108
- if (cwd === null) {
109
- return;
110
- }
111
-
112
- removeAutoresearchSessionLock(cwd);
113
- clearAutoresearchRuntimeState(cwd);
107
+ const scope = resolveHookScope(ctx);
108
+ removeAutoresearchSessionLock(scope);
109
+ clearAutoresearchRuntimeState(scope);
110
+ forgetAutoresearchScope(scope);
114
111
  });
115
112
  return;
116
113
  }
@@ -119,13 +116,8 @@ export function registerAutoresearchHooks(api: OpenClawPluginApi): void {
119
116
  return;
120
117
  }
121
118
 
122
- hookApi.registerHook("before_agent_start", (event, ctx) => {
123
- const cwd = resolveHookCwd(api, ctx);
124
- if (cwd === null) {
125
- return;
126
- }
127
-
128
- const addition = buildBeforePromptBuildContext(cwd);
119
+ hookApi.registerHook("before_agent_start", (event: BeforeAgentStartEvent, ctx: HookContext) => {
120
+ const addition = buildBeforePromptBuildContext(resolveHookScope(ctx));
129
121
  if (addition === null) {
130
122
  return;
131
123
  }
@@ -137,11 +129,16 @@ export function registerAutoresearchHooks(api: OpenClawPluginApi): void {
137
129
  });
138
130
  }
139
131
 
140
- export function buildBeforePromptBuildContext(cwd: string): string | null {
141
- const state = reconstructStateFromJsonl(cwd);
142
- const runtimeState = getAutoresearchRuntimeState(cwd);
143
- const checkpoint = readAutoresearchCheckpoint(cwd);
144
- if (!shouldEnforceAutoresearchMode(cwd, state, runtimeState)) {
132
+ export function buildBeforePromptBuildContext(scopeRef: AutoresearchScopeRef): string | null {
133
+ const scope = resolveAutoresearchScope(scopeRef);
134
+ if (!scope.repoDir) {
135
+ return null;
136
+ }
137
+
138
+ const state = reconstructStateFromJsonl(scope.repoDir);
139
+ const runtimeState = getAutoresearchRuntimeState(scopeRef);
140
+ const checkpoint = readAutoresearchCheckpoint(scope.repoDir);
141
+ if (!shouldEnforceAutoresearchMode(scopeRef, state, runtimeState)) {
145
142
  return null;
146
143
  }
147
144
 
@@ -150,8 +147,8 @@ export function buildBeforePromptBuildContext(cwd: string): string | null {
150
147
  AUTORESEARCH_ROOT_FILES.runnerScript,
151
148
  AUTORESEARCH_ROOT_FILES.resultsLog,
152
149
  ];
153
- const pendingCommand = consumeAutoresearchPendingCommand(cwd);
154
- const needsContinuationReminder = consumeAutoresearchContinuationReminder(cwd);
150
+ const pendingCommand = consumeAutoresearchPendingCommand(scopeRef);
151
+ const needsContinuationReminder = consumeAutoresearchContinuationReminder(scopeRef);
155
152
 
156
153
  const lines = ["", "", "## Autoresearch Mode (ACTIVE)"];
157
154
 
@@ -195,7 +192,7 @@ export function buildBeforePromptBuildContext(cwd: string): string | null {
195
192
  }
196
193
 
197
194
  lines.push(
198
- `For discard or crash results, log_experiment records the outcome but does not revert your tree for you. Run \`git checkout -- .\` yourself after logging when you want to discard tracked changes.`,
195
+ "For discard or crash results, log_experiment records the outcome but does not revert your tree for you. Run `git checkout -- .` yourself after logging when you want to discard tracked changes.",
199
196
  );
200
197
 
201
198
  if (state.ideas.hasBacklog) {
@@ -219,17 +216,14 @@ export function buildBeforePromptBuildContext(cwd: string): string | null {
219
216
  return lines.join("\n");
220
217
  }
221
218
 
222
- function resolveHookCwd(api: OpenClawPluginApi, ctx: HookContext | undefined): string | null {
223
- if (ctx && typeof ctx.cwd === "string" && ctx.cwd.trim().length > 0) {
224
- return ctx.cwd;
225
- }
226
-
227
- try {
228
- const resolved = api.resolvePath(".");
229
- return resolved.trim().length > 0 ? resolved : null;
230
- } catch {
231
- return null;
232
- }
219
+ function resolveHookScope(ctx: HookContext | undefined): Exclude<AutoresearchScopeRef, string> {
220
+ return {
221
+ sessionKey: ctx?.sessionKey,
222
+ sessionId: ctx?.sessionId,
223
+ workspaceDir: ctx?.workspaceDir,
224
+ runId: ctx?.runId,
225
+ legacyCwd: ctx?.cwd,
226
+ };
233
227
  }
234
228
 
235
229
  function extractMessageText(event: unknown): string | null {
@@ -278,15 +272,18 @@ function isCommandLikeMessage(text: string): boolean {
278
272
  }
279
273
 
280
274
  function shouldEnforceAutoresearchMode(
281
- cwd: string,
282
- state = reconstructStateFromJsonl(cwd),
283
- runtimeState = getAutoresearchRuntimeState(cwd),
275
+ scopeRef: AutoresearchScopeRef,
276
+ state = (() => {
277
+ const scope = resolveAutoresearchScope(scopeRef);
278
+ return scope.repoDir ? reconstructStateFromJsonl(scope.repoDir) : null;
279
+ })(),
280
+ runtimeState = getAutoresearchRuntimeState(scopeRef),
284
281
  ): boolean {
285
282
  return (
286
283
  runtimeState.mode === "on" ||
287
284
  runtimeState.runInFlight ||
288
285
  runtimeState.pendingRun !== null ||
289
- (runtimeState.mode !== "off" && (state.mode === "active" || state.hasSessionDoc))
286
+ (runtimeState.mode !== "off" && Boolean(state && (state.mode === "active" || state.hasSessionDoc)))
290
287
  );
291
288
  }
292
289