wicked-crew 0.6.0 → 0.7.1
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 +103 -0
- package/dist/api/audit.d.ts +13 -0
- package/dist/api/audit.d.ts.map +1 -1
- package/dist/api/audit.js +18 -2
- package/dist/api/audit.js.map +1 -1
- package/dist/api/endpoint-manifest-live.d.ts +29 -0
- package/dist/api/endpoint-manifest-live.d.ts.map +1 -0
- package/dist/api/endpoint-manifest-live.js +72 -0
- package/dist/api/endpoint-manifest-live.js.map +1 -0
- package/dist/api/endpoint-manifest.d.ts +107 -0
- package/dist/api/endpoint-manifest.d.ts.map +1 -0
- package/dist/api/endpoint-manifest.js +108 -0
- package/dist/api/endpoint-manifest.js.map +1 -0
- package/dist/api/guidance-index.d.ts +39 -0
- package/dist/api/guidance-index.d.ts.map +1 -0
- package/dist/api/guidance-index.js +67 -0
- package/dist/api/guidance-index.js.map +1 -0
- package/dist/api/open-path.d.ts +16 -0
- package/dist/api/open-path.d.ts.map +1 -1
- package/dist/api/open-path.js +22 -0
- package/dist/api/open-path.js.map +1 -1
- package/dist/api/requirements.d.ts +7 -0
- package/dist/api/requirements.d.ts.map +1 -1
- package/dist/api/requirements.js +23 -2
- package/dist/api/requirements.js.map +1 -1
- package/dist/api/retry-index.d.ts +30 -0
- package/dist/api/retry-index.d.ts.map +1 -0
- package/dist/api/retry-index.js +45 -0
- package/dist/api/retry-index.js.map +1 -0
- package/dist/api/routes.d.ts +53 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +399 -30
- package/dist/api/routes.js.map +1 -1
- package/dist/api/run-files.d.ts +63 -0
- package/dist/api/run-files.d.ts.map +1 -0
- package/dist/api/run-files.js +271 -0
- package/dist/api/run-files.js.map +1 -0
- package/dist/api/server.d.ts +79 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +183 -8
- package/dist/api/server.js.map +1 -1
- package/dist/api/stall-watchdog.d.ts +62 -0
- package/dist/api/stall-watchdog.d.ts.map +1 -0
- package/dist/api/stall-watchdog.js +138 -0
- package/dist/api/stall-watchdog.js.map +1 -0
- package/dist/cli/index.js +89 -15
- package/dist/cli/index.js.map +1 -1
- package/dist/core/adapter.d.ts +36 -10
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +245 -31
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/bridge-reaper.d.ts +134 -0
- package/dist/core/bridge-reaper.d.ts.map +1 -0
- package/dist/core/bridge-reaper.js +286 -0
- package/dist/core/bridge-reaper.js.map +1 -0
- package/dist/core/deliver.d.ts +118 -0
- package/dist/core/deliver.d.ts.map +1 -0
- package/dist/core/deliver.js +241 -0
- package/dist/core/deliver.js.map +1 -0
- package/dist/core/deliverable-floor.d.ts +155 -0
- package/dist/core/deliverable-floor.d.ts.map +1 -0
- package/dist/core/deliverable-floor.js +248 -0
- package/dist/core/deliverable-floor.js.map +1 -0
- package/dist/core/exec.d.ts +2 -0
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js.map +1 -1
- package/dist/core/types.d.ts +79 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -1
- package/dist/interactive/bridge-pool.d.ts +52 -0
- package/dist/interactive/bridge-pool.d.ts.map +1 -1
- package/dist/interactive/bridge-pool.js +93 -12
- package/dist/interactive/bridge-pool.js.map +1 -1
- package/dist/interactive/chat-events.d.ts +207 -0
- package/dist/interactive/chat-events.d.ts.map +1 -0
- package/dist/interactive/chat-events.js +769 -0
- package/dist/interactive/chat-events.js.map +1 -0
- package/dist/interactive/demo-events.d.ts +283 -0
- package/dist/interactive/demo-events.d.ts.map +1 -0
- package/dist/interactive/demo-events.js +889 -0
- package/dist/interactive/demo-events.js.map +1 -0
- package/dist/interactive/draft-events.d.ts +87 -7
- package/dist/interactive/draft-events.d.ts.map +1 -1
- package/dist/interactive/draft-events.js +352 -49
- package/dist/interactive/draft-events.js.map +1 -1
- package/dist/interactive/edit-events.d.ts +25 -2
- package/dist/interactive/edit-events.d.ts.map +1 -1
- package/dist/interactive/edit-events.js +88 -9
- package/dist/interactive/edit-events.js.map +1 -1
- package/dist/interactive/repo-snapshot.d.ts +100 -0
- package/dist/interactive/repo-snapshot.d.ts.map +1 -0
- package/dist/interactive/repo-snapshot.js +289 -0
- package/dist/interactive/repo-snapshot.js.map +1 -0
- package/dist/projects/graph-paths.d.ts +122 -0
- package/dist/projects/graph-paths.d.ts.map +1 -0
- package/dist/projects/graph-paths.js +175 -0
- package/dist/projects/graph-paths.js.map +1 -0
- package/dist/projects/graph.d.ts +191 -0
- package/dist/projects/graph.d.ts.map +1 -0
- package/dist/projects/graph.js +829 -0
- package/dist/projects/graph.js.map +1 -0
- package/dist/projects/routes.d.ts +17 -0
- package/dist/projects/routes.d.ts.map +1 -1
- package/dist/projects/routes.js +139 -0
- package/dist/projects/routes.js.map +1 -1
- package/dist/qe/ledger.d.ts +3 -2
- package/dist/qe/ledger.d.ts.map +1 -1
- package/dist/qe/ledger.js +5 -4
- package/dist/qe/ledger.js.map +1 -1
- package/dist/studio/assets/index-D-BFUYnY.js +530 -0
- package/dist/studio/assets/index-D6S9zUtO.css +32 -0
- package/dist/studio/index.html +5 -3
- package/endpoint-manifest.json +624 -0
- package/package.json +8 -5
- package/dist/studio/assets/index-CCwXa1cn.js +0 -428
- package/dist/studio/assets/index-HWxo0h41.css +0 -32
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP bridge child reaper — bridges must die with the daemon (crew#285).
|
|
3
|
+
*
|
|
4
|
+
* # The defect
|
|
5
|
+
*
|
|
6
|
+
* The engine spawns ACP bridge binaries as direct OS children of this daemon process,
|
|
7
|
+
* and the only kill handles for them live in the engine's in-memory registry. When the
|
|
8
|
+
* daemon dies — pkill during a restart, an operator's Ctrl-C, a plain `process.exit` —
|
|
9
|
+
* that registry dies with it and nothing reaps the bridges: they linger detached until
|
|
10
|
+
* their own session-idle logic (if any) gets around to exiting. Operators observed
|
|
11
|
+
* three `claude-agent-acp` processes coexisting while exactly one unit was executing.
|
|
12
|
+
*
|
|
13
|
+
* Same defect family as crew#277's cancel-orphans, but for daemon death rather than
|
|
14
|
+
* run cancellation.
|
|
15
|
+
*
|
|
16
|
+
* # The fix, in two halves
|
|
17
|
+
*
|
|
18
|
+
* 1. THIS MODULE (daemon side): a central registry of bridge child pids plus an OS
|
|
19
|
+
* process-table sweep, wired into the daemon's shutdown path. On SIGTERM/SIGINT the
|
|
20
|
+
* daemon SIGTERMs every bridge child, waits a short grace for them to exit, then
|
|
21
|
+
* SIGKILLs survivors. On plain `exit` (where no async work is possible) it fires a
|
|
22
|
+
* synchronous best-effort SIGTERM sweep.
|
|
23
|
+
*
|
|
24
|
+
* `register()` exists for any JS-side spawn site (and for tests); the engine-spawned
|
|
25
|
+
* bridges are found by `discoverBridgeChildren()` — a scan of the OS process table
|
|
26
|
+
* for DIRECT children of this daemon whose command line names a bridge binary. The
|
|
27
|
+
* direct-child restriction is the safety rail: another daemon's bridges have a
|
|
28
|
+
* different parent pid and are never touched.
|
|
29
|
+
*
|
|
30
|
+
* 2. `packages/agent-acp-bridges` (bridge side): the bridge treats stdin EOF as the
|
|
31
|
+
* portable parent-death signal and reaps its own CLI child instead of lingering
|
|
32
|
+
* until the CLI finishes. See `bridge.mjs`. That half covers our own bridges even
|
|
33
|
+
* when the daemon dies too hard (SIGKILL) for this module to run at all.
|
|
34
|
+
*
|
|
35
|
+
* # Why a process-table sweep rather than tracked pids alone
|
|
36
|
+
*
|
|
37
|
+
* The spawn happens inside the engine (the native actor thread), which reports no pid
|
|
38
|
+
* back to JS — there is nothing for the daemon to `register()`. The bridges ARE this
|
|
39
|
+
* process's direct children though, and their command lines name the bridge binaries
|
|
40
|
+
* (npm `.bin` shims exec `node .../<bridge-name>/...`), so a ppid-filtered scan
|
|
41
|
+
* recovers exactly the set the in-memory kill handles would have covered. The sweep
|
|
42
|
+
* fails OPEN (returns nothing) when the platform tooling is unavailable: a shutdown
|
|
43
|
+
* that cannot enumerate children must still shut down.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* The bridge binaries the engine spawns by bare name on PATH. Mirrors the set audited
|
|
47
|
+
* by `bridge-names.test.ts` (declared dependencies of this package + the shims a real
|
|
48
|
+
* install produces); `bridge-reaper.test.ts` cross-checks this list against those same
|
|
49
|
+
* dependency manifests so a bridge added or dropped there cannot silently drift here.
|
|
50
|
+
*/
|
|
51
|
+
export declare const BRIDGE_BINS: readonly string[];
|
|
52
|
+
/** How long a SIGTERM'd bridge gets to exit before the SIGKILL escalation. */
|
|
53
|
+
export declare const BRIDGE_KILL_GRACE_MS = 2000;
|
|
54
|
+
export declare function parseBridgeChildren(listing: string, parentPid: number): number[];
|
|
55
|
+
/**
|
|
56
|
+
* Pids of every live bridge process that is a direct child of `parentPid`. Fails open:
|
|
57
|
+
* an unreadable process table yields `[]`, never a throw — shutdown must proceed.
|
|
58
|
+
*/
|
|
59
|
+
export declare function discoverBridgeChildren(parentPid?: number): number[];
|
|
60
|
+
/**
|
|
61
|
+
* Pids of bridge processes ORPHANED by a previous daemon generation: reparented to
|
|
62
|
+
* init (ppid 1). Deliberately conservative — a bridge owned by another LIVE daemon
|
|
63
|
+
* still has that daemon as its parent and is never matched, so a boot sweep cannot
|
|
64
|
+
* shoot a neighbour's workers (#285, Copilot review: shutdown-only reaping leaves
|
|
65
|
+
* pre-existing orphans alive forever).
|
|
66
|
+
*/
|
|
67
|
+
export declare function parseOrphanedBridges(listing: string): number[];
|
|
68
|
+
/**
|
|
69
|
+
* Boot-time sweep: SIGTERM orphaned bridges from a prior daemon generation — ppid 1
|
|
70
|
+
* AND cwd inside a run worktree, so user-started bridges are never matched.
|
|
71
|
+
*/
|
|
72
|
+
export declare function reapOrphansAtBoot(io?: BridgeReaperIo): number[];
|
|
73
|
+
/** Injectable seams so the reaper is testable without signalling real bridges. */
|
|
74
|
+
export interface BridgeReaperIo {
|
|
75
|
+
/** Signal sender; must throw like `process.kill` (ESRCH when the pid is gone). */
|
|
76
|
+
kill?: (pid: number, signal: NodeJS.Signals | 0) => void;
|
|
77
|
+
/** Bridge-child discovery; defaults to the process-table sweep above. */
|
|
78
|
+
discover?: (parentPid: number) => number[];
|
|
79
|
+
/** Boot-sweep worktree-cwd discriminator; injectable so tests avoid real lsof. */
|
|
80
|
+
cwdInWorktree?: (pid: number) => boolean;
|
|
81
|
+
sleep?: (ms: number) => Promise<void>;
|
|
82
|
+
graceMs?: number;
|
|
83
|
+
}
|
|
84
|
+
/** What `shutdown()` did, for logs and assertions. */
|
|
85
|
+
export interface ReapReport {
|
|
86
|
+
/** Pids that exited within the grace window after SIGTERM. */
|
|
87
|
+
terminated: number[];
|
|
88
|
+
/** Pids that ignored SIGTERM and were SIGKILLed. */
|
|
89
|
+
killed: number[];
|
|
90
|
+
/** Pids whose SIGKILL could not be delivered (e.g. EPERM) — still possibly alive. */
|
|
91
|
+
undeliverable?: number[];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The central bridge-child registry plus the shutdown path that empties it.
|
|
95
|
+
*
|
|
96
|
+
* A single instance (`bridgeReaper`) is wired into the daemon's shutdown handlers;
|
|
97
|
+
* tests construct their own with fake IO.
|
|
98
|
+
*/
|
|
99
|
+
export declare class BridgeReaper {
|
|
100
|
+
private readonly tracked;
|
|
101
|
+
private readonly io;
|
|
102
|
+
constructor(io?: BridgeReaperIo);
|
|
103
|
+
/** Track a bridge child pid. Invalid pids (spawn failures yield `undefined`) are ignored. */
|
|
104
|
+
register(pid: number | undefined): void;
|
|
105
|
+
/** Stop tracking a pid — call when the child's `close`/`exit` is observed. */
|
|
106
|
+
unregister(pid: number): void;
|
|
107
|
+
/** Currently tracked pids (registered only; discovery happens at kill time). */
|
|
108
|
+
pids(): number[];
|
|
109
|
+
/**
|
|
110
|
+
* Deliver `signal`. For the signal-0 liveness probe, EPERM means "alive but not
|
|
111
|
+
* ours" and counts as existing; for real signals EPERM means the kill was NOT
|
|
112
|
+
* delivered and must not be reported as success (Copilot review on #300).
|
|
113
|
+
*/
|
|
114
|
+
private signal;
|
|
115
|
+
/** Registered pids ∪ discovered direct-child bridges, deduplicated. */
|
|
116
|
+
private targets;
|
|
117
|
+
/**
|
|
118
|
+
* Graceful reap: SIGTERM every target, poll liveness for the grace window, SIGKILL
|
|
119
|
+
* survivors. Idempotent — dead pids are skipped, and the registry is cleared so a
|
|
120
|
+
* second invocation (the `exit` sweep after a signal-path shutdown) finds nothing
|
|
121
|
+
* registered and only re-discovers what actually still lives.
|
|
122
|
+
*/
|
|
123
|
+
shutdown(): Promise<ReapReport>;
|
|
124
|
+
/**
|
|
125
|
+
* Synchronous best-effort sweep for the `exit` event, where no async work (and so no
|
|
126
|
+
* grace window) is possible. SIGTERM only — a synchronous SIGKILL would deny a bridge
|
|
127
|
+
* the chance to reap ITS child CLI, recreating the orphan problem one level down.
|
|
128
|
+
* The bridges' own stdin-EOF watchdog is the backstop for anything that ignores this.
|
|
129
|
+
*/
|
|
130
|
+
sweepSync(): void;
|
|
131
|
+
}
|
|
132
|
+
/** The daemon-wide reaper the CLI wires into its shutdown handlers. */
|
|
133
|
+
export declare const bridgeReaper: BridgeReaper;
|
|
134
|
+
//# sourceMappingURL=bridge-reaper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge-reaper.d.ts","sourceRoot":"","sources":["../../src/core/bridge-reaper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAIH;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,SAAS,MAAM,EAKxC,CAAC;AAEF,8EAA8E;AAC9E,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAwBzC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAehF;AA8BD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,GAAE,MAAoB,GAAG,MAAM,EAAE,CAGhF;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAY9D;AAwBD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,GAAE,cAAmB,GAAG,MAAM,EAAE,CAgBnE;AAED,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC7B,kFAAkF;IAClF,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,CAAC,KAAK,IAAI,CAAC;IACzD,yEAAyE;IACzE,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IAC3C,kFAAkF;IAClF,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACzC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAID,sDAAsD;AACtD,MAAM,WAAW,UAAU;IACzB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,oDAAoD;IACpD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,qFAAqF;IACrF,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED;;;;;GAKG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;gBAExB,EAAE,GAAE,cAAmB;IAInC,6FAA6F;IAC7F,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAIvC,8EAA8E;IAC9E,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAI7B,gFAAgF;IAChF,IAAI,IAAI,MAAM,EAAE;IAIhB;;;;OAIG;IACH,OAAO,CAAC,MAAM;IAUd,uEAAuE;IACvE,OAAO,CAAC,OAAO;IAMf;;;;;OAKG;IACG,QAAQ,IAAI,OAAO,CAAC,UAAU,CAAC;IAuBrC;;;;;OAKG;IACH,SAAS,IAAI,IAAI;CAIlB;AAED,uEAAuE;AACvE,eAAO,MAAM,YAAY,cAAqB,CAAC"}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP bridge child reaper — bridges must die with the daemon (crew#285).
|
|
3
|
+
*
|
|
4
|
+
* # The defect
|
|
5
|
+
*
|
|
6
|
+
* The engine spawns ACP bridge binaries as direct OS children of this daemon process,
|
|
7
|
+
* and the only kill handles for them live in the engine's in-memory registry. When the
|
|
8
|
+
* daemon dies — pkill during a restart, an operator's Ctrl-C, a plain `process.exit` —
|
|
9
|
+
* that registry dies with it and nothing reaps the bridges: they linger detached until
|
|
10
|
+
* their own session-idle logic (if any) gets around to exiting. Operators observed
|
|
11
|
+
* three `claude-agent-acp` processes coexisting while exactly one unit was executing.
|
|
12
|
+
*
|
|
13
|
+
* Same defect family as crew#277's cancel-orphans, but for daemon death rather than
|
|
14
|
+
* run cancellation.
|
|
15
|
+
*
|
|
16
|
+
* # The fix, in two halves
|
|
17
|
+
*
|
|
18
|
+
* 1. THIS MODULE (daemon side): a central registry of bridge child pids plus an OS
|
|
19
|
+
* process-table sweep, wired into the daemon's shutdown path. On SIGTERM/SIGINT the
|
|
20
|
+
* daemon SIGTERMs every bridge child, waits a short grace for them to exit, then
|
|
21
|
+
* SIGKILLs survivors. On plain `exit` (where no async work is possible) it fires a
|
|
22
|
+
* synchronous best-effort SIGTERM sweep.
|
|
23
|
+
*
|
|
24
|
+
* `register()` exists for any JS-side spawn site (and for tests); the engine-spawned
|
|
25
|
+
* bridges are found by `discoverBridgeChildren()` — a scan of the OS process table
|
|
26
|
+
* for DIRECT children of this daemon whose command line names a bridge binary. The
|
|
27
|
+
* direct-child restriction is the safety rail: another daemon's bridges have a
|
|
28
|
+
* different parent pid and are never touched.
|
|
29
|
+
*
|
|
30
|
+
* 2. `packages/agent-acp-bridges` (bridge side): the bridge treats stdin EOF as the
|
|
31
|
+
* portable parent-death signal and reaps its own CLI child instead of lingering
|
|
32
|
+
* until the CLI finishes. See `bridge.mjs`. That half covers our own bridges even
|
|
33
|
+
* when the daemon dies too hard (SIGKILL) for this module to run at all.
|
|
34
|
+
*
|
|
35
|
+
* # Why a process-table sweep rather than tracked pids alone
|
|
36
|
+
*
|
|
37
|
+
* The spawn happens inside the engine (the native actor thread), which reports no pid
|
|
38
|
+
* back to JS — there is nothing for the daemon to `register()`. The bridges ARE this
|
|
39
|
+
* process's direct children though, and their command lines name the bridge binaries
|
|
40
|
+
* (npm `.bin` shims exec `node .../<bridge-name>/...`), so a ppid-filtered scan
|
|
41
|
+
* recovers exactly the set the in-memory kill handles would have covered. The sweep
|
|
42
|
+
* fails OPEN (returns nothing) when the platform tooling is unavailable: a shutdown
|
|
43
|
+
* that cannot enumerate children must still shut down.
|
|
44
|
+
*/
|
|
45
|
+
import { spawnSync } from 'node:child_process';
|
|
46
|
+
/**
|
|
47
|
+
* The bridge binaries the engine spawns by bare name on PATH. Mirrors the set audited
|
|
48
|
+
* by `bridge-names.test.ts` (declared dependencies of this package + the shims a real
|
|
49
|
+
* install produces); `bridge-reaper.test.ts` cross-checks this list against those same
|
|
50
|
+
* dependency manifests so a bridge added or dropped there cannot silently drift here.
|
|
51
|
+
*/
|
|
52
|
+
export const BRIDGE_BINS = [
|
|
53
|
+
'agy-acp',
|
|
54
|
+
'claude-agent-acp',
|
|
55
|
+
'codex-acp',
|
|
56
|
+
'pi-acp',
|
|
57
|
+
];
|
|
58
|
+
/** How long a SIGTERM'd bridge gets to exit before the SIGKILL escalation. */
|
|
59
|
+
export const BRIDGE_KILL_GRACE_MS = 2000;
|
|
60
|
+
/** How often the grace window re-checks survivor liveness. */
|
|
61
|
+
const POLL_INTERVAL_MS = 100;
|
|
62
|
+
/**
|
|
63
|
+
* One line of a `pid ppid command` process listing → the pids of DIRECT children of
|
|
64
|
+
* `parentPid` whose command line names a bridge binary. Pure, so the parsing is
|
|
65
|
+
* testable without a real process table.
|
|
66
|
+
*/
|
|
67
|
+
/**
|
|
68
|
+
* Matches `bin` as a whole command token, never a substring: start/whitespace/path-sep/
|
|
69
|
+
* quote before; an optional Windows launcher extension (.cmd/.exe/.bat) and then
|
|
70
|
+
* end/whitespace/path-sep/quote after — so `pi-acp`, `/x/pi-acp`, `"C:\\x\\pi-acp.cmd"`
|
|
71
|
+
* all match while `api-acp` and `copy-of-pi-acp-backup` never do.
|
|
72
|
+
*/
|
|
73
|
+
function bridgeTokenRe(bin) {
|
|
74
|
+
const esc = bin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
75
|
+
return new RegExp(`(?:^|[\\s/\\\\"'])${esc}(?:\\.(?:cmd|exe|bat))?(?:[\\s/\\\\"']|$)`, 'i');
|
|
76
|
+
}
|
|
77
|
+
/** Precompiled per-bin matchers — the scan loops run one test per line, no per-line RegExp churn. */
|
|
78
|
+
const BRIDGE_TOKEN_RES = BRIDGE_BINS.map((bin) => bridgeTokenRe(bin));
|
|
79
|
+
export function parseBridgeChildren(listing, parentPid) {
|
|
80
|
+
const pids = [];
|
|
81
|
+
for (const line of listing.split('\n')) {
|
|
82
|
+
const m = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line);
|
|
83
|
+
if (m === null)
|
|
84
|
+
continue;
|
|
85
|
+
const pid = Number(m[1]);
|
|
86
|
+
const ppid = Number(m[2]);
|
|
87
|
+
const command = m[3];
|
|
88
|
+
if (ppid !== parentPid || pid === parentPid)
|
|
89
|
+
continue;
|
|
90
|
+
// Token-boundary match: `pi-acp` must not match inside `api-acp` (Copilot review
|
|
91
|
+
// on #300 post-merge). A bridge binary appears as its own token — start-of-line,
|
|
92
|
+
// whitespace, or a path separator before it; end-of-token after.
|
|
93
|
+
if (BRIDGE_TOKEN_RES.some((re) => re.test(command)))
|
|
94
|
+
pids.push(pid);
|
|
95
|
+
}
|
|
96
|
+
return pids;
|
|
97
|
+
}
|
|
98
|
+
/** `pid ppid command` listing of every process, or null when the platform tooling fails. */
|
|
99
|
+
function listProcesses() {
|
|
100
|
+
try {
|
|
101
|
+
const out = process.platform === 'win32'
|
|
102
|
+
? // wmic is removed on current Windows; CIM via PowerShell is the stable surface.
|
|
103
|
+
spawnSync('powershell', [
|
|
104
|
+
'-NoProfile',
|
|
105
|
+
'-Command',
|
|
106
|
+
"Get-CimInstance Win32_Process | ForEach-Object { '{0} {1} {2}' -f $_.ProcessId, $_.ParentProcessId, $_.CommandLine }",
|
|
107
|
+
], { encoding: 'utf8', windowsHide: true, maxBuffer: 16 * 1024 * 1024 })
|
|
108
|
+
: // POSIX keywords (`args`, not the BSD/procps-specific `command`): same spelling
|
|
109
|
+
// works on macOS and Linux.
|
|
110
|
+
spawnSync('ps', ['-A', '-o', 'pid=,ppid=,args='], {
|
|
111
|
+
encoding: 'utf8',
|
|
112
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
113
|
+
});
|
|
114
|
+
if (out.error !== undefined || out.status !== 0 || typeof out.stdout !== 'string')
|
|
115
|
+
return null;
|
|
116
|
+
return out.stdout;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Pids of every live bridge process that is a direct child of `parentPid`. Fails open:
|
|
124
|
+
* an unreadable process table yields `[]`, never a throw — shutdown must proceed.
|
|
125
|
+
*/
|
|
126
|
+
export function discoverBridgeChildren(parentPid = process.pid) {
|
|
127
|
+
const listing = listProcesses();
|
|
128
|
+
return listing === null ? [] : parseBridgeChildren(listing, parentPid);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Pids of bridge processes ORPHANED by a previous daemon generation: reparented to
|
|
132
|
+
* init (ppid 1). Deliberately conservative — a bridge owned by another LIVE daemon
|
|
133
|
+
* still has that daemon as its parent and is never matched, so a boot sweep cannot
|
|
134
|
+
* shoot a neighbour's workers (#285, Copilot review: shutdown-only reaping leaves
|
|
135
|
+
* pre-existing orphans alive forever).
|
|
136
|
+
*/
|
|
137
|
+
export function parseOrphanedBridges(listing) {
|
|
138
|
+
const pids = [];
|
|
139
|
+
for (const line of listing.split('\n')) {
|
|
140
|
+
const m = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line);
|
|
141
|
+
if (m === null)
|
|
142
|
+
continue;
|
|
143
|
+
const pid = Number(m[1]);
|
|
144
|
+
const ppid = Number(m[2]);
|
|
145
|
+
const command = m[3];
|
|
146
|
+
if (ppid !== 1 || pid === process.pid)
|
|
147
|
+
continue;
|
|
148
|
+
if (BRIDGE_TOKEN_RES.some((re) => re.test(command)))
|
|
149
|
+
pids.push(pid);
|
|
150
|
+
}
|
|
151
|
+
return pids;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* True when `pid`'s working directory sits inside an engine run worktree — the cwd
|
|
155
|
+
* contract of every engine-spawned bridge. A user's own nohup'd `*-acp` process also
|
|
156
|
+
* reparents to init but runs from an arbitrary cwd, so this is the discriminator that
|
|
157
|
+
* keeps the boot sweep from shooting it (Copilot review on #300). POSIX only (lsof);
|
|
158
|
+
* anywhere the cwd cannot be read the answer is false — never kill on uncertainty.
|
|
159
|
+
*/
|
|
160
|
+
function pidRunsInRunWorktree(pid) {
|
|
161
|
+
if (process.platform === 'win32')
|
|
162
|
+
return false;
|
|
163
|
+
try {
|
|
164
|
+
const out = spawnSync('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], {
|
|
165
|
+
encoding: 'utf8',
|
|
166
|
+
maxBuffer: 1024 * 1024,
|
|
167
|
+
});
|
|
168
|
+
if (out.error !== undefined || out.status !== 0 || typeof out.stdout !== 'string')
|
|
169
|
+
return false;
|
|
170
|
+
const cwd = out.stdout.split('\n').find((l) => l.startsWith('n'))?.slice(1) ?? '';
|
|
171
|
+
return cwd.includes('wicked-worktrees');
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Boot-time sweep: SIGTERM orphaned bridges from a prior daemon generation — ppid 1
|
|
179
|
+
* AND cwd inside a run worktree, so user-started bridges are never matched.
|
|
180
|
+
*/
|
|
181
|
+
export function reapOrphansAtBoot(io = {}) {
|
|
182
|
+
const listing = listProcesses();
|
|
183
|
+
if (listing === null)
|
|
184
|
+
return [];
|
|
185
|
+
const orphans = parseOrphanedBridges(listing).filter((pid) => (io.cwdInWorktree ?? pidRunsInRunWorktree)(pid));
|
|
186
|
+
const reaped = [];
|
|
187
|
+
for (const pid of orphans) {
|
|
188
|
+
try {
|
|
189
|
+
(io.kill ?? process.kill)(pid, 'SIGTERM');
|
|
190
|
+
reaped.push(pid);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// ESRCH (already gone) / EPERM (not ours): skip silently — fail open.
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return reaped;
|
|
197
|
+
}
|
|
198
|
+
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
199
|
+
/**
|
|
200
|
+
* The central bridge-child registry plus the shutdown path that empties it.
|
|
201
|
+
*
|
|
202
|
+
* A single instance (`bridgeReaper`) is wired into the daemon's shutdown handlers;
|
|
203
|
+
* tests construct their own with fake IO.
|
|
204
|
+
*/
|
|
205
|
+
export class BridgeReaper {
|
|
206
|
+
tracked = new Set();
|
|
207
|
+
io;
|
|
208
|
+
constructor(io = {}) {
|
|
209
|
+
this.io = io;
|
|
210
|
+
}
|
|
211
|
+
/** Track a bridge child pid. Invalid pids (spawn failures yield `undefined`) are ignored. */
|
|
212
|
+
register(pid) {
|
|
213
|
+
if (typeof pid === 'number' && Number.isInteger(pid) && pid > 0)
|
|
214
|
+
this.tracked.add(pid);
|
|
215
|
+
}
|
|
216
|
+
/** Stop tracking a pid — call when the child's `close`/`exit` is observed. */
|
|
217
|
+
unregister(pid) {
|
|
218
|
+
this.tracked.delete(pid);
|
|
219
|
+
}
|
|
220
|
+
/** Currently tracked pids (registered only; discovery happens at kill time). */
|
|
221
|
+
pids() {
|
|
222
|
+
return [...this.tracked];
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Deliver `signal`. For the signal-0 liveness probe, EPERM means "alive but not
|
|
226
|
+
* ours" and counts as existing; for real signals EPERM means the kill was NOT
|
|
227
|
+
* delivered and must not be reported as success (Copilot review on #300).
|
|
228
|
+
*/
|
|
229
|
+
signal(pid, signal) {
|
|
230
|
+
try {
|
|
231
|
+
(this.io.kill ?? process.kill)(pid, signal);
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
const eperm = err.code === 'EPERM';
|
|
236
|
+
return signal === 0 ? eperm : false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** Registered pids ∪ discovered direct-child bridges, deduplicated. */
|
|
240
|
+
targets() {
|
|
241
|
+
const all = new Set(this.tracked);
|
|
242
|
+
for (const pid of (this.io.discover ?? discoverBridgeChildren)(process.pid))
|
|
243
|
+
all.add(pid);
|
|
244
|
+
return [...all];
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Graceful reap: SIGTERM every target, poll liveness for the grace window, SIGKILL
|
|
248
|
+
* survivors. Idempotent — dead pids are skipped, and the registry is cleared so a
|
|
249
|
+
* second invocation (the `exit` sweep after a signal-path shutdown) finds nothing
|
|
250
|
+
* registered and only re-discovers what actually still lives.
|
|
251
|
+
*/
|
|
252
|
+
async shutdown() {
|
|
253
|
+
const graceMs = this.io.graceMs ?? BRIDGE_KILL_GRACE_MS;
|
|
254
|
+
const sleep = this.io.sleep ?? defaultSleep;
|
|
255
|
+
const targets = this.targets().filter((pid) => this.signal(pid, 0));
|
|
256
|
+
for (const pid of targets)
|
|
257
|
+
this.signal(pid, 'SIGTERM');
|
|
258
|
+
const deadline = Date.now() + graceMs;
|
|
259
|
+
let survivors = targets.filter((pid) => this.signal(pid, 0));
|
|
260
|
+
while (survivors.length > 0 && Date.now() < deadline) {
|
|
261
|
+
await sleep(Math.min(POLL_INTERVAL_MS, graceMs));
|
|
262
|
+
survivors = survivors.filter((pid) => this.signal(pid, 0));
|
|
263
|
+
}
|
|
264
|
+
const killed = survivors.filter((pid) => this.signal(pid, 'SIGKILL'));
|
|
265
|
+
this.tracked.clear();
|
|
266
|
+
return {
|
|
267
|
+
terminated: targets.filter((pid) => !survivors.includes(pid)),
|
|
268
|
+
killed,
|
|
269
|
+
undeliverable: survivors.filter((pid) => !killed.includes(pid)),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Synchronous best-effort sweep for the `exit` event, where no async work (and so no
|
|
274
|
+
* grace window) is possible. SIGTERM only — a synchronous SIGKILL would deny a bridge
|
|
275
|
+
* the chance to reap ITS child CLI, recreating the orphan problem one level down.
|
|
276
|
+
* The bridges' own stdin-EOF watchdog is the backstop for anything that ignores this.
|
|
277
|
+
*/
|
|
278
|
+
sweepSync() {
|
|
279
|
+
for (const pid of this.targets())
|
|
280
|
+
this.signal(pid, 'SIGTERM');
|
|
281
|
+
this.tracked.clear();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/** The daemon-wide reaper the CLI wires into its shutdown handlers. */
|
|
285
|
+
export const bridgeReaper = new BridgeReaper();
|
|
286
|
+
//# sourceMappingURL=bridge-reaper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge-reaper.js","sourceRoot":"","sources":["../../src/core/bridge-reaper.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first-class deliver phase (crew#293) — a run opens its own PR, opt-in.
|
|
3
|
+
*
|
|
4
|
+
* Productizes the operator-side `feature-pr` overlay proven during the DES-MERGE-001 campaign:
|
|
5
|
+
* a Tool phase appended after the workflow's last phase that COMMITS the run's work, pushes the
|
|
6
|
+
* run's branch and opens a PR via `gh`. What was data on one machine becomes a launch option
|
|
7
|
+
* (`deliver: "pr"` on `POST /runs`), composed PER RUN — the shared workflow def is never mutated.
|
|
8
|
+
*
|
|
9
|
+
* Field-proven hardening, replicated here:
|
|
10
|
+
* (a) the branch is derived from the run worktree's basename (`wicked/<run-id>`), falling back
|
|
11
|
+
* to the current branch when that ref does not exist;
|
|
12
|
+
* (b) the script REFUSES to push `main`/`master` (or an empty/detached branch name) — the
|
|
13
|
+
* deliver phase only ever pushes run branches;
|
|
14
|
+
* (c) it STAGES AND COMMITS the run's work, then rebases onto origin's default branch before
|
|
15
|
+
* pushing, and a conflicting rebase FAILS the phase visibly (aborting the rebase, pushing
|
|
16
|
+
* nothing) rather than pushing a conflicted tree;
|
|
17
|
+
* (d) `git push -u origin <branch>`;
|
|
18
|
+
* (e) `gh pr create --head <branch> --fill`, with gh's output and exit status captured
|
|
19
|
+
* SEPARATELY so a gh failure fails the phase carrying gh's own message;
|
|
20
|
+
* (f) the PR URL is the last line of the phase output.
|
|
21
|
+
*
|
|
22
|
+
* One deliberate change from the field version: NO gh account is baked into crew code (the
|
|
23
|
+
* overlay guarded a personal account). Instead, when the `GH_ACCOUNT` env var is set the script
|
|
24
|
+
* compares it against `gh api user -q .login` and runs
|
|
25
|
+
* `gh auth switch --hostname github.com --user "$GH_ACCOUNT"` only when they differ.
|
|
26
|
+
*
|
|
27
|
+
* Merge stays human: the phase opens the PR, never merges it.
|
|
28
|
+
*
|
|
29
|
+
* ## crew#317 — "pushed an empty branch and reported success"
|
|
30
|
+
*
|
|
31
|
+
* Run `d1bc72c2` (wicked-studio) delivered nothing while reporting `completed`. The persisted
|
|
32
|
+
* unit is the evidence, and it names the cause precisely. Its `tool_cmd` was NOT this script —
|
|
33
|
+
* it was the operator's hand-written `feature-pr` OVERLAY def, which begins `set -e` with **no
|
|
34
|
+
* `pipefail`**, so `gh pr create … | tail -1` reported `tail`'s status (0) and the phase passed
|
|
35
|
+
* with gh's error text where the PR URL belongs:
|
|
36
|
+
*
|
|
37
|
+
* ```text
|
|
38
|
+
* could not compute title or body defaults: could not find any commits between origin/main and
|
|
39
|
+
* wicked/d1bc72c2-…
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* So the masking mechanism the issue hypothesised is real, but it belonged to the overlay, not
|
|
43
|
+
* here: `pipefail` IS in force for this executor (it is line 1 of this script, `bash -lc` runs it
|
|
44
|
+
* verbatim, and core's `run_tool_cmd` maps any non-zero exit to `StepStatus::Failed`). The three
|
|
45
|
+
* defects this script genuinely shared with the overlay are fixed below:
|
|
46
|
+
*
|
|
47
|
+
* 1. **No commit.** Agents write files and do not commit, so the pushed branch equalled the
|
|
48
|
+
* default branch. The script now stages and commits the run's work itself, and REFUSES to
|
|
49
|
+
* push when there is nothing to deliver.
|
|
50
|
+
* 2. **A masked result.** `| tail -1` discarded everything gh said except one line and made the
|
|
51
|
+
* phase's verdict depend on a pipe option. gh's output and status are now captured
|
|
52
|
+
* separately, and success is re-derived from a real PR URL rather than from an exit code.
|
|
53
|
+
* 3. **Ungoverned.** The phase shipped `verified_evidence: false` / `validator_pin: null`, so
|
|
54
|
+
* nothing re-derived what it claimed. It now declares `verified_evidence: true` — see
|
|
55
|
+
* {@link deliverPrPhase}.
|
|
56
|
+
*/
|
|
57
|
+
import type { PhaseDef, WorkflowDef } from './types.js';
|
|
58
|
+
/** The id of the appended phase — also the collision probe when a def already delivers. */
|
|
59
|
+
export declare const DELIVER_PHASE_ID = "deliver";
|
|
60
|
+
/**
|
|
61
|
+
* The hardened deliver script, run as `bash -lc <script>` (login shell so the operator's PATH —
|
|
62
|
+
* where `gh` lives — is loaded, same as the field overlay).
|
|
63
|
+
*
|
|
64
|
+
* `set -euo pipefail` is load-bearing and verified in force for this executor (crew#317): the
|
|
65
|
+
* engine spawns `bash -lc` with this text verbatim, and `run_tool_cmd` turns a non-zero exit into
|
|
66
|
+
* `StepStatus::Failed`. It is no longer the ONLY thing standing between a failed `gh` and a green
|
|
67
|
+
* phase, though — the gh result is captured explicitly and success is re-derived from evidence.
|
|
68
|
+
*
|
|
69
|
+
* `intent` (the run's problem statement) rides in the commit subject; it is sanitised by
|
|
70
|
+
* {@link commitSubjectIntent} before it is spliced in.
|
|
71
|
+
*/
|
|
72
|
+
export declare function deliverPrScript(intent?: string): string;
|
|
73
|
+
/**
|
|
74
|
+
* The deliver phase definition — the PhaseDef JSON shape core accepts, fully spelled out so the
|
|
75
|
+
* composed def round-trips through `registerWorkflow` (core's serde) and crew's own `WorkflowDef`
|
|
76
|
+
* type without casts. `gate: 'auto'` + `executes_code: false`: the phase is deterministic tooling,
|
|
77
|
+
* not governed agent work — its failure surface is the exit code + output, which core reports as
|
|
78
|
+
* a failed unit.
|
|
79
|
+
*
|
|
80
|
+
* ## Why `verified_evidence: true` and `validator_pin: null` (crew#317)
|
|
81
|
+
*
|
|
82
|
+
* The phase that touches the remote was the one phase nothing re-derived. The engine's phase model
|
|
83
|
+
* DOES let a Tool-executor phase carry a deterministic floor, and the mechanism is the flag rather
|
|
84
|
+
* than a pin crew mints itself:
|
|
85
|
+
*
|
|
86
|
+
* - a pin is a CONTENT ADDRESS into core's validator vault, and `attach_pinned_validators` is
|
|
87
|
+
* fail-closed on one that does not resolve — it BAILS the run at plan time. Authoring and
|
|
88
|
+
* approving a validator is `wicked-core provision-validator` + `approve-validator`, neither of
|
|
89
|
+
* which is exposed through the napi surface crew drives, so a pin invented here would fail
|
|
90
|
+
* every run of every deliver-composed workflow on a machine that had not been seeded by hand;
|
|
91
|
+
* - `verified_evidence: true` with no pin of its own is armed AT REGISTRATION by
|
|
92
|
+
* `enforce_verified_evidence` with the built-in evidence floor (`EVIDENCE_FLOOR_PIN`,
|
|
93
|
+
* criterion: "the run left a change in its worktree (done is re-derived from the diff, never
|
|
94
|
+
* asserted)"), which `pre_distribute` seeds on the plan path so it always resolves. Same
|
|
95
|
+
* mechanism `feature/test`, `bug/verify` and `migration/verify` reach it by.
|
|
96
|
+
*
|
|
97
|
+
* The floor then re-runs against the run's worktree at the gate, INDEPENDENTLY of anything this
|
|
98
|
+
* script printed, and denies the phase when the run left no change. Layer 2 (the agent judge)
|
|
99
|
+
* stays out of it: core hands the Tool path `agent_verdict: None`, so a tool phase's floor is
|
|
100
|
+
* deterministic and costs no LLM call. The PR-URL and branch-ahead assertions stay in the script
|
|
101
|
+
* because no vaulted floor can see the remote.
|
|
102
|
+
*/
|
|
103
|
+
export declare function deliverPrPhase(dependsOn?: string[], intent?: string): PhaseDef;
|
|
104
|
+
/**
|
|
105
|
+
* Compose a PER-RUN workflow def: `base`'s phases (untouched — the shared def is never mutated)
|
|
106
|
+
* plus the deliver phase appended last, under a run-scoped id. The caller registers the result
|
|
107
|
+
* with the engine for THIS run only; nothing is written to the overlay dir and the composed id
|
|
108
|
+
* never enters the user-workflow registry, so the catalog (`GET /workflows`) stays clean.
|
|
109
|
+
*
|
|
110
|
+
* `intent` is the run's problem statement — it names WHAT was delivered in the commit subject
|
|
111
|
+
* (`wicked-crew run <run-id>: <intent>`); omit it and the subject carries the run id alone.
|
|
112
|
+
*
|
|
113
|
+
* Throws when `base` already carries a `deliver` phase — appending a second phase with the same
|
|
114
|
+
* id would be ambiguous at best; the caller launches such a def as-is instead (see
|
|
115
|
+
* `CoreAdapter.launchRun`).
|
|
116
|
+
*/
|
|
117
|
+
export declare function composeDeliverWorkflow(base: WorkflowDef, runId: string, intent?: string): WorkflowDef;
|
|
118
|
+
//# sourceMappingURL=deliver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deliver.d.ts","sourceRoot":"","sources":["../../src/core/deliver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAExD,2FAA2F;AAC3F,eAAO,MAAM,gBAAgB,YAAY,CAAC;AA0B1C;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAqEvD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,cAAc,CAAC,SAAS,GAAE,MAAM,EAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAgBlF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,MAAM,GACd,WAAW,CAmBb"}
|