@bolloon/bolloon-agent 0.2.1 → 0.2.3

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 (118) hide show
  1. package/bin/ipfs +0 -0
  2. package/dist/agents/judgment-protocol.js +479 -0
  3. package/dist/agents/parse-tool-call.js +304 -0
  4. package/dist/agents/pi-sdk.js +190 -267
  5. package/dist/agents/react-loop.js +86 -0
  6. package/dist/agents/session-store.js +157 -0
  7. package/dist/agents/tool-registry.js +136 -0
  8. package/dist/agents/workflow-pivot-loop.js +40 -0
  9. package/dist/bollharness/src/index.js +5 -0
  10. package/dist/bollharness/src/scripts/checks/check_adr_plan_numbering.js +6 -0
  11. package/dist/bollharness/src/scripts/checks/check_api_types.js +45 -0
  12. package/dist/bollharness/src/scripts/checks/check_artifact_link.js +146 -0
  13. package/dist/bollharness/src/scripts/checks/check_bridge_deps.js +6 -0
  14. package/dist/bollharness/src/scripts/checks/check_bugfix_binding.js +6 -0
  15. package/dist/bollharness/src/scripts/checks/check_bugfix_binding_ci.js +6 -0
  16. package/dist/bollharness/src/scripts/checks/check_doc_file_references.js +6 -0
  17. package/dist/bollharness/src/scripts/checks/check_doc_freshness.js +135 -0
  18. package/dist/bollharness/src/scripts/checks/check_doc_links.js +31 -0
  19. package/dist/bollharness/src/scripts/checks/check_file_existence_claims.js +6 -0
  20. package/dist/bollharness/src/scripts/checks/check_fragment_integrity.js +34 -0
  21. package/dist/bollharness/src/scripts/checks/check_hook_installed.js +63 -0
  22. package/dist/bollharness/src/scripts/checks/check_issue_closure.js +41 -0
  23. package/dist/bollharness/src/scripts/checks/check_mcp_parity.js +6 -0
  24. package/dist/bollharness/src/scripts/checks/check_security.js +48 -0
  25. package/dist/bollharness/src/scripts/checks/check_skill_parity.js +6 -0
  26. package/dist/bollharness/src/scripts/checks/check_versions.js +6 -0
  27. package/dist/bollharness/src/scripts/checks/finding.js +13 -0
  28. package/dist/bollharness/src/scripts/checks/next_decision_number.js +20 -0
  29. package/dist/bollharness/src/scripts/checks/regenerate_magic_docs.js +6 -0
  30. package/dist/bollharness/src/scripts/ci/detect_rebaseline_triggers.js +8 -0
  31. package/dist/bollharness/src/scripts/ci/scan_subprocess_cfg.js +8 -0
  32. package/dist/bollharness/src/scripts/ci/scan_verify_artifacts.js +8 -0
  33. package/dist/bollharness/src/scripts/ci/scan_yaml_schema.js +8 -0
  34. package/dist/bollharness/src/scripts/context_router.js +67 -0
  35. package/dist/bollharness/src/scripts/deploy-guard.js +157 -0
  36. package/dist/bollharness/src/scripts/guard-feedback.js +192 -0
  37. package/dist/bollharness/src/scripts/guard_router.js +158 -0
  38. package/dist/bollharness/src/scripts/hooks/_hook_output.js +6 -0
  39. package/dist/bollharness/src/scripts/hooks/auto-python3.js +6 -0
  40. package/dist/bollharness/src/scripts/hooks/deploy-progress-on-session-end.js +6 -0
  41. package/dist/bollharness/src/scripts/hooks/failure-analyzer.js +6 -0
  42. package/dist/bollharness/src/scripts/hooks/gate-judgment-inject.js +92 -0
  43. package/dist/bollharness/src/scripts/hooks/gate-transition-judgment.js +63 -0
  44. package/dist/bollharness/src/scripts/hooks/inbox-ack.js +6 -0
  45. package/dist/bollharness/src/scripts/hooks/inbox-inject-on-start.js +6 -0
  46. package/dist/bollharness/src/scripts/hooks/inbox-validate.js +6 -0
  47. package/dist/bollharness/src/scripts/hooks/inbox-write-ledger.js +6 -0
  48. package/dist/bollharness/src/scripts/hooks/initializer-agent.js +6 -0
  49. package/dist/bollharness/src/scripts/hooks/loop-detection.js +73 -0
  50. package/dist/bollharness/src/scripts/hooks/owner-guard.js +6 -0
  51. package/dist/bollharness/src/scripts/hooks/precompact.js +6 -0
  52. package/dist/bollharness/src/scripts/hooks/review-agent-gatekeeper.js +6 -0
  53. package/dist/bollharness/src/scripts/hooks/risk-tracker.js +108 -0
  54. package/dist/bollharness/src/scripts/hooks/sanitize-on-read.js +6 -0
  55. package/dist/bollharness/src/scripts/hooks/session-reflection.js +7 -0
  56. package/dist/bollharness/src/scripts/hooks/session-start-magic-docs.js +7 -0
  57. package/dist/bollharness/src/scripts/hooks/session-start-reset-risk.js +7 -0
  58. package/dist/bollharness/src/scripts/hooks/session-start-toolkit-reminder.js +7 -0
  59. package/dist/bollharness/src/scripts/hooks/stop-evaluator.js +157 -0
  60. package/dist/bollharness/src/scripts/hooks/tool-call-counter.js +6 -0
  61. package/dist/bollharness/src/scripts/hooks/trace-analyzer.js +10 -0
  62. package/dist/bollharness/src/scripts/install/install-trust-token.js +7 -0
  63. package/dist/bollharness/src/scripts/install/multi_project_registry.js +9 -0
  64. package/dist/bollharness/src/scripts/install/phase2_auto.js +21 -0
  65. package/dist/bollharness/src/scripts/install/pre_commit_installer.js +6 -0
  66. package/dist/bollharness/src/scripts/install/tier_selector.js +7 -0
  67. package/dist/bollharness/src/scripts/install/transcript_miner.js +7 -0
  68. package/dist/bollharness/src/scripts/lib/claim_patterns.js +10 -0
  69. package/dist/bollharness/src/scripts/lib/sanitize_patterns.js +12 -0
  70. package/dist/bollharness/src/scripts/sanitize.js +6 -0
  71. package/dist/constraint-runtime/src/tools/PolymarketSDK/cancelOrder.js +6 -0
  72. package/dist/constraint-runtime/src/tools/PolymarketSDK/createOrder.js +6 -0
  73. package/dist/constraint-runtime/src/tools/PolymarketSDK/getMarket.js +5 -0
  74. package/dist/constraint-runtime/src/tools/PolymarketSDK/getOrders.js +6 -0
  75. package/dist/constraint-runtime/src/tools/PolymarketSDK/listMarkets.js +4 -0
  76. package/dist/constraint-runtime/src/tools/SafeSDK/deploySafe.js +6 -0
  77. package/dist/constraint-runtime/src/tools/WalletTools/autoPay.js +29 -0
  78. package/dist/constraint-runtime/src/tools/WalletTools/createWallet.js +10 -0
  79. package/dist/constraint-runtime/src/tools/WalletTools/getBalance.js +13 -0
  80. package/dist/constraint-runtime/src/tools/WalletTools/importWallet.js +22 -0
  81. package/dist/constraint-runtime/src/tools/WalletTools/sendTransaction.js +25 -0
  82. package/dist/constraint-runtime/src/tools/WalletTools/signMessage.js +10 -0
  83. package/dist/constraint-runtime/src/tools/WalletTools/transferToken.js +25 -0
  84. package/dist/electron/config.js.map +1 -0
  85. package/dist/electron/dialogs.js.map +1 -0
  86. package/dist/electron/first-run.js.map +1 -0
  87. package/dist/electron/ipc.js.map +1 -0
  88. package/dist/electron/logger.js.map +1 -0
  89. package/dist/electron/main.js.map +1 -0
  90. package/dist/electron/menu.js.map +1 -0
  91. package/dist/electron/paths.js.map +1 -0
  92. package/dist/electron/server.js +2 -1
  93. package/dist/electron/server.js.map +1 -0
  94. package/dist/electron/tray.js.map +1 -0
  95. package/dist/electron/window.js.map +1 -0
  96. package/dist/electron-preload.js +29 -0
  97. package/dist/electron-preload.js.map +1 -0
  98. package/dist/electron.js +5 -0
  99. package/dist/electron.js.map +1 -0
  100. package/dist/llm/config-store.js +24 -8
  101. package/dist/llm/pi-ai.js +33 -19
  102. package/dist/workflows/collaboration.js +374 -0
  103. package/dist/workflows/index.js +54 -0
  104. package/package.json +18 -4
  105. package/.comm/README.md +0 -21
  106. package/.comm/default/2026-06-17T08-23-00-017Z-8a735de8.md +0 -7
  107. package/CLAUDE.md +0 -3
  108. package/build/icon.icns +0 -0
  109. package/build/icon.ico +0 -0
  110. package/build/icon.png +0 -0
  111. package/build/tray.png +0 -0
  112. package/build/trayTemplate.png +0 -0
  113. package/scripts/auto-evolve-oneshot.sh +0 -155
  114. package/scripts/auto-evolve-snapshot.sh +0 -136
  115. package/scripts/build-cli.js +0 -216
  116. package/scripts/detect-schema-changes.sh +0 -48
  117. package/scripts/lefthook-helper.sh +0 -69
  118. package/scripts/postinstall.js +0 -153
@@ -0,0 +1,86 @@
1
+ /**
2
+ * ReAct Loop 决策层 (decideNext + runStep)
3
+ *
4
+ * 2026-06-30 抽出:
5
+ * - 原 PiAgentSession.runReActLoop 是 1100+ 行的 private 巨石函数, 既管 LLM IO 又管状态机.
6
+ * - 现在抽出"决策下一步"为纯函数, claude code / 外部 harness 可独立消融验证.
7
+ *
8
+ * 决策表 (apply 到 LLM 回复 + 当前 state):
9
+ * 1. reply 是 AI failure sentinel → continue (push error 进 history, 让 LLM 反思)
10
+ * 2. reply 可 parse 出 tool call → execute-tool (调 tool, push result 进 history)
11
+ * 3. reply 含 <final gen> 且无 tool_call → final (LLM 显式终止)
12
+ * 4. reply 无 <final gen>, 无 tool_call → continue (LLM 可能再想想, 让下一轮重新生成)
13
+ *
14
+ * 这个模块不调 LLM, 只接受 reply/state 返回下一步决策. LLM 调用 + 真正状态变更
15
+ * 仍由 PiAgentSession 协调. 这样 claude code 可以独立 unit-test 决策边界.
16
+ */
17
+ import { parseToolCall, isFinalResponse } from './parse-tool-call.js';
18
+ /** 把 LLM 回复归到一种 next action — 纯函数. */
19
+ export function decideNext(ctx) {
20
+ if (ctx.signalAborted) {
21
+ return { kind: 'continue', reason: 'signal-aborted-upstream' };
22
+ }
23
+ if (ctx.atMaxIterations) {
24
+ return { kind: 'final', answer: '(max iterations reached, fail-safe)', reason: 'max-iterations' };
25
+ }
26
+ // AI failure sentinel 优先 (matches pi-sdk.ts 现有顺序 L2558)
27
+ // LLM 偶尔在 retry 中夹杂 sentinel + 工具块, sentinel 是必须先看的"停止信号"
28
+ if (isAiFailureSentinel(ctx.reply)) {
29
+ return { kind: 'continue', reason: 'ai-failure-sentinel' };
30
+ }
31
+ // 工具调用优先 (per 2026-06-19 fix) — 在 sentinel 之后
32
+ const tc = parseToolCall(ctx.reply, { tools: ctx.knownToolNames });
33
+ if (tc) {
34
+ if (!ctx.knownToolNames.has(tc.name)) {
35
+ // 名字解析出来但不在已知集合 — LLM 幻觉高频场景
36
+ return { kind: 'parse-error', reason: `unknown-tool:${tc.name}` };
37
+ }
38
+ return {
39
+ kind: 'execute-tool',
40
+ name: tc.name,
41
+ args: tc.args,
42
+ reason: 'parse-tool-call',
43
+ };
44
+ }
45
+ // final gen 终止
46
+ if (isFinalResponse(ctx.reply)) {
47
+ return { kind: 'final', answer: ctx.reply, reason: 'final-gen-marker' };
48
+ }
49
+ // 默认 — 普通 text, 让 LLM 继续想
50
+ return { kind: 'continue', reason: 'no-tool-no-final' };
51
+ }
52
+ /**
53
+ * isAiFailureSentinel — 检测 LLM 上游失败哨兵.
54
+ * bolloon 用 [AI 服务调用失败] 前缀作为哨兵 — runReActLoop 收到时
55
+ * 把错误当 tool result push 进 history, 让 LLM 下轮能反思.
56
+ */
57
+ export function isAiFailureSentinel(reply) {
58
+ if (!reply)
59
+ return false;
60
+ const trimmed = reply.trim();
61
+ return trimmed.startsWith('[AI 服务调用失败]') ||
62
+ trimmed.startsWith('[AI 调用失败]') ||
63
+ trimmed.startsWith('[错误:');
64
+ }
65
+ /** extractFinalText — 抽 LLM 输出里能呈现给用户的最终文字 (已是 Message 状态). */
66
+ export function extractFinalText(reply, marker = '<final gen>') {
67
+ const idx = reply.indexOf(marker);
68
+ if (idx !== -1) {
69
+ const after = reply.substring(idx + marker.length).trim();
70
+ return after || reply.substring(0, idx).trim();
71
+ }
72
+ return reply;
73
+ }
74
+ /**
75
+ * 错误累计判定 — 当 totalErrors 达阈值时, 给 final 答案 + 汇总.
76
+ * 这是独立的小函数, 因为 runReActLoop 用它决定是否继续.
77
+ */
78
+ export function shouldForceExit(totalErrors, maxTotalErrors) {
79
+ return totalErrors >= maxTotalErrors;
80
+ }
81
+ /**
82
+ * 同一工具连续失败 N 次, 提示 LLM 不要再用同一个工具 (force final).
83
+ */
84
+ export function shouldHintToStopSameTool(consecutiveFails, threshold) {
85
+ return consecutiveFails >= threshold;
86
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * session-store — 持久化 Message[] (LLM 历史) 到 ~/.bolloon/sessions/cache/<key>.json
3
+ *
4
+ * 2026-06-30 抽出:
5
+ * - 之前 PiAgentSession 用 constraint-runtime 的 saveSession (存 string[]) + 独立的 hydrateMessageHistory
6
+ * 读路径, 写路径不存在 — claude code / 外部 harness 想"接续 prompt"完全无入口.
7
+ * - 现在抽出 SessionStore 模块, 暴露纯 IO 接口, 完全可消融测试:
8
+ * saveMessages(key, msgs) — 写
9
+ * loadMessages(key) — 读
10
+ * listKeys() — 列全部 key
11
+ * deleteKey(key) — 清
12
+ * - 任何目录位置都可注入 (默认 ~/.bolloon/sessions/cache/), 测试里指向临时 dir.
13
+ *
14
+ * 使用:
15
+ * import { sessionStore } from './session-store';
16
+ * sessionStore.saveMessages('cli:session-A', messageHistory);
17
+ * const restored = sessionStore.loadMessages('cli:session-A');
18
+ */
19
+ import * as fs from 'fs/promises';
20
+ import * as path from 'path';
21
+ import * as os from 'os';
22
+ export class SessionStore {
23
+ cacheDir;
24
+ constructor(config = {}) {
25
+ this.cacheDir = config.cacheDir ?? path.join(os.homedir(), '.bolloon', 'sessions', 'cache');
26
+ }
27
+ /** 当前缓存目录 (只读). */
28
+ get dir() {
29
+ return this.cacheDir;
30
+ }
31
+ /** 单文件路径 — 暴露出来方便测试和外部读取. */
32
+ pathFor(key) {
33
+ if (!key || key.includes('/') || key.includes('..')) {
34
+ throw new Error(`SessionStore: invalid key ${JSON.stringify(key)}`);
35
+ }
36
+ return path.join(this.cacheDir, `${key}.json`);
37
+ }
38
+ /**
39
+ * 写 history to disk.
40
+ * - 自动 ensure 缓存目录
41
+ * - 失败抛错 (写失败不应静默 — 用户可能依赖持久化作为审计)
42
+ */
43
+ async saveMessages(key, messages) {
44
+ if (!key || key.includes('/') || key.includes('..')) {
45
+ throw new Error(`SessionStore: invalid key ${JSON.stringify(key)}`);
46
+ }
47
+ await fs.mkdir(this.cacheDir, { recursive: true });
48
+ const payload = {
49
+ key,
50
+ messages,
51
+ metadata: {
52
+ savedAt: Date.now(),
53
+ cwd: process.cwd(),
54
+ totalCount: messages.length,
55
+ },
56
+ };
57
+ const filePath = this.pathFor(key);
58
+ // 写临时文件 + rename — 防止半写损坏
59
+ const tmpPath = `${filePath}.tmp`;
60
+ await fs.writeFile(tmpPath, JSON.stringify(payload, null, 2), 'utf-8');
61
+ await fs.rename(tmpPath, filePath);
62
+ }
63
+ /**
64
+ * 同步版 (PiAgentSession 里多条路径用同步 — 避免 async 链)
65
+ * 失败抛错
66
+ */
67
+ saveMessagesSync(key, messages) {
68
+ if (!key || key.includes('/') || key.includes('..')) {
69
+ throw new Error(`SessionStore: invalid key ${JSON.stringify(key)}`);
70
+ }
71
+ fsSyncMkdir(this.cacheDir);
72
+ const payload = {
73
+ key,
74
+ messages,
75
+ metadata: {
76
+ savedAt: Date.now(),
77
+ cwd: process.cwd(),
78
+ totalCount: messages.length,
79
+ },
80
+ };
81
+ const filePath = this.pathFor(key);
82
+ const tmpPath = `${filePath}.tmp`;
83
+ fsSyncWrite(tmpPath, JSON.stringify(payload, null, 2));
84
+ fsSyncRename(tmpPath, filePath);
85
+ }
86
+ /**
87
+ * 读 history from disk.
88
+ * - key 不存在 → 返回 null (不抛错)
89
+ * - 文件存在但 JSON 损坏 → 抛错 (claude code 应该感知坏数据)
90
+ * - 文件 schema 不匹配 (新版格式) → 抛错含 details
91
+ */
92
+ async loadMessages(key) {
93
+ if (!key || key.includes('/') || key.includes('..')) {
94
+ throw new Error(`SessionStore: invalid key ${JSON.stringify(key)}`);
95
+ }
96
+ const filePath = this.pathFor(key);
97
+ let raw;
98
+ try {
99
+ raw = await fs.readFile(filePath, 'utf-8');
100
+ }
101
+ catch (e) {
102
+ if (e?.code === 'ENOENT')
103
+ return null;
104
+ throw e;
105
+ }
106
+ const parsed = JSON.parse(raw);
107
+ if (!parsed || !Array.isArray(parsed.messages)) {
108
+ throw new Error(`SessionStore: ${key} schema invalid (missing messages[])`);
109
+ }
110
+ return parsed.messages;
111
+ }
112
+ /** 列所有 keys — 用于调试 / claude code "看有哪些 session 可接续". */
113
+ async listKeys() {
114
+ try {
115
+ await fs.mkdir(this.cacheDir, { recursive: true });
116
+ const files = await fs.readdir(this.cacheDir);
117
+ return files
118
+ .filter(f => f.endsWith('.json') && !f.endsWith('.tmp'))
119
+ .map(f => f.slice(0, -'.json'.length))
120
+ .sort();
121
+ }
122
+ catch {
123
+ return [];
124
+ }
125
+ }
126
+ /** 删一条 session — 失败抛错. */
127
+ async deleteKey(key) {
128
+ if (!key || key.includes('/') || key.includes('..')) {
129
+ throw new Error(`SessionStore: invalid key ${JSON.stringify(key)}`);
130
+ }
131
+ try {
132
+ await fs.unlink(this.pathFor(key));
133
+ }
134
+ catch (e) {
135
+ if (e?.code !== 'ENOENT')
136
+ throw e;
137
+ }
138
+ }
139
+ }
140
+ // --- sync fs helpers (避免 import 'fs' 二次) ---
141
+ function fsSyncMkdir(dir) {
142
+ try {
143
+ require('fs').mkdirSync(dir, { recursive: true });
144
+ }
145
+ catch (e) {
146
+ if (e?.code !== 'EEXIST')
147
+ throw e;
148
+ }
149
+ }
150
+ function fsSyncWrite(filePath, content) {
151
+ require('fs').writeFileSync(filePath, content, 'utf-8');
152
+ }
153
+ function fsSyncRename(from, to) {
154
+ require('fs').renameSync(from, to);
155
+ }
156
+ /** 默认 store 实例 (用 ~/.bolloon/sessions/cache/). 业务代码用这个. */
157
+ export const sessionStore = new SessionStore();
@@ -0,0 +1,136 @@
1
+ /**
2
+ * ToolRegistry — 工具注册表 + 别名解析器
3
+ *
4
+ * 2026-06-30 抽出:
5
+ * - 原 PiAgentSession.tools Map + resolveToolName 是 private 方法, 测试无法直接验证 alias 映射
6
+ * - 现在抽到独立模块, 完全可消融测试.
7
+ *
8
+ * 设计目标:
9
+ * 1. claude code / 外部 harness 直接 import, 用同一份 alias 表
10
+ * 2. 注册/反注册/查表都是纯方法, 无副作用
11
+ * 3. 默认 alias 表跟 parse-tool-call.ts 保持一致 (read→read_file, bash→shell_exec 等)
12
+ * 4. 自定义 alias 可注入 (业务可扩)
13
+ *
14
+ * 用法:
15
+ * import { ToolRegistry, DEFAULT_ALIASES } from './tool-registry';
16
+ * const reg = new ToolRegistry();
17
+ * reg.register({ name: 'shell_exec', description: '...', parameters: {}, execute });
18
+ * reg.has('bash'); // true (alias 命中)
19
+ * reg.resolve('bash'); // 'shell_exec'
20
+ * reg.get('shell_exec')?.execute({ command: 'ls' });
21
+ */
22
+ /** 默认 alias 表 — Claude Code 风格 → bolloon 工具名 */
23
+ export const DEFAULT_ALIASES = Object.freeze({
24
+ // file ops
25
+ read: 'read_file',
26
+ edit: 'edit_file',
27
+ write: 'write_file',
28
+ rm: 'delete_file',
29
+ mv: 'move_file',
30
+ cat: 'read_file',
31
+ // shell
32
+ bash: 'shell_exec',
33
+ shell: 'shell_exec',
34
+ sh: 'shell_exec',
35
+ // test/build
36
+ test: 'vitest_run',
37
+ vitest: 'vitest_run',
38
+ typecheck: 'tsc_check',
39
+ tsc: 'tsc_check',
40
+ // git
41
+ log: 'git_log',
42
+ show: 'git_show',
43
+ diff: 'git_diff',
44
+ commit: 'git_commit',
45
+ push: 'git_push',
46
+ branch: 'git_branch',
47
+ checkout: 'git_branch',
48
+ stash: 'git_stash',
49
+ // task
50
+ todo_write: 'create_task',
51
+ todowrite: 'create_task',
52
+ task: 'create_task',
53
+ });
54
+ export class ToolRegistry {
55
+ tools = new Map();
56
+ aliases;
57
+ constructor(aliases = DEFAULT_ALIASES) {
58
+ this.aliases = new Map();
59
+ for (const [k, v] of Object.entries(aliases)) {
60
+ this.aliases.set(k.toLowerCase(), v);
61
+ }
62
+ }
63
+ /** 注册一个 tool (alias 字典不动) */
64
+ register(tool) {
65
+ if (!tool.name)
66
+ throw new Error('ToolRegistry.register: tool.name 必填');
67
+ this.tools.set(tool.name, tool);
68
+ }
69
+ /** 批量注册 */
70
+ registerAll(tools) {
71
+ for (const t of tools)
72
+ this.register(t);
73
+ }
74
+ /** 反注册 — 名字 + 对应 alias (如果有指向它) 全部清掉. */
75
+ unregister(name) {
76
+ const existed = this.tools.delete(name);
77
+ // 反向清 alias
78
+ for (const [alias, target] of this.aliases.entries()) {
79
+ if (target === name)
80
+ this.aliases.delete(alias);
81
+ }
82
+ return existed;
83
+ }
84
+ /** 查 tool (只查主名, alias 走 resolve). */
85
+ get(name) {
86
+ return this.tools.get(name);
87
+ }
88
+ /** 是否注册 (主名 + alias 命中都算). */
89
+ has(name) {
90
+ if (this.tools.has(name))
91
+ return true;
92
+ const aliased = this.aliases.get(name.toLowerCase());
93
+ return aliased !== undefined && this.tools.has(aliased);
94
+ }
95
+ /** 把 LLM 输出的名字 (可能是 alias / 大小写不一致) 解析为标准主名. */
96
+ resolve(name) {
97
+ if (this.tools.has(name))
98
+ return name;
99
+ const lower = name.toLowerCase();
100
+ const aliased = this.aliases.get(lower);
101
+ if (aliased && this.tools.has(aliased))
102
+ return aliased;
103
+ if (this.tools.has(lower))
104
+ return lower;
105
+ return null;
106
+ }
107
+ /** 列出所有主名. 顺序按注册时间. */
108
+ list() {
109
+ return Array.from(this.tools.keys());
110
+ }
111
+ /** 当前 alias 表快照 (调试用). */
112
+ get aliasMap() {
113
+ const out = {};
114
+ for (const [k, v] of this.aliases.entries())
115
+ out[k] = v;
116
+ return out;
117
+ }
118
+ /** 个数 */
119
+ get size() {
120
+ return this.tools.size;
121
+ }
122
+ /** 清空 */
123
+ clear() {
124
+ this.tools.clear();
125
+ // alias 表保留 (除非调用方显式 clear, alias 表是稳定的)
126
+ }
127
+ /** 调一个 tool, 名字可能是 alias. throw if 未知. */
128
+ async invoke(name, args) {
129
+ const canonical = this.resolve(name);
130
+ if (!canonical) {
131
+ throw new Error(`ToolRegistry.invoke: 未知工具名 ${JSON.stringify(name)}`);
132
+ }
133
+ const tool = this.tools.get(canonical);
134
+ return tool.execute(args);
135
+ }
136
+ }
@@ -173,6 +173,21 @@ export class WorkflowPivotLoop {
173
173
  // Check if this is a final response (no tool calls)
174
174
  const pendingTools = this.extractPendingToolUses(reply);
175
175
  if (pendingTools.length === 0) {
176
+ // Check if the reply contains tool call intent but couldn't be parsed
177
+ const containsToolCallIntent = reply.includes('调用工具') || reply.includes('tool(') ||
178
+ reply.includes('使用工具') || reply.includes('需要获取') || reply.includes('需要查看') ||
179
+ reply.includes('tool =>') || reply.includes('[TOOL_CALL]') ||
180
+ /<\w+>[\s\S]*?<\/\w+>/.test(reply);
181
+ // If there's tool call intent but no parsed tools, continue the loop
182
+ if (containsToolCallIntent && this.state.iteration < effectiveConfig.maxIterations) {
183
+ this.emit({
184
+ type: 'status',
185
+ content: `🔄 检测到工具调用意图但格式无法解析,继续循环...`,
186
+ tool: 'system'
187
+ });
188
+ this.state.consecutiveNoProgress++;
189
+ continue;
190
+ }
176
191
  // No pending tool uses - this is a normal completion
177
192
  this.state.pendingToolUses = [];
178
193
  // Evaluate quality before accepting
@@ -447,6 +462,31 @@ export class WorkflowPivotLoop {
447
462
  catch {
448
463
  // JSON parsing failed, ignore
449
464
  }
465
+ // Pattern 4: Single JSON tool call format {"name": "tool_name", "arguments": {...}}
466
+ try {
467
+ const singleJsonRe = /\{\s*"name"\s*:\s*"(\w+)"\s*,\s*"arguments"\s*:\s*(\{[\s\S]*?\})\s*\}/g;
468
+ let singleMatch;
469
+ while ((singleMatch = singleJsonRe.exec(content)) !== null) {
470
+ const name = singleMatch[1];
471
+ if (pending.some(p => p.name === name))
472
+ continue;
473
+ if (!this.tools.has(name))
474
+ continue;
475
+ try {
476
+ const args = JSON.parse(singleMatch[2]);
477
+ if (args && typeof args === 'object') {
478
+ const normalizedArgs = this.normalizeArgs(args);
479
+ pending.push({ name, args: normalizedArgs, description: '', parameters: {} });
480
+ }
481
+ }
482
+ catch {
483
+ // JSON parsing failed, skip
484
+ }
485
+ }
486
+ }
487
+ catch {
488
+ // Pattern matching failed, ignore
489
+ }
450
490
  return pending;
451
491
  }
452
492
  /**
@@ -0,0 +1,5 @@
1
+ export { createFinding } from "./scripts/checks/finding";
2
+ export { route, runGuards, writeSessionSignal, readAllSignals, GUARD_MAP, DEFAULT_GUARDS, CATEGORY_TO_SKILLS } from "./scripts/guard_router";
3
+ export { match, loadFragment, FALLBACK_FRAGMENTS, CONTEXT_MAP } from "./scripts/context_router";
4
+ export { main as guardFeedbackMain } from "./scripts/guard-feedback";
5
+ export { main as deployGuardMain } from "./scripts/deploy-guard";
@@ -0,0 +1,6 @@
1
+ export function run(repoRoot, mode = "full") {
2
+ const findings = [];
3
+ if (mode !== "full" && mode !== "ci")
4
+ return findings;
5
+ return findings;
6
+ }
@@ -0,0 +1,45 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ export function run(repoRoot, mode = "full") {
4
+ const findings = [];
5
+ const tsFile = path.join(repoRoot, "generated", "api-types.ts");
6
+ if (!fs.existsSync(tsFile)) {
7
+ findings.push({
8
+ severity: "P1",
9
+ message: "generated/api-types.ts does not exist — run scripts/export_api_types.py",
10
+ file: "generated/api-types.ts",
11
+ blocking: false,
12
+ category: "general",
13
+ problem_class: "unknown",
14
+ required_skills: [],
15
+ required_reads: [],
16
+ });
17
+ return findings;
18
+ }
19
+ const content = fs.readFileSync(tsFile, "utf-8");
20
+ const interfaceMatch = content.match(/export interface (\w+) \{/g);
21
+ if (!interfaceMatch) {
22
+ findings.push({
23
+ severity: "P1",
24
+ message: "generated/api-types.ts contains no interfaces",
25
+ file: "generated/api-types.ts",
26
+ blocking: false,
27
+ category: "general",
28
+ problem_class: "unknown",
29
+ required_skills: [],
30
+ required_reads: [],
31
+ });
32
+ return findings;
33
+ }
34
+ findings.push({
35
+ severity: "P2",
36
+ message: "API type checking requires backend model comparison (Python 3.10+ backend not available in TypeScript check)",
37
+ file: "generated/api-types.ts",
38
+ blocking: false,
39
+ category: "general",
40
+ problem_class: "unknown",
41
+ required_skills: [],
42
+ required_reads: [],
43
+ });
44
+ return findings;
45
+ }
@@ -0,0 +1,146 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { execSync } from "child_process";
4
+ const CODE_EXTENSIONS = new Set([".py", ".ts", ".tsx", ".js", ".jsx"]);
5
+ const CONFIG_EXTENSIONS = new Set([".json", ".yml", ".yaml", ".toml"]);
6
+ const TEST_PREFIXES = ["tests/", "test_"];
7
+ const ARTIFACT_PREFIXES = ["docs/issues/", "docs/decisions/"];
8
+ const BUILD_DIRS = new Set([".open-next", "dist", "build", ".next", "node_modules"]);
9
+ function isCodeFile(filePath) {
10
+ const p = path.parse(filePath);
11
+ if (!CODE_EXTENSIONS.has(p.ext))
12
+ return false;
13
+ const parts = p.dir.split(path.sep);
14
+ for (const part of parts) {
15
+ if (part.startsWith("test_") || part === "tests")
16
+ return false;
17
+ }
18
+ for (const part of parts) {
19
+ if (BUILD_DIRS.has(part))
20
+ return false;
21
+ }
22
+ return true;
23
+ }
24
+ function isArtifactFile(filePath) {
25
+ return ARTIFACT_PREFIXES.some(prefix => filePath.startsWith(prefix)) && filePath.endsWith(".md");
26
+ }
27
+ function extractScope(artifactPath) {
28
+ try {
29
+ const text = fs.readFileSync(artifactPath, "utf-8");
30
+ if (!text.startsWith("---"))
31
+ return [];
32
+ const end = text.indexOf("\n---", 3);
33
+ if (end === -1)
34
+ return [];
35
+ const frontmatter = text.slice(3, end);
36
+ const scopes = [];
37
+ let inScope = false;
38
+ for (const line of frontmatter.split("\n")) {
39
+ const stripped = line.trim();
40
+ if (stripped.startsWith("scope:")) {
41
+ const value = stripped.slice(6).trim();
42
+ if (value && !value.startsWith("[")) {
43
+ scopes.push(value);
44
+ inScope = false;
45
+ }
46
+ else if (value.startsWith("[")) {
47
+ const inner = value.replace(/[\[\] ]/g, "");
48
+ scopes.push(...inner.split(",").filter(s => s));
49
+ inScope = false;
50
+ }
51
+ else {
52
+ inScope = true;
53
+ }
54
+ continue;
55
+ }
56
+ if (inScope) {
57
+ if (stripped.startsWith("- ")) {
58
+ scopes.push(stripped.slice(2).trim().replace(/^["']|["']$/g, ""));
59
+ }
60
+ else if (stripped && !stripped.startsWith("#")) {
61
+ inScope = false;
62
+ }
63
+ }
64
+ }
65
+ return scopes.filter(s => s);
66
+ }
67
+ catch {
68
+ return [];
69
+ }
70
+ }
71
+ function checkScopeBinding(codeFiles, artifactFiles, repoRoot) {
72
+ const allScopes = [];
73
+ for (const af of artifactFiles) {
74
+ allScopes.push(...extractScope(path.join(repoRoot, af)));
75
+ }
76
+ if (allScopes.length === 0)
77
+ return [];
78
+ const uncovered = [];
79
+ for (const cf of codeFiles) {
80
+ if (!allScopes.some(scope => cf.startsWith(scope))) {
81
+ uncovered.push(cf);
82
+ }
83
+ }
84
+ if (uncovered.length === 0)
85
+ return [];
86
+ return [{
87
+ severity: "P2",
88
+ message: `Scope binding gap: ${uncovered.length} code file(s) not covered by any artifact scope. Uncovered: ${uncovered.slice(0, 5).join(", ")}${uncovered.length > 5 ? ` (and ${uncovered.length - 5} more)` : ""}. Add a scope field to the relevant artifact doc.`,
89
+ file: uncovered[0],
90
+ blocking: false,
91
+ category: "scope_binding",
92
+ problem_class: "unknown",
93
+ required_skills: ["lead"],
94
+ required_reads: [],
95
+ }];
96
+ }
97
+ function getChangedFiles(mode, repoRoot) {
98
+ if (mode === "staged") {
99
+ try {
100
+ const result = execSync("git diff --cached --name-only", { cwd: repoRoot, encoding: "utf-8" });
101
+ return result.split("\n").map(l => l.trim()).filter(l => l);
102
+ }
103
+ catch {
104
+ return [];
105
+ }
106
+ }
107
+ else if (mode === "ci") {
108
+ try {
109
+ const result = execSync("git diff origin/main..HEAD --name-only", { cwd: repoRoot, encoding: "utf-8" });
110
+ return result.split("\n").map(l => l.trim()).filter(l => l);
111
+ }
112
+ catch {
113
+ return [];
114
+ }
115
+ }
116
+ return [];
117
+ }
118
+ export function run(repoRoot, mode = "full") {
119
+ if (mode === "full")
120
+ return [];
121
+ const files = getChangedFiles(mode, repoRoot);
122
+ if (!files.length)
123
+ return [];
124
+ const codeFiles = files.filter(f => isCodeFile(f));
125
+ const artifactFiles = files.filter(f => isArtifactFile(f));
126
+ const hasCode = codeFiles.length > 0;
127
+ const hasArtifact = artifactFiles.length > 0;
128
+ const findings = [];
129
+ if (hasCode && !hasArtifact) {
130
+ findings.push({
131
+ severity: "P1",
132
+ message: `Code changes without artifact documentation. ${codeFiles.length} code file(s) changed but no docs/issues/ or docs/decisions/ file included. Add an issue or decision doc.`,
133
+ file: codeFiles[0],
134
+ blocking: true,
135
+ category: "artifact_linkage",
136
+ problem_class: "unknown",
137
+ required_skills: ["lead"],
138
+ required_reads: [],
139
+ });
140
+ return findings;
141
+ }
142
+ if (hasCode && hasArtifact) {
143
+ findings.push(...checkScopeBinding(codeFiles, artifactFiles, repoRoot));
144
+ }
145
+ return findings;
146
+ }
@@ -0,0 +1,6 @@
1
+ export function run(repoRoot, mode = "full") {
2
+ const findings = [];
3
+ if (mode !== "full" && mode !== "ci")
4
+ return findings;
5
+ return findings;
6
+ }
@@ -0,0 +1,6 @@
1
+ export function run(repoRoot, mode = "full") {
2
+ const findings = [];
3
+ if (mode !== "full" && mode !== "ci")
4
+ return findings;
5
+ return findings;
6
+ }
@@ -0,0 +1,6 @@
1
+ export function run(repoRoot, mode = "full") {
2
+ const findings = [];
3
+ if (mode !== "full" && mode !== "ci")
4
+ return findings;
5
+ return findings;
6
+ }
@@ -0,0 +1,6 @@
1
+ export function run(repoRoot, mode = "full") {
2
+ const findings = [];
3
+ if (mode !== "full" && mode !== "ci")
4
+ return findings;
5
+ return findings;
6
+ }