@gianfrancopiana/openclaw-autoresearch 1.0.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/LICENSE +22 -0
- package/README.md +107 -0
- package/docs/non-parity.md +95 -0
- package/extensions/openclaw-autoresearch/index.ts +30 -0
- package/extensions/openclaw-autoresearch/src/commands/autoresearch.ts +182 -0
- package/extensions/openclaw-autoresearch/src/config.ts +9 -0
- package/extensions/openclaw-autoresearch/src/execute.ts +122 -0
- package/extensions/openclaw-autoresearch/src/files.ts +37 -0
- package/extensions/openclaw-autoresearch/src/git.ts +132 -0
- package/extensions/openclaw-autoresearch/src/hooks.ts +234 -0
- package/extensions/openclaw-autoresearch/src/logging.ts +55 -0
- package/extensions/openclaw-autoresearch/src/runtime-state.ts +143 -0
- package/extensions/openclaw-autoresearch/src/state.ts +290 -0
- package/extensions/openclaw-autoresearch/src/tools/autoresearch-status.ts +99 -0
- package/extensions/openclaw-autoresearch/src/tools/init-experiment.ts +90 -0
- package/extensions/openclaw-autoresearch/src/tools/log-experiment.ts +378 -0
- package/extensions/openclaw-autoresearch/src/tools/run-experiment.ts +64 -0
- package/extensions/openclaw-autoresearch/src/tools/schemas.ts +72 -0
- package/extensions/openclaw-autoresearch/src/tools/tool-cwd.ts +19 -0
- package/index.ts +1 -0
- package/openclaw.plugin.json +14 -0
- package/package.json +56 -0
- package/skills/autoresearch-create/SKILL.md +91 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tobi Lutke, David Cortés
|
|
4
|
+
Copyright (c) 2026 Gianfranco Piana
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# openclaw-autoresearch
|
|
2
|
+
|
|
3
|
+
Autonomous experiment loop for any optimization target.
|
|
4
|
+
|
|
5
|
+
Faithful OpenClaw port of [`davebcn87/pi-autoresearch`](https://github.com/davebcn87/pi-autoresearch).
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
The agent runs a loop: edit code, run a benchmark, measure the result, keep or discard. Each iteration is logged. The loop runs autonomously until interrupted.
|
|
10
|
+
|
|
11
|
+
Three tools drive the loop:
|
|
12
|
+
|
|
13
|
+
| Tool | What it does |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `init_experiment` | Configures the session: name, primary metric, unit, direction (lower/higher). Re-calling starts a new segment. |
|
|
16
|
+
| `run_experiment` | Executes a shell command, times it, captures stdout/stderr, returns pass/fail via exit code. |
|
|
17
|
+
| `log_experiment` | Records the result. `keep` auto-commits to git. `discard`/`crash` log without committing. Tracks secondary metrics alongside the primary. |
|
|
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.
|
|
20
|
+
|
|
21
|
+
All state lives in four repo-root files:
|
|
22
|
+
|
|
23
|
+
| File | Purpose |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `autoresearch.md` | Session doc: objective, metrics, files in scope, constraints, what's been tried. A fresh agent reads this to resume. |
|
|
26
|
+
| `autoresearch.sh` | Benchmark script. Outputs `METRIC name=number` lines. |
|
|
27
|
+
| `autoresearch.jsonl` | Structured log: config headers + experiment entries (metric, status, timestamp, segment, commit hash). |
|
|
28
|
+
| `autoresearch.ideas.md` | Backlog of promising ideas not yet tried. Optional. |
|
|
29
|
+
|
|
30
|
+
The design is file-first: any agent can pick up the repo-root files and continue the loop without prior context.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
For local development, link the repo directly into OpenClaw:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
openclaw plugins install -l /absolute/path/to/openclaw-autoresearch
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For a packaged local install, build the tarball and install that artifact:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install
|
|
44
|
+
npm pack
|
|
45
|
+
openclaw plugins install ./gianfrancopiana-openclaw-autoresearch-1.0.0.tgz
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
After publishing to npm, the same package can be installed with:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
openclaw plugins install @gianfrancopiana/openclaw-autoresearch
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
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).
|
|
55
|
+
|
|
56
|
+
Verify:
|
|
57
|
+
|
|
58
|
+
- skill: `autoresearch-create`
|
|
59
|
+
- tools: `init_experiment`, `run_experiment`, `log_experiment`
|
|
60
|
+
- command: `/autoresearch` (recommended)
|
|
61
|
+
- direct skill fallback: `/skill autoresearch-create`
|
|
62
|
+
|
|
63
|
+
Prefer the explicit `/autoresearch` command surface in OpenClaw. The auto-generated native skill alias `/autoresearch_create` may not trigger reliably on some hosts, so use `/skill autoresearch-create` if you need to invoke the skill directly.
|
|
64
|
+
|
|
65
|
+
## Use
|
|
66
|
+
|
|
67
|
+
In the repo you want to optimize:
|
|
68
|
+
|
|
69
|
+
1. Load the plugin.
|
|
70
|
+
2. Run `/autoresearch` or `/autoresearch setup <goal>`.
|
|
71
|
+
3. Send a normal message with the goal, command, metric (+ direction), files in scope, and constraints.
|
|
72
|
+
4. If you need the raw skill invocation, use `/skill autoresearch-create`.
|
|
73
|
+
5. The agent writes `autoresearch.md` and `autoresearch.sh`, runs a baseline, then starts looping.
|
|
74
|
+
6. Use `/autoresearch` or `/autoresearch status` to re-prime context on a later turn.
|
|
75
|
+
|
|
76
|
+
To resume an existing session, a new agent reads the repo-root files and continues from where the last one stopped.
|
|
77
|
+
|
|
78
|
+
### User steers
|
|
79
|
+
|
|
80
|
+
Messages sent while an experiment is running are queued and surfaced after the next `log_experiment`. The agent finishes the current experiment before incorporating the steer.
|
|
81
|
+
|
|
82
|
+
### Ideas backlog
|
|
83
|
+
|
|
84
|
+
When the agent discovers promising but complex ideas mid-loop, it appends them to `autoresearch.ideas.md`. On resume, the agent reads the backlog, prunes stale entries, and uses the remaining ideas as experiment paths.
|
|
85
|
+
|
|
86
|
+
## Upstream reference
|
|
87
|
+
|
|
88
|
+
This port preserves upstream semantics, names, and file contracts while adapting presentation to OpenClaw. There is no Pi-style widget, dashboard, or editor shortcut layer. Remaining differences are tracked in [`docs/non-parity.md`](docs/non-parity.md).
|
|
89
|
+
|
|
90
|
+
- upstream repo: `https://github.com/davebcn87/pi-autoresearch`
|
|
91
|
+
- pinned upstream commit: `2227029fa5712944a36938b5fe59f709cb30ed22` (`2227029f`)
|
|
92
|
+
|
|
93
|
+
## Validation
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npm install --include=dev
|
|
97
|
+
npm run typecheck
|
|
98
|
+
npm test
|
|
99
|
+
npm run validate
|
|
100
|
+
npm run release:verify
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The local test shim supports typechecking and tests without a full OpenClaw host checkout. Runtime behavior depends on a real OpenClaw host.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Non-Parity Notes for the OpenClaw Port
|
|
2
|
+
|
|
3
|
+
This repo aims to be a **faithful port** of `davebcn87/pi-autoresearch`, but it will not be a literal 1:1 port of Pi's UI/runtime.
|
|
4
|
+
|
|
5
|
+
Pinned upstream reference:
|
|
6
|
+
|
|
7
|
+
- Repo: `https://github.com/davebcn87/pi-autoresearch`
|
|
8
|
+
- Commit: `2227029fa5712944a36938b5fe59f709cb30ed22` (`2227029f`)
|
|
9
|
+
|
|
10
|
+
## Principle
|
|
11
|
+
|
|
12
|
+
Non-parity is acceptable only when it is forced by the host/runtime difference.
|
|
13
|
+
|
|
14
|
+
That means:
|
|
15
|
+
- preserve semantics first
|
|
16
|
+
- preserve names and file layout second
|
|
17
|
+
- adapt presentation/runtime integration last
|
|
18
|
+
|
|
19
|
+
## Expected non-parity in v1
|
|
20
|
+
|
|
21
|
+
### 1. No Pi widget parity
|
|
22
|
+
The Pi extension renders an always-visible status widget above the editor.
|
|
23
|
+
|
|
24
|
+
OpenClaw may provide a thinner status surface instead, such as:
|
|
25
|
+
- tool output
|
|
26
|
+
- command output
|
|
27
|
+
- lightweight summaries
|
|
28
|
+
|
|
29
|
+
### 2. No fullscreen dashboard / TUI parity
|
|
30
|
+
Pi provides an inline dashboard with keyboard interaction.
|
|
31
|
+
|
|
32
|
+
OpenClaw v1 should not try to fake this with a new UI system. If a status view exists, it should be thin and optional.
|
|
33
|
+
|
|
34
|
+
### 3. No keyboard shortcut parity
|
|
35
|
+
Pi has `Ctrl+X` and `Escape` affordances tied to its editor runtime.
|
|
36
|
+
|
|
37
|
+
These are considered host-specific and are not part of the core port contract.
|
|
38
|
+
|
|
39
|
+
### 4. Lifecycle hook names will differ
|
|
40
|
+
Pi uses hooks such as:
|
|
41
|
+
- `session_start`
|
|
42
|
+
- `session_switch`
|
|
43
|
+
- `session_fork`
|
|
44
|
+
- `session_tree`
|
|
45
|
+
- `before_agent_start`
|
|
46
|
+
- `agent_end`
|
|
47
|
+
- `input`
|
|
48
|
+
|
|
49
|
+
OpenClaw has a different hook model. We should preserve intent, not literal event names.
|
|
50
|
+
|
|
51
|
+
In practice, this port now uses documented OpenClaw lifecycle hooks such as:
|
|
52
|
+
- `before_prompt_build`
|
|
53
|
+
- `message_received`
|
|
54
|
+
- `agent_end`
|
|
55
|
+
- `session_end`
|
|
56
|
+
|
|
57
|
+
The result should preserve queued-steer handling and ideas-backlog continuation intent without pretending that Pi's hook names or UI affordances exist in OpenClaw.
|
|
58
|
+
|
|
59
|
+
### 5. `/autoresearch` command is thinner than Pi
|
|
60
|
+
The upstream repo includes a dedicated `/autoresearch` dashboard/entry surface.
|
|
61
|
+
|
|
62
|
+
OpenClaw v1 keeps the main UX command-first and implements `/autoresearch` as a mode and status helper that:
|
|
63
|
+
- provides a stable explicit entrypoint when host-native skill aliases vary
|
|
64
|
+
- detects canonical repo-root files
|
|
65
|
+
- enables or disables autoresearch mode for later agent turns
|
|
66
|
+
- offers terse status text
|
|
67
|
+
- points the agent back to `autoresearch.md`
|
|
68
|
+
|
|
69
|
+
It is intentionally not a dashboard replacement or a fullscreen UI.
|
|
70
|
+
|
|
71
|
+
## Non-parity that is **not** acceptable
|
|
72
|
+
|
|
73
|
+
The following would be design drift, not justified non-parity:
|
|
74
|
+
|
|
75
|
+
- moving canonical runtime files under `.autoresearch/` in v1
|
|
76
|
+
- renaming `init_experiment`, `run_experiment`, or `log_experiment`
|
|
77
|
+
- making a provider/runtime the product identity
|
|
78
|
+
- replacing the explicit command and direct-skill setup surface with a provider-specific worker-first UX
|
|
79
|
+
- changing keep/discard/crash behavior for convenience
|
|
80
|
+
- relying on hidden runtime state instead of file-first resumability
|
|
81
|
+
|
|
82
|
+
## Honest product statement for v1
|
|
83
|
+
|
|
84
|
+
The correct way to describe v1 is:
|
|
85
|
+
|
|
86
|
+
> A faithful OpenClaw port of `pi-autoresearch` that preserves upstream semantics, names, and file contracts, while explicitly not matching Pi's editor widget/dashboard UX.
|
|
87
|
+
|
|
88
|
+
## Future parity work
|
|
89
|
+
|
|
90
|
+
Possible later work, if OpenClaw surfaces support it cleanly:
|
|
91
|
+
- richer status presentation
|
|
92
|
+
- optional command polish
|
|
93
|
+
- optional provider adapters behind the plugin boundary
|
|
94
|
+
|
|
95
|
+
These should remain secondary to semantic fidelity.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import {
|
|
3
|
+
AUTORESEARCH_PLUGIN_DESCRIPTION,
|
|
4
|
+
AUTORESEARCH_PLUGIN_ID,
|
|
5
|
+
AUTORESEARCH_PLUGIN_NAME,
|
|
6
|
+
autoresearchPluginConfigSchema,
|
|
7
|
+
} from "./src/config.js";
|
|
8
|
+
import { createInitExperimentTool } from "./src/tools/init-experiment.js";
|
|
9
|
+
import { createRunExperimentTool } from "./src/tools/run-experiment.js";
|
|
10
|
+
import { createLogExperimentTool } from "./src/tools/log-experiment.js";
|
|
11
|
+
import { createAutoresearchStatusTool } from "./src/tools/autoresearch-status.js";
|
|
12
|
+
import { registerAutoresearchHooks } from "./src/hooks.js";
|
|
13
|
+
import { registerAutoresearchCommand } from "./src/commands/autoresearch.js";
|
|
14
|
+
|
|
15
|
+
const plugin = {
|
|
16
|
+
id: AUTORESEARCH_PLUGIN_ID,
|
|
17
|
+
name: AUTORESEARCH_PLUGIN_NAME,
|
|
18
|
+
description: AUTORESEARCH_PLUGIN_DESCRIPTION,
|
|
19
|
+
configSchema: autoresearchPluginConfigSchema,
|
|
20
|
+
register(api: OpenClawPluginApi) {
|
|
21
|
+
registerAutoresearchHooks(api);
|
|
22
|
+
registerAutoresearchCommand(api);
|
|
23
|
+
api.registerTool(createInitExperimentTool(api));
|
|
24
|
+
api.registerTool(createRunExperimentTool(api));
|
|
25
|
+
api.registerTool(createLogExperimentTool(api));
|
|
26
|
+
api.registerTool(createAutoresearchStatusTool(api));
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default plugin;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
|
+
import {
|
|
4
|
+
AUTORESEARCH_ROOT_FILES,
|
|
5
|
+
getAutoresearchRootFilePath,
|
|
6
|
+
type AutoresearchRootFileKey,
|
|
7
|
+
} from "../files.js";
|
|
8
|
+
import { reconstructStateFromJsonl } from "../state.js";
|
|
9
|
+
import { formatAutoresearchStatusText } from "../tools/autoresearch-status.js";
|
|
10
|
+
import {
|
|
11
|
+
clearAutoresearchSteers,
|
|
12
|
+
getAutoresearchRuntimeState,
|
|
13
|
+
setAutoresearchPendingCommand,
|
|
14
|
+
setAutoresearchRunInFlight,
|
|
15
|
+
setAutoresearchRuntimeMode,
|
|
16
|
+
} from "../runtime-state.js";
|
|
17
|
+
|
|
18
|
+
type CommandContext = {
|
|
19
|
+
args?: string;
|
|
20
|
+
channel?: string;
|
|
21
|
+
senderId?: string;
|
|
22
|
+
cwd?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const COMMAND_USAGE = [
|
|
26
|
+
"Enable or inspect repo-root autoresearch mode.",
|
|
27
|
+
"",
|
|
28
|
+
"Usage:",
|
|
29
|
+
"/autoresearch",
|
|
30
|
+
"/autoresearch on",
|
|
31
|
+
"/autoresearch off",
|
|
32
|
+
"/autoresearch setup",
|
|
33
|
+
"/autoresearch status",
|
|
34
|
+
"/autoresearch help",
|
|
35
|
+
].join("\n");
|
|
36
|
+
|
|
37
|
+
export function registerAutoresearchCommand(api: OpenClawPluginApi): void {
|
|
38
|
+
api.registerCommand({
|
|
39
|
+
name: "autoresearch",
|
|
40
|
+
description: "Enable, disable, or inspect repo-root autoresearch mode.",
|
|
41
|
+
acceptsArgs: true,
|
|
42
|
+
handler: (ctx: CommandContext) => {
|
|
43
|
+
const cwd = resolveCommandCwd(api, ctx);
|
|
44
|
+
const rawArgs = (ctx.args ?? "").trim();
|
|
45
|
+
const [verb, ...rest] = rawArgs.split(/\s+/).filter(Boolean);
|
|
46
|
+
const action = (verb ?? "").toLowerCase();
|
|
47
|
+
const remainder = rest.join(" ").trim() || null;
|
|
48
|
+
|
|
49
|
+
if (!rawArgs || action === "resume" || action === "on") {
|
|
50
|
+
return {
|
|
51
|
+
text: enableAutoresearchMode(cwd, rawArgs && action !== "resume" && action !== "on" ? rawArgs : remainder),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (action === "setup") {
|
|
55
|
+
return { text: primeAutoresearchSetup(cwd, remainder) };
|
|
56
|
+
}
|
|
57
|
+
if (action === "off") {
|
|
58
|
+
setAutoresearchRuntimeMode(cwd, "off");
|
|
59
|
+
setAutoresearchPendingCommand(cwd, null);
|
|
60
|
+
clearAutoresearchSteers(cwd);
|
|
61
|
+
setAutoresearchRunInFlight(cwd, false);
|
|
62
|
+
return {
|
|
63
|
+
text: [
|
|
64
|
+
"Autoresearch mode OFF.",
|
|
65
|
+
`Canonical files remain at repo root: ${Object.values(AUTORESEARCH_ROOT_FILES).join(", ")}`,
|
|
66
|
+
].join("\n"),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (action === "status") {
|
|
70
|
+
return { text: buildAutoresearchCommandText(cwd, "status") };
|
|
71
|
+
}
|
|
72
|
+
if (action === "help") {
|
|
73
|
+
return { text: `${COMMAND_USAGE}\n\n${buildAutoresearchCommandText(cwd, "default")}` };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
text: enableAutoresearchMode(cwd, rawArgs),
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function buildAutoresearchCommandText(
|
|
84
|
+
cwd: string,
|
|
85
|
+
mode: "default" | "status",
|
|
86
|
+
): string {
|
|
87
|
+
const runtimeState = getAutoresearchRuntimeState(cwd);
|
|
88
|
+
const presentFiles = getPresentCanonicalFiles(cwd);
|
|
89
|
+
const hasSession = presentFiles.length > 0;
|
|
90
|
+
|
|
91
|
+
if (!hasSession) {
|
|
92
|
+
return [
|
|
93
|
+
"No repo-root autoresearch session detected.",
|
|
94
|
+
"",
|
|
95
|
+
`Expected canonical files: ${Object.values(AUTORESEARCH_ROOT_FILES).join(", ")}`,
|
|
96
|
+
"Recommended OpenClaw entrypoint: `/autoresearch` or `/autoresearch setup <goal>`.",
|
|
97
|
+
"Direct skill fallback: `/skill autoresearch-create`.",
|
|
98
|
+
].join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const state = reconstructStateFromJsonl(cwd);
|
|
102
|
+
const lines = [
|
|
103
|
+
`Autoresearch session detected at repo root: ${presentFiles.join(", ")}`,
|
|
104
|
+
`Read \`${AUTORESEARCH_ROOT_FILES.sessionDoc}\` before resuming or changing the loop.`,
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
if (mode === "status") {
|
|
108
|
+
lines.push("", formatAutoresearchStatusText(state, runtimeState));
|
|
109
|
+
} else if (state.mode === "active" || state.hasSessionDoc) {
|
|
110
|
+
lines.push(
|
|
111
|
+
"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.",
|
|
112
|
+
);
|
|
113
|
+
} else {
|
|
114
|
+
lines.push(
|
|
115
|
+
`The canonical files exist, but the session brief looks incomplete. Open \`${AUTORESEARCH_ROOT_FILES.sessionDoc}\` and finish setup, or restart via \`/skill autoresearch-create\` if needed.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return lines.join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function enableAutoresearchMode(cwd: string, args: string | null): string {
|
|
123
|
+
setAutoresearchRuntimeMode(cwd, "on");
|
|
124
|
+
const presentFiles = getPresentCanonicalFiles(cwd);
|
|
125
|
+
const hasSession = presentFiles.length > 0;
|
|
126
|
+
|
|
127
|
+
if (!hasSession) {
|
|
128
|
+
setAutoresearchPendingCommand(cwd, {
|
|
129
|
+
kind: "setup",
|
|
130
|
+
args,
|
|
131
|
+
});
|
|
132
|
+
return [
|
|
133
|
+
"Autoresearch mode ON.",
|
|
134
|
+
"No repo-root session was detected, so the next agent turn will be primed for setup.",
|
|
135
|
+
"Next step: send a normal message so the next agent turn can gather setup details.",
|
|
136
|
+
"Direct skill fallback: `/skill autoresearch-create`.",
|
|
137
|
+
args ? `Captured setup instruction: ${args}` : "Run `/autoresearch setup <goal>` to attach a setup hint.",
|
|
138
|
+
].join("\n");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
setAutoresearchPendingCommand(cwd, {
|
|
142
|
+
kind: "resume",
|
|
143
|
+
args,
|
|
144
|
+
});
|
|
145
|
+
return [
|
|
146
|
+
"Autoresearch mode ON.",
|
|
147
|
+
`Next agent turn will be primed from \`${AUTORESEARCH_ROOT_FILES.sessionDoc}\` and the canonical repo-root files.`,
|
|
148
|
+
args ? `Captured resume instruction: ${args}` : "Send a normal message to continue the loop, or use `/autoresearch status` for a snapshot first.",
|
|
149
|
+
].join("\n");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function primeAutoresearchSetup(cwd: string, args: string | null): string {
|
|
153
|
+
setAutoresearchRuntimeMode(cwd, "on");
|
|
154
|
+
setAutoresearchPendingCommand(cwd, {
|
|
155
|
+
kind: "setup",
|
|
156
|
+
args,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return [
|
|
160
|
+
"Autoresearch setup primed.",
|
|
161
|
+
"The next agent turn will be told to create the canonical repo-root files and start the loop.",
|
|
162
|
+
"Continue with a normal message on the next turn, or invoke the skill directly with `/skill autoresearch-create`.",
|
|
163
|
+
args ? `Captured setup instruction: ${args}` : "Add an argument to `/autoresearch setup` if you want a specific goal or constraint carried forward.",
|
|
164
|
+
].join("\n");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function resolveCommandCwd(api: OpenClawPluginApi, ctx: CommandContext): string {
|
|
168
|
+
if (typeof ctx.cwd === "string" && ctx.cwd.trim().length > 0) {
|
|
169
|
+
return ctx.cwd;
|
|
170
|
+
}
|
|
171
|
+
return api.resolvePath(".");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getPresentCanonicalFiles(cwd: string): string[] {
|
|
175
|
+
const present: string[] = [];
|
|
176
|
+
for (const key of Object.keys(AUTORESEARCH_ROOT_FILES) as AutoresearchRootFileKey[]) {
|
|
177
|
+
if (fs.existsSync(getAutoresearchRootFilePath(cwd, key))) {
|
|
178
|
+
present.push(AUTORESEARCH_ROOT_FILES[key]);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return present;
|
|
182
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const AUTORESEARCH_PLUGIN_ID = "openclaw-autoresearch";
|
|
2
|
+
export const AUTORESEARCH_PLUGIN_NAME = "Autoresearch";
|
|
3
|
+
export const AUTORESEARCH_PLUGIN_DESCRIPTION = "Faithful OpenClaw port of pi-autoresearch.";
|
|
4
|
+
|
|
5
|
+
export const autoresearchPluginConfigSchema = {
|
|
6
|
+
type: "object",
|
|
7
|
+
additionalProperties: false,
|
|
8
|
+
properties: {},
|
|
9
|
+
} as const;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const OUTPUT_TAIL_LINES = 80;
|
|
4
|
+
const DEFAULT_TIMEOUT_SECONDS = 600;
|
|
5
|
+
const FORCE_KILL_GRACE_MS = 1_000;
|
|
6
|
+
|
|
7
|
+
export type ExperimentExecutionResult = {
|
|
8
|
+
readonly command: string;
|
|
9
|
+
readonly exitCode: number | null;
|
|
10
|
+
readonly durationSeconds: number;
|
|
11
|
+
readonly passed: boolean;
|
|
12
|
+
readonly crashed: boolean;
|
|
13
|
+
readonly timedOut: boolean;
|
|
14
|
+
readonly tailOutput: string;
|
|
15
|
+
readonly stdout: string;
|
|
16
|
+
readonly stderr: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export async function executeExperimentCommand(options: {
|
|
20
|
+
command: string;
|
|
21
|
+
cwd: string;
|
|
22
|
+
timeoutSeconds?: number;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
}): Promise<ExperimentExecutionResult> {
|
|
25
|
+
const timeoutSeconds = options.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
|
|
26
|
+
const timeoutMs = Math.max(0, timeoutSeconds) * 1_000;
|
|
27
|
+
const startedAt = Date.now();
|
|
28
|
+
|
|
29
|
+
return await new Promise<ExperimentExecutionResult>((resolve) => {
|
|
30
|
+
const child = spawn("bash", ["-c", options.command], {
|
|
31
|
+
cwd: options.cwd,
|
|
32
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
let stdout = "";
|
|
36
|
+
let stderr = "";
|
|
37
|
+
let timedOut = false;
|
|
38
|
+
let forceKillTimer: NodeJS.Timeout | undefined;
|
|
39
|
+
|
|
40
|
+
const timeoutTimer =
|
|
41
|
+
timeoutMs > 0
|
|
42
|
+
? setTimeout(() => {
|
|
43
|
+
timedOut = true;
|
|
44
|
+
child.kill("SIGTERM");
|
|
45
|
+
forceKillTimer = setTimeout(() => {
|
|
46
|
+
if (!child.killed) {
|
|
47
|
+
child.kill("SIGKILL");
|
|
48
|
+
}
|
|
49
|
+
}, FORCE_KILL_GRACE_MS);
|
|
50
|
+
}, timeoutMs)
|
|
51
|
+
: undefined;
|
|
52
|
+
|
|
53
|
+
const abortHandler = () => {
|
|
54
|
+
child.kill("SIGTERM");
|
|
55
|
+
forceKillTimer = setTimeout(() => {
|
|
56
|
+
if (!child.killed) {
|
|
57
|
+
child.kill("SIGKILL");
|
|
58
|
+
}
|
|
59
|
+
}, FORCE_KILL_GRACE_MS);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (options.signal) {
|
|
63
|
+
if (options.signal.aborted) {
|
|
64
|
+
abortHandler();
|
|
65
|
+
} else {
|
|
66
|
+
options.signal.addEventListener("abort", abortHandler, { once: true });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
child.stdout.on("data", (chunk: string | Buffer) => {
|
|
71
|
+
stdout += chunk.toString();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
child.stderr.on("data", (chunk: string | Buffer) => {
|
|
75
|
+
stderr += chunk.toString();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
child.on("error", (error) => {
|
|
79
|
+
stderr += `${stderr ? "\n" : ""}${String(error.message || error)}`;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
child.on("close", (code) => {
|
|
83
|
+
if (timeoutTimer) {
|
|
84
|
+
clearTimeout(timeoutTimer);
|
|
85
|
+
}
|
|
86
|
+
if (forceKillTimer) {
|
|
87
|
+
clearTimeout(forceKillTimer);
|
|
88
|
+
}
|
|
89
|
+
if (options.signal) {
|
|
90
|
+
options.signal.removeEventListener("abort", abortHandler);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const durationSeconds = (Date.now() - startedAt) / 1_000;
|
|
94
|
+
const passed = code === 0 && !timedOut;
|
|
95
|
+
|
|
96
|
+
resolve({
|
|
97
|
+
command: options.command,
|
|
98
|
+
exitCode: code,
|
|
99
|
+
durationSeconds,
|
|
100
|
+
passed,
|
|
101
|
+
crashed: !passed,
|
|
102
|
+
timedOut,
|
|
103
|
+
tailOutput: createOutputTail(stdout, stderr),
|
|
104
|
+
stdout,
|
|
105
|
+
stderr,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createOutputTail(stdout: string, stderr: string): string {
|
|
112
|
+
const combined = [stdout, stderr]
|
|
113
|
+
.filter((value) => value.trim().length > 0)
|
|
114
|
+
.join("\n")
|
|
115
|
+
.trim();
|
|
116
|
+
|
|
117
|
+
if (!combined) {
|
|
118
|
+
return "";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return combined.split(/\r?\n/).slice(-OUTPUT_TAIL_LINES).join("\n");
|
|
122
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
export const AUTORESEARCH_ROOT_FILES = {
|
|
4
|
+
sessionDoc: "autoresearch.md",
|
|
5
|
+
runnerScript: "autoresearch.sh",
|
|
6
|
+
resultsLog: "autoresearch.jsonl",
|
|
7
|
+
ideasBacklog: "autoresearch.ideas.md",
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export type AutoresearchRootFileKey = keyof typeof AUTORESEARCH_ROOT_FILES;
|
|
11
|
+
|
|
12
|
+
export function getAutoresearchRootFilePath(
|
|
13
|
+
cwd: string,
|
|
14
|
+
file: AutoresearchRootFileKey,
|
|
15
|
+
): string {
|
|
16
|
+
return `${cwd}/${AUTORESEARCH_ROOT_FILES[file]}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readAutoresearchRootFile(
|
|
20
|
+
cwd: string,
|
|
21
|
+
file: AutoresearchRootFileKey,
|
|
22
|
+
): string | null {
|
|
23
|
+
const filePath = getAutoresearchRootFilePath(cwd, file);
|
|
24
|
+
if (!fs.existsSync(filePath)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return fs.readFileSync(filePath, "utf8");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* PR 2 skeleton only.
|
|
33
|
+
* This module will own canonical root-level file IO helpers in later PRs.
|
|
34
|
+
*/
|
|
35
|
+
export function describeCanonicalFiles(): typeof AUTORESEARCH_ROOT_FILES {
|
|
36
|
+
return AUTORESEARCH_ROOT_FILES;
|
|
37
|
+
}
|