lazyclaw 6.3.1 → 6.5.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/README.ko.md +5 -1
- package/README.md +17 -13
- package/agents.mjs +54 -4
- package/channels-discord/index.mjs +5 -1
- package/channels-email/index.mjs +4 -3
- package/channels-whatsapp/index.mjs +3 -2
- package/chat_window.mjs +11 -2
- package/cli.mjs +103 -3
- package/commands/agents.mjs +7 -193
- package/commands/agents_registry.mjs +205 -0
- package/commands/auth_nodes.mjs +19 -1
- package/commands/automation.mjs +28 -94
- package/commands/automation_loops.mjs +94 -0
- package/commands/channels.mjs +46 -3
- package/commands/chat.mjs +127 -704
- package/commands/chat_hardening.mjs +23 -0
- package/commands/chat_legacy_slash.mjs +716 -0
- package/commands/config.mjs +36 -2
- package/commands/daemon.mjs +99 -1
- package/commands/gateway.mjs +123 -4
- package/commands/help_text.mjs +78 -0
- package/commands/mcp.mjs +150 -0
- package/commands/misc.mjs +20 -0
- package/commands/sessions.mjs +68 -8
- package/commands/setup.mjs +44 -80
- package/commands/setup_channels.mjs +219 -47
- package/commands/skills.mjs +19 -1
- package/commands/workflow_named.mjs +137 -0
- package/config-validate.mjs +10 -1
- package/config_features.mjs +45 -0
- package/cron.mjs +19 -8
- package/daemon/lib/auth.mjs +25 -0
- package/daemon/lib/cost.mjs +41 -0
- package/daemon/lib/respond.mjs +20 -1
- package/daemon/lib/team_inbound.mjs +69 -0
- package/daemon/route_table.mjs +5 -1
- package/daemon/routes/_deps.mjs +2 -2
- package/daemon/routes/config.mjs +11 -6
- package/daemon/routes/conversation.mjs +103 -40
- package/daemon/routes/events.mjs +45 -0
- package/daemon/routes/meta.mjs +38 -7
- package/daemon/routes/providers.mjs +26 -6
- package/daemon/routes/workflows.mjs +30 -0
- package/daemon.mjs +27 -3
- package/dotenv_min.mjs +15 -3
- package/gateway/challenge_registry.mjs +90 -0
- package/gateway/device_auth.mjs +71 -97
- package/gateway/http_gateway.mjs +29 -3
- package/lib/args.mjs +69 -3
- package/lib/config.mjs +85 -3
- package/lib/render.mjs +43 -0
- package/lib/service_install.mjs +15 -1
- package/mas/agent_turn.mjs +68 -12
- package/mas/confidence.mjs +20 -1
- package/mas/embedder.mjs +100 -0
- package/mas/events.mjs +57 -0
- package/mas/index_db.mjs +103 -10
- package/mas/learning.mjs +96 -72
- package/mas/mention_router.mjs +48 -90
- package/mas/prompt_stack.mjs +60 -3
- package/mas/recall_blend.mjs +56 -0
- package/mas/redact.mjs +55 -0
- package/mas/router_posting.mjs +96 -0
- package/mas/router_termination.mjs +63 -0
- package/mas/scrub_env.mjs +21 -6
- package/mas/skill_synth.mjs +7 -33
- package/mas/tool_runner.mjs +3 -2
- package/mas/tools/bash.mjs +9 -2
- package/mas/tools/coding.mjs +28 -5
- package/mas/tools/delegation.mjs +34 -4
- package/mas/tools/git.mjs +19 -9
- package/mas/tools/ha.mjs +2 -0
- package/mas/tools/learning.mjs +55 -0
- package/mas/tools/media.mjs +21 -3
- package/mas/tools/os.mjs +70 -16
- package/mas/tools/recall.mjs +28 -2
- package/mcp/client.mjs +8 -1
- package/mcp/server_spawn.mjs +5 -1
- package/memory.mjs +24 -0
- package/package.json +3 -1
- package/providers/anthropic.mjs +101 -6
- package/providers/cache.mjs +9 -1
- package/providers/claude_cli.mjs +104 -22
- package/providers/claude_cli_session.mjs +183 -0
- package/providers/cli_error.mjs +38 -0
- package/providers/cli_login.mjs +179 -0
- package/providers/codex_cli.mjs +70 -12
- package/providers/gemini.mjs +104 -3
- package/providers/gemini_cli.mjs +78 -17
- package/providers/model_catalogue.mjs +33 -2
- package/providers/ollama.mjs +104 -3
- package/providers/openai.mjs +110 -8
- package/providers/openai_compat.mjs +97 -6
- package/providers/orchestrator.mjs +112 -12
- package/providers/registry.mjs +15 -9
- package/providers/retry.mjs +14 -2
- package/providers/tool_use/anthropic.mjs +17 -3
- package/providers/tool_use/claude_cli.mjs +72 -20
- package/providers/tool_use/gemini.mjs +29 -2
- package/providers/tool_use/openai.mjs +35 -5
- package/sandbox/confiners/seatbelt.mjs +37 -12
- package/sandbox/index.mjs +49 -0
- package/sandbox/local.mjs +7 -1
- package/sandbox/spawn.mjs +144 -0
- package/sandbox.mjs +5 -1
- package/scripts/loop-worker.mjs +25 -1
- package/sessions.mjs +0 -0
- package/skills/channel-style.md +20 -0
- package/skills/code-review.md +33 -0
- package/skills/commit-message.md +30 -0
- package/skills/concise.md +24 -0
- package/skills/debug-coach.md +25 -0
- package/skills/explain.md +24 -0
- package/skills/korean.md +25 -0
- package/skills/summarize.md +33 -0
- package/skills.mjs +24 -2
- package/skills_curator.mjs +6 -0
- package/skills_install.mjs +10 -2
- package/tasks.mjs +6 -1
- package/teams.mjs +78 -0
- package/tui/banner.mjs +72 -0
- package/tui/chat_mode_slash.mjs +59 -0
- package/tui/config_picker.mjs +1 -0
- package/tui/editor.mjs +0 -0
- package/tui/editor_anchor.mjs +46 -0
- package/tui/editor_keys.mjs +275 -0
- package/tui/hud.mjs +111 -0
- package/tui/login_flow.mjs +113 -0
- package/tui/modal_picker.mjs +10 -1
- package/tui/model_pick.mjs +287 -0
- package/tui/orchestrator_flow.mjs +164 -0
- package/tui/orchestrator_setup.mjs +135 -0
- package/tui/pickers.mjs +218 -248
- package/tui/repl.mjs +118 -209
- package/tui/repl_altbuffer.mjs +63 -0
- package/tui/repl_reducers.mjs +114 -0
- package/tui/repl_reset.mjs +37 -0
- package/tui/run_turn.mjs +228 -26
- package/tui/slash_args.mjs +159 -0
- package/tui/slash_channels.mjs +208 -0
- package/tui/slash_commands.mjs +7 -5
- package/tui/slash_dashboard.mjs +220 -0
- package/tui/slash_dispatcher.mjs +339 -774
- package/tui/slash_helpers.mjs +68 -0
- package/tui/slash_popup.mjs +5 -1
- package/tui/slash_trainer.mjs +173 -0
- package/tui/splash.mjs +15 -2
- package/tui/status_bar.mjs +26 -0
- package/tui/theme.mjs +28 -0
- package/web/avatars/01.png +0 -0
- package/web/avatars/02.png +0 -0
- package/web/avatars/03.png +0 -0
- package/web/avatars/04.png +0 -0
- package/web/avatars/05.png +0 -0
- package/web/avatars/06.png +0 -0
- package/web/avatars/07.png +0 -0
- package/web/avatars/08.png +0 -0
- package/web/avatars/09.png +0 -0
- package/web/avatars/10.png +0 -0
- package/web/avatars/11.png +0 -0
- package/web/avatars/12.png +0 -0
- package/web/avatars/13.png +0 -0
- package/web/avatars/14.png +0 -0
- package/web/avatars/15.png +0 -0
- package/web/avatars/16.png +0 -0
- package/web/avatars/17.png +0 -0
- package/web/avatars/18.png +0 -0
- package/web/avatars/19.png +0 -0
- package/web/avatars/20.png +0 -0
- package/web/dashboard.css +77 -0
- package/web/dashboard.html +29 -2
- package/web/dashboard.js +296 -33
- package/workflow/builtin_caps.mjs +94 -0
- package/workflow/declarative.mjs +101 -0
- package/workflow/named.mjs +71 -0
- package/workflow/named_cron.mjs +50 -0
- package/workflow/nodes.mjs +67 -0
- package/workflow/run_request.mjs +74 -0
- package/workflow/yaml_min.mjs +97 -0
package/sandbox/index.mjs
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
// bindings: { '<workerName>': '<kind>' | { kind, ...overrides } },
|
|
17
17
|
// }
|
|
18
18
|
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
19
21
|
import { SANDBOX_KINDS, SandboxError } from './base.mjs';
|
|
20
22
|
import { LocalSandbox } from './local.mjs';
|
|
21
23
|
import { DockerSandbox, parseSandboxSpec } from './docker.mjs';
|
|
@@ -65,3 +67,50 @@ export function resolveSandbox(cfg, workerName) {
|
|
|
65
67
|
};
|
|
66
68
|
return new CTORS[kind](spec);
|
|
67
69
|
}
|
|
70
|
+
|
|
71
|
+
// Directories whose reads are blocked inside a confined child even though reads
|
|
72
|
+
// are otherwise allowed — credential stores + the lazyclaw config dir (which
|
|
73
|
+
// holds auth profiles and channel tokens).
|
|
74
|
+
function defaultDenyRead(homeDir, configDir) {
|
|
75
|
+
const h = homeDir || os.homedir();
|
|
76
|
+
const dirs = [
|
|
77
|
+
path.join(h, '.ssh'),
|
|
78
|
+
path.join(h, '.aws'),
|
|
79
|
+
path.join(h, '.gnupg'),
|
|
80
|
+
path.join(h, '.config', 'gcloud'),
|
|
81
|
+
path.join(h, '.docker'),
|
|
82
|
+
path.join(h, '.kube'),
|
|
83
|
+
path.join(h, '.npmrc'),
|
|
84
|
+
path.join(h, '.netrc'),
|
|
85
|
+
];
|
|
86
|
+
if (configDir) dirs.push(configDir);
|
|
87
|
+
return dirs;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Build the flat sandbox spec applied BY DEFAULT to sensitive tool execution
|
|
91
|
+
// (the tool hot path: bash, python_exec/node_exec, git_*, os_*). Returns null to
|
|
92
|
+
// mean "no confinement" (bare host).
|
|
93
|
+
//
|
|
94
|
+
// Default-on policy (operator-chosen): confine every sensitive child-spawning
|
|
95
|
+
// tool, with the filesystem confined to the workspace (cwd) + temp, secret dirs
|
|
96
|
+
// unreadable, and network ALLOWED. Opt out with cfg.sandbox.confine === false or
|
|
97
|
+
// cfg.sandbox.default of 'off'/'none'. An explicitly-configured docker backend
|
|
98
|
+
// is honoured instead of local confinement.
|
|
99
|
+
//
|
|
100
|
+
// This is threaded in by the production entrypoints (task tick, agentic chat,
|
|
101
|
+
// task_spawn); the library defaults of runTool/runAgentTurn stay null so direct
|
|
102
|
+
// API callers and unit tests remain byte-stable.
|
|
103
|
+
export function defaultSandboxSpec(cfg, { cwd, configDir } = {}) {
|
|
104
|
+
const sb = (cfg && cfg.sandbox) || {};
|
|
105
|
+
if (sb.confine === false || sb.default === 'off' || sb.default === 'none') return null;
|
|
106
|
+
if (sb.default === 'docker' && sb.docker && sb.docker.image) {
|
|
107
|
+
return { kind: 'docker', ...sb.docker };
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
kind: 'local',
|
|
111
|
+
confiner: sb.local?.confiner || 'auto',
|
|
112
|
+
readWrite: [cwd || process.cwd()],
|
|
113
|
+
denyRead: defaultDenyRead(sb.homeDir, configDir),
|
|
114
|
+
allowNet: sb.allowNet !== false,
|
|
115
|
+
};
|
|
116
|
+
}
|
package/sandbox/local.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { spawn, spawnSync } from 'node:child_process';
|
|
5
5
|
import { Sandbox, SandboxSession, SandboxError } from './base.mjs';
|
|
6
|
+
import { pickAvailableConfiner } from './spawn.mjs';
|
|
6
7
|
import * as seatbelt from './confiners/seatbelt.mjs';
|
|
7
8
|
import * as bubblewrap from './confiners/bubblewrap.mjs';
|
|
8
9
|
import * as firejail from './confiners/firejail.mjs';
|
|
@@ -45,7 +46,12 @@ class LocalSession extends SandboxSession {
|
|
|
45
46
|
export class LocalSandbox extends Sandbox {
|
|
46
47
|
constructor(spec) {
|
|
47
48
|
super(spec);
|
|
48
|
-
|
|
49
|
+
// Resolve 'auto' to a concrete confiner (or none) at construction so
|
|
50
|
+
// _wrap() never sees 'auto' and the genuinely-unknown-key guard below
|
|
51
|
+
// still fires for typos. 'auto' that resolves to 'none' → no wrapping.
|
|
52
|
+
const key = spec.confiner === 'auto'
|
|
53
|
+
? pickAvailableConfiner()
|
|
54
|
+
: (spec.confiner || 'none');
|
|
49
55
|
if (key !== 'none' && !(key in CONFINERS)) {
|
|
50
56
|
throw new SandboxError(`unknown confiner "${key}"`, 'SANDBOX_BAD_CONFINER');
|
|
51
57
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// sandbox/spawn.mjs — unified spawn dispatcher + auto-confiner.
|
|
2
|
+
//
|
|
3
|
+
// Bridges the docker-spec shim (used by mas/tools/bash.mjs and
|
|
4
|
+
// providers/{claude,codex,gemini}_cli.mjs via sandbox.mjs) with the v5
|
|
5
|
+
// confiner backend. Today only docker specs and null are handled by the
|
|
6
|
+
// docker.mjs spawnSandboxed; this module adds {kind:'local', confiner:...}
|
|
7
|
+
// support by wrapping the child argv via a confiner module.
|
|
8
|
+
//
|
|
9
|
+
// CAPABILITY-ONLY: no caller passes a local spec yet, so null and docker
|
|
10
|
+
// paths are byte-identical to docker.mjs's spawnSandboxed and there is zero
|
|
11
|
+
// behaviour change. The default-OFF posture is unchanged.
|
|
12
|
+
|
|
13
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
14
|
+
import { buildDockerArgs } from './docker.mjs';
|
|
15
|
+
import { SandboxError } from './base.mjs';
|
|
16
|
+
import * as seatbelt from './confiners/seatbelt.mjs';
|
|
17
|
+
import * as bubblewrap from './confiners/bubblewrap.mjs';
|
|
18
|
+
import * as firejail from './confiners/firejail.mjs';
|
|
19
|
+
import * as landlock from './confiners/landlock.mjs';
|
|
20
|
+
|
|
21
|
+
const CONFINERS = { seatbelt, bubblewrap, firejail, landlock };
|
|
22
|
+
|
|
23
|
+
// Process-wide memo of the real-probe result, keyed by platform. Auto-confiner
|
|
24
|
+
// selection sits on a hot path (every confined child spawn), so we avoid
|
|
25
|
+
// repeated execFileSync probes. The memo is bypassed when an explicit `avail`
|
|
26
|
+
// override is supplied (tests need a fresh evaluation per call).
|
|
27
|
+
const _probeMemo = new Map();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Pick the best available confiner for a platform.
|
|
31
|
+
*
|
|
32
|
+
* @param {{platform?: string, avail?: {seatbelt?: boolean, bubblewrap?: boolean, firejail?: boolean}}} [opts]
|
|
33
|
+
* @returns {'seatbelt'|'bubblewrap'|'firejail'|'none'}
|
|
34
|
+
* Never returns 'landlock' — it is fail-closed (available() === false).
|
|
35
|
+
*/
|
|
36
|
+
export function pickAvailableConfiner({ platform = process.platform, avail } = {}) {
|
|
37
|
+
const probe = avail
|
|
38
|
+
? () => avail
|
|
39
|
+
: () => _probeAvailability(platform);
|
|
40
|
+
const a = probe();
|
|
41
|
+
if (platform === 'darwin') {
|
|
42
|
+
return a.seatbelt ? 'seatbelt' : 'none';
|
|
43
|
+
}
|
|
44
|
+
if (platform === 'linux') {
|
|
45
|
+
if (a.bubblewrap) return 'bubblewrap';
|
|
46
|
+
if (a.firejail) return 'firejail';
|
|
47
|
+
return 'none';
|
|
48
|
+
}
|
|
49
|
+
return 'none';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _probeAvailability(platform) {
|
|
53
|
+
if (_probeMemo.has(platform)) return _probeMemo.get(platform);
|
|
54
|
+
const a = {
|
|
55
|
+
seatbelt: seatbelt.available({ platform }),
|
|
56
|
+
bubblewrap: bubblewrap.available(),
|
|
57
|
+
firejail: firejail.available(),
|
|
58
|
+
};
|
|
59
|
+
_probeMemo.set(platform, a);
|
|
60
|
+
return a;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build the WRAPPED argv for a {kind:'local'} spec WITHOUT spawning, so it is
|
|
65
|
+
* unit-testable.
|
|
66
|
+
*
|
|
67
|
+
* @param {{kind:'local', confiner?: string} & Record<string, unknown>} spec
|
|
68
|
+
* @param {string} bin
|
|
69
|
+
* @param {string[]} args
|
|
70
|
+
* @returns {string[]} the wrapped argv ([bin, ...args] when confiner is none)
|
|
71
|
+
*/
|
|
72
|
+
export function buildLocalArgv(spec, bin, args) {
|
|
73
|
+
const requested = spec.confiner === 'auto'
|
|
74
|
+
? pickAvailableConfiner()
|
|
75
|
+
: (spec.confiner || 'none');
|
|
76
|
+
if (requested === 'none') return [bin, ...args];
|
|
77
|
+
const mod = CONFINERS[requested];
|
|
78
|
+
if (!mod) {
|
|
79
|
+
throw new SandboxError(`unknown confiner "${requested}"`, 'SANDBOX_BAD_CONFINER');
|
|
80
|
+
}
|
|
81
|
+
// landlock is fail-closed: its buildArgv throws. Do NOT special-case it.
|
|
82
|
+
return mod.buildArgv([bin, ...args], spec);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Spawn a child under the requested sandbox spec.
|
|
87
|
+
*
|
|
88
|
+
* - null → bare spawn (byte-identical to docker.mjs today)
|
|
89
|
+
* - kind 'docker' → spawn('docker', buildDockerArgs(...)) (byte-identical)
|
|
90
|
+
* - kind 'local' → wrap argv via confiner, then spawn the wrapper
|
|
91
|
+
* - anything else → SANDBOX_UNSUPPORTED
|
|
92
|
+
*
|
|
93
|
+
* @param {object|null} spec
|
|
94
|
+
* @param {string} bin
|
|
95
|
+
* @param {string[]} args
|
|
96
|
+
* @param {object} [spawnOpts]
|
|
97
|
+
* @returns {import('node:child_process').ChildProcess}
|
|
98
|
+
*/
|
|
99
|
+
export function spawnSandboxed(spec, bin, args, spawnOpts = {}) {
|
|
100
|
+
if (!spec) return spawn(bin, args, spawnOpts);
|
|
101
|
+
if (spec.kind === 'docker') {
|
|
102
|
+
const dockerArgs = buildDockerArgs(spec, [bin, ...args], { cwd: spawnOpts.cwd });
|
|
103
|
+
return spawn('docker', dockerArgs, spawnOpts);
|
|
104
|
+
}
|
|
105
|
+
if (spec.kind === 'local') {
|
|
106
|
+
const wrapped = buildLocalArgv(spec, bin, args);
|
|
107
|
+
return spawn(wrapped[0], wrapped.slice(1), spawnOpts);
|
|
108
|
+
}
|
|
109
|
+
throw new SandboxError(
|
|
110
|
+
`spawnSandboxed shim handles docker+local only; got "${spec.kind}"`,
|
|
111
|
+
'SANDBOX_UNSUPPORTED',
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Synchronous mirror of {@link spawnSandboxed} for callers that shell out via
|
|
117
|
+
* spawnSync (e.g. mas/tools/git.mjs). Same kind handling, just sync:
|
|
118
|
+
*
|
|
119
|
+
* - null → bare spawnSync (byte-identical to a plain spawnSync)
|
|
120
|
+
* - kind 'docker' → spawnSync('docker', buildDockerArgs(...)) (byte-identical)
|
|
121
|
+
* - kind 'local' → wrap argv via confiner, then spawnSync the wrapper
|
|
122
|
+
* - anything else → SANDBOX_UNSUPPORTED
|
|
123
|
+
*
|
|
124
|
+
* @param {object|null} spec
|
|
125
|
+
* @param {string} bin
|
|
126
|
+
* @param {string[]} args
|
|
127
|
+
* @param {object} [spawnOpts]
|
|
128
|
+
* @returns {import('node:child_process').SpawnSyncReturns<string>}
|
|
129
|
+
*/
|
|
130
|
+
export function spawnSyncSandboxed(spec, bin, args, spawnOpts = {}) {
|
|
131
|
+
if (!spec) return spawnSync(bin, args, spawnOpts);
|
|
132
|
+
if (spec.kind === 'docker') {
|
|
133
|
+
const dockerArgs = buildDockerArgs(spec, [bin, ...args], { cwd: spawnOpts.cwd });
|
|
134
|
+
return spawnSync('docker', dockerArgs, spawnOpts);
|
|
135
|
+
}
|
|
136
|
+
if (spec.kind === 'local') {
|
|
137
|
+
const wrapped = buildLocalArgv(spec, bin, args);
|
|
138
|
+
return spawnSync(wrapped[0], wrapped.slice(1), spawnOpts);
|
|
139
|
+
}
|
|
140
|
+
throw new SandboxError(
|
|
141
|
+
`spawnSyncSandboxed shim handles docker+local only; got "${spec.kind}"`,
|
|
142
|
+
'SANDBOX_UNSUPPORTED',
|
|
143
|
+
);
|
|
144
|
+
}
|
package/sandbox.mjs
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
export {
|
|
7
7
|
parseSandboxSpec,
|
|
8
8
|
buildDockerArgs,
|
|
9
|
-
spawnSandboxed,
|
|
10
9
|
} from './sandbox/docker.mjs';
|
|
10
|
+
// spawnSandboxed now comes from the unified dispatcher, which adds
|
|
11
|
+
// {kind:'local'} confiner support on top of the byte-identical null/docker
|
|
12
|
+
// paths. bash.mjs + the 3 CLI providers import it via this shim, so they
|
|
13
|
+
// transparently get the upgraded dispatcher.
|
|
14
|
+
export { spawnSandboxed, spawnSyncSandboxed } from './sandbox/spawn.mjs';
|
|
11
15
|
export { SandboxError } from './sandbox/base.mjs';
|
package/scripts/loop-worker.mjs
CHANGED
|
@@ -64,10 +64,15 @@ const sessions = await import(path.join(REPO_ROOT, 'sessions.mjs'));
|
|
|
64
64
|
const loopEngine = await import(path.join(REPO_ROOT, 'loop-engine.mjs'));
|
|
65
65
|
const registryUrl = path.join(REPO_ROOT, 'providers', 'registry.mjs');
|
|
66
66
|
const { PROVIDERS } = await import(registryUrl);
|
|
67
|
+
const { readConfig, _resolveAuthKey } = await import(path.join(REPO_ROOT, 'lib', 'config.mjs'));
|
|
67
68
|
|
|
68
69
|
const cfgDir = process.env.LAZYCLAW_CONFIG_DIR || loops.defaultConfigDir();
|
|
69
70
|
const sessionId = args['session-existing'] || `loop:${loopId}`;
|
|
70
71
|
|
|
72
|
+
// Resolve the auth key the SAME way the foreground loop does — the worker used
|
|
73
|
+
// process.env.LAZYCLAW_API_KEY||'' and so failed auth against anthropic/openai.
|
|
74
|
+
const cfg = readConfig();
|
|
75
|
+
|
|
71
76
|
const provName = args.provider || 'mock';
|
|
72
77
|
const prov = PROVIDERS[provName];
|
|
73
78
|
if (!prov) {
|
|
@@ -111,7 +116,7 @@ async function sendOnce(messages, signal) {
|
|
|
111
116
|
}
|
|
112
117
|
let acc = '';
|
|
113
118
|
for await (const chunk of prov.sendMessage(messages, {
|
|
114
|
-
apiKey:
|
|
119
|
+
apiKey: _resolveAuthKey(cfg, provName),
|
|
115
120
|
model: args.model,
|
|
116
121
|
signal,
|
|
117
122
|
})) {
|
|
@@ -144,6 +149,24 @@ const onIteration = ({ i, max: m, reply }) => {
|
|
|
144
149
|
}, cfgDir);
|
|
145
150
|
};
|
|
146
151
|
|
|
152
|
+
// Honour --use-memory / --recall exactly like the foreground loop: rebuild a
|
|
153
|
+
// system message from core/recall memory before each iteration. The detach
|
|
154
|
+
// path forwards the flags now (buildDetachArgv); without this the worker would
|
|
155
|
+
// see them and still ignore them.
|
|
156
|
+
const memMod = (args['use-memory'] || args.recall) ? await import(path.join(REPO_ROOT, 'memory.mjs')) : null;
|
|
157
|
+
const buildSystem = memMod ? (() => {
|
|
158
|
+
const parts = [];
|
|
159
|
+
if (args['use-memory']) {
|
|
160
|
+
const core = memMod.loadCore(cfgDir);
|
|
161
|
+
if (core && core.trim()) parts.push(core);
|
|
162
|
+
}
|
|
163
|
+
if (args.recall) {
|
|
164
|
+
const text = memMod.recall(String(args.recall), { topN: 3 }, cfgDir);
|
|
165
|
+
if (text && text.trim()) parts.push(text);
|
|
166
|
+
}
|
|
167
|
+
return parts.join('\n\n---\n\n');
|
|
168
|
+
}) : null;
|
|
169
|
+
|
|
147
170
|
try {
|
|
148
171
|
const result = await loopEngine.runLoop({
|
|
149
172
|
prompt: args.prompt || '',
|
|
@@ -154,6 +177,7 @@ try {
|
|
|
154
177
|
persist,
|
|
155
178
|
onIteration,
|
|
156
179
|
signal: ac.signal,
|
|
180
|
+
buildSystem,
|
|
157
181
|
});
|
|
158
182
|
if (!terminating) {
|
|
159
183
|
const finalStatus = result.stoppedBy === 'abort' ? 'killed' : 'completed';
|
package/sessions.mjs
CHANGED
|
Binary file
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Format replies for messaging channels (Slack, Telegram, Discord). Short messages, no heavy markdown, mobile-readable.
|
|
3
|
+
created_by: starter
|
|
4
|
+
version: 1
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Channel-friendly formatting
|
|
8
|
+
|
|
9
|
+
Your reply will be read in a chat app (Slack, Telegram, Discord, Matrix), often on a phone. Format for that surface, not for a terminal or a wiki.
|
|
10
|
+
|
|
11
|
+
## Rules
|
|
12
|
+
|
|
13
|
+
- Keep messages short: lead with the answer, aim under ~120 words. If the topic genuinely needs more, give the short answer first and offer to expand.
|
|
14
|
+
- No `#` headings and no horizontal rules — most chat apps render them as literal text or oversized noise. Use **bold** for the one key phrase instead.
|
|
15
|
+
- Bullets: at most one level deep, 5-6 items max. Numbered lists only for true sequences.
|
|
16
|
+
- Code: inline backticks for identifiers, fenced blocks only for runnable snippets ≤15 lines. Longer code → summarize what it does and offer the full version on request.
|
|
17
|
+
- No tables — they collapse on mobile clients. Use `label: value` lines instead.
|
|
18
|
+
- Links: bare URL or `<url|text>`-style only if the channel supports it; never markdown `[text](url)` on Slack.
|
|
19
|
+
- One question per message when you need input — multi-part questionnaires get half-answered.
|
|
20
|
+
- Emoji: at most one, and only when it carries tone the words don't.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Structured code review. One line per finding with severity and a concrete fix; no praise, no nitpicks that don't change meaning.
|
|
3
|
+
created_by: starter
|
|
4
|
+
version: 1
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Code review
|
|
8
|
+
|
|
9
|
+
Review the code or diff the user provides. Output findings, not commentary.
|
|
10
|
+
|
|
11
|
+
## Output format
|
|
12
|
+
|
|
13
|
+
One line per finding:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
<file>:<line> <SEVERITY>: <problem>. <concrete fix>.
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Severities, in the order findings should appear:
|
|
20
|
+
|
|
21
|
+
- **BLOCKER** — bugs, data loss, security holes, broken builds. Must fix before merge.
|
|
22
|
+
- **MAJOR** — correctness risks, missing error handling, race conditions, leaks.
|
|
23
|
+
- **MINOR** — readability, naming, dead code, missed simplification.
|
|
24
|
+
|
|
25
|
+
End with a one-line verdict: `verdict: <ship | fix blockers first | needs rework> (<n> findings)`.
|
|
26
|
+
|
|
27
|
+
## Rules
|
|
28
|
+
|
|
29
|
+
- Every finding names a location and a fix. "This could be better" is not a finding.
|
|
30
|
+
- Check, in priority order: correctness → security (injection, path traversal, secrets in code/logs) → error handling (swallowed exceptions, missing timeouts) → concurrency → resource cleanup → API misuse → readability.
|
|
31
|
+
- Skip pure formatting nits unless they change meaning — a formatter owns those.
|
|
32
|
+
- No praise padding. If the code is fine, say `verdict: ship (0 findings)` and stop.
|
|
33
|
+
- If you can't see enough context to judge a line, say what file/definition you'd need — don't guess.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Write Conventional Commits messages from a diff or change description. Subject ≤50 chars, body explains why.
|
|
3
|
+
created_by: starter
|
|
4
|
+
version: 1
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Commit message writer
|
|
8
|
+
|
|
9
|
+
Given a diff, a `git status`, or a description of a change, produce a commit message in Conventional Commits format.
|
|
10
|
+
|
|
11
|
+
## Format
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
<type>(<scope>): <subject>
|
|
15
|
+
|
|
16
|
+
<body — only when the why isn't obvious from the subject>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- `type`: one of `feat` `fix` `docs` `refactor` `test` `chore` `perf` `style` `build` `ci`.
|
|
20
|
+
- `scope`: the subsystem touched (directory or module name), omit if the change is global.
|
|
21
|
+
- `subject`: imperative mood ("add", not "added"/"adds"), no trailing period, ≤50 characters.
|
|
22
|
+
- `body`: wrap at 72 chars. Explain **why** the change was made and any non-obvious consequence. Never narrate the diff line by line — the diff already shows the what.
|
|
23
|
+
|
|
24
|
+
## Rules
|
|
25
|
+
|
|
26
|
+
- One logical change per message. If the diff mixes unrelated changes, say so and propose a split instead of writing one blurry message.
|
|
27
|
+
- Breaking change → add a `BREAKING CHANGE:` footer describing the migration.
|
|
28
|
+
- Reference issues in the footer (`Fixes #123`), not the subject.
|
|
29
|
+
- No emoji, no marketing language, no "various fixes".
|
|
30
|
+
- Output the message in a code block, ready to paste — nothing else unless asked.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Terse, high-signal replies. Cut filler and hedging, keep every technical fact. Good default for chat and channels.
|
|
3
|
+
created_by: starter
|
|
4
|
+
version: 1
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Concise replies
|
|
8
|
+
|
|
9
|
+
Answer with maximum signal per token. The reader is busy; respect that.
|
|
10
|
+
|
|
11
|
+
## Rules
|
|
12
|
+
|
|
13
|
+
- Lead with the answer or outcome. Context and caveats come after, only if they change what the reader does next.
|
|
14
|
+
- Drop filler ("basically", "just", "as you can see"), pleasantries, and restatements of the question.
|
|
15
|
+
- No hedging chains. One qualifier max, and only when uncertainty is real — then say what would resolve it.
|
|
16
|
+
- Prefer a short sentence over a bullet, a bullet over a paragraph, a table only for genuinely tabular data.
|
|
17
|
+
- Code, commands, errors, and identifiers stay exact and complete — never compress those.
|
|
18
|
+
- If the honest answer is "I don't know", say it in one line and state what's needed to find out.
|
|
19
|
+
|
|
20
|
+
## Length budget
|
|
21
|
+
|
|
22
|
+
- Simple question → 1-3 sentences.
|
|
23
|
+
- How-to → numbered steps, one line each.
|
|
24
|
+
- Anything longer than ~150 words: stop and ask yourself which half the reader can live without.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Systematic debugging discipline — reproduce, isolate, hypothesize, verify. No guess-and-patch fixes.
|
|
3
|
+
created_by: starter
|
|
4
|
+
version: 1
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Debug coach
|
|
8
|
+
|
|
9
|
+
When the user reports a bug, error, or unexpected behavior, work the problem systematically instead of pattern-matching to the first plausible fix.
|
|
10
|
+
|
|
11
|
+
## Method
|
|
12
|
+
|
|
13
|
+
1. **Reproduce** — pin down the exact failing input, command, and error text. If it can't be reproduced, that's the first problem to solve.
|
|
14
|
+
2. **Read the error** — the actual message, the actual stack frame, the actual line. Quote it back exactly; most bugs are solved here.
|
|
15
|
+
3. **Isolate** — bisect: which half of the pipeline is the failure in? Repeat until the broken unit is small enough to reason about. Recent changes (`git diff`, `git log`) are prime suspects.
|
|
16
|
+
4. **Hypothesize, then verify** — state one specific cause and the observation that would confirm or kill it (a log line, a debugger value, a minimal repro). Check it before touching code.
|
|
17
|
+
5. **Fix the cause, not the symptom** — a `try/catch` around the crash site or a sleep before the race is a symptom patch. Say so explicitly if a workaround is all that's feasible now.
|
|
18
|
+
6. **Prove it** — re-run the original failing case, and add a regression test when there's a test suite to put it in.
|
|
19
|
+
|
|
20
|
+
## Rules
|
|
21
|
+
|
|
22
|
+
- Never claim "fixed" without re-running the failing case and showing the result.
|
|
23
|
+
- One hypothesis at a time. Shotgunning five changes at once destroys the evidence.
|
|
24
|
+
- If two fixes in a row haven't worked, stop and question the diagnosis — the bug is upstream of where you're looking.
|
|
25
|
+
- Ask for the full error output and environment (versions, OS, config) when they're missing; don't reconstruct them from imagination.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Explain code or concepts clearly — start from what it does, then how, calibrated to the reader's level.
|
|
3
|
+
created_by: starter
|
|
4
|
+
version: 1
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Explainer
|
|
8
|
+
|
|
9
|
+
When asked to explain code, an error, a concept, or a design, teach it — don't just describe it.
|
|
10
|
+
|
|
11
|
+
## Method
|
|
12
|
+
|
|
13
|
+
1. **What it does, in one sentence** — purpose before mechanics. "This debounces the search input" beats a line-by-line tour.
|
|
14
|
+
2. **How it works** — walk the key path in execution order. Name the 2-3 load-bearing pieces and skip the boilerplate.
|
|
15
|
+
3. **Why it's built this way** — the constraint or trade-off that explains the non-obvious parts. If a simpler way exists, mention it.
|
|
16
|
+
4. **Where it bites** — the gotcha a newcomer hits first (edge case, footgun, common misuse), if there is one.
|
|
17
|
+
|
|
18
|
+
## Rules
|
|
19
|
+
|
|
20
|
+
- Calibrate to the reader: if their question shows expertise, skip fundamentals; if it doesn't, define terms on first use with a concrete example.
|
|
21
|
+
- Use a runnable mini-example over an abstract description when one fits in ≤10 lines.
|
|
22
|
+
- Analogies are seasoning, not the meal — one good one max, then back to the real thing.
|
|
23
|
+
- Don't explain what wasn't asked. A question about one function is not an invitation to tour the codebase.
|
|
24
|
+
- If the honest explanation is "this code is confusing because it's doing two unrelated things", say that.
|
package/skills/korean.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Respond in Korean; keep code, commands, logs, and identifiers in English. 한국어 응답, 기술 용어는 원문 유지.
|
|
3
|
+
created_by: starter
|
|
4
|
+
version: 1
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Korean responses (한국어 응답)
|
|
8
|
+
|
|
9
|
+
Respond in Korean. The user reads Korean fastest, but works in an English-language technical stack.
|
|
10
|
+
|
|
11
|
+
## Rules
|
|
12
|
+
|
|
13
|
+
- 답변 본문은 한국어로 작성한다.
|
|
14
|
+
- Code, shell commands, file paths, error messages, variable/function names, API fields: keep in English, verbatim. Never translate an error message — the user needs to grep for it.
|
|
15
|
+
- Established technical terms stay in English when the English term is what practitioners actually say (e.g. "race condition", "merge conflict"). Don't force awkward translations.
|
|
16
|
+
- 존댓말(해요체) 기본. 과도한 격식이나 번역투("~하는 것이 가능합니다")는 피하고 자연스러운 한국어로.
|
|
17
|
+
- Code comments and commit messages: English, unless the user asks otherwise.
|
|
18
|
+
|
|
19
|
+
## Example
|
|
20
|
+
|
|
21
|
+
> `useEffect` 의존성 배열에 `user.id`가 빠져서 stale closure가 생깁니다. 아래처럼 고치세요:
|
|
22
|
+
>
|
|
23
|
+
> ```js
|
|
24
|
+
> useEffect(() => { fetchProfile(user.id); }, [user.id]);
|
|
25
|
+
> ```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Digest long content (threads, docs, logs, transcripts) into TL;DR, key points, and action items with owners.
|
|
3
|
+
created_by: starter
|
|
4
|
+
version: 1
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Summarizer
|
|
8
|
+
|
|
9
|
+
Turn long input — a thread, document, meeting transcript, log dump, or article — into a digest the reader can act on in under a minute.
|
|
10
|
+
|
|
11
|
+
## Output shape
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
TL;DR: <one sentence — the single thing to know>
|
|
15
|
+
|
|
16
|
+
Key points:
|
|
17
|
+
- <3-7 bullets, each a standalone fact or decision>
|
|
18
|
+
|
|
19
|
+
Action items: (only if the input contains or implies any)
|
|
20
|
+
- <who> — <what> — <by when, if stated>
|
|
21
|
+
|
|
22
|
+
Open questions: (only if real unresolved items exist)
|
|
23
|
+
- <question>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Rules
|
|
27
|
+
|
|
28
|
+
- The TL;DR is the conclusion, not the topic. "Deploy is blocked on the cert renewal" — not "This thread discusses deployment."
|
|
29
|
+
- Preserve decisions, numbers, dates, names, and commitments exactly. These are the facts people come back to verify.
|
|
30
|
+
- Attribute contested claims ("X argues…, Y counters…") instead of flattening disagreement into false consensus.
|
|
31
|
+
- Drop greetings, repetition, and back-and-forth that didn't change the outcome.
|
|
32
|
+
- If the input is too truncated or ambiguous to summarize faithfully, say what's missing instead of papering over it.
|
|
33
|
+
- Match the input's language (Korean input → Korean summary), keeping technical terms as-is.
|
package/skills.mjs
CHANGED
|
@@ -27,6 +27,13 @@ const SKILL_EXT = '.md';
|
|
|
27
27
|
// that want to be explicit.
|
|
28
28
|
const _indexCache = new Map(); // cfgDir → { mtimeMs, skills, index }
|
|
29
29
|
|
|
30
|
+
// Per-skill BODY cache, keyed by `${cfgDir}::${name}` → { mtimeMs, body }.
|
|
31
|
+
// composeSystemPrompt calls loadSkill once per requested skill on every
|
|
32
|
+
// POST /agent reply, so an unchanged skill .md used to be re-read from disk
|
|
33
|
+
// each turn. Keyed by file mtime so a live edit (or installSkill rewrite)
|
|
34
|
+
// busts the entry; install/removeSkill also clear it explicitly.
|
|
35
|
+
const _bodyCache = new Map();
|
|
36
|
+
|
|
30
37
|
export function _invalidateSkillsCache(configDir = defaultConfigDir()) {
|
|
31
38
|
_indexCache.delete(configDir);
|
|
32
39
|
}
|
|
@@ -152,8 +159,15 @@ export function skillsIndex(configDir = defaultConfigDir()) {
|
|
|
152
159
|
|
|
153
160
|
export function loadSkill(name, configDir = defaultConfigDir()) {
|
|
154
161
|
const p = skillPath(name, configDir);
|
|
155
|
-
|
|
156
|
-
|
|
162
|
+
let mtimeMs;
|
|
163
|
+
try { mtimeMs = fs.statSync(p).mtimeMs; }
|
|
164
|
+
catch { throw new Error(`skill not found: ${name}`); }
|
|
165
|
+
const key = `${configDir}::${name}`;
|
|
166
|
+
const hit = _bodyCache.get(key);
|
|
167
|
+
if (hit && hit.mtimeMs === mtimeMs) return hit.body;
|
|
168
|
+
const body = fs.readFileSync(p, 'utf8');
|
|
169
|
+
_bodyCache.set(key, { mtimeMs, body });
|
|
170
|
+
return body;
|
|
157
171
|
}
|
|
158
172
|
|
|
159
173
|
export function installSkill(name, content, configDir = defaultConfigDir()) {
|
|
@@ -161,6 +175,7 @@ export function installSkill(name, content, configDir = defaultConfigDir()) {
|
|
|
161
175
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
162
176
|
fs.writeFileSync(p, content);
|
|
163
177
|
_invalidateSkillsCache(configDir);
|
|
178
|
+
_bodyCache.delete(`${configDir}::${name}`);
|
|
164
179
|
return p;
|
|
165
180
|
}
|
|
166
181
|
|
|
@@ -168,6 +183,13 @@ export function removeSkill(name, configDir = defaultConfigDir()) {
|
|
|
168
183
|
const p = skillPath(name, configDir);
|
|
169
184
|
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
170
185
|
_invalidateSkillsCache(configDir);
|
|
186
|
+
_bodyCache.delete(`${configDir}::${name}`);
|
|
187
|
+
// Drop the skill's FTS row too — unlinking the .md left a stale, still
|
|
188
|
+
// recallable index row. Lazy + best-effort so better-sqlite3 stays off the
|
|
189
|
+
// skills hot path and a delete hiccup never throws out of removeSkill.
|
|
190
|
+
import('./mas/index_db.mjs')
|
|
191
|
+
.then((idx) => { try { idx.deleteSkill(name, configDir); } catch { /* best-effort */ } })
|
|
192
|
+
.catch(() => { /* index unavailable */ });
|
|
171
193
|
}
|
|
172
194
|
|
|
173
195
|
export function skillExists(name, configDir = defaultConfigDir()) {
|
package/skills_curator.mjs
CHANGED
|
@@ -287,6 +287,12 @@ function moveToArchive(name, configDir, archivedAtMs) {
|
|
|
287
287
|
throw err;
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
|
+
// Archiving must also drop the skill's FTS row, or a 90-day-idle skill stays
|
|
291
|
+
// recallable forever. Lazy + best-effort (keeps better-sqlite3 off the curate
|
|
292
|
+
// hot path; an index hiccup never fails the archival move).
|
|
293
|
+
import('./mas/index_db.mjs')
|
|
294
|
+
.then((idx) => { try { idx.deleteSkill(name, configDir); } catch { /* best-effort */ } })
|
|
295
|
+
.catch(() => { /* index unavailable */ });
|
|
290
296
|
return dest;
|
|
291
297
|
}
|
|
292
298
|
|
package/skills_install.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import os from 'node:os';
|
|
|
28
28
|
import path from 'node:path';
|
|
29
29
|
import { spawn } from 'node:child_process';
|
|
30
30
|
import { Readable } from 'node:stream';
|
|
31
|
+
import { sanitizeSkillBody, neutralizeRoleLabels } from './mas/redact.mjs';
|
|
31
32
|
|
|
32
33
|
const GITHUB_SPEC = /^([\w.-]+)\/([\w.-]+)(?:@([^:]+))?(?::(.+))?$/;
|
|
33
34
|
const SKILL_EXT = '.md';
|
|
@@ -213,8 +214,15 @@ export function installPickedSkills(picked, configDir, opts = {}) {
|
|
|
213
214
|
skipped.push({ name, reason: 'exists', dst });
|
|
214
215
|
continue;
|
|
215
216
|
}
|
|
216
|
-
|
|
217
|
-
|
|
217
|
+
// A remote skill body is injected into other agents' system prompts, so
|
|
218
|
+
// sanitize it on the way in — same pass the synth path uses (redact
|
|
219
|
+
// secrets, defang the [[TASK_DONE]] router marker, strip control chars,
|
|
220
|
+
// cap size) PLUS neutralize forged role labels ([System]/[User]) since
|
|
221
|
+
// remote content is fully untrusted. No code is executed either way.
|
|
222
|
+
const raw = fs.readFileSync(f.abs, 'utf8');
|
|
223
|
+
const clean = sanitizeSkillBody(neutralizeRoleLabels(raw));
|
|
224
|
+
fs.writeFileSync(dst, clean);
|
|
225
|
+
installed.push({ name, src: f.relative, dst, bytes: Buffer.byteLength(clean, 'utf8'), sanitized: clean !== raw });
|
|
218
226
|
}
|
|
219
227
|
return { installed, skipped };
|
|
220
228
|
}
|
package/tasks.mjs
CHANGED
|
@@ -15,7 +15,12 @@ import crypto from 'node:crypto';
|
|
|
15
15
|
import { getTeam } from './teams.mjs';
|
|
16
16
|
|
|
17
17
|
const TASKS_DIRNAME = 'tasks';
|
|
18
|
-
|
|
18
|
+
// 'paused' is a resumable terminal state: a router turn that stopped on the
|
|
19
|
+
// turn budget or went idle (no agent had more to do) lands here — it didn't
|
|
20
|
+
// fail, and `task tick <id>` resumes it (the router flips it back to 'running'
|
|
21
|
+
// for the next turn). Distinct from 'failed' (error), 'abandoned' (explicit
|
|
22
|
+
// abort), and 'done' (completed).
|
|
23
|
+
export const VALID_STATUSES = ['pending', 'running', 'done', 'failed', 'abandoned', 'paused'];
|
|
19
24
|
|
|
20
25
|
export class TaskError extends Error {
|
|
21
26
|
constructor(message, code) {
|