@zhangj1164/dsh-github-issue 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +20 -5
- package/README.zh.md +20 -5
- package/lib/index.d.ts +24 -6
- package/lib/index.js +118 -27
- package/package.json +1 -1
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/github-issue/dsh-github-issue/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 8bea5576f968f66e8eadb75bf5d6da52c2501df0
|
|
6
|
+
README.zh.md: d52ed1b3284b6c2d5e62eb3c5615c3396734b9ef
|
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ GitHub issue generation and optimization service for DeepSeek Harness. Builds st
|
|
|
28
28
|
| `autopilotPreset` | empty | Agent preset the dispatched Sessions run. Empty follows the deployment default. |
|
|
29
29
|
| `autopilotTimeZone` | empty | IANA zone "today" is measured in. Empty follows the host's own zone, so the user's local midnight starts a new day. |
|
|
30
30
|
| `autopilotPrompt` | the built-in template | Prompt handed to every dispatched Session. Placeholders `{number}`, `{title}`, `{url}`, `{body}`, and `{labels}` are replaced; an unknown placeholder stays visible. |
|
|
31
|
+
| `dispatchTaskDelaySeconds` | `1` | Seconds between registering a dispatch's automation task and its delivery, 1–3600. The Schedule service refuses a non-positive delay, so this cannot be `0`. |
|
|
31
32
|
|
|
32
33
|
**Settings-editable fields.** Eight of these fields — `repoUrl`, `claimLabel`, `maxIssuesPerRun`, `autopilotEnabled`, `autopilotCwd`, `autopilotPreset`, `autopilotTimeZone`, and `autopilotPrompt` — are declared `.volatile()`, which is what lets `@zhangj1164/dsh-client-ui-memo-settings` expose them in the Settings panel: `dsh-settings` projects only volatile fields, an accepted write lands in the profile's user layer, and the service reads each value where it is used instead of copying it at construction, so the change reaches the running process without a restart. `autopilotEnabled` is read by the `Service.init` startup check, so flipping it applies from the next host start. The remaining fields stay deployment-only.
|
|
33
34
|
|
|
@@ -40,7 +41,7 @@ GitHub issue generation and optimization service for DeepSeek Harness. Builds st
|
|
|
40
41
|
| `optimizeIssue(request)` | Rewrites a natural-language description into a structured issue following a pinned Markdown template. Returns `empty-input` for blank descriptions, `route-missing` when no route resolves, `llm-failure` when the model call failed. |
|
|
41
42
|
| `createIssue(request)` | Files the report through the GitHub REST API and returns the created issue's `{ number, url }`. The body is filed without its leading `## title` heading, because GitHub takes the title as its own field. |
|
|
42
43
|
| `listClaimableIssues(request)` | Lists the open issues carrying the claim label, excluding pull requests, which GitHub's issue endpoints also return. |
|
|
43
|
-
| `dispatchIssue(request)` | Creates one Session through the Session controller,
|
|
44
|
+
| `dispatchIssue(request)` | Creates one Session through the Session controller, names it, and registers the rendered prompt as a one-shot automation task on the Schedule service, which delivers it into that Session; returns its `sessionId`. A deployment that mounts no Schedule service admits the prompt directly instead. |
|
|
44
45
|
| `runIntake(request)` | Runs one pass: lists the claimable issues, skips those an earlier pass already dispatched, dispatches the rest, and records the local day durably. |
|
|
45
46
|
|
|
46
47
|
The intake methods report one stable failure code instead of a raw `fetch` error: `auth-missing`, `auth-failure`, `rate-limited`, `invalid-repo`, `invalid-request`, `http-failure`, `network-failure`, `session-unavailable`, or `dispatch-failed`. `auth-missing` is separate from `auth-failure` because no credential and a rejected credential are different deployment problems with different fixes.
|
|
@@ -79,13 +80,13 @@ Both `generateReport` and `optimizeIssue` use built-in system prompts that pin a
|
|
|
79
80
|
|
|
80
81
|
The loop has three steps, and this plugin owns the last two. The user describes a problem in natural language, `optimizeIssue` turns it into the pinned report, and `createIssue` files it. That replaces the point where the loop used to stop: a pre-filled URL still required the user to open the form and press Submit, so nothing could pick the issue up automatically.
|
|
81
82
|
|
|
82
|
-
`runIntake` then reads the issues that carry `claimLabel` and hands each one to a fresh Session
|
|
83
|
+
`runIntake` then reads the issues that carry `claimLabel` and hands each one to a fresh Session, registered as a one-shot automation task. The Session is created through the Session controller, so it gets the same preset and workspace policy as one the user opened in the Web. The work is handed over as a task on the Schedule service — the same board the Web's **Automation tasks** page lists — and that service delivers the rendered prompt into the Session when the task comes due. A task always belongs to an existing Session, so the Session is created first; a deployment that mounts no Schedule service (a non-Web composition) admits the prompt directly with the controller's `prompt` instead, which is the same durable path the Web prompt uses: it records the message with a user source and queues it on the Session's Agent, resolving (and resuming) that Agent itself. Naming the Session is best-effort: a deployment that mounts no title service still gets a working Session.
|
|
83
84
|
|
|
84
85
|
The credential is resolved once per process, in this order: `token`, then `tokenEnv`, then `tokenCommand`. The default command is `gh auth token`, so a machine already logged in to GitHub needs no extra secret; a deployment that manages secrets itself sets `token` and disables the command with an empty string. The token is never logged.
|
|
85
86
|
|
|
86
87
|
### First startup of the day
|
|
87
88
|
|
|
88
|
-
`autopilotEnabled` turns on one check per local day at host startup. The local day is read from durable storage-domain state, so a restart later the same day does nothing, and an issue that was already dispatched is never dispatched again — its Session exists, and a second one would duplicate the work. The day is committed only after the listing succeeded, so a pass that could not reach GitHub is retried on the next startup instead of being remembered as done.
|
|
89
|
+
`autopilotEnabled` turns on one check per local day at host startup. The local day is read from durable storage-domain state, so a restart later the same day does nothing, and an issue that was already dispatched is never dispatched again — its Session exists, and a second one would duplicate the work. The day is committed only after the listing succeeded, so a pass that could not reach GitHub is retried on the next startup instead of being remembered as done. A pass whose every dispatch failed is retried the same way: the day is committed only when an issue reached a Session or nothing needed dispatching, because remembering a broken dispatch as a finished day is what made a failing startup look like a day with nothing to do. The listing failure, the dispatch failures, and an intake state that cannot be opened are each written as a telemetry error event, so the reason a check did nothing is readable afterwards.
|
|
89
90
|
|
|
90
91
|
The check needs durable state and refuses to run without one: without a memory of what it dispatched, a restart would open a second Session for the same issue. It is also off by default, because opening a Session without being asked is a deployment decision rather than something a plugin install does on its own.
|
|
91
92
|
|
|
@@ -103,7 +104,21 @@ The check needs durable state and refuses to run without one: without a memory o
|
|
|
103
104
|
claimLabel: ready-for-agent
|
|
104
105
|
```
|
|
105
106
|
|
|
106
|
-
The host must be running for the check to happen at all; a day with no startup is a day with no check. That is the intended behavior — the check is a startup hook, not a scheduler — and the Automation tasks page
|
|
107
|
+
The host must be running for the check to happen at all; a day with no startup is a day with no check. That is the intended behavior — the check is a startup hook, not a scheduler — and the Automation tasks page now lists each dispatch as the one-shot task it registers, with the delivered ones showing as ended.
|
|
108
|
+
|
|
109
|
+
A dispatched Session runs the repository workflow itself, and that workflow starts with `git fetch`. Under the default `workspace-write` sandbox a network command is blocked and raises an approval prompt that an unattended dispatch cannot answer, so a deployment that wants the loop to run on its own should pin the permission preset to `danger-full-access` in its own patch layer:
|
|
110
|
+
|
|
111
|
+
```yaml
|
|
112
|
+
- id: permission
|
|
113
|
+
config:
|
|
114
|
+
presets:
|
|
115
|
+
read-only: { sandbox: read-only, approval: ask }
|
|
116
|
+
workspace-write: { sandbox: workspace-write, approval: ask }
|
|
117
|
+
danger-full-access: { sandbox: danger-full-access, approval: never }
|
|
118
|
+
defaultPreset: danger-full-access
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
That is a deployment-wide choice: it applies to every new Session the host creates, not only to dispatched ones.
|
|
107
122
|
|
|
108
123
|
## Requirement mapping
|
|
109
124
|
|
|
@@ -166,5 +181,5 @@ Every subpath resolves to the flat `lib/*.js` layout tsdown emits.
|
|
|
166
181
|
|
|
167
182
|
- **Model dependency** — `inject: ['llm']` means the service will not activate without an LLM provider.
|
|
168
183
|
- **No browser UI of its own** — this package registers no model tool and ships no browser UI. Installing it on its own gives a deployment `ctx.get('githubIssue')` and nothing a user can see or click; the visible entry point is `@zhangj1164/dsh-client-ui-memo`'s issue editor, whose "File on GitHub" button and log-analysis action reach it over Remote. Treat it as a backend that a UI plugin consumes, not as a standalone feature.
|
|
169
|
-
- **Dispatch needs the Web composition** — `dispatchIssue` and `runIntake` read `ctx.get('sessionController')` and `ctx.get('
|
|
184
|
+
- **Dispatch needs the Web composition** — `dispatchIssue` and `runIntake` read `ctx.get('sessionController')` and, when it exists, `ctx.get('schedule')`. A headless or SDK-only composition that mounts no Session controller reports `session-unavailable` and files or lists issues only; one that mounts a controller but no Schedule service still dispatches, admitting the prompt directly.
|
|
170
185
|
- **The first-startup check is a startup hook, not a scheduler** — it runs only when the host starts. A deployment that keeps the host running across midnight gets the check on the next start, not at midnight.
|
package/README.zh.md
CHANGED
|
@@ -28,6 +28,7 @@ DSH 的 GitHub issue 生成与优化服务。从遥测分析构建结构化 issu
|
|
|
28
28
|
| `autopilotPreset` | 空 | 派发会话使用的 agent preset。空值跟随部署默认。 |
|
|
29
29
|
| `autopilotTimeZone` | 空 | 判定「今天」所用的 IANA 时区。空值跟随宿主自身时区,因此用户的本地午夜才是新一天的开始。 |
|
|
30
30
|
| `autopilotPrompt` | 内置模板 | 交给每个派发会话的提示词模板。占位符 `{number}`、`{title}`、`{url}`、`{body}`、`{labels}` 会被替换;未知占位符保留原样可见。 |
|
|
31
|
+
| `dispatchTaskDelaySeconds` | `1` | 派发时登记的自动化任务从登记到送达的秒数,1–3600。Schedule 服务拒绝非正数延迟,所以不能是 `0`。 |
|
|
31
32
|
|
|
32
33
|
**可在设置页编辑的字段。** 其中八个——`repoUrl`、`claimLabel`、`maxIssuesPerRun`、`autopilotEnabled`、`autopilotCwd`、`autopilotPreset`、`autopilotTimeZone`、`autopilotPrompt`——声明了 `.volatile()`,这正是 `@zhangj1164/dsh-client-ui-memo-settings` 能把它们放进设置面板的原因:`dsh-settings` 只投影易变字段,被接受的写入落进 profile 的用户层,而服务在用到的位置读取每个值、不在构造时拷贝,因此改动无需重启就能到达运行中的进程。`autopilotEnabled` 由 `Service.init` 的启动检查读取,拨动它从下一次宿主启动生效。其余字段仍只属于部署层。
|
|
33
34
|
|
|
@@ -40,7 +41,7 @@ DSH 的 GitHub issue 生成与优化服务。从遥测分析构建结构化 issu
|
|
|
40
41
|
| `optimizeIssue(request)` | 将自然语言描述重写为按固定 Markdown 模板组织的 issue。空描述返回 `empty-input`,无路由可解析时返回 `route-missing`,模型调用失败返回 `llm-failure`。 |
|
|
41
42
|
| `createIssue(request)` | 经 GitHub REST API 直接建单,返回新 issue 的 `{ number, url }`。正文写入时会去掉首行的 `## 标题`,因为 GitHub 把标题作为独立字段。 |
|
|
42
43
|
| `listClaimableIssues(request)` | 列出带接单标签的 open issue,并排除 PR——GitHub 的 issue 端点也会返回 PR。 |
|
|
43
|
-
| `dispatchIssue(request)` | 经 Session
|
|
44
|
+
| `dispatchIssue(request)` | 经 Session 控制器新建一个会话并命名,把渲染后的提示词登记为该会话上的一次性自动化任务,由 Schedule 服务送达;返回 `sessionId`。没有挂载 Schedule 服务的部署改为直接排入提示词。 |
|
|
44
45
|
| `runIntake(request)` | 执行一次接单:列出可接单 issue,跳过此前已派发的,派发其余,并把本地日期持久化。 |
|
|
45
46
|
|
|
46
47
|
接单方法只报一个稳定的失败码,而不是原始的 `fetch` 错误:`auth-missing`、`auth-failure`、`rate-limited`、`invalid-repo`、`invalid-request`、`http-failure`、`network-failure`、`session-unavailable`、`dispatch-failed`。`auth-missing` 与 `auth-failure` 分开,是因为「没有凭据」和「凭据被拒绝」是两种部署问题,修法不同。
|
|
@@ -79,13 +80,13 @@ DSH 的 GitHub issue 生成与优化服务。从遥测分析构建结构化 issu
|
|
|
79
80
|
|
|
80
81
|
闭环有三步,本插件负责后两步。用户用自然语言描述问题,`optimizeIssue` 把它变成固定模板的报告,`createIssue` 直接建单。这一步替换掉闭环原本停住的地方:预填 URL 仍然要用户打开表单并按下提交,任何东西都无法自动接住这个 issue。
|
|
81
82
|
|
|
82
|
-
`runIntake` 随后读取带 `claimLabel` 的 issue
|
|
83
|
+
`runIntake` 随后读取带 `claimLabel` 的 issue,并把每个交给一个新会话,同时登记为一次性自动化任务。会话经 Session 控制器创建,因此与用户在 Web 里新建的会话拿到相同的 preset 与工作区策略;工作本身以任务的形式交给 Schedule 服务——也就是 Web「自动化任务」页面列出的那块面板——由它在任务到点时把渲染好的提示词送进该会话。任务永远绑定一个已存在的会话,所以会话先建;没有挂载 Schedule 服务的部署(非 Web 组合)改为用控制器的 `prompt` 直接排入提示词,那正是 Web 端发提示词走的同一条持久路径:把消息以用户来源记录下来,排进会话 Agent 的收件箱,由控制器自己解析(必要时先恢复)那个 Agent。命名是尽力而为:没有挂载标题服务的部署依然得到一个可用的会话。
|
|
83
84
|
|
|
84
85
|
凭据每个进程只解析一次,顺序是 `token`、`tokenEnv`、`tokenCommand`。默认命令是 `gh auth token`,因此已经登录 GitHub 的机器不需要额外密钥;自行管理密钥的部署设置 `token` 并用空字符串停用命令。token 永远不会被记录到日志。
|
|
85
86
|
|
|
86
87
|
### 每天首次启动
|
|
87
88
|
|
|
88
|
-
`autopilotEnabled` 打开后,宿主启动时每个本地日期执行一次检查。本地日期从 storage-domain 的持久状态读取,因此同一天稍后重启什么也不做,已派发的 issue 永不再次派发——它的会话已经存在,再来一个只会重复劳动。只有列取成功后才提交日期,所以连不上 GitHub
|
|
89
|
+
`autopilotEnabled` 打开后,宿主启动时每个本地日期执行一次检查。本地日期从 storage-domain 的持久状态读取,因此同一天稍后重启什么也不做,已派发的 issue 永不再次派发——它的会话已经存在,再来一个只会重复劳动。只有列取成功后才提交日期,所以连不上 GitHub 的那次会在下次启动重试,而不会被记成已完成。每次派发都失败的那一次同样会重试:只有在有 issue 进入会话、或本来就没有需要派发的东西时才提交日期——把一次坏掉的派发记成「今天查过了」,正是让失败的启动看起来像「当天无事可做」的原因。列取失败、派发失败,以及打不开接单状态,都会写一条 telemetry 错误事件,事后能读到这次检查为什么什么都没做。
|
|
89
90
|
|
|
90
91
|
检查需要持久状态,没有就拒绝运行:没有「派发过什么」的记忆,重启会为同一个 issue 打开第二个会话。它也默认关闭,因为未经请求就打开会话是部署决定,而不是插件安装时自带的行为。
|
|
91
92
|
|
|
@@ -103,7 +104,21 @@ DSH 的 GitHub issue 生成与优化服务。从遥测分析构建结构化 issu
|
|
|
103
104
|
claimLabel: ready-for-agent
|
|
104
105
|
```
|
|
105
106
|
|
|
106
|
-
|
|
107
|
+
宿主必须在运行,检查才可能发生;没有启动的一天就没有检查。这是有意的行为——检查是启动钩子,不是调度器——而「自动化任务」页面现在会列出每次派发登记的那条一次性任务,已送达的显示为已结束。
|
|
108
|
+
|
|
109
|
+
派发出来的会话要自己跑仓库流程,第一步就是 `git fetch`。默认的 `workspace-write` 沙箱会拦住联网命令并弹出审批,而无人值守的派发无法回答它;因此想让闭环自己跑起来的部署,应在自己的 patch 层把权限预设固定为 `danger-full-access`:
|
|
110
|
+
|
|
111
|
+
```yaml
|
|
112
|
+
- id: permission
|
|
113
|
+
config:
|
|
114
|
+
presets:
|
|
115
|
+
read-only: { sandbox: read-only, approval: ask }
|
|
116
|
+
workspace-write: { sandbox: workspace-write, approval: ask }
|
|
117
|
+
danger-full-access: { sandbox: danger-full-access, approval: never }
|
|
118
|
+
defaultPreset: danger-full-access
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
这是部署级取舍:它对宿主新建的每个会话生效,而不只是派发的会话。
|
|
107
122
|
|
|
108
123
|
## 需求映射
|
|
109
124
|
|
|
@@ -166,5 +181,5 @@ dsh plugin --profile web add @zhangj1164/dsh-telemetry @zhangj1164/dsh-github-is
|
|
|
166
181
|
|
|
167
182
|
- **模型依赖** — `inject: ['llm']` 意味着服务在没有 LLM 提供方时不会激活。
|
|
168
183
|
- **自身没有浏览器界面** — 本包不注册模型工具,也不附带浏览器 UI。单独安装只会给部署带来 `ctx.get('githubIssue')`,用户看不到也点不到任何东西;可见的入口是 `@zhangj1164/dsh-client-ui-memo` 的 issue 编辑器(其「直接提交到 GitHub」按钮)与日志分析动作,它们经 Remote 调用本服务。请把它当作供 UI 插件消费的后端,而不是一个能独立使用的功能。
|
|
169
|
-
- **派发需要 Web 组合** — `dispatchIssue` 与 `runIntake` 读取 `ctx.get('sessionController')
|
|
184
|
+
- **派发需要 Web 组合** — `dispatchIssue` 与 `runIntake` 读取 `ctx.get('sessionController')`,存在时再读 `ctx.get('schedule')`。没有 Session 控制器的 headless 或纯 SDK 组合会返回 `session-unavailable`,只能建单和列取;有控制器但没有 Schedule 服务的组合仍能派发,只是改为直接排入提示词。
|
|
170
185
|
- **首次启动检查是启动钩子,不是调度器** — 它只在宿主启动时运行。跨过午夜仍保持运行的部署会在下一次启动时检查,而不是在午夜检查。
|
package/lib/index.d.ts
CHANGED
|
@@ -142,6 +142,16 @@ interface Config {
|
|
|
142
142
|
* location and points at the repository workflow.
|
|
143
143
|
*/
|
|
144
144
|
readonly autopilotPrompt: Volatile<string>;
|
|
145
|
+
/**
|
|
146
|
+
* Seconds between registering a dispatch's automation task and its delivery.
|
|
147
|
+
*
|
|
148
|
+
* The Schedule service refuses a non-positive delay, so this is at least one
|
|
149
|
+
* second. It stays a deployment-level value instead of a constant because a
|
|
150
|
+
* deployment may want the task to sit visibly on the board before it fires,
|
|
151
|
+
* and it stays out of the settings surface: it times a dispatch, it does not
|
|
152
|
+
* describe one.
|
|
153
|
+
*/
|
|
154
|
+
readonly dispatchTaskDelaySeconds: number;
|
|
145
155
|
}
|
|
146
156
|
/** Schemastery configuration for the GitHub-issue service. */
|
|
147
157
|
declare const Config: s<Config>;
|
|
@@ -287,13 +297,21 @@ declare class GithubIssueService extends TypertRemoteService {
|
|
|
287
297
|
*/
|
|
288
298
|
runIntake(request?: GithubIssueIntakeRequest): Promise<GithubIssueIntakeResult>;
|
|
289
299
|
/**
|
|
290
|
-
* Create one Session,
|
|
300
|
+
* Create one Session, register its automation task, and name the Session.
|
|
301
|
+
*
|
|
302
|
+
* A dispatch is two steps. The Session is created through the controller, so
|
|
303
|
+
* the deployment's preset and workspace policy apply. The work is then handed
|
|
304
|
+
* over as a one-shot automation task on the Schedule service: the task shows
|
|
305
|
+
* up in the Web's automation board, and the service delivers its prompt into
|
|
306
|
+
* that Session when the task becomes due. A task is always bound to an
|
|
307
|
+
* existing Session — the board never opens one by itself — so the Session has
|
|
308
|
+
* to exist first, and registering the task is what makes the dispatch visible
|
|
309
|
+
* beyond the sidebar.
|
|
291
310
|
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
* wakes the driver without waiting for a turn to be open elsewhere.
|
|
311
|
+
* A deployment that mounts no Schedule service (a non-Web composition) falls
|
|
312
|
+
* back to admitting the prompt directly, the same durable admission the Web
|
|
313
|
+
* prompt uses. Both paths fail the dispatch when the host refuses, so a
|
|
314
|
+
* refusal never leaves the day marked as checked.
|
|
297
315
|
*
|
|
298
316
|
* @param request - the issue and optional Session placement.
|
|
299
317
|
* @returns the created Session, or a stable failure.
|
package/lib/index.js
CHANGED
|
@@ -598,7 +598,8 @@ const Config = s.object({
|
|
|
598
598
|
autopilotCwd: s.string().default("").volatile(),
|
|
599
599
|
autopilotPreset: s.string().default("").volatile(),
|
|
600
600
|
autopilotTimeZone: s.string().default("").volatile(),
|
|
601
|
-
autopilotPrompt: s.string().default(DEFAULT_DISPATCH_PROMPT).volatile()
|
|
601
|
+
autopilotPrompt: s.string().default(DEFAULT_DISPATCH_PROMPT).volatile(),
|
|
602
|
+
dispatchTaskDelaySeconds: s.number().step(1).min(1).max(3600).default(1)
|
|
602
603
|
});
|
|
603
604
|
/** The built-in system prompt that structures a natural-language issue description. */
|
|
604
605
|
const OPTIMIZE_SYSTEM_PROMPT = [
|
|
@@ -711,9 +712,31 @@ const SESSION_UNAVAILABLE = {
|
|
|
711
712
|
ok: false,
|
|
712
713
|
error: {
|
|
713
714
|
code: "session-unavailable",
|
|
714
|
-
message: "this deployment mounts no Session controller
|
|
715
|
+
message: "this deployment mounts no Session controller with prompt admission, so no issue can be dispatched"
|
|
715
716
|
}
|
|
716
717
|
};
|
|
718
|
+
/**
|
|
719
|
+
* The longest title the Session and Schedule services accept.
|
|
720
|
+
*
|
|
721
|
+
* Both truncate or reject beyond this length, and the Schedule service rejects
|
|
722
|
+
* with `invalid_prompt`, which would fail a dispatch that otherwise succeeded.
|
|
723
|
+
* The number is that service's protocol limit, not a deployment knob, so it
|
|
724
|
+
* lives here as a constant rather than in `Config`.
|
|
725
|
+
*/
|
|
726
|
+
const MAX_TITLE_LENGTH = 120;
|
|
727
|
+
/**
|
|
728
|
+
* Build the title a dispatch gives its Session and its automation task.
|
|
729
|
+
*
|
|
730
|
+
* The issue title is cut by code point rather than by UTF-16 unit: slicing a
|
|
731
|
+
* string at a surrogate boundary would keep the length the service accepts but
|
|
732
|
+
* leave a reader with half a character.
|
|
733
|
+
* @param issue - the issue being dispatched.
|
|
734
|
+
* @returns a title naming the issue, at most {@link MAX_TITLE_LENGTH} code points.
|
|
735
|
+
*/
|
|
736
|
+
function dispatchTitle(issue) {
|
|
737
|
+
const title = "Issue #" + issue.number + ": " + issue.title;
|
|
738
|
+
return title.length <= MAX_TITLE_LENGTH ? title : [...title].slice(0, MAX_TITLE_LENGTH).join("");
|
|
739
|
+
}
|
|
717
740
|
/** Promise-based `exec`, used only for the configured token command. */
|
|
718
741
|
const execAsync = promisify(exec);
|
|
719
742
|
/**
|
|
@@ -901,6 +924,11 @@ let GithubIssueService = (() => {
|
|
|
901
924
|
await this.openIntakeState();
|
|
902
925
|
if (!this.config.autopilotEnabled.get()) return;
|
|
903
926
|
if (!this.intakePersistent) {
|
|
927
|
+
this.trackError("startupIntake", {
|
|
928
|
+
code: "state-unavailable",
|
|
929
|
+
message: "autopilot is enabled but no durable intake state is available",
|
|
930
|
+
featureCodeRef: this.featureCode("startupIntake")
|
|
931
|
+
});
|
|
904
932
|
this.warn("autopilot is enabled but no durable intake state is available; skipping the startup check so it cannot dispatch the same issue twice");
|
|
905
933
|
return;
|
|
906
934
|
}
|
|
@@ -1118,7 +1146,16 @@ let GithubIssueService = (() => {
|
|
|
1118
1146
|
});
|
|
1119
1147
|
}
|
|
1120
1148
|
const checkedAt = Date.now();
|
|
1121
|
-
|
|
1149
|
+
const day = localDayKey(checkedAt, this.intakeTimeZone());
|
|
1150
|
+
if (dispatched.length === 0 && failures.length > 0) this.trackError("runIntake", {
|
|
1151
|
+
code: "dispatch-failed",
|
|
1152
|
+
message: "no issue was dispatched and " + failures.length + " failed: " + failures.map((entry) => "#" + entry.number + " " + entry.code).join(", "),
|
|
1153
|
+
featureCodeRef: this.featureCode("runIntake")
|
|
1154
|
+
}, {
|
|
1155
|
+
claimable: listed.value.length,
|
|
1156
|
+
failed: failures.map((entry) => entry.number)
|
|
1157
|
+
});
|
|
1158
|
+
else await this.commitIntakeState(day, dispatched);
|
|
1122
1159
|
return {
|
|
1123
1160
|
ok: true,
|
|
1124
1161
|
value: Object.freeze({
|
|
@@ -1132,21 +1169,28 @@ let GithubIssueService = (() => {
|
|
|
1132
1169
|
};
|
|
1133
1170
|
}
|
|
1134
1171
|
/**
|
|
1135
|
-
* Create one Session,
|
|
1172
|
+
* Create one Session, register its automation task, and name the Session.
|
|
1136
1173
|
*
|
|
1137
|
-
* The Session is created through the
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1141
|
-
*
|
|
1174
|
+
* A dispatch is two steps. The Session is created through the controller, so
|
|
1175
|
+
* the deployment's preset and workspace policy apply. The work is then handed
|
|
1176
|
+
* over as a one-shot automation task on the Schedule service: the task shows
|
|
1177
|
+
* up in the Web's automation board, and the service delivers its prompt into
|
|
1178
|
+
* that Session when the task becomes due. A task is always bound to an
|
|
1179
|
+
* existing Session — the board never opens one by itself — so the Session has
|
|
1180
|
+
* to exist first, and registering the task is what makes the dispatch visible
|
|
1181
|
+
* beyond the sidebar.
|
|
1182
|
+
*
|
|
1183
|
+
* A deployment that mounts no Schedule service (a non-Web composition) falls
|
|
1184
|
+
* back to admitting the prompt directly, the same durable admission the Web
|
|
1185
|
+
* prompt uses. Both paths fail the dispatch when the host refuses, so a
|
|
1186
|
+
* refusal never leaves the day marked as checked.
|
|
1142
1187
|
*
|
|
1143
1188
|
* @param request - the issue and optional Session placement.
|
|
1144
1189
|
* @returns the created Session, or a stable failure.
|
|
1145
1190
|
*/
|
|
1146
1191
|
async dispatch(request) {
|
|
1147
1192
|
const controller = optionalService(this.ctx, "sessionController");
|
|
1148
|
-
|
|
1149
|
-
if (controller === void 0 || agents === void 0) return SESSION_UNAVAILABLE;
|
|
1193
|
+
if (controller === void 0 || typeof controller.prompt !== "function") return SESSION_UNAVAILABLE;
|
|
1150
1194
|
const cwd = firstNonBlank(request.cwd, this.config.autopilotCwd.get());
|
|
1151
1195
|
const agentPreset = firstNonBlank(request.agentPreset, this.config.autopilotPreset.get());
|
|
1152
1196
|
let sessionId;
|
|
@@ -1164,22 +1208,46 @@ let GithubIssueService = (() => {
|
|
|
1164
1208
|
}
|
|
1165
1209
|
};
|
|
1166
1210
|
}
|
|
1167
|
-
const
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1211
|
+
const prompt = renderDispatchPrompt(this.config.autopilotPrompt.get(), request.issue);
|
|
1212
|
+
const schedule = optionalService(this.ctx, "schedule");
|
|
1213
|
+
if (schedule !== void 0 && typeof schedule.create === "function") {
|
|
1214
|
+
let taskId;
|
|
1215
|
+
try {
|
|
1216
|
+
taskId = (await schedule.create(sessionId, {
|
|
1217
|
+
prompt,
|
|
1218
|
+
title: dispatchTitle(request.issue),
|
|
1219
|
+
after_seconds: this.config.dispatchTaskDelaySeconds
|
|
1220
|
+
})).id;
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
return {
|
|
1223
|
+
ok: false,
|
|
1224
|
+
error: {
|
|
1225
|
+
code: "dispatch-failed",
|
|
1226
|
+
message: "Session \"" + sessionId + "\" was created but its automation task was refused: " + describeError(error)
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1173
1229
|
}
|
|
1174
|
-
|
|
1230
|
+
this.renameSession(controller, sessionId, dispatchTitle(request.issue));
|
|
1231
|
+
this.track("dispatchIssue", "success", {
|
|
1232
|
+
issue: request.issue.number,
|
|
1233
|
+
sessionId,
|
|
1234
|
+
taskId
|
|
1235
|
+
});
|
|
1236
|
+
return {
|
|
1237
|
+
ok: true,
|
|
1238
|
+
value: Object.freeze({ sessionId })
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1175
1241
|
try {
|
|
1176
|
-
|
|
1242
|
+
await controller.prompt({
|
|
1243
|
+
requestId: "github-issue:" + request.issue.number + ":" + sessionId,
|
|
1244
|
+
sessionId,
|
|
1245
|
+
mode: "queue",
|
|
1177
1246
|
content: [{
|
|
1178
1247
|
type: "text",
|
|
1179
|
-
text:
|
|
1180
|
-
}]
|
|
1181
|
-
|
|
1182
|
-
});
|
|
1248
|
+
text: prompt
|
|
1249
|
+
}]
|
|
1250
|
+
}, new AbortController().signal);
|
|
1183
1251
|
} catch (error) {
|
|
1184
1252
|
return {
|
|
1185
1253
|
ok: false,
|
|
@@ -1189,7 +1257,7 @@ let GithubIssueService = (() => {
|
|
|
1189
1257
|
}
|
|
1190
1258
|
};
|
|
1191
1259
|
}
|
|
1192
|
-
this.renameSession(controller, sessionId,
|
|
1260
|
+
this.renameSession(controller, sessionId, dispatchTitle(request.issue));
|
|
1193
1261
|
this.track("dispatchIssue", "success", {
|
|
1194
1262
|
issue: request.issue.number,
|
|
1195
1263
|
sessionId
|
|
@@ -1232,9 +1300,27 @@ let GithubIssueService = (() => {
|
|
|
1232
1300
|
if (this.intakeState.lastCheckedDay === today) return;
|
|
1233
1301
|
const result = await this.runIntake({});
|
|
1234
1302
|
if (!result.ok) {
|
|
1303
|
+
this.trackError("startupIntake", {
|
|
1304
|
+
code: result.error.code,
|
|
1305
|
+
message: result.error.message,
|
|
1306
|
+
featureCodeRef: this.featureCode("startupIntake")
|
|
1307
|
+
});
|
|
1235
1308
|
this.warn("issue intake did not complete: " + result.error.code + ": " + result.error.message);
|
|
1236
1309
|
return;
|
|
1237
1310
|
}
|
|
1311
|
+
if (result.value.failures.length > 0) {
|
|
1312
|
+
const detail = result.value.failures.map((entry) => "#" + entry.number + " " + entry.code).join(", ");
|
|
1313
|
+
this.trackError("startupIntake", {
|
|
1314
|
+
code: "dispatch-failed",
|
|
1315
|
+
message: "dispatched " + result.value.dispatched.length + " issue(s); failed: " + detail,
|
|
1316
|
+
featureCodeRef: this.featureCode("startupIntake")
|
|
1317
|
+
}, {
|
|
1318
|
+
dispatched: result.value.dispatched.length,
|
|
1319
|
+
failed: result.value.failures.map((entry) => entry.number)
|
|
1320
|
+
});
|
|
1321
|
+
this.warn("issue intake dispatched " + result.value.dispatched.length + " issue(s) and failed " + result.value.failures.length + ": " + detail);
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1238
1324
|
this.track("startupIntake", "success", { dispatched: result.value.dispatched.length });
|
|
1239
1325
|
} catch (error) {
|
|
1240
1326
|
this.warn("issue intake failed: " + describeError(error));
|
|
@@ -1260,6 +1346,11 @@ let GithubIssueService = (() => {
|
|
|
1260
1346
|
this.intakeState = table.get("intake") ?? EMPTY_INTAKE_STATE;
|
|
1261
1347
|
this.intakePersistent = true;
|
|
1262
1348
|
} catch (error) {
|
|
1349
|
+
this.trackError("openIntakeState", {
|
|
1350
|
+
code: "state-unavailable",
|
|
1351
|
+
message: describeError(error),
|
|
1352
|
+
featureCodeRef: this.featureCode("openIntakeState")
|
|
1353
|
+
});
|
|
1263
1354
|
this.warn("intake state is unavailable: " + describeError(error));
|
|
1264
1355
|
}
|
|
1265
1356
|
}
|
|
@@ -1546,9 +1637,9 @@ function buildPrefillUrl(repoUrl, report, maxLength, note) {
|
|
|
1546
1637
|
* Read one optional host service by its Cordis key.
|
|
1547
1638
|
*
|
|
1548
1639
|
* The intake loop works with services this package does not depend on: a
|
|
1549
|
-
* deployment may mount no storage
|
|
1550
|
-
*
|
|
1551
|
-
*
|
|
1640
|
+
* deployment may mount no storage or no Session controller. Reading them by key
|
|
1641
|
+
* keeps those features optional without turning the plugin into a hard
|
|
1642
|
+
* dependency of the whole Web composition.
|
|
1552
1643
|
*
|
|
1553
1644
|
* @param ctx - the host context.
|
|
1554
1645
|
* @param name - the service key.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhangj1164/dsh-github-issue",
|
|
3
3
|
"description": "GitHub issue generation, filing, and intake service for DeepSeek Harness: structured reports from telemetry analysis, AI-optimized descriptions, direct issue creation, and first-startup dispatch of claimable issues to new Sessions",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|