@enderfga/claw-orchestrator 3.4.2 → 3.5.5
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 +9 -9
- package/configs/autoloop-coder-prompt.md +59 -0
- package/configs/autoloop-planner-prompt.md +136 -0
- package/configs/autoloop-reviewer-prompt.md +76 -0
- package/dist/src/autoloop/agent-tools.d.ts +44 -0
- package/dist/src/autoloop/agent-tools.js +71 -0
- package/dist/src/autoloop/agent-tools.js.map +1 -0
- package/dist/src/autoloop/dispatcher.d.ts +137 -0
- package/dist/src/autoloop/dispatcher.js +580 -0
- package/dist/src/autoloop/dispatcher.js.map +1 -0
- package/dist/src/autoloop/messages.d.ts +114 -0
- package/dist/src/autoloop/messages.js +92 -0
- package/dist/src/autoloop/messages.js.map +1 -0
- package/dist/src/autoloop/notify.d.ts +36 -0
- package/dist/src/autoloop/notify.js +166 -0
- package/dist/src/autoloop/notify.js.map +1 -0
- package/dist/src/autoloop/planner-tools.d.ts +81 -0
- package/dist/src/autoloop/planner-tools.js +140 -0
- package/dist/src/autoloop/planner-tools.js.map +1 -0
- package/dist/src/autoloop/runner.d.ts +48 -0
- package/dist/src/autoloop/runner.js +214 -0
- package/dist/src/autoloop/runner.js.map +1 -0
- package/dist/src/autoloop/types.d.ts +69 -0
- package/dist/src/autoloop/types.js +16 -0
- package/dist/src/autoloop/types.js.map +1 -0
- package/dist/src/dashboard/index.html +910 -0
- package/dist/src/embedded-server.js +173 -32
- package/dist/src/embedded-server.js.map +1 -1
- package/dist/src/index.d.ts +5 -2
- package/dist/src/index.js +67 -105
- package/dist/src/index.js.map +1 -1
- package/dist/src/session-manager.d.ts +47 -14
- package/dist/src/session-manager.js +135 -47
- package/dist/src/session-manager.js.map +1 -1
- package/package.json +2 -2
- package/skills/references/autoloop.md +176 -193
- package/configs/autoloop-bootstrap-prompt.md +0 -30
- package/configs/autoloop-compress-prompt.md +0 -50
- package/configs/autoloop-propose-prompt.md +0 -57
- package/configs/autoloop-ratchet-prompt.md +0 -56
- package/dist/src/autoloop-types.d.ts +0 -218
- package/dist/src/autoloop-types.js +0 -130
- package/dist/src/autoloop-types.js.map +0 -1
- package/dist/src/autoloop.d.ts +0 -70
- package/dist/src/autoloop.js +0 -910
- package/dist/src/autoloop.js.map +0 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbox message envelope + discriminated union of v2 message types.
|
|
3
|
+
*
|
|
4
|
+
* Contract: see tasks/autoloop.md §3.3.
|
|
5
|
+
*
|
|
6
|
+
* Wire format: serialised as JSON in InboxManager's `text` field, with
|
|
7
|
+
* `summary` set to the message `type` for human-readable inbox listings.
|
|
8
|
+
* Routing addressing convention: session name = `autoloop:<run_id>:<role>`,
|
|
9
|
+
* where role ∈ { planner, coder, reviewer, runner }. The literal `user`
|
|
10
|
+
* is *not* a session — it appears only as the `from` of an external chat
|
|
11
|
+
* injection (`autoloop_chat`) and as the `to` of `push_user` messages
|
|
12
|
+
* (which the runner consumes to invoke notify_user).
|
|
13
|
+
*/
|
|
14
|
+
export type AutoloopRole = 'planner' | 'coder' | 'reviewer' | 'runner' | 'user';
|
|
15
|
+
export interface AutoloopEnvelope<T extends AutoloopMessageType = AutoloopMessageType> {
|
|
16
|
+
msg_id: string;
|
|
17
|
+
iter: number;
|
|
18
|
+
from: AutoloopRole;
|
|
19
|
+
to: AutoloopRole;
|
|
20
|
+
type: T;
|
|
21
|
+
ts: string;
|
|
22
|
+
payload: PayloadFor<T>;
|
|
23
|
+
}
|
|
24
|
+
export interface UserChatPayload {
|
|
25
|
+
text: string;
|
|
26
|
+
}
|
|
27
|
+
export interface DirectivePayload {
|
|
28
|
+
goal: string;
|
|
29
|
+
constraints: string[];
|
|
30
|
+
success_criteria: string[];
|
|
31
|
+
max_attempts: number;
|
|
32
|
+
}
|
|
33
|
+
export interface DirectiveAckPayload {
|
|
34
|
+
understood: boolean;
|
|
35
|
+
clarification?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface IterArtifactsPayload {
|
|
38
|
+
diff: string;
|
|
39
|
+
eval_output: unknown;
|
|
40
|
+
files_changed: string[];
|
|
41
|
+
}
|
|
42
|
+
export interface ReviewRequestPayload {
|
|
43
|
+
iter: number;
|
|
44
|
+
ledger_path: string;
|
|
45
|
+
prior_metrics: number[];
|
|
46
|
+
}
|
|
47
|
+
export interface ReviewVerdictPayload {
|
|
48
|
+
decision: 'advance' | 'hold' | 'rollback';
|
|
49
|
+
metric: number | null;
|
|
50
|
+
audit_notes: string;
|
|
51
|
+
}
|
|
52
|
+
export interface IterDonePayload {
|
|
53
|
+
iter: number;
|
|
54
|
+
verdict: 'advance' | 'hold' | 'rollback';
|
|
55
|
+
metric: number | null;
|
|
56
|
+
regression?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export type PushLevel = 'info' | 'warn' | 'decision' | 'error';
|
|
59
|
+
export type PushChannel = 'auto' | 'wechat' | 'webchat' | 'both' | 'email';
|
|
60
|
+
export interface PushUserPayload {
|
|
61
|
+
level: PushLevel;
|
|
62
|
+
summary: string;
|
|
63
|
+
detail?: string;
|
|
64
|
+
channel: PushChannel;
|
|
65
|
+
}
|
|
66
|
+
export interface PausePayload {
|
|
67
|
+
reason: string;
|
|
68
|
+
}
|
|
69
|
+
export type ResumePayload = Record<string, never>;
|
|
70
|
+
export interface TerminatePayload {
|
|
71
|
+
reason: string;
|
|
72
|
+
}
|
|
73
|
+
export type AutoloopMessageType = 'chat' | 'directive' | 'directive_ack' | 'iter_artifacts' | 'review_request' | 'review_verdict' | 'iter_done' | 'push_user' | 'pause' | 'resume' | 'terminate';
|
|
74
|
+
type PayloadMap = {
|
|
75
|
+
chat: UserChatPayload;
|
|
76
|
+
directive: DirectivePayload;
|
|
77
|
+
directive_ack: DirectiveAckPayload;
|
|
78
|
+
iter_artifacts: IterArtifactsPayload;
|
|
79
|
+
review_request: ReviewRequestPayload;
|
|
80
|
+
review_verdict: ReviewVerdictPayload;
|
|
81
|
+
iter_done: IterDonePayload;
|
|
82
|
+
push_user: PushUserPayload;
|
|
83
|
+
pause: PausePayload;
|
|
84
|
+
resume: ResumePayload;
|
|
85
|
+
terminate: TerminatePayload;
|
|
86
|
+
};
|
|
87
|
+
export type PayloadFor<T extends AutoloopMessageType> = PayloadMap[T];
|
|
88
|
+
export type AnyAutoloopMessage = {
|
|
89
|
+
[T in AutoloopMessageType]: AutoloopEnvelope<T>;
|
|
90
|
+
}[AutoloopMessageType];
|
|
91
|
+
export declare class AutoloopRoutingError extends Error {
|
|
92
|
+
envelope?: AnyAutoloopMessage | undefined;
|
|
93
|
+
constructor(msg: string, envelope?: AnyAutoloopMessage | undefined);
|
|
94
|
+
}
|
|
95
|
+
export declare function validateMessage(env: AnyAutoloopMessage): void;
|
|
96
|
+
export declare const Msg: {
|
|
97
|
+
chat: (iter: number, payload: UserChatPayload) => AutoloopEnvelope<"chat">;
|
|
98
|
+
directive: (iter: number, payload: DirectivePayload) => AutoloopEnvelope<"directive">;
|
|
99
|
+
directiveAck: (iter: number, payload: DirectiveAckPayload) => AutoloopEnvelope<"directive_ack">;
|
|
100
|
+
iterArtifacts: (iter: number, payload: IterArtifactsPayload) => AutoloopEnvelope<"iter_artifacts">;
|
|
101
|
+
reviewRequest: (iter: number, payload: ReviewRequestPayload) => AutoloopEnvelope<"review_request">;
|
|
102
|
+
reviewVerdict: (iter: number, payload: ReviewVerdictPayload) => AutoloopEnvelope<"review_verdict">;
|
|
103
|
+
iterDone: (iter: number, payload: IterDonePayload) => AutoloopEnvelope<"iter_done">;
|
|
104
|
+
pushUser: (iter: number, payload: PushUserPayload) => AutoloopEnvelope<"push_user">;
|
|
105
|
+
pause: (iter: number, payload: PausePayload) => AutoloopEnvelope<"pause">;
|
|
106
|
+
resume: (iter: number) => AutoloopEnvelope<"resume">;
|
|
107
|
+
terminate: (iter: number, payload: TerminatePayload) => AutoloopEnvelope<"terminate">;
|
|
108
|
+
};
|
|
109
|
+
export declare function serialise(env: AnyAutoloopMessage): {
|
|
110
|
+
text: string;
|
|
111
|
+
summary: string;
|
|
112
|
+
};
|
|
113
|
+
export declare function deserialise(text: string): AnyAutoloopMessage;
|
|
114
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbox message envelope + discriminated union of v2 message types.
|
|
3
|
+
*
|
|
4
|
+
* Contract: see tasks/autoloop.md §3.3.
|
|
5
|
+
*
|
|
6
|
+
* Wire format: serialised as JSON in InboxManager's `text` field, with
|
|
7
|
+
* `summary` set to the message `type` for human-readable inbox listings.
|
|
8
|
+
* Routing addressing convention: session name = `autoloop:<run_id>:<role>`,
|
|
9
|
+
* where role ∈ { planner, coder, reviewer, runner }. The literal `user`
|
|
10
|
+
* is *not* a session — it appears only as the `from` of an external chat
|
|
11
|
+
* injection (`autoloop_chat`) and as the `to` of `push_user` messages
|
|
12
|
+
* (which the runner consumes to invoke notify_user).
|
|
13
|
+
*/
|
|
14
|
+
// ─── Sender/recipient validity table ────────────────────────────────────────
|
|
15
|
+
//
|
|
16
|
+
// Allowed (from, to, type) tuples. Anything else is a routing error caught
|
|
17
|
+
// by `validateMessage`. Centralising this table keeps the runner's switch
|
|
18
|
+
// statements honest and gives us one place to update when v2.1 adds new
|
|
19
|
+
// message types (e.g. weixin-inbound chat reply).
|
|
20
|
+
const ALLOWED_ROUTES = [
|
|
21
|
+
['user', 'planner', 'chat'],
|
|
22
|
+
['planner', 'coder', 'directive'],
|
|
23
|
+
['coder', 'planner', 'directive_ack'],
|
|
24
|
+
['coder', 'runner', 'iter_artifacts'],
|
|
25
|
+
['runner', 'reviewer', 'review_request'],
|
|
26
|
+
['reviewer', 'runner', 'review_verdict'],
|
|
27
|
+
['runner', 'planner', 'iter_done'],
|
|
28
|
+
['planner', 'user', 'push_user'],
|
|
29
|
+
['planner', 'runner', 'pause'],
|
|
30
|
+
['planner', 'runner', 'resume'],
|
|
31
|
+
['planner', 'runner', 'terminate'],
|
|
32
|
+
];
|
|
33
|
+
export class AutoloopRoutingError extends Error {
|
|
34
|
+
envelope;
|
|
35
|
+
constructor(msg, envelope) {
|
|
36
|
+
super(msg);
|
|
37
|
+
this.envelope = envelope;
|
|
38
|
+
this.name = 'AutoloopRoutingError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function validateMessage(env) {
|
|
42
|
+
const ok = ALLOWED_ROUTES.some(([f, t, ty]) => f === env.from && t === env.to && ty === env.type);
|
|
43
|
+
if (!ok) {
|
|
44
|
+
throw new AutoloopRoutingError(`Invalid v2 routing: ${env.from} → ${env.to} (type=${env.type})`, env);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// ─── Constructors ────────────────────────────────────────────────────────────
|
|
48
|
+
let __counter = 0;
|
|
49
|
+
function nextMsgId() {
|
|
50
|
+
// Cheap monotonic IDs; collisions across runs are not load-bearing.
|
|
51
|
+
__counter = (__counter + 1) | 0;
|
|
52
|
+
return `m_${Date.now().toString(36)}_${__counter.toString(36)}`;
|
|
53
|
+
}
|
|
54
|
+
function envelope(iter, from, to, type, payload) {
|
|
55
|
+
return {
|
|
56
|
+
msg_id: nextMsgId(),
|
|
57
|
+
iter,
|
|
58
|
+
from,
|
|
59
|
+
to,
|
|
60
|
+
type,
|
|
61
|
+
ts: new Date().toISOString(),
|
|
62
|
+
payload,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export const Msg = {
|
|
66
|
+
chat: (iter, payload) => envelope(iter, 'user', 'planner', 'chat', payload),
|
|
67
|
+
directive: (iter, payload) => envelope(iter, 'planner', 'coder', 'directive', payload),
|
|
68
|
+
directiveAck: (iter, payload) => envelope(iter, 'coder', 'planner', 'directive_ack', payload),
|
|
69
|
+
iterArtifacts: (iter, payload) => envelope(iter, 'coder', 'runner', 'iter_artifacts', payload),
|
|
70
|
+
reviewRequest: (iter, payload) => envelope(iter, 'runner', 'reviewer', 'review_request', payload),
|
|
71
|
+
reviewVerdict: (iter, payload) => envelope(iter, 'reviewer', 'runner', 'review_verdict', payload),
|
|
72
|
+
iterDone: (iter, payload) => envelope(iter, 'runner', 'planner', 'iter_done', payload),
|
|
73
|
+
pushUser: (iter, payload) => envelope(iter, 'planner', 'user', 'push_user', payload),
|
|
74
|
+
pause: (iter, payload) => envelope(iter, 'planner', 'runner', 'pause', payload),
|
|
75
|
+
resume: (iter) => envelope(iter, 'planner', 'runner', 'resume', {}),
|
|
76
|
+
terminate: (iter, payload) => envelope(iter, 'planner', 'runner', 'terminate', payload),
|
|
77
|
+
};
|
|
78
|
+
// ─── Wire serialisation (for InboxManager transport) ─────────────────────────
|
|
79
|
+
export function serialise(env) {
|
|
80
|
+
return {
|
|
81
|
+
text: JSON.stringify(env),
|
|
82
|
+
summary: env.type,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function deserialise(text) {
|
|
86
|
+
const parsed = JSON.parse(text);
|
|
87
|
+
if (typeof parsed !== 'object' || parsed === null || typeof parsed.type !== 'string') {
|
|
88
|
+
throw new AutoloopRoutingError('Malformed v2 envelope (not an object or missing type)');
|
|
89
|
+
}
|
|
90
|
+
return parsed;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=messages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages.js","sourceRoot":"","sources":["../../../src/autoloop/messages.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* notify_user — wechat → whatsapp → email fallback chain.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the bash `push()` recipe in ~/.claude/skills/push-api-skill/SKILL.md
|
|
5
|
+
* §B.2.5. Purposefully shells out via `openclaw` and the email script rather
|
|
6
|
+
* than reimplementing the API contracts — the skill is the source of truth.
|
|
7
|
+
*
|
|
8
|
+
* Per the recovery-path principle in CLAUDE.md: this code path must stay
|
|
9
|
+
* thin. If openclaw itself is down, the email fallback is independent
|
|
10
|
+
* (Gmail SMTP via the script).
|
|
11
|
+
*/
|
|
12
|
+
import type { PushChannel, PushLevel } from './messages.js';
|
|
13
|
+
import { type Logger } from '../logger.js';
|
|
14
|
+
/**
|
|
15
|
+
* Send a notification, walking the fallback chain. Returns the channel that
|
|
16
|
+
* succeeded (or 'none' if all failed). Caller should record both attempt and
|
|
17
|
+
* outcome to the run's push log regardless.
|
|
18
|
+
*/
|
|
19
|
+
export declare function notifyUserFallbackChain(opts: {
|
|
20
|
+
level: PushLevel;
|
|
21
|
+
summary: string;
|
|
22
|
+
detail?: string;
|
|
23
|
+
channel: PushChannel;
|
|
24
|
+
logger?: Logger;
|
|
25
|
+
}): Promise<{
|
|
26
|
+
channel_used: 'wechat' | 'whatsapp' | 'webchat' | 'email' | 'none';
|
|
27
|
+
}>;
|
|
28
|
+
/** Append a single push-log entry as JSONL. Best-effort; swallows fs errors. */
|
|
29
|
+
export declare function appendPushLog(ledgerDir: string, entry: {
|
|
30
|
+
ts: string;
|
|
31
|
+
level: PushLevel;
|
|
32
|
+
summary: string;
|
|
33
|
+
detail?: string;
|
|
34
|
+
channel_requested: PushChannel;
|
|
35
|
+
channel_used: string;
|
|
36
|
+
}): void;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* notify_user — wechat → whatsapp → email fallback chain.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the bash `push()` recipe in ~/.claude/skills/push-api-skill/SKILL.md
|
|
5
|
+
* §B.2.5. Purposefully shells out via `openclaw` and the email script rather
|
|
6
|
+
* than reimplementing the API contracts — the skill is the source of truth.
|
|
7
|
+
*
|
|
8
|
+
* Per the recovery-path principle in CLAUDE.md: this code path must stay
|
|
9
|
+
* thin. If openclaw itself is down, the email fallback is independent
|
|
10
|
+
* (Gmail SMTP via the script).
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
import * as os from 'node:os';
|
|
16
|
+
import { nullLogger } from '../logger.js';
|
|
17
|
+
// Recipient identifiers are personal contact info — never hard-code them.
|
|
18
|
+
// Set the following env vars before relying on the matching channel; if a
|
|
19
|
+
// var is unset that channel is silently skipped and the fallback chain moves
|
|
20
|
+
// on. Email (independent SMTP via push-api-skill script) is the final tier
|
|
21
|
+
// regardless of these.
|
|
22
|
+
const WECHAT_RECIPIENT = process.env.AUTOLOOP_WECHAT_RECIPIENT ?? '';
|
|
23
|
+
const WECHAT_ACCOUNT = process.env.AUTOLOOP_WECHAT_ACCOUNT ?? '';
|
|
24
|
+
const WHATSAPP_RECIPIENT = process.env.AUTOLOOP_WHATSAPP_RECIPIENT ?? '';
|
|
25
|
+
async function runCmd(argv, opts = {}) {
|
|
26
|
+
const timeoutMs = opts.timeoutMs ?? 30_000;
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
const child = spawn(argv[0], argv.slice(1), {
|
|
29
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
30
|
+
});
|
|
31
|
+
let stdout = '';
|
|
32
|
+
let stderr = '';
|
|
33
|
+
let timedOut = false;
|
|
34
|
+
child.stdout?.on('data', (b) => (stdout += b.toString()));
|
|
35
|
+
child.stderr?.on('data', (b) => (stderr += b.toString()));
|
|
36
|
+
const t = setTimeout(() => {
|
|
37
|
+
timedOut = true;
|
|
38
|
+
try {
|
|
39
|
+
child.kill('SIGKILL');
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* ignore */
|
|
43
|
+
}
|
|
44
|
+
}, timeoutMs);
|
|
45
|
+
child.on('error', (err) => {
|
|
46
|
+
clearTimeout(t);
|
|
47
|
+
resolve({ exit_code: 127, stdout: '', stderr: err.message, timed_out: false });
|
|
48
|
+
});
|
|
49
|
+
child.on('exit', (code) => {
|
|
50
|
+
clearTimeout(t);
|
|
51
|
+
resolve({ exit_code: code ?? 0, stdout, stderr, timed_out: timedOut });
|
|
52
|
+
});
|
|
53
|
+
if (opts.stdin !== undefined) {
|
|
54
|
+
child.stdin?.write(opts.stdin);
|
|
55
|
+
child.stdin?.end();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function formatMessage(level, summary) {
|
|
60
|
+
const emoji = level === 'info' ? '🔔' : level === 'warn' ? '⚠️' : level === 'error' ? '❌' : '🚦';
|
|
61
|
+
return `${emoji} ${summary}`;
|
|
62
|
+
}
|
|
63
|
+
async function tryWechat(text, logger) {
|
|
64
|
+
if (!WECHAT_RECIPIENT || !WECHAT_ACCOUNT) {
|
|
65
|
+
logger.info?.('[autoloop/notify] wechat skipped: AUTOLOOP_WECHAT_RECIPIENT / AUTOLOOP_WECHAT_ACCOUNT not set');
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const r = await runCmd([
|
|
69
|
+
'openclaw',
|
|
70
|
+
'message',
|
|
71
|
+
'send',
|
|
72
|
+
'--channel',
|
|
73
|
+
'openclaw-weixin',
|
|
74
|
+
'--account',
|
|
75
|
+
WECHAT_ACCOUNT,
|
|
76
|
+
'-t',
|
|
77
|
+
WECHAT_RECIPIENT,
|
|
78
|
+
'-m',
|
|
79
|
+
text,
|
|
80
|
+
]);
|
|
81
|
+
if (r.exit_code === 0 && /✅\s*Sent/.test(r.stdout))
|
|
82
|
+
return true;
|
|
83
|
+
logger.warn?.(`[autoloop/notify] wechat failed: code=${r.exit_code} stderr=${r.stderr.slice(0, 200)}`);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
async function tryWhatsApp(text, logger) {
|
|
87
|
+
if (!WHATSAPP_RECIPIENT) {
|
|
88
|
+
logger.info?.('[autoloop/notify] whatsapp skipped: AUTOLOOP_WHATSAPP_RECIPIENT not set');
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const r = await runCmd([
|
|
92
|
+
'openclaw',
|
|
93
|
+
'message',
|
|
94
|
+
'send',
|
|
95
|
+
'--channel',
|
|
96
|
+
'whatsapp',
|
|
97
|
+
'-t',
|
|
98
|
+
WHATSAPP_RECIPIENT,
|
|
99
|
+
'-m',
|
|
100
|
+
text,
|
|
101
|
+
]);
|
|
102
|
+
if (r.exit_code === 0 && /✅\s*Sent/.test(r.stdout))
|
|
103
|
+
return true;
|
|
104
|
+
logger.warn?.(`[autoloop/notify] whatsapp failed: code=${r.exit_code} stderr=${r.stderr.slice(0, 200)}`);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
async function tryEmail(subject, body, logger) {
|
|
108
|
+
// Prefer the user-installed skill script; tolerate either ~/clawd or ~/.claude path.
|
|
109
|
+
const candidates = [
|
|
110
|
+
path.join(os.homedir(), 'clawd', 'skills', 'push-api-skill', 'scripts', 'send-email.sh'),
|
|
111
|
+
path.join(os.homedir(), '.claude', 'skills', 'push-api-skill', 'scripts', 'send-email.sh'),
|
|
112
|
+
];
|
|
113
|
+
const script = candidates.find((p) => fs.existsSync(p));
|
|
114
|
+
if (!script) {
|
|
115
|
+
logger.warn?.('[autoloop/notify] email fallback unavailable: send-email.sh not found');
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
const r = await runCmd(['bash', script, '-s', subject], { stdin: body, timeoutMs: 30_000 });
|
|
119
|
+
if (r.exit_code === 0)
|
|
120
|
+
return true;
|
|
121
|
+
logger.warn?.(`[autoloop/notify] email failed: code=${r.exit_code} stderr=${r.stderr.slice(0, 200)}`);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Send a notification, walking the fallback chain. Returns the channel that
|
|
126
|
+
* succeeded (or 'none' if all failed). Caller should record both attempt and
|
|
127
|
+
* outcome to the run's push log regardless.
|
|
128
|
+
*/
|
|
129
|
+
export async function notifyUserFallbackChain(opts) {
|
|
130
|
+
const logger = opts.logger ?? nullLogger;
|
|
131
|
+
const wechatText = formatMessage(opts.level, opts.summary);
|
|
132
|
+
const emailSubject = `[autoloop] ${opts.summary}`;
|
|
133
|
+
const emailBody = opts.detail ?? opts.summary;
|
|
134
|
+
// Channel-specific shortcuts: caller asked for a specific channel only.
|
|
135
|
+
if (opts.channel === 'wechat') {
|
|
136
|
+
return { channel_used: (await tryWechat(wechatText, logger)) ? 'wechat' : 'none' };
|
|
137
|
+
}
|
|
138
|
+
if (opts.channel === 'webchat') {
|
|
139
|
+
// S2 has no webchat session id wired up at the run level; treat as no-op.
|
|
140
|
+
return { channel_used: 'none' };
|
|
141
|
+
}
|
|
142
|
+
if (opts.channel === 'email') {
|
|
143
|
+
return { channel_used: (await tryEmail(emailSubject, emailBody, logger)) ? 'email' : 'none' };
|
|
144
|
+
}
|
|
145
|
+
// 'auto' or 'both' → walk the chain.
|
|
146
|
+
if (await tryWechat(wechatText, logger))
|
|
147
|
+
return { channel_used: 'wechat' };
|
|
148
|
+
if (await tryWhatsApp(`[微信失败已 fallback] ${wechatText}`, logger))
|
|
149
|
+
return { channel_used: 'whatsapp' };
|
|
150
|
+
if (await tryEmail(emailSubject, emailBody, logger))
|
|
151
|
+
return { channel_used: 'email' };
|
|
152
|
+
return { channel_used: 'none' };
|
|
153
|
+
}
|
|
154
|
+
/** Append a single push-log entry as JSONL. Best-effort; swallows fs errors. */
|
|
155
|
+
export function appendPushLog(ledgerDir, entry) {
|
|
156
|
+
try {
|
|
157
|
+
const file = path.join(ledgerDir, 'push_log.jsonl');
|
|
158
|
+
if (!fs.existsSync(ledgerDir))
|
|
159
|
+
fs.mkdirSync(ledgerDir, { recursive: true });
|
|
160
|
+
fs.appendFileSync(file, JSON.stringify(entry) + '\n');
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
/* swallow — logging failure shouldn't crash the run */
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=notify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.js","sourceRoot":"","sources":["../../../src/autoloop/notify.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Planner-emitted "tool calls" — parsing + handler dispatch.
|
|
3
|
+
*
|
|
4
|
+
* The Planner is a Claude Code subprocess; we cannot register first-class
|
|
5
|
+
* MCP tools without standing up an MCP server. Instead, the Planner emits
|
|
6
|
+
* structured intent as **fenced code blocks tagged `autoloop`**:
|
|
7
|
+
*
|
|
8
|
+
* ```autoloop
|
|
9
|
+
* {"tool": "notify_user", "args": {"level": "info", "summary": "plan ready"}}
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* After each Planner turn, the dispatcher scans the reply for these blocks,
|
|
13
|
+
* validates them, and translates them into runner-queue messages or direct
|
|
14
|
+
* runner-state mutations. Multiple blocks per turn are allowed and processed
|
|
15
|
+
* in order.
|
|
16
|
+
*
|
|
17
|
+
* The naming intentionally mirrors tasks/autoloop.md §4.2 so a future
|
|
18
|
+
* MCP-based implementation can swap the parser for real tool dispatch
|
|
19
|
+
* without changing any Planner-facing semantics.
|
|
20
|
+
*/
|
|
21
|
+
import { type AnyAutoloopMessage } from './messages.js';
|
|
22
|
+
export type PlannerToolName = 'notify_user' | 'spawn_subagents' | 'send_directive' | 'pause_loop' | 'resume_loop' | 'terminate' | 'update_push_policy' | 'write_plan_committed' | 'write_goal_committed';
|
|
23
|
+
export interface PlannerToolCall {
|
|
24
|
+
tool: PlannerToolName;
|
|
25
|
+
args: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
export interface PlannerToolParseResult {
|
|
28
|
+
calls: PlannerToolCall[];
|
|
29
|
+
/** Reply text with autoloop blocks stripped — what we actually show to user. */
|
|
30
|
+
cleaned_reply: string;
|
|
31
|
+
/** Per-block parse errors (block kept in cleaned reply for forensics). */
|
|
32
|
+
parse_errors: Array<{
|
|
33
|
+
block_index: number;
|
|
34
|
+
error: string;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Scan reply text for `autoloop` fenced JSON blocks. Returns parsed tool calls
|
|
39
|
+
* plus a cleaned reply with the blocks removed (so we don't show raw JSON to
|
|
40
|
+
* the user).
|
|
41
|
+
*/
|
|
42
|
+
export declare function parsePlannerReply(reply: string): PlannerToolParseResult;
|
|
43
|
+
export interface SpawnSubagentsArgs {
|
|
44
|
+
coder_model?: string;
|
|
45
|
+
coder_engine?: string;
|
|
46
|
+
reviewer_model?: string;
|
|
47
|
+
reviewer_engine?: string;
|
|
48
|
+
initial_directive?: {
|
|
49
|
+
goal: string;
|
|
50
|
+
constraints?: string[];
|
|
51
|
+
success_criteria?: string[];
|
|
52
|
+
max_attempts?: number;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export interface PlannerToolEffects {
|
|
56
|
+
/** Start Coder + Reviewer persistent sessions. S4 implements this. */
|
|
57
|
+
spawnSubagents: (args: SpawnSubagentsArgs) => Promise<void>;
|
|
58
|
+
/** Mutate in-memory push policy (key→rule object). Unknown keys ignored. */
|
|
59
|
+
updatePushPolicy: (delta: Record<string, unknown>) => void;
|
|
60
|
+
/** Best-effort `git add <file> && git commit -m <msg>` on the workspace. */
|
|
61
|
+
commitPlanFile: (file: 'plan.md' | 'goal.json', message?: string) => Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
export interface PlannerToolHandlerResult {
|
|
64
|
+
/** Messages the runner should push into its own queue. */
|
|
65
|
+
emitted_messages: AnyAutoloopMessage[];
|
|
66
|
+
/** Errors encountered while handling this batch (does not throw). */
|
|
67
|
+
errors: Array<{
|
|
68
|
+
tool: string;
|
|
69
|
+
error: string;
|
|
70
|
+
}>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Apply a batch of parsed tool calls in order. Returns any v2 envelopes that
|
|
74
|
+
* the dispatcher should hand back to the runner so it can route them.
|
|
75
|
+
*
|
|
76
|
+
* Note: notify_user / pause_loop / resume_loop / terminate / send_directive
|
|
77
|
+
* become v2 messages and flow through the runner's normal queue (so policy,
|
|
78
|
+
* dedup, push_log accounting all apply). Only spawn_subagents / commit /
|
|
79
|
+
* push-policy mutation are direct side effects.
|
|
80
|
+
*/
|
|
81
|
+
export declare function applyPlannerToolCalls(calls: PlannerToolCall[], fx: PlannerToolEffects, iter: number): Promise<PlannerToolHandlerResult>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Planner-emitted "tool calls" — parsing + handler dispatch.
|
|
3
|
+
*
|
|
4
|
+
* The Planner is a Claude Code subprocess; we cannot register first-class
|
|
5
|
+
* MCP tools without standing up an MCP server. Instead, the Planner emits
|
|
6
|
+
* structured intent as **fenced code blocks tagged `autoloop`**:
|
|
7
|
+
*
|
|
8
|
+
* ```autoloop
|
|
9
|
+
* {"tool": "notify_user", "args": {"level": "info", "summary": "plan ready"}}
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* After each Planner turn, the dispatcher scans the reply for these blocks,
|
|
13
|
+
* validates them, and translates them into runner-queue messages or direct
|
|
14
|
+
* runner-state mutations. Multiple blocks per turn are allowed and processed
|
|
15
|
+
* in order.
|
|
16
|
+
*
|
|
17
|
+
* The naming intentionally mirrors tasks/autoloop.md §4.2 so a future
|
|
18
|
+
* MCP-based implementation can swap the parser for real tool dispatch
|
|
19
|
+
* without changing any Planner-facing semantics.
|
|
20
|
+
*/
|
|
21
|
+
import { Msg } from './messages.js';
|
|
22
|
+
const FENCE_RE = /```autoloop\s*\n([\s\S]*?)\n```/g;
|
|
23
|
+
/**
|
|
24
|
+
* Scan reply text for `autoloop` fenced JSON blocks. Returns parsed tool calls
|
|
25
|
+
* plus a cleaned reply with the blocks removed (so we don't show raw JSON to
|
|
26
|
+
* the user).
|
|
27
|
+
*/
|
|
28
|
+
export function parsePlannerReply(reply) {
|
|
29
|
+
const calls = [];
|
|
30
|
+
const parse_errors = [];
|
|
31
|
+
let blockIndex = 0;
|
|
32
|
+
const cleaned = reply.replace(FENCE_RE, (_match, body) => {
|
|
33
|
+
const idx = blockIndex++;
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(body.trim());
|
|
36
|
+
if (typeof parsed?.tool !== 'string' || typeof parsed?.args !== 'object' || parsed.args === null) {
|
|
37
|
+
parse_errors.push({ block_index: idx, error: 'block missing tool/args fields' });
|
|
38
|
+
return ''; // strip even malformed blocks so user doesn't see raw JSON
|
|
39
|
+
}
|
|
40
|
+
calls.push(parsed);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
parse_errors.push({ block_index: idx, error: err.message });
|
|
44
|
+
}
|
|
45
|
+
return '';
|
|
46
|
+
});
|
|
47
|
+
return { calls, cleaned_reply: cleaned.trim(), parse_errors };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Apply a batch of parsed tool calls in order. Returns any v2 envelopes that
|
|
51
|
+
* the dispatcher should hand back to the runner so it can route them.
|
|
52
|
+
*
|
|
53
|
+
* Note: notify_user / pause_loop / resume_loop / terminate / send_directive
|
|
54
|
+
* become v2 messages and flow through the runner's normal queue (so policy,
|
|
55
|
+
* dedup, push_log accounting all apply). Only spawn_subagents / commit /
|
|
56
|
+
* push-policy mutation are direct side effects.
|
|
57
|
+
*/
|
|
58
|
+
export async function applyPlannerToolCalls(calls, fx, iter) {
|
|
59
|
+
const emitted_messages = [];
|
|
60
|
+
const errors = [];
|
|
61
|
+
for (const call of calls) {
|
|
62
|
+
try {
|
|
63
|
+
switch (call.tool) {
|
|
64
|
+
case 'notify_user': {
|
|
65
|
+
const { level, summary, detail, channel } = call.args;
|
|
66
|
+
if (!summary)
|
|
67
|
+
throw new Error('notify_user requires `summary`');
|
|
68
|
+
emitted_messages.push(Msg.pushUser(iter, {
|
|
69
|
+
level: level ?? 'info',
|
|
70
|
+
summary,
|
|
71
|
+
detail,
|
|
72
|
+
channel: channel ?? 'auto',
|
|
73
|
+
}));
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case 'spawn_subagents': {
|
|
77
|
+
await fx.spawnSubagents(call.args);
|
|
78
|
+
// If caller asked for an initial directive, fire it via the runner queue.
|
|
79
|
+
const init = call.args.initial_directive;
|
|
80
|
+
if (init?.goal) {
|
|
81
|
+
emitted_messages.push(Msg.directive(iter, {
|
|
82
|
+
goal: init.goal,
|
|
83
|
+
constraints: init.constraints ?? [],
|
|
84
|
+
success_criteria: init.success_criteria ?? [],
|
|
85
|
+
max_attempts: init.max_attempts ?? 1,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case 'send_directive': {
|
|
91
|
+
const { goal, constraints, success_criteria, max_attempts } = call.args;
|
|
92
|
+
if (!goal)
|
|
93
|
+
throw new Error('send_directive requires `goal`');
|
|
94
|
+
emitted_messages.push(Msg.directive(iter, {
|
|
95
|
+
goal,
|
|
96
|
+
constraints: constraints ?? [],
|
|
97
|
+
success_criteria: success_criteria ?? [],
|
|
98
|
+
max_attempts: max_attempts ?? 1,
|
|
99
|
+
}));
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case 'pause_loop': {
|
|
103
|
+
emitted_messages.push(Msg.pause(iter, {
|
|
104
|
+
reason: call.args.reason ?? 'planner-pause',
|
|
105
|
+
}));
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case 'resume_loop': {
|
|
109
|
+
emitted_messages.push(Msg.resume(iter));
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case 'terminate': {
|
|
113
|
+
emitted_messages.push(Msg.terminate(iter, {
|
|
114
|
+
reason: call.args.reason ?? 'planner-terminate',
|
|
115
|
+
}));
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case 'update_push_policy': {
|
|
119
|
+
fx.updatePushPolicy(call.args);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case 'write_plan_committed': {
|
|
123
|
+
await fx.commitPlanFile('plan.md', call.args.message);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case 'write_goal_committed': {
|
|
127
|
+
await fx.commitPlanFile('goal.json', call.args.message);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
default:
|
|
131
|
+
errors.push({ tool: call.tool, error: `unknown planner tool: ${call.tool}` });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
errors.push({ tool: call.tool, error: err.message });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { emitted_messages, errors };
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=planner-tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planner-tools.js","sourceRoot":"","sources":["../../../src/autoloop/planner-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAA2B,GAAG,EAAoC,MAAM,eAAe,CAAC;AA0B/F,MAAM,QAAQ,GAAG,kCAAkC,CAAC;AAEpD;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,MAAM,KAAK,GAAsB,EAAE,CAAC;IACpC,MAAM,YAAY,GAAkD,EAAE,CAAC;IACvE,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,IAAY,EAAE,EAAE;QAC/D,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAoB,CAAC;YAC1D,IAAI,OAAO,MAAM,EAAE,IAAI,KAAK,QAAQ,IAAI,OAAO,MAAM,EAAE,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;gBACjG,YAAY,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,EAAE,gCAAgC,EAAE,CAAC,CAAC;gBACjF,OAAO,EAAE,CAAC,CAAC,2DAA2D;YACxE,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACrB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,YAAY,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,EAAG,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACzE,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC,CAAC,CAAC;IACH,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,YAAY,EAAE,CAAC;AAChE,CAAC;AAuCD;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,KAAwB,EACxB,EAAsB,EACtB,IAAY;IAEZ,MAAM,gBAAgB,GAAyB,EAAE,CAAC;IAClD,MAAM,MAAM,GAA2C,EAAE,CAAC;IAE1D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;gBAClB,KAAK,aAAa,CAAC,CAAC,CAAC;oBACnB,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,IAKhD,CAAC;oBACF,IAAI,CAAC,OAAO;wBAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;oBAChE,gBAAgB,CAAC,IAAI,CACnB,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE;wBACjB,KAAK,EAAE,KAAK,IAAI,MAAM;wBACtB,OAAO;wBACP,MAAM;wBACN,OAAO,EAAE,OAAO,IAAI,MAAM;qBAC3B,CAAC,CACH,CAAC;oBACF,MAAM;gBACR,CAAC;gBACD,KAAK,iBAAiB,CAAC,CAAC,CAAC;oBACvB,MAAM,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,IAA0B,CAAC,CAAC;oBACzD,0EAA0E;oBAC1E,MAAM,IAAI,GAAI,IAAI,CAAC,IAA2B,CAAC,iBAAiB,CAAC;oBACjE,IAAI,IAAI,EAAE,IAAI,EAAE,CAAC;wBACf,gBAAgB,CAAC,IAAI,CACnB,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE;4BAClB,IAAI,EAAE,IAAI,CAAC,IAAI;4BACf,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,EAAE;4BACnC,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,IAAI,EAAE;4BAC7C,YAAY,EAAE,IAAI,CAAC,YAAY,IAAI,CAAC;yBACrC,CAAC,CACH,CAAC;oBACJ,CAAC;oBACD,MAAM;gBACR,CAAC;gBACD,KAAK,gBAAgB,CAAC,CAAC,CAAC;oBACtB,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC,IAKlE,CAAC;oBACF,IAAI,CAAC,IAAI;wBAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;oBAC7D,gBAAgB,CAAC,IAAI,CACnB,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE;wBAClB,IAAI;wBACJ,WAAW,EAAE,WAAW,IAAI,EAAE;wBAC9B,gBAAgB,EAAE,gBAAgB,IAAI,EAAE;wBACxC,YAAY,EAAE,YAAY,IAAI,CAAC;qBAChC,CAAC,CACH,CAAC;oBACF,MAAM;gBACR,CAAC;gBACD,KAAK,YAAY,CAAC,CAAC,CAAC;oBAClB,gBAAgB,CAAC,IAAI,CACnB,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE;wBACd,MAAM,EAAI,IAAI,CAAC,IAA4B,CAAC,MAAiB,IAAI,eAAe;qBACjF,CAAC,CACH,CAAC;oBACF,MAAM;gBACR,CAAC;gBACD,KAAK,aAAa,CAAC,CAAC,CAAC;oBACnB,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;oBACxC,MAAM;gBACR,CAAC;gBACD,KAAK,WAAW,CAAC,CAAC,CAAC;oBACjB,gBAAgB,CAAC,IAAI,CACnB,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE;wBAClB,MAAM,EAAI,IAAI,CAAC,IAA4B,CAAC,MAAiB,IAAI,mBAAmB;qBACrF,CAAC,CACH,CAAC;oBACF,MAAM;gBACR,CAAC;gBACD,KAAK,oBAAoB,CAAC,CAAC,CAAC;oBAC1B,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBAC/B,MAAM;gBACR,CAAC;gBACD,KAAK,sBAAsB,CAAC,CAAC,CAAC;oBAC5B,MAAM,EAAE,CAAC,cAAc,CAAC,SAAS,EAAG,IAAI,CAAC,IAA6B,CAAC,OAAO,CAAC,CAAC;oBAChF,MAAM;gBACR,CAAC;gBACD,KAAK,sBAAsB,CAAC,CAAC,CAAC;oBAC5B,MAAM,EAAE,CAAC,cAAc,CAAC,WAAW,EAAG,IAAI,CAAC,IAA6B,CAAC,OAAO,CAAC,CAAC;oBAClF,MAAM;gBACR,CAAC;gBACD;oBACE,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAc,EAAE,KAAK,EAAE,yBAAyB,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YAC5F,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAG,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;IAED,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,CAAC;AACtC,CAAC"}
|