berry-agent 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/.api-emit.stamp +1 -0
- package/dist/.build-meta.json +6 -0
- package/dist/agent/events.js +1 -0
- package/dist/agent/index.js +2 -0
- package/dist/agent/loop.js +139 -0
- package/dist/agent/queue.js +119 -0
- package/dist/agent/stream.js +62 -0
- package/dist/agent/tools-batch.js +221 -0
- package/dist/agent/types.js +8 -0
- package/dist/api/berry-agent-llm.d.ts +18 -0
- package/dist/api/berry-agent.d.ts +9 -0
- package/dist/api/surface.json +1632 -0
- package/dist/api/tsconfig.paths.json +12 -0
- package/dist/api/typebox-compile.d.ts +2 -0
- package/dist/api/typebox-value.d.ts +2 -0
- package/dist/api/typebox.d.ts +2 -0
- package/dist/browser/cdp.js +256 -0
- package/dist/browser/codes.js +31 -0
- package/dist/browser/discover.js +107 -0
- package/dist/browser/engine.js +169 -0
- package/dist/browser/index.js +17 -0
- package/dist/browser/install.js +342 -0
- package/dist/browser/page.js +321 -0
- package/dist/browser/service.js +158 -0
- package/dist/browser/tools.js +198 -0
- package/dist/browser/types.js +81 -0
- package/dist/channels/ask-queue.js +82 -0
- package/dist/channels/commands.js +129 -0
- package/dist/channels/engine/cell.js +143 -0
- package/dist/channels/engine/diff.js +143 -0
- package/dist/channels/engine/engine.js +416 -0
- package/dist/channels/engine/index.js +8 -0
- package/dist/channels/engine/input-keys.js +86 -0
- package/dist/channels/engine/input.js +596 -0
- package/dist/channels/engine/memory-io.js +71 -0
- package/dist/channels/engine/process-io.js +38 -0
- package/dist/channels/engine/types.js +17 -0
- package/dist/channels/engine/width.js +122 -0
- package/dist/channels/index.js +15 -0
- package/dist/channels/registry.js +56 -0
- package/dist/channels/sdk/admit.js +29 -0
- package/dist/channels/sdk/backend.js +102 -0
- package/dist/channels/sdk/cursor.js +56 -0
- package/dist/channels/sdk/index.js +20 -0
- package/dist/channels/sdk/jsonl.js +208 -0
- package/dist/channels/sdk/protocol.js +24 -0
- package/dist/channels/sdk/schema.js +91 -0
- package/dist/channels/sdk/wire-core.js +446 -0
- package/dist/channels/service.js +169 -0
- package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
- package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
- package/dist/channels/tui/autocomplete/popup.js +148 -0
- package/dist/channels/tui/autocomplete/provider.js +7 -0
- package/dist/channels/tui/autocomplete/token.js +68 -0
- package/dist/channels/tui/backend/ansi-rows.js +211 -0
- package/dist/channels/tui/backend/main-screen.js +207 -0
- package/dist/channels/tui/backend/osc.js +91 -0
- package/dist/channels/tui/backend/transcript.js +230 -0
- package/dist/channels/tui/backend/tui-backend.js +915 -0
- package/dist/channels/tui/editor/editor-model.js +543 -0
- package/dist/channels/tui/editor/editor-view.js +140 -0
- package/dist/channels/tui/editor/editor.js +275 -0
- package/dist/channels/tui/editor/undo-stack.js +35 -0
- package/dist/channels/tui/editor/visual-lines.js +126 -0
- package/dist/channels/tui/editor/word-nav.js +106 -0
- package/dist/channels/tui/history/history-viewer.js +403 -0
- package/dist/channels/tui/index.js +3 -0
- package/dist/channels/tui/layout.js +167 -0
- package/dist/channels/tui/markdown/blocks.js +111 -0
- package/dist/channels/tui/markdown/inline.js +75 -0
- package/dist/channels/tui/markdown/markdown.js +182 -0
- package/dist/channels/tui/memory/memory-viewer.js +540 -0
- package/dist/channels/tui/overlay/alt-screen.js +105 -0
- package/dist/channels/tui/overlay/overlay.js +57 -0
- package/dist/channels/tui/overlay/select-confirm.js +156 -0
- package/dist/channels/tui/panels/todo-panel.js +54 -0
- package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
- package/dist/channels/tui/scroll/scroll-view.js +254 -0
- package/dist/channels/tui/status/status-line.js +76 -0
- package/dist/channels/tui/text.js +38 -0
- package/dist/channels/tui/theme.js +41 -0
- package/dist/channels/types.js +11 -0
- package/dist/channels/ui-core.js +209 -0
- package/dist/checkpoint/capture.js +45 -0
- package/dist/checkpoint/codes.js +33 -0
- package/dist/checkpoint/command.js +82 -0
- package/dist/checkpoint/gate.js +48 -0
- package/dist/checkpoint/index.js +20 -0
- package/dist/checkpoint/restore.js +172 -0
- package/dist/checkpoint/store.js +247 -0
- package/dist/checkpoint/types.js +4 -0
- package/dist/checkpoint/walk.js +146 -0
- package/dist/compaction/ccr-tools.js +112 -0
- package/dist/compaction/ccr.js +84 -0
- package/dist/compaction/codes.js +21 -0
- package/dist/compaction/index.js +18 -0
- package/dist/compaction/policy.js +200 -0
- package/dist/compaction/service.js +422 -0
- package/dist/compaction/slots.js +121 -0
- package/dist/compaction/types.js +10 -0
- package/dist/context/codes.js +40 -0
- package/dist/context/events.js +160 -0
- package/dist/context/index.js +13 -0
- package/dist/context/logger.js +136 -0
- package/dist/context/scope.js +182 -0
- package/dist/context/workspace.js +104 -0
- package/dist/contracts/agent-events.d.ts +73 -0
- package/dist/contracts/agent-events.js +16 -0
- package/dist/contracts/api.d.ts +249 -0
- package/dist/contracts/api.js +299 -0
- package/dist/contracts/approval.d.ts +37 -0
- package/dist/contracts/approval.js +9 -0
- package/dist/contracts/env-ref.d.ts +41 -0
- package/dist/contracts/env-ref.js +32 -0
- package/dist/contracts/errors.d.ts +56 -0
- package/dist/contracts/errors.js +266 -0
- package/dist/contracts/events.d.ts +70 -0
- package/dist/contracts/events.js +329 -0
- package/dist/contracts/index.d.ts +32 -0
- package/dist/contracts/index.js +26 -0
- package/dist/contracts/llm.d.ts +275 -0
- package/dist/contracts/llm.js +12 -0
- package/dist/contracts/messages.d.ts +63 -0
- package/dist/contracts/messages.js +65 -0
- package/dist/contracts/redact.d.ts +46 -0
- package/dist/contracts/redact.js +223 -0
- package/dist/contracts/tools.d.ts +197 -0
- package/dist/contracts/tools.js +33 -0
- package/dist/contracts/types.d.ts +302 -0
- package/dist/contracts/types.js +50 -0
- package/dist/contracts/ui.d.ts +122 -0
- package/dist/contracts/ui.js +15 -0
- package/dist/conversation/agent-service.js +61 -0
- package/dist/conversation/approval-wiring.js +82 -0
- package/dist/conversation/backoff.js +38 -0
- package/dist/conversation/codes.js +47 -0
- package/dist/conversation/control-tools.js +102 -0
- package/dist/conversation/control.js +210 -0
- package/dist/conversation/driver.js +922 -0
- package/dist/conversation/index.js +12 -0
- package/dist/conversation/model-visible.js +169 -0
- package/dist/conversation/open-tools.js +126 -0
- package/dist/conversation/reseed.js +132 -0
- package/dist/conversation/sessions.js +215 -0
- package/dist/conversation/todo.js +203 -0
- package/dist/conversation/types.js +35 -0
- package/dist/conversation/wiring.js +204 -0
- package/dist/credentials/codes.js +59 -0
- package/dist/credentials/commands.js +186 -0
- package/dist/credentials/env-ref.js +34 -0
- package/dist/credentials/index.js +28 -0
- package/dist/credentials/migration.js +18 -0
- package/dist/credentials/oauth.js +266 -0
- package/dist/credentials/refresh.js +152 -0
- package/dist/credentials/secrets.js +118 -0
- package/dist/credentials/types.js +30 -0
- package/dist/exec/bash.js +279 -0
- package/dist/exec/codes.js +37 -0
- package/dist/exec/env.js +99 -0
- package/dist/exec/environment.js +39 -0
- package/dist/exec/git-guard.js +623 -0
- package/dist/exec/index.js +24 -0
- package/dist/exec/registry.js +120 -0
- package/dist/exec/spawn.js +281 -0
- package/dist/exec/tail.js +77 -0
- package/dist/exec/types.js +5 -0
- package/dist/goal/codes.js +33 -0
- package/dist/goal/command.js +175 -0
- package/dist/goal/fold.js +121 -0
- package/dist/goal/gates.js +122 -0
- package/dist/goal/index.js +22 -0
- package/dist/goal/migration.js +48 -0
- package/dist/goal/service.js +501 -0
- package/dist/goal/todo-tool.js +174 -0
- package/dist/goal/types.js +1 -0
- package/dist/goal/update-tool.js +46 -0
- package/dist/host/approval-cmd.js +276 -0
- package/dist/host/assembly.js +1146 -0
- package/dist/host/boot-failures.js +104 -0
- package/dist/host/budget-advisory.js +74 -0
- package/dist/host/budget-broadcast.js +50 -0
- package/dist/host/builtins.js +54 -0
- package/dist/host/cli.js +544 -0
- package/dist/host/codes.js +120 -0
- package/dist/host/config-schema.js +195 -0
- package/dist/host/conversation-stack.js +654 -0
- package/dist/host/core-plugins.js +1685 -0
- package/dist/host/credentials-cmd.js +42 -0
- package/dist/host/disclosure.js +30 -0
- package/dist/host/dispatch.js +105 -0
- package/dist/host/doors-cmd.js +154 -0
- package/dist/host/dump-config.js +44 -0
- package/dist/host/hook-dispatch-guard.js +27 -0
- package/dist/host/import-gate.js +153 -0
- package/dist/host/index.js +54 -0
- package/dist/host/issue-session.js +241 -0
- package/dist/host/loader.js +372 -0
- package/dist/host/main.js +161 -0
- package/dist/host/manifest.js +381 -0
- package/dist/host/mcp-entry.js +53 -0
- package/dist/host/plugin-boot.js +952 -0
- package/dist/host/plugin-context.js +672 -0
- package/dist/host/plugin-install.js +480 -0
- package/dist/host/plugin-reload.js +124 -0
- package/dist/host/plugin-store.js +462 -0
- package/dist/host/plugin-tools.js +359 -0
- package/dist/host/plugin-uninstall.js +257 -0
- package/dist/host/plugins-cmd.js +302 -0
- package/dist/host/plugins-command.js +130 -0
- package/dist/host/plugins-config.js +195 -0
- package/dist/host/prompt-sections.js +133 -0
- package/dist/host/run-entry.js +544 -0
- package/dist/host/runtime.js +209 -0
- package/dist/host/scheduler-tick.js +294 -0
- package/dist/host/serve-daemon.js +501 -0
- package/dist/host/serve-entry.js +279 -0
- package/dist/host/session-anchor.js +41 -0
- package/dist/host/sessions-cmd.js +219 -0
- package/dist/host/sessions-face.js +47 -0
- package/dist/host/settings-store.js +112 -0
- package/dist/host/signals.js +69 -0
- package/dist/host/single-instance.js +105 -0
- package/dist/host/subagent-factory.js +291 -0
- package/dist/host/testkit/harness.js +283 -0
- package/dist/host/testkit/index.js +14 -0
- package/dist/host/testkit/matrix.js +302 -0
- package/dist/host/tool-policy-store.js +177 -0
- package/dist/host/triggers.js +212 -0
- package/dist/host/tui-entry.js +216 -0
- package/dist/host/webui-bridge.js +165 -0
- package/dist/issue/codes.js +38 -0
- package/dist/issue/filter.js +146 -0
- package/dist/issue/github.js +116 -0
- package/dist/issue/index.js +17 -0
- package/dist/issue/mount.js +70 -0
- package/dist/issue/poll.js +58 -0
- package/dist/issue/service.js +341 -0
- package/dist/issue/tools.js +70 -0
- package/dist/issue/types.js +34 -0
- package/dist/issue/webhook.js +126 -0
- package/dist/llm/codes.js +77 -0
- package/dist/llm/complete.js +196 -0
- package/dist/llm/events.js +15 -0
- package/dist/llm/index.js +24 -0
- package/dist/llm/inflight.js +65 -0
- package/dist/llm/model-id.js +58 -0
- package/dist/llm/provider-face.d.ts +32 -0
- package/dist/llm/provider-face.js +34 -0
- package/dist/llm/recovery.js +146 -0
- package/dist/llm/runtime.js +46 -0
- package/dist/llm/stream-fn.js +147 -0
- package/dist/lsp/codes.js +31 -0
- package/dist/lsp/connection.js +147 -0
- package/dist/lsp/frame.js +110 -0
- package/dist/lsp/index.js +16 -0
- package/dist/lsp/inject.js +58 -0
- package/dist/lsp/instance.js +264 -0
- package/dist/lsp/service.js +522 -0
- package/dist/lsp/tools.js +53 -0
- package/dist/lsp/types.js +201 -0
- package/dist/mcp/bridge.js +222 -0
- package/dist/mcp/codes.js +21 -0
- package/dist/mcp/index.js +19 -0
- package/dist/mcp/jsonrpc.js +230 -0
- package/dist/mcp/service.js +125 -0
- package/dist/mcp/tools.js +145 -0
- package/dist/mcp/types.js +108 -0
- package/dist/memory/cite.js +131 -0
- package/dist/memory/codes.js +50 -0
- package/dist/memory/command.js +71 -0
- package/dist/memory/consolidate.js +219 -0
- package/dist/memory/cycle.js +109 -0
- package/dist/memory/dao.js +878 -0
- package/dist/memory/diff.js +238 -0
- package/dist/memory/extract.js +181 -0
- package/dist/memory/fts.js +58 -0
- package/dist/memory/index.js +42 -0
- package/dist/memory/inject.js +354 -0
- package/dist/memory/merge.js +178 -0
- package/dist/memory/migration.js +132 -0
- package/dist/memory/pollution.js +59 -0
- package/dist/memory/port.js +385 -0
- package/dist/memory/review.js +201 -0
- package/dist/memory/scan.js +89 -0
- package/dist/memory/tools.js +508 -0
- package/dist/memory/types.js +140 -0
- package/dist/obs/codes.js +38 -0
- package/dist/obs/db.js +115 -0
- package/dist/obs/index.js +20 -0
- package/dist/obs/rollup.js +61 -0
- package/dist/obs/service.js +363 -0
- package/dist/obs/session-tools.js +282 -0
- package/dist/obs/session-view.js +225 -0
- package/dist/obs/tool.js +97 -0
- package/dist/obs/types.js +1 -0
- package/dist/persist/audit.js +63 -0
- package/dist/persist/aux.js +39 -0
- package/dist/persist/codes.js +35 -0
- package/dist/persist/index.js +30 -0
- package/dist/persist/load-history.js +50 -0
- package/dist/persist/migrations.js +37 -0
- package/dist/persist/paths.js +108 -0
- package/dist/persist/persistence.js +229 -0
- package/dist/persist/schema.js +99 -0
- package/dist/persist/secret-box.js +95 -0
- package/dist/persist/store.js +706 -0
- package/dist/persist/write-behind.js +238 -0
- package/dist/safety/approval.js +171 -0
- package/dist/safety/bwrap.js +123 -0
- package/dist/safety/codes.js +57 -0
- package/dist/safety/danger.js +505 -0
- package/dist/safety/gate.js +226 -0
- package/dist/safety/index.js +24 -0
- package/dist/safety/presets.js +73 -0
- package/dist/safety/roots.js +139 -0
- package/dist/safety/sandbox.js +214 -0
- package/dist/safety/seatbelt.js +88 -0
- package/dist/safety/sensitive.js +34 -0
- package/dist/safety/tool-policy.js +203 -0
- package/dist/safety/types.js +13 -0
- package/dist/scheduler/codes.js +54 -0
- package/dist/scheduler/cron-backend.js +135 -0
- package/dist/scheduler/engine.js +280 -0
- package/dist/scheduler/gates.js +60 -0
- package/dist/scheduler/index.js +25 -0
- package/dist/scheduler/migration.js +22 -0
- package/dist/scheduler/runner.js +171 -0
- package/dist/scheduler/schedule.js +193 -0
- package/dist/scheduler/service.js +283 -0
- package/dist/scheduler/tick.js +139 -0
- package/dist/scheduler/types.js +1 -0
- package/dist/sdk/http.js +865 -0
- package/dist/sdk/index.js +22 -0
- package/dist/sdk/mcp.js +289 -0
- package/dist/sdk/plugin-route-registry.js +126 -0
- package/dist/sdk/plugin-routes.js +149 -0
- package/dist/sdk/security.js +116 -0
- package/dist/sdk/types.js +32 -0
- package/dist/session/budget.js +159 -0
- package/dist/session/codes.js +40 -0
- package/dist/session/derive.js +180 -0
- package/dist/session/event-data.js +1 -0
- package/dist/session/fork.js +72 -0
- package/dist/session/import-gates.js +148 -0
- package/dist/session/index.js +18 -0
- package/dist/session/recover.js +103 -0
- package/dist/session/session.js +279 -0
- package/dist/session/snapshot.js +107 -0
- package/dist/skills/agents.js +242 -0
- package/dist/skills/codes.js +51 -0
- package/dist/skills/discovery.js +266 -0
- package/dist/skills/frontmatter.js +215 -0
- package/dist/skills/index.js +28 -0
- package/dist/skills/load.js +118 -0
- package/dist/skills/manage.js +201 -0
- package/dist/skills/registry.js +138 -0
- package/dist/skills/render.js +124 -0
- package/dist/skills/sections.js +171 -0
- package/dist/skills/types.js +21 -0
- package/dist/subagent/codes.js +50 -0
- package/dist/subagent/declarative.js +55 -0
- package/dist/subagent/index.js +19 -0
- package/dist/subagent/notify.js +51 -0
- package/dist/subagent/provide.js +7 -0
- package/dist/subagent/registry.js +149 -0
- package/dist/subagent/service.js +338 -0
- package/dist/subagent/surface.js +44 -0
- package/dist/subagent/tool.js +153 -0
- package/dist/subagent/types.js +25 -0
- package/dist/tools/apply-patch.js +164 -0
- package/dist/tools/codes.js +109 -0
- package/dist/tools/fs.js +501 -0
- package/dist/tools/index.js +17 -0
- package/dist/tools/observed.js +99 -0
- package/dist/tools/pipeline.js +176 -0
- package/dist/tools/protected-read.js +54 -0
- package/dist/tools/registry.js +250 -0
- package/dist/tools/search.js +451 -0
- package/dist/tools/worktree.js +327 -0
- package/dist/web/codes.js +37 -0
- package/dist/web/gate.js +43 -0
- package/dist/web/hygiene.js +195 -0
- package/dist/web/index.js +16 -0
- package/dist/web/service.js +207 -0
- package/dist/web/ssrf-guard.js +44 -0
- package/dist/web/tool.js +76 -0
- package/dist/web/types.js +10 -0
- package/dist/webui/assets/index-B-t2O8gh.js +22 -0
- package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
- package/dist/webui/index.html +14 -0
- package/dist/webui/index.js +14 -0
- package/dist/webui/server.js +549 -0
- package/dist/webui/types.js +38 -0
- package/examples/README.md +26 -0
- package/examples/minimal-code-plugin/entry.js +43 -0
- package/examples/minimal-code-plugin/package.json +12 -0
- package/examples/pure-skill-pack/package.json +14 -0
- package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
- package/package.json +86 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* issue 服务面(03 §10.7 全条款的编排核心——无人值守 issue→PR 件)。
|
|
3
|
+
*
|
|
4
|
+
* 入队纪律(enqueue 同步裁决、running 判定到 register 零 await——双触发
|
|
5
|
+
* 源竞速安全):
|
|
6
|
+
* capabilities 预检(goal/exec/checkpoint 三名缺席任一拒)→ closed 弃 →
|
|
7
|
+
* dedupeKey 在飞互斥(running() 扫)→ 全局日池判(canAffordIssue)→
|
|
8
|
+
* jobs.register(kind 'issue'、name=dedupeKey、owner=合成键——会话未起,
|
|
9
|
+
* 见 types.ts owner 注记)→ fire runOne(不 await)。
|
|
10
|
+
*
|
|
11
|
+
* runOne 编舞(单 issue 全程):
|
|
12
|
+
* worktree 名候选让位(issue-N → -r2..-r9,分支留史撞名域)→ create →
|
|
13
|
+
* startHeadless(cwd=worktree、prompt=buildIssuePrompt、预算帽、issue_get)→
|
|
14
|
+
* grant(拿到 sessionId 才能授予——create 时会话不存在的补授位)→ await
|
|
15
|
+
* outcome → 交付映射(04 §10 issue 消费注):
|
|
16
|
+
* - draft+completed → 评论贴分支+补丁(60k 帽)→ settle completed;
|
|
17
|
+
* - auto+completed → 危险闸交付腿(04 §13——deliver = 闸包裹的 push/PR
|
|
18
|
+
* 执行):push 分支 → 开 PR → 回执贴 PR 链接 → settle completed;闸拒
|
|
19
|
+
* (DANGER_ 族)= 评论转人审 + 指路修复动作 + settle failed 需人审;
|
|
20
|
+
* 闸缺席(装配未注入 face)= 03 §10.7 原语义保持——阻塞转人审;
|
|
21
|
+
* - failed → 评论贴原因 → settle failed;
|
|
22
|
+
* - needs-human → 评论转人审 → settle failed(需人审);
|
|
23
|
+
* - paused → **不 settle 不 clean 不释授予**——worktree/授予/在飞记账全
|
|
24
|
+
* 保留(budget_extended 唤醒 watcher 已接线——issue-session 起跑时登记
|
|
25
|
+
* 全部停靠 run,预算恢复自动唤醒续跑)。
|
|
26
|
+
* 收尾(非 paused):worktree clean(dirty 保留不强拆——变更可能正是交付
|
|
27
|
+
* 物)、releaseSession。评论投递失败不阻塞 settle 的反面——settle 恒在评论
|
|
28
|
+
* 后落(人可见面优先;网络挂死场景 fetch 层兜底)。
|
|
29
|
+
*/
|
|
30
|
+
import { BaseError } from '../contracts/index.js';
|
|
31
|
+
import { createIssueTools } from './tools.js';
|
|
32
|
+
import { createIssuePoller } from './poll.js';
|
|
33
|
+
import { handleWebhookRequest } from './webhook.js';
|
|
34
|
+
import { ISSUE_POLL_JOB_NAME, ISSUE_RECEIPT_PATCH_CHARS, ISSUE_WORKTREE_NAME_RE, issueDedupeKey } from './types.js';
|
|
35
|
+
/** capabilities 预检名单(03 §10.7 ③入队定值——三名缺席任一拒) */
|
|
36
|
+
const REQUIRED_CAPABILITIES = ['goal', 'exec', 'checkpoint'];
|
|
37
|
+
/**
|
|
38
|
+
* 危险闸拒码 → 回执指路(04 §13 消费面按码分流呈现——approve / 重签 /
|
|
39
|
+
* 删 HALT / 次日或提帽重签四路)。未知码兜底呈现状态命令。
|
|
40
|
+
*/
|
|
41
|
+
const DANGER_REMEDIES = {
|
|
42
|
+
DANGER_CONSENT_ABSENT: 'consent 缺席——TUI 运行 /danger approve 签发授权后重跑',
|
|
43
|
+
DANGER_CONSENT_INVALID: 'consent 过期或配置漂移——重跑 /danger approve 重签',
|
|
44
|
+
DANGER_HALTED: 'HALT 哨兵在场——删除数据目录下 HALT 文件即恢复',
|
|
45
|
+
DANGER_TARGET_DENIED: '目标不在危险闸值域——检查件配置 repos 后重签 consent',
|
|
46
|
+
DANGER_CAP_EXCEEDED: '当日交付帽已达——UTC 次日自动恢复,或提帽后重签 consent',
|
|
47
|
+
DANGER_LEDGER_CORRUPT: '危险闸审计账本链损坏——人工检修(截断/重建是人工决策)',
|
|
48
|
+
};
|
|
49
|
+
/** 拒码指路取值(未知码兜底——不静默) */
|
|
50
|
+
function dangerRemedy(code) {
|
|
51
|
+
return DANGER_REMEDIES[code] ?? '运行 /danger status 检查危险闸状态';
|
|
52
|
+
}
|
|
53
|
+
/** builtin 轮询行的 prompt 占位(RunnerFactory 对该行名程序化分派 pollOnce 零 token——行 prompt 不入模型面) */
|
|
54
|
+
const POLL_PROMPT_PLACEHOLDER = '(builtin) issue 轮询占位——挂钟行由 issue 件程序化分派(pollOnce),本 prompt 不发往任何模型';
|
|
55
|
+
/** 组 issue 服务 */
|
|
56
|
+
export function createIssueService(deps) {
|
|
57
|
+
const warn = deps.warn ?? (() => undefined);
|
|
58
|
+
// 在飞记账(dedupeKey → worktree 名——orphanScan 的在飞减集;paused 停靠保留)
|
|
59
|
+
const inflight = new Map();
|
|
60
|
+
/** worktree 名候选序列(issue-N 首选;分支留史撞名让位 -r2..-r9——九连撞即弃转人审) */
|
|
61
|
+
function* worktreeNameCandidates(number) {
|
|
62
|
+
yield `issue-${number}`;
|
|
63
|
+
for (let r = 2; r <= 9; r++)
|
|
64
|
+
yield `issue-${number}-r${r}`;
|
|
65
|
+
}
|
|
66
|
+
/** 组单 issue 的 headless 首跑 prompt(交付纪律内嵌:禁 push/PR/评论、预算可见、issue_get 指引) */
|
|
67
|
+
function buildIssuePrompt(issue, branch) {
|
|
68
|
+
return [
|
|
69
|
+
`处理 GitHub issue ${issue.repo}#${issue.number}:「${issue.title}」。`,
|
|
70
|
+
'',
|
|
71
|
+
'- 先用 issue_get 工具读 issue 正文与全部评论(本会话已绑定该 issue,无需参数)。',
|
|
72
|
+
`- 当前目录是为本次任务建的独立 git worktree(分支 ${branch})——所有改动在此分支上做并本地提交。`,
|
|
73
|
+
'- 严禁 push、严禁创建 PR、严禁对 issue 发评论(交付由编排层收口——越界动作将被拒)。',
|
|
74
|
+
`- 消息预算上限 ${deps.config.perIssueBudgetMessages} 条——聚焦最小可用改动,相关测试跑绿即算达成。`,
|
|
75
|
+
'- 目标:完成 issue 所述改动(含测试)并在本地提交。',
|
|
76
|
+
].join('\n');
|
|
77
|
+
}
|
|
78
|
+
/** 评论投递(失败不抛——回执后补语义,warn 记;终态必落优先于投递成功) */
|
|
79
|
+
async function postReceipt(issue, body) {
|
|
80
|
+
try {
|
|
81
|
+
await deps.backend.postComment({ repo: issue.repo, number: issue.number, body });
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
warn(`issue 回执评论投递失败(${issue.repo}#${issue.number}):${err instanceof Error ? err.message : String(err)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* runOne:单 issue 全程编舞(enqueue fire——不 await 调用方)。自身异常
|
|
89
|
+
* 兜底 settle failed(终态必落——编舞崩溃不悬挂注册表)。
|
|
90
|
+
*/
|
|
91
|
+
async function runOne(issue, key) {
|
|
92
|
+
const handle = deps.jobs.register({ kind: 'issue', name: key, owner: `issue-job:${key}` });
|
|
93
|
+
let created;
|
|
94
|
+
let sessionId;
|
|
95
|
+
let retain = false; // paused 停靠:授予/worktree/在飞记账全保留(唤醒接线随装配批)
|
|
96
|
+
try {
|
|
97
|
+
// ── 隔离:worktree 名候选让位(撞名 = 前次重跑残留——分支留史的代价面) ──
|
|
98
|
+
let lastExists;
|
|
99
|
+
for (const name of worktreeNameCandidates(issue.number)) {
|
|
100
|
+
try {
|
|
101
|
+
created = await deps.worktree.create({ name });
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
if (err instanceof BaseError && err.code === 'FS_WORKTREE_EXISTS') {
|
|
106
|
+
lastExists = err;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (created === undefined) {
|
|
113
|
+
throw lastExists instanceof Error
|
|
114
|
+
? lastExists
|
|
115
|
+
: new Error(`worktree 名候选 9 连撞(issue-${issue.number}——需人清残留分支)`);
|
|
116
|
+
}
|
|
117
|
+
inflight.set(key, created.name);
|
|
118
|
+
// ── 起跑:headless 会话(cwd=worktree、预算帽、issue_get 绑定面) ──
|
|
119
|
+
const started = await deps.session.startHeadless({
|
|
120
|
+
cwd: created.path,
|
|
121
|
+
prompt: buildIssuePrompt(issue, created.branch),
|
|
122
|
+
budgetMessages: deps.config.perIssueBudgetMessages,
|
|
123
|
+
tools: createIssueTools({ backend: deps.backend, repo: issue.repo, number: issue.number }),
|
|
124
|
+
});
|
|
125
|
+
sessionId = started.sessionId;
|
|
126
|
+
// 拿到 sessionId 才能授予(create 时会话不存在的补授位——04 §7 补钉①编排路径)
|
|
127
|
+
await deps.worktree.grant({ sessionId: started.sessionId, path: created.path });
|
|
128
|
+
const outcome = await started.outcome;
|
|
129
|
+
// ── 交付映射(04 §10 issue 消费注·终态三因) ──
|
|
130
|
+
if (outcome.status === 'paused') {
|
|
131
|
+
retain = true;
|
|
132
|
+
warn(`issue run 停靠(${key}):全局预算日池尽——worktree ${created.name} 与授予保留,预算恢复后经 budget_extended 唤醒自动续跑`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (outcome.status === 'completed') {
|
|
136
|
+
if (deps.config.mode === 'draft') {
|
|
137
|
+
// draft 档:评论贴分支 + 补丁(60k 字符帽——GitHub 评论体上限内留余量)
|
|
138
|
+
let patch = '';
|
|
139
|
+
try {
|
|
140
|
+
patch = await deps.worktree.diffPatch({ name: created.name, baseRef: deps.config.baseBranch });
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
warn(`补丁取数失败(${key}):${err instanceof Error ? err.message : String(err)}`);
|
|
144
|
+
}
|
|
145
|
+
if (patch.length > ISSUE_RECEIPT_PATCH_CHARS) {
|
|
146
|
+
patch = `${patch.slice(0, ISSUE_RECEIPT_PATCH_CHARS)}\n…(补丁超 ${ISSUE_RECEIPT_PATCH_CHARS} 字符帽截断)`;
|
|
147
|
+
}
|
|
148
|
+
await postReceipt(issue, [
|
|
149
|
+
`🤖 issue run 完成(draft 档):分支 \`${created.branch}\`(本地——未 push)`,
|
|
150
|
+
outcome.summary,
|
|
151
|
+
'',
|
|
152
|
+
'```diff',
|
|
153
|
+
patch || '(无补丁——无改动或取数失败)',
|
|
154
|
+
'```',
|
|
155
|
+
].join('\n'));
|
|
156
|
+
handle.settle({
|
|
157
|
+
status: 'completed',
|
|
158
|
+
detail: `draft:分支 ${created.branch}(${outcome.messagesUsed} 条消息)`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
// auto 档:危险闸交付腿(04 §13——deliver = 闸包裹的 push/PR 执行闭包)。
|
|
163
|
+
// 闸缺席(装配未注入 face)= 03 §10.7 原语义保持:阻塞转人审。
|
|
164
|
+
if (deps.danger === undefined) {
|
|
165
|
+
await postReceipt(issue, [
|
|
166
|
+
`🤖 issue run 完成(auto 档)但不可逆外部写闸缺席——阻塞转人审:分支 \`${created.branch}\` 已就绪(本地未 push),请人工确认后交付。`,
|
|
167
|
+
outcome.summary,
|
|
168
|
+
].join('\n'));
|
|
169
|
+
handle.settle({
|
|
170
|
+
status: 'failed',
|
|
171
|
+
detail: '需人审:不可逆外部写闸缺席(auto 档成果已备,push/PR 需人工)',
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// 交付序:先 push 分支(PR 依赖远端分支在场)→ 再开 PR → 回执贴链接。
|
|
176
|
+
// pushed 位供拒/败回执如实陈述分支状态(已推远端 vs 本地未推)。
|
|
177
|
+
let pushed = false;
|
|
178
|
+
try {
|
|
179
|
+
await deps.danger.deliver({
|
|
180
|
+
kind: 'push',
|
|
181
|
+
repo: issue.repo,
|
|
182
|
+
branch: created.branch,
|
|
183
|
+
worktreePath: created.path,
|
|
184
|
+
});
|
|
185
|
+
pushed = true;
|
|
186
|
+
const pr = await deps.danger.deliver({
|
|
187
|
+
kind: 'create-pr',
|
|
188
|
+
repo: issue.repo,
|
|
189
|
+
branch: created.branch,
|
|
190
|
+
base: deps.config.baseBranch,
|
|
191
|
+
title: issue.title,
|
|
192
|
+
body: [outcome.summary, '', `Closes #${issue.number}`, '', `Source issue: ${issue.htmlUrl}`].join('\n'),
|
|
193
|
+
worktreePath: created.path,
|
|
194
|
+
});
|
|
195
|
+
const prRef = pr.prUrl !== undefined && pr.prUrl !== '' ? pr.prUrl : `#${pr.prNumber ?? '?'}`;
|
|
196
|
+
await postReceipt(issue, [
|
|
197
|
+
`🤖 issue run 完成(auto 档——危险闸放行交付):PR ${prRef} 已开(分支 \`${created.branch}\`)。`,
|
|
198
|
+
outcome.summary,
|
|
199
|
+
].join('\n'));
|
|
200
|
+
handle.settle({
|
|
201
|
+
status: 'completed',
|
|
202
|
+
detail: `auto:PR ${prRef}(分支 ${created.branch},${outcome.messagesUsed} 条消息)`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
// 两路分账:DANGER_ 族 = 闸拒(零外联——execute 未被调用)转人审 +
|
|
207
|
+
// 指路修复;其余 = 过闸外部写失败(allow-failed 已记账)留分支重试。
|
|
208
|
+
const branchState = pushed
|
|
209
|
+
? `分支 \`${created.branch}\` 已推远端(PR 未开成——可人工补开)`
|
|
210
|
+
: `分支 \`${created.branch}\` 已就绪(本地未推)`;
|
|
211
|
+
if (err instanceof BaseError && err.code.startsWith('DANGER_')) {
|
|
212
|
+
const remedy = dangerRemedy(err.code);
|
|
213
|
+
await postReceipt(issue, [
|
|
214
|
+
`🤖 issue run 需人审(危险闸拒 ${err.code})——${branchState}。`,
|
|
215
|
+
`修复指路:${remedy}`,
|
|
216
|
+
`原始判据:${err.message}`,
|
|
217
|
+
outcome.summary,
|
|
218
|
+
].join('\n'));
|
|
219
|
+
handle.settle({ status: 'failed', detail: `需人审:危险闸拒(${err.code})——${remedy}` });
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
223
|
+
await postReceipt(issue, [`🤖 issue run 交付失败(外部写未成):${detail}——${branchState},可重试。`, outcome.summary].join('\n'));
|
|
224
|
+
handle.settle({ status: 'failed', detail: `交付失败:${detail}` });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (outcome.status === 'failed') {
|
|
230
|
+
await postReceipt(issue, `🤖 issue run 失败:${outcome.reason}(分支 \`${created.branch}\` 留存供排查)`);
|
|
231
|
+
handle.settle({ status: 'failed', detail: outcome.reason });
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
// needs-human:无应答者审批拒/写动作无策略表覆盖(04 §9 fail-closed)——转人审
|
|
235
|
+
await postReceipt(issue, `🤖 issue run 需人审:${outcome.reason}(分支 \`${created.branch}\` 留存——处理后可重开)`);
|
|
236
|
+
handle.settle({ status: 'failed', detail: `需人审:${outcome.reason}` });
|
|
237
|
+
}
|
|
238
|
+
// ── 收尾:拆 worktree(dirty 保留——未提交变更可能是交付物残余) ──
|
|
239
|
+
try {
|
|
240
|
+
await deps.worktree.clean({ name: created.name });
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
if (err instanceof BaseError && err.code === 'FS_WORKTREE_DIRTY') {
|
|
244
|
+
warn(`issue worktree 未提交变更保留不拆(${created.name})——分支 ${created.branch} 留存`);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
warn(`issue worktree 清理失败(${created.name}):${err instanceof Error ? err.message : String(err)}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
// 兜底:编舞自身异常——终态必落(注册表不悬挂)
|
|
253
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
254
|
+
handle.settle({ status: 'failed', detail });
|
|
255
|
+
warn(`issue run 编舞异常(${key}):${detail}`);
|
|
256
|
+
if (created !== undefined) {
|
|
257
|
+
try {
|
|
258
|
+
await deps.worktree.clean({ name: created.name });
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// 兜底清理失败——残留归 orphanScan 标注
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
if (!retain) {
|
|
267
|
+
inflight.delete(key);
|
|
268
|
+
if (sessionId !== undefined)
|
|
269
|
+
deps.worktree.releaseSession(sessionId);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* 入队裁决(同步、零 await 到 register——竞速窗口不存在)。预检序:
|
|
275
|
+
* capabilities → closed → dedupe → budget → register → fire。
|
|
276
|
+
*/
|
|
277
|
+
function enqueue(issue) {
|
|
278
|
+
for (const cap of REQUIRED_CAPABILITIES) {
|
|
279
|
+
if (!deps.capabilities.includes(cap)) {
|
|
280
|
+
return { status: 'rejected', reason: `能力缺席:${cap}(issue 件依赖 goal/exec/checkpoint 三件在场)` };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (issue.state !== 'open') {
|
|
284
|
+
return { status: 'rejected', reason: `issue 已 ${issue.state}(终态不再入队)` };
|
|
285
|
+
}
|
|
286
|
+
const key = issueDedupeKey(issue.repo, issue.number);
|
|
287
|
+
if (deps.jobs.running().some((j) => j.name === key)) {
|
|
288
|
+
warn(`[ISSUE_JOB_DUPLICATE] 在飞互斥撞锁:${key}(进程内——不重入)`);
|
|
289
|
+
return { status: 'duplicate', key };
|
|
290
|
+
}
|
|
291
|
+
const afford = deps.budget.canAffordIssue();
|
|
292
|
+
if (!afford.ok) {
|
|
293
|
+
return { status: 'rejected', reason: afford.reason ?? '全局预算日池尽(停靠)' };
|
|
294
|
+
}
|
|
295
|
+
// fire(runOne 首语句 register 同步执行——enqueue 返回前已入册,紧随的
|
|
296
|
+
// 二次 enqueue 在 running() 扫描即见:互斥成立)
|
|
297
|
+
void runOne(issue, key);
|
|
298
|
+
return { status: 'started', key };
|
|
299
|
+
}
|
|
300
|
+
const poller = createIssuePoller({ backend: deps.backend, state: deps.state, config: deps.config, enqueue });
|
|
301
|
+
const service = {
|
|
302
|
+
start() {
|
|
303
|
+
deps.jobs.registerKind('issue');
|
|
304
|
+
try {
|
|
305
|
+
deps.scheduler.registerPollJob({
|
|
306
|
+
name: ISSUE_POLL_JOB_NAME,
|
|
307
|
+
schedule: deps.config.schedule,
|
|
308
|
+
prompt: POLL_PROMPT_PLACEHOLDER,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
// 幂等:行已在(重启再启)——warn 不炸装配;其余守卫拒(schedule 坏串)同样响亮可见
|
|
313
|
+
warn(`issue 轮询行登记失败:${err instanceof Error ? err.message : String(err)}`);
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
stop() {
|
|
317
|
+
deps.scheduler.removePollJob(ISSUE_POLL_JOB_NAME);
|
|
318
|
+
},
|
|
319
|
+
pollOnce() {
|
|
320
|
+
return poller.pollOnce();
|
|
321
|
+
},
|
|
322
|
+
enqueue(issue) {
|
|
323
|
+
return enqueue(issue);
|
|
324
|
+
},
|
|
325
|
+
async handleWebhook(req) {
|
|
326
|
+
if (deps.webhookSecret === undefined || deps.webhookSecret === '') {
|
|
327
|
+
throw new BaseError('ISSUE_WEBHOOK_INVALID', '[ISSUE_WEBHOOK_INVALID] webhook 面未开启(缺 secret 配置)');
|
|
328
|
+
}
|
|
329
|
+
return handleWebhookRequest({ secret: deps.webhookSecret, config: deps.config, enqueue }, req);
|
|
330
|
+
},
|
|
331
|
+
async orphanScan() {
|
|
332
|
+
// 名形匹配减在飞——只标注不自动清(销账#3:残留 worktree = 标注孤儿)
|
|
333
|
+
const entries = await deps.worktree.list();
|
|
334
|
+
const inflightNames = new Set(inflight.values());
|
|
335
|
+
return entries
|
|
336
|
+
.filter((e) => ISSUE_WORKTREE_NAME_RE.test(e.name) && !inflightNames.has(e.name))
|
|
337
|
+
.map((e) => ({ name: e.name, path: e.path }));
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
return service;
|
|
341
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* issue 工具面(03 §10.7——件注册的只读工具)。
|
|
3
|
+
*
|
|
4
|
+
* 单工具 `issue_get`:取本 issue 正文 + 评论清单(升序)。绑定面设计——
|
|
5
|
+
* repo/number 由件侧闭包注入(模型不可指定他仓他号:外部输入最小暴露面),
|
|
6
|
+
* 参数面零自由度(无参数——headless 会话生来只服务这一个 issue)。
|
|
7
|
+
*
|
|
8
|
+
* 上下文帽:正文 + 评论合计 ISSUE_CONTEXT_CAP_BYTES(64KiB)——外部不可信
|
|
9
|
+
* 文本进模型上下文的总闸(出口治理④字段瘦身同律);超帽截断尾部并注记
|
|
10
|
+
* (截断标记让模型自知信息不全,不静默丢)。
|
|
11
|
+
*/
|
|
12
|
+
import { Type } from 'typebox';
|
|
13
|
+
import { ISSUE_CONTEXT_CAP_BYTES } from './types.js';
|
|
14
|
+
/** 组 issue_get 工具(单件数组——effect read 恒免审批) */
|
|
15
|
+
export function createIssueTools(deps) {
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
name: 'issue_get',
|
|
19
|
+
effect: 'read',
|
|
20
|
+
description: '取当前 issue 的正文与全部评论(时间升序)。无参数——本会话已绑定该 issue。输出超 64KiB 时尾部截断并注记。',
|
|
21
|
+
parameters: Type.Object({}),
|
|
22
|
+
execute: async () => {
|
|
23
|
+
const [issues, comments] = await Promise.all([
|
|
24
|
+
deps.backend.listIssues({ repo: deps.repo }),
|
|
25
|
+
deps.backend.listComments({ repo: deps.repo, number: deps.number }),
|
|
26
|
+
]);
|
|
27
|
+
const mine = issues.find((i) => i.number === deps.number);
|
|
28
|
+
if (mine === undefined) {
|
|
29
|
+
return {
|
|
30
|
+
content: [
|
|
31
|
+
{ type: 'text', text: `issue ${deps.repo}#${deps.number} 不在源返回集内(可能已关闭或删除)` },
|
|
32
|
+
],
|
|
33
|
+
isError: true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// 正文段(不可信外部文本——原样引用不转义解释,帽前标注来源)
|
|
37
|
+
const sections = [
|
|
38
|
+
`# ${mine.title}(${mine.repo}#${mine.number})`,
|
|
39
|
+
`状态:${mine.state}|标签:${mine.labels.join(', ') || '(无)'}|指派:${mine.assignees.join(', ') || '(无)'}`,
|
|
40
|
+
`链接:${mine.htmlUrl}`,
|
|
41
|
+
'',
|
|
42
|
+
mine.body || '(正文为空)',
|
|
43
|
+
];
|
|
44
|
+
if (comments.length > 0) {
|
|
45
|
+
sections.push('', '## 评论');
|
|
46
|
+
for (const c of comments) {
|
|
47
|
+
sections.push(`— ${c.author}(${c.createdAt}):`, c.body || '(空)');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let text = sections.join('\n');
|
|
51
|
+
// 总闸帽:超限截断尾部 + 截断注记(模型自知信息不全)
|
|
52
|
+
const bytes = Buffer.byteLength(text, 'utf8');
|
|
53
|
+
if (bytes > ISSUE_CONTEXT_CAP_BYTES) {
|
|
54
|
+
// 逐段回退到帽内(段级截断——不产生半个多字节字符)
|
|
55
|
+
const kept = [];
|
|
56
|
+
let used = 0;
|
|
57
|
+
for (const section of sections) {
|
|
58
|
+
const b = Buffer.byteLength(section, 'utf8') + 1;
|
|
59
|
+
if (used + b > ISSUE_CONTEXT_CAP_BYTES - 200)
|
|
60
|
+
break; // 尾注记余量
|
|
61
|
+
kept.push(section);
|
|
62
|
+
used += b;
|
|
63
|
+
}
|
|
64
|
+
text = `${kept.join('\n')}\n\n…(内容超 ${ISSUE_CONTEXT_CAP_BYTES} 字节帽已截断——评论可能不全)`;
|
|
65
|
+
}
|
|
66
|
+
return { content: [{ type: 'text', text }], details: { comments: comments.length } };
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* ---------------- 常量(缺省值单源——03 §10.7 落码定形注) ---------------- */
|
|
2
|
+
/** 轮询挂钟行名(builtin 行——装配 RunnerFactory 对该名程序化分派 pollOnce) */
|
|
3
|
+
export const ISSUE_POLL_JOB_NAME = 'issue-poll';
|
|
4
|
+
/** 轮询缺省周期(120s——schedule 串形态承 scheduler 词法) */
|
|
5
|
+
export const ISSUE_DEFAULT_SCHEDULE = 'every:120s';
|
|
6
|
+
/** Job kind issue 并行帽缺省(04 §10 定值注——装配 createJobRegistry parallelLimits 槽用) */
|
|
7
|
+
export const ISSUE_PARALLEL_LIMIT_DEFAULT = 2;
|
|
8
|
+
/** 每 issue 预算帽缺省(消息条数——实测定值校准挂账) */
|
|
9
|
+
export const ISSUE_PER_ISSUE_MESSAGES_DEFAULT = 400;
|
|
10
|
+
/** 回执评论补丁字符帽(GitHub 评论体 64KiB 上限内留余量——超限截断注记) */
|
|
11
|
+
export const ISSUE_RECEIPT_PATCH_CHARS = 60_000;
|
|
12
|
+
/** issue_get 工具上下文帽字节(外部文本进上下文的总闸——出口治理④字段瘦身同律) */
|
|
13
|
+
export const ISSUE_CONTEXT_CAP_BYTES = 64 * 1024;
|
|
14
|
+
/** webhook 端点路径(POST——`/webhooks/*` 入站外部投递路由族首径;03 §10.7 批 18a-4' 落码定形) */
|
|
15
|
+
export const ISSUE_WEBHOOK_ENDPOINT = '/webhooks/issue';
|
|
16
|
+
/** webhook 请求体帽(per-route bodyLimitBytes 位——GitHub issue 载荷 64KiB 级 + 元数据余量) */
|
|
17
|
+
export const ISSUE_WEBHOOK_BODY_LIMIT_BYTES = 256 * 1024;
|
|
18
|
+
/** GitHub token 凭证名(host 域——03 §10.9「env 与库优先级」c-5 迁移:凭证库
|
|
19
|
+
* 优先、env `BERRY_AGENT_GITHUB_TOKEN` 过渡载体回落——/credentials add
|
|
20
|
+
* github-token <token> 录入即生效) */
|
|
21
|
+
export const ISSUE_GITHUB_TOKEN_NAME = 'github-token';
|
|
22
|
+
/** webhook HMAC secret 凭证名(host 域——同上律:库优先、env
|
|
23
|
+
* `BERRY_AGENT_ISSUE_WEBHOOK_SECRET` 过渡载体回落) */
|
|
24
|
+
export const ISSUE_WEBHOOK_SECRET_NAME = 'issue-webhook-secret';
|
|
25
|
+
/** 交付日成功帽缺省(04 §13——mandate maxPerDay 原料;正整数律在
|
|
26
|
+
* normalizeIssueConfig 执法:0/负数/坏形 = 空帽即坏形拒,彻底关停走 HALT
|
|
27
|
+
* 哨兵或撤 consent 不走空帽) */
|
|
28
|
+
export const ISSUE_MAX_DELIVERIES_PER_DAY_DEFAULT = 10;
|
|
29
|
+
/** dedupeKey(03 §10.7 入队条:`repo#issue`——进程内在飞互斥键非 durable 幂等键) */
|
|
30
|
+
export function issueDedupeKey(repo, number) {
|
|
31
|
+
return `${repo}#${number}`;
|
|
32
|
+
}
|
|
33
|
+
/** worktree 名形(`issue-<n>` 首选、重跑递进 `-r<k>`——分支留史撞名让位) */
|
|
34
|
+
export const ISSUE_WORKTREE_NAME_RE = /^issue-\d+(-r\d+)?$/;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* issue webhook 腿(03 §10.7 触发面——可开启面)。
|
|
3
|
+
*
|
|
4
|
+
* 验签:X-Signature-256 头 = `sha256=` + HMAC-SHA256(secret, 原始 body)——
|
|
5
|
+
* timingSafeEqual 常时比对(防时序侧信道);缺头/坏头/签名不符一律
|
|
6
|
+
* ISSUE_WEBHOOK_INVALID 响亮拒(不静默吞——运维可见)。
|
|
7
|
+
*
|
|
8
|
+
* 载荷路由(GitHub webhook 事件面子集):
|
|
9
|
+
* - `issues`(opened/labeled/assigned/reopened)→ 归一 IssueRef → 域匹配
|
|
10
|
+
* (issueMatchesFilter)→ enqueue;域外静默记 ignored(他仓事件正常流);
|
|
11
|
+
* - `issue_comment` → v1 留位(03 §10.7 裁决⑧:评论不触发——防评论风暴
|
|
12
|
+
* 自激;receipt 'comment-ignored' 可见);
|
|
13
|
+
* - `pull_request` → ignore(PR 域不属本件触发面);
|
|
14
|
+
* - 其余事件 → ignored。
|
|
15
|
+
*
|
|
16
|
+
* 不可信外部文本纪律同 github.ts:载荷字段进上下文前经调用方帽。
|
|
17
|
+
*/
|
|
18
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
19
|
+
import { BaseError } from '../contracts/index.js';
|
|
20
|
+
import { issueMatchesFilter } from './filter.js';
|
|
21
|
+
/** 计算 X-Signature-256 期望值(`sha256=` + hex——与 GitHub 官方算法一致) */
|
|
22
|
+
export function computeSignature(secret, rawBody) {
|
|
23
|
+
return `sha256=${createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex')}`;
|
|
24
|
+
}
|
|
25
|
+
/** 常时签名比对(等长前提 + timingSafeEqual;长度不等直接 false) */
|
|
26
|
+
export function signatureMatches(secret, rawBody, header) {
|
|
27
|
+
const expected = computeSignature(secret, rawBody);
|
|
28
|
+
const a = Buffer.from(expected, 'utf8');
|
|
29
|
+
const b = Buffer.from(header, 'utf8');
|
|
30
|
+
if (a.length !== b.length || a.length === 0)
|
|
31
|
+
return false;
|
|
32
|
+
return timingSafeEqual(a, b);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 解析载荷(事件名 + JSON 体 → 判别联合)。坏形(JSON 坏/域字段缺/类型错)
|
|
36
|
+
* 折 ISSUE_WEBHOOK_INVALID——外部输入不信任、但拒绝可观测。
|
|
37
|
+
*/
|
|
38
|
+
export function parseWebhookPayload(event, rawBody) {
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(rawBody);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new BaseError('ISSUE_WEBHOOK_INVALID', '[ISSUE_WEBHOOK_INVALID] webhook 载荷非合法 JSON');
|
|
45
|
+
}
|
|
46
|
+
const obj = parsed;
|
|
47
|
+
if (obj === null || typeof obj !== 'object') {
|
|
48
|
+
throw new BaseError('ISSUE_WEBHOOK_INVALID', '[ISSUE_WEBHOOK_INVALID] webhook 载荷须 JSON 对象形');
|
|
49
|
+
}
|
|
50
|
+
if (event === 'issues') {
|
|
51
|
+
const p = obj;
|
|
52
|
+
const repo = p.repository?.full_name;
|
|
53
|
+
const i = p.issue;
|
|
54
|
+
if (typeof repo !== 'string' || i === null || typeof i !== 'object') {
|
|
55
|
+
throw new BaseError('ISSUE_WEBHOOK_INVALID', '[ISSUE_WEBHOOK_INVALID] issues 事件缺 repository/issue 域');
|
|
56
|
+
}
|
|
57
|
+
if (typeof i.number !== 'number' || typeof i.title !== 'string' || typeof i.updated_at !== 'string') {
|
|
58
|
+
throw new BaseError('ISSUE_WEBHOOK_INVALID', '[ISSUE_WEBHOOK_INVALID] issue 域字段坏形(number/title/updated_at)');
|
|
59
|
+
}
|
|
60
|
+
const asString = (v) => (typeof v === 'string' ? v : '');
|
|
61
|
+
return {
|
|
62
|
+
kind: 'issues',
|
|
63
|
+
action: asString(p.action),
|
|
64
|
+
issue: {
|
|
65
|
+
repo,
|
|
66
|
+
number: i.number,
|
|
67
|
+
title: i.title,
|
|
68
|
+
body: typeof i.body === 'string' ? i.body : '',
|
|
69
|
+
labels: (i.labels ?? []).map((l) => asString(l?.name)).filter((n) => n !== ''),
|
|
70
|
+
assignees: (i.assignees ?? []).map((a) => asString(a?.login)).filter((n) => n !== ''),
|
|
71
|
+
state: i.state === 'closed' ? 'closed' : 'open',
|
|
72
|
+
updatedAt: i.updated_at,
|
|
73
|
+
htmlUrl: asString(i.html_url),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (event === 'issue_comment') {
|
|
78
|
+
const p = obj;
|
|
79
|
+
const repo = p.repository?.full_name;
|
|
80
|
+
const number = p.issue?.number;
|
|
81
|
+
if (typeof repo !== 'string' || typeof number !== 'number') {
|
|
82
|
+
throw new BaseError('ISSUE_WEBHOOK_INVALID', '[ISSUE_WEBHOOK_INVALID] issue_comment 事件缺 repository/issue 域');
|
|
83
|
+
}
|
|
84
|
+
return { kind: 'issue_comment', action: typeof p.action === 'string' ? p.action : '', repo, number };
|
|
85
|
+
}
|
|
86
|
+
if (event === 'pull_request') {
|
|
87
|
+
return {
|
|
88
|
+
kind: 'pull_request',
|
|
89
|
+
action: typeof obj.action === 'string' ? obj.action : '',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return { kind: 'ignored', event };
|
|
93
|
+
}
|
|
94
|
+
/** issues 事件触发动词集(opened/labeled/assigned/reopened——edited/closed/deleted 等不触发) */
|
|
95
|
+
const TRIGGERING_ACTIONS = new Set(['opened', 'labeled', 'assigned', 'reopened']);
|
|
96
|
+
/**
|
|
97
|
+
* 处理单次 webhook 请求(验签 → 解析 → 路由 → enqueue 裁决)。
|
|
98
|
+
* 验签失败/载荷坏形抛 BaseError(ISSUE_WEBHOOK_INVALID——装配层折 400);
|
|
99
|
+
* 域外/非触发动作 receipt 可见不抛(他仓事件是正常流非错误)。
|
|
100
|
+
*/
|
|
101
|
+
export async function handleWebhookRequest(deps, req) {
|
|
102
|
+
// 验签先行(未过验不解析——坏载荷信息不外泄)
|
|
103
|
+
if (!req.signatureHeader || !signatureMatches(deps.secret, req.rawBody, req.signatureHeader)) {
|
|
104
|
+
throw new BaseError('ISSUE_WEBHOOK_INVALID', '[ISSUE_WEBHOOK_INVALID] webhook 签名不符(X-Signature-256 缺失或错值——检查 secret 配置)');
|
|
105
|
+
}
|
|
106
|
+
const payload = parseWebhookPayload(req.event, req.rawBody);
|
|
107
|
+
if (payload.kind === 'issues') {
|
|
108
|
+
if (!TRIGGERING_ACTIONS.has(payload.action))
|
|
109
|
+
return { receipt: 'out-of-scope' };
|
|
110
|
+
if (!issueMatchesFilter(payload.issue, deps.config))
|
|
111
|
+
return { receipt: 'out-of-scope' };
|
|
112
|
+
const result = await deps.enqueue(payload.issue);
|
|
113
|
+
if (result.status === 'started')
|
|
114
|
+
return { receipt: 'enqueued', key: `${payload.issue.repo}#${payload.issue.number}` };
|
|
115
|
+
if (result.status === 'duplicate')
|
|
116
|
+
return { receipt: 'duplicate', key: result.key };
|
|
117
|
+
return { receipt: 'rejected', reason: result.reason };
|
|
118
|
+
}
|
|
119
|
+
if (payload.kind === 'issue_comment') {
|
|
120
|
+
// v1 留位(裁决⑧):评论不触发——receipt 可见便于日志判读
|
|
121
|
+
return { receipt: 'comment-ignored', repo: payload.repo, number: payload.number };
|
|
122
|
+
}
|
|
123
|
+
if (payload.kind === 'pull_request')
|
|
124
|
+
return { receipt: 'ignored', event: 'pull_request' };
|
|
125
|
+
return { receipt: 'ignored', event: payload.event };
|
|
126
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* llm 域错误码注册(04 篇 §3.5 四桶码族 + §3.7 + §5——LLM_ 前缀族首批)。
|
|
3
|
+
*
|
|
4
|
+
* 前缀族明列见 contracts/errors.ts ERROR_CODE_PREFIXES(02 §5.3 #1)。本文件
|
|
5
|
+
* 由模块公开面 index.ts 引入(注册纪律:写入点文件必须实际 import 本文件,
|
|
6
|
+
* 注册才发生——与 session/persist 的 codes.ts 同款)。
|
|
7
|
+
*
|
|
8
|
+
* 两类载体:抛出面(BaseError——MODEL_* / COMPLETE_FAILED / BUDGET_EXCEEDED)
|
|
9
|
+
* 与数据面(AssistantMessage.errorCode 携带——桶码族,供 classifyError 在场
|
|
10
|
+
* 码优先判定)。
|
|
11
|
+
*/
|
|
12
|
+
import { registerErrorCodes } from '../contracts/index.js';
|
|
13
|
+
registerErrorCodes([
|
|
14
|
+
// 04 §5 模型标识解析细则:语法层 fail-loud
|
|
15
|
+
{
|
|
16
|
+
code: 'LLM_MODEL_SPEC_INVALID',
|
|
17
|
+
module: 'llm',
|
|
18
|
+
description: '模型标识语法非法(缺斜杠/空段——必须是 "provider/model-id" 形式,首斜杠分割)',
|
|
19
|
+
},
|
|
20
|
+
// 04 §5:目录层 fail-loud(provider 未注册或模型不在目录)
|
|
21
|
+
{
|
|
22
|
+
code: 'LLM_MODEL_NOT_FOUND',
|
|
23
|
+
module: 'llm',
|
|
24
|
+
description: '模型解析失败:provider 未注册或该 provider 目录中无此模型 id',
|
|
25
|
+
},
|
|
26
|
+
// 04 §3.5 transient 桶码(429 限流——errorCode 归一位,provider 钩子纵切前无 provider 写点)
|
|
27
|
+
{
|
|
28
|
+
code: 'LLM_RATE_LIMITED',
|
|
29
|
+
module: 'llm',
|
|
30
|
+
description: '供应商限流(429)——transient 桶,turn 级 auto-retry 退避后重试',
|
|
31
|
+
},
|
|
32
|
+
// 04 §3.6 在飞帽拒绝(宿主合成码——transient 桶)
|
|
33
|
+
{
|
|
34
|
+
code: 'LLM_INFLIGHT_LIMIT',
|
|
35
|
+
module: 'llm',
|
|
36
|
+
description: 'per-provider 在飞请求达帽(缺省 4)——显式拒绝不排队,退避后槽已释放可重试',
|
|
37
|
+
},
|
|
38
|
+
// 04 §3.5 non-retryable 桶码(鉴权败——即 failed 用户面修复)
|
|
39
|
+
{
|
|
40
|
+
code: 'LLM_AUTH_INVALID',
|
|
41
|
+
module: 'llm',
|
|
42
|
+
description: '鉴权被拒(凭证无效/过期/权限不足)——non-retryable 桶,不重试',
|
|
43
|
+
},
|
|
44
|
+
// 04 §3.5 quota 桶码(配额尽——即 failed 换 key/等窗)
|
|
45
|
+
{
|
|
46
|
+
code: 'LLM_QUOTA_EXCEEDED',
|
|
47
|
+
module: 'llm',
|
|
48
|
+
description: '配额/余额耗尽——quota 桶,重试治不了(换 key 或等窗口)',
|
|
49
|
+
},
|
|
50
|
+
// 04 §3.5 overflow 桶码(上下文溢出——3.4 兜底 1/1)
|
|
51
|
+
{
|
|
52
|
+
code: 'LLM_CONTEXT_OVERFLOW',
|
|
53
|
+
module: 'llm',
|
|
54
|
+
description: '上下文溢出——overflow 桶,compact-and-retry-once 单次压缩重试',
|
|
55
|
+
},
|
|
56
|
+
// 04 §5 预算执法码(llm 层自产拒发错——不入四桶,调用方捕获即跳过本轮)
|
|
57
|
+
{
|
|
58
|
+
code: 'LLM_BUDGET_EXCEEDED',
|
|
59
|
+
module: 'llm',
|
|
60
|
+
description: '当日后台预算耗尽拒发(拒在请求发出前)——下个周期再试,不进 run 终态',
|
|
61
|
+
},
|
|
62
|
+
// 04 §3.7:complete 错误终态上抛(Promise 面错误回到异常形态)
|
|
63
|
+
{
|
|
64
|
+
code: 'LLM_COMPLETE_FAILED',
|
|
65
|
+
module: 'llm',
|
|
66
|
+
description: 'complete 单发补全失败(stopReason=error/aborted 终态或达帽拒绝上抛)',
|
|
67
|
+
},
|
|
68
|
+
// 03 §3.4 钩子段禁模型调用执法码(guard 前置查命中——双入口同码:stream 路
|
|
69
|
+
// errorStream 携带、complete 路 BaseError 抛;词面 02 §5.3 LLM_ 前缀族早已
|
|
70
|
+
// 在册,码册条目随 ca 批执法接线补齐)。non-retryable:钩子段内重试无意义
|
|
71
|
+
// ——违例是结构性的,换时刻(派发收口后)再调即合法
|
|
72
|
+
{
|
|
73
|
+
code: 'LLM_CALL_IN_HOOK',
|
|
74
|
+
module: 'llm',
|
|
75
|
+
description: '钩子派发段内禁模型调用(03 §3.4——起异步任务不等结果合法、await 耦合违法;non-retryable)',
|
|
76
|
+
},
|
|
77
|
+
]);
|