@reserendipity/dsh-superagent 0.0.0-stage → 0.1.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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +50 -0
  3. package/README.md +206 -2
  4. package/cordis.patch.yml +214 -0
  5. package/examples/profile.cordis.patch.yml +31 -0
  6. package/lib/client.js +7695 -0
  7. package/lib/feishu-Dvj-1eJf.js +1325 -0
  8. package/lib/feishu-watch.d.ts +125 -0
  9. package/lib/feishu-watch.js +2 -0
  10. package/lib/github-pull-DRiuuSs8.js +597 -0
  11. package/lib/github-watch.d.ts +2 -0
  12. package/lib/github-watch.js +836 -0
  13. package/lib/host-BIWKSB5j.js +124 -0
  14. package/lib/inbox-watch.d.ts +43 -0
  15. package/lib/inbox-watch.js +343 -0
  16. package/lib/index.d.ts +369 -0
  17. package/lib/index.js +13654 -0
  18. package/lib/instance-status-Cqaju-dw.js +87 -0
  19. package/lib/instances-view.d.ts +164 -0
  20. package/lib/instances-view.js +258 -0
  21. package/lib/intake-TNIkqilh.d.ts +735 -0
  22. package/lib/kind--8woUEex.d.ts +1019 -0
  23. package/lib/poll-Bm5EYA2c.js +819 -0
  24. package/lib/schemas-MhNERx8d.js +4484 -0
  25. package/lib/schemas-lx-F2nnt.d.ts +2134 -0
  26. package/locale/en.json +6 -0
  27. package/locale/zh.json +6 -0
  28. package/package.json +159 -4
  29. package/prompts/admission.md +68 -0
  30. package/prompts/cli.md +17 -0
  31. package/prompts/dispatch.md +59 -0
  32. package/prompts/handoff-orchestrator.md +4 -0
  33. package/prompts/handoff-worker.md +7 -0
  34. package/prompts/handoff.md +6 -0
  35. package/prompts/orchestrator.md +16 -0
  36. package/prompts/presets/brief/communication.md +4 -0
  37. package/prompts/presets/brief/delivery.md +3 -0
  38. package/prompts/presets/brief/scope.md +4 -0
  39. package/prompts/presets/evidence/communication.md +6 -0
  40. package/prompts/presets/evidence/delivery.md +4 -0
  41. package/prompts/presets/evidence/scope.md +5 -0
  42. package/prompts/presets/result/communication.md +4 -0
  43. package/prompts/presets/result/delivery.md +3 -0
  44. package/prompts/presets/result/scope.md +5 -0
  45. package/prompts/replies.md +35 -0
  46. package/prompts/shared.md +9 -0
  47. package/prompts/subscriptions.md +13 -0
  48. package/prompts/team/communication.md +101 -0
  49. package/prompts/team/delivery.md +35 -0
  50. package/prompts/team/scope.md +26 -0
  51. package/prompts/work.md +49 -0
  52. package/prompts/worker.md +10 -0
  53. package/schema/declarative-settings.example.json +26 -0
  54. package/schema/declarative-settings.schema.json +348 -0
  55. package/skills/README.md +14 -0
  56. package/skills/dsh-superagent-feishu/SKILL.md +12 -0
  57. package/skills/dsh-superagent-github/SKILL.md +14 -0
@@ -0,0 +1,819 @@
1
+ import { a as fill, p as liveTicket } from "./host-BIWKSB5j.js";
2
+ import { c as object, i as boolean, n as _enum, p as unknown, r as array, s as number, u as string } from "./schemas-MhNERx8d.js";
3
+ import { accessSync, constants, realpathSync, statSync } from "node:fs";
4
+ import { basename, delimiter, isAbsolute, join, resolve } from "node:path";
5
+ import { lstat, mkdir, readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
6
+ import { execFile } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+ import s from "@deepseek-ai/schemastery";
9
+ //#region src/limits.ts
10
+ /** Validation bounds shared between host schemas and management panel forms. */
11
+ const LIMITS = {
12
+ ruleName: 100,
13
+ ruleInstruction: 2e3,
14
+ evalTimeoutSeconds: {
15
+ min: 30,
16
+ max: 600
17
+ },
18
+ maxWorkers: {
19
+ min: 1,
20
+ max: 16
21
+ },
22
+ prepareCommand: 4e3,
23
+ promptMaxChars: {
24
+ min: 1e3,
25
+ max: 1e6
26
+ },
27
+ admissionContextMaxPages: {
28
+ min: 1,
29
+ max: 100
30
+ },
31
+ admissionContextTimeoutMs: {
32
+ min: 1,
33
+ max: 9e4
34
+ }
35
+ };
36
+ //#endregion
37
+ //#region src/setup.ts
38
+ /** Structured startup and configuration errors with the settings location where the operator can fix them. */
39
+ const SETUP_PROBLEMS = {
40
+ foreignFile: ["deployment", "{file} was not written by dsh-superagent; move it, or choose another state directory."],
41
+ gitTooOld: ["deployment", "git does not read the github.com credential configuration dsh-superagent writes; dsh-superagent needs git 2.32 or later."],
42
+ multipleSourcesUnsupported: ["deployment", "This process serves one context boundary, but these GitHub Sources were configured: {sources}. Run one instance per repository (or per team sharing memory and credentials) instead of adding repositories here; see docs/011."],
43
+ multiAccountUnsupported: ["deployment", "These GitHub Sources use different agent accounts: {accounts}. One process serves several repositories only when they share one account, until per-instance accounts land (docs/010-multi-source.md)."],
44
+ environmentFailed: ["deployment", "Preparing the agent environment failed."],
45
+ repositoryUnset: ["github", "The GitHub Source names no repository."],
46
+ agentCliUnset: ["github", "The agent GitHub CLI is not set."],
47
+ agentCliMissing: ["github", "The agent GitHub CLI was not found."],
48
+ agentCliSignedOut: ["github", "The agent GitHub CLI cannot read its account."],
49
+ agentCliNoAccount: ["github", "The agent GitHub CLI did not report a GitHub account."],
50
+ repositoryUnreadable: ["github", "@{account} cannot read {repository}."],
51
+ repositoryUnreported: ["github", "The agent GitHub CLI did not return {repository}."],
52
+ repositoryReadOnly: ["github", "@{account} cannot push to {repository}."],
53
+ feishuCliFailed: ["feishu", "The bot lark-cli does not work."],
54
+ feishuNoApp: ["feishu", "auth status of the bot lark-cli did not report the app."],
55
+ feishuBotNotReady: ["feishu", "The bot identity of app {app} is {status}; configure the app secret with lark-cli."],
56
+ feishuUserIdentity: ["feishu", "The bot lark-cli allows user identities (strict mode {mode}); give the bot its own lark-cli configuration in strict mode bot, as docs/006-deployment.md describes."],
57
+ feishuNoBot: ["feishu", "The Feishu bot API did not return the bot's open_id."]
58
+ };
59
+ /**
60
+ * Thrown by deployment and account verification checks when a required CLI, account, or repository check fails.
61
+ * `Setup` records it for the management overview, and the HTTP layer returns `problem` so the panel can localize it.
62
+ */
63
+ var SetupError = class extends Error {
64
+ code;
65
+ values;
66
+ place;
67
+ constructor(code, values = {}) {
68
+ const [place, text] = SETUP_PROBLEMS[code];
69
+ const sentence = fill(text, values);
70
+ super(values.output ? `${sentence}\n${values.output}` : sentence);
71
+ this.code = code;
72
+ this.values = values;
73
+ this.place = place;
74
+ }
75
+ get problem() {
76
+ return {
77
+ place: this.place,
78
+ code: this.code,
79
+ values: this.values,
80
+ detail: this.message
81
+ };
82
+ }
83
+ };
84
+ //#endregion
85
+ //#region src/infra/commands.ts
86
+ /** Subprocess wrapper for running gh, lark-cli, and git commands under the deployment's configured accounts. */
87
+ const exec = promisify(execFile);
88
+ const TIMEOUT_MS = 9e4;
89
+ /**
90
+ * Error thrown by Commands when a CLI invocation fails, capturing stderr diagnostics, API error codes, and exit status without echoing command arguments.
91
+ * Source channels and pollers inspect its code to distinguish deleted conversations from retryable failures.
92
+ */
93
+ var CliError = class extends Error {
94
+ code;
95
+ exitCode;
96
+ constructor(message, code, exitCode) {
97
+ super(message);
98
+ this.code = code;
99
+ this.exitCode = exitCode;
100
+ }
101
+ };
102
+ /** Extracts a sanitized error from a failed subprocess without echoing argv, which may contain message text. */
103
+ function commandError(error, program = "command") {
104
+ if (error instanceof Error && error.name === "AbortError") return error;
105
+ const exitCode = error && typeof error === "object" && "code" in error && typeof error.code === "number" ? error.code : void 0;
106
+ if (error && typeof error === "object" && "stderr" in error && typeof error.stderr === "string") {
107
+ const stderr = error.stderr.trim();
108
+ let parsed;
109
+ try {
110
+ parsed = JSON.parse(stderr.slice(stderr.indexOf("{")));
111
+ } catch {}
112
+ const code = parsed?.error?.code;
113
+ if (parsed?.error?.message) return new CliError(`${parsed.error.message}${code ? ` (${code})` : ""}`, code, exitCode);
114
+ if (stderr) return new CliError(stderr.slice(0, 2e3), Number(/\bHTTP (\d{3})\b/.exec(stderr)?.[1]) || void 0, exitCode);
115
+ }
116
+ return new CliError(`${basename(program)} ${ended(error)}`, void 0, exitCode);
117
+ }
118
+ function ended(error) {
119
+ const { killed, signal, code } = error ?? {};
120
+ if (code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") return "printed more output than it may";
121
+ if (killed && signal === "SIGTERM") return `timed out after ${TIMEOUT_MS / 1e3} s`;
122
+ if (signal) return `was stopped by ${signal}`;
123
+ if (typeof code === "number") return `exited with code ${code}`;
124
+ return typeof code === "string" ? `could not run: ${code}` : "failed";
125
+ }
126
+ /** Parses array elements against a schema, skipping invalid items and incrementing `tally.skipped`. */
127
+ function validItems(item, list, tally) {
128
+ const items = list.flatMap((value) => {
129
+ const parsed = item.safeParse(value);
130
+ return parsed.success ? [parsed.data] : [];
131
+ });
132
+ if (tally) tally.skipped += list.length - items.length;
133
+ return items;
134
+ }
135
+ /** Rewrites Feishu error 232011 (bot not in chat) with remediation instructions. */
136
+ function membership(error) {
137
+ if (!(error instanceof CliError) || error.code !== 232011) return error;
138
+ return new CliError("The Feishu bot is not a member of the chat. Add the bot in the chat settings, then retry.", error.code, error.exitCode);
139
+ }
140
+ /**
141
+ * Executes gh, lark-cli, and git subprocesses with deployment account credentials, timeouts, and abort signals.
142
+ * Restricts GitHub API calls to the configured repository and translates CLI failures into CliError instances for Sources, MasterAssessor, Outbox, and Settings.
143
+ */
144
+ var Commands = class Commands {
145
+ resolved;
146
+ environment;
147
+ signal;
148
+ /**
149
+ * @param resolved - returns the resolved deployment setup, or throws while setup is still pending.
150
+ * @param environment - variables overriding `process.env` for every command (such as isolated git settings).
151
+ */
152
+ constructor(resolved, environment, signal) {
153
+ this.resolved = resolved;
154
+ this.environment = environment;
155
+ this.signal = signal;
156
+ }
157
+ get deployment() {
158
+ return this.resolved();
159
+ }
160
+ get repository() {
161
+ return this.deployment.repository;
162
+ }
163
+ withSignal(signal) {
164
+ return signal ? new Commands(this.resolved, this.environment, AbortSignal.any([this.signal, signal])) : this;
165
+ }
166
+ async run(program, args, cwd) {
167
+ try {
168
+ const { stdout } = await exec(program, args, {
169
+ cwd,
170
+ signal: this.signal,
171
+ timeout: TIMEOUT_MS,
172
+ maxBuffer: 16777216,
173
+ env: {
174
+ ...process.env,
175
+ ...this.environment,
176
+ GH_PROMPT_DISABLED: "1",
177
+ LARKSUITE_CLI_NO_UPDATE_NOTIFIER: "1",
178
+ LARKSUITE_CLI_NO_SKILLS_NOTIFIER: "1"
179
+ }
180
+ });
181
+ return stdout;
182
+ } catch (error) {
183
+ throw commandError(error, program);
184
+ }
185
+ }
186
+ /** Runs the agent's GitHub CLI and parses its JSON output; callers should prefer `api`, which is restricted to the configured repository and validates output. */
187
+ async gh(args) {
188
+ return JSON.parse(await this.run(this.deployment.github.cli, args));
189
+ }
190
+ async api(schema, ...rest) {
191
+ if (typeof schema === "string") return this.run(this.deployment.github.cli, [
192
+ "api",
193
+ this.path(schema),
194
+ ...rest
195
+ ]);
196
+ const [path = "", ...flags] = rest;
197
+ return schema.parse(await this.gh([
198
+ "api",
199
+ this.path(path),
200
+ ...flags
201
+ ]));
202
+ }
203
+ /** Fetches all pages of a GitHub API list within the configured repository, skipping items that fail schema validation. */
204
+ async pages(item, path, tally) {
205
+ return validItems(item, array(array(unknown())).parse(await this.gh([
206
+ "api",
207
+ this.path(path),
208
+ "--paginate",
209
+ "--slurp"
210
+ ])).flat(), tally);
211
+ }
212
+ path(path) {
213
+ if (/^\/|^[a-z]+:|(^|\/)\.\.?(\/|\?|$)/i.test(path)) throw new Error("GitHub calls are restricted to the configured repository");
214
+ return path ? `repos/${this.repository}/${path}` : `repos/${this.repository}`;
215
+ }
216
+ /**
217
+ * Runs the Feishu bot's `lark-cli` and returns the JSON `data` payload.
218
+ * @throws CliError with Feishu's error code on failure (rewriting 232011 when the bot is not in the chat).
219
+ */
220
+ async lark(args, cwd) {
221
+ const bot = this.deployment.feishu;
222
+ if (!bot) throw new Error("The Feishu Source has no working bot CLI");
223
+ const output = await this.run(bot.cli, [
224
+ ...args,
225
+ "--as",
226
+ "bot",
227
+ "--format",
228
+ "json"
229
+ ], cwd).catch((error) => {
230
+ throw membership(error);
231
+ });
232
+ const result = JSON.parse(output);
233
+ const code = result.error?.code;
234
+ if (!result.ok) throw membership(new CliError(`${result.error?.message ?? "Feishu command failed"}${code ? ` (${code})` : ""}`, code));
235
+ return result.data;
236
+ }
237
+ /** Runs `git` on PATH using the agent's isolated git environment and credential helper. */
238
+ async git(args, cwd) {
239
+ return this.run("git", args, cwd);
240
+ }
241
+ };
242
+ //#endregion
243
+ //#region src/infra/environment.ts
244
+ /**
245
+ * Process environment and CLI shims that bind git, gh, and lark-cli to the deployment's accounts and block disallowed commands.
246
+ * Variables read by workers avoid `KEY|PASSWORD|SECRET|TOKEN` in their names because DSH strips credential-shaped variables from child environments.
247
+ * Provides default-path guardrails rather than an OS-level security boundary, as workers run under the operator's OS user.
248
+ */
249
+ const MARKER = "# Written by dsh-superagent on every start; edits are lost.";
250
+ /**
251
+ * Some networks drop github.com:443 while ssh.github.com:443 stays reachable, which would make every worker
252
+ * `git fetch` fail even though the GitHub API (api.github.com) works. Setting SUPERAGENT_GIT_SSH=1 rewrites the
253
+ * github.com HTTPS URLs worker git uses to SSH, so the transport can follow the network without touching accounts.
254
+ */
255
+ const sshTransport = () => process.env.SUPERAGENT_GIT_SSH === "1";
256
+ /** Markers of files written under the project's earlier names (`dsh-tag`, `dsh-go`, `dsh-freelancer`), which `dsh-superagent` owns and replaces too. */
257
+ const LEGACY_MARKERS = [
258
+ "dsh-tag",
259
+ "dsh-go",
260
+ "dsh-freelancer"
261
+ ].map((name) => `# Written by ${name} on every start; edits are lost.`);
262
+ /** Windows needs batch shims and drive-qualified paths; every other platform keeps the POSIX forms below. */
263
+ const windows = process.platform === "win32";
264
+ /** Commands workers need, which `blockedCommands` cannot block: the shims and git. */
265
+ const WORKER_COMMANDS = [
266
+ "gh",
267
+ "git",
268
+ "lark-cli"
269
+ ];
270
+ const workerBin = (stateDir) => join(stateDir, "worker-bin");
271
+ const workerGitConfig = (stateDir) => join(stateDir, "worker-gitconfig");
272
+ /**
273
+ * The agent's own GitHub CLI configuration directory. Windows selects the agent account through GH_CONFIG_DIR because
274
+ * Node refuses to spawn a `.cmd` wrapper (`spawn EINVAL`), so the upstream wrapper-script trick cannot be the CLI here.
275
+ */
276
+ const agentGhConfig = (stateDir) => join(stateDir, "gh-config");
277
+ const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
278
+ const gitQuote = (value) => `"${value.replace(/[\\"]/g, (char) => `\\${char}`)}"`;
279
+ const credentialHelper = (cli) => `!${shellQuote(cli)} auth git-credential`;
280
+ function sameDirectory(a, b) {
281
+ const real = (path) => {
282
+ try {
283
+ return realpathSync(path);
284
+ } catch {
285
+ return resolve(path);
286
+ }
287
+ };
288
+ return real(a) === real(b);
289
+ }
290
+ function withoutDirectory(path, dir) {
291
+ return path.split(delimiter).filter((entry) => !sameDirectory(entry, dir)).join(delimiter);
292
+ }
293
+ /**
294
+ * The variables that tell git which repository, work tree and index to act on. A host started inside git, such as by
295
+ * `git rebase --exec`, passes them on, and a git started below it would act on that repository. GIT_CEILING_DIRECTORIES is
296
+ * kept: it names no repository, and it stops git's search for one where the operator chose.
297
+ */
298
+ const GIT_LOCATION = [
299
+ "GIT_DIR",
300
+ "GIT_WORK_TREE",
301
+ "GIT_INDEX_FILE",
302
+ "GIT_PREFIX",
303
+ "GIT_COMMON_DIR",
304
+ "GIT_OBJECT_DIRECTORY",
305
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",
306
+ "GIT_NAMESPACE"
307
+ ];
308
+ /**
309
+ * Returns environment variable overrides that strip ambient GitHub tokens, interactive prompts, the repository the host's own
310
+ * environment may name, and operator git identity/config overrides.
311
+ */
312
+ function agentVariables(stateDir) {
313
+ return {
314
+ GH_TOKEN: void 0,
315
+ GITHUB_TOKEN: void 0,
316
+ ...windows ? { GH_CONFIG_DIR: agentGhConfig(stateDir) } : {},
317
+ ...Object.fromEntries(GIT_LOCATION.map((name) => [name, void 0])),
318
+ GIT_CONFIG_GLOBAL: workerGitConfig(stateDir),
319
+ GIT_CONFIG_NOSYSTEM: "1",
320
+ GIT_CONFIG_COUNT: void 0,
321
+ GIT_CONFIG_PARAMETERS: void 0,
322
+ GIT_ASKPASS: "",
323
+ SSH_ASKPASS: void 0,
324
+ GIT_TERMINAL_PROMPT: "0",
325
+ GIT_AUTHOR_NAME: void 0,
326
+ GIT_COMMITTER_NAME: void 0,
327
+ GIT_AUTHOR_EMAIL: void 0,
328
+ GIT_COMMITTER_EMAIL: void 0
329
+ };
330
+ }
331
+ /** True when a regular file starts with the dsh-superagent marker in its first two lines; a symbolic link, directory, or other non-regular file never counts as written by dsh-superagent. */
332
+ async function written(file) {
333
+ if (!(await lstat(file)).isFile()) return false;
334
+ const head = (await readFile(file, "utf8")).split(/\r?\n/, 2).map((line) => line.replace(/^@rem\s+/i, ""));
335
+ return [MARKER, ...LEGACY_MARKERS].some((marker) => head.includes(marker));
336
+ }
337
+ async function own(file, text, mode = 420) {
338
+ let foreign = false;
339
+ try {
340
+ foreign = !await written(file);
341
+ } catch {}
342
+ if (foreign) throw new SetupError("foreignFile", { file });
343
+ const temporary = `${file}.${process.pid}.tmp`;
344
+ await writeFile(temporary, text, { mode });
345
+ await rename(temporary, file);
346
+ }
347
+ /**
348
+ * Atomically writes worker CLI shims and the isolated git configuration file into the state directory, refusing to overwrite foreign files.
349
+ * Each shim strips the shim directory from PATH before exec so wrapper scripts resolving `gh` or `lark-cli` by name reach the real binary.
350
+ */
351
+ async function writeEnvironment(deployment, blockedCommands) {
352
+ const { stateDir, repository, github, feishu } = deployment;
353
+ const dir = workerBin(stateDir);
354
+ await mkdir(dir, { recursive: true });
355
+ await mkdir(agentGhConfig(stateDir), { recursive: true });
356
+ const before = await readdir(dir);
357
+ for (const name of before) {
358
+ const file = join(dir, name);
359
+ if (!await written(file)) throw new SetupError("foreignFile", { file });
360
+ }
361
+ const kept = /* @__PURE__ */ new Set();
362
+ const shim = async (name, body) => {
363
+ const entry = windows ? `${name}.cmd` : name;
364
+ const temporary = join(dir, `.${name}.${process.pid}.tmp`);
365
+ const text = windows ? `@echo off\r\n@REM ${MARKER}\r\n${body}\r\n` : `#!/bin/sh\n${MARKER}\n${body}\n`;
366
+ await writeFile(temporary, text, { mode: windows ? 420 : 493 });
367
+ await rename(temporary, join(dir, entry));
368
+ kept.add(entry);
369
+ };
370
+ const withoutShims = [
371
+ `self=${shellQuote(dir)}`,
372
+ "rest=",
373
+ "IFS=:",
374
+ "set -f",
375
+ "for entry in $PATH; do [ \"$entry\" = \"$self\" ] || rest=\"${rest:+$rest:}$entry\"; done",
376
+ "set +f",
377
+ "unset IFS",
378
+ "PATH=$rest",
379
+ "export PATH"
380
+ ].join("\n");
381
+ const withoutShimsWindows = [
382
+ "set \"_SELF=%~dp0\"",
383
+ "if \"%_SELF:~-1%\"==\"\\\" set \"_SELF=%_SELF:~0,-1%\"",
384
+ "setlocal EnableDelayedExpansion",
385
+ "set \"_NEW=\"",
386
+ "for %%P in (\"!PATH:;=\" \"!\") do if /I not \"%%~P\"==\"!_SELF!\" set \"_NEW=!_NEW!;%%~P\"",
387
+ "endlocal & set \"PATH=%_NEW:~1%\""
388
+ ].join("\r\n");
389
+ const run = (program) => windows ? `${withoutShimsWindows}\r\n"${program}" %*\r\nexit /b %ERRORLEVEL%` : `${withoutShims}\nexec ${shellQuote(program)} "$@"`;
390
+ const fail = (message) => windows ? `>&2 echo ${message}\r\nexit /b 1` : `echo ${shellQuote(message)} >&2\nexit 1`;
391
+ if (github.cli) await shim("gh", run(github.cli));
392
+ await shim("lark-cli", feishu ? run(feishu.cli) : fail("lark-cli is unavailable: this installation has no Feishu bot."));
393
+ for (const name of new Set(blockedCommands.filter((name) => !WORKER_COMMANDS.includes(name)))) await shim(name, fail(github.login ? `${name} is blocked in this installation; use gh, which acts as ${github.login}.` : `${name} is blocked in this installation.`));
394
+ for (const name of before) if (!kept.has(name)) await unlink(join(dir, name));
395
+ await own(workerGitConfig(stateDir), [
396
+ MARKER,
397
+ "[user]",
398
+ " useConfigOnly = true",
399
+ "[credential]",
400
+ " helper =",
401
+ ...github.cli ? [
402
+ "[credential \"https://github.com\"]",
403
+ `\thelper = ${gitQuote(credentialHelper(github.cli))}`,
404
+ "[url \"https://github.com/\"]",
405
+ " insteadOf = git@github.com:",
406
+ " insteadOf = ssh://git@github.com/",
407
+ ...sshTransport() ? ["[url \"ssh://git@github.com/\"]", " insteadOf = https://github.com/"] : []
408
+ ] : [],
409
+ "[filter \"lfs\"]",
410
+ " clean = git-lfs clean -- %f",
411
+ " smudge = git-lfs smudge -- %f",
412
+ " process = git-lfs filter-process",
413
+ " required = true"
414
+ ].join("\n") + "\n");
415
+ }
416
+ /** Applies environment overrides and prepends worker shims to `process.env.PATH`, returning a restore callback. */
417
+ function applyEnvironment(variables, stateDir, path) {
418
+ const values = {
419
+ ...variables,
420
+ PATH: [workerBin(stateDir), path].join(delimiter)
421
+ };
422
+ const previous = Object.fromEntries(Object.keys(values).map((name) => [name, process.env[name]]));
423
+ const apply = (env) => {
424
+ for (const [name, value] of Object.entries(env)) if (value === void 0) delete process.env[name];
425
+ else process.env[name] = value;
426
+ };
427
+ apply(values);
428
+ return () => {
429
+ apply(previous);
430
+ };
431
+ }
432
+ /** Spawns a command through the DSH subprocess runtime, which strips credential-shaped environment variables. */
433
+ async function runAsWorker(subprocess, argv, cwd, signal, env) {
434
+ const spawnArgv = process.platform === "win32" && /\.(cmd|bat)$/i.test(argv[0] ?? "") ? [
435
+ "cmd.exe",
436
+ "/d",
437
+ "/c",
438
+ argv[0],
439
+ ...argv.slice(1)
440
+ ] : argv;
441
+ const handle = subprocess.spawn({
442
+ argv: spawnArgv,
443
+ cwd,
444
+ graceMs: 2e3,
445
+ signal,
446
+ ...env ? { env } : {},
447
+ stdio: {
448
+ stdin: "ignore",
449
+ stdout: { maxBytes: 1 << 20 },
450
+ stderr: { maxBytes: 65536 }
451
+ }
452
+ });
453
+ const { exitCode } = await handle.done;
454
+ const read = (reader) => reader?.readFrom(0).text ?? "";
455
+ if (exitCode === 0) return read(handle.collected.stdout);
456
+ const ended = exitCode === null ? "was stopped before it finished" : `exited with ${exitCode}`;
457
+ throw new CliError(read(handle.collected.stderr).trim() || `${argv.join(" ")} ${ended}`, void 0, exitCode ?? void 0);
458
+ }
459
+ /** Verifies that worker git invocations resolve the deployment's GitHub credential helper. */
460
+ async function verifyEnvironment(deployment, subprocess, signal) {
461
+ const { github, stateDir } = deployment;
462
+ if (!github.cli) return;
463
+ if ((await runAsWorker(subprocess, [
464
+ "git",
465
+ "config",
466
+ "-z",
467
+ "--show-origin",
468
+ "--get-all",
469
+ "credential.https://github.com.helper"
470
+ ], stateDir, AbortSignal.any([signal, AbortSignal.timeout(6e4)])).catch(() => "")).split("\0").join("\0") !== [
471
+ `file:${workerGitConfig(stateDir)}`,
472
+ credentialHelper(github.cli),
473
+ ""
474
+ ].join("\0")) throw new SetupError("gitTooOld");
475
+ }
476
+ /**
477
+ * Verifies that a ticket directory's local or worktree git config defines no custom `credential.*` or `http.*extraheader`
478
+ * keys, which the agent's git would read from the repository's own config and act on.
479
+ */
480
+ async function verifyDirectory(directory, subprocess, signal) {
481
+ const scoped = (await runAsWorker(subprocess, [
482
+ "git",
483
+ "config",
484
+ "-z",
485
+ "--show-scope",
486
+ "--get-regexp",
487
+ "^(credential\\..*|http\\..*extraheader)$"
488
+ ], directory, AbortSignal.any([signal, AbortSignal.timeout(6e4)])).catch((error) => {
489
+ if (error instanceof CliError && error.exitCode === 1) return "";
490
+ throw error;
491
+ })).split("\0");
492
+ const local = scoped.flatMap((scope, index) => index % 2 === 0 && (scope === "local" || scope === "worktree") ? [scoped[index + 1].split("\n")[0]] : []);
493
+ if (local.length) throw new Error(`The git configuration of the repository in ${directory} sets ${local.join(", ")}, which the agent's git would also read; remove them.`);
494
+ }
495
+ //#endregion
496
+ //#region src/deployment.ts
497
+ /** Plugin configuration and startup verification for repository access and agent CLI identities. */
498
+ const reason = (error) => error instanceof Error ? error.message : String(error);
499
+ const absolute = (description) => s.string().pattern(/^(?:[A-Za-z]:[\\/]|\/)/).description(`${description} Absolute path.`);
500
+ /** Plugin configuration of the dsh-superagent entry. Volatile fields apply without a restart because dsh-superagent reads them on each use; changing `stateDir` remounts the plugin. */
501
+ const Config = s.object({
502
+ stateDir: absolute("Directory for the database, locks, worker shims, the agent's git configuration and, by default, the ticket directories.").required(),
503
+ configPath: s.string().description("Path to a declarative settings file (JSON or YAML) applied at startup; omitted blocks keep the stored settings."),
504
+ promptMaxChars: s.number().min(LIMITS.promptMaxChars.min).max(LIMITS.promptMaxChars.max).step(1).default(1e5).description("Largest saved prompt configuration, in characters.").volatile(),
505
+ admissionContextMaxPages: s.number().min(LIMITS.admissionContextMaxPages.min).max(LIMITS.admissionContextMaxPages.max).step(1).default(10).description("Pages of thread context one assessment may read.").volatile(),
506
+ admissionContextTimeoutMs: s.number().min(LIMITS.admissionContextTimeoutMs.min).max(LIMITS.admissionContextTimeoutMs.max).step(1).default(15e3).description("Time limit of one context read during an assessment, in milliseconds.").volatile(),
507
+ mode: s.union([s.const("github"), s.const("local")]).default("github").description("Where tickets come from: `github` (default) polls a repository; `local` runs with no GitHub Source at all, taking tickets from the inbox and delivering a commit on the ticket branch for a person to merge.")
508
+ });
509
+ const MOVED = {
510
+ repository: "on the GitHub Source",
511
+ branchPrefix: "in the prepare command of the Execution settings",
512
+ github: "on the GitHub Source",
513
+ feishu: "on the Feishu Source",
514
+ author: "prompt text",
515
+ repositoryPath: "in the prepare command of the Execution settings",
516
+ worktreeRoot: "the ticket directory of the Execution settings",
517
+ blockedCommands: "in the Execution settings",
518
+ watchdogMinutes: "in the Execution settings (control loop)",
519
+ watchdogStallMinutes: "in the Execution settings (control loop)",
520
+ watchdogGuidance: "in the Execution settings (control loop)",
521
+ routerShards: "in the Execution settings (Router shards)"
522
+ };
523
+ /** Read machine settings and reject unknown configuration keys (naming where deprecated keys moved). */
524
+ function machineOf(config) {
525
+ const unknown = Object.keys(config).filter((key) => !(key in (Config.dict ?? {})));
526
+ if (unknown.length) throw new Error(`dsh-superagent configuration has keys it does not use: ${unknown.map((key) => MOVED[key] ? `${key} (now ${MOVED[key]})` : key).join(", ")}; docs/006-deployment.md lists the keys`);
527
+ const { stateDir, configPath, promptMaxChars, admissionContextMaxPages, admissionContextTimeoutMs, mode } = config;
528
+ return {
529
+ stateDir,
530
+ mode,
531
+ configPath,
532
+ promptMaxChars,
533
+ admissionContextMaxPages,
534
+ admissionContextTimeoutMs
535
+ };
536
+ }
537
+ const GithubAccountsSchema = object({
538
+ repository: string().trim().regex(/^([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)?$/, "owner/name").default(""),
539
+ cli: string().trim().default("")
540
+ });
541
+ const FeishuBotSchema = object({ cli: string().trim().default("") });
542
+ /** Source configuration keys that affect deployment setup and trigger a restart when changed. */
543
+ const SETUP_FIELDS = {
544
+ github: Object.keys(GithubAccountsSchema.shape),
545
+ feishu: Object.keys(FeishuBotSchema.shape)
546
+ };
547
+ /**
548
+ * A deployment with no GitHub repository or account: the GitHub Source is not installed, so there is nothing to read
549
+ * notifications from, push to, or open a pull request on. Tickets come from the local inbox and the deliverable is a
550
+ * commit on the ticket branch, which the operator reviews and merges by hand. `noreply` still has to be set because
551
+ * workers commit with `user.useConfigOnly`, so a local placeholder stands in for the account's GitHub noreply address;
552
+ * it never reaches GitHub, since nothing is pushed.
553
+ */
554
+ const localDeployment = (machine) => ({
555
+ ...machine,
556
+ repository: "",
557
+ github: {
558
+ cli: "",
559
+ login: "",
560
+ noreply: "superagent@localhost"
561
+ }
562
+ });
563
+ /** Resolve a command name or absolute path to an executable file, ignoring the worker shim directory. */
564
+ function executable(name, path, exclude) {
565
+ if (name.includes("/") && !isAbsolute(name)) throw new Error(`${name} must be a command name or an absolute path`);
566
+ const candidates = isAbsolute(name) ? [name] : path.split(delimiter).filter((dir) => isAbsolute(dir) && !(exclude && sameDirectory(dir, exclude))).map((dir) => join(dir, name));
567
+ for (const candidate of candidates) try {
568
+ if (statSync(candidate).isFile()) {
569
+ accessSync(candidate, constants.X_OK);
570
+ return candidate;
571
+ }
572
+ } catch {}
573
+ throw new Error(`${name} is not an executable file${isAbsolute(name) ? "" : " on PATH"}`);
574
+ }
575
+ /** Run startup checks through DSH's worker subprocess service with worker shims removed from PATH so a `gh` wrapper script reaches the real binary. */
576
+ function workerProbe(subprocess, stateDir, cwd, signal) {
577
+ const path = withoutDirectory(process.env.PATH ?? "", workerBin(stateDir));
578
+ const timeout = () => AbortSignal.any([signal, AbortSignal.timeout(6e4)]);
579
+ if (process.platform !== "win32") return (argv) => runAsWorker(subprocess, [
580
+ "env",
581
+ `PATH=${path}`,
582
+ ...argv
583
+ ], cwd, timeout());
584
+ const environment = {
585
+ [Object.keys(process.env).find((name) => name.toUpperCase() === "PATH") ?? "PATH"]: path,
586
+ GH_CONFIG_DIR: agentGhConfig(stateDir)
587
+ };
588
+ return (argv) => runAsWorker(subprocess, argv, cwd, timeout(), environment);
589
+ }
590
+ const GithubUser = object({
591
+ login: string().regex(/^[A-Za-z0-9-]+$/),
592
+ id: number().int().positive()
593
+ });
594
+ const RepositoryAccess = object({
595
+ name: string().regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/),
596
+ push: boolean().nullable()
597
+ });
598
+ function parsed(schema, text) {
599
+ let value;
600
+ try {
601
+ value = JSON.parse(text);
602
+ } catch {
603
+ return;
604
+ }
605
+ const result = schema.safeParse(value);
606
+ return result.success ? result.data : void 0;
607
+ }
608
+ /** Verify the agent's GitHub CLI identity and push access to the configured repository. */
609
+ async function resolveDeployment(machine, github, probe, path = process.env.PATH ?? "") {
610
+ const { stateDir } = machine;
611
+ const accounts = GithubAccountsSchema.parse(github ?? {});
612
+ const remote = accounts.repository;
613
+ if (!remote) throw new SetupError("repositoryUnset");
614
+ if (!accounts.cli) throw new SetupError("agentCliUnset");
615
+ let cli;
616
+ try {
617
+ cli = executable(accounts.cli, path, workerBin(stateDir));
618
+ } catch (error) {
619
+ throw new SetupError("agentCliMissing", { output: reason(error) });
620
+ }
621
+ const text = await probe([
622
+ cli,
623
+ "api",
624
+ "user"
625
+ ]).catch((error) => {
626
+ throw new SetupError("agentCliSignedOut", { output: reason(error) });
627
+ });
628
+ const agent = parsed(GithubUser, text);
629
+ if (!agent) throw new SetupError("agentCliNoAccount");
630
+ const access = parsed(RepositoryAccess, await probe([
631
+ cli,
632
+ "api",
633
+ `repos/${remote}`,
634
+ "--jq",
635
+ "{name: .full_name, push: .permissions.push}"
636
+ ]).catch((error) => {
637
+ throw new SetupError("repositoryUnreadable", {
638
+ account: agent.login,
639
+ repository: remote,
640
+ output: reason(error)
641
+ });
642
+ }));
643
+ if (!access) throw new SetupError("repositoryUnreported", { repository: remote });
644
+ const repository = access.name;
645
+ if (access.push !== true) throw new SetupError("repositoryReadOnly", {
646
+ account: agent.login,
647
+ repository
648
+ });
649
+ return {
650
+ ...machine,
651
+ repository,
652
+ github: {
653
+ cli,
654
+ login: agent.login,
655
+ noreply: `${agent.id}+${agent.login}@users.noreply.github.com`
656
+ }
657
+ };
658
+ }
659
+ const AuthStatus = object({
660
+ appId: string().regex(/^cli_[A-Za-z0-9]+$/),
661
+ brand: _enum(["feishu", "lark"]).default("feishu"),
662
+ identities: object({ bot: object({ status: string() }) })
663
+ });
664
+ const BotInfo = object({ bot: object({
665
+ open_id: string().regex(/^ou_[A-Za-z0-9]+$/),
666
+ app_name: string().default("")
667
+ }) });
668
+ /**
669
+ * Verify that the configured Feishu CLI is authenticated as a bot and restricted to strict mode `bot`.
670
+ * Workers invoke this CLI as `lark-cli`, so a CLI that also permits a user identity would let workers act as that user.
671
+ */
672
+ async function resolveFeishuBot(machine, feishu, probe, path = process.env.PATH ?? "") {
673
+ const { cli: name } = FeishuBotSchema.parse(feishu ?? {});
674
+ if (!name) return void 0;
675
+ const fail = (error) => {
676
+ throw error instanceof SetupError ? error : new SetupError("feishuCliFailed", { output: reason(error) });
677
+ };
678
+ let cli = "";
679
+ try {
680
+ cli = executable(name, path, workerBin(machine.stateDir));
681
+ } catch (error) {
682
+ fail(error);
683
+ }
684
+ const run = (...args) => probe([
685
+ "env",
686
+ "LARKSUITE_CLI_NO_UPDATE_NOTIFIER=1",
687
+ "LARKSUITE_CLI_NO_SKILLS_NOTIFIER=1",
688
+ cli,
689
+ ...args
690
+ ]).catch(fail);
691
+ const text = await run("auth", "status");
692
+ const status = parsed(AuthStatus, text.slice(text.indexOf("{")));
693
+ if (!status) return fail(new SetupError("feishuNoApp"));
694
+ if (status.identities.bot.status !== "ready") fail(new SetupError("feishuBotNotReady", {
695
+ app: status.appId,
696
+ status: status.identities.bot.status
697
+ }));
698
+ const mode = /strict-mode:\s*(\w+)/.exec(await run("config", "strict-mode"))?.[1];
699
+ if (mode !== "bot") fail(new SetupError("feishuUserIdentity", { mode: mode ?? "unknown" }));
700
+ const info = parsed(BotInfo, (await run("api", "GET", "/open-apis/bot/v3/info", "--as", "bot", "--format", "ndjson")).split("\n")[0] ?? "");
701
+ if (!info) return fail(new SetupError("feishuNoBot"));
702
+ return {
703
+ cli,
704
+ appId: status.appId,
705
+ brand: status.brand,
706
+ botId: info.bot.open_id,
707
+ name: info.bot.app_name
708
+ };
709
+ }
710
+ //#endregion
711
+ //#region src/kv.ts
712
+ /** Define a single typed KV key; external Source plugins use `record` and `family` to type their own keys. */
713
+ const record = (key) => key;
714
+ /** Define a KV key family whose keys join `prefix` and name parts with `:` (or a custom `format`). */
715
+ function family(prefix, format = (...name) => name.join(":")) {
716
+ return Object.assign((...name) => `${prefix}${format(...name)}`, { prefix });
717
+ }
718
+ const KV = {
719
+ /** Store: current database schema version. */
720
+ store: { schema: record("schema") },
721
+ /** Settings: current revision, per-block revision, and saved settings snapshots (including the current one). */
722
+ settings: {
723
+ revision: record("settings-revision"),
724
+ block: family("settings-block-revision:"),
725
+ version: family("settings-version:")
726
+ },
727
+ /** Prompts: current revision and saved prompt versions. */
728
+ prompts: {
729
+ revision: record("prompt-revision"),
730
+ version: family("prompt-version:"),
731
+ /** The built-in text of a prompt part an override was edited from, by its `base`, which shows what an upgrade changed. */
732
+ builtIn: family("prompt-built-in:")
733
+ },
734
+ /** Runtime: the main Session's ID. */
735
+ runtime: { masterId: record("master-id") },
736
+ /** Sessions: prompt revisions each Session has worked under, ordered oldest first; the last entry (`.at(-1)`) is its active revision. */
737
+ sessions: { prompts: family("session-prompts:") },
738
+ /** Schema 11 migration: legacy main-Session queues imported into the effect table. */
739
+ queues: {
740
+ relays: record("relay-queue"),
741
+ unsubscribed: record("subscription-results"),
742
+ prompts: record("prompt-notification"),
743
+ settings: record("settings-notification")
744
+ },
745
+ /** Per-Source resolved display names mapping a named ID (such as a Feishu chat ID) to its display name. */
746
+ sources: { labels: family("source-labels:") },
747
+ /**
748
+ * GitHub Source: notification cursor and pending retries; per-Issue/PR read cursor shared by notification and polled reads;
749
+ * per-stream cursor, start times by kind, and ETag; default-branch CI state; and per-PR CI fingerprint and revision (`revision`
750
+ * counts reported CI changes so each produces a distinct Trigger ID).
751
+ */
752
+ github: {
753
+ since: record("github-since"),
754
+ retries: record("github-retry"),
755
+ cursor: family("github-cursor:"),
756
+ stream: family("github-stream:"),
757
+ branchCi: record("github-branch-ci"),
758
+ ci: family("github-ci:")
759
+ },
760
+ /**
761
+ * Feishu Source: known chats (`true` for direct messages); per-chat history cursor and active topics mapped from topic ID
762
+ * to last-seen timestamp; and per-polled-thread seen message IDs and initialization flag.
763
+ */
764
+ feishu: {
765
+ chats: record("feishu-chats"),
766
+ history: family("feishu-history:"),
767
+ topics: family("feishu-history-threads:"),
768
+ seen: family("thread-seen:"),
769
+ initialized: family("thread-initialized:")
770
+ }
771
+ };
772
+ //#endregion
773
+ //#region src/sources/poll.ts
774
+ /** Shared polling helpers for subscribed threads, overlap-safe cursors, and timer-friendly delays. */
775
+ /** How long a subscribed Thread with no live Ticket continues to be polled after its last Trigger or Ticket end. */
776
+ const POLL_HORIZON_MS = 12096e5;
777
+ /** True when a subscribed thread has a live ticket or recent activity/completion within `POLL_HORIZON_MS`. */
778
+ function polled(ctx, thread, now = Date.now()) {
779
+ const tickets = thread.subscriptions.map((id) => ctx.tickets.get(id));
780
+ if (tickets.some((ticket) => !!ticket && liveTicket(ticket))) return true;
781
+ const times = [thread.lastAt ?? thread.createdAt, ...tickets.map((ticket) => ticket?.ended?.at ?? "")].map(Date.parse).filter(Number.isFinite);
782
+ return now - Math.max(...times) < POLL_HORIZON_MS;
783
+ }
784
+ /** Steps a poll start timestamp back by 5 seconds so late-arriving remote events are not missed; deterministic trigger IDs deduplicate overlaps. */
785
+ const cursorAt = (started) => (/* @__PURE__ */ new Date(Date.parse(started) - 5e3)).toISOString();
786
+ /** Polls each subscribed Thread of `source` within the horizon independently; records failures on the Thread as `pollError` (unless `stopped()`) and clears `pollError` on success. */
787
+ async function pollThreads(ctx, source, stopped, poll, skip = () => false) {
788
+ for (const thread of ctx.threads.subscribed(source)) {
789
+ if (stopped()) return;
790
+ if (skip(thread) || !polled(ctx, thread)) continue;
791
+ try {
792
+ await poll(thread);
793
+ ctx.threads.patch(thread.key, { pollError: void 0 });
794
+ } catch (error) {
795
+ if (stopped()) return;
796
+ ctx.threads.patch(thread.key, { pollError: String(error) });
797
+ }
798
+ }
799
+ }
800
+ /** Waits `ms` or until `signal` aborts using global `setTimeout` so test fake timers can drive polling loops. */
801
+ function pause(ms, signal) {
802
+ return new Promise((resolve) => {
803
+ if (signal.aborted) {
804
+ resolve();
805
+ return;
806
+ }
807
+ const stop = () => {
808
+ clearTimeout(timer);
809
+ resolve();
810
+ };
811
+ const timer = setTimeout(() => {
812
+ signal.removeEventListener("abort", stop);
813
+ resolve();
814
+ }, ms);
815
+ signal.addEventListener("abort", stop, { once: true });
816
+ });
817
+ }
818
+ //#endregion
819
+ export { LIMITS as A, withoutDirectory as C, Commands as D, CliError as E, validItems as O, verifyEnvironment as S, writeEnvironment as T, agentVariables as _, polled as a, sameDirectory as b, FeishuBotSchema as c, localDeployment as d, machineOf as f, WORKER_COMMANDS as g, workerProbe as h, pollThreads as i, SetupError as k, GithubAccountsSchema as l, resolveFeishuBot as m, cursorAt as n, KV as o, resolveDeployment as p, pause as r, Config as s, POLL_HORIZON_MS as t, SETUP_FIELDS as u, applyEnvironment as v, workerBin as w, verifyDirectory as x, runAsWorker as y };