@genee/omp-opsx-addon 0.2.0 → 0.3.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/index.ts CHANGED
@@ -121,6 +121,7 @@ import { DIRECT_FETCHERS } from './lib/direct-fetchers.js';
121
121
  import { createUsageWidget } from './lib/usage-widget.js';
122
122
  import { renderUsageReports, type ConsumptionTrack } from './lib/usage-render.js';
123
123
  import { markExhausted, markAvailable, getExhaustedProviders } from './lib/provider-status.js';
124
+ import { scheduleStatusProbes } from './lib/status-probe.js';
124
125
  import {
125
126
  startSharedPoller,
126
127
  getUnconfirmedProviders,
@@ -794,9 +795,37 @@ export default (pi: ExtensionAPI) => {
794
795
  // provider's aggregated consumption RATE (all sessions, main +
795
796
  // subagent via cache backflow), and re-renders the widget. It never
796
797
  // recomputes a selection, never applies overrides, never setModel.
798
+ const cfg = buildEffectiveConfig();
797
799
  const onTick = () => {
800
+ // Fire-and-forget status probe: do not block the tick.
801
+ const { placeholderIds, active } = resolveActiveProviders();
802
+ let models: { id: string; provider: string; baseUrl?: string }[] = [];
803
+ try {
804
+ if (ctx.modelRegistry && typeof ctx.modelRegistry.getAvailable === 'function') {
805
+ models = ctx.modelRegistry.getAvailable() as { id: string; provider: string; baseUrl?: string }[];
806
+ }
807
+ } catch {
808
+ if (ctx.modelRegistry && typeof ctx.modelRegistry.getAll === 'function') {
809
+ try {
810
+ models = ctx.modelRegistry.getAll() as { id: string; provider: string; baseUrl?: string }[];
811
+ } catch {
812
+ models = [];
813
+ }
814
+ }
815
+ }
816
+ void scheduleStatusProbes({
817
+ placeholderIds,
818
+ models,
819
+ getApiKey,
820
+ config: {
821
+ enabled: cfg.status_probe_enabled,
822
+ ttlMs: cfg.probe_ttl_ms,
823
+ timeoutMs: cfg.probe_timeout_ms,
824
+ excluded: cfg.excluded_providers,
825
+ },
826
+ }).then((written) => { if (written > 0) render(); }).catch(() => {});
827
+
798
828
  sessionUsageRecorder.flush();
799
- const { active } = resolveActiveProviders();
800
829
  // Waveform source = per-second consumption sequence (timestamped
801
830
  // deltas aggregated across sessions). Sample EVERY elapsed second
802
831
  // for EVERY active provider — zeros when a second had no
@@ -822,7 +851,6 @@ export default (pi: ExtensionAPI) => {
822
851
  lastSampledSec = lastSec;
823
852
  render();
824
853
  };
825
- const cfg = buildEffectiveConfig();
826
854
  await startSharedPoller(auth, DIRECT_FETCHERS, getApiKey, {
827
855
  onTick: claimsOwnership ? onTick : undefined,
828
856
  ownsUi,
package/lib/agent-defs.ts CHANGED
@@ -14,6 +14,12 @@ tools: read, bash, glob, grep${model ? `\nmodel: ${model}` : ''}
14
14
  1. 从 prompt 或上下文确定要审查的变更
15
15
  2. 读取 proposal、design 和 tasks 文件
16
16
  3. 运行 \`git diff HEAD~1\` 或 \`git diff --stat\` 查看改动
17
+ ## scratchpad.md 共享缓存
18
+ - 审查前 MUST 先读 \`openspec/changes/<name>/scratchpad.md\`(若存在),只读不改写(工具集无 write/edit)。
19
+ - 以「调研与设计」+「实现探索」两区累计的涉及文件 \`path:line\` 集合为代码审查与增量验证范围:Tier 1(typecheck/lint)与 Tier 3(E2E,如适用)聚焦涉及文件改动面;Tier 2 冒烟套件仍全量执行项目现有测试套件。
20
+ - 无 scratchpad.md 时回退:从 proposal/design/tasks 确定审查范围。
21
+ - 识别 \`- [R<n> coder] supersede:\` 条目:以紧随其后的新结论为权威,旧 \`path:line\` 不再作为验证范围;只读不改写。
22
+ - 发现 scratchpad.md 结论与代码现状不符时,作为 P0/P1 审查发现发回,不直接改写。
17
23
 
18
24
  ## 审查维度
19
25
  1. **规范遵循**:是否精确实现了 proposal/design/tasks 定义的内容?有无缺失或偏离?
@@ -73,6 +79,11 @@ tools: read, bash, glob, grep${model ? `\nmodel: ${model}` : ''}
73
79
  ## 启动
74
80
  1. 从 prompt 或上下文确定要审查的提案名称
75
81
  2. 读取 proposal.md、design.md(如果存在)
82
+ ## scratchpad.md 共享缓存
83
+ - 审查前 MUST 先读 \`openspec/changes/<name>/scratchpad.md\`(若存在),复用其中调研结论与涉及文件,避免重复 read/grep。
84
+ - 只读,不改写 scratchpad.md;你的工具集不含 write/edit,物理上无法写入。
85
+ - 识别 \`- [R<n> coder] supersede:\` 条目:以紧随其后的新结论为权威,旧结论不再作为调研依据;只读不改写。
86
+ - 发现的 P0/P1 与关注点写入审查报告,由主 agent 中转回流,不直接写 scratchpad.md。
76
87
 
77
88
  ## 审查维度
78
89
  1. **完整性**:Why/What/Capabilities/Impact 是否齐全?有无缺失章节?
@@ -131,6 +142,19 @@ skill: openspec-apply-change
131
142
  - 代码全部完成后将所有未完成项标记为 \`- [x]\`,在 ACTION 报告中说明变更已全部实现
132
143
  - 不修改 \`proposal.md\`、\`design.md\` 或 \`.openspec.yaml\`
133
144
  - 直接用 read/write/edit/bash 等内置工具实现;不要尝试调用 task 二次委派。
145
+ ## scratchpad.md 共享缓存
146
+ - 开工前 MUST 读 \`openspec/changes/<name>/scratchpad.md\`(若存在),复用已记录结论,禁止重复探索。
147
+ - 交付前 MUST 将本轮新探索结论 append 到「实现探索」区(关键符号/数据流、新增涉及文件 \`path:line\`、验证/构建命令、已排除假设),标注 \`- [R<n> coder] <结论>\`。
148
+ - 收到主 agent 中转的 code-reviewer P0/P1 关注点时,开工时先将关注点 append 进「代码审查范围」区,再开始修复。
149
+ - 后续轮次只做增量:仅探索未记录的文件/符号/假设,不重复 read/grep 已记录内容;append-only,不得改写他人结论。
150
+
151
+ ## scratchpad.md 与代码不一致时的两档处置
152
+ 执行中发现代码现实与 scratchpad.md 记录不一致时,按两档客观判据处置;判据仅为「偏差是否影响任何 task 的前提或产出定义」,禁止主观估量「问题大小」:
153
+ - **档一:事实快照过期**——偏差不影响任何 task 的前提或产出定义(纯探索性信息:路径 / 行号 / 符号 / 命令漂移)。处置:继续执行当前任务,不终止;在「实现探索」区 append supersede 修正。
154
+ - **档二:契约动摇**——偏差导致 tasks / proposal / design / specs 的有效性存疑(前提不成立、产出定义变、代码现状与设计决策冲突)。处置:立即停止实现,输出 STATUS: blocked(无 SESSION)并说明冲突点,交由主 agent 裁决(改提案 / 确认「现状即新设计」/ 开新 change)。
155
+ - supersede 标注格式:\`- [R<n> coder] supersede: <旧结论摘要>\`,随后一行以 \`- [R<n> coder]\` 标注新结论;两条均落「实现探索」分区,旧结论保留(append-only),不删除。
156
+ - 交付摘要 SUMMARY 义务:本轮发生过 supersede 时,输出格式中的 SUMMARY 行 MUST 提及本次 supersede 清单。
157
+
134
158
  ## 验证(交付门槛)
135
159
  - 交付前必须跑过 **scoped 到本次改动面** 的 lint、typecheck 与针对性单测(proposal 约定的测试必须通过);任一未通过不得交付,先修到通过再标记 ACTION: REVIEW_REQUIRED。
136
160
  - 项目级全量校验(全仓库 lint/typecheck/\`bun test\`)不属于自验证门槛:若失败可归因于并发 sibling 的半成品改动(与本改动无关),把失败连同「已排查与本改动无关」的证据作为上下文上报(交付摘要中注明),不要被其阻塞;最终由 code-reviewer 的全局验证兜底确认。
@@ -148,6 +172,7 @@ ACTION: REVIEW_REQUIRED
148
172
  CHANGE: <change-name 或 "general">
149
173
  STATUS: success | partial | blocked
150
174
  SESSION: <session_name>(仅 STATUS: blocked 且需 resume 时填写)
175
+ SUMMARY: <交付摘要;若本轮有 supersede,必须包含「supersede: <清单>」>
151
176
  TASKS: N/M complete
152
177
  FILES: <逗号分隔的修改/创建文件列表>
153
178
  ---
@@ -173,6 +198,17 @@ tools: read, write, edit, bash, glob, grep, todo${model ? `\nmodel: ${model}` :
173
198
  ## 工作范围
174
199
  - **只负责提案文档**:proposal.md、design.md、tasks.md、specs/(openspec/changes/ 目录下)
175
200
  - 直接用内置工具;不要尝试调用 task 二次委派。
201
+ ## scratchpad.md 共享缓存
202
+ 变更目录下维护 \`openspec/changes/<name>/scratchpad.md\` 作四角色共享探索缓存(与 proposal.md/design.md/tasks.md 同级),固定四阶段分区:
203
+ - \`## 调研与设计\`:调研结论、涉及文件(\`path:line\`)、已排除方案
204
+ - \`## 提案审查关注点\`:proposal-reviewer 审查报告中的 P0/P1 与关注点(经主 agent 中转回流)
205
+ - \`## 实现探索\`:coder 每轮 append 的关键符号/数据流、新增涉及文件、验证/构建命令、已排除假设
206
+ - \`## 代码审查范围\`:code-reviewer 审查报告中的 P0/P1 与验证范围结论(经主 agent 中转回流)
207
+ 规则:
208
+ - **出提案时 MUST 创建** \`openspec/changes/<name>/scratchpad.md\` 并写入四阶段骨架,预填「调研与设计」初始骨架:调研结论、涉及文件、已排除方案。
209
+ - 后续轮次修改提案时,先读回 scratchpad.md:将 proposal-reviewer 关注点 append 进「提案审查关注点」区,新调研结论增量 append 进「调研与设计」区。
210
+ - 条目以 \`- [R<n> planner] <结论>\` 标注轮次与角色;append-only,不得改写或删除他人结论。
211
+ - 读回 scratchpad.md 时,以最新 supersede 条目为权威(识别 \`- [R<n> coder] supersede:\` 标记,其后的新结论优先于旧结论)。
176
212
 
177
213
  ## 提案拆分
178
214
  - 接大需求先评估契约边界:能拆则拆成多个小变更,先输出拆分建议(契约边界+提案清单)待主 agent 确认后再开写。
@@ -183,7 +183,7 @@ export interface HttpProbeOptions {
183
183
  }
184
184
 
185
185
  /** 区域封锁判定的关键词 */
186
- const REGION_BLOCK_TOKENS = ['region', 'not available', 'in your area'];
186
+ export const REGION_BLOCK_TOKENS = ['region', 'not available', 'in your area'];
187
187
 
188
188
  /**
189
189
  * 对 TLS 可达的端点发送最小 HTTP 请求(1 token chat completion),
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Provider 状态探测模块。
3
+ *
4
+ * 对无 usage fetcher 的 provider(如 ArkCodingPlan)主动发最小探测请求,
5
+ * 区分「429 类错误 vs 正常模型」,驱动底部 widget「可用/耗尽」二元态。
6
+ *
7
+ * 探测是 fire-and-forget 异步操作,绝不阻塞渲染路径。结果写入 provider-status.ts
8
+ * 的进程内状态机(exhausted 集合),渲染自动读取。
9
+ *
10
+ * 探测分类:
11
+ * - ok(2xx)→ markAvailable(清除耗尽)
12
+ * - exhausted(429,或 403 非 region)→ markExhausted
13
+ * - region-blocked(403 + region 关键词)→ markExhausted(归耗尽显示)
14
+ * - auth-error(401)/ model-not-found(404)/ network-error(异常/超时)→ 不写状态
15
+ * (宁缺毋谎:无法区分「provider 真不可用」与「探测请求构造失真」)
16
+ *
17
+ * Cooldown ledger:进程内 Map<canonicalProvider, lastProbedAt>,复用 probe_ttl_ms。
18
+ * markStatusProbeStarted 在任何 await 之前写入,彻底关死 in-flight 竞态窗口。
19
+ */
20
+
21
+ import { REGION_BLOCK_TOKENS } from './reachability-probe.js';
22
+ import { canonicalizeProvider } from './usage-resolver.js';
23
+ import { markExhausted, markAvailable } from './provider-status.js';
24
+
25
+ // ── 类型 ────────────────────────────────────────────────────────────────
26
+
27
+ /** 状态探测的分类结果。 */
28
+ export type StatusProbeClass =
29
+ | 'ok' // 2xx:真实模型、真实 key,请求走通
30
+ | 'exhausted' // 429,或 403 非 region 文本
31
+ | 'region-blocked' // 403 + region 关键词
32
+ | 'auth-error' // 401
33
+ | 'model-not-found' // 404
34
+ | 'network-error'; // fetch 抛异常 / 超时
35
+
36
+ /** 状态探测结果。 */
37
+ export interface StatusProbeResult {
38
+ provider: string;
39
+ class: StatusProbeClass;
40
+ detail?: string;
41
+ probedAt: number;
42
+ }
43
+
44
+ /** 网络探测选项。 */
45
+ export interface StatusProbeOptions {
46
+ baseUrl: string;
47
+ model: string;
48
+ apiKey: string;
49
+ timeoutMs: number;
50
+ fetchImpl?: typeof fetch;
51
+ }
52
+
53
+ /** 调度配置。 */
54
+ export interface ScheduleConfig {
55
+ enabled: boolean;
56
+ ttlMs: number;
57
+ timeoutMs: number;
58
+ excluded: string[];
59
+ }
60
+
61
+ /** 调度选项。 */
62
+ export interface ScheduleOptions {
63
+ placeholderIds: string[];
64
+ models: { id: string; provider: string; baseUrl?: string }[];
65
+ getApiKey?: (provider: string) => Promise<string | undefined>;
66
+ config: ScheduleConfig;
67
+ fetchImpl?: typeof fetch;
68
+ }
69
+
70
+ // ── 纯分类函数 ─────────────────────────────────────────────────────────
71
+
72
+ /**
73
+ * 纯分类:HTTP 状态 + 响应体文本 → 分类。
74
+ * 无 IO,直接单测。
75
+ */
76
+ export function classifyStatusResponse(status: number, bodyText: string): StatusProbeClass {
77
+ // 2xx → ok
78
+ if (status >= 200 && status < 300) {
79
+ return 'ok';
80
+ }
81
+ // 429 → exhausted
82
+ if (status === 429) {
83
+ return 'exhausted';
84
+ }
85
+ // 403 + region 关键词 → region-blocked,否则 exhausted
86
+ if (status === 403) {
87
+ const lower = bodyText.toLowerCase();
88
+ if (REGION_BLOCK_TOKENS.some((tok) => lower.includes(tok))) {
89
+ return 'region-blocked';
90
+ }
91
+ return 'exhausted';
92
+ }
93
+ // 401 → auth-error
94
+ if (status === 401) {
95
+ return 'auth-error';
96
+ }
97
+ // 404 → model-not-found
98
+ if (status === 404) {
99
+ return 'model-not-found';
100
+ }
101
+ // 其余一律 → network-error(detail 记原始 status)
102
+ return 'network-error';
103
+ }
104
+
105
+ // ── 网络探测函数 ───────────────────────────────────────────────────────
106
+
107
+ /**
108
+ * 对单个 provider 执行状态探测。
109
+ * 绝不抛异常:所有错误/超时/解析失败 → network-error 结果。
110
+ */
111
+ export async function probeProviderStatus(
112
+ provider: string,
113
+ opts: StatusProbeOptions,
114
+ ): Promise<StatusProbeResult> {
115
+ const fetchFn = opts.fetchImpl ?? fetch;
116
+ const url = `${opts.baseUrl.replace(/\/+$/, '')}/chat/completions`;
117
+
118
+ try {
119
+ const response = await fetchFn(url, {
120
+ method: 'POST',
121
+ headers: {
122
+ 'Content-Type': 'application/json',
123
+ 'Authorization': `Bearer ${opts.apiKey}`,
124
+ },
125
+ body: JSON.stringify({
126
+ model: opts.model,
127
+ messages: [{ role: 'user', content: 'hi' }],
128
+ max_tokens: 1,
129
+ }),
130
+ signal: AbortSignal.timeout(opts.timeoutMs),
131
+ });
132
+
133
+ let bodyText = '';
134
+ try {
135
+ bodyText = await response.text();
136
+ } catch {
137
+ // 响应体读取失败 → 按空文本处理
138
+ bodyText = '';
139
+ }
140
+
141
+ const classification = classifyStatusResponse(response.status, bodyText);
142
+ return {
143
+ provider,
144
+ class: classification,
145
+ detail: classification === 'network-error' ? `status ${response.status}` : undefined,
146
+ probedAt: Date.now(),
147
+ };
148
+ } catch (e: unknown) {
149
+ // 网络/超时/AbortError → network-error,绝不抛
150
+ const err = e as Error & { name?: string };
151
+ const detail = err.name === 'AbortError' || err.name === 'TimeoutError'
152
+ ? 'timeout'
153
+ : (err.message ?? String(e));
154
+ return {
155
+ provider,
156
+ class: 'network-error',
157
+ detail,
158
+ probedAt: Date.now(),
159
+ };
160
+ }
161
+ }
162
+
163
+ // ── Cooldown ledger ─────────────────────────────────────────────────────
164
+
165
+ const probeLedger = new Map<string, number>();
166
+
167
+ /** 检查是否已到下次探测时间。 */
168
+ export function isStatusProbeDue(provider: string, ttlMs: number): boolean {
169
+ const canonical = canonicalizeProvider(provider);
170
+ const lastProbedAt = probeLedger.get(canonical);
171
+ if (lastProbedAt === undefined) {
172
+ return true;
173
+ }
174
+ return Date.now() - lastProbedAt >= ttlMs;
175
+ }
176
+
177
+ /** 标记探测已开始(在任何 await 之前调用,关死竞态窗口)。 */
178
+ export function markStatusProbeStarted(provider: string): void {
179
+ const canonical = canonicalizeProvider(provider);
180
+ probeLedger.set(canonical, Date.now());
181
+ }
182
+
183
+ /** 测试 seam:清空 ledger。 */
184
+ export function _resetForTest(): void {
185
+ probeLedger.clear();
186
+ }
187
+
188
+ // ── 调度入口 ─────────────────────────────────────────────────────────
189
+
190
+ /**
191
+ * 调度状态探测。
192
+ *
193
+ * 探测是 fire-and-forget 异步操作,绝不阻塞调用路径。
194
+ * 返回写入计数(可能为任意非负整数),调用方可据此判断是否需要重渲染。
195
+ *
196
+ * 异常全部吞没:fetch 失败/超时/解析失败/状态机写入失败均不影响其他 provider。
197
+ */
198
+ export async function scheduleStatusProbes(opts: ScheduleOptions): Promise<number> {
199
+ const { placeholderIds, models, getApiKey, config, fetchImpl } = opts;
200
+
201
+ // 快速短路:未启用或无可用的 getApiKey → 零请求,不抛 TypeError
202
+ if (!config.enabled || getApiKey === undefined) {
203
+ return 0;
204
+ }
205
+
206
+ // 同步过滤候选:excluded + 无 baseUrl/无模型
207
+ const excludedSet = new Set(config.excluded.map(canonicalizeProvider));
208
+ const modelMap = new Map<string, { id: string; baseUrl: string }>();
209
+ for (const m of models) {
210
+ if (m.baseUrl && !modelMap.has(canonicalizeProvider(m.provider))) {
211
+ modelMap.set(canonicalizeProvider(m.provider), { id: m.id, baseUrl: m.baseUrl });
212
+ }
213
+ }
214
+
215
+ const candidates = placeholderIds.filter((id) => {
216
+ const canonical = canonicalizeProvider(id);
217
+ if (excludedSet.has(canonical)) {
218
+ return false;
219
+ }
220
+ const modelInfo = modelMap.get(canonical);
221
+ if (!modelInfo) {
222
+ return false;
223
+ }
224
+ // cooldown 检查
225
+ if (!isStatusProbeDue(id, config.ttlMs)) {
226
+ return false;
227
+ }
228
+ return true;
229
+ });
230
+
231
+ if (candidates.length === 0) {
232
+ return 0;
233
+ }
234
+
235
+ // 对每个候选:先 markStatusProbeStarted(占位),再 await getApiKey
236
+ // 关死 in-flight 竞态窗口
237
+ const probePromises = candidates.map(async (provider) => {
238
+ markStatusProbeStarted(provider);
239
+ const apiKey = await getApiKey(provider);
240
+ if (!apiKey) {
241
+ // 无 key → 跳过请求但保留 cooldown 占位(下个 TTL 再查)
242
+ return null;
243
+ }
244
+ const modelInfo = modelMap.get(canonicalizeProvider(provider));
245
+ if (!modelInfo) {
246
+ return null;
247
+ }
248
+ return probeProviderStatus(provider, {
249
+ baseUrl: modelInfo.baseUrl,
250
+ model: modelInfo.id,
251
+ apiKey,
252
+ timeoutMs: config.timeoutMs,
253
+ fetchImpl,
254
+ });
255
+ });
256
+
257
+ // 并发探测,单个失败不连坐
258
+ const results = await Promise.allSettled(probePromises);
259
+ let written = 0;
260
+
261
+ for (const result of results) {
262
+ if (result.status === 'fulfilled') {
263
+ const probeResult = result.value;
264
+ if (!probeResult) {
265
+ continue; // 跳过的 provider
266
+ }
267
+ const canonical = canonicalizeProvider(probeResult.provider);
268
+ // 映射到状态机:exhausted/region-blocked → markExhausted;ok → markAvailable
269
+ if (probeResult.class === 'exhausted' || probeResult.class === 'region-blocked') {
270
+ markExhausted(probeResult.provider);
271
+ written++;
272
+ console.info(`[omp-opsx-addon] status probe: ${canonical} -> ${probeResult.class}`);
273
+ } else if (probeResult.class === 'ok') {
274
+ markAvailable(probeResult.provider);
275
+ written++;
276
+ console.info(`[omp-opsx-addon] status probe: ${canonical} -> ok`);
277
+ } else {
278
+ // auth-error/model-not-found/network-error 不写状态
279
+ console.warn(
280
+ `[omp-opsx-addon] status probe: ${canonical} -> ${probeResult.class}${probeResult.detail ? ` (${probeResult.detail})` : ''}`,
281
+ );
282
+ }
283
+ }
284
+ // rejected promise 在理论上不应发生(probeProviderStatus 已吞没所有异常),防御性忽略
285
+ }
286
+
287
+ return written;
288
+ }
@@ -100,15 +100,15 @@ export function buildStaticPrompt(): string {
100
100
  | 审查代码实现 | task(agent:"reviewer", task:"审阅代码 + 执行全局验证(lint/单测/e2e)") |
101
101
 
102
102
  ## 2 个 Loop
103
- - **Loop 1 – Propose → Review**:planner 完成 → proposal-reviewer → P0/P1 发回 → 通过告知用户。Propose-review loop:最多 planner→reviewer 往复 2 轮。
104
- - **Loop 2 – Code → Review**:coder 完成(交付前自验证)→ code-reviewer 审阅 + 全局验证 → P0/P1 发回修复 → 仅 P2+ 视为通过。Code-review loop:最多 coder→reviewer 往复 3 轮(含首次实现),reviewer 兼执行全局验证。
103
+ - **Loop 1 – Propose → Review**:planner 完成 → proposal-reviewer → P0/P1 发回 → 通过告知用户。Propose-review loop:最多 planner→reviewer 往复 2 轮。委派 planner/proposal-reviewer 的 task 描述须含『先读 openspec/changes/<name>/scratchpad.md,禁止重复探索』。
104
+ - **Loop 2 – Code → Review**:coder 完成(交付前自验证)→ code-reviewer 审阅 + 全局验证 → P0/P1 发回修复 → 仅 P2+ 视为通过。Code-review loop:最多 coder→reviewer 往复 3 轮(含首次实现),reviewer 兼执行全局验证。委派 coder/code-reviewer 的 task 描述须含『先读 openspec/changes/<name>/scratchpad.md,禁止重复探索』。
105
105
  - 达上限时停止自动流转,向用户说明未解决的问题并请求决策。
106
106
 
107
107
  ## /goal 集成(可选)
108
108
  Planner 在 proposal 末尾输出 \`## Budget Estimate\`;主 agent 提取用于 \`/goal\`。
109
109
 
110
110
  ## 自动流程协作
111
- coder 输出 STATUS: blocked 且含 SESSION: → 阅读阻塞原因决策;收到 P0/P1 → 修复再审。
111
+ coder 输出 STATUS: blocked 且含 SESSION: → 阅读阻塞原因决策;收到 P0/P1 → 修复再审。P0/P1 发回修复时 task 描述同样须含『先读 openspec/changes/<name>/scratchpad.md,禁止重复探索』。
112
112
 
113
113
  ## 上下文卫生(主 session 预算)
114
114
  - 代码调研/搜代码/理解架构 → task(agent:"scout");主 session 不亲自 read 全文调研。
@@ -118,7 +118,7 @@ coder 输出 STATUS: blocked 且含 SESSION: → 阅读阻塞原因决策;收到
118
118
  - 主 session 上下文超过 ~50k tokens:必须先委托再继续,禁止继续亲自调研。`;
119
119
  }
120
120
 
121
- const STATIC_LINE_COUNT = buildStaticPrompt().split('\n').length;
121
+ export const STATIC_LINE_COUNT = buildStaticPrompt().split('\n').length;
122
122
 
123
123
  /** Dynamic layer: Dispatch config + change status. Placed after the static
124
124
  * layer so only this tail is re-prefilled when it changes between turns. */
@@ -66,6 +66,7 @@ export interface OpsxYamlShape {
66
66
  probe_http_enabled?: boolean;
67
67
  probe_ttl_ms?: number;
68
68
  excluded_providers?: string[];
69
+ status_probe_enabled?: boolean;
69
70
  redis_enabled?: boolean;
70
71
  redis_host?: string;
71
72
  redis_port?: number;
@@ -102,6 +103,8 @@ export interface ResolvedOpsxConfig {
102
103
  probe_ttl_ms: number;
103
104
  /** 静态排除的 provider 列表(零探测成本) */
104
105
  excluded_providers: string[];
106
+ /** 是否开启状态探测,默认 true */
107
+ status_probe_enabled: boolean;
105
108
  /** 是否启用本地 Redis 实时中转(默认 true;false 时纯文件路径)。 */
106
109
  redis_enabled: boolean;
107
110
  /** Redis host(默认 127.0.0.1)。 */
@@ -309,6 +312,7 @@ export function parseOpsxConfig(
309
312
  probe_http_enabled: parseProbeBool(obj.probe_http_enabled, 'probe_http_enabled', false, localWarn),
310
313
  probe_ttl_ms: parsePositiveInt(obj.probe_ttl_ms, 'probe_ttl_ms', 600_000, localWarn),
311
314
  excluded_providers: parseStringArray(obj.excluded_providers, 'excluded_providers', localWarn),
315
+ status_probe_enabled: parseProbeBool(obj.status_probe_enabled, 'status_probe_enabled', true, localWarn),
312
316
  redis_enabled: parseProbeBool(obj.redis_enabled, 'redis_enabled', true, localWarn),
313
317
  redis_host: redisHost,
314
318
  redis_port: parsePositiveInt(obj.redis_port, 'redis_port', 6379, localWarn),
@@ -359,6 +363,7 @@ function pickTopLevelKeys(raw: OpsxYamlShape): OpsxYamlShape {
359
363
  probe_http_enabled: raw.probe_http_enabled,
360
364
  probe_ttl_ms: raw.probe_ttl_ms,
361
365
  excluded_providers: raw.excluded_providers,
366
+ status_probe_enabled: raw.status_probe_enabled,
362
367
  redis_enabled: raw.redis_enabled,
363
368
  redis_host: raw.redis_host,
364
369
  redis_port: raw.redis_port,
@@ -449,6 +454,7 @@ export function readOpsxSettingsFromPaths(
449
454
  merged.probe_http_enabled = projectTop.probe_http_enabled ?? globalTop.probe_http_enabled;
450
455
  merged.probe_ttl_ms = projectTop.probe_ttl_ms ?? globalTop.probe_ttl_ms;
451
456
  merged.excluded_providers = projectTop.excluded_providers ?? globalTop.excluded_providers;
457
+ merged.status_probe_enabled = projectTop.status_probe_enabled ?? globalTop.status_probe_enabled;
452
458
  merged.redis_enabled = projectTop.redis_enabled ?? globalTop.redis_enabled;
453
459
  merged.redis_host = projectTop.redis_host ?? globalTop.redis_host;
454
460
  merged.redis_port = projectTop.redis_port ?? globalTop.redis_port;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genee/omp-opsx-addon",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Pi Extension: OpenSpec workflow orchestration - coder/reviewer/planner agents, session title & progress",
6
6
  "main": "./index.ts",